Changeset 638 for trunk/cgi-bin


Ignore:
Timestamp:
10/09/14 18:13:22 (10 years ago)
Author:
Kris Deugau
Message:

/trunk

Commit 10/mumble for work done intermittently over the past ~year.
See #5, comment 25:

  • Update block delete. Includes a little current work to fix outstanding edge case issues
Location:
trunk/cgi-bin
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/cgi-bin/IPDB.pm

    r637 r638  
    12471247# as well as the reverse entry
    12481248sub deleteBlock {
    1249   my ($dbh,undef,$rdepth,$vrf,$delfwd,$user) = @_;
    1250   my $cidr = new NetAddr::IP $_[1];
     1249  my ($dbh,$id,$basetype,$delfwd,$user) = @_;
     1250
     1251  # Collect info about the block we're going to delete
     1252  my $binfo = getBlockData($dbh, $id, $basetype);
     1253  my $cidr = new NetAddr::IP $binfo->{block};
    12511254
    12521255# For possible auto-VRF-ignoring (since public IPs shouldn't usually be present in more than one VRF)
     
    12671270  my $con_type;
    12681271
    1269   # Collect info about the block we're going to delete
    1270   my $binfo = getBlockData($dbh, $cidr, $rdepth, $vrf);
    12711272
    12721273  # temporarily forced null, until a sane UI for VRF tracking can be found.
    1273   $vrf = '';# if !$vrf; # as with SQL, the null value is not equal to ''.  *sigh*
     1274#  $vrf = '';# if !$vrf;        # as with SQL, the null value is not equal to ''.  *sigh*
    12741275
    12751276  # To contain the error message, if any.
     
    12811282  local $dbh->{RaiseError} = 1;
    12821283
    1283   # First case.  The "block" is a static IP
    1284   # Note that we still need some additional code in the odd case
    1285   # of a netblock-aligned contiguous group of static IPs
    12861284  if ($binfo->{type} =~ /^.i$/) {
     1285    # First case.  The "block" is a static IP
     1286    # Note that we still need some additional code in the odd case
     1287    # of a netblock-aligned contiguous group of static IPs
    12871288
    12881289    eval {
    12891290      $msg = "Unable to deallocate $disp_alloctypes{$binfo->{type}} $cidr";
    1290       my ($pool,$pcust,$pvrf) = $dbh->selectrow_array("SELECT pool,custid,vrf FROM poolips WHERE ip=?", undef, ($cidr) );
     1291      my $pinfo = getBlockData($dbh, $binfo->{parent_id}, 'b');
    12911292##fixme: VRF and rdepth
    1292       $dbh->do("UPDATE poolips SET custid=?,available='y',".
    1293         "city=(SELECT city FROM allocations WHERE cidr=?),".
    1294         "description='',notes='',circuitid='',vrf=? WHERE ip=?", undef, ($pcust, $pool, $pvrf, $cidr) );
    1295       $goback = $pool;
     1293      $dbh->do("UPDATE poolips SET custid = ?, available = 'y',".
     1294        "city = (SELECT city FROM allocations WHERE id = ?),".
     1295        "description = '', notes = '', circuitid = '', vrf = ? WHERE id = ?", undef,
     1296        ($pinfo->{custid}, $binfo->{parent_id}, $pinfo->{vrf}, $id) );
    12961297      $dbh->commit;
    12971298    };
     
    13021303    } else {
    13031304##fixme:  RPC return code?
    1304       _rpc('delByCIDR', cidr => "$cidr", user => $user, delforward => $delfwd);
     1305      _rpc('delByCIDR', cidr => "$cidr", user => $user, delforward => $delfwd, rpcuser => $user);
    13051306      return ('OK',"OK");
    13061307    }
    13071308
    13081309  } elsif ($binfo->{type} eq 'mm') { # end alloctype =~ /.i/
     1310    # Second case.  The block is a full master block
    13091311
    13101312##fixme: VRF limit
    13111313    $msg = "Unable to delete master block $cidr";
    13121314    eval {
    1313       $dbh->do("DELETE FROM masterblocks WHERE cidr = ?", undef, ($cidr) );
    1314       $dbh->do("DELETE FROM allocations WHERE cidr <<= ?", undef, ($cidr) );
    1315       $dbh->do("DELETE FROM freeblocks WHERE cidr <<= ?", undef, ($cidr) );
     1315      $dbh->do("DELETE FROM allocations WHERE cidr <<= ? AND master_id = ?", undef, ($cidr, $binfo->{master_id}) );
     1316      $dbh->do("DELETE FROM freeblocks WHERE cidr <<= ? AND master_id = ?", undef, ($cidr, $binfo->{master_id}) );
    13161317      $dbh->commit;
    13171318    };
     
    13351336    my @fails;
    13361337    foreach my $subzone (@zonelist) {
    1337       if ($rpc_url && !_rpc('delZone', zone => "$subzone", revrec => 'y', user => $user, delforward => $delfwd) ) {
     1338      if ($rpc_url && !_rpc('delZone', zone => "$subzone", revrec => 'y', rpcuser => $user, delforward => $delfwd) ) {
    13381339        push @fails, ("$subzone" => $errstr);
    13391340      }
     
    13521353
    13531354    my $retcode = 'OK';
    1354     my ($ptype,$pcity,$ppatt);
     1355    my ($ptype,$pcity,$ppatt,$p_id);
    13551356
    13561357    eval {
     
    13591360# explicitly deleting any suballocations of the block to be deleted.
    13601361
    1361       # find the current parent of the block we're deleting
    1362       my ($parent) = $dbh->selectrow_array("SELECT parent FROM allocations WHERE cidr=? AND rdepth=?",
    1363         undef, ($cidr, $rdepth) );
     1362      # get parent info of the block we're deleting
     1363      my $pinfo = getBlockData($dbh, $binfo->{parent_id});
     1364      $ptype = $pinfo->{type};
     1365      $pcity = $pinfo->{city};
     1366      $ppatt = $pinfo->{rdns};
     1367      $p_id = $binfo->{parent_id};
    13641368
    13651369      # Delete the block
    1366       $dbh->do("DELETE FROM allocations WHERE cidr=? AND rdepth=?", undef, ($cidr, $rdepth) );
    1367 
    1368 ##fixme:  we could maybe eliminate a special case if we put masterblocks in the allocations table...?
    1369       if ($rdepth == 1) {
    1370         # parent is a master block.
    1371         $ptype = 'mm';
    1372         $pcity = '<NULL>';
    1373         $ppatt = $dbh->selectrow_array("SELECT rdns FROM masterblocks WHERE cidr=?", undef, ($parent) );
    1374       } else {
    1375         # get that parent's details
    1376         ($ptype,$pcity,$ppatt) = $dbh->selectrow_array("SELECT type,city,rdns FROM allocations ".
    1377                 "WHERE cidr=? AND rdepth=?", undef, ($parent, $rdepth-1) );
    1378       }
     1370      $dbh->do("DELETE FROM allocations WHERE id = ?", undef, ($id) );
    13791371
    13801372      # munge the parent type a little
    1381       $ptype = (split //, $ptype)[0];
     1373      $ptype = (split //, $ptype)[1];
    13821374
    13831375##fixme:  you can't...  CAN NOT....  assign the same public IP to multiple things.
     
    13851377# -> $isprivnet flag from start of sub
    13861378
    1387       my $fbrdepth = $rdepth;
    1388 
    13891379      # check to see if any container allocations could be the "true" parent
    1390       my ($tparent,$trdepth,$trtype,$tcity) = $dbh->selectrow_array("SELECT cidr,rdepth,type,city FROM allocations ".
    1391         "WHERE (type='rm' OR type LIKE '_c') AND cidr >> ? ".
    1392         "ORDER BY masklen(cidr) DESC", undef, ($cidr) );
    1393 
    1394       my $fparent;
    1395       if ($tparent && $tparent ne $parent) {
    1396         # found an alternate parent;  reset some parent-info bits
    1397         $parent = $tparent;
    1398         $ptype = (split //, $trtype)[0];
    1399         $pcity = $tcity;
    1400         ##fixme: hmm.  collect $rdepth into $goback here before vanishing?
    1401         $retcode = 'WARNMERGE'; # may be redundant
    1402         $goback = $tparent;
    1403         # munge freeblock rdepth and parent to match true parent
    1404         $dbh->do("UPDATE freeblocks SET rdepth = ?, parent = ?, routed = ? WHERE cidr <<= ? AND rdepth = ?", undef,
    1405                 ($trdepth+1, $parent, $ptype, $cidr, $rdepth) );
    1406         $rdepth = $trdepth;
    1407         $fbrdepth = $trdepth+1;
    1408       }
    1409 
    1410       $parent = new NetAddr::IP $parent;
    1411       $goback = "$parent,$fbrdepth";    # breadcrumb in case of live-parent-is-not-true-parent
    1412 
    1413       # Special case - delete pool IPs
    1414       if ($binfo->{type} =~ /^.[pd]$/) {
    1415         # We have to delete the IPs from the pool listing.
    1416 ##fixme:  rdepth?  vrf?
    1417         $dbh->do("DELETE FROM poolips WHERE pool = ?", undef, ($cidr) );
    1418       }
    1419 
    1420       # Find out if the block we're deallocating is within a DSL pool (legacy goo)
    1421       my ($pool,$poolcity,$pooltype,$pooldepth) = $dbh->selectrow_array(
    1422         "SELECT cidr,city,type,rdepth FROM allocations WHERE type LIKE '_p' AND cidr >>= ?",
    1423         undef, ($cidr) );
    1424 
    1425       # If so, return the block's IPs to the pool, instead of to freeblocks
    1426 ## NB: not possible to currently cause this even via admin tools, only legacy data.
    1427       if ($pool) {
    1428         ## Deallocate legacy blocks stashed in the middle of a static IP pool
    1429         ## This may be expandable to an even more general case of contained netblock, or other pool types.
    1430         $retcode = 'WARNPOOL';
    1431         $goback = "$pool,$pooldepth";
    1432         # We've already deleted the block, now we have to stuff its IPs into the pool.
    1433         $pooltype =~ s/p$/i/;   # change type to static IP
    1434         my $sth2 = $dbh->prepare("INSERT INTO poolips (pool,ip,city,type,custid) VALUES ".
    1435                 "('$pool',?,'$poolcity','$pooltype','$defcustid')");
    1436         # don't insert .0
     1380      my ($tparent,$tpar_id,$trtype,$tcity);
     1381      $tpar_id = 0;
     1382
     1383##fixme:  this is far simpler in the strict VRF case;  we "know" that any allocation
     1384# contained by a container is a part of the same allocation tree when the VRF fields are equal.
     1385
     1386# logic:
     1387# For each possible container of $cidr
     1388#  note the parent id
     1389#  walk the chain up the parents
     1390#    if we intersect $cidr's current parent, break
     1391#  if we've intersected $cidr's current parent
     1392#    set some variables to track that block
     1393#    break
     1394
     1395# Set up part of "is it in the middle of a pool?" check
     1396      my $wuzpool = $dbh->selectrow_hashref("SELECT cidr,parent_id,type,city,custid,id FROM allocations ".
     1397        "WHERE (type LIKE '_d' OR type LIKE '_p') AND cidr >> ? AND master_id = ?", { Slice => {} },
     1398        ($cidr, $binfo->{master_id}) );
     1399
     1400##fixme?
     1401# edge cases not handled, or handled badly:
     1402# -> $cidr managed to get to be the entirety of an IP pool
     1403
     1404      if ($wuzpool && $wuzpool->{id} != $id) {
     1405        # we have legacy goo to be purified
     1406        # going to ignore nested pools;  not possible to create them via API and no current legacy data includes any.
     1407
     1408        # for convenience
     1409        my $poolid = $wuzpool->{id};
     1410        my $pool = $wuzpool->{cidr};
     1411        my $poolcity = $wuzpool->{city};
     1412        my $pooltype = $wuzpool->{type};
     1413        my $poolcustid = $wuzpool->{custid};
     1414
     1415        $retcode = 'WARNPOOL';
     1416        $goback = "$poolid,$pool";
     1417        # We've already deleted the block, now we have to stuff its IPs into the pool.
     1418        $pooltype =~ s/[dp]$/i/;        # change type to static IP
     1419        my $sth2 = $dbh->prepare("INSERT INTO poolips (ip,city,type,custid,parent_id) VALUES ".
     1420          "(?,'$poolcity','$pooltype','$poolcustid',$poolid)");
    14371421##fixme:  need to not insert net, gateway, and bcast on "real netblock" pools (DHCPish)
    1438         $sth2->execute($cidr->addr) unless $cidr->addr =~ m|\.0$|;
    1439         foreach my $ip ($cidr->hostenum) {
    1440           $sth2->execute($ip);
    1441         }
    1442         $cidr--;
    1443         # don't insert .255
    1444         $sth2->execute($cidr->addr) unless $cidr->addr =~ m|\.255$|;
    1445       } else {  # done returning IPs from a block to a static DSL pool
     1422        # don't insert .0
     1423        $sth2->execute($cidr->addr) unless $cidr->addr =~ m|\.0$|;
     1424        foreach my $ip ($cidr->hostenum) {
     1425          $sth2->execute($ip);
     1426        }
     1427        $cidr--;
     1428        # don't insert .255
     1429        $sth2->execute($cidr->addr) unless $cidr->addr =~ m|\.255$|;
     1430      }
     1431
     1432## important!
     1433# ... or IS IT?
     1434# we may have undef'ed $wuzpool above, if the allocation tree $cidr is in doesn't intersect the pool we found
     1435#if (!$wuzpool) {
     1436
     1437      else {
     1438
     1439        # Get all possible (and probably a number of impossible) containers for $cidr
     1440        $sth = $dbh->prepare("SELECT cidr,parent_id,type,city,id FROM allocations ".
     1441          "WHERE (type LIKE '_m' OR type LIKE '_c') AND cidr >>= ? AND master_id = ? ".
     1442          "ORDER BY masklen(cidr) DESC");
     1443        $sth->execute($cidr, $binfo->{master_id});
     1444
     1445        # Quickly get certain fields (simpler than getBlockData()
     1446        my $sth2 = $dbh->prepare("SELECT cidr,parent_id,type,city FROM allocations ".
     1447          "WHERE (type LIKE '_m' OR type LIKE '_c') AND id = ? AND master_id = ?");
     1448
     1449        # For each possible container of $cidr...
     1450        while (my @data = $sth->fetchrow_array) {
     1451          my $i = 0;
     1452          # Save some state and set a start point - parent ID of container we're checking
     1453          $tparent = $data[0];
     1454          my $ppid = $data[1];
     1455          $trtype = $data[2];
     1456          $tcity = $data[3];
     1457          $tpar_id = $data[4];
     1458          last if $data[4] == $binfo->{parent_id};  # Preemptively break if we're already in the right place
     1459          last if $ppid == $binfo->{parent_id};     # ... or if the parent of the container is the block's parent
     1460          while (1) {
     1461            # Retrieve bits on that parent ID
     1462            $sth2->execute($ppid, $binfo->{master_id});
     1463            my @container = $sth2->fetchrow_array;
     1464            $ppid = $container[1];
     1465            last if $container[1] == 0;   # Break if we've hit a master block
     1466            last if $ppid == $binfo->{parent_id};   # Break if we've reached the block $cidr is currently in
     1467          }
     1468          last if $ppid == $binfo->{parent_id};
     1469        }
     1470
     1471        # found an alternate parent;  reset some parent-info bits
     1472        if ($tpar_id != $binfo->{parent_id}) {
     1473          $ptype = (split //, $trtype)[1];
     1474          $pcity = $tcity;
     1475          $retcode = 'WARNMERGE';       # may be redundant
     1476          $p_id = $tpar_id;
     1477        }
     1478
     1479        $goback = "$p_id,$tparent";     # breadcrumb, currently only used in case of live-parent-is-not-true-parent
     1480
     1481        # Special case - delete pool IPs
     1482        if ($binfo->{type} =~ /^.[pd]$/) {
     1483          # We have to delete the IPs from the pool listing.
     1484          $dbh->do("DELETE FROM poolips WHERE parent_id = ?", undef, ($id) );
     1485        }
     1486
     1487        $pinfo = getBlockData($dbh, $p_id);
    14461488
    14471489        # If the block wasn't legacy goo embedded in a static pool, we check the
     
    14521494          # move the freeblocks into the parent
    14531495          # we don't insert a new freeblock because there could be a live reparented sub.
    1454           $dbh->do("UPDATE freeblocks SET rdepth=rdepth-1,parent=?,routed=?,city=? ".
    1455                 "WHERE parent=? AND rdepth=?", undef,
    1456                 ($parent, $ptype, $pcity, $cidr, $rdepth+1) );
     1496          $dbh->do("UPDATE freeblocks SET parent_id = ?, routed = ?, city = ? WHERE parent_id = ?", undef,
     1497                ($p_id, $ptype, $pcity, $id) );
    14571498        } else {
    14581499          # ... otherwise, add the freeblock
    1459           $dbh->do("INSERT INTO freeblocks (cidr, city, routed, parent, rdepth) VALUES (?,?,?,?,?)", undef,
    1460                 ($cidr, $pcity, $ptype, $parent, $rdepth) );
     1500          $dbh->do("INSERT INTO freeblocks (cidr, city, routed, parent_id, master_id) VALUES (?,?,?,?,?)", undef,
     1501                ($cidr, $pcity, $ptype, $p_id, $binfo->{master_id}) );
    14611502        }
    14621503
    14631504##fixme: vrf
     1505##fixme:  simplify since all containers now represent different "layers"/"levels"?
    14641506        # set up the query to get the list of blocks to try to merge.
    1465         $sth = $dbh->prepare("SELECT cidr FROM freeblocks ".
    1466                 "WHERE parent = ? AND routed = ? AND rdepth = ? ".
     1507        $sth = $dbh->prepare("SELECT cidr,id FROM freeblocks ".
     1508                "WHERE parent_id = ? ".
    14671509                "ORDER BY masklen(cidr) DESC");
    14681510
    1469         $sth->execute($parent, $ptype, $fbrdepth);
     1511        $sth->execute($p_id);
    14701512
    14711513# NetAddr::IP->compact() attempts to produce the smallest inclusive block
     
    14771519#       $cidr=.32/27, $ip1=.96/27, $ip2=.0/27, and $ip3=.64/27.
    14781520
    1479         my (@rawfb, @combinelist);
     1521        my (@rawfb, @combinelist, %rawid);
    14801522        my $i=0;
    14811523        # for each free block under $parent, push a NetAddr::IP object into one list, and
     
    14841526          my $testIP = new NetAddr::IP $data[0];
    14851527          push @rawfb, $testIP;
     1528          $rawid{$data[0]} = $data[1];
    14861529          @combinelist = $testIP->compact(@combinelist);
    14871530        }
     
    14891532        # now that we have the full list of "compacted" freeblocks, go back over
    14901533        # the list of raw freeblocks, and delete the ones that got merged.
    1491         $sth = $dbh->prepare("DELETE FROM freeblocks WHERE cidr=? AND parent=? AND rdepth=?");
     1534        $sth = $dbh->prepare("DELETE FROM freeblocks WHERE id = ?");
    14921535        foreach my $rawfree (@rawfb) {
    14931536          next if grep { $rawfree == $_ } @combinelist; # skip if the raw block is in the compacted list
    1494           $sth->execute($rawfree, $parent, $fbrdepth);
     1537          $sth->execute($rawid{$rawfree});
    14951538        }
    14961539
    14971540        # now we walk the new list of compacted blocks, and see which ones we need to insert
    1498         $sth = $dbh->prepare("INSERT INTO freeblocks (cidr,city,routed,parent,rdepth) VALUES (?,?,?,?,?)");
     1541        $sth = $dbh->prepare("INSERT INTO freeblocks (cidr,city,routed,parent_id,master_id) VALUES (?,?,?,?,?)");
    14991542        foreach my $cme (@combinelist) {
    15001543          next if grep { $cme == $_ } @rawfb;   # skip if the combined block was in the raw list
    1501           $sth->execute($cme, $pcity, $ptype, $parent, $fbrdepth);
     1544          $sth->execute($cme, $pcity, $ptype, $p_id, $binfo->{master_id});
    15021545        }
    15031546
     
    15131556    } else {
    15141557##fixme:  RPC return code?
    1515       _rpc('delByCIDR', cidr => "$cidr", user => $user, delforward => $delfwd, delsubs => 'y', parpatt => $ppatt);
     1558      _rpc('delByCIDR', cidr => "$cidr", rpcuser => $user, delforward => $delfwd, delsubs => 'y', parpatt => $ppatt);
    15161559      return ($retcode, $goback);
    15171560    }
  • trunk/cgi-bin/main.cgi

    r634 r638  
    821821
    822822  my $blockdata;
    823 
    824   if ($webvar{rdepth} == 0) {   # $webvar{alloctype} eq 'mm'
    825 
    826     $blockdata->{block} = $webvar{block};
     823  $blockdata = getBlockData($ip_dbh, $webvar{block}, $webvar{basetype});
     824
     825  if ($blockdata->{parent_id} == 0) {   # $webvar{alloctype} eq 'mm'
    827826    $blockdata->{city} = "N/A";
    828827    $blockdata->{custid} = "N/A";
    829     $blockdata->{type} = 'mm';
    830828    $blockdata->{circuitid} = "N/A";
    831829    $blockdata->{description} = "N/A";
    832830    $blockdata->{notes} = "N/A";
    833831    $blockdata->{privdata} = "N/A";
    834     $blockdata->{rdepth} = 0;
    835 
    836   } else {
    837 
    838     $blockdata = getBlockData($ip_dbh, $webvar{block}, $webvar{rdepth});
    839 
    840832  } # end cases for different alloctypes
    841833
     834  $page->param(blockid => $webvar{block});
     835  $page->param(basetype => $webvar{basetype});
     836
    842837  $page->param(block => $blockdata->{block});
    843 
    844838  $page->param(rdns => $blockdata->{rdns});
    845839
     
    853847  }
    854848
    855   $page->param(rdepth => $blockdata->{rdepth});
    856849  $page->param(disptype => $disp_alloctypes{$blockdata->{type}});
    857 #  $page->param(type => $blockdata->{type});
    858850  $page->param(city => $blockdata->{city});
    859851  $page->param(custid => $blockdata->{custid});
     
    883875
    884876  # need to retrieve block data before deleting so we can notify on that
    885   my $blockinfo = getBlockData($ip_dbh, $webvar{block}, $webvar{rdepth});
    886 
    887   my ($code,$msg) = deleteBlock($ip_dbh, $webvar{block}, $webvar{rdepth}, $webvar{vrf}, $webvar{delforward}, $authuser);
    888 
    889   $page->param(block => $webvar{block});
    890   $page->param(delparent => $blockinfo->{parent}) if $webvar{rdepth};
    891   $page->param(prdepth => $webvar{rdepth});
     877  my $blockinfo = getBlockData($ip_dbh, $webvar{block}, $webvar{basetype});
     878  my $pinfo = getBlockData($ip_dbh, $blockinfo->{parent_id}, 'b');
     879
     880  my ($code,$msg) = deleteBlock($ip_dbh, $webvar{block}, $webvar{basetype}, $webvar{delforward}, $authuser);
     881
     882  $page->param(block => $blockinfo->{block});
     883  $page->param(delparent_id => $blockinfo->{parent_id});# if $webvar{rdepth};
     884  $page->param(delparent => $pinfo->{block});
     885  $page->param(returnpool => ($webvar{basetype} eq 'i') );
    892886  if ($code =~ /^WARN(POOL|MERGE)/) {
    893     my ($bp,$bd) = split /,/, $msg;
    894     $page->param(bparent => $bp);
    895     $page->param(brdepth => $bd);
     887    my ($pid,$pcidr) = split /,/, $msg;
     888    $page->param(parent_id => $pid);
     889    $page->param(parent => $pcidr);
    896890    $page->param(mergeip => $code eq 'WARNPOOL');
    897891  }
     
    901895  }
    902896  if ($code eq 'OK' || $code =~ /^WARN/) {
    903     syslog "notice", "$authuser deallocated '$webvar{alloctype}'-type netblock $webvar{block} ".
     897    syslog "notice", "$authuser deallocated '".$blockinfo->{type}."'-type netblock $webvar{block} ".
    904898        $blockinfo->{custid}.", ".$blockinfo->{city}.", desc='".$blockinfo->{description}."'";
    905     mailNotify($ip_dbh, 'da', "REMOVED: $disp_alloctypes{$webvar{alloctype}} $webvar{block}",
    906         "$disp_alloctypes{$webvar{alloctype}} $webvar{block} deallocated by $authuser\n".
     899    mailNotify($ip_dbh, 'da', "REMOVED: ".$disp_alloctypes{$blockinfo->{type}}." $webvar{block}",
     900        $disp_alloctypes{$blockinfo->{type}}." $webvar{block} deallocated by $authuser\n".
    907901        "CustID: ".$blockinfo->{custid}."\nCity: ".$blockinfo->{city}.
    908902        "\nDescription: ".$blockinfo->{description}."\n");
Note: See TracChangeset for help on using the changeset viewer.