Index: tags/2.5/addmaster.html
===================================================================
--- tags/2.5/addmaster.html	(revision 381)
+++ tags/2.5/addmaster.html	(revision 381)
@@ -0,0 +1,11 @@
+<div class="indent">
+<div class="heading">Add new master IP block</div><br>
+<table class="regular" bgcolor="black" cellspacing="1" cellpadding="1">
+<form method="POST" action="/ip/cgi-bin/main.cgi" class="regular">
+<tr class="color1"><td>Master block to add: (CIDR)&nbsp</td><td><input type=text name=cidr></td></tr> <tr class="color2">
+<td class="center" colspan="2"><input type="submit" value="  Assign  "></td>
+<input type="hidden" name="action" value="newmaster">
+</form>
+</tr>
+</table>
+</div>
Index: tags/2.5/alloctypes.html
===================================================================
--- tags/2.5/alloctypes.html	(revision 381)
+++ tags/2.5/alloctypes.html	(revision 381)
@@ -0,0 +1,109 @@
+<html><head>
+
+<title>IP Database</title><meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+
+<link rel="stylesheet" type="text/css" href="/ip/ipdb.css">
+
+</head>
+<body bgcolor="#ffffff" text="#000000" link="#000000" vlink="#000000" alink="#ff0000" leftmargin="0" topmargin="0" marginwidth="0">
+
+<table class="regular">
+
+<tr><td colspan=2 class="heading">Allocation Types:</td><tr>
+
+<tr class="color1">
+<td>Customer netblock (default)</td><td>A direct allocation /30 or larger to a
+customer.  Note that this does NOT include RADIUS-routed "netblocks".  Internal systems
+should use the "End-use netblock" type.</td>
+</tr>
+
+<tr class="color2">
+<td>Static IP - Server pool</td><td>A single IP from a pool designated for
+servers</td>
+</tr><tr class="color1">
+<td>Static IP - Cable</td><td>A single IP address from a designated pool
+on the cable network.</td>
+</tr><tr class="color2">
+<td>Static IP - DSL</td><td>A single IP address from a designated pool
+on the DSL network.  Note that these are nominally in Sudbury or North Bay,
+even if the customer isn't.</td>
+</tr><tr class="color1">
+<td>Static IP - Dialup</td><td>A single IP address from a designated pool
+on a dialup RAS.</td>
+</tr><tr class="color2">
+<td>Static IP - Wireless</td><td>A single IP address from a designated pool
+on one of our wireless networks.</td>
+</tr>
+
+<tr class="color1">
+<td>Static pool - Servers</td><td>A pool of IP addresses available for one-by-one
+assignment to servers in a POP.  This will usually only be in Sudbury.</td>
+</tr><tr class="color2">
+<td>Static pool - Cable</td><td>A pool of IP addresses available for one-by-one assignment
+to customers on cable.</td>
+</tr><tr class="color1">
+<td>Static pool - DSL</td><td>A pool of IP addresses available for one-by-one assignment to
+customers on DSL.  Note that individual IPs will be "assigned" to the customer's
+city, but the pool they are a part of will normally be "assigned" to Sudbury or
+North Bay.</td>
+</tr><tr class="color2">
+<td>Static pool - Dialup</td><td>A pool of IP addresses available for one-by-one
+assignment to dialup customers.</td>
+</tr><tr class="color1">
+<td>Static pool - Wireless</td><td>A pool of IP addresses available for one-by-one
+assignment to customers on one of our wireless networks.</td>
+</tr>
+
+<tr class="color2">
+<td>End-use netblock</td><td>A /30 or larger allocation for arbitrary services (LAN
+management, servers we want fully subnet-isolated, etc)</td>
+
+</tr><tr class="color1">
+<td>Dialup netblock</td><td>Netblock assigned to one or more RAS units in a POP</td>
+</tr><tr class="color2">
+<td>Dynamic DSL block</td><td>Netblock for (mostly residential) PPPoE DSL.  Nominally
+"assigned" to Sudbury</td>
+</tr><tr class="color1">
+<td>Dynamic cable block</td><td>Netblock for (mostly residential) DHCP cable.</td>
+</tr><tr class="color2">
+<td>Dynamic WiFi block</td><td>Netblock for (mostly residential) (mostly) PPPoE wireless.</td>
+</tr><tr class="color1">
+<td>Dynamic VoIP block</td><td>Netblock for DHCP-assigned VoIP services.</td>
+
+</tr><tr class="color2">
+<td>Static IP - LAN/POP</td><td>A single IP address from a designated pool for our
+internal LANs - either at a single POP or for a VPN.</td>
+</tr><tr class="color1">
+<td>Static pool - LAN/POP</td><td>A pool of IP addresses available for one-by-one
+assignment to our internal LAN systems or VPNs.</td>
+
+</tr><tr class="color2">
+<td>Reserve for WAN blocks</td><td>Reserve a chunk of IP space for core routers/etc.</td>
+</tr><tr class="color1">
+<td>Reserve for dynamic-route DSL netblocks</td><td>Reserve a chunk of IP space for
+RADIUS-assigned "netblocks" from a PPPoE pool.  Note that this is similar to the static DSL
+pool, but IP space is to be allocated as /30 and larger netblocks, not single static IPs.</td>
+</tr><tr class="color2">
+<td>Reserve for ATM</td><td>Reserve a chunk of IP space for allocation to customers on ATM</td>
+</tr><tr class="color1">
+<td>WAN block</td><td>Individual netblock assignment for a core router.  Always taken from a
+block which has previously been reserved for such assignments.</td>
+</tr><tr class="color2">
+<td>Dynamic-route DSL netblock</td><td>Customer assignment for a RADIUS-assigned PPPoE
+"netblock".  Always taken from a block which has previously been reserved for such
+assignments.</td>
+</tr><tr class="color1">
+<td>ATM block</td><td>Customer assignment for a customer on ATM.  Always taken from a
+block which has previously been reserved for such assignments.</td>
+
+</tr><tr class="color2">
+<td>Routing</td><td>Blocks not actually assigned to a service on their own, but which
+have been routed to individual POPs- typically /24, /23, /22</td>
+
+</tr><tr class="color1">
+<td>Master block</td><td>Our allocations from ARIN</td>
+</tr>
+
+</table>
+
+</body></html>
Index: tags/2.5/assign.html
===================================================================
--- tags/2.5/assign.html	(revision 381)
+++ tags/2.5/assign.html	(revision 381)
@@ -0,0 +1,47 @@
+<div class="indent">
+<div class="heading">Assign IPs</div><br>
+<table class="regular" bgcolor="black" cellspacing="1" cellpadding="1">
+<form method="POST" action="main.cgi" class="regular">
+<tr class="color1">
+<td>Customer location:&nbsp;</td><td>
+<select name="city"><option selected>-</option>
+$$ALLCITIES$$
+</select>
+&nbsp;<a href="javascript:popNotes('/ip/newcity.html')">Add new location</a>
+</td>
+</tr>
+<tr class="color2">
+<td>Allocation type:</td><td>
+<select name="alloctype">
+$$TYPELIST$$
+</select>
+<input type="button" value=" ? " onclick="helpAllocTypes()" class="regular">
+</td>
+</tr><tr class="color1">
+<td>Subnet CIDR mask length:&nbsp;</td><td valign=top>&nbsp;/&nbsp;<input type="text" name="maskbits" size="3" maxlength="3"></td>
+</tr><tr class="color2">
+<td>Customer ID:&nbsp;</td><td><input type="text" name="custid" size="15" maxlength="15"> (Only required for Customer allocations)</td>
+</tr><tr class="color1">
+<td>Route from/through:&nbsp;</td><td>
+<select name="pop"><option selected>-</option>
+$$POPLIST$$
+</select>
+</td>
+</tr><tr class="color2">
+<td>Route/allocate from this master:&nbsp;</td><td>$$MASTERLIST$$
+Allow automatic allocation from private IP ranges:<input type=checkbox name=allowpriv>
+</td>
+</tr><tr class="color1">
+<td>Circuit ID:&nbsp;</td><td><input name=circid size=40></td>
+</tr><tr class="color2">
+<td>Description/Name:&nbsp;</td><td><input name="desc" size=40></td>
+</tr><tr class="color1">
+<td>Notes:&nbsp;</td><td><textarea name="notes" rows="3" cols="40"></textarea></td>
+</tr>
+$$PRIVDATA$$
+<tr class="$$BUTTONROWCOLOUR$$">
+<td class="center" colspan="2"><input type="submit" value="  Assign  "></td>
+<input type="hidden" name="action" value="confirm">
+</tr>
+</table>
+</div>
Index: tags/2.5/cgi-bin/CommonWeb.pm
===================================================================
--- tags/2.5/cgi-bin/CommonWeb.pm	(revision 381)
+++ tags/2.5/cgi-bin/CommonWeb.pm	(revision 381)
@@ -0,0 +1,146 @@
+# ipdb/cgi-bin/CommonWeb.pm
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+
+package CommonWeb;
+
+# 08/13/2004 kdeugau@vianet
+#	Split DB-related functions into new module;  they're not
+#	specific to CGI/web stuff.
+
+use strict;		
+use warnings;
+use Exporter;
+use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+$VERSION     = 1.00;
+@ISA         = qw(Exporter);
+@EXPORT_OK      = qw(&parse_post &printFooter &printHeader &printError &printAndExit &desanitize &cleanInput &desanitize);
+
+@EXPORT      = (); #export nothing by default
+%EXPORT_TAGS = ( ALL => [qw( &parse_post &printFooter &printHeader &printError
+				&printAndExit &desanitize &cleanInput )],
+                 lean    => [qw( &parse_post &printFooter &printHeader &printError
+				&printAndExit &cleanInput )]
+		);
+
+sub parse_post {
+  my $buffer;
+  if ($ENV{'REQUEST_METHOD'} eq "GET") {
+    $buffer=$ENV{'QUERY_STRING'}
+  } elsif ($ENV{'REQUEST_METHOD'} eq 'POST' && $ENV{'CONTENT_TYPE'} eq "application/x-www-form-urlencoded") {
+    read(STDIN, $buffer, $ENV{CONTENT_LENGTH});
+  } else {
+    $buffer = $ENV{'QUERY_STRING'};
+    $buffer || read(STDIN, $buffer, $ENV{CONTENT_LENGTH});
+  }
+  my @pairs = split(/&/, $buffer);
+  my %webvarLocal;
+  foreach my $pair (@pairs) {
+    my ($name, $value) = split(/=/, $pair);
+    $name  =~ tr/+/ /;
+    $name  =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
+    $value =~ tr/+/ /;
+    $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
+    $value =~ s/\'/\\\'/g;
+    $webvarLocal{$name} = $value;
+  }
+  return %webvarLocal;
+}
+
+
+sub printHeader {
+  my $title = shift;
+  print "Content-type: text/html\n\n";
+# This doesn't work well.  Must investigate.
+#  my $realm = shift;
+#  print qq(WWW-Authenticate: Basic realm="$realm"\n) if $realm;
+  open FILE, "../header.inc"
+	or carp $!;
+  my $html = join('',<FILE>);
+  close FILE;
+
+  $html =~ s/\$\$TITLE\$\$/$title/;
+# Necessary for mangling arbitrary bits of the header
+  my $i=0;
+  while (defined(my $param = shift)) {
+    $html =~ s/\$\$EXTRA$i\$\$/$param/g;
+    $i++;
+  }
+  print $html;
+}
+
+sub printFooter
+{
+  open FILE, "../footer.inc"
+	or croak $!;
+  while (<FILE>) 
+  {
+    print;
+  }
+  close FILE;
+}
+
+sub printError($)
+{
+	my $errStr = $_[0];
+	print qq(
+	<center><p class="regular"> $errStr </p>
+	<input type="button" value="Back" onclick="history.go(-1)">
+	</center>
+	);
+}
+
+sub printAndExit($)
+{
+	my $errStr = $_[0];
+	print qq(
+	<center><p class="regular"> $errStr </p>
+	<input type="button" value="Back" onclick="history.go(-1)">
+	</center>
+	);
+	printFooter();
+	exit(0);
+}
+
+sub loginAgain
+{
+	print qq(
+	<center><p>Your session has expired. Please login again.</p>
+	<p>
+	<a href="http://hosttest.vianet.ca"> Click here to login again.</a>
+	</p>
+	</center>
+	);
+	printFooter();
+	exit(0);
+}
+
+# needs a reference to the webvar hash.
+# takes out backticks and single quotes
+sub cleanInput($)
+{
+	my $hashRef = $_[0];
+
+	foreach my $key (keys %$hashRef) 
+	{
+		$hashRef->{$key} =~ s/`/\\`/g;
+		$hashRef->{$key} =~ s/'/\'/g;
+	}
+}
+
+# undoes clean input.  takes a string as an arg.
+sub desanitize($)
+{
+	my $string = $_[0];
+	$string =~ s/\\`/`/g;
+	$string =~ s/\\'/'/g;
+	return $string;
+}
+
+# indicate that the module loaded okay.
+1;
Index: tags/2.5/cgi-bin/CustIDCK.pm
===================================================================
--- tags/2.5/cgi-bin/CustIDCK.pm	(revision 381)
+++ tags/2.5/cgi-bin/CustIDCK.pm	(revision 381)
@@ -0,0 +1,87 @@
+# ipdb/cgi-bin/CustIDCK.pm
+# External Customer ID checker stub
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+
+package CustIDCK;
+
+use strict;
+use warnings;
+use Exporter;
+use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+use DBI;
+
+# Supposed to provide cross-Perl-version signal handling.
+# Not part of stock Debian Perl, use dh-make-perl or just
+#  install straight from CPAN.
+# Not part of stock RHEL/CentOS, use cpan2perl, cpanflute,
+#  or just install straight from CPAN.
+use Sys::SigAction;
+
+$VERSION        = 1.00;
+@ISA            = qw(Exporter);
+@EXPORT         = ();
+@EXPORT_OK      = qw ( &custid_exist );
+
+# this is really an example stub, and should be replaced by
+# the local admin on installation
+sub custid_exist {
+  my $custid = shift;
+
+  return 1 if $custid =~ /^STAFF$/;
+  return 1 if $custid =~ /^6750400$/;  # just in case some later change might block this
+  return 1 if $custid =~ /^\d{7}$/;
+  return 1 if $custid =~ /^\d{10}$/;
+
+# some example code for a database check
+  # Try to catch failures to connect.  If the remote server is up but
+  # not responding (this has HAPPENED) we need to break out rather than hanging.
+  my $dbh;
+  eval {
+    my $h = Sys::SigAction::set_sig_handler( 'ALRM',
+      sub { die "failed connection to apex!!"; } );
+
+    alarm 3;	# 3-second timeout.  This may be too aggressive.
+
+    eval {
+      $dbh = DBI->connect ("DBI:Pg:host=billing;dbname=custids", "cidcheck", "c1dch4ck");
+      die "failed connection to billing!!" if !$dbh;
+# Not certain if this is needed here.  It doesn't seem to be.
+#      $dbh->ping;	# Gotta do this to "force" a "failure".  NRGH.
+    };
+    alarm 0;	# cancel the alarm
+    $dbh->ping;	# Gotta do this to "force" a "failure".  NRGH.
+  };
+  alarm 0;	# avoid race conditions.  May not be needed here.  (Hah!)
+  if ($@ && $@ !~ /failed connection to billing!!/) {
+    $CustIDCK::Error = 1;
+    $CustIDCK::ErrMsg = "Failed connection to billing DB host!  Unable to verify CustIDs.";
+    return 0;
+  }
+
+  # We should have a valid DB connection by now.
+  my $sth = $dbh->prepare("SELECT custid FROM custid WHERE custid = '$custid'");
+  $sth->execute;
+  if ($dbh->err) {
+    $CustIDCK::Error = 1;
+    $CustIDCK::ErrMsg = $dbh->errstr();
+    $sth->finish;
+    $dbh->disconnect;
+    return 0;
+  }
+  my $hr = $sth->fetchrow_hashref();
+  my $status = 0;
+  $status = 1 if ( $hr->{custid} );
+  $sth->finish;
+  $dbh->disconnect;
+  return $status;
+
+  return 0;
+  # Stubs for error messages
+  $CustIDCK::Error = 1 if 1 == 0;
+  $CustIDCK::ErrMsg = "bad foo-frob: 1 == 0";
+}
Index: tags/2.5/cgi-bin/IPDB.pm
===================================================================
--- tags/2.5/cgi-bin/IPDB.pm	(revision 381)
+++ tags/2.5/cgi-bin/IPDB.pm	(revision 381)
@@ -0,0 +1,807 @@
+# ipdb/cgi-bin/IPDB.pm
+# Contains functions for IPDB - database access, subnet mangling, block allocation, etc
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+# Copyright (C) 2004-2006 - Kris Deugau
+
+package IPDB;
+
+use strict;
+use warnings;
+use Exporter;
+use DBI;
+use Net::SMTP;
+use NetAddr::IP qw( Compact );
+use POSIX;
+use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+
+$VERSION	= 2.0;
+@ISA		= qw(Exporter);
+@EXPORT_OK    = qw(
+	%disp_alloctypes %list_alloctypes %def_custids @citylist @poplist @masterblocks
+	%allocated %free %routed %bigfree %IPDBacl
+	&initIPDBGlobals &connectDB &finish &checkDBSanity &allocateBlock &addMaster
+	&deleteBlock &getBlockData &mailNotify
+	);
+
+@EXPORT		= (); # Export nothing by default.
+%EXPORT_TAGS	= ( ALL => [qw(
+		%disp_alloctypes %list_alloctypes %def_custids @citylist @poplist
+		@masterblocks %allocated %free %routed %bigfree %IPDBacl
+		&initIPDBGlobals &connectDB &finish &checkDBSanity &allocateBlock
+		&addMaster &deleteBlock &getBlockData &mailNotify
+		)]
+	);
+
+##
+## Global variables
+##
+our %disp_alloctypes;
+our %list_alloctypes;
+our %def_custids;
+our @citylist;
+our @poplist;
+our @masterblocks;
+our %allocated;
+our %free;
+our %routed;
+our %bigfree;
+our %IPDBacl;
+
+# Let's initialize the globals.
+## IPDB::initIPDBGlobals()
+# Initialize all globals.  Takes a database handle, returns a success or error code
+sub initIPDBGlobals {
+  my $dbh = $_[0];
+  my $sth;
+
+  # Initialize alloctypes hashes
+  $sth = $dbh->prepare("select type,listname,dispname,listorder,def_custid from alloctypes order by listorder");
+  $sth->execute;
+  while (my @data = $sth->fetchrow_array) {
+    $disp_alloctypes{$data[0]} = $data[2];
+    $def_custids{$data[0]} = $data[4];
+    if ($data[3] < 900) {
+      $list_alloctypes{$data[0]} = $data[1];
+    }
+  }
+
+  # City and POP listings
+  $sth = $dbh->prepare("select city,routing from cities order by city");
+  $sth->execute;
+  return (undef,$sth->errstr) if $sth->err;
+  while (my @data = $sth->fetchrow_array) {
+    push @citylist, $data[0];
+    if ($data[1] eq 'y') {
+      push @poplist, $data[0];
+    }
+  }
+
+  # Master block list
+  $sth = $dbh->prepare("select cidr from masterblocks order by cidr");
+  $sth->execute;
+  return (undef,$sth->errstr) if $sth->err;
+  for (my $i=0; my @data = $sth->fetchrow_array(); $i++) {
+    $masterblocks[$i] = new NetAddr::IP $data[0];
+    $allocated{"$masterblocks[$i]"} = 0;
+    $free{"$masterblocks[$i]"} = 0;
+    $bigfree{"$masterblocks[$i]"} = 128; # Larger number means smaller block.
+					# Set to 128 to prepare for IPv6
+    $routed{"$masterblocks[$i]"} = 0;
+  }
+
+  # Load ACL data.  Specific username checks are done at a different level.
+  $sth = $dbh->prepare("select username,acl from users");
+  $sth->execute;
+  return (undef,$sth->errstr) if $sth->err;
+  while (my @data = $sth->fetchrow_array) {
+    $IPDBacl{$data[0]} = $data[1];
+  }
+
+  return (1,"OK");
+} # end initIPDBGlobals
+
+
+## IPDB::connectDB()
+# Creates connection to IPDB.
+# Requires the database name, username, and password.
+# Returns a handle to the db.
+# Set up for a PostgreSQL db;  could be any transactional DBMS with the
+# right changes.
+# This definition should be sub connectDB($$$) to be technically correct,
+# but this breaks.  GRR.
+sub connectDB {
+  my ($dbname,$user,$pass) = @_;
+  my $dbh;
+  my $DSN = "DBI:Pg:dbname=$dbname";
+#  my $user = 'ipdb';
+#  my $pw   = 'ipdbpwd';
+
+# Note that we want to autocommit by default, and we will turn it off locally as necessary.
+# We may not want to print gobbledygook errors;  YMMV.  Have to ponder that further.
+  $dbh = DBI->connect($DSN, $user, $pass, {
+	AutoCommit => 1,
+	PrintError => 0
+	})
+    or return (undef, $DBI::errstr) if(!$dbh);
+
+# Return here if we can't select.  Note that this indicates a
+# problem executing the select.
+  my $sth = $dbh->prepare("select type from alloctypes");
+  $sth->execute();
+  return (undef,$DBI::errstr) if ($sth->err);
+
+# See if the select returned anything (or null data).  This should
+# succeed if the select executed, but...
+  $sth->fetchrow();
+  return (undef,$DBI::errstr)  if ($sth->err);
+
+# If we get here, we should be OK.
+  return ($dbh,"DB connection OK");
+} # end connectDB
+
+
+## IPDB::finish()
+# Cleans up after database handles and so on.
+# Requires a database handle
+sub finish {
+  my $dbh = $_[0];
+  $dbh->disconnect;
+} # end finish
+
+
+## IPDB::checkDBSanity()
+# Quick check to see if the db is responding.  A full integrity
+# check will have to be a separate tool to walk the IP allocation trees.
+sub checkDBSanity {
+  my ($dbh) = $_[0];
+
+  if (!$dbh) {
+    print "No database handle, or connection has been closed.";
+    return -1;
+  } else {
+    # it connects, try a stmt.
+    my $sth = $dbh->prepare("select type from alloctypes");
+    my $err = $sth->execute();
+
+    if ($sth->fetchrow()) {
+      # all is well.
+      return 1;
+    } else {
+      print "Connected to the database, but could not execute test statement.  ".$sth->errstr();
+      return -1;
+    }
+  }
+  # Clean up after ourselves.
+#  $dbh->disconnect;
+} # end checkDBSanity
+
+
+## IPDB::addMaster()
+# Does all the magic necessary to sucessfully add a master block
+# Requires database handle, block to add
+# Returns failure code and error message or success code and "message"
+sub addMaster {
+  my $dbh = shift;
+  my $cidr = new NetAddr::IP shift;
+
+  # Allow transactions, and raise an exception on errors so we can catch it later.
+  # Use local to make sure these get "reset" properly on exiting this block
+  local $dbh->{AutoCommit} = 0;
+  local $dbh->{RaiseError} = 1;
+
+  # Wrap all the SQL in a transaction
+  eval {
+    my $sth = $dbh->prepare("select count(*) from masterblocks where cidr <<= '$cidr'");
+    $sth->execute;
+    my @data = $sth->fetchrow_array;
+
+    if ($data[0] eq 0) {
+      # First case - master is brand-spanking-new.
+##fixme: rwhois should be globally-flagable somewhere, much like a number of other things
+## maybe a db table called "config"?
+      $sth = $dbh->prepare("insert into masterblocks (cidr,rwhois) values ('$cidr','y')");
+      $sth->execute;
+
+# Unrouted blocks aren't associated with a city (yet).  We don't rely on this
+# elsewhere though;  legacy data may have traps and pitfalls in it to break this.
+# Thus the "routed" flag.
+
+      $sth = $dbh->prepare("insert into freeblocks (cidr,maskbits,city,routed)".
+        " values ('$cidr',".$cidr->masklen.",'<NULL>','n')");
+      $sth->execute;
+
+      # If we get here, everything is happy.  Commit changes.
+      $dbh->commit;
+
+    } # new master does not contain existing master(s)
+    else {
+
+      # collect the master(s) we're going to absorb, and snag the longest netmask while we're at it.
+      my $smallmask = $cidr->masklen;
+      $sth = $dbh->prepare("select cidr as mask from masterblocks where cidr <<= '$cidr'");
+      $sth->execute;
+      my @cmasters;
+      while (my @data = $sth->fetchrow_array) {
+	my $master = new NetAddr::IP $data[0];
+	push @cmasters, $master;
+	$smallmask = $master->masklen if $master->masklen > $smallmask;
+      }
+
+      # split the new master, and keep only those blocks not part of an existing master
+      my @blocklist;
+      foreach my $seg ($cidr->split($smallmask)) {
+	my $contained = 0;
+	foreach my $master (@cmasters) {
+	  $contained = 1 if $master->contains($seg);
+	}
+	push @blocklist, $seg if !$contained;
+      }
+
+      # collect the unrouted free blocks within the new master
+      $sth = $dbh->prepare("select cidr from freeblocks where ".
+		"maskbits>=$smallmask and cidr <<= '$cidr' and routed='n'");
+      $sth->execute;
+      while (my @data = $sth->fetchrow_array) {
+	my $freeblock = new NetAddr::IP $data[0];
+	push @blocklist, $freeblock;
+      }
+
+      # combine the set of free blocks we should have now.
+      @blocklist = Compact(@blocklist);
+
+      # and now insert the new data.  Make sure to delete old masters too.
+
+      # freeblocks
+      $sth = $dbh->prepare("delete from freeblocks where cidr <<= ?");
+      my $sth2 = $dbh->prepare("insert into freeblocks (cidr,maskbits,city,routed) values (?,?,'<NULL>','n')");
+      foreach my $newblock (@blocklist) {
+        $sth->execute("$newblock");
+        $sth2->execute("$newblock", $newblock->masklen);
+      }
+
+      # master
+      $sth = $dbh->prepare("delete from masterblocks where cidr <<= '$cidr'");
+      $sth->execute;
+      $sth = $dbh->prepare("insert into masterblocks (cidr,rwhois) values ('$cidr','y')");
+      $sth->execute;
+
+      # *whew*  If we got here, we likely suceeded.
+      $dbh->commit;
+    } # new master contained existing master(s)
+  }; # end eval
+
+  if ($@) {
+    my $msg = $@;
+    eval { $dbh->rollback; };
+    return ('FAIL',$msg);
+  } else {
+    return ('OK','OK');
+  }
+} # end addMaster
+
+
+## IPDB::allocateBlock()
+# Does all of the magic of actually allocating a netblock
+# Requires database handle, block to allocate, custid, type, city,
+#	description, notes, circuit ID, block to allocate from, private data
+# Returns a success code and optional error message.
+sub allocateBlock {
+  my ($dbh,undef,undef,$custid,$type,$city,$desc,$notes,$circid,$privdata) = @_;
+
+  my $cidr = new NetAddr::IP $_[1];
+  my $alloc_from = new NetAddr::IP $_[2];
+  my $sth;
+
+  # Snag the "type" of the freeblock (alloc_from) "just in case"
+  $sth = $dbh->prepare("select routed from freeblocks where cidr='$alloc_from'");
+  $sth->execute;
+  my ($alloc_from_type) = $sth->fetchrow_array;
+
+  # To contain the error message, if any.
+  my $msg = "Unknown error allocating $cidr as '$type'";
+
+  # Enable transactions and error handling
+  local $dbh->{AutoCommit} = 0;	# These need to be local so we don't
+  local $dbh->{RaiseError} = 1;	# step on our toes by accident.
+
+  if ($type =~ /^.i$/) {
+    $msg = "Unable to assign static IP $cidr to $custid";
+    eval {
+      # We have to do this in two parts because otherwise we lose
+      # the ability to return the IP assigned.  Should that change,
+      # the commented SQL statement below may become usable.
+# update poolips set custid='$custid',city='$city',available='n',
+#	description='$desc',notes='$notes',circuitid='$circid'
+#	where ip=(select ip from poolips where pool='$alloc_from'
+#	and available='y' order by ip limit 1);
+
+      $sth = $dbh->prepare("select ip from poolips where pool='$alloc_from'".
+	" and available='y' order by ip");
+      $sth->execute;
+
+      my @data = $sth->fetchrow_array;
+      $cidr = $data[0];  # $cidr is already declared when we get here!
+
+      $sth = $dbh->prepare("update poolips set custid='$custid',".
+	"city='$city',available='n',description='$desc',notes='$notes',".
+	"circuitid='$circid',privdata='$privdata'".
+	" where ip='$cidr'");
+      $sth->execute;
+      $dbh->commit;
+    };
+    if ($@) {
+      $msg .= ": '".$sth->errstr."'";
+      eval { $dbh->rollback; };
+      return ('FAIL',$msg);
+    } else {
+      return ('OK',"$cidr");
+    }
+
+  } else { # end IP-from-pool allocation
+
+    if ($cidr == $alloc_from) {
+      # Easiest case- insert in one table, delete in the other, and go home.  More or less.
+      # insert into allocations values (cidr,custid,type,city,desc) and
+      # delete from freeblocks where cidr='cidr'
+      # For data safety on non-transaction DBs, we delete first.
+
+      eval {
+	$msg = "Unable to allocate $cidr as '$disp_alloctypes{$type}'";
+	if ($type eq 'rm') {
+	  $sth = $dbh->prepare("update freeblocks set routed='y',city='$city'".
+	    " where cidr='$cidr'");
+	  $sth->execute;
+	  $sth = $dbh->prepare("insert into routed (cidr,maskbits,city)".
+		" values ('$cidr',".$cidr->masklen.",'$city')");
+	  $sth->execute;
+	} else {
+	  # common stuff for end-use, dialup, dynDSL, pools, etc, etc.
+
+	  # special case - block is a container/"reserve" block
+	  if ($type =~ /^(.)c$/) {
+	    $sth = $dbh->prepare("update freeblocks set routed='$1' where cidr='$cidr'");
+	    $sth->execute;
+	  } else {
+	    # "normal" case
+	    $sth = $dbh->prepare("delete from freeblocks where cidr='$cidr'");
+	    $sth->execute;
+	  }
+	  $sth = $dbh->prepare("insert into allocations".
+		" (cidr,custid,type,city,description,notes,maskbits,circuitid,privdata)".
+		" values ('$cidr','$custid','$type','$city','$desc','$notes',".
+		$cidr->masklen.",'$circid','$privdata')");
+	  $sth->execute;
+
+	  # And initialize the pool, if necessary
+	  # PPPoE pools (currently dialup, DSL, and WiFi) get all IPs made available
+	  # "DHCP" or "real-subnet" pools have the net, gw, and bcast IPs removed.
+	  if ($type =~ /^.p$/) {
+	    $msg = "Could not initialize IPs in new $disp_alloctypes{$type} $cidr";
+	    my ($code,$rmsg) = initPool($dbh,$cidr,$type,$city,"all");
+	    die $rmsg if $code eq 'FAIL';
+	  } elsif ($type =~ /^.d$/) {
+	    $msg = "Could not initialize IPs in new $disp_alloctypes{$type} $cidr";
+	    my ($code,$rmsg) = initPool($dbh,$cidr,$type,$city,"normal");
+	    die $rmsg if $code eq 'FAIL';
+	  }
+
+	} # routing vs non-routing netblock
+
+	$dbh->commit;
+      }; # end of eval
+      if ($@) {
+	$msg .= ": ".$@;
+	eval { $dbh->rollback; };
+	return ('FAIL',$msg);
+      } else {
+	return ('OK',"OK");
+      }
+
+    } else { # cidr != alloc_from
+
+      # Hard case.  Allocation is smaller than free block.
+      my $wantmaskbits = $cidr->masklen;
+      my $maskbits = $alloc_from->masklen;
+
+      my @newfreeblocks;	# Holds free blocks generated from splitting the source freeblock.
+
+      # This determines which blocks will be left "free" after allocation.  We take the
+      # block we're allocating from, and split it in half.  We see which half the wanted
+      # block is in, and repeat until the wanted block is equal to one of the halves.
+      my $i=0;
+      my $tmp_from = $alloc_from;	# So we don't munge $alloc_from
+      while ($maskbits++ < $wantmaskbits) {
+	my @subblocks = $tmp_from->split($maskbits);
+	$newfreeblocks[$i++] = (($cidr->within($subblocks[0])) ? $subblocks[1] : $subblocks[0]);
+	$tmp_from = ( ($cidr->within($subblocks[0])) ? $subblocks[0] : $subblocks[1] );
+      } # while
+
+      # Begin SQL transaction block
+      eval {
+	$msg = "Unable to allocate $cidr as '$disp_alloctypes{$type}'";
+
+	# Delete old freeblocks entry
+	$sth = $dbh->prepare("delete from freeblocks where cidr='$alloc_from'");
+	$sth->execute();
+
+	# now we have to do some magic for routing blocks
+	if ($type eq 'rm') {
+
+	  # Insert the new freeblocks entries
+	  # Note that non-routed blocks are assigned to <NULL>
+	  # and use the default value for the routed column ('n')
+	  $sth = $dbh->prepare("insert into freeblocks (cidr,maskbits,city)".
+		" values (?, ?, '<NULL>')");
+	  foreach my $block (@newfreeblocks) {
+ 	    $sth->execute("$block", $block->masklen);
+	  }
+
+	  # Insert the entry in the routed table
+	  $sth = $dbh->prepare("insert into routed (cidr,maskbits,city)".
+		" values ('$cidr',".$cidr->masklen.",'$city')");
+	  $sth->execute;
+	  # Insert the (almost) same entry in the freeblocks table
+	  $sth = $dbh->prepare("insert into freeblocks (cidr,maskbits,city,routed)".
+		" values ('$cidr',".$cidr->masklen.",'$city','y')");
+	  $sth->execute;
+
+	} else { # done with alloctype == rm
+
+	  # Insert the new freeblocks entries
+	  # Along with some more HairyPerl(TM):
+	  #   if $alloc_type_from is p
+	  #   OR
+	  #   $type matches /^(.)r$/
+	  # inserted value for routed column should match.
+	  # This solves the case of inserting an arbitrary block into a
+	  # "Reserve-for-routed-DSL" block.  Which you really shouldn't
+	  # do in the first place, but anyway...
+	  $sth = $dbh->prepare("insert into freeblocks (cidr,maskbits,city,routed)".
+		" values (?, ?, (select city from routed where cidr >>= '$cidr'),'".
+		( ( ($alloc_from_type =~ /^(p)$/) || ($type =~ /^(.)r$/) ) ? "$1" : 'y')."')");
+	  foreach my $block (@newfreeblocks) {
+ 	    $sth->execute("$block", $block->masklen);
+	  }
+	  # Special-case for reserve/"container" blocks - generate
+	  # the "extra" freeblocks entry for the container
+	  if ($type =~ /^(.)c$/) {
+	    $sth = $dbh->prepare("insert into freeblocks (cidr,maskbits,city,routed)".
+		" values ('$cidr',".$cidr->masklen.",'$city','$1')");
+	    $sth->execute;
+	  }
+	  # Insert the allocations entry
+	  $sth = $dbh->prepare("insert into allocations (cidr,custid,type,city,".
+		"description,notes,maskbits,circuitid,privdata)".
+		" values ('$cidr','$custid','$type','$city','$desc','$notes',".
+		$cidr->masklen.",'$circid','$privdata')");
+	  $sth->execute;
+
+	  # And initialize the pool, if necessary
+	  # PPPoE pools (currently dialup, DSL, and WiFi) get all IPs made available
+	  # "DHCP" or "real-subnet" pools have the net, gw, and bcast IPs removed.
+	  if ($type =~ /^.p$/) {
+	    $msg = "Could not initialize IPs in new $disp_alloctypes{$type} $cidr";
+	    my ($code,$rmsg) = initPool($dbh,$cidr,$type,$city,"all");
+	    die $rmsg if $code eq 'FAIL';
+	  } elsif ($type =~ /^.d$/) {
+	    $msg = "Could not initialize IPs in new $disp_alloctypes{$type} $cidr";
+	    my ($code,$rmsg) = initPool($dbh,$cidr,$type,$city,"normal");
+	    die $rmsg if $code eq 'FAIL';
+	  }
+
+	} # done with netblock alloctype != rm
+
+        $dbh->commit;
+      }; # end eval
+      if ($@) {
+	$msg .= ": ".$@;
+	eval { $dbh->rollback; };
+	return ('FAIL',$msg);
+      } else {
+	return ('OK',"OK");
+      }
+
+    } # end fullcidr != alloc_from
+
+  } # end static-IP vs netblock allocation
+
+} # end allocateBlock()
+
+
+## IPDB::initPool()
+# Initializes a pool
+# Requires a database handle, the pool CIDR, type, city, and a parameter
+# indicating whether the pool should allow allocation of literally every
+# IP, or if it should reserve network/gateway/broadcast IPs
+# Note that this is NOT done in a transaction, that's why it's a private
+# function and should ONLY EVER get called from allocateBlock()
+sub initPool {
+  my ($dbh,undef,$type,$city,$class) = @_;
+  my $pool = new NetAddr::IP $_[1];
+
+##fixme Need to just replace 2nd char of type with i rather than capturing 1st char of type
+  $type =~ s/[pd]$/i/;
+  my $sth;
+  my $msg;
+
+  # Trap errors so we can pass them back to the caller.  Even if the
+  # caller is only ever supposed to be local, and therefore already
+  # trapping errors.  >:(
+  local $dbh->{AutoCommit} = 0; # These need to be local so we don't
+  local $dbh->{RaiseError} = 1; # step on our toes by accident.
+
+  eval {
+    # have to insert all pool IPs into poolips table as "unallocated".
+    $sth = $dbh->prepare("insert into poolips (pool,ip,custid,city,type)".
+	" values ('$pool', ?, '6750400', '$city', '$type')");
+    my @poolip_list = $pool->hostenum;
+    if ($class eq 'all') { # (DSL-ish block - *all* IPs available
+      if ($pool->addr !~ /\.0$/) {	# .0 causes weirdness.
+	$sth->execute($pool->addr);
+      }
+      for (my $i=0; $i<=$#poolip_list; $i++) {
+	$sth->execute($poolip_list[$i]->addr);
+      }
+      $pool--;
+      if ($pool->addr !~ /\.255$/) {	# .255 can cause weirdness.
+	$sth->execute($pool->addr);
+      }
+    } else { # (real netblock)
+      for (my $i=1; $i<=$#poolip_list; $i++) {
+	$sth->execute($poolip_list[$i]->addr);
+      }
+    }
+  };
+  if ($@) {
+    $msg = "'".$sth->errstr."'";
+    eval { $dbh->rollback; };
+    return ('FAIL',$msg);
+  } else {
+    return ('OK',"OK");
+  }
+} # end initPool()
+
+
+## IPDB::deleteBlock()
+# Removes an allocation from the database, including deleting IPs
+# from poolips and recombining entries in freeblocks if possible
+# Also handles "deleting" a static IP allocation, and removal of a master
+# Requires a database handle, the block to delete, and the type of block
+sub deleteBlock {
+  my ($dbh,undef,$type) = @_;
+  my $cidr = new NetAddr::IP $_[1];
+
+  my $sth;
+
+  # Magic variables used for odd allocation cases.
+  my $container;
+  my $con_type;
+
+  # To contain the error message, if any.
+  my $msg = "Unknown error deallocating $type $cidr";
+  # Enable transactions and exception-on-errors... but only for this sub
+  local $dbh->{AutoCommit} = 0;
+  local $dbh->{RaiseError} = 1;
+
+  # First case.  The "block" is a static IP
+  # Note that we still need some additional code in the odd case
+  # of a netblock-aligned contiguous group of static IPs
+  if ($type =~ /^.i$/) {
+
+    eval {
+      $msg = "Unable to deallocate $disp_alloctypes{$type} $cidr";
+      $sth = $dbh->prepare("update poolips set custid='6750400',available='y',".
+	"city=(select city from allocations where cidr >>= '$cidr'".
+	" order by masklen(cidr) desc limit 1),".
+	"description='',notes='',circuitid='' where ip='$cidr'");
+      $sth->execute;
+      $dbh->commit;
+    };
+    if ($@) {
+      eval { $dbh->rollback; };
+      return ('FAIL',$msg);
+    } else {
+      return ('OK',"OK");
+    }
+
+  } elsif ($type eq 'mm') { # end alloctype =~ /.i/
+
+    $msg = "Unable to delete master block $cidr";
+    eval {
+      $sth = $dbh->prepare("delete from masterblocks where cidr='$cidr'");
+      $sth->execute;
+      $sth = $dbh->prepare("delete from freeblocks where cidr <<= '$cidr'");
+      $sth->execute;
+      $dbh->commit;
+    };
+    if ($@) {
+      eval { $dbh->rollback; };
+      return ('FAIL', $msg);
+    } else {
+      return ('OK',"OK");
+    }
+
+  } else { # end alloctype master block case
+
+    ## This is a big block; but it HAS to be done in a chunk.  Any removal
+    ## of a netblock allocation may result in a larger chunk of free
+    ## contiguous IP space - which may in turn be combined into a single
+    ## netblock rather than a number of smaller netblocks.
+
+    eval {
+
+      if ($type eq 'rm') {
+        $msg = "Unable to remove routing allocation $cidr";
+	$sth = $dbh->prepare("delete from routed where cidr='$cidr'");
+	$sth->execute;
+	# Make sure block getting deleted is properly accounted for.
+	$sth = $dbh->prepare("update freeblocks set routed='n',city='<NULL>'".
+		" where cidr='$cidr'");
+	$sth->execute;
+	# Set up query to start compacting free blocks.
+	$sth = $dbh->prepare("select cidr from freeblocks where ".
+		"maskbits<=".$cidr->masklen." and routed='n' order by maskbits desc");
+
+      } else { # end alloctype routing case
+
+	# Magic.  We need to get information about the containing block (if any)
+	# so as to make sure that the freeblocks we insert get the correct "type".
+	$sth = $dbh->prepare("select cidr,type from allocations where cidr >> '$cidr'");
+	$sth->execute;
+	($container, $con_type) = $sth->fetchrow_array;
+
+	# Delete all allocations within the block being deleted.  This is
+	# deliberate and correct, and removes the need to special-case
+	# removal of "container" blocks.
+	$sth = $dbh->prepare("delete from allocations where cidr <<='$cidr'");
+	$sth->execute;
+
+	# Special case - delete pool IPs
+	if ($type =~ /^.[pd]$/) {
+	  # We have to delete the IPs from the pool listing.
+	  $sth = $dbh->prepare("delete from poolips where pool='$cidr'");
+	  $sth->execute;
+	}
+
+	# Set up query for compacting free blocks.
+	if ($con_type && $con_type eq 'pc') {
+	  # Clean up after "bad" allocations (blocks that are not formally
+	  # contained which have nevertheless been allocated from a container block)
+	  # We want to make certain that the freeblocks are properly "labelled"
+	  $sth = $dbh->prepare("select cidr from freeblocks where cidr <<= '$container' order by maskbits desc");
+	} else {
+	  # Standard deallocation.
+	  $sth = $dbh->prepare("select cidr from freeblocks where cidr <<= ".
+		"(select cidr from routed where cidr >>= '$cidr') ".
+		" and maskbits<=".$cidr->masklen.
+		" and routed='".(($type =~ /^(.)r$/) ? "$1" : 'y').
+		"' order by maskbits desc");
+	}
+
+      } # end alloctype general case
+
+##TEMP
+## Temporary wrapper to "properly" deallocate sIP PPPoE/DSL "netblocks" in 209.91.185.0/24
+## Note that we should really general-case this.
+my $staticpool = new NetAddr::IP "209.91.185.0/24";
+##TEMP
+if ($cidr->within($staticpool)) {
+##TEMP
+  # We've already deleted the block, now we have to stuff its IPs into the pool.
+  my $sth2 = $dbh->prepare("insert into poolips values ('209.91.185.0/24',?,'6750400','Sudbury','di','y','','','')");
+  $sth2->execute($cidr->addr);
+  foreach my $ip ($cidr->hostenum) {
+    $sth2->execute("$ip");
+  }
+  $cidr--;
+  $sth2->execute($cidr->addr);
+
+##TEMP
+} else {
+##TEMP
+
+      # Now we look for larger-or-equal-sized free blocks in the same master (routed)
+      # (super)block. If there aren't any, we can't combine blocks anyway.  If there
+      # are, we check to see if we can combine blocks.
+      # Execute the statement prepared in the if-else above.
+
+      $sth->execute;
+
+# NetAddr::IP->compact() attempts to produce the smallest inclusive block
+# from the caller and the passed terms.
+# EG:  if you call $cidr->compact($ip1,$ip2,$ip3) when $cidr, $ip1, $ip2,
+#	and $ip3 are consecutive /27's starting on .0 (.0-.31, .32-.63,
+#	.64-.95, and .96-.128), you will get an array containing a single
+#	/25 as element 0 (.0-.127).  Order is not important;  you could have
+#	$cidr=.32/27, $ip1=.96/27, $ip2=.0/27, and $ip3=.64/27.
+
+      my (@together, @combinelist);
+      my $i=0;
+      while (my @data = $sth->fetchrow_array) {
+	my $testIP = new NetAddr::IP $data[0];
+	@together = $testIP->compact($cidr);
+	my $num = @together;
+	if ($num == 1) {
+	  $cidr = $together[0];
+	  $combinelist[$i++] = $testIP;
+	}
+      }
+
+      # Clear old freeblocks entries - if any.  They should all be within
+      # the $cidr determined above.
+      $sth = $dbh->prepare("delete from freeblocks where cidr <<='$cidr'");
+      $sth->execute;
+
+      # insert "new" freeblocks entry
+      if ($type eq 'rm') {
+	$sth = $dbh->prepare("insert into freeblocks (cidr,maskbits,city)".
+		" values ('$cidr',".$cidr->masklen.",'<NULL>')");
+      } else {
+	# Magic hackery to insert "correct" data for deallocation of
+	# non-contained blocks allocated from within a container.
+	$type = 'pr' if $con_type && $con_type eq 'pc';
+
+	$sth = $dbh->prepare("insert into freeblocks (cidr,maskbits,city,routed)".
+		" values ('$cidr',".$cidr->masklen.
+		",(select city from routed where cidr >>= '$cidr'),'".
+		(($type =~ /^(.)r$/) ? "$1" : 'y')."')");
+      }
+      $sth->execute;
+
+##TEMP
+}
+##TEMP
+
+      # If we got here, we've succeeded.  Whew!
+      $dbh->commit;
+    }; # end eval
+    if ($@) {
+      eval { $dbh->rollback; };
+      return ('FAIL', $msg);
+    } else {
+      return ('OK',"OK");
+    }
+
+  } # end alloctype != netblock
+
+} # end deleteBlock()
+
+
+## IPDB::getBlockData()
+# Return custid, type, city, and description for a block
+sub getBlockData {
+  my $dbh = shift;
+  my $block = shift;
+
+  my $sth = $dbh->prepare("select cidr,custid,type,city,description from searchme".
+	" where cidr='$block'");
+  $sth->execute();
+  return $sth->fetchrow_array();
+} # end getBlockData()
+
+
+## IPDB::mailNotify()
+# Sends notification mail to recipients regarding an IPDB operation
+sub mailNotify ($$$) {
+  my ($recip,$subj,$message) = @_;
+  my $mailer = Net::SMTP->new("smtp.example.com", Hello => "ipdb.example.com");
+
+  $mailer->mail('ipdb@example.com');
+  $mailer->to($recip);
+  $mailer->data("From: \"IP Database\" <ipdb\@example.com>\n",
+	"To: $recip\n",
+	"Date: ".strftime("%a, %d %b %Y %H:%M:%S %z",localtime)."\n",
+	"Subject: {IPDB} $subj\n",
+	"X-Mailer: IPDB Notify v".sprintf("%.1d",$IPDB::VERSION)."\n",
+	"Organization: Example Corp\n",
+	"\n$message\n");
+  $mailer->quit;
+}
+
+# Indicates module loaded OK.  Required by Perl.
+1;
Index: tags/2.5/cgi-bin/MyIPDB.pm
===================================================================
--- tags/2.5/cgi-bin/MyIPDB.pm	(revision 381)
+++ tags/2.5/cgi-bin/MyIPDB.pm	(revision 381)
@@ -0,0 +1,22 @@
+# ipdb/cgi-bin/MyIPDB.pm
+# Contains site-specific functions for IPDB
+# May override some functions from IPDB.pm, wraps others
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+# Copyright (C) 2004-2006 - Kris Deugau
+
+use IPDB 2.0 qw(:ALL);
+
+## connectDB_My()
+# Wrapper for IPDB::connectDB
+# Takes no arguments, returns whatever IPDB::connectDB returns.
+sub connectDB_My {
+  return connectDB("ipdb", "ipdb", "ipdbpwd");
+} # end connectDB_My()
+
+# Keep Perl from complaining.
+1;
Index: tags/2.5/cgi-bin/access-pwd-update.pl
===================================================================
--- tags/2.5/cgi-bin/access-pwd-update.pl	(revision 381)
+++ tags/2.5/cgi-bin/access-pwd-update.pl	(revision 381)
@@ -0,0 +1,51 @@
+#!/usr/bin/perl
+# Update IPDB users table with user/password data from 5-minute
+# cron'ed push from billing
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+# Copyright (C) 2007 - Kris Deugau
+
+use strict;
+use warnings;
+use DBI;
+use MyIPDB;
+
+my $ip_dbh;
+my $errstr;
+($ip_dbh,$errstr) = connectDB_My;
+if (!$ip_dbh) {
+  die "Database error: $errstr\n";
+}
+initIPDBGlobals($ip_dbh);
+
+my %userhash;
+
+open HTPASS, "</var/www/ipdb.example.com/ip/.htpasswd";
+
+my $sth = $ip_dbh->prepare("select count(*) from users where username=?");
+
+while (<HTPASS>) {
+  chomp;
+  my ($user,$pass) = split /:/;
+  $sth->execute($user);
+  my @data = $sth->fetchrow_array();
+  my $sql;
+  if ($data[0] == 0) {
+    $sql = "insert into users (username,password) values ('$user','$pass')";
+  } else {
+    $sql = "update users set password='$pass' where username='$user'";
+  }
+  my $sth2 = $ip_dbh->prepare($sql);
+  $sth2->execute or print "error executing $sql: ".$DBI::errstr."\n";
+  $userhash{$user} = $pass;
+}
+my @userlist = @{ $ip_dbh->selectall_arrayref("select username from users") };
+$sth = $ip_dbh->prepare("delete from users where username=?");
+foreach my $user (@userlist) {
+  $sth->execute if !$userhash{$user}
+    or print "error deleting $user: ".$DBI::errstr."\n";
+}
Index: tags/2.5/cgi-bin/admin.cgi
===================================================================
--- tags/2.5/cgi-bin/admin.cgi	(revision 381)
+++ tags/2.5/cgi-bin/admin.cgi	(revision 381)
@@ -0,0 +1,543 @@
+#!/usr/bin/perl
+# ipdb/cgi-bin/admin.cgi
+# Hack interface to make specific changes to IPDB that (for one reason
+# or another) can't be made through the main interface.
+#
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+# Copyright (C) 2004-2006 - Kris Deugau
+
+use strict;
+use warnings;
+use CGI::Carp qw(fatalsToBrowser);
+use DBI;
+use CommonWeb qw(:ALL);
+use MyIPDB;
+use CustIDCK;
+#use POSIX qw(ceil);
+use NetAddr::IP;
+
+use Sys::Syslog;
+
+openlog "IPDB-admin","pid","local2";
+
+# Collect the username from HTTP auth.  If undefined, we're in a test environment.
+my $authuser;
+if (!defined($ENV{'REMOTE_USER'})) {
+  $authuser = '__temptest';
+} else {
+  $authuser = $ENV{'REMOTE_USER'};
+}
+
+syslog "debug", "$authuser active";
+
+# Why not a global DB handle?  (And a global statement handle, as well...)
+# Use the connectDB function, otherwise we end up confusing ourselves
+my $ip_dbh;
+my $sth;
+my $errstr;
+($ip_dbh,$errstr) = connectDB_My;
+if (!$ip_dbh) {
+  printAndExit("Database error: $errstr\n");
+}
+initIPDBGlobals($ip_dbh);
+
+if ($IPDBacl{$authuser} !~ /A/) {
+  print "Content-Type: text/html\n\n".
+	"<html><head><title>Access denied</title></head><body>\n".
+	'Access to this tool is restricted.  Contact <a href="mailto:kdeugau@vianet.ca">Kris</a> '.
+	"for more information.</body></html>\n";
+  exit;
+}
+
+my %webvar = parse_post();
+cleanInput(\%webvar);
+
+print "Content-type: text/html\n\n".
+	"<html>\n<head>\n\t<title>[IPDB admin tools]</title>\n".
+	qq(\t<link rel="stylesheet" type="text/css" href="/ip/ipdb.css">\n).
+	"</head>\n<body>\n".
+	"<h2>IPDB - Administrative Tools</h2>\n<hr>\n";
+
+if(!defined($webvar{action})) {
+  $webvar{action} = "<NULL>";   #shuts up the warnings.
+
+  my $typelist = '';
+  $sth = $ip_dbh->prepare("select type,listname from alloctypes where listorder < 900 order by listorder");
+  $sth->execute;
+  my @data = $sth->fetchrow_array;
+  $typelist .= "<option value='$data[0]' selected>$data[1]</option>\n";
+  while (my @data = $sth->fetchrow_array) {
+    $typelist .= "<option value='$data[0]'>$data[1]</option>\n";
+  }
+
+  print qq(WARNING:  There are FAR fewer controls on what you can do here.  Use the
+main interface if at all possible.
+<hr>
+<a href="admin.cgi?action=newalloc">Add allocation</a>
+<hr>
+<form action="admin.cgi" method="POST">
+<input type=hidden name=action value=alloc>
+Allocate block/IP: <input name=cidr> as <select name=alloctype>$typelist</select> to <input name=custid>
+<input type=submit value=" GIMME!! "></form>
+<hr><form action="admin.cgi" method="POST">
+<input type=hidden name=action value=alloctweak>
+Manually update allocation data in this /24: <input name=allocfrom>
+<input type=submit value="Show allocations">
+</form>
+<hr><a href="admin.cgi?action=showpools">List IP Pools</a> for manual tweaking and updates
+<hr><a href="admin.cgi?action=showusers">Manage users</a> (add/remove users;  change
+internal access controls - note that this does NOT include IP-based limits)
+<hr>Consistency check tools<br>
+<a href="consistency-check.pl">General</a>:  Check general netblock consistency.<br>
+<a href="freespace.pl">Free space</a>:  List total and aggregate free space.  Does not 
+include private networks (192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8)
+<hr>(r)WHOIS<br>
+<a href="list-cust.php">List customer data for WHOIS</a> - data used for blocks with the SWIP box checkmarked.
+Links to edit/add data are on this page.
+);
+} else {
+  print '<a href="/ip/cgi-bin/admin.cgi">Back</a> to main<hr>';
+}
+
+
+## Possible actions.
+if ($webvar{action} eq 'alloc') {
+  # OK, we know what we're allocating.
+
+  if ($webvar{cidr} !~ /^\s*(\d{1,3}\.){3}\d{1,3}(\/\d{2})?\s*$/) {
+    printAndExit("Can't allocate something that's not a netblock/ip");
+  }
+
+  $sth = $ip_dbh->prepare("select def_custid from alloctypes where type='$webvar{alloctype}'");
+  $sth->execute;
+  my @data = $sth->fetchrow_array;
+  my $custid = $data[0];
+  if ($custid eq '') {
+    if ($webvar{custid} !~ /^(?:\d{10}|\d{7}|STAFF)(?:-\d\d?)?$/) {
+      # Force uppercase for now...
+      $webvar{custid} =~ tr/a-z/A-Z/;
+      # Crosscheck with billing.
+      my $status = CustIDCK->custid_exist($webvar{custid});
+      if ($CustIDCK::Error) {
+	printError("Error verifying customer ID: ".$CustIDCK::ErrMsg);
+	return;
+      }
+      if (!$status) {
+	printError("Customer ID not valid.  Make sure the Customer ID ".
+	  "is correct.<br>\nUse STAFF for staff static IPs, and 6750400 for any other ".
+	  "non-customer assignments.");
+	return;
+      }
+    }
+    # Type that doesn't have a default custid
+    $custid = $webvar{custid};
+  }
+
+  my $cidr = new NetAddr::IP $webvar{cidr};
+  my @data;
+  if ($webvar{alloctype} eq 'rm') {
+    $sth = $ip_dbh->prepare("select cidr from freeblocks where cidr >>='$cidr' and routed='n'");
+    $sth->execute;
+    @data = $sth->fetchrow_array;
+# User deserves errors if user can't be bothered to find the free block first.
+    printAndExit("Can't allocate from outside a free block!!\n")
+        if !$data[0];
+  } elsif ($webvar{alloctype} =~ /^(.)i$/) {
+    $sth = $ip_dbh->prepare("select cidr from allocations where cidr >>='$cidr' and (type like '_d' or type like '_p')");
+    $sth->execute;
+    @data = $sth->fetchrow_array;
+# User deserves errors if user can't be bothered to find the pool and a free IP first.
+    printAndExit("Can't allocate static IP from outside a pool!!\n")
+	if !$data[0];
+  } else {
+    $sth = $ip_dbh->prepare("select cidr from freeblocks where cidr >>='$cidr' and not (routed='n')");
+    $sth->execute;
+    @data = $sth->fetchrow_array;
+# User deserves errors if user can't be bothered to find the free block first.
+    printAndExit("Can't allocate from outside a routed block!!\n")
+        if !$data[0];
+  }
+
+  my $alloc_from = new NetAddr::IP $data[0];
+  $sth->finish;
+
+  my $cities = '';
+  foreach my $city (@citylist) {
+    $cities .= "<option>$city</option>\n";
+  }
+
+  print qq(<table class=regular>
+<form method=POST action=admin.cgi>
+<tr class=color1>
+<td>Allocating:</td>
+<td>$cidr<input type=hidden name=cidr value="$cidr"></td>
+</tr><tr class=color2>
+<td>Type:</td><td>$disp_alloctypes{$webvar{alloctype}}
+<input type=hidden name=alloctype value="$webvar{alloctype}"></td>
+</tr><tr class=color1>
+<td>Allocated from:</td>
+<td>$alloc_from<input type=hidden name=alloc_from value="$alloc_from"></td>
+</tr><tr class="color2">
+<td>Customer ID:</td><td>$custid<input type=hidden name=custid value="$custid"></td>
+</tr><tr class=color1>
+<td>Customer location:</td><td>
+<select name="city"><option selected>-</option>
+$cities
+</select>
+&nbsp;<a href="javascript:popNotes('/ip/newcity.html')">Add new location</a>
+</td>
+</tr>
+<tr class="color2">
+<td>Circuit ID:</td><td><input name=circid size=40></td>
+</tr><tr class="color1">
+<td>Description/Name:</td><td><input name="desc" size=40></td>
+</tr><tr class="color2">
+<td>Notes:</td><td><textarea name="notes" rows="3" cols="40"></textarea></td>
+</tr><tr class="warning">
+<td colspan=2><center>WARNING:  This will IMMEDIATELY assign this block!!</center></td>
+</tr><tr class="color2">
+<td class="center" colspan="2"><input type="submit" value="  Assign  "></td>
+<input type="hidden" name="action" value="confirm">
+</form>
+</tr>
+</table>
+);
+
+
+} elsif ($webvar{action} eq 'confirm') {
+
+  print "Assigning $webvar{cidr} to $webvar{custid} (\"$webvar{desc}\") as ".
+	"$disp_alloctypes{$webvar{alloctype}}...<br>\n";
+  # Only need to check city here.
+  if ($webvar{city} eq '-') {
+    printError("Invalid customer location!  Go back and select customer's location.");
+  } else {
+    if ($webvar{alloctype} =~ /^.i$/) {
+      $sth = $ip_dbh->prepare("update poolips set available='n', custid='$webvar{custid}', ".
+	"city='$webvar{city}', description='$webvar{desc}', notes='$webvar{notes}' ".
+	"where ip='$webvar{cidr}'");
+      $sth->execute;
+      if ($sth->err) {
+	print "Allocation failed!  DBI said:\n".$sth->errstr."\n";
+        syslog "err", "($authuser) Allocation of '$webvar{cidr}' to '$webvar{custid}' as ".
+		"'$webvar{alloctype}' failed: '".$sth->errstr."'";
+      } else {
+	print "Allocation OK!\n";
+	syslog "notice", "$authuser allocated '$webvar{cidr}' to '$webvar{custid}' as ".
+		"'$webvar{alloctype}'";
+	# Notify tech@example.com
+	mailNotify('tech@example.com',"$disp_alloctypes{$webvar{alloctype}} allocation",
+	  "$disp_alloctypes{$webvar{alloctype}} $webvar{cidr} allocated to customer".
+	  " $webvar{custid}\n".
+	  "Description: $webvar{desc}\n\nAllocated by: $authuser\n");
+      }
+    } else {
+      my ($retcode,$msg) = allocateBlock($ip_dbh, $webvar{cidr}, $webvar{alloc_from},
+	$webvar{custid}, $webvar{alloctype}, $webvar{city}, $webvar{desc}, $webvar{notes},
+	$webvar{circid});
+      if ($retcode eq 'OK') {
+	print "Allocation OK!\n";
+	syslog "notice", "$authuser allocated '$webvar{cidr}' to '$webvar{custid}' as ".
+		"'$webvar{alloctype}'";
+      } else {
+	print "Allocation failed!  IPDB::allocateBlock said:\n$msg\n";
+        syslog "err", "($authuser) Allocation of '$webvar{cidr}' to '$webvar{custid}' as ".
+		"'$webvar{alloctype}' failed: '$msg'";
+      }
+    } # static IP vs netblock
+
+  } # done city check
+
+} elsif ($webvar{action} eq 'alloctweak') {
+  fix_allocfrom();
+  showAllocs($webvar{allocfrom});
+} elsif ($webvar{action} eq 'update') {
+  update();
+} elsif ($webvar{action} eq 'assign') {
+  # Display a list of possible blocks within the requested block.
+  open (HTML, "../admin_alloc.html")
+	or croak "Could not open admin_alloc.html :$!";
+  my $html = join('', <HTML>);
+  $html =~ s/\$\$MASK\$\$/$webvar{masklen}/g;
+  $html =~ s/\$\$ALLOCFROM\$\$/$webvar{allocfrom}/g;
+
+  my $from = new NetAddr::IP $webvar{allocfrom};
+  my @blocklist = $from->split($webvar{masklen});
+  my $availblocks;
+  foreach (@blocklist) {
+    $availblocks .= qq(<tr><td colspan=2 align=center><input type=radio name=block value="$_">$_</td></tr>\n);
+  }
+  $html =~ s/\$\$BLOCKLIST\$\$/$availblocks/g;
+
+  print $html;
+} elsif ($webvar{action} eq 'showpools') {
+  print "IP Pools currently allocated:\n".
+	"<table border=1>\n<tr><td>Pool</td><td># of free IPs</td></tr>\n";
+  $sth = $ip_dbh->prepare("select cidr from allocations where type like '%p' or type like '%d' order by cidr");
+  $sth->execute;
+  my %poolfree;
+  while (my @data = $sth->fetchrow_array) {
+    $poolfree{$data[0]} = 0;
+  }
+  $sth = $ip_dbh->prepare("select pool,ip from poolips where available='y' order by ip");
+  $sth->execute;
+  while (my @data = $sth->fetchrow_array) {
+    $poolfree{$data[0]}++;
+  }
+  foreach my $key (keys %poolfree) {
+    print qq(<tr><td><a href="admin.cgi?action=tweakpool&pool=$key">$key</a></td>).
+	"<td>$poolfree{$key}</td></tr>\n";
+  }
+  print "</table>\n";
+} elsif ($webvar{action} eq 'tweakpool') {
+  showPool($webvar{pool});
+} elsif ($webvar{action} eq 'updatepool') {
+
+  $sth = $ip_dbh->prepare("update poolips set custid='$webvar{custid}', ".
+	"city='$webvar{city}', type='$webvar{type}', available='".
+	(($webvar{available} eq 'y') ? 'y' : 'n').
+	"', notes='$webvar{notes}', description='$webvar{desc}' ".
+	"where ip='$webvar{ip}'");
+  $sth->execute;
+  if ($sth->err) {
+    print "Error updating pool IP $webvar{ip}: $@<hr>\n";
+    syslog "err", "$authuser could not update pool IP $webvar{ip}: $@";
+  } else {  
+    $sth = $ip_dbh->prepare("select pool from poolips where ip='$webvar{ip}'");
+    $sth->execute;
+    my @data = $sth->fetchrow_array;
+    print "$webvar{ip} in $data[0] updated\n<hr>\n";
+    syslog "notice", "$authuser updated pool IP $webvar{ip}";
+  }
+} elsif ($webvar{action} eq 'showusers') {
+  print "Notes:<br>\n".
+	"<li>Admin users automatically get all other priviledges.\n".
+	"<li>Everyone has basic read access.\n".
+	"<hr>Add new user:<form action=admin.cgi method=POST>\n".
+	"Username: <input name=username><br>\n".
+	"Password: <input name=password> <input type=checkbox name=preenc>Password is pre-encrypted (MUST be crypt() encrypted)<br>\n".
+	"<input type=submit value='Add user'><input type=hidden name=action value=newuser></form>\n";
+
+  print "<hr>Users with access:\n<table border=1>\n";
+  print "<tr><td></td><td align=center colspan=3>General access</td></tr>\n";
+  print "<tr><td>Username</td><td>Add new</td><td>Change</td>".
+	"<td>Delete</td><td>Systems/Networking</td><td>Admin user</td></tr>\n".
+	"<form action=admin.cgi method=POST>\n";
+  $sth = $ip_dbh->prepare("select username,acl from users order by username");
+  $sth->execute;
+  while (my @data = $sth->fetchrow_array) {
+    print "<form action=admin.cgi method=POST><input type=hidden name=action value=updacl>".
+	qq(<tr><td>$data[0]<input type=hidden name=username value="$data[0]"></td><td>).
+    # Now for the fun bit.  We have to pull apart the ACL field and
+    # output a bunch of checkboxes.
+    	"<input type=checkbox name=add".($data[1] =~ /a/ ? ' checked=y' : '').
+	"></td><td><input type=checkbox name=change".($data[1] =~ /c/ ? ' checked=y' : '').
+	"></td><td><input type=checkbox name=del".($data[1] =~ /d/ ? ' checked=y' : '').
+	"></td><td><input type=checkbox name=sysnet".($data[1] =~ /s/ ? ' checked=y' : '').
+	"></td><td><input type=checkbox name=admin".($data[1] =~ /A/ ? ' checked=y' : '').
+	qq(></td><td><input type=submit value="Update"></td></form>\n).
+	"<form action=admin.cgi method=POST><td><input type=hidden name=action value=deluser>".
+	"<input type=hidden name=username value=$data[0]>".
+	qq(<input type=submit value="Delete user"></tr></form>\n);
+
+  }
+  print "</table>\n";
+} elsif ($webvar{action} eq 'updacl') {
+  print "Updating ACL for $webvar{username}:<br>\n";
+  my $acl = 'b';
+  if ($webvar{admin} eq 'on') {
+    $acl .= "acdsA";
+  } else {
+    $acl .= ($webvar{add} eq 'on' ? 'a' : '').
+	($webvar{change} eq 'on' ? 'c' : '').
+	($webvar{del} eq 'on' ? 'd' : '').
+	($webvar{sysnet} eq 'on' ? 's' : '');
+  }
+  print "New ACL: $acl<br>\n";
+
+  $sth = $ip_dbh->prepare("update users set acl='$acl' where username='$webvar{username}'");
+  $sth->execute;
+  print "OK\n" if !$sth->err;
+
+  print qq(<hr><a href="admin.cgi?action=showusers">Back</a> to user listing\n);
+
+} elsif ($webvar{action} eq 'newuser') {
+  print "Adding user $webvar{username}...\n";
+  my $cr_pass = ($webvar{preenc} ? $webvar{password} :
+	crypt $webvar{password}, join('',('.','/',0..9,'A'..'Z','a'..'z')[rand 64, rand 64]));
+  $sth = $ip_dbh->prepare("insert into users (username,password,acl) values ".
+	"('$webvar{username}','$cr_pass','b')");
+  $sth->execute;
+  if ($sth->err) {
+    print "<br>Error adding user: ".$sth->errstr;
+  } else {
+    print "OK\n";
+  }
+
+  print qq(<hr><a href="admin.cgi?action=showusers">Back</a> to user listing\n);
+
+} elsif ($webvar{action} eq 'deluser') {
+  print "Deleting user $webvar{username}.<br>\n";
+  $sth = $ip_dbh->prepare("delete from users where username='$webvar{username}'");
+  $sth->execute;
+  print "OK\n" if !$sth->err;
+
+  print qq(<hr><a href="admin.cgi?action=showusers">Back</a> to user listing\n);
+
+} elsif ($webvar{action} ne '<NULL>') {
+  print "webvar{action} check failed: Don't know how to $webvar{action}";
+}
+
+# Hokay.  This is a little different.  We have a few specific functions here:
+#  -> Assign arbitrary subnet from arbitrary free space
+#  -> Tweak individual DB fields
+#
+
+print qq(<hr><a href="/ip/">Back</a> to main interface</a>\n);
+
+printFooter;
+
+$ip_dbh->disconnect;
+
+exit;
+
+
+# Tweak allocfrom into shape.
+sub fix_allocfrom {
+  if ($webvar{allocfrom} =~ /^(\d+\.){2}\d+$/) {
+    # 3-octet class C specified
+    $webvar{allocfrom} .= ".0/24";
+  } elsif ($webvar{allocfrom} =~ /^(\d+\.){3}\d+$/) {
+    # 4-octet IP specified;  
+    $webvar{allocfrom} .= "/24";
+  }
+}
+
+
+# List free blocks in a /24 for arbitrary manual allocation
+sub showfree($) {
+  my $cidr = new NetAddr::IP $_[0];
+  print "Showing free blocks in $cidr<br>\n".
+	"<table border=1>\n";
+  $sth = $ip_dbh->prepare("select * from freeblocks where cidr <<= '$cidr' order by cidr");
+  $sth->execute;
+  while (my @data = $sth->fetchrow_array) {
+    my $temp = new NetAddr::IP $data[0];
+    print "<tr><form action=admin.cgi method=POST><input type=hidden name=action value=assign>\n".
+	qq(<td>$temp<input type=hidden name=allocfrom value="$temp"></td>\n).
+	"<td>".
+	(($temp->masklen == 30) ? '<input type=hidden name=masklen value=30>30'
+	  : "<select name=masklen><option>30</option>\n<option>29</option>\n") .
+	(($temp->masklen < 29) ? "<option>28</option>\n" : '') .
+	(($temp->masklen < 28) ? "<option>27</option>\n" : '') .
+	(($temp->masklen < 27) ? "<option>26</option>\n" : '') .
+	(($temp->masklen < 26) ? "<option>25</option>\n" : '') .
+	(($temp->masklen < 25) ? "<option>24</option>\n" : '') .
+	"</td>".
+	qq(<td>$data[2]</td><td><input type=submit value="Allocate from here"></td>).
+	"\n</form></tr>\n";
+  }
+  print "</table>\n";
+}
+
+
+# Show allocations to allow editing.
+sub showAllocs($) {
+  my $cidr = new NetAddr::IP $_[0];
+  print "Edit custID, allocation type, city for allocations in ".
+	"$cidr:\n<table border=1>";
+  $sth = $ip_dbh->prepare("select * from allocations where cidr <<= '$cidr' order by cidr");
+  $sth->execute;
+  while (my @data = $sth->fetchrow_array) {
+    print "<tr><form action=admin.cgi method=POST><input type=hidden name=action value=update>\n".
+	qq(<td>$data[0]<input type=hidden value="$data[0]" name=block></td>\n).
+	qq(<td><input name=custid value="$data[1]"></td>\n).
+	"<td><select name=alloctype>";
+
+    my $sth2 = $ip_dbh->prepare("select type,listname from alloctypes".
+	" where listorder < 500 and not (type like '_i') order by listorder");
+    $sth2->execute;
+    while (my @types = $sth2->fetchrow_array) {
+      print "<option". (($data[2] eq $types[0]) ? ' selected' : '') .
+	" value='$types[0]'>$types[1]</option>\n";
+    }
+
+    print qq(<td><input name=city value="$data[3]"></td>\n).
+	"<td>$data[4]</td><td>$data[5]</td>".
+	qq(<td><input type=submit value="Update"></td></form></tr>\n);
+  }
+  print "</table>\n";
+
+  # notes
+  print "<hr><b>Notes:</b>\n".
+	"<ul>\n<li>Use the main interface to update description and notes fields\n".
+	"<li>Changing the allocation type here will NOT affect IP pool data.\n".
+	"</ul>\n";
+}
+
+
+# Stuff updates into DB
+sub update {
+  eval {
+    # Relatively simple SQL transaction here.  Note that we're deliberately NOT
+    # updating notes/desc here as it's available through the main interface.
+    $sth = $ip_dbh->prepare("update allocations set custid='$webvar{custid}',".
+	"city='$webvar{city}',type='$webvar{alloctype}' where cidr='$webvar{block}'");
+    $sth->execute;
+    $ip_dbh->commit;
+  };
+  if ($@) {
+    carp "Transaction aborted because $@";
+    eval { $ip_dbh->rollback; };
+    syslog "err", "$authuser could not update block '$webvar{block}': '$@'";
+  } else {
+    # If we get here, the operation succeeded.
+    syslog "notice", "$authuser updated $webvar{block}";
+    print "Allocation $webvar{block} updated<hr>\n";
+  }
+  # need to get /24 that block is part of
+  my @bits = split /\./, $webvar{block};
+  $bits[3] = "0/24";
+  showAllocs((join ".", @bits));
+}
+
+
+# showPool()
+# List all IPs in a pool, and allow arbitrary admin changes to each
+# Allow changes to ALL fields
+sub showPool($) {
+  my $pool = new NetAddr::IP $_[0];
+  print qq(Listing pool $pool:\n<table border=1>
+<form action=admin.cgi method=POST>
+<input type=hidden name=action value=updatepool>
+<tr><td align=right>Customer ID:</td><td><input name=custid></td></tr>
+<tr><td align=right>Customer location:</td><td><input name=city></td></tr>
+<tr><td align=right>Type:</td><td><select name=type><option selected>-</option>\n);
+
+  $sth = $ip_dbh->prepare("select type,listname from alloctypes where type like '_i' order by listorder");
+  $sth->execute;
+  while (my @data = $sth->fetchrow_array) {
+    print "<option value='$data[0]'>$data[1]</option>\n";
+  }
+
+  print qq(</select></td></tr>
+<tr><td align=right>Available?</td><td><input type=checkbox value=y></td></tr>
+<tr><td align=right>Description/name:</td><td><input name=desc size=40></td></tr>
+<tr><td align=right>Notes:</td><td><textarea name=notes rows=3 cols=40></textarea></td></tr>
+<tr><td colspan=2 align=center><input type=submit value="Update"></td></tr>
+).
+	"</table>Update the following record:<table border=1>\n";
+  $sth = $ip_dbh->prepare("select pool,ip,custid,city,type,available,description,notes from poolips where pool='$pool' order by ip");
+  $sth->execute;
+  while (my @data = $sth->fetchrow_array) {
+    print qq(<tr><td><input type=radio name=ip value="$data[1]">$data[1]</td>).
+	"<td>$data[2]</td><td>$data[3]</td><td>$data[4]</td>".
+	"<td>$data[5]</td><td>$data[6]</td><td>$data[7]</td></tr>\n";
+  }
+  print "</form></table>\n";
+}
Index: tags/2.5/cgi-bin/allocate.pl
===================================================================
--- tags/2.5/cgi-bin/allocate.pl	(revision 381)
+++ tags/2.5/cgi-bin/allocate.pl	(revision 381)
@@ -0,0 +1,104 @@
+#!/usr/bin/perl
+# Shell-based script to allocate arbitrary block
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+
+use strict;
+use warnings;
+#use CGI::Carp qw(fatalsToBrowser);
+use Carp;
+use DBI;
+use CommonWeb qw(:ALL);
+use MyIPDB;
+use IBLink;
+use POSIX qw(ceil);
+use NetAddr::IP;
+
+use Sys::Syslog;
+
+openlog "IPDBshell","pid","local2";
+
+# Collect the username from the environment.  If undefined, something
+# is Officially Hosed.
+my $authuser;
+if (!defined($ENV{'USER'})) {
+  die "Bad environment!  USER not defined.\n";
+} else {
+  $authuser = $ENV{'USER'};
+}
+
+# Why not a global DB handle?  (And a global statement handle, as well...)
+# Use the connectDB function, otherwise we end up confusing ourselves
+my $ip_dbh;
+my $sth;
+my $errstr;
+($ip_dbh,$errstr) = connectDB_My;
+if (!$ip_dbh) {
+  printAndExit("Failed to connect to database: $errstr\n");
+}
+checkDBSanity($ip_dbh);
+initIPDBGlobals($ip_dbh);
+
+# Hokay, now we can start to handle the allocation.
+
+my ($cidr, $type, $custid, $city, $desc, $alloc_from);
+# Check ARGV.  We need some information to determine what to allocate.
+if (!$ARGV[1]) {
+  # Usage message
+  print "Usage:  allocate.pl [IP/subnet] [Type] [CustID] [City] [\"Description\"]\n".
+	"	Further information can be entered via the web interface\n";
+  exit;
+} else {
+  $cidr = new NetAddr::IP "$ARGV[0]";
+  $sth = $ip_dbh->prepare("select cidr from freeblocks where cidr >>='$cidr'");
+  $sth->execute;
+  my @data = $sth->fetchrow_array;
+# User deserves errors if user can't be bothered to find the free block first.
+  die "Can't allocate from outside a free block!!\n"
+	if !$data[0];
+  $alloc_from = new NetAddr::IP $data[0];
+  $sth->finish;
+  $type = $ARGV[1];
+  if (!$ARGV[4]) {
+    # Default desc
+    $desc = "DEFAULT: $disp_alloctypes{$type}";
+  } else {
+    $desc = $ARGV[4];
+  }
+  if (!$ARGV[3]) {
+    # Default city
+    $sth = $ip_dbh->prepare("select city from routed where cidr >>='$cidr'");
+    $sth->execute;
+    my @data = $sth->fetchrow_array;
+    $city = $data[0];
+    $sth->finish;
+  } else {
+    $city = $ARGV[3];
+  }
+  if (!$ARGV[2]) {
+    # Default custid - make it REAL obvious.
+    $custid = "FIXME";
+  } else {
+    $custid = $ARGV[2];
+  }
+}
+
+print "Allocating $cidr as $type to $custid in $city: '$desc'\n";
+
+my ($code,$msg) = allocateBlock($ip_dbh, $cidr, $alloc_from, $custid, $type, $city,
+	$desc, '', '');
+
+if ($code eq 'OK') {
+  print "Allocation OK!\n";
+  syslog "notice", "($authuser) Allocated '$cidr' to '$custid' as '$type'";
+} else {
+  print "Allocation failed!  IPDB::allocateBlock said:\n$msg\n";
+  syslog "err", "($authuser) Allocation of '$cidr' to '$custid' as '$type' failed: '$msg'";
+}
+
+# Close it down.
+finish($ip_dbh);
Index: tags/2.5/cgi-bin/checkcusts.pl
===================================================================
--- tags/2.5/cgi-bin/checkcusts.pl	(revision 381)
+++ tags/2.5/cgi-bin/checkcusts.pl	(revision 381)
@@ -0,0 +1,67 @@
+#!/usr/bin/perl
+# Check all customer IDs to see which are invalid
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+# Copyright (C) 2004-2006 Kris Deugau
+
+use DBI;
+use IPDB 2.0 qw(:ALL);
+
+# We'll be hosing the server with several thousand queries.  We
+# REALLY don't want the overhead and load of opening a new connection
+# for each query.
+#use CustIDCK;
+
+use NetAddr::IP;
+
+$priv1 = new NetAddr::IP '10.0.0.0/8';
+$priv2 = new NetAddr::IP '172.16.0.0/12';
+$priv3 = new NetAddr::IP '192.168.0.0/16';
+
+print "Content-type: text/plain\n\n";
+
+($dbh,$errstr) = connectDB("ipdb", "ipdb", "ipdbpwd");
+$IDH = DBI->connect ("DBI:Pg:host=billing;dbname=custids", "cidcheck", "c1dch4ck");
+
+$sth = $dbh->prepare("select distinct def_custid from alloctypes where listorder >=40");
+$sth->execute;
+while (@data = $sth->fetchrow_array) {
+  push @def_custids, $data[0];
+}
+$sth = $dbh->prepare("select cidr,custid from searchme where not (custid='6750400') ".
+	"and not (custid='STAFF') order by cidr");
+#$sth = $dbh->prepare("select cidr,custid from searchme order by cidr");
+$sth->execute;
+
+$IDS = $IDH->prepare("select custid from custid where custid=?");
+
+$count = $bad = 0;
+while (@data = $sth->fetchrow_array) {
+  $cidr = new NetAddr::IP $data[0];
+  if ($cidr->within($priv1) or $cidr->within($priv2) or $cidr->within($priv3) or
+	(grep /$data[1]/, @def_custids)) {
+    # no-op.  we ignore these.
+  } else {
+    $count++;
+    $IDS->execute($data[1]);
+    $hr = $IDS->fetchrow_hashref();
+    if (!$hr->{custid}) {
+      print "  $data[0]\thas invalid CustID '$data[1]'\n";
+      $bad++;
+    }
+    $IDS->finish;
+  }
+}
+
+$IDH->disconnect;
+$dbh->disconnect;
+
+print "$count customer blocks, $bad bad.\n";
+exit 0;
+
+
+### Ported subs of sorts from CustIDCK.pm
Index: tags/2.5/cgi-bin/combineblocks.pl
===================================================================
--- tags/2.5/cgi-bin/combineblocks.pl	(revision 381)
+++ tags/2.5/cgi-bin/combineblocks.pl	(revision 381)
@@ -0,0 +1,75 @@
+#!/usr/bin/perl
+# ipdb/cgi-bin/combineblocks.pl
+# Quick hack to clean up mangled deallocations
+###
+# Revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+
+use strict;
+use warnings;
+#use CGI::Carp qw(fatalsToBrowser);
+use DBI;
+#use CommonWeb qw(:ALL);
+use MyIPDB;
+#use POSIX qw(ceil);
+use NetAddr::IP;
+
+#use Sys::Syslog;
+
+my $null = new NetAddr::IP "255.255.255.255/32";
+
+my @fbtypes;
+
+my ($dbh,$ret) = connectDB_My;
+initIPDBGlobals($dbh);
+my $sth;
+
+# Get the types of freeblocks
+$sth = $dbh->prepare("select distinct routed from freeblocks");
+$sth->execute;
+while (my @data = $sth->fetchrow_array) {
+  push @fbtypes, @data;
+}
+
+foreach my $type (@fbtypes) {
+
+  my $i=0;
+  my @compacted;
+
+  my $sth = $dbh->prepare("select cidr from freeblocks where routed='$type'");
+  $sth->execute;
+  while (my @data = $sth->fetchrow_array) {
+    my $temp = new NetAddr::IP $data[0];
+    @compacted = $temp->compact(@compacted);
+    $i++;
+  }
+
+#print $compacted[0];
+my $numcomp = @compacted;
+#print " $numcomp";
+
+  if ($i == $numcomp) {
+    print "No compactable blocks for type $type ($i free)\n";
+    next;
+  }
+  print "Type $type: $i free blocks to start, $numcomp to finish\n";
+
+  $sth = $dbh->prepare("select cidr from freeblocks where cidr << ?");
+  foreach my $cip (@compacted) {
+    $sth->execute("$cip");
+    if ($sth->rows > 0) {
+      print "  $cip combines from:\n";
+      while (my @data = $sth->fetchrow_array) {
+        print "    $data[0]\n";
+      }
+#    } else {
+#      print "  $cip does not compact\n";
+    }
+  }
+
+} # each @fbtype
+
+finish($dbh);
Index: tags/2.5/cgi-bin/consistency-check.pl
===================================================================
--- tags/2.5/cgi-bin/consistency-check.pl	(revision 381)
+++ tags/2.5/cgi-bin/consistency-check.pl	(revision 381)
@@ -0,0 +1,235 @@
+#!/usr/bin/perl
+# ipdb/cgi-bin/consistency-check.pl
+# Does full check to see if the data in the db is consistent and complete.
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+
+use DBI;
+use MyIPDB;
+use NetAddr::IP;
+
+print "Content-type: text/plain\n\n";
+
+($dbh,$errstr) = connectDB_My;
+# May as well.  We need a number of globals.
+initIPDBGlobals($dbh);
+
+print "Checking master containment...\n";
+
+# First check - make sure ALL routes and allocated blocks are part
+# of one of the master blocks.
+print "  Checking routed blocks:";
+$sth = $dbh->prepare("select cidr from routed");
+$sth->execute;
+$flag = '';
+ROUTED: while (@data = $sth->fetchrow_array) {
+  $cidr = new NetAddr::IP $data[0];
+  foreach $master (@masterblocks) {
+    if ($master->contains($cidr)) { next ROUTED; }
+  }
+  print "\n    $cidr not mastered";
+}
+print "$flag done.\n";
+
+# Next test:  All allocations must be part of a master.
+print "  Checking allocations:";
+$sth = $dbh->prepare("select cidr from allocations");
+$sth->execute;
+$flag = '';
+ALLOCATED: while (@data = $sth->fetchrow_array) {
+  $cidr = new NetAddr::IP $data[0];
+  foreach $master (@masterblocks) {
+    if ($master->contains($cidr)) { next ALLOCATED; }
+  }
+  print "\n    $cidr not mastered";
+  $flag = "\n ";
+}
+print "$flag done.\n";
+
+# Next:  free blocks
+print "  Checking freeblocks:";
+$sth = $dbh->prepare("select cidr from freeblocks order by cidr");
+$sth->execute;
+$flag = '';
+FREEBLOCK: while (@data = $sth->fetchrow_array) {
+  $cidr = new NetAddr::IP $data[0];
+  foreach $master (@masterblocks) {
+    if ($master->contains($cidr)) { next FREEBLOCK; }
+  }
+  print "\n    $cidr not mastered";
+  $flag = "\n ";
+}
+print "$flag done.\n";
+
+print "Done checking master containment.\n\n";
+
+print "Checking pool containment...\n";
+
+$sth = $dbh->prepare("select pool,ip from poolips order by ip");
+$sth->execute;
+while (@data = $sth->fetchrow_array) {
+  $pool = new NetAddr::IP $data[0];
+  $ip = new NetAddr::IP $data[1];
+  print "  IP $ip listed with incorrect pool $pool\n"
+    if !$pool->contains($ip);
+}
+$sth = $dbh->prepare("select distinct pool from poolips order by pool");
+$sth->execute;
+while (@data = $sth->fetchrow_array) {
+  $sth2 = $dbh->prepare("select cidr from allocations where cidr='$data[0]'");
+  $sth2->execute;
+  print "  Pool $data[0] does not exist in allocations table\n"
+    if (($sth2->fetchrow_array)[0] eq '');
+}
+
+print "Done checking pool containment.\n\n";
+
+print "Checking block-alignment consistency...\n";
+
+# Block alignment consistency:  All allocated+free blocks within a master
+# must NOT overlap, and they must show no gaps.
+# eg, if we have blocks:
+#  master is 192.168.2.0/24
+#  allocated 192.168.2.0/29, 192.168.2.8/29, 192.168.2.64/26
+#  free 192.168.2.16/28, 192.168.2.32/27, 192.168.2.128/25
+# then we're OK, but if any one of the allocated or free blocks is missing,
+# something b0rked.
+
+# (select cidr from allocations where cidr <<= '$master') union
+#  (select cidr from freeblocks where cidr <<= '$master')
+#  order by cidr
+
+foreach $master (@masterblocks) {
+  print "  Master $master:\n";
+  $prev = $master;
+  $sth = $dbh->prepare("(select network(cidr) as net, broadcast(cidr) as bcast ".
+	"from allocations where cidr <<= '$master' and type not like '_c') ".
+	"union (select network(cidr) as net, broadcast(cidr) as bcast ".
+	"from freeblocks where cidr <<= '$master' and not (routed='c')) order by net");
+  $sth->execute;
+
+  while (@data = $sth->fetchrow_array) {
+    $cur = new NetAddr::IP $data[0];
+
+    if ($master->numeric == $prev->numeric) {
+      # check if cur starts with master
+      if ($cur->numeric > $prev->numeric) {
+        print "    Gap from start of master $master to first block $cur\n";
+      } elsif ($cur->numeric < $prev->numeric) {
+        print "    BIG problem!  Current block $cur begins before master $master!\n";
+      }
+    } else {
+      if ($cur->numeric < ($prev->numeric + 1)) {
+        print "    Block ".$prev->network." overlaps block $cur\n";
+      } elsif ($cur->numeric > ($prev->numeric + 1)) {
+        print "    Gap between end of block ".$prev->network." and block $cur\n";
+      }
+    }
+
+    $prev = $cur;
+    $prev--;
+
+  } # while (@data...)
+
+  $cur--;
+  $master--;
+  if ($cur->numeric ne $master->numeric) {
+    print "    Gap from $cur to end of master at $master\n";
+  }
+  $master++;
+  print "  done $master.\n";
+}
+
+print "Done checking block alignment.\n\n";
+
+print "Checking containment on container blocks...\n";
+# First, we need a list of containers.
+# Then, we check all of the contained blocks to see if they're within
+# the proper container.
+$sth = $dbh->prepare("select cidr from allocations where type like '_c' order by cidr");
+$sth->execute;
+$i=0;
+while (@data = $sth->fetchrow_array) {
+  $containers[$i++] = new NetAddr::IP $data[0];
+}
+print "  Checking general containment:";
+$sth = $dbh->prepare("select cidr from allocations where type like '_r' order by cidr");
+$sth->execute;
+$flag = '';
+CONTAINED: while (@data = $sth->fetchrow_array) {
+  $cidr = new NetAddr::IP $data[0];
+  foreach $container (@containers) {
+    next CONTAINED if $container->contains($cidr);
+  }
+  print "\n    $cidr not contained";
+  $flag = "\n ";
+}
+print "$flag done.\n";
+print "  Checking alignment:\n";
+foreach $container (@containers) {
+  print "    Container $container:\n";
+  $prev = $container;
+  $sth = $dbh->prepare("(select network(cidr) as net, broadcast(cidr) as bcast ".
+        "from allocations where cidr <<= '$container' and type like '_r') ".
+        "union (select network(cidr) as net, broadcast(cidr) as bcast ".
+        "from freeblocks where cidr <<= '$container' and not (routed='y' or routed='n')) ".
+	"order by net");
+    $sth->execute;
+
+  while (@data = $sth->fetchrow_array) {
+    $cur = new NetAddr::IP $data[0];
+
+    if ($container->numeric == $prev->numeric) {
+      # check if cur starts with master
+      if ($cur->numeric > $prev->numeric) {
+        print "      Gap from start of container $container to first block $cur\n";
+      } elsif ($cur->numeric < $prev->numeric) {
+        print "      BIG problem!  Current block $cur begins before container $container!\n";
+      }
+    } else {
+      if ($cur->numeric < ($prev->numeric + 1)) {
+        print "      Block ".$prev->network." overlaps block $cur\n";
+      } elsif ($cur->numeric > ($prev->numeric + 1)) {
+        print "      Gap between end of block ".$prev->network." and block $cur\n";
+      }
+    }
+
+    $prev = $cur;
+    $prev--;
+
+  } # while (@data...)
+
+  $cur--;
+  $container--;
+  if ($cur->numeric ne $container->numeric) {
+    print "      Gap from $cur to end of container at $container\n";
+  }
+  $container++;
+  print "    done $container.\n";
+
+}
+print "  done container alignment.\n";
+print "Done checking container containment.\n\n";
+
+print "Checking for correctness on 'defined' CustIDs:\n";
+# New check:  Make sure "defined" CustIDs are correct.
+$sth = $dbh->prepare("select cidr,type,custid from allocations where not (type='cn' or type like '_r') order by cidr");
+$sth->execute;
+while (@data = $sth->fetchrow_array) {
+  print "$data[0] ($disp_alloctypes{$data[1]}) has incorrect CustID $data[2]\n"
+	if $data[2] ne $def_custids{$data[1]};
+}
+print "Done predefined CustID correctness check.\n\n";
+
+print "Checking for customer blocks with 'bad' CustIDs:\n";
+# Make sure cn-type ("customer netblock") blocks have "real" CustIDs.
+$sth = $dbh->prepare("select cidr,type,custid from allocations where type='cn' and (custid='6750400' or custid='STAFF') order by cidr");
+$sth->execute;
+while (@data = $sth->fetchrow_array) {
+  print "cn block $data[0] has incorrect CustID $data[2]\n";
+}
+print "Done checking customer blocks\n";
Index: tags/2.5/cgi-bin/editcust.php
===================================================================
--- tags/2.5/cgi-bin/editcust.php	(revision 381)
+++ tags/2.5/cgi-bin/editcust.php	(revision 381)
@@ -0,0 +1,77 @@
+<?php
+/* editcust.php
+** Show customer data in form for editing.
+*****
+** SVN revision info
+** $Date$
+** SVN revision $Rev$
+** Last update by $Author$
+*****
+** Copyright (C) 2006 - Kris Deugau
+*/
+?>
+<html><head>
+<title>[IPDB admin] Edit customer data for rWHOIS export</title>
+</head>
+<body>
+<h2>Edit customer data for rWHOIS export</h2>
+<a href="/ip/cgi-bin/admin.cgi">Back</a> to admin<br>
+<a href="/ip/cgi-bin/list-cust.php">Back</a> to WHOIS customer list
+<form action=updatecust.php method=post>
+<?
+  $db_conn = pg_connect("dbname=ipdb user=ipdb password=ipdbpwd");
+
+  if (!$db_conn) {
+    echo ("<H1>Failed connection to db ipdb.  Can't continue.</H1>");
+    exit;
+  }
+
+// snag form data into local vars.  Not being excessively paranoid because we're
+// buried deep and this is a hack anyway.
+  $newcust = $_REQUEST['newcust'];
+  $custid = $_REQUEST['custid'];
+
+  if ($newcust==1) {
+    echo "New customer.  Creating blank record in the database...\n";
+    $qu = pg_exec ($db_conn, "INSERT INTO customers (custid) VALUES ('$custid')");
+  }
+
+  $qu = pg_exec ($db_conn, "SELECT * FROM customers WHERE custid='$custid'");
+  if (!$qu) {
+    echo "DB error - select on customers.  Save this page and tell Kris.<BR>";
+  }
+
+  $data = pg_fetch_array($qu, 0);
+
+  echo "<table border=2 cellpadding=2>".
+	"<tr><td align=right>CustID</td><td>$data[0]<input type=hidden name=custid value=$data[0]></td></tr>\n".
+	"<tr><td>Name</td><td><input name=name value=\"$data[1]\"></td></tr>\n".
+	"<tr><td>Street address</td><td><input name=addr1 value=\"$data[2]\"></td></tr>\n".
+	"<tr><td>Address 2</td><td><input name=addr2 value=\"$data[3]\"></td></tr>\n".
+	"<tr><td>City</td><td><input name=city value=\"$data[4]\"></td></tr>\n".
+	"<tr><td>Province</td><td><input name=prov value=\"$data[5]\"></td></tr>\n".
+	"<tr><td>Country</td><td><input name=country value=\"$data[6]\"></td></tr>\n".
+	"<tr><td>Postal code</td><td><input name=pocode value=\"$data[7]\"></td></tr>\n".
+	"<tr><td>Phone</td><td><input name=phone value=\"$data[8]\"></td></tr>\n".
+	"<tr><td>Tech handle</td><td><input name=tech value=\"$data[9]\"></td></tr>\n".
+	"<tr><td>Abuse handle</td><td><input name=abuse value=\"$data[10]\"></td></tr>\n".
+	"<tr><td>Admin handle</td><td><input name=admin value=\"$data[11]\"></td></tr>\n".
+	"<tr><td>Default rDNS</td><td><input name=rdns value=\"$data[12]\"></td></tr>\n".
+	"<tr><td>\"Special\"</td><td><textarea name=special rows=4 cols=40>$data[13]</textarea></td></tr>\n".
+	'<tr><td colspan=2 align=center><input type=submit value="Add/update customer data"></td></tr>'.
+	"\n</table></form><br>\n";
+
+?>
+<h3>Explanation for "Special" field:</h3>
+This is the field I've mangled into providing a custom WHOIS netname identifier for blocks tagged "SWIP".
+It may be removed later.
+<p>It's formatted like this, one line for each custom net name:
+<pre>NetName[CIDR block]: NET-NAME</pre>
+Example:
+<pre>NetName209.91.133.0/24: CYBERSUDBURY-1</pre>
+Note:
+<li>Spacing is important - there should only be ONE space, in between the colon and the net name.
+<li>The CIDR block name nust include all four octets - no short forms are accepted.
+<li>Net names must be all uppercase, and consist only of A-Z, 0-9, and - (same as for SWIPed net names).
+</body>
+</html>
Index: tags/2.5/cgi-bin/extras/db2rwhois.pl
===================================================================
--- tags/2.5/cgi-bin/extras/db2rwhois.pl	(revision 381)
+++ tags/2.5/cgi-bin/extras/db2rwhois.pl	(revision 381)
@@ -0,0 +1,345 @@
+#!/usr/bin/perl
+# -T
+# ipdb/cgi-bin/extras/db2rwhois.pl
+# Pull data from ipdb and mangle it into RWHOIS
+# Initial version 03/26/2004 kdeugau against IPDB v1
+###
+# Revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+# Copyright (C) 2004-2007 - Kris Deugau
+
+use strict;
+use warnings;
+use DBI;
+use NetAddr::IP;
+use MyIPDB;
+use File::Path 'rmtree';
+
+$ENV{"PATH"} = "/bin;/usr/bin";
+
+my $rwhoisDataPath = "/etc/rwhoisd";
+
+my @autharea;
+my $authrw;
+# Use the template file to allow us to keep persistent nodes aside from netblock data
+open AUTHTEMPLATE, "<$rwhoisDataPath/rwhoisd.auth_template";
+my $template_persist;
+while (<AUTHTEMPLATE>) {
+  next if /^##/;
+  $template_persist = 1 if /^[a-z]/i;
+  $autharea[0] .= $_;
+}
+
+my ($dbh,$msg) = connectDB_My;
+
+# For WHOIS purposes this may not be very useful.  YMMV, we'll see.
+#initIPDBGlobals($dbh);
+
+my @masterblocks;
+my %netnameprefix;
+
+# Get the list of live directories for potential deletion
+opendir RWHOISROOT, $rwhoisDataPath;
+my %rwhoisdirs;
+foreach (readdir RWHOISROOT) {
+  $rwhoisdirs{$_} = 1 if /^net-/;
+}
+closedir RWHOISROOT;
+
+# prefetch alloctype data
+my $sth = $dbh->prepare("select type,def_custid,arin_netname from alloctypes");
+$sth->execute;
+while (my @data = $sth->fetchrow_array) {
+  $netnameprefix{$data[0]} = $data[2];
+}
+
+# Get the list of masters to export
+my $msth = $dbh->prepare("select cidr,ctime,mtime from masterblocks where rwhois='y'");
+$msth->execute;
+
+# Prepare to select subblocks for each master
+# Make sure to remove the private netblocks from this,
+# no use or point in broadcasting our use of them.
+# Also remove the details of our "reserved CORE/WAN" blocks;  they're not critical.
+my $ssth = $dbh->prepare("select cidr,custid,type,city,description,createstamp,modifystamp,swip ".
+	"from allocations where ".
+	"not (cidr <<= '192.168.0.0/16') and ".
+	"not (cidr <<= '172.16.0.0/12') and ".
+	"not (cidr <<= '10.0.0.0/8') and ".
+	"not (type = 'wr') and ".
+	"masklen(cidr) <=30 and ".
+	"cidr <<= ?");
+
+# Customer data, for those rare blocks we really need to delegate.
+my $custsth = $dbh->prepare("select name,street,city,province,country,pocode,phone,tech_handle,special ".
+	"from customers where custid=?");
+
+# Fill in data about our master blocks as allocated from ARIN
+# We open separate files for each of these as appropriate.
+# Changes in master blocks are treated as complete new masters - since we're exporting
+# all data every time, this isn't so terrible as it might seem.
+my $i=0;
+while (my @data = $msth->fetchrow_array()) {
+
+  $masterblocks[$i] = new NetAddr::IP $data[0];
+  my ($ctime,undef) = split /\s/, $data[1];
+  my ($mtime,undef) = split /\s/, $data[2];
+
+  print "$masterblocks[$i] $ctime $mtime\n";
+
+  my $date;
+  chomp ($date = `/bin/date +"%Y-%m-%d"`);
+
+  my $rwnet = "net-".$masterblocks[$i]->addr."-".$masterblocks[$i]->masklen;
+
+  # unflag the directory for deletion.  Whee!  Roundabout!
+  delete $rwhoisdirs{$rwnet};
+
+# Hokay.  Gonna do checks *here* to see if we need to create new master trees
+  my $netdatadir = "$rwhoisDataPath/$rwnet";
+  if (! -e $netdatadir) {
+    print " New master $masterblocks[$i]!\n";
+    print "  Creating directories...\n";
+    mkdir $netdatadir;
+    mkdir "$netdatadir/attribute_defs";
+    mkdir "$netdatadir/data";
+    mkdir "$netdatadir/data/network";
+    mkdir "$netdatadir/data/org";
+    mkdir "$netdatadir/data/referral";
+
+    my $serial;
+    chomp ($serial = `/bin/date '+%Y%m%d'000000000`);
+
+    print "  Creating SOA...\n";
+    open SOAFILE, ">$netdatadir/soa";
+    print SOAFILE qq(Serial-Number: $serial
+Refresh-Interval: 3600
+Increment-Interval: 1800
+Retry-Interval: 1800
+Time-To-Live: 86400
+Primary-Server: rwhois.example.com:4321
+Hostmaster: dns\@example.com
+);
+    close SOAFILE;
+
+    print "  Creating Schema...\n";
+    open SCHEMAFILE, ">$netdatadir/schema";
+    print SCHEMAFILE qq(name: network
+attributedef: $rwnet/attribute_defs/network.tmpl
+dbdir: $rwnet/data/network
+Schema-Version: $serial
+---
+name: organization
+attributedef: $rwnet/attribute_defs/org.tmpl
+dbdir: $rwnet/data/org
+description: Organization object
+Schema-Version: $serial
+---
+name: referral
+attributedef:$rwnet/attribute_defs/referral.tmpl
+dbdir:$rwnet/data/referral
+Schema-Version: $serial
+);
+    close SCHEMAFILE;
+
+    print "  Copying template files...\n";
+    qx { /bin/cp $rwhoisDataPath/skel/attribute_defs/* $netdatadir/attribute_defs/ };
+
+    print "  Creating org data...\n";
+    open ORGDATAFILE, ">$netdatadir/data/org/friendlyisp.txt";
+    print ORGDATAFILE qq(ID: NETBLK-ISP.$masterblocks[$i]
+Auth-Area: $masterblocks[$i]
+Org-Name: Friendly ISP
+Street-Address: 123 4th Street
+City: Anytown
+State: ON
+Postal-Code: H0H 0H0
+Country-Code: CA
+Phone: 000-555-1234
+Created: 20040308
+Updated: 20040308
+);
+    close ORGDATAFILE;
+
+    # Generate auth_area record, and add it to the array.
+    $authrw = 1;	# Flag for rewrite and daemon reload/restart
+
+  } # new master
+
+  # do this for all masters, so that we can use this array to export the data
+  # to rwhoisd.auth_area later if we need to
+  push @autharea, qq(type:master
+name:$masterblocks[$i]
+data-dir: $rwnet/data
+schema-file: $rwnet/schema
+soa-file: $rwnet/soa
+);
+
+  # Recreate the net-nnn.nnn.nnn.nnn-nn.txt data file
+  my $masterfilename = "$rwnet/data/network/".$masterblocks[$i]->addr."-".$masterblocks[$i]->masklen.".txt";
+
+  open MASTERFILE,">$rwhoisDataPath/$masterfilename";
+
+  print MASTERFILE "ID: NETBLK-ISP.$masterblocks[$i]\n".
+  	"Auth-Area: $masterblocks[$i]\n".
+	"Network-Name: ISP-".$masterblocks[$i]->network."\n".
+	"IP-Network: $masterblocks[$i]\n".
+	"IP-Network-Block: ".$masterblocks[$i]->range."\n".
+	"Org-Name: Friendly ISP\n".
+	"Street-Address: 123 4th Street\n".
+	"City: Anytown\n".
+	"StateProv: Ontario\n".
+	"Postal-Code: H0H 0H0\n".
+	"Country-Code: CA\n".
+	"Tech-Contact: ISP-ARIN-HANDLE\n".
+	"Created: $ctime\n".
+	"Updated: $mtime\n".
+	"Updated-By: noc\@example.com\n";
+
+  # And now the subblocks
+  $ssth->execute("$masterblocks[$i]");
+  while (my ($cidr, $custid, $type, $city, $desc, $ctime, $mtime, $swip) = $ssth->fetchrow_array) {
+
+# We get master block info from @masterblocks.
+ # ID: NETBLK-ISP.10.0.0.0/8
+ # Auth-Area: 10.0.0.0/8
+ # Network-Name: ISP-10.0.2.144
+ # IP-Network: 10.0.2.144.144/29
+ # IP-Network-Block: 10.0.2.144 - 10.0.2.151
+ # Organization: WidgetCorp
+ # Tech-Contact: bob@widgetcorp.com
+ # Admin-Contact: ISP-ARIN-HANDLE
+ # Created: 20040314
+ # Updated: 20040314
+ # Updated-By: noc@example.com
+
+    # Get the "full" network number
+    my $net = new NetAddr::IP $cidr;
+
+# Assumptions:  All data in ipdb is public
+# If not, we need another field to indicate "public/private".
+
+# cidr custid type city description notes maskbits
+
+# Fill in a generic entry for nameless allocations
+if ($desc =~ /^\s*$/) { $desc = 'Friendly ISP'; }
+
+    # Fix up datestamps.  We don't *really* need sub-microsecond resolution on our exports...
+    ($ctime) = ($ctime =~ /^(\d+-\d+-\d+)\s+/);
+    ($mtime) = ($mtime =~ /^(\d+-\d+-\d+)\s+/);
+
+# Notes:
+# Network-name should contain some component of "description"
+# Cust address/contact data should be included;  NB, no phone for ARIN!
+#  network:ID: NET-WIDGET
+#  network:Network-Name: WIDGET			[IPDB description, sort of]
+#  network:IP-Network: 10.1.1.0/24
+#  network:Org-Name: Widget Corp		[Cust name;  from billing?]
+#  network:Street-Address: 211 Oak Drive	[May need more than one line, OR...]
+#  network:City: Pineville			[...this line...]
+#  network:StateProv: WI			[...and this line...]
+#  network:Postal-Code: 48888			[...and this line]
+#  network:Country-Code: US
+#  network:Tech-Contact: BZ142-MYRWHOIS		[ARIN handle?]
+#  network:Updated: 19991221			[timestamp from db]
+#  network:Updated-By: jo@myrwhois.net		[noc@example, since that's our POC for IP netspace issues]
+#  network:Class-Name:network			[Provided by rWHOIS protocol]
+
+    my $netname = $netnameprefix{$type};
+
+    if ($swip eq 'n') {
+      print MASTERFILE "---\nID: NETBLK-ISP.$masterblocks[$i]\n".
+	"Auth-Area: $masterblocks[$i]\n".
+	"Network-Name: $netname-".$net->network."\n".
+	"IP-Network: $net\n".
+	"IP-Network-Block: ".$net->range."\n".
+	"Org-Name: Friendly ISP\n".
+	"Street-Address: 123 4th Street\n".
+	"City: Anytown\n".
+	"StateProv: Ontario\n".
+	"Postal-Code: H0H 0H0\n".
+	"Country-Code: CA\n".
+	"Tech-Contact: ISP-ARIN-HANDLE\n".
+	"Created: $ctime\n".
+	"Updated: $mtime\n".
+	"Updated-By: noc\@example.com\n";
+    } else {
+      $custsth->execute($custid);
+      my ($name, $street, $city, $prov, $country, $pocode, $phone, $tech, $special) = $custsth->fetchrow_array;
+      $custsth->finish;
+      if ($special && $special =~ /NetName/ && $special =~ /$cidr/) {
+	($netname) = ($special =~ /NetName$cidr: ([A-Z0-9_-]+)/);
+      } else {
+	$netname .= "-".$net->network;
+      }
+      print MASTERFILE "---\nID: NETBLK-ISP.$masterblocks[$i]\n".
+  	"Auth-Area: $masterblocks[$i]\n".
+	"Network-Name: $netname\n".
+	"IP-Network: $net\n".
+	"IP-Network-Block: ".$net->range."\n".
+	"Org-Name: ".($name ? $name : 'Friendly ISP')."\n".
+	"Street-Address: ".($street ? $street : '123 4th Street')."\n".
+	"City: ".($city ? $city : 'Anytown')."\n".
+	"StateProv: ".($prov ? $prov : 'Ontario')."\n".
+	"Postal-Code: ".($pocode ? $pocode : 'H0H 0H0')."\n".
+	"Country-Code: ".($country ? $country : 'CA')."\n".
+	"Tech-Contact: ".($tech ? $tech : 'ISP-ARIN-HANDLE')."\n".
+	"Created: $ctime\n".
+	"Updated: $mtime\n".
+	"Updated-By: noc\@example.com\n";
+    } # swip
+
+  } # while $ssth->fetchrow_array()
+
+  close MASTERFILE;
+
+  $i++;
+} # while $msth->fetchrow_array()
+
+# Now we see if there's obsolete netdata directories to be deleted,
+# and therefore an auth-area file to regenerate
+foreach my $netdir (keys %rwhoisdirs) {
+  print "deleting obsolete directory $netdir...\n";
+  rmtree ( "$rwhoisDataPath/$netdir", { verbose => 1, error => \my $errlist } );
+  for my $diag (@$errlist) {
+    my ($file, $message) = each %$diag;
+    if ($file eq '') {
+      print "general error: $message\n";
+    }
+  }
+  $authrw = 1;	# there's probably a more efficient place to put this.  Feh.
+}
+
+# Regenerate rwhoisd.auth_area if needed
+if ($authrw) {
+  print "Regenerating auth_area\n";
+  open RWHOISDAUTH, ">$rwhoisDataPath/rwhoisd.auth_area";
+  print RWHOISDAUTH "# WARNING: This file is autogenerated!  Any static nodes should\n".
+		"# be entered in /etc/rwhoisd/rwhoisd.auth_template\n";
+  if ($template_persist) {
+    print RWHOISDAUTH shift @autharea;
+    print RWHOISDAUTH "---\n";
+  }
+  # feh.  we need to know when we're at the end of the loop, because then
+  # we DON'T want to write the separator...
+  for (;@autharea;) {	# my head hurts.
+    print RWHOISDAUTH shift @autharea;
+    print RWHOISDAUTH "---\n" if @autharea;
+  }
+  close RWHOISDAUTH;
+
+  # restart/reload rwhoisd
+  if (-e "$rwhoisDataPath/rwhoisd.pid") {	# no pidfile, no restart.
+    print "Restarting rwhoisd\n";
+    open PIDFILE, "<$rwhoisDataPath/rwhoisd.pid";
+    my ($rwpid) = (<PIDFILE> =~ /^(\d+)/);
+    close PIDFILE;
+    kill 'HUP', $rwpid;
+  }
+}
+
+# and finally
+$dbh->disconnect;
Index: tags/2.5/cgi-bin/extras/network.tmpl
===================================================================
--- tags/2.5/cgi-bin/extras/network.tmpl	(revision 381)
+++ tags/2.5/cgi-bin/extras/network.tmpl	(revision 381)
@@ -0,0 +1,306 @@
+attribute:       Network-Name
+attribute-alias: NN
+description:     Network name
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           EXACT
+type:            TEXT
+---
+attribute:       IP-Network
+attribute-alias: IPN
+description:     IP network in CIDR notation
+is-primary-key:  TRUE
+is-required:     TRUE
+is-repeatable:   TRUE
+is-multi-line:   FALSE
+is-hierarchical: TRUE
+index:           CIDR
+type:            TEXT
+---
+attribute:       IP-Network-Block
+attribute-alias: IPNB
+description:     IP address range
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Org-Name
+attribute-alias: ORG
+description:     Organization
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Street-Address
+attribute-alias: SA
+description:     Street address
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       City
+attribute-alias: CITY
+description:     City
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       StateProv
+attribute-alias: SP
+description:     State or province
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Postal-Code
+attribute-alias: ZIP
+description:     ZIP or postal code
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Country-Code
+attribute-alias: CC
+description:     Two-letter country code
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Admin-Contact
+attribute-alias: AC
+description:     Administrative contact
+is-primary-key:  FALSE
+is-required:     FALSE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            ID
+---
+attribute:       Tech-Contact
+attribute-alias: TC
+description:     Technical contact
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            ID
+---
+attribute:       Abuse-Contact
+attribute-alias: NA
+description:     Network Abuse contact
+is-primary-key:  FALSE
+is-required:     FALSE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            ID
+---
+attribute:       Created
+attribute-alias: CR
+description:     Creation date
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Updated-By
+attribute-alias: UB
+description:     Updated by
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+attribute:       Network-Name
+attribute-alias: NN
+description:     Network name
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           EXACT
+type:            TEXT
+---
+attribute:       IP-Network
+attribute-alias: IPN
+description:     IP network in CIDR notation
+is-primary-key:  TRUE
+is-required:     TRUE
+is-repeatable:   TRUE
+is-multi-line:   FALSE
+is-hierarchical: TRUE
+index:           CIDR
+type:            TEXT
+---
+attribute:       IP-Network-Block
+attribute-alias: IPNB
+description:     IP address range
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Org-Name
+attribute-alias: ORG
+description:     Organization
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Street-Address
+attribute-alias: SA
+description:     Street address
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       City
+attribute-alias: CITY
+description:     City
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       StateProv
+attribute-alias: SP
+description:     State or province
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Postal-Code
+attribute-alias: ZIP
+description:     ZIP or postal code
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Country-Code
+attribute-alias: CC
+description:     Two-letter country code
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Admin-Contact
+attribute-alias: AC
+description:     Administrative contact
+is-primary-key:  FALSE
+is-required:     FALSE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            ID
+---
+attribute:       Tech-Contact
+attribute-alias: TC
+description:     Technical contact
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            ID
+---
+attribute:       Abuse-Contact
+attribute-alias: NA
+description:     Network Abuse contact
+is-primary-key:  FALSE
+is-required:     FALSE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            ID
+---
+attribute:       Created
+attribute-alias: CR
+description:     Creation date
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
+---
+attribute:       Updated-By
+attribute-alias: UB
+description:     Updated by
+is-primary-key:  FALSE
+is-required:     TRUE
+is-repeatable:   FALSE
+is-multi-line:   FALSE
+is-hierarchical: FALSE
+index:           NONE
+type:            TEXT
Index: tags/2.5/cgi-bin/extras/rwhois-config
===================================================================
--- tags/2.5/cgi-bin/extras/rwhois-config	(revision 381)
+++ tags/2.5/cgi-bin/extras/rwhois-config	(revision 381)
@@ -0,0 +1,40 @@
+Notes on requirements for "proper" rWHOIS configuration
+
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+
+- Each "master" block has its own auth area
+- Only the network records are used;  referral records may be useful
+  someday but not yet.
+- "Org" data is stored in the network records directly.  Doing
+  otherwise more or less works, but it's tedious and headache-ful
+  to retrieve it.  This according to an ARIN staffer who gets
+  paid to check rWHOIS servers for "correctness".
+- Suitable templates and scripts for adding a fresh new master
+  (manually) are included in this directory.  Note that adding a
+  master automagically is unlikely to happen soon.  (As of 2006-04-06)
+- The "register" directive should be disabled;  we're not using it.
+Notes on requirements for "proper" rWHOIS configuration
+
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+
+- Each "master" block has its own auth area
+- Only the network records are used;  referral records may be useful
+  someday but not yet.
+- "Org" data is stored in the network records directly.  Doing
+  otherwise more or less works, but it's tedious and headache-ful
+  to retrieve it.  This according to an ARIN staffer who gets
+  paid to check rWHOIS servers for "correctness".
+- Suitable templates and scripts for adding a fresh new master
+  (manually) are included in this directory.  Note that adding a
+  master automagically is unlikely to happen soon.  (As of 2006-04-06)
+- The "register" directive should be disabled;  we're not using it.
Index: tags/2.5/cgi-bin/freespace.pl
===================================================================
--- tags/2.5/cgi-bin/freespace.pl	(revision 381)
+++ tags/2.5/cgi-bin/freespace.pl	(revision 381)
@@ -0,0 +1,55 @@
+#!/usr/bin/perl
+# ipdb/cgi-bin/freespace.pl
+# Quick hack to calculate aggregate free IP space, and statistic-ify the blocks.
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+# Copyright (C) 2004-2006 - Kris Deugau
+
+use DBI;
+use MyIPDB;
+use NetAddr::IP;
+
+($dbh,$errstr) = connectDB_My;
+
+print "Content-type: text/plain\n\n";
+
+$tnumfree = $bigrfree = $bigufree = 0;
+
+$sql = 'select * from freeblocks where ';
+# General counts first
+if ($ARGV[0]) {
+  $sql .= 'maskbits >= $ARGV[0] and ';
+}
+$sql .= "not (cidr <<= '192.168.0.0/16') ".
+		"and not (cidr <<= '172.16.0.0/12') ".
+		"and not (cidr <<= '10.0.0.0/8') ";
+$sql .= "order by maskbits desc";
+
+$sth = $dbh->prepare($sql);
+$sth->execute;
+while (@data = $sth->fetchrow_array) {
+  # cidr,maskbits,city,routed
+  $tnumfree++;
+  $numfree{"$data[1]"}++;
+}
+
+print "Free block counts:\n";
+foreach $size (sort {$a cmp $b} keys %numfree) {
+  print "/$size: $numfree{$size}\n";
+}
+print "\n";
+
+for ($i=30; $i>16; $i--) {
+  $j = $i-1;
+  $numfree{"$j"} += int $numfree{"$i"}/2;
+  $numfree{"$i"} -= (int $numfree{"$i"}/2)*2;
+}
+
+print "Aggregate free space:\n";
+foreach $size (sort {$a cmp $b} keys %numfree) {
+  print "/$size: $numfree{$size}\n";
+}
Index: tags/2.5/cgi-bin/ipdb.psql
===================================================================
--- tags/2.5/cgi-bin/ipdb.psql	(revision 381)
+++ tags/2.5/cgi-bin/ipdb.psql	(revision 381)
@@ -0,0 +1,213 @@
+DROP DATABASE ipdb;
+
+CREATE USER ipdb WITH PASSWORD 'ipdbpwd';
+
+CREATE DATABASE ipdb;
+
+-- Need to do this or our triggers don't work.  Why do we need to do this?
+CREATE FUNCTION "plpgsql_call_handler" () RETURNS language_handler AS '$libdir/plpgsql' LANGUAGE C;
+CREATE TRUSTED LANGUAGE "plpgsql" HANDLER "plpgsql_call_handler";
+
+UPDATE pg_database SET datdba=(SELECT usesysid FROM pg_shadow WHERE usename='ipdb')
+	WHERE datname='ipdb';
+
+\connect ipdb ipdb
+
+CREATE TABLE "customers" (
+	"custid" character varying(16) DEFAULT '' NOT NULL,
+	"name" character varying(64),
+	"street" character varying(25),
+	"street2" character varying(25),
+	"city" character varying(30),
+	"province" character(2),
+	"country" character(2),
+	"pocode" character varying(7),
+	"phone" character varying(15),
+	"tech_handle" character varying(50),
+	"abuse_handle" character varying(50),
+	"admin_handle" character varying(50),
+	"def_rdns" character varying(40),
+	"special" text,
+	Constraint "customers_pkey" Primary Key ("custid")
+);
+
+REVOKE ALL on "customers" from PUBLIC;
+GRANT ALL on "customers" to "ipdb";
+
+CREATE TABLE "masterblocks" (
+	"cidr" cidr DEFAULT '255.255.255.255/32' NOT NULL PRIMARY KEY,
+	"ctime" timestamp DEFAULT now(),
+	"mtime" timestamp DEFAULT now(),
+	"rwhois" character(1) DEFAULT 'n' NOT NULL
+);
+
+REVOKE ALL on "masterblocks" from PUBLIC;
+GRANT ALL on "masterblocks" to "ipdb";
+
+CREATE TABLE "routed" (
+	"cidr" cidr DEFAULT '255.255.255.255/32' NOT NULL PRIMARY KEY,
+	"maskbits" integer DEFAULT 128,
+	"city" character varying(30) DEFAULT '',
+	"ctime" timestamp DEFAULT now()
+);
+
+REVOKE ALL on "routed" from PUBLIC;
+GRANT ALL on "routed" to "ipdb";
+GRANT SELECT on "routed" to "ipdb";
+
+CREATE TABLE "temp" (
+	"ofs" integer
+);
+
+REVOKE ALL on "temp" from PUBLIC;
+GRANT ALL on "temp" to "ipdb";
+
+CREATE TABLE "freeblocks" (
+	"cidr" cidr DEFAULT '255.255.255.255/32' NOT NULL PRIMARY KEY,
+	"maskbits" integer DEFAULT 128,
+	"city" character varying(30) DEFAULT '',
+	"routed" character(1) DEFAULT 'n'
+);
+
+REVOKE ALL on "freeblocks" from PUBLIC;
+GRANT ALL on "freeblocks" to "ipdb";
+
+CREATE TABLE "poolips" (
+	"pool" cidr DEFAULT '255.255.255.255/32' NOT NULL,
+	"ip" cidr DEFAULT '255.255.255.255/32' NOT NULL PRIMARY KEY,
+	"oldcustid" character varying(16) DEFAULT '' NOT NULL,
+	"city" character varying(30) DEFAULT '' NOT NULL,
+	"type" character(2) DEFAULT '' NOT NULL,
+	"available" character(1) DEFAULT 'y' NOT NULL,
+	"notes" text DEFAULT '' NOT NULL,
+	"description" character varying(64) DEFAULT '' NOT NULL,
+	"circuitid" character varying(128) DEFAULT '' NOT NULL,
+	"privdata" text DEFAULT '' NOT NULL,
+	"custid" character varying(16) DEFAULT '',
+	"createstamp" timestamp DEFAULT now(),
+	"modifystamp" timestamp DEFAULT now(),
+	CHECK (((available = 'y'::bpchar) OR (available = 'n'::bpchar)))
+);
+
+REVOKE ALL on "poolips" from PUBLIC;
+GRANT ALL on "poolips" to "ipdb";
+
+CREATE TABLE "allocations" (
+	"cidr" cidr DEFAULT '255.255.255.255/32' NOT NULL PRIMARY KEY,
+	"oldcustid" character varying(16) DEFAULT '',
+	"type" character(2) DEFAULT '',
+	"city" character varying(30) DEFAULT '',
+	"description" character varying(64) DEFAULT '',
+	"notes" text DEFAULT '',
+	"maskbits" integer DEFAULT 128,
+	"circuitid" character varying(128) DEFAULT '',
+	"createstamp" timestamp DEFAULT now(),
+	"modifystamp" timestamp DEFAULT now(),
+	"privdata" text DEFAULT '' NOT NULL,
+	"custid" character varying(16) DEFAULT '',
+	swip character(1) DEFAULT 'n'
+);
+
+REVOKE ALL on "allocations" from PUBLIC;
+GRANT ALL on "allocations" to "ipdb";
+
+CREATE VIEW "searchme" as SELECT allocations.cidr, allocations.custid, allocations."type", allocations.city, allocations.description, allocations.notes, allocations.oldcustid, allocations.circuitid FROM allocations UNION SELECT poolips.ip, poolips.custid, poolips.type, poolips.city, poolips.description, poolips.notes, poolips.oldcustid, poolips.circuitid FROM poolips;
+
+REVOKE ALL on "searchme" from PUBLIC;
+GRANT ALL on "searchme" to "ipdb";
+
+CREATE TABLE "alloctypes" (
+	"type" character(2) DEFAULT '' NOT NULL PRIMARY KEY,
+	"listname" character varying(40) DEFAULT '',
+	"dispname" character varying(40) DEFAULT '',
+	"listorder" integer DEFAULT 0,
+	"def_custid" character varying(16) DEFAULT '',
+	"arin_netname" character varying(20) DEFAULT 'ISP'
+);
+
+--
+-- Name: alloctypes; Type: TABLE DATA; Schema: public; Owner: ipdb
+--
+
+COPY "alloctypes" FROM stdin;
+cn	Customer netblock	Customer netblock	0		ISPCUST
+si	Static IP - Server pool	Server pool IP	20	6750400	ISP
+ci	Static IP - Cable	Static cable IP	21		ISP
+di	Static IP - DSL	Static DSL IP	22		ISP
+mi	Static IP - Dialup	Static dialup IP	23		ISP
+wi	Static IP - Wireless	Static wireless IP	24		ISP
+sd	Static Pool - Servers	Server pool	40	6750400	ISP
+cd	Static Pool - Cable	Cable pool	41	CBL-BUS	ISP-STATIC-CABLE
+dp	Static Pool - DSL	DSL pool	42	DSL-BUS	ISP-STATIC-DSL
+mp	Static Pool - Dialup	Static dialup pool	43	DIAL-BUS	ISP-STATIC-DIAL
+wp	Static Pool - Wireless	Static wireless pool	44	WL-BUS	ISP-STATIC-WIFI
+en	End-use netblock	End-use netblock	100	6750400	ISP
+me	Dialup netblock	Dialup netblock	101	DIAL-RES	ISP-DIAL
+de	Dynamic DSL block	Dynamic DSL block	102	DSL-RES	ISP-DSL
+ce	Dynamic cable block	Dynamic cable block	103	CBL-RES	ISP-CABLE
+we	Dynamic WiFi block	Dynamic WiFi block	104	WL-RES	ISP-WIFI
+ve	Dynamic VoIP block	Dynamic VoIP block	105	DYN-VOIP	ISP
+li	Static IP - LAN/POP	Static LAN/POP IP	190	NOC-VPN	ISP
+ai	Static IP - Management	Static management IP	191	NOC-VPN	ISP
+ld	Static Pool - LAN/POP	LAN pool	195	NOC-VPN	ISP
+ad	Static Pool - Management	Management pool	196	NOC-VPN	ISP
+in	Internal netblock	Internal netblock	199	6750400	ISP
+wc	Reserve for CORE/WAN blocks	CORE/WAN blocks	200	6750400	ISP
+pc	Reserve for dynamic-route DSL netblocks	Dynamic-route netblocks	201	6750400	ISP-STATIC-DSL
+ac	Reserve for ATM	ATM blocks	202	6750400	ISP
+fc	Reserve for fibre	Fibre blocks	203	6750400	ISP
+wr	CORE/WAN block	CORE/WAN block	220	6750400	ISP
+pr	Dynamic-route DSL netblock (cust)	Dynamic-route DSL (cust)	221		ISPCUST
+ar	ATM block	ATM block	222		ISP
+fr	Fibre	Fibre	223	ATM-BUS	ISP
+rm	Routing	Routed netblock	500	6750400	ISP
+mm	Master block	Master block	999	6750400	ISP
+\.
+
+REVOKE ALL on "alloctypes" from PUBLIC;
+GRANT ALL on "alloctypes" to "ipdb";
+
+CREATE TABLE "cities" (
+	"id" serial NOT NULL PRIMARY KEY,
+	"city" character varying(30) DEFAULT '' NOT NULL,
+	"routing" character(1) DEFAULT 'n' NOT NULL
+);
+
+REVOKE ALL on "cities" from PUBLIC;
+GRANT ALL on "cities" to "ipdb";
+
+--
+-- Trigger and matching function to update modifystamp on allocations, poolips
+--
+CREATE FUNCTION up_modtime () RETURNS OPAQUE AS '
+    BEGIN
+	NEW.modifystamp := ''now'';
+	RETURN NEW;
+    END;
+' LANGUAGE 'plpgsql';
+
+CREATE TRIGGER up_modtime BEFORE UPDATE ON allocations
+    FOR EACH ROW EXECUTE PROCEDURE up_modtime();
+
+CREATE TRIGGER up_modtime BEFORE UPDATE ON poolips
+    FOR EACH ROW EXECUTE PROCEDURE up_modtime();
+
+--
+-- User data table - required for proper ACLs
+--
+
+CREATE TABLE "users" (
+	"username" varchar(16) NOT NULL PRIMARY KEY,
+	"password" varchar(16) DEFAULT '',
+	"acl" varchar(16) DEFAULT 'b'
+);
+
+-- Default password is admin
+INSERT INTO users VALUES ('admin','luef5C4XumqIs','bacdsA');
+
+CREATE TABLE "dns" (
+	"ip" inet NOT NULL PRIMARY KEY,
+	"hostname" character varying(128),
+	"auto" character(1) DEFAULT 'y'
+);
+
Index: tags/2.5/cgi-bin/list-cust.php
===================================================================
--- tags/2.5/cgi-bin/list-cust.php	(revision 381)
+++ tags/2.5/cgi-bin/list-cust.php	(revision 381)
@@ -0,0 +1,83 @@
+<?php
+/* list-cust.php
+** List customer data in customers table, and allow editing, adding.  Delete
+** is sort of a null action and might Break Things.
+*****
+** SVN revision info
+** $Date$
+** SVN revision $Rev$
+** Last update by $Author$
+*****
+** Copyright (C) 2006 - Kris Deugau
+*/
+?>
+<html><head>
+<title>[IPDB admin] Customer data for rWHOIS export</title>
+</head>
+<body>
+<h2>Customer data for rWHOIS export</h2>
+<a href="/ip/cgi-bin/admin.cgi">Back</a> to admin
+<p><li>Click the CustID to edit information
+<li>Enter information below to add a new entry - it's best if this is done **BEFORE** flipping the "SWIP" option
+on the netblock.
+<form action=editcust.php method=POST>
+CustID:<input name=custid>
+<input type=submit value="Go to edit page for new customer">
+<input type=hidden name=newcust value=1>
+</form>
+<?
+  $db_conn = pg_connect("dbname=ipdb user=ipdb password=ipdbpwd");
+
+  if (!$db_conn) {
+    echo ("<H1>Failed connection to db ipdb.  Can't continue.</H1>");
+    exit;
+  }
+
+  $qu = pg_exec ($db_conn, "SELECT * FROM customers order by custid");
+
+  if (!$qu) {
+    echo "DB error - select on customers.  Save this page and tell Kris.<BR>";
+  }
+
+  print "<br>".pg_NumRows($qu)." customers.\n";
+
+  echo "<table border=2 cellpadding=2>";
+  echo "<tr><td width=150>CustID<br>(Click to update)</td>".
+       "<td>Name</td>".
+       "<td>Street address</td>".
+       "<td>Address 2<br>(if needed)</td>".
+       "<td>City</td>".
+       "<td>Province</td>".
+       "<td>Country</td>".
+       "<td>Postal code</td>".
+       "<td>Phone</td>".
+       "<td>Tech handle</td>".
+       "<td>Abuse handle</td>".
+       "<td>Admin handle</td>".
+       "<td>Default rDNS<br>(not used)</td>".
+       "<td>\"Special\"</td></tr>\n";
+
+  $row = 0;
+  for ($row=0; $row < pg_NumRows($qu); $row++) {
+    $data = pg_fetch_array($qu, $row);
+    echo '<tr><td><a href="editcust.php?custid='.$data[0].'&newcust=0">'.$data[0]."</a> &nbsp; ".
+	'<a href="updatecust.php?custid='.$data[0].'&deletecust=1">(delete)</a></td>'.
+	"<td>$data[1]</td>".
+	"<td>$data[2]</td>".
+	"<td>$data[3]</td>".
+	"<td>$data[4]</td>".
+	"<td>$data[5]</td>".
+	"<td>$data[6]</td>".
+	"<td>$data[7]</td>".
+	"<td>$data[8]</td>".
+	"<td>$data[9]</td>".
+	"<td>$data[10]</td>".
+	"<td>$data[11]</td>".
+	"<td>$data[12]</td>".
+	"<td>$data[13]</td></tr>\n";
+  }
+  echo "</table><br>\n";
+
+?>
+</body>
+</html>
Index: tags/2.5/cgi-bin/main.cgi
===================================================================
--- tags/2.5/cgi-bin/main.cgi	(revision 381)
+++ tags/2.5/cgi-bin/main.cgi	(revision 381)
@@ -0,0 +1,1345 @@
+#!/usr/bin/perl
+# ipdb/cgi-bin/main.cgi
+# Started munging from noc.vianet's old IPDB 04/22/2004
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+
+use strict;		
+use warnings;	
+use CGI::Carp qw(fatalsToBrowser);
+use DBI;
+use CommonWeb qw(:ALL);
+use MyIPDB;
+use CustIDCK;
+use POSIX qw(ceil);
+use NetAddr::IP;
+
+use Sys::Syslog;
+
+openlog "IPDB","pid","local2";
+
+# Collect the username from HTTP auth.  If undefined, we're in
+# a test environment, or called without a username.
+my $authuser;
+if (!defined($ENV{'REMOTE_USER'})) {
+  $authuser = '__temptest';
+} else {
+  $authuser = $ENV{'REMOTE_USER'};
+}
+
+syslog "debug", "$authuser active";
+
+# Why not a global DB handle?  (And a global statement handle, as well...)
+# Use the connectDB function, otherwise we end up confusing ourselves
+my $ip_dbh;
+my $sth;
+my $errstr;
+($ip_dbh,$errstr) = connectDB_My;
+if (!$ip_dbh) {
+  exitError("Database error: $errstr\n");
+}
+initIPDBGlobals($ip_dbh);
+
+# Headerize!  Make sure we replace the $$EXTRA0$$ bit as needed.
+printHeader('', ($IPDBacl{$authuser} =~ /a/ ?
+	'<td align=right><a href="/ip/cgi-bin/main.cgi?action=assign">Add new assignment</a>' : ''
+	));
+
+
+# Global variables
+my %webvar = parse_post();
+cleanInput(\%webvar);
+
+
+#main()
+
+if(!defined($webvar{action})) {
+  $webvar{action} = "<NULL>";	#shuts up the warnings.
+}
+
+if($webvar{action} eq 'index') {
+  showSummary();
+} elsif ($webvar{action} eq 'addmaster') {
+  if ($IPDBacl{$authuser} !~ /a/) {
+    printError("You shouldn't have been able to get here.  Access denied.");
+  } else {
+    open HTML, "<../addmaster.html";
+    print while <HTML>;
+  }
+} elsif ($webvar{action} eq 'newmaster') {
+
+  if ($IPDBacl{$authuser} !~ /a/) {
+    printError("You shouldn't have been able to get here.  Access denied.");
+  } else {
+
+    my $cidr = new NetAddr::IP $webvar{cidr};
+
+    print "<div type=heading align=center>Adding $cidr as master block....</div>\n";
+
+    my ($code,$msg) = addMaster($ip_dbh, $webvar{cidr});
+
+    if ($code eq 'FAIL') {
+      carp "Transaction aborted because $msg";
+      syslog "err", "Could not add master block '$webvar{cidr}' to database: '$msg'";
+      printError("Could not add master block $webvar{cidr} to database: $msg");
+    } else {
+      print "<div type=heading align=center>Success!</div>\n";
+      syslog "info", "$authuser added master block $webvar{cidr}";
+    }
+
+  } # ACL check
+
+} # end add new master
+
+elsif($webvar{action} eq 'showmaster') {
+  showMaster();
+}
+elsif($webvar{action} eq 'showrouted') {
+  showRBlock();
+}
+elsif($webvar{action} eq 'listpool') {
+  listPool();
+}
+
+# Not modified or added;  just shuffled
+elsif($webvar{action} eq 'assign') {
+  assignBlock();
+}
+elsif($webvar{action} eq 'confirm') {
+  confirmAssign();
+}
+elsif($webvar{action} eq 'insert') {
+  insertAssign();
+}
+elsif($webvar{action} eq 'edit') {
+  edit();
+}
+elsif($webvar{action} eq 'update') {
+  update();
+}
+elsif($webvar{action} eq 'delete') {
+  remove();
+}
+elsif($webvar{action} eq 'finaldelete') {
+  finalDelete();
+}
+
+# Default is an error.  It shouldn't be possible to easily get here.
+# The only way I can think of offhand is to just call main.cgi bare-
+# which is not in any way guaranteed to provide anything useful.
+else {
+  my $rnd = rand 500;
+  my $boing = sprintf("%.2f", rand 500);
+  my @excuses = ("Aether cloudy.  Ask again later.","The gods are unhappy with your sacrifice.",
+	"Because one of it's legs are both the same", "*wibble*",
+	"Hey! Stop pushing my buttons!", "I ain't done nuttin'", "9",
+	"8", "9", "10", "11", "12", "13", "14", "15", "16", "17");
+  printAndExit("Error $boing:  ".$excuses[$rnd/30.0]);
+}
+## Finally! Done with that NASTY "case" emulation!
+
+
+
+# Clean up IPDB globals, DB handle, etc.
+finish($ip_dbh);
+
+print qq(<div align=right style="position: absolute; right: 30px;">).
+	qq(<a href="/ip/cgi-bin/admin.cgi">Admin tools</a></div><br>\n)
+	if $IPDBacl{$authuser} =~ /A/;
+
+# We print the footer here, so we don't have to do it elsewhere.
+printFooter;
+# Just in case something waaaayyy down isn't in place
+# properly... we exit explicitly.
+exit;
+
+
+
+# args are: a reference to an array with the row to be printed and the 
+# class(stylesheet) to use for formatting.
+# if ommitting the class - call the sub as &printRow(\@array)
+sub printRow {
+  my ($rowRef,$class) = @_;
+
+  if (!$class) {
+    print "<tr>\n";
+  } else {
+    print "<tr class=\"$class\">\n";
+  }
+
+ELEMENT:  foreach my $element (@$rowRef) {
+    if (!defined($element)) {
+      print "<td></td>\n";
+      next ELEMENT;
+    }
+    $element =~ s|\n|</br>|g;
+    print "<td>$element</td>\n";
+  }
+  print "</tr>";
+} # printRow
+
+
+# Prints table headings.  Accepts any number of arguments;
+# each argument is a table heading.
+sub startTable {
+  print qq(<center><table width="98%" cellspacing="0" class="center"><tr>);
+
+  foreach(@_) {
+    print qq(<td class="heading">$_</td>);
+  }
+  print "</tr>\n";
+} # startTable
+
+
+# Initial display:  Show master blocks with total allocated subnets, total free subnets
+sub showSummary {
+
+  startTable('Master netblock', 'Routed netblocks', 'Allocated netblocks',
+	'Free netblocks', 'Largest free block');
+
+  my %allocated;
+  my %free;
+  my %routed;
+  my %bigfree;
+
+  # Count the allocations.
+  $sth = $ip_dbh->prepare("select count(*) from allocations where cidr <<= ?");
+  foreach my $master (@masterblocks) {
+    $sth->execute("$master");
+    $sth->bind_columns(\$allocated{"$master"});
+    $sth->fetch();
+  }
+
+  # Count routed blocks
+  $sth = $ip_dbh->prepare("select count(*) from routed where cidr <<= ?");
+  foreach my $master (@masterblocks) {
+    $sth->execute("$master");
+    $sth->bind_columns(\$routed{"$master"});
+    $sth->fetch();
+  }
+
+  # Count the free blocks.
+  $sth = $ip_dbh->prepare("select count(*) from freeblocks where cidr <<= ? and ".
+	"(routed='y' or routed='n')");
+  foreach my $master (@masterblocks) {
+    $sth->execute("$master");
+    $sth->bind_columns(\$free{"$master"});
+    $sth->fetch();
+  }
+
+  # Find the largest free block in each master
+  $sth = $ip_dbh->prepare("select maskbits from freeblocks where cidr <<= ? and ".
+	"(routed='y' or routed='n') order by maskbits limit 1");
+  foreach my $master (@masterblocks) {
+    $sth->execute("$master");
+    $sth->bind_columns(\$bigfree{"$master"});
+    $sth->fetch();
+  }
+
+  # Print the data.
+  my $count=0;
+  foreach my $master (@masterblocks) {
+    my @row = ("<a href=\"/ip/cgi-bin/main.cgi?action=showmaster&block=$master\">$master</a>",
+	$routed{"$master"}, $allocated{"$master"}, $free{"$master"}, 
+	( ($bigfree{"$master"} eq '') ? ("&lt;NONE&gt;") : ("/".$bigfree{"$master"}) )
+	);
+
+    printRow(\@row, 'color1' ) if($count%2==0);
+    printRow(\@row, 'color2' ) if($count%2!=0);
+    $count++;
+  }
+  print "</table>\n";
+  if ($IPDBacl{$authuser} =~ /a/) {
+    print qq(<a href="/ip/cgi-bin/main.cgi?action=addmaster">Add new master block</a><br><br>\n);
+  }
+  print "Note:  Free blocks noted here include both routed and unrouted blocks.\n";
+
+} # showSummary
+
+
+# Display detail on master
+# Alrighty then!  We're showing routed blocks within a single master this time.
+# We should be able to steal code from showSummary(), and if I'm really smart
+# I'll figger a way to munge the two together.  (Once I've done that, everything
+# else should follow.  YMMV.)
+sub showMaster {
+
+  print qq(<center><div class="heading">Summarizing routed blocks for ).
+	qq($webvar{block}:</div></center><br>\n);
+
+  my %allocated;
+  my %free;
+  my %routed;
+  my %bigfree;
+
+  my $master = new NetAddr::IP $webvar{block};
+  my @localmasters;
+
+  # Fetch only the blocks relevant to this master
+  $sth = $ip_dbh->prepare("select cidr,city from routed where cidr <<= '$master' order by cidr");
+  $sth->execute();
+
+  my $i=0;
+  while (my @data = $sth->fetchrow_array()) {
+    my $cidr = new NetAddr::IP $data[0];
+    $localmasters[$i++] = $cidr;
+    $free{"$cidr"} = 0;
+    $allocated{"$cidr"} = 0;
+    $bigfree{"$cidr"} = 128;
+    # Retain the routing destination
+    $routed{"$cidr"} = $data[1];
+  }
+
+  # Check if there were actually any blocks routed from this master
+  if ($i > 0) {
+    startTable('Routed block','Routed to','Allocated blocks',
+	'Free blocks','Largest free block');
+
+    # Count the allocations
+    $sth = $ip_dbh->prepare("select count(*) from allocations where cidr <<= ?");
+    foreach my $master (@localmasters) {
+      $sth->execute("$master");
+      $sth->bind_columns(\$allocated{"$master"});
+      $sth->fetch();
+    }
+
+    # Count the free blocks.
+    $sth = $ip_dbh->prepare("select count(*) from freeblocks where cidr <<= ? and ".
+	"(routed='y' or routed='n')");
+    foreach my $master (@localmasters) {
+      $sth->execute("$master");
+      $sth->bind_columns(\$free{"$master"});
+      $sth->fetch();
+    }
+
+    # Get the size of the largest free block
+    $sth = $ip_dbh->prepare("select maskbits from freeblocks where cidr <<= ? and ".
+	"(routed='y' or routed='n') order by maskbits limit 1");
+    foreach my $master (@localmasters) {
+      $sth->execute("$master");
+      $sth->bind_columns(\$bigfree{"$master"});
+      $sth->fetch();
+    }
+
+    # Print the data.
+    my $count=0;
+    foreach my $master (@localmasters) {
+      my @row = ("<a href=\"/ip/cgi-bin/main.cgi?action=showrouted&block=$master\">$master</a>",
+	$routed{"$master"}, $allocated{"$master"},
+	$free{"$master"},
+	( ($bigfree{"$master"} eq 128) ? ("&lt;NONE&gt;") : ("/".$bigfree{"$master"}) )
+      );
+      printRow(\@row, 'color1' ) if($count%2==0);
+      printRow(\@row, 'color2' ) if($count%2!=0);
+      $count++;
+    }
+  } else {
+    # If a master block has no routed blocks, then by definition it has no
+    # allocations, and can be deleted.
+    print qq(<hr width="60%"><center><div class="heading">No allocations in ).
+        qq($master.</div>\n).
+	($IPDBacl{$authuser} =~ /d/ ?
+	        qq(<form action="/ip/cgi-bin/main.cgi" method=POST>\n).
+	        qq(<input type=hidden name=action value="delete">\n).
+	        qq(<input type=hidden name=block value="$master">\n).
+	        qq(<input type=hidden name=alloctype value="mm">\n).
+	        qq(<input type=submit value=" Remove this master ">\n).
+	        qq(</form></center>\n) :
+		'');
+
+  } # end check for existence of routed blocks in master
+
+  print qq(</table>\n<hr width="60%">\n).
+	qq(<center><div class="heading">Unrouted blocks in $master:</div></center><br>\n);
+
+  startTable('Netblock','Range');
+
+  # Snag the free blocks.
+  my $count = 0;
+  $sth = $ip_dbh->prepare("select cidr from freeblocks where cidr <<='$master' and ".
+	"routed='n' order by cidr");
+  $sth->execute();
+  while (my @data = $sth->fetchrow_array()) {
+    my $cidr = new NetAddr::IP $data[0];
+    my @row = ("$cidr", $cidr->range);
+    printRow(\@row, 'color1' ) if($count%2==0);
+    printRow(\@row, 'color2' ) if($count%2!=0);
+    $count++;
+  }
+
+  print "</table>\n";
+} # showMaster
+
+
+# Display details of a routed block
+# Alrighty then!  We're showing allocations within a routed block this time.
+# We should be able to steal code from showSummary() and showMaster(), and if
+# I'm really smart I'll figger a way to munge all three together.  (Once I've
+# done that, everything else should follow.  YMMV.
+# This time, we check the database before spewing, because we may
+# not have anything useful to spew.
+sub showRBlock {
+
+  my $master = new NetAddr::IP $webvar{block};
+
+  $sth = $ip_dbh->prepare("select city from routed where cidr='$master'");
+  $sth->execute;
+  my @data = $sth->fetchrow_array;
+
+  print qq(<center><div class="heading">Summarizing allocated blocks for ).
+	qq($master ($data[0]):</div></center><br>\n);
+
+  startTable('CIDR allocation','Customer Location','Type','CustID','SWIPed?','Description/Name');
+
+  # Snag the allocations for this block
+  $sth = $ip_dbh->prepare("select cidr,city,type,custid,swip,description".
+	" from allocations where cidr <<= '$master' order by cidr");
+  $sth->execute();
+
+  # hack hack hack
+  # set up to flag swip=y records if they don't actually have supporting data in the customers table
+  my $custsth = $ip_dbh->prepare("select count(*) from customers where custid=?");
+
+  my $count=0;
+  while (my @data = $sth->fetchrow_array()) {
+    # cidr,city,type,custid,swip,description, as per the SELECT
+    my $cidr = new NetAddr::IP $data[0];
+
+    # Clean up extra spaces that are borking things.
+#    $data[2] =~ s/\s+//g;
+
+    $custsth->execute($data[3]);
+    my ($ncust) = $custsth->fetchrow_array();
+
+    # Prefix subblocks with "Sub "
+    my @row = ( (($data[2] =~ /^.r$/) ? 'Sub ' : '').
+	qq(<a href="/ip/cgi-bin/main.cgi?action=edit&block=$data[0]">$data[0]</a>),
+	$data[1], $disp_alloctypes{$data[2]}, $data[3], 
+	($data[4] eq 'y' ? ($ncust == 0 ? 'Yes<small>*</small>' : 'Yes') : 'No'), $data[5]);
+    # If the allocation is a pool, allow listing of the IPs in the pool.
+    if ($data[2] =~ /^.[pd]$/) {
+      $row[0] .= ' &nbsp; <a href="/ip/cgi-bin/main.cgi?action=listpool'.
+	"&pool=$data[0]\">List IPs</a>";
+    }
+
+    printRow(\@row, 'color1') if ($count%2 == 0);
+    printRow(\@row, 'color2') if ($count%2 != 0);
+    $count++;
+  }
+
+  print "</table>\n";
+
+  # If the routed block has no allocations, by definition it only has
+  # one free block, and therefore may be deleted.
+  if ($count == 0) {
+    print qq(<hr width="60%"><center><div class="heading">No allocations in ).
+	qq($master.</div></center>\n).
+	($IPDBacl{$authuser} =~ /d/ ?
+		qq(<form action="/ip/cgi-bin/main.cgi" method=POST>\n).
+		qq(<input type=hidden name=action value="delete">\n).
+		qq(<input type=hidden name=block value="$master">\n).
+		qq(<input type=hidden name=alloctype value="rm">\n).
+		qq(<input type=submit value=" Remove this block ">\n).
+		qq(</form>\n) :
+		'');
+  }
+
+  print qq(<hr width="60%">\n<center><div class="heading">Free blocks within routed ).
+	qq(submaster $master</div></center>\n);
+
+  startTable('CIDR block','Range');
+
+  # Snag the free blocks.  We don't really *need* to be pedantic about avoiding
+  # unrouted free blocks, but it's better to let the database do the work if we can.
+  $count = 0;
+  $sth = $ip_dbh->prepare("select cidr,routed from freeblocks where cidr <<= '$master'".
+	" order by cidr");
+  $sth->execute();
+  while (my @data = $sth->fetchrow_array()) {
+    # cidr,routed
+    my $cidr = new NetAddr::IP $data[0];
+    # Include some HairyPerl(TM) to prefix subblocks with "Sub "
+    my @row = ((($data[1] ne 'y' && $data[1] ne 'n') ? 'Sub ' : '').
+	($IPDBacl{$authuser} =~ /a/ ? qq(<a href="/ip/cgi-bin/main.cgi?action=assign&block=$cidr&fbtype=$data[1]">$cidr</a>) : $cidr),
+	$cidr->range);
+    printRow(\@row, 'color1') if ($count%2 == 0);
+    printRow(\@row, 'color2') if ($count%2 != 0);
+    $count++;
+  }
+
+  print "</table>\n";
+} # showRBlock
+
+
+# List the IPs used in a pool
+sub listPool {
+
+  my $cidr = new NetAddr::IP $webvar{pool};
+
+  my ($pooltype,$poolcity);
+
+  # Snag pool info for heading
+  $sth = $ip_dbh->prepare("select type,city from allocations where cidr='$cidr'");
+  $sth->execute;
+  $sth->bind_columns(\$pooltype, \$poolcity);
+  $sth->fetch() || carp $sth->errstr;
+
+  print qq(<center><div class="heading">Listing pool IPs for $cidr<br>\n).
+	qq(($disp_alloctypes{$pooltype} in $poolcity)</div></center><br>\n);
+  # Only display net/gw/bcast if it's a "real" netblock and not a PPP(oE) lunacy
+  if ($pooltype =~ /^.d$/) {
+    print qq(<div class="indent"><b>Reserved IPs:</b><br>\n);
+    print qq(<div class="indent"><table><tr class=color1><td>Network IP:</td><td>).
+	$cidr->addr."</td></tr>\n";
+    $cidr++;
+    print "<tr class=color2><td>Gateway:</td><td>".$cidr->addr."</td></tr>\n";
+    $cidr--;  $cidr--;
+    print "<tr class=color1><td>Broadcast:</td><td>".$cidr->addr."</td></tr>\n".
+	"<tr><td>Netmask:</td><td>".$cidr->mask."</td></tr>\n".
+	"</table></div></div>\n";
+  }
+
+# probably have to add an "edit IP allocation" link here somewhere.
+
+  startTable('IP','Customer ID','Available?','Description','');
+  $sth = $ip_dbh->prepare("select ip,custid,available,description,type".
+	" from poolips where pool='$webvar{pool}' order by ip");
+  $sth->execute;
+  my $count = 0;
+  while (my @data = $sth->fetchrow_array) {
+    # pool,ip,custid,city,ptype,available,notes,description,circuitid
+    # ip,custid,available,description,type
+    # If desc is "null", make it not null.  <g>
+    if ($data[3] eq '') {
+      $data[3] = '&nbsp;';
+    }
+    # Some nice hairy Perl to decide whether to allow unassigning each IP
+    #   -> if $data[2] (aka poolips.available) == 'n' then we print the unassign link
+    #	   else we print a blank space
+    my @row = ( qq(<a href="/ip/cgi-bin/main.cgi?action=edit&block=$data[0]">$data[0]</a>),
+	$data[1],$data[2],$data[3],
+	( (($data[2] eq 'n') && ($IPDBacl{$authuser} =~ /d/)) ?
+	  ("<a href=\"/ip/cgi-bin/main.cgi?action=delete&block=$data[0]&".
+	   "alloctype=$data[4]\">Unassign this IP</a>") :
+	  ("&nbsp;") )
+	);
+    printRow(\@row, 'color1') if($count%2==0);
+    printRow(\@row, 'color2') if($count%2!=0);
+    $count++;
+  }
+  print "</table>\n";
+
+} # end listPool
+
+
+# Show "Add new allocation" page.  Note that the actual page may
+# be one of two templates, and the lists come from the database.
+sub assignBlock {
+
+  if ($IPDBacl{$authuser} !~ /a/) {
+    printError("You shouldn't have been able to get here.  Access denied.");
+    return;
+  }
+
+  my $html;
+
+  # New special case- block to assign is specified
+  if ($webvar{block} ne '') {
+    open HTML, "../fb-assign.html"
+	or croak "Could not open fb-assign.html: $!";
+    $html = join('',<HTML>);
+    close HTML;
+    my $block = new NetAddr::IP $webvar{block};
+    $html =~ s|\$\$BLOCK\$\$|$block|g;
+    $html =~ s|\$\$MASKBITS\$\$|$block->masklen|;
+    my $typelist = '';
+
+    # This is a little dangerous, as it's *theoretically* possible to
+    # get fbtype='n' (aka a non-routed freeblock).  However, should
+    # someone manage to get there, they get what they deserve.
+    if ($webvar{fbtype} ne 'y') {
+      # Snag the type of the block from the database.  We have no
+      # convenient way to pass this in from the calling location.  :/
+      $sth = $ip_dbh->prepare("select type from allocations where cidr >>='$block'");
+      $sth->execute;
+      my @data = $sth->fetchrow_array;
+      $data[0] =~ s/c$/r/;	# Munge the type into the correct form
+      $typelist = "$list_alloctypes{$data[0]}<input type=hidden name=alloctype value=$data[0]>\n";
+    } else {
+      $typelist .= qq(<select name="alloctype">\n);
+      $sth = $ip_dbh->prepare("select type,listname from alloctypes where listorder < 500 ".
+	"and type not like '_i' and type not like '_r' order by listorder");
+      $sth->execute;
+      my @data = $sth->fetchrow_array;
+      $typelist .= "<option value='$data[0]' selected>$data[1]</option>\n";
+      while (my @data = $sth->fetchrow_array) {
+        $typelist .= "<option value='$data[0]'>$data[1]</option>\n";
+      }
+      $typelist .= "</select>\n";
+    }
+    $html =~ s|\$\$TYPELIST\$\$|$typelist|g;
+  } else {
+    open HTML, "../assign.html"
+	or croak "Could not open assign.html: $!";
+    $html = join('',<HTML>);
+    close HTML;
+    my $masterlist = "<select name=allocfrom><option selected>-</option>\n";
+    foreach my $master (@masterblocks) {
+      $masterlist .= "<option>$master</option>\n";
+    }
+    $masterlist .= "</select>\n";
+    $html =~ s|\$\$MASTERLIST\$\$|$masterlist|g;
+    my $pops = '';
+    foreach my $pop (@poplist) {
+      $pops .= "<option>$pop</option>\n";
+    }
+    $html =~ s|\$\$POPLIST\$\$|$pops|g;
+    my $typelist = '';
+    $sth = $ip_dbh->prepare("select type,listname from alloctypes where listorder < 900 order by listorder");
+    $sth->execute;
+    my @data = $sth->fetchrow_array;
+    $typelist .= "<option value='$data[0]' selected>$data[1]</option>\n";
+    while (my @data = $sth->fetchrow_array) {
+      $typelist .= "<option value='$data[0]'>$data[1]</option>\n";
+    }
+    $html =~ s|\$\$TYPELIST\$\$|$typelist|g;
+  }
+  my $cities = '';
+  foreach my $city (@citylist) {
+    $cities .= "<option>$city</option>\n";
+  }
+  $html =~ s|\$\$ALLCITIES\$\$|$cities|g;
+
+  my $i = 0;
+  $i++ if $webvar{fbtype} eq 'y';
+  # Check to see if user is allowed to do anything with sensitive data
+  my $privdata = '';
+  if ($IPDBacl{$authuser} =~ /s/) {
+    $privdata = qq(<tr class="color).($i%2).qq("><td>Restricted data:</td>).
+        qq(<td class=regular><textarea rows="3" cols="64" name="privdata" class="regular">).
+        qq(</textarea></td></tr>\n);
+    $i++;
+  }
+  $html =~ s/\$\$PRIVDATA\$\$/$privdata/g;
+
+  $i = $i % 2;
+  $html =~ s/\$\$BUTTONROWCOLOUR\$\$/color$i/;
+
+  print $html;
+
+} # assignBlock
+
+
+# Take info on requested IP assignment and see what we can provide.
+sub confirmAssign {
+  if ($IPDBacl{$authuser} !~ /a/) {
+    printError("You shouldn't have been able to get here.  Access denied.");
+    return;
+  }
+
+  my $cidr;
+  my $alloc_from;
+
+  # Going to manually validate some items.
+  # custid and city are automagic.
+  return if !validateInput();
+
+# Several different cases here.
+# Static IP vs netblock
+#  + Different flavours of static IP
+#  + Different flavours of netblock
+
+  if ($webvar{alloctype} =~ /^.i$/) {
+    my ($base,undef) = split //, $webvar{alloctype};	# split into individual chars
+    my ($sql,$city);
+    # Check for pools in Subury, North Bay, or Toronto if DSL or server pool.
+    # Anywhere else is invalid and shouldn't be in the db in the first place.
+    # ... aside from #^%#$%#@#^%^^!!!! legacy data.  GRRR.
+    # Note that we want to retain the requested city to relate to customer info.
+    if ($base =~ /^[ds]$/) {
+      $city = "(allocations.city='Sudbury' or allocations.city='North Bay' or ".
+	"allocations.city='Toronto')";
+    } else {
+      $city = "allocations.city='$webvar{pop}'";
+    }
+
+# Ewww.  But it works.
+    $sth = $ip_dbh->prepare("SELECT (SELECT city FROM allocations WHERE cidr=poolips.pool), ".
+	"poolips.pool, COUNT(*) FROM poolips,allocations WHERE poolips.available='y' AND ".
+	"poolips.pool=allocations.cidr AND $city AND poolips.type LIKE '".$base."_' ".
+	"GROUP BY pool");
+    $sth->execute;
+    my $optionlist;
+    while (my @data = $sth->fetchrow_array) {
+      # city,pool cidr,free IP count
+      if ($data[2] > 0) {
+        $optionlist .= "<option value='$data[1]'>$data[1] [$data[2] free IP(s)] in $data[0]</option>\n";
+      }
+    }
+    $cidr = "Single static IP";
+    $alloc_from = "<select name=alloc_from>".$optionlist."</select>\n";
+
+  } else { # end show pool options
+
+    if ($webvar{fbassign} eq 'y') {
+      $cidr = new NetAddr::IP $webvar{block};
+      $webvar{maskbits} = $cidr->masklen;
+    } else { # done with direct freeblocks assignment
+
+      if (!$webvar{maskbits}) {
+        printError("Please specify a CIDR mask length.");
+	return;
+      }
+      my $sql;
+      my $city;
+      my $failmsg;
+      my $extracond = '';
+      if ($webvar{allocfrom} eq '-') {
+	$extracond = ($webvar{allowpriv} eq 'on' ? '' : 
+		" and not (cidr <<= '192.168.0.0/16'".
+			" or cidr <<= '10.0.0.0/8'".
+			" or cidr <<= '172.16.0.0/12')");
+      }
+      my $sortorder;
+      if ($webvar{alloctype} eq 'rm') {
+        if ($webvar{allocfrom} ne '-') {
+	  $sql = "select * from freeblocks where maskbits<=$webvar{maskbits} and routed='n'".
+		" and cidr <<= '$webvar{allocfrom}'";
+	  $sortorder = "maskbits desc";
+	} else {
+	  $sql = "select * from freeblocks where maskbits<=$webvar{maskbits} and routed='n'";
+	  $sortorder = "maskbits desc";
+	}
+	$failmsg = "No suitable free block found.<br>\nWe do not have a free".
+	  " routeable block of that size.<br>\nYou will have to either route".
+	  " a set of smaller netblocks or a single smaller netblock.";
+      } else {
+##fixme
+# This section needs serious Pondering.
+	# Pools of most types get assigned to the POP they're "routed from"
+	# This includes WAN blocks and other netblock "containers"
+	# This does NOT include cable pools.
+	if ($webvar{alloctype} =~ /^.[pc]$/) {
+	  if (($webvar{city} !~ /^(Sudbury|North Bay|Toronto)$/) && ($webvar{alloctype} eq 'dp')) {
+	    printError("You must chose Sudbury, North Bay, or Toronto for DSL pools.");
+	    return;
+	  }
+	  $city = $webvar{city};
+	  $failmsg = "No suitable free block found.<br>\nYou will have to route another".
+	    " superblock from one of the<br>\nmaster blocks in Sudbury or chose a smaller".
+	    " block size for the pool.";
+	} else {
+	  $city = $webvar{pop};
+	  $failmsg = "No suitable free block found.<br>\nYou will have to route another".
+	    " superblock to $webvar{pop}<br>\nfrom one of the master blocks in Sudbury or".
+	    " chose a smaller blocksize.";
+	}
+	if (defined $webvar{allocfrom} && $webvar{allocfrom} ne '-') {
+	  $sql = "select cidr from freeblocks where city='$city' and maskbits<=$webvar{maskbits}".
+		" and cidr <<= '$webvar{allocfrom}' and routed='".
+		(($webvar{alloctype} =~ /^(.)r$/) ? "$1" : 'y')."'";
+	  $sortorder = "maskbits desc,cidr";
+	} else {
+	  $sql = "select cidr from freeblocks where city='$city' and maskbits<=$webvar{maskbits}".
+		" and routed='".(($webvar{alloctype} =~ /^(.)r$/) ? "$1" : 'y')."'";
+	  $sortorder = "maskbits desc,cidr";
+	}
+      }
+      $sql = $sql.$extracond." order by ".$sortorder;
+      $sth = $ip_dbh->prepare($sql);
+      $sth->execute;
+      my @data = $sth->fetchrow_array();
+      if ($data[0] eq "") {
+	printError($failmsg);
+	return;
+      }
+      $cidr = new NetAddr::IP $data[0];
+    } # check for freeblocks assignment or IPDB-controlled assignment
+
+    $alloc_from = qq($cidr<input type=hidden name=alloc_from value="$cidr">);
+
+    # If the block to be allocated is smaller than the one we found,
+    # figure out the "real" block to be allocated.
+    if ($cidr->masklen() ne $webvar{maskbits}) {
+      my $maskbits = $cidr->masklen();
+      my @subblocks;
+      while ($maskbits++ < $webvar{maskbits}) {
+	@subblocks = $cidr->split($maskbits);
+      }
+      $cidr = $subblocks[0];
+    }
+  } # if ($webvar{alloctype} =~ /^.i$/)
+
+  open HTML, "../confirm.html"
+	or croak "Could not open confirm.html: $!";
+  my $html = join '', <HTML>;
+  close HTML;
+
+### gotta fix this in final
+  # Stick in customer info as necessary - if it's blank, it just ends
+  # up as blank lines ignored in the rendering of the page
+	my $custbits;
+  $html =~ s|\$\$CUSTBITS\$\$|$custbits|g;
+###
+
+  # Stick in the allocation data
+  $html =~ s|\$\$ALLOC_TYPE\$\$|$webvar{alloctype}|g;
+  $html =~ s|\$\$TYPEFULL\$\$|$disp_alloctypes{$webvar{alloctype}}|g;
+  $html =~ s|\$\$ALLOC_FROM\$\$|$alloc_from|g;
+  $html =~ s|\$\$CIDR\$\$|$cidr|g;
+  $webvar{city} = desanitize($webvar{city});
+  $html =~ s|\$\$CITY\$\$|$webvar{city}|g;
+  $html =~ s|\$\$CUSTID\$\$|$webvar{custid}|g;
+  $webvar{circid} = desanitize($webvar{circid});
+  $html =~ s|\$\$CIRCID\$\$|$webvar{circid}|g;
+  $webvar{desc} = desanitize($webvar{desc});
+  $html =~ s|\$\$DESC\$\$|$webvar{desc}|g;
+  $webvar{notes} = desanitize($webvar{notes});
+  $html =~ s|\$\$NOTES\$\$|$webvar{notes}|g;
+  $html =~ s|\$\$ACTION\$\$|insert|g;
+
+  my $i=1;
+  # Check to see if user is allowed to do anything with sensitive data
+  my $privdata = '';
+  if ($IPDBacl{$authuser} =~ /s/) {
+    $privdata = qq(<tr class="color).($i%2).qq("><td>Restricted data:</td>).
+        qq(<td class=regular>$webvar{privdata}).
+	qq(<input type=hidden name=privdata value="$webvar{privdata}"></td></tr>\n);
+    $i++;
+  }
+# We're going to abuse $$PRIVDATA$$ to stuff in some stuff for billing.
+  $privdata .= "<input type=hidden name=billinguser value=$webvar{userid}>\n"
+	if $webvar{userid};
+  $html =~ s/\$\$PRIVDATA\$\$/$privdata/g;
+
+  $i = $i % 2;
+  $html =~ s/\$\$BUTTONROWCOLOUR\$\$/color$i/;
+
+  print $html;
+
+} # end confirmAssign
+
+
+# Do the work of actually inserting a block in the database.
+sub insertAssign {
+  if ($IPDBacl{$authuser} !~ /a/) {
+    printError("You shouldn't have been able to get here.  Access denied.");
+    return;
+  }
+  # Some things are done more than once.
+  return if !validateInput();
+
+  if (!defined($webvar{privdata})) {
+    $webvar{privdata} = '';
+  }
+  # $code is "success" vs "failure", $msg contains OK for a
+  # successful netblock allocation, the IP allocated for static
+  # IP, or the error message if an error occurred.
+  my ($code,$msg) = allocateBlock($ip_dbh, $webvar{fullcidr}, $webvar{alloc_from},
+	$webvar{custid}, $webvar{alloctype}, $webvar{city}, $webvar{desc}, $webvar{notes},
+	$webvar{circid}, $webvar{privdata});
+
+  if ($code eq 'OK') {
+    if ($webvar{alloctype} =~ /^.i$/) {
+      $msg =~ s|/32||;
+      print qq(<div class="center"><div class="heading">The IP $msg has been allocated to customer $webvar{custid}</div>).
+	( ($webvar{alloctype} eq 'di' && $webvar{billinguser}) ?
+		qq(<div><a href="https://billing.example.com/radius.pl?).
+		"action=new_radius_user&custid=$webvar{custid}&userid=$webvar{billinguser}".
+		qq(&ipdb=1&ip=$msg">Add this IP to RADIUS user table</a></div>)
+	: "</div>");
+      # Notify tech@example.com
+      mailNotify('tech@example.com',"ADDED: $disp_alloctypes{$webvar{alloctype}} allocation",
+	"$disp_alloctypes{$webvar{alloctype}} $msg allocated to customer $webvar{custid}\n".
+	"Description: $webvar{desc}\n\nAllocated by: $authuser\n");
+    } else {
+      my $netblock = new NetAddr::IP $webvar{fullcidr};
+      print qq(<div class="center"><div class="heading">The block $webvar{fullcidr} was ).
+	"sucessfully added as: $disp_alloctypes{$webvar{alloctype}}</div>".
+	( ($webvar{alloctype} eq 'pr' && $webvar{billinguser}) ?
+		qq(<div><a href="https://billing.example.com/radius.pl?).
+		"action=new_radius_user&custid=$webvar{custid}&userid=$webvar{billinguser}".
+		"&route_subnet=".$netblock->addr."&subnet_slash=".$netblock->masklen.
+		"&include_routed_subnet=1&ipdb=1".
+		qq(">Add this netblock to RADIUS user table</a></div>)
+	: "</div>");
+    }
+    syslog "notice", "$authuser allocated '$webvar{fullcidr}' to '$webvar{custid}' as ".
+	"'$webvar{alloctype}' ($msg)";
+  } else {
+    syslog "err", "Allocation of '$webvar{fullcidr}' to '$webvar{custid}' as ".
+	"'$webvar{alloctype}' by $authuser failed: '$msg'";
+    printError("Allocation of $webvar{fullcidr} as '$disp_alloctypes{$webvar{alloctype}}'".
+	" failed:<br>\n$msg\n");
+  }
+
+} # end insertAssign()
+
+
+# Does some basic checks on common input data to make sure nothing
+# *really* weird gets in to the database through this script.
+# Does NOT do complete input validation!!!
+sub validateInput {
+  if ($webvar{city} eq '-') {
+    printError("Please choose a city.");
+    return;
+  }
+
+  # Alloctype check.
+  chomp $webvar{alloctype};
+  if (!grep /$webvar{alloctype}/, keys %disp_alloctypes) {
+    # Danger! Danger!  alloctype should ALWAYS be set by a dropdown.  Anyone
+    # managing to call things in such a way as to cause this deserves a cryptic error.
+    printError("Invalid alloctype");
+    return;
+  }
+
+  # CustID check
+  # We have different handling for customer allocations and "internal" or "our" allocations
+  if ($def_custids{$webvar{alloctype}} eq '') {
+    if (!$webvar{custid}) {
+      printError("Please enter a customer ID.");
+      return;
+    }
+    if ($webvar{custid} !~ /^(?:\d{10}|\d{7}|STAFF)(?:-\d\d?)?$/) {
+      # Force uppercase for now...
+      $webvar{custid} =~ tr/a-z/A-Z/;
+      # Crosscheck with billing.
+      my $status = CustIDCK->custid_exist($webvar{custid});
+      if ($CustIDCK::Error) {
+	printError("Error verifying customer ID: ".$CustIDCK::ErrMsg);
+	return;
+      }
+      if (!$status) {
+	printError("Customer ID not valid.  Make sure the Customer ID ".
+	  "is correct.<br>\nUse STAFF for staff static IPs, and 6750400 for any other ".
+	  "non-customer assignments.");
+	return;
+      }
+#"Please enter a valid customer ID- this must be a 7- or 10-digit number, or STAFF for
+#static IPs for staff.");
+    }
+#    print "<!-- [ In validateInput().  Insert customer ID cross-check here. ] -->\n";
+  } else {
+    # New!  Improved!  And now Loaded From The Database!!
+    if ((!$webvar{custid}) || ($webvar{custid} ne 'STAFF')) {
+      $webvar{custid} = $def_custids{$webvar{alloctype}};
+    }
+  }
+
+  # Check POP location
+  my $flag;
+  if ($webvar{alloctype} eq 'rm') {
+    $flag = 'for a routed netblock';
+    foreach (@poplist) {
+      if (/^$webvar{city}$/) {
+	$flag = 'n';
+	last;
+      }
+    }
+  } else {
+    $flag = 'n';
+    if ($webvar{alloctype} =~ /[wp][cr]|[ds][pi]/) {
+      # Set this forcibly rather than messing around elsewhere.  Yes, this *is* a hack.  PTHBTT!!
+      $webvar{pop} = 'Sudbury';
+    }
+    if ($webvar{pop} =~ /^-$/) {
+      $flag = 'to route the block from/through';
+    }
+  }
+  if ($flag ne 'n') {
+    printError("Please choose a valid POP location $flag.  Valid ".
+	"POP locations are currently:<br>\n".join (" - ", @poplist));
+    return;
+  }
+
+  return 'OK';
+} # end validateInput
+
+
+# Displays details of a specific allocation in a form
+# Allows update/delete
+# action=edit
+sub edit {
+
+  my $sql;
+
+  # Two cases:  block is a netblock, or block is a static IP from a pool
+  # because I'm lazy, we'll try to make the SELECT's bring out identical)ish) data
+  if ($webvar{block} =~ /\/32$/) {
+    $sql = "select ip,custid,type,city,circuitid,description,notes,modifystamp,privdata,oldcustid from poolips where ip='$webvar{block}'";
+  } else {
+    $sql = "select cidr,custid,type,city,circuitid,description,notes,modifystamp,privdata,oldcustid,swip from allocations where cidr='$webvar{block}'"
+  }
+
+  # gotta snag block info from db
+  $sth = $ip_dbh->prepare($sql);
+  $sth->execute;
+  my @data = $sth->fetchrow_array;
+
+  # Clean up extra whitespace on alloc type
+  $data[2] =~ s/\s//;
+
+  open (HTML, "../editDisplay.html")
+	or croak "Could not open editDisplay.html :$!";
+  my $html = join('', <HTML>);
+
+  # We can't let the city be changed here;  this block is a part of
+  # a larger routed allocation and therefore by definition can't be moved.
+  # block and city are static.
+##fixme
+# Needs thinking.  Have to allow changes to city to correct errors, no?
+  $html =~ s/\$\$BLOCK\$\$/$webvar{block}/g;
+
+  if ($IPDBacl{$authuser} =~ /c/) {
+    $html =~ s/\$\$CUSTID\$\$/<input type=text name=custid value="$data[1]" maxlength=15 class="regular">/;
+
+# Screw it.  Changing allocation types gets very ugly VERY quickly- especially
+# with the much longer list of allocation types.
+# We'll just show what type of block it is.
+
+# this has now been Requested, so here goes.
+
+##fixme The check here should be built from the database
+    if ($data[2] =~ /^.[ne]$/) {
+      # Block that can be changed
+      my $blockoptions = "<select name=alloctype><option".
+	(($data[2] eq 'me') ? ' selected' : '') ." value='me'>Dialup netblock</option>\n<option".
+	(($data[2] eq 'de') ? ' selected' : '') ." value='de'>Dynamic DSL netblock</option>\n<option".
+	(($data[2] eq 'ce') ? ' selected' : '') ." value='ce'>Dynamic cable netblock</option>\n<option".
+	(($data[2] eq 'we') ? ' selected' : '') ." value='we'>Dynamic wireless netblock</option>\n<option".
+	(($data[2] eq 'cn') ? ' selected' : '') ." value='cn'>Customer netblock</option>\n<option".
+	(($data[2] eq 'en') ? ' selected' : '') ." value='en'>End-use netblock</option>\n<option".
+	(($data[2] eq 'in') ? ' selected' : '') ." value='in'>Internal netblock</option>\n".
+	"</select>\n";
+      $html =~ s/\$\$TYPESELECT\$\$/$blockoptions/g;
+    } else {
+      $html =~ s/\$\$TYPESELECT\$\$/$disp_alloctypes{$data[2]}<input type=hidden name=alloctype value="$data[2]">/g;
+    }
+    $html =~ s/\$\$OLDCUSTID\$\$/$data[9]/g;
+    $html =~ s/\$\$CITY\$\$/<input type=text name=city value="$data[3]">/g;
+    $html =~ s/\$\$CIRCID\$\$/<input type="text" name="circid" value="$data[4]" maxlength=64 size=64 class="regular">/g;
+    $html =~ s/\$\$DESC\$\$/<input type="text" name="desc" value="$data[5]" maxlength=64 size=64 class="regular">/g;
+    $html =~ s|\$\$NOTES\$\$|<textarea rows="8" cols="64" name="notes" class="regular">$data[6]</textarea>|g;
+  } else {
+    $html =~ s/\$\$CUSTID\$\$/$data[1]/g;
+    $html =~ s/\$\$OLDCUSTID\$\$/$data[9]/g;
+    $html =~ s/\$\$TYPESELECT\$\$/$disp_alloctypes{$data[2]}/g;
+    $html =~ s/\$\$CITY\$\$/$data[3]/g;
+    $html =~ s/\$\$CIRCID\$\$/$data[4]/g;
+    $html =~ s/\$\$DESC\$\$/$data[5]/g;
+    $html =~ s/\$\$NOTES\$\$/$data[6]/g;
+  }
+  my ($lastmod,undef) = split /\s+/, $data[7];
+  $html =~ s/\$\$LASTMOD\$\$/$lastmod/g;
+
+## Hack time!  SWIP isn't going to stay, so I'm not going to integrate it with ACLs.
+if ($data[2] =~ /.i/) {
+  $html =~ s/\$\$SWIP\$\$/N\/A/;
+} else {
+  my $tmp = (($data[10] eq 'n') ? '<input type=checkbox name=swip>' : 
+	'<input type=checkbox name=swip checked=yes>');
+  $html =~ s/\$\$SWIP\$\$/$tmp/;
+}
+
+  # Allows us to "correctly" colour backgrounds in table
+  my $i=1;
+
+  # Check to see if we can display sensitive data
+  my $privdata = '';
+  if ($IPDBacl{$authuser} =~ /s/) {
+    $privdata = qq(<tr class="color).($i%2).qq("><td class=heading>Restricted data:</td>).
+	qq(<td class=regular><textarea rows="3" cols="64" name="privdata" class="regular">).
+	qq($data[8]</textarea></td></tr>\n);
+    $i++;
+  }
+  $html =~ s/\$\$PRIVDATA\$\$/$privdata/g;
+
+  # More ACL trickery - we can live with forms that don't submit,
+  # but we can't leave the extra table rows there, and we *really*
+  # can't leave the submit buttons there.
+  my $updok = '';
+  if ($IPDBacl{$authuser} =~ /c/) {
+    $updok = qq(<tr class="color).($i%2).qq("><td colspan=2><div class="center">).
+	qq(<input type="submit" value=" Update this block " class="regular">).
+	"</div></td></tr></form>\n";
+    $i++;
+  }
+  $html =~ s/\$\$UPDOK\$\$/$updok/g;
+
+  my $delok = '';
+  if ($IPDBacl{$authuser} =~ /d/) {
+    $delok = qq(<form method="POST" action="main.cgi">
+	<tr class="color).($i%2).qq("><td colspan=2 class="regular"><div class=center>
+	<input type="hidden" name="action" value="delete">
+	<input type="hidden" name="block" value="$webvar{block}">
+	<input type="hidden" name="alloctype" value="$data[2]">
+	<input type=submit value=" Delete this block ">
+	</div></td></tr>);
+  }
+  $html =~ s/\$\$DELOK\$\$/$delok/;
+
+  print $html;
+
+} # edit()
+
+
+# Stuff new info about a block into the db
+# action=update
+sub update {
+  if ($IPDBacl{$authuser} !~ /c/) {
+    printError("You shouldn't have been able to get here.  Access denied.");
+    return;
+  }
+
+  # Check to see if we can update restricted data
+  my $privdata = '';
+  if ($IPDBacl{$authuser} =~ /s/) {
+    $privdata = ",privdata='$webvar{privdata}'";
+  }
+
+  # Make sure incoming data is in correct format - custID among other things.
+  return if !validateInput;
+
+  # SQL transaction wrapper
+  eval {
+    # Relatively simple SQL transaction here.
+    my $sql;
+    if (my $pooltype = ($webvar{alloctype} =~ /^(.)i$/) ) {
+      $sql = "update poolips set custid='$webvar{custid}',notes='$webvar{notes}',".
+	"circuitid='$webvar{circid}',description='$webvar{desc}',city='$webvar{city}'".
+	"$privdata where ip='$webvar{block}'";
+    } else {
+      $sql = "update allocations set custid='$webvar{custid}',".
+	"description='$webvar{desc}',notes='$webvar{notes}',city='$webvar{city}',".
+	"type='$webvar{alloctype}',circuitid='$webvar{circid}'$privdata,".
+	"swip='".($webvar{swip} eq 'on' ? 'y' : 'n')."' ".
+	"where cidr='$webvar{block}'";
+    }
+    # Log the details of the change.
+    syslog "debug", $sql;
+    $sth = $ip_dbh->prepare($sql);
+    $sth->execute;
+    $ip_dbh->commit;
+  };
+  if ($@) {
+    my $msg = $@;
+    carp "Transaction aborted because $msg";
+    eval { $ip_dbh->rollback; };
+    syslog "err", "$authuser could not update block/IP '$webvar{block}': '$msg'";
+    printError("Could not update block/IP $webvar{block}: $msg");
+    return;
+  }
+
+  # If we get here, the operation succeeded.
+  syslog "notice", "$authuser updated $webvar{block}";
+  open (HTML, "../updated.html")
+	or croak "Could not open updated.html :$!";
+  my $html = join('', <HTML>);
+
+  # Link back to browse-routed or list-pool page on "Update complete" page.
+  my $backlink = "/ip/cgi-bin/main.cgi?action=";
+  my $cblock;	# to contain the CIDR of the container block we're retrieving.
+  my $sql;
+  if (my $pooltype = ($webvar{alloctype} =~ /^(.)i$/) ) {
+    $sql = "select pool from poolips where ip='$webvar{block}'";
+    $backlink .= "listpool&pool=";
+  } else {
+    $sql = "select cidr from routed where cidr >>= '$webvar{block}'";
+    $backlink .= "showrouted&block=";
+  }
+  # I define there to be no errors on this operation...  so we don't need to check for them.
+  $sth = $ip_dbh->prepare($sql);
+  $sth->execute;
+  $sth->bind_columns(\$cblock);
+  $sth->fetch();
+  $sth->finish;
+  $backlink .= $cblock;
+
+my $swiptmp = ($webvar{swip} eq 'on' ? 'Yes' : 'No');
+  $html =~ s/\$\$BLOCK\$\$/$webvar{block}/g;
+  $webvar{city} = desanitize($webvar{city});
+  $html =~ s/\$\$CITY\$\$/$webvar{city}/g;
+  $html =~ s/\$\$ALLOCTYPE\$\$/$webvar{alloctype}/g;
+  $html =~ s/\$\$TYPEFULL\$\$/$disp_alloctypes{$webvar{alloctype}}/g;
+  $html =~ s/\$\$CUSTID\$\$/$webvar{custid}/g;
+  $html =~ s/\$\$SWIP\$\$/$swiptmp/g;
+  $webvar{circid} = desanitize($webvar{circid});
+  $html =~ s/\$\$CIRCID\$\$/$webvar{circid}/g;
+  $webvar{desc} = desanitize($webvar{desc});
+  $html =~ s/\$\$DESC\$\$/$webvar{desc}/g;
+  $webvar{notes} = desanitize($webvar{notes});
+  $html =~ s/\$\$NOTES\$\$/$webvar{notes}/g;
+  $html =~ s/\$\$BACKLINK\$\$/$backlink/g;
+  $html =~ s/\$\$BACKBLOCK\$\$/$cblock/g;
+
+  if ($IPDBacl{$authuser} =~ /s/) {
+    $privdata = qq(<tr class="color2"><td valign="top">Restricted data:</td>).
+	qq(<td class="regular">).desanitize($webvar{privdata}).qq(</td></tr>\n);
+  }
+  $html =~ s/\$\$PRIVDATA\$\$/$privdata/g;
+
+  print $html;
+
+} # update()
+
+
+# Delete an allocation.
+sub remove {
+  if ($IPDBacl{$authuser} !~ /d/) {
+    printError("You shouldn't have been able to get here.  Access denied.");
+    return;
+  }
+
+  #show confirm screen.
+  open HTML, "../confirmRemove.html"
+	or croak "Could not open confirmRemove.html :$!";
+  my $html = join('', <HTML>);
+  close HTML;
+
+  # Serves'em right for getting here...
+  if (!defined($webvar{block})) {
+    printError("Error 332");
+    return;
+  }
+
+  my ($cidr, $custid, $type, $city, $circid, $desc, $notes, $alloctype, $privdata);
+
+  if ($webvar{alloctype} eq 'rm') {
+    $sth = $ip_dbh->prepare("select cidr,city from routed where cidr='$webvar{block}'");
+    $sth->execute();
+
+# This feels...  extreme.
+    croak $sth->errstr() if($sth->errstr());
+
+    $sth->bind_columns(\$cidr,\$city);
+    $sth->execute();
+    $sth->fetch || croak $sth->errstr();
+    $custid = "N/A";
+    $alloctype = $webvar{alloctype};
+    $circid = "N/A";
+    $desc = "N/A";
+    $notes = "N/A";
+
+  } elsif ($webvar{alloctype} eq 'mm') {
+    $cidr = $webvar{block};
+    $city = "N/A";
+    $custid = "N/A";
+    $alloctype = $webvar{alloctype};
+    $circid = "N/A";
+    $desc = "N/A";
+    $notes = "N/A";
+  } elsif ($webvar{alloctype} =~ /^.i$/) { # done with alloctype=[rm]m
+
+    # Unassigning a static IP
+    my $sth = $ip_dbh->prepare("select ip,custid,city,type,notes,circuitid,privdata".
+	" from poolips where ip='$webvar{block}'");
+    $sth->execute();
+#  croak $sth->errstr() if($sth->errstr());
+
+    $sth->bind_columns(\$cidr, \$custid, \$city, \$alloctype, \$notes, \$circid,
+	\$privdata);
+    $sth->fetch() || croak $sth->errstr;
+
+  } else { # done with alloctype=~ /^.i$/
+
+    my $sth = $ip_dbh->prepare("select cidr,custid,type,city,circuitid,description,notes,privdata".
+	" from allocations where cidr='$webvar{block}'");
+    $sth->execute();
+#	croak $sth->errstr() if($sth->errstr());
+
+    $sth->bind_columns(\$cidr, \$custid, \$alloctype, \$city, \$circid, \$desc,
+	\$notes, \$privdata);
+    $sth->fetch() || carp $sth->errstr;
+  } # end cases for different alloctypes
+
+  # Munge everything into HTML
+  $html =~ s|Please confirm|Please confirm <b>removal</b> of|;
+  $html =~ s|\$\$BLOCK\$\$|$cidr|g;
+  $html =~ s|\$\$TYPEFULL\$\$|$disp_alloctypes{$alloctype}|g;
+  $html =~ s|\$\$ALLOCTYPE\$\$|$alloctype|g;
+  $html =~ s|\$\$CITY\$\$|$city|g;
+  $html =~ s|\$\$CUSTID\$\$|$custid|g;
+  $html =~ s|\$\$CIRCID\$\$|$circid|g;
+  $html =~ s|\$\$DESC\$\$|$desc|g;
+  $html =~ s|\$\$NOTES\$\$|$notes|g;
+
+  $html =~ s|\$\$ACTION\$\$|finaldelete|g;
+
+  # Set the warning text.
+  if ($alloctype =~ /^.[pd]$/) {
+    $html =~ s|<!--warn-->|<tr bgcolor="black"><td colspan="2"><div class="red">Warning: clicking confirm will remove this record entirely.<br>Any IPs allocated from this pool will also be removed!</div></td></tr>|;
+  } else {
+    $html =~ s|<!--warn-->|<tr bgcolor="black"><td colspan="2"><div class="red">Warning: clicking confirm will remove this record entirely.</div></td></tr>|;
+  }
+
+  my $i = 1;
+  # Check to see if user is allowed to do anything with sensitive data
+  if ($IPDBacl{$authuser} =~ /s/) {
+    $privdata = qq(<tr class="color).($i%2).qq("><td>Restricted data:</td>).
+	qq(<td class=regular>$privdata</td></tr>\n);
+    $i++;
+  }
+  $html =~ s/\$\$PRIVDATA\$\$/$privdata/g;
+
+  $i = ++$i % 2;
+  $html =~ s/\$\$BUTTONROWCOLOUR\$\$/color$i/;
+
+  print $html;
+} # end edit()
+
+
+# Delete an allocation.  Return it to the freeblocks table;  munge
+# data as necessary to keep as few records as possible in freeblocks
+# to prevent weirdness when allocating blocks later.
+# Remove IPs from pool listing if necessary
+sub finalDelete {
+  if ($IPDBacl{$authuser} !~ /d/) {
+    printError("You shouldn't have been able to get here.  Access denied.");
+    return;
+  }
+
+  # need to retrieve block data before deleting so we can notify on that
+  my ($cidr,$custid,$type,$city,$description) = getBlockData($ip_dbh, $webvar{block});
+
+  my ($code,$msg) = deleteBlock($ip_dbh, $webvar{block}, $webvar{alloctype});
+
+  if ($code eq 'OK') {
+    print "<div class=heading align=center>Success!  $webvar{block} deallocated.</div>\n";
+    syslog "notice", "$authuser deallocated '$webvar{alloctype}'-type netblock $webvar{block}".
+	" $custid, $city, desc='$description'";
+    # Notify tech@ when a block/IP is deallocated
+    mailNotify('tech@example.com',"REMOVED: $disp_alloctypes{$webvar{alloctype}} $webvar{block}",
+	"$disp_alloctypes{$webvar{alloctype}} $webvar{block} deallocated by $authuser\n".
+	"CustID: $custid\nCity: $city\nDescription: $description\n");
+  } else {
+    if ($webvar{alloctype} =~ /^.i$/) {
+      syslog "err", "$authuser could not deallocate static IP '$webvar{block}': '$msg'";
+      printError("Could not deallocate static IP $webvar{block}: $msg");
+    } else {
+      syslog "err", "$authuser could not deallocate netblock '$webvar{block}': '$msg'";
+      printError("Could not deallocate netblock $webvar{block}: $msg");
+    }
+  }
+
+} # finalDelete
+
+
+sub exitError {
+  my $errStr = $_[0];
+  printHeader('','');
+  print qq(<center><p class="regular"> $errStr </p>
+<input type="button" value="Back" onclick="history.go(-1)">
+</center>
+);
+  printFooter();
+  exit;
+} # errorExit
+
+
+# Just in case we manage to get here.
+exit 0;
Index: tags/2.5/cgi-bin/newcity.cgi
===================================================================
--- tags/2.5/cgi-bin/newcity.cgi	(revision 381)
+++ tags/2.5/cgi-bin/newcity.cgi	(revision 381)
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+# ipdb/cgi-bin/newcity.cgi
+# Add new city to database
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+# Copyright (C) 2004,2005 - Kris Deugau
+
+use strict;
+use warnings;
+#use CGI::Carp qw(fatalsToBrowser);
+use DBI;
+use CommonWeb qw(:ALL);
+use MyIPDB;
+#use POSIX qw(ceil);
+use NetAddr::IP;
+
+use Sys::Syslog;
+
+openlog "IPDB","pid","local2";
+
+# Collect the username from HTTP auth.  If undefined, we're in a test environment.
+my $authuser;
+if (!defined($ENV{'REMOTE_USER'})) {
+  $authuser = '__temptest';
+} else {
+  $authuser = $ENV{'REMOTE_USER'};
+}
+
+my %webvar = parse_post();
+cleanInput(\%webvar);
+
+my ($dbh,$errstr) = connectDB_My;
+my $sth;
+
+print "Content-type: text/html\n\n";
+
+if ($webvar{pop} eq 'on') {
+  $sth = $dbh->prepare("insert into cities (city,routing) values ('$webvar{city}','y')");
+} else {
+  $sth = $dbh->prepare("insert into cities (city,routing) values ('$webvar{city}','n')");
+}
+$sth->execute;
+
+if ($sth->err) {
+  print "Error adding city to database: ".$sth->errstr;
+  mailNotify('kdeugau@vianet.ca',"IPDB city add failure",
+	"$authuser could not add city '$webvar{city}' to database: ".$sth->errstr);
+  syslog "err", "$authuser could not add city '$webvar{city}' to database: ".$sth->errstr;
+} else {
+  print "City added.  Closing this window should refresh the page.";
+  syslog "notice", "$authuser added city/location '$webvar{pop}'".
+	(($webvar{pop} eq 'on') ? ' as POP location' : '');
+}
+
+finish($dbh);
Index: tags/2.5/cgi-bin/search.cgi
===================================================================
--- tags/2.5/cgi-bin/search.cgi	(revision 381)
+++ tags/2.5/cgi-bin/search.cgi	(revision 381)
@@ -0,0 +1,478 @@
+#!/usr/bin/perl
+# ipdb/cgi-bin/search.cgi
+# Started splitting search functions (quick and otherwise) from
+# main IPDB interface 03/11/2005
+###
+# SVN revision info
+# $Date$
+# SVN revision $Rev$
+# Last update by $Author$
+###
+# Copyright 2005,2006 Kris Deugau
+
+use strict;		
+use warnings;	
+use CGI::Carp qw(fatalsToBrowser);
+use DBI;
+use CommonWeb qw(:ALL);
+use MyIPDB;
+use POSIX qw(ceil);
+use NetAddr::IP;
+
+# Don't need a username or syslog here.  syslog left active for debugging.
+use Sys::Syslog;
+openlog "IPDBsearch","pid","local2";
+
+# Why not a global DB handle?  (And a global statement handle, as well...)
+# Use the connectDB function, otherwise we end up confusing ourselves
+my $ip_dbh;
+my $sth;
+my $errstr;
+($ip_dbh,$errstr) = connectDB_My;
+if (!$ip_dbh) {
+  printAndExit("Failed to connect to database: $errstr\n");
+}
+checkDBSanity($ip_dbh);
+initIPDBGlobals($ip_dbh);
+
+# Global variables
+my $RESULTS_PER_PAGE = 25;
+my %webvar = parse_post();
+cleanInput(\%webvar);
+
+if (defined($webvar{rpp})) {
+  ($RESULTS_PER_PAGE) = ($webvar{rpp} =~ /(\d+)/);
+}
+
+if (!defined($webvar{stype})) {
+  $webvar{stype} = "<NULL>";   #shuts up the warnings.
+}
+
+printHeader('Searching...');
+
+if ($webvar{stype} eq 'q') {
+  # Quick search.
+
+  if (!$webvar{input}) {
+    # No search term.  Display everything.
+    viewBy('all', '');
+  } else {
+    # Search term entered.  Display matches.
+    # We should really sanitize $webvar{input}, no?
+    my $searchfor;
+    # Chew up leading and trailing whitespace
+    $webvar{input} =~ s/^\s+//;
+    $webvar{input} =~ s/\s+$//;
+    if ($webvar{input} =~ /^\d+$/) {
+      # All-digits, new custID
+      $searchfor = "cust";
+    } elsif ($webvar{input} =~ /^[\d\.]+(\/\d{1,3})?$/) {
+      # IP addresses should only have numbers, digits, and maybe a slash+netmask
+      $searchfor = "ipblock";
+    } else {
+      # Anything else.
+      $searchfor = "desc";
+    }
+    viewBy($searchfor, $webvar{input});
+  }
+
+} elsif ($webvar{stype} eq 'c') {
+  # Complex search.
+
+  # Several major cases, and a whole raft of individual cases.
+  # -> Show all types means we do not need to limit records retrieved by type
+  # -> Show all cities means we do not need to limit records retrieved by city
+  # Individual cases are for the CIDR/IP, CustID, Description, Notes, and individual type
+  # requests.
+
+  my $sqlconcat;
+  if ($webvar{which} eq 'all') {
+    # Must match *all* specified criteria.	## use INTERSECT or EXCEPT
+    $sqlconcat = "INTERSECT";
+  } elsif ($webvar{which} eq 'any') {
+    # Match on any specified criteria		## use UNION
+    $sqlconcat = "UNION";
+  } else {
+    # We can't get here.  PTHBTT!
+    printAndExit "PTHBTT!!  Your search has been rejected due to Microsoft excuse #4432: ".
+	"Not enough mana";
+  }
+
+# We actually construct a monster SQL statement for all criteria.
+# Iff something has been entered, it will be used as a filter.
+# Iff something has NOT been entered, we still include it but in
+# such a way that it does not actually filter anything out.
+
+  # Columns actually returned.  Slightly better than hardcoding it
+  # in each (sub)select
+  my $cols = "cidr,custid,type,city,description";
+
+  # First chunk of SQL.  Filter on custid, description, and notes as necessary.
+  my $sql = "(select $cols from searchme where".
+	" $webvar{custexclude} (custid ilike '%$webvar{custid}%'".
+	" OR $webvar{custexclude} oldcustid ilike '%$webvar{custid}%'))".
+	" $sqlconcat (select $cols from searchme where $webvar{descexclude} description ilike '%$webvar{desc}%')".
+	" $sqlconcat (select $cols from searchme where $webvar{notesexclude} notes ilike '%$webvar{notes}%')";
+
+  # If we're not supposed to search for all types, search for the selected types.
+  if ($webvar{alltypes} ne 'on') {
+    $sql .= " $sqlconcat (select $cols from searchme where $webvar{typeexclude} type in (";
+    foreach my $key (keys %webvar) {
+      $sql .= "'$1'," if $key =~ /type\[(..)\]/;
+    }
+    chop $sql;
+    $sql .= "))";
+  }
+
+  # If we're not supposed to search for all cities, search for the selected cities.
+  # This could be vastly improved with proper foreign keys in the database.
+  if ($webvar{allcities} ne 'on') {
+    $sql .= " $sqlconcat (select $cols from searchme where $webvar{cityexclude} city in (";
+    $sth = $ip_dbh->prepare("select city from cities where id=?");
+    foreach my $key (keys %webvar) {
+      if ($key =~ /city\[(\d+)\]/) {
+        $sth->execute($1);
+        my $city;
+        $sth->bind_columns(\$city);
+        $sth->fetch;
+        $city =~ s/'/''/;
+        $sql .= "'$city',";
+      }
+    }
+    chop $sql;
+    $sql .= "))";
+  }
+
+  ## CIDR query options.
+  $webvar{cidr} =~ s/\s+//;	# Hates the nasty spaceseseses we does.
+  if ($webvar{cidr} eq '') { # We has a blank CIDR.  Ignore it.
+  } elsif ($webvar{cidr} =~ /\//) {
+    # 209.91.179/26 should show all /26 subnets in 209.91.179
+    my ($net,$maskbits) = split /\//, $webvar{cidr};
+    if ($webvar{cidr} =~ /^(\d{1,3}\.){3}\d{1,3}\/\d{2}$/) {
+      # /0->/9 are silly to worry about right now.  I don't think
+      # we'll be getting a class A anytime soon.  <g>
+      $sql .= " $sqlconcat (select $cols from searchme where ".
+	"$webvar{cidrexclude} cidr<<='$webvar{cidr}')";
+    } else {
+      # Partial match;  beginning of subnet and maskbits are provided
+      # Show any blocks with the leading octet(s) and that masklength
+      # Need some more magic for bare /nn searches:
+      my $condition = ($net eq '' ?
+	"masklen(cidr)=$maskbits" : "text(cidr) like '$net%' and masklen(cidr)=$maskbits");
+      $sql .= " $sqlconcat (select $cols from searchme where $webvar{cidrexclude} ".
+	"($condition))";
+    }
+  } elsif ($webvar{cidr} =~ /^(\d{1,3}\.){3}\d{1,3}$/) {
+    # Specific IP address match.  Will show either a single netblock,
+    # or a static pool plus an IP.
+    $sql .= " $sqlconcat (select $cols from searchme where $webvar{cidrexclude} ".
+	"cidr >>= '$webvar{cidr}')";
+  } elsif ($webvar{cidr} =~ /^\d{1,3}(\.(\d{1,3}(\.(\d{1,3}\.?)?)?)?)?$/) {
+    # Leading octets in CIDR
+    $sql .= " $sqlconcat (select $cols from searchme where $webvar{cidrexclude} ".
+	"text(cidr) like '$webvar{cidr}%')";
+  } else {
+    # This shouldn't happen, but if it does, whoever gets it deserves what they get...
+    printAndExit("Invalid netblock query.");
+  } # done with CIDR query options.
+
+  # Find the offset for multipage results
+  my $offset = ($webvar{page}-1)*$RESULTS_PER_PAGE;
+
+  # Find out how many rows the "core" query will return.
+  my $count = countRows($sql);
+
+  if ($count == 0) {
+    printError "No matches found.  Try eliminating one of the criteria,".
+	" or making one or more criteria more general.";
+  } else {
+    # Add the limit/offset clauses
+    $sql .= " order by cidr";
+    $sql .= " limit $RESULTS_PER_PAGE offset $offset" if $RESULTS_PER_PAGE != 0;
+    # And tell the user.
+    print "<div class=heading>Searching...............</div>\n";
+    queryResults($sql, $webvar{page}, $count);
+  }
+
+} else { # how script was called.  General case is to show the search criteria page.
+
+  # Display search page.  We have to do this here, because otherwise
+  # we can't retrieve data from the database for the types and cities.  >:(
+  my $html;
+  open HTML,"<../compsearch.html";
+  $html = join('',<HTML>);
+  close HTML;
+
+# Generate table of types
+  my $typetable = "<table class=regular cellspacing=0>\n<tr>";
+  $sth = $ip_dbh->prepare("select type,dispname from alloctypes where listorder <500 ".
+	"order by listorder");
+  $sth->execute;
+  my $i=0;
+  while (my @data = $sth->fetchrow_array) {
+    $typetable .= "<td><input type=checkbox name=type[$data[0]]>$data[1]</td>";
+    $i++;
+    $typetable .= "</tr>\n<tr>"
+	if ($i % 4 == 0);
+  }
+  if ($i %4 == 0) {
+    $typetable =~ s/<tr>$//;
+  } else {
+    $typetable .= "</tr>\n";
+  }
+  $typetable .= "</table>\n";
+
+# Generate table of cities
+  my $citytable = "<table class=regular cellspacing=0>\n<tr>";
+  $sth = $ip_dbh->prepare("select id,city from cities order by city");
+  $sth->execute;
+  my $i=0;
+  while (my @data = $sth->fetchrow_array) {
+    $citytable .= "<td><input type=checkbox name=city[$data[0]]>$data[1]</td>";
+    $i++;
+    $citytable .= "</tr>\n<tr>"
+	if ($i % 5 == 0);
+  }
+  if ($i %5 == 0) {
+    $citytable =~ s/<tr>$//;
+  } else {
+    $citytable .= "</tr>\n";
+  }
+  $citytable .= "</table>\n";
+
+  $html =~ s/\$\$TYPELIST\$\$/$typetable/;
+  $html =~ s/\$\$CITYLIST\$\$/$citytable/;
+
+  print $html;
+}
+
+# Shut down and clean up.
+finish($ip_dbh);
+printFooter;
+# We shouldn't need to directly execute any code below here;  it's all subroutines.
+exit 0;
+
+
+# viewBy()
+# The quick search
+# Takes a category descriptor and a query string
+# Creates appropriate SQL to run the search and display the results
+# with queryResults()
+sub viewBy($$) {
+  my ($category,$query) = @_;
+
+  # Local variables
+  my $sql;
+
+  # Calculate start point for LIMIT clause
+  my $offset = ($webvar{page}-1)*$RESULTS_PER_PAGE;
+
+# Possible cases:
+# 1) Partial IP/subnet.  Treated as "octet-prefix".
+# 2a) CIDR subnet.  Exact match.
+# 2b) CIDR netmask.  YMMV but it should be octet-prefix-with-netmask
+#	(ie, all matches with the octet prefix *AND* that netmask)
+# 3) Customer ID.  "Match-any-segment"
+# 4) Description.  "Match-any-segment"
+# 5) Invalid data which might be interpretable as an IP or something, but
+#    which probably shouldn't be for reasons of sanity.
+
+  my $cols = "cidr,custid,type,city,description";
+
+  if ($category eq 'all') {
+
+    print qq(<div class="heading">Showing all netblock and static-IP allocations</div><br>\n);
+    $sql = "select $cols from searchme";
+    my $count = countRows($sql);
+    $sql .= " order by cidr limit $RESULTS_PER_PAGE offset $offset";
+    queryResults($sql, $webvar{page}, $count);
+
+  } elsif ($category eq 'cust') {
+
+    print qq(<div class="heading">Searching for Customer IDs containing '$query'</div><br>\n);
+
+    # Query for a customer ID.  Note that we can't restrict to "numeric-only"
+    # as we have non-numeric custIDs in the legacy data.  :/
+    $sql = "select $cols from searchme where custid ilike '%$query%' or oldcustid ilike '%$query%'";
+    my $count = countRows($sql);
+    $sql .= " order by cidr limit $RESULTS_PER_PAGE offset $offset";
+    queryResults($sql, $webvar{page}, $count);
+
+  } elsif ($category eq 'desc') {
+
+    print qq(<div class="heading">Searching for descriptions containing '$query'</div><br>\n);
+    # Query based on description (includes "name" from old DB).
+    $sql = "select $cols from searchme where description ilike '%$query%'".
+	" or custid ilike '%$query%'";
+    my $count = countRows($sql);
+    $sql .= " order by cidr limit $RESULTS_PER_PAGE offset $offset";
+    queryResults($sql, $webvar{page}, $count);
+
+  } elsif ($category =~ /ipblock/) {
+
+    # Query is for a partial IP, a CIDR block in some form, or a flat IP.
+    print qq(<div class="heading">Searching for IP-based matches on '$query'</div><br>\n);
+
+    $query =~ s/\s+//g;
+    if ($query =~ /\//) {
+      # 209.91.179/26 should show all /26 subnets in 209.91.179
+      my ($net,$maskbits) = split /\//, $query;
+      if ($query =~ /^(\d{1,3}\.){3}\d{1,3}\/\d{2}$/) {
+	# /0->/9 are silly to worry about right now.  I don't think
+	# we'll be getting a class A anytime soon.  <g>
+        $sql = "select $cols from searchme where cidr='$query'";
+	queryResults($sql, $webvar{page}, 1);
+      } else {
+	#print "Finding all blocks with netmask /$maskbits, leading octet(s) $net<br>\n";
+	# Partial match;  beginning of subnet and maskbits are provided
+	$sql = "select $cols from searchme where text(cidr) like '$net%' and ".
+		"text(cidr) like '%$maskbits'";
+	my $count = countRows($sql);
+	$sql .= " order by cidr limit $RESULTS_PER_PAGE offset $offset";
+	queryResults($sql, $webvar{page}, $count);
+      }
+    } elsif ($query =~ /^(\d{1,3}\.){3}\d{1,3}$/) {
+      # Specific IP address match
+      #print "4-octet pattern found;  finding netblock containing IP $query<br>\n";
+      my ($net,$ip) = ($query =~ /(\d{1,3}\.\d{1,3}\.\d{1,3}\.)(\d{1,3})/);
+      my $sfor = new NetAddr::IP $query;
+      $sth = $ip_dbh->prepare("select $cols from searchme where text(cidr) like '$net%'");
+      $sth->execute;
+      while (my @data = $sth->fetchrow_array()) {
+        my $cidr = new NetAddr::IP $data[0];
+	if ($cidr->contains($sfor)) {
+	  queryResults("select $cols from searchme where cidr='$cidr'", $webvar{page}, 1);
+	}
+      }
+    } elsif ($query =~ /^(\d{1,3}\.){1,3}\d{1,3}\.?$/) {
+      #print "Finding matches with leading octet(s) $query<br>\n";
+      $sql = "select $cols from searchme where text(cidr) like '$query%'";
+      my $count = countRows($sql);
+      $sql .= " order by cidr limit $RESULTS_PER_PAGE offset $offset";
+      queryResults($sql, $webvar{page}, $count);
+    } else {
+      # This shouldn't happen, but if it does, whoever gets it deserves what they get...
+      printError("Invalid query.");
+    }
+  } else {
+    # This shouldn't happen, but if it does, whoever gets it deserves what they get...
+    printError("Invalid searchfor.");
+  }
+} # viewBy
+
+
+# args are: a reference to an array with the row to be printed and the 
+# class(stylesheet) to use for formatting.
+# if ommitting the class - call the sub as &printRow(\@array)
+sub printRow {
+  my ($rowRef,$class) = @_;
+
+  if (!$class) {
+    print "<tr>\n";
+  } else {
+    print "<tr class=\"$class\">\n";
+  }
+
+ELEMENT:  foreach my $element (@$rowRef) {
+    if (!defined($element)) {
+      print "<td></td>\n";
+      next ELEMENT;
+    }
+    $element =~ s|\n|</br>|g;
+    print "<td>$element</td>\n";
+  }
+  print "</tr>";
+} # printRow
+
+
+# queryResults()
+# Display search queries based on the passed SQL.
+# Takes SQL, page number (for multipage search results), and a total count.
+sub queryResults($$$) {
+  my ($sql, $pageNo, $rowCount) = @_;
+  my $offset = 0;
+  $offset = $1 if($sql =~ m/.*limit\s+(.*),.*/);
+
+  my $sth = $ip_dbh->prepare($sql);
+  $sth->execute();
+
+  startTable('Allocation','CustID','Type','City','Description/Name');
+  my $count = 0;
+
+  while (my @data = $sth->fetchrow_array) {
+
+    # cidr,custid,type,city,description,notes
+    # Another bit of HairyPerl(TM) to prefix subblocks with "Sub"
+    my @row = (($data[2] =~ /^.r$/ ? 'Sub ' : '').
+	qq(<a href="/ip/cgi-bin/main.cgi?action=edit&block=$data[0]">$data[0]</a>),
+	$data[1], $disp_alloctypes{$data[2]}, $data[3], $data[4]);
+    # Allow listing of pool if desired/required.
+    if ($data[2] =~ /^.[pd]$/) {
+      $row[0] .= ' &nbsp; <a href="/ip/cgi-bin/main.cgi?action=listpool'.
+	"&pool=$data[0]\">List IPs</a>";
+    }
+    printRow(\@row, 'color1', 1) if ($count%2==0); 
+    printRow(\@row, 'color2', 1) if ($count%2!=0);
+    $count++;
+  }
+
+  # Have to think on this call, it's primarily to clean up unfetched rows from a select.
+  # In this context it's probably a good idea.
+  $sth->finish();
+
+  my $upper = $offset+$count;
+  print "<tr><td colspan=10 bgcolor=white class=regular>Records found: $rowCount<br><i>Displaying: ".($offset+1)." - $upper</i></td></tr>\n";
+  print "</table></center>\n";
+
+  # print the page thing..
+  if ($RESULTS_PER_PAGE > 0 && $rowCount > $RESULTS_PER_PAGE) {
+    my $pages = ceil($rowCount/$RESULTS_PER_PAGE);
+    print qq(<div class="center"> Page: );
+    for (my $i = 1; $i <= $pages; $i++) {
+      if ($i == $pageNo) {
+	print "<b>$i&nbsp;</b>\n";
+      } else {
+	print qq(<a href="/ip/cgi-bin/search.cgi?page=$i&stype=$webvar{stype}&);
+	if ($webvar{stype} eq 'c') {
+	  print "cidr=$webvar{cidr}&custid=$webvar{custid}&desc=$webvar{desc}&".
+		"notes=$webvar{notes}&which=$webvar{which}&alltypes=$webvar{alltypes}&".
+		"allcities=$webvar{allcities}&";
+	  foreach my $key (keys %webvar) {
+	    if ($key =~ /^(?:type|city)\[/ || $key =~ /exclude$/) {
+	      print "$key=$webvar{$key}&";
+	    }
+	  }
+	} else {
+	  print "input=$webvar{input}&";
+	}
+	print qq(">$i</a>&nbsp;\n);
+      }
+    }
+    print "</div>";
+  }
+} # queryResults
+
+
+# Prints table headings.  Accepts any number of arguments;
+# each argument is a table heading.
+sub startTable {
+  print qq(<center><table width="98%" cellspacing="0" class="center"><tr>);
+
+  foreach(@_) {
+    print qq(<td class="heading">$_</td>);
+  }
+  print "</tr>\n";
+} # startTable
+
+
+# Return count of rows to be returned in a "real" query
+# with the passed SQL statement
+sub countRows($) {
+  # Note that the "as foo" is required
+  my $sth = $ip_dbh->prepare("select count(*) from ($_[0]) as foo");
+  $sth->execute();
+  my @a = $sth->fetchrow_array();
+  $sth->finish();
+  return $a[0];
+}
Index: tags/2.5/cgi-bin/snCalc.cgi
===================================================================
--- tags/2.5/cgi-bin/snCalc.cgi	(revision 381)
+++ tags/2.5/cgi-bin/snCalc.cgi	(revision 381)
@@ -0,0 +1,103 @@
+#!/usr/bin/perl
+
+use strict;		
+use warnings;	
+use CGI::Carp qw(fatalsToBrowser);
+use NetAddr::IP;
+use CommonWeb qw(:ALL);;
+
+#file snCalc.cgi	little subnet calculator app
+
+my %webvar = parse_post();
+my $input;
+
+print "Content-Type: text/html\n\n";
+
+open(HTML, "../startsn.html")|| die "Could not open startsn.html :$!";
+my $start = join('', <HTML>);
+close(HTML);
+print $start;
+
+# Clean up input so we don't divide by zero or something equally silly
+if ($webvar{input} =~ m/(\d+)/) {
+  $input = 1*$1;
+  $input = 3 if $input < 3;
+  $input = 29 if $input > 29;	# Not doing IPv6 yet...
+} else {
+  $input = 29;
+}
+
+my $ltinput = $input - 1;
+my $gtinput = $input + 1;
+
+my $prenet = new NetAddr::IP "0.0.0.0/$ltinput";
+my $net = new NetAddr::IP "0.0.0.0/$input";
+my $postnet = new NetAddr::IP "0.0.0.0/$gtinput";
+
+print qq(<div class="center">
+<table align="center" cellspacing="3" cellpadding="3">
+<tr>
+	<td class="heading" align="center">Results for /$ltinput</td>
+	<td class="heading" align="center">Results for /$input</td>
+	<td class="heading" align="center">Results for /$gtinput</td>
+</tr>
+);
+
+print qq(<tr><td valign="top">\n).
+	qq(	<div class="mask">).$prenet->mask."</div>\n".
+	qq(	<div class="wildcard">).$prenet->wildcard."</div>\n".
+	getranges($ltinput).
+	qq(</td>\n<td valign="top" bgcolor="#d0e0e0">\n).
+	qq(	<div class="mask">).$net->mask."</div>\n".
+	qq(	<div class="wildcard">).$net->wildcard."</div>\n".
+	getranges($input).
+	qq(</td>\n<td valign="top">).
+	qq(	<div class="mask">).$postnet->mask."</div>\n".
+	qq(	<div class="wildcard">).$postnet->wildcard."</div>\n".
+	getranges($gtinput);
+
+print "</td></tr>\n</table>\n";
+
+print qq(<input type="button" value="Back" onclick="history.go(-1)" class="heading">
+</div>
+</body>
+</html>
+);
+	
+# Just In Case
+exit 0;
+
+# subs
+sub xrange {
+  my $block = shift;
+  my $masklen = shift;
+  my $data = $block->range;
+  if ($masklen >= 24) {
+    $data =~ s/\b0\.0\.0\./x.x.x./g;
+  } elsif ($masklen >=16) {
+    $data =~ s/\b0\.0\.(\d+\.\d+)/x.x.$1/g;
+  } elsif ($masklen >=8) {
+    $data =~ s/\b0\.(\d+\.\d+\.\d+)/x.$1/g;
+  }
+  return $data;
+} # xrange()
+
+
+sub getranges {
+  my $masklen = shift;
+  my $ret = '';
+  my $super;
+  if ($masklen < 8) {
+    $super = new NetAddr::IP "0.0.0.0/0";
+  } elsif ($masklen < 16) {
+    $super = new NetAddr::IP "0.0.0.0/8";
+  } elsif ($masklen < 24) {
+    $super = new NetAddr::IP "0.0.0.0/16";
+  } else {
+    $super = new NetAddr::IP "0.0.0.0/24";
+  }
+  foreach my $net ($super->split($masklen)) {
+    $ret .= "\t".xrange($net,$masklen)."<br />\n";
+  }
+  return $ret;
+} # getranges()
Index: tags/2.5/cgi-bin/updatecust.php
===================================================================
--- tags/2.5/cgi-bin/updatecust.php	(revision 381)
+++ tags/2.5/cgi-bin/updatecust.php	(revision 381)
@@ -0,0 +1,84 @@
+<?php
+/* updatecust.php
+** Stuff updated cust data into db
+*****
+** SVN revision info
+** $Date$
+** SVN revision $Rev$
+** Last update by $Author$
+*****
+** Copyright (C) 2006 - Kris Deugau
+*/
+
+  $db_conn = pg_connect("dbname=ipdb user=ipdb password=ipdbpwd");
+
+  if (!$db_conn) {
+    echo ("<H1>Failed connection to db ipdb.  Can't continue.</H1>");
+    exit;
+  }
+
+  $custid = $_REQUEST['custid'];
+  if (isset($_REQUEST['deletecust'])) {
+    print "<html><head><title>Deleting $custid from WHOIS</title></head><body>\n".
+	"<h2>Deleting $custid from WHOIS</h2>\n".
+	"<a href=\"/ip/cgi-bin/admin.cgi\">Back</a> to admin<br>\n".
+	"<a href=\"/ip/cgi-bin/list-cust.php\">Back</a> to WHOIS customer list\n";
+    $qu = pg_exec ("DELETE FROM customers WHERE custid='$custid'");
+    if (!$qu) {
+      print "Error deleting $custid.  Mail Kris.\n";
+    }
+    print "</body></html>\n";
+    exit;
+  }
+?>
+<html><head>
+<title>[IPDB admin] Updating WHOIS data</title>
+</head>
+<body>
+<h2>Updating WHOIS data for <? print $custid; ?></h2>
+<a href="/ip/cgi-bin/admin.cgi">Back</a> to admin<br>
+<a href="/ip/cgi-bin/list-cust.php">Back</a> to WHOIS customer list
+<?
+
+// snag form data into local vars.  Not being excessively paranoid because we're
+// buried deep and this is a hack anyway.
+
+  $name = $_REQUEST['name'];
+  $addr1 = $_REQUEST['addr1'];
+  $addr2 = $_REQUEST['addr2'];
+  $city = $_REQUEST['city'];
+  $prov = $_REQUEST['prov'];
+  $country = $_REQUEST['country'];
+  $pocode = $_REQUEST['pocode'];
+  $phone = $_REQUEST['phone'];
+  $tech = $_REQUEST['tech'];
+  $abuse = $_REQUEST['abuse'];
+  $admin = $_REQUEST['admin'];
+  $rdns = $_REQUEST['rdns'];
+  $special = $_REQUEST['special'];
+
+  $qu = pg_exec ($db_conn, "UPDATE customers SET name='$name',street='$addr1',street2='$addr2',".
+		"city='$city',province='$prov',country='$country',pocode='$pocode',phone='$phone',".
+		"tech_handle='$tech',abuse_handle='$abuse',admin_handle='$admin',".
+		"def_rdns='$rdns',special='$special' WHERE custid='$custid'");
+
+  echo "<table border=2 cellpadding=2>".
+	"<tr><td align=right>CustID</td><td>$custid</td></tr>\n".
+	"<tr><td>Name</td><td>$name</td></tr>\n".
+	"<tr><td>Street address</td><td>$addr1</td></tr>\n".
+	"<tr><td>Address 2</td><td>$addr2</td></tr>\n".
+	"<tr><td>City</td><td>$city</td></tr>\n".
+	"<tr><td>Province</td><td>$prov</td></tr>\n".
+	"<tr><td>Country</td><td>$country</td></tr>\n".
+	"<tr><td>Postal code</td><td>$pocode</td></tr>\n".
+	"<tr><td>Phone</td><td>$phone</td></tr>\n".
+	"<tr><td>Tech handle</td><td>$tech</td></tr>\n".
+	"<tr><td>Abuse handle</td><td>$abuse</td></tr>\n".
+	"<tr><td>Admin handle</td><td>$admin</td></tr>\n".
+	"<tr><td>Default rDNS</td><td>$rdns</td></tr>\n".
+	"<tr><td>\"Special\"</td><td><pre>$special</pre></td></tr>\n".
+	"</table>\n";
+
+?>
+</body>
+</html>
Index: tags/2.5/changes.html
===================================================================
--- tags/2.5/changes.html	(revision 381)
+++ tags/2.5/changes.html	(revision 381)
@@ -0,0 +1,45 @@
+<html><head>
+
+<title>IP Database</title><meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+
+<link rel="stylesheet" type="text/css" href="/ip/ipdb.css" />
+
+</head>
+<body bgcolor="#ffffff" text="#000000" link="#000000" vlink="#000000" alink="#ff0000" leftmargin="0" topmargin="0" marginwidth="0">
+
+<table class="regular">
+
+<tr><td class="heading" colspan=2>Changes to the IPDB</td><tr>
+
+<tr class="color2">
+<td valign=top>03/10/2005</td>
+<td>Finally!  Support for "container"/"reserve" netblocks to make tracking superblocks like
+209.91.191.0/24 easier.  Also supports "dynamically routed DSL netblocks";  ie,
+netblock-aligned chunks of static DSL IPs with netblock-ish subnet masks such as
+209.91.186.0/25.
+</td></tr>
+<tr class="color1">
+<td valign=top>02/09/2005</td>
+<td>Pool handling has been cleaned up - all PPP-ish pools (modem, DSL, WiFi) now generate a
+full list of IPs rather than reserving the network, gateway, and broadcast IPs necessary on
+a "real" subnet.  They will also not display this incorrect and useless information.
+</td></tr>
+<tr class="color2">
+<td valign=top>01/27/2005</td>
+<td>New cities/locations can now be added to the database here.
+</td></tr>
+<tr class="color1">
+<td valign=top>11/30/2004</td>
+<td>Overhauled the city listings;  they should be somewhat more complete now.  Still needs a
+mechanism to easily add new cities.
+</td></tr>
+<tr class="color2">
+<td valign=top>11/26/2004</td>
+<td>Allocating a static IP is now a little different.  Use the "Customer Location" field to
+enter the customer's actual city, and use the "Route from/through" field to pick the POP or
+cable node the IP should come from.
+</td></tr>
+</table>
+
+
+</body></html>
Index: tags/2.5/compsearch.html
===================================================================
--- tags/2.5/compsearch.html	(revision 381)
+++ tags/2.5/compsearch.html	(revision 381)
@@ -0,0 +1,42 @@
+<div class="indent">
+<div class="heading">Complex Search</div>
+<form action="/ip/cgi-bin/search.cgi" method=POST>
+<table bgcolor="black" cellspacing="1" cellpadding="2">
+<tr class="color1">
+<td colspan=2>Match on:<input type=radio checked name=which value="all">All
+<input type=radio name=which value="any">Any
+<td>Results per page: <select name=rpp>
+<option value='25'>25</option>
+<option value='50'>50</option>
+<option value='100'>100</option>
+<option value='200'>200</option>
+<option value='0'>Show all</option>
+</select>
+<td align=right><input type=submit value="Search Now"></td>
+</td>
+<tr class="color1">
+<td>Exclude?<input type=checkbox name=cidrexclude value='not'></td>
+<td>IP/netblock:</td><td colspan=2><input name=cidr></td>
+</tr><tr class="color2">
+<td>Exclude?<input type=checkbox name=custexclude value='not'></td>
+<td>CustID:</td><td colspan=2><input name=custid></td>
+</tr><tr class="color1">
+<td>Exclude?<input type=checkbox name=descexclude value='not'></td>
+<td>Description:</td><td colspan=2><input name=desc></td>
+</tr><tr class="color2">
+<td>Exclude?<input type=checkbox name=notesexclude value='not'></td>
+<td>Notes:</td><td colspan=2><input name=notes></td>
+</tr><tr class="color1">
+<td>Exclude?<input type=checkbox name=typeexclude value='not'></td>
+<td>Types:</td><td colspan=2><input type=checkbox name=alltypes checked>Show all types
+$$TYPELIST$$</td>
+</tr><tr class="color2">
+<td>Exclude?<input type=checkbox name=cityexclude value='not'></td>
+<td>Cities:</td><td colspan=2><input type=checkbox name=allcities checked>Show all cities
+$$CITYLIST$$</td>
+</tr>
+<input type=hidden name=stype value=c>
+<input type=hidden name=page value=1>
+</table>
+</form>
+</div>
Index: tags/2.5/confirm.html
===================================================================
--- tags/2.5/confirm.html	(revision 381)
+++ tags/2.5/confirm.html	(revision 381)
@@ -0,0 +1,32 @@
+<div class="heading">Please confirm</div>
+<div class="indent">
+<table class="regular" bgcolor="white" cellspacing="1" cellpadding="1">
+<form method="POST" action="main.cgi" class="regular">
+$$CUSTBITS$$
+<tr class="color1">
+<td>CIDR block to be allocated:&nbsp;</td><td>&nbsp;$$CIDR$$<input type=hidden name="fullcidr" value="$$CIDR$$"></td>
+</tr><tr class="color2">
+<td>Block allocation is taken from:&nbsp;</td><td>&nbsp;$$ALLOC_FROM$$</td>
+</tr><tr class="color1">
+<td>City:&nbsp;</td><td>&nbsp;$$CITY$$<input type="hidden" name="city" value="$$CITY$$"></td>
+</tr><tr class="color2">
+<td>Allocation type:&nbsp;</td><td>&nbsp;$$TYPEFULL$$</td>
+</tr><tr class="color1">
+<td>Customer ID:&nbsp;</td><td>&nbsp;$$CUSTID$$<input type="hidden" name="custid" value="$$CUSTID$$"></td>
+</tr><tr class="color2">
+<td valign="top">Cicruit ID:&nbsp;</td><td>&nbsp;$$CIRCID$$<input type="hidden" name="circid" value="$$CIRCID$$"></td>
+</tr><tr class="color1">
+<td valign="top">Description/name:&nbsp;</td><td>&nbsp;$$DESC$$<input type="hidden" name="desc" value="$$DESC$$"></td>
+</tr><tr class="color2">
+<td valign="top">Notes:&nbsp;</td><td>&nbsp;$$NOTES$$<input type="hidden" name="notes" value="$$NOTES$$"></td>
+</tr>
+$$PRIVDATA$$
+<!-- warn -->
+<input type="hidden" name="alloctype" value="$$ALLOC_TYPE$$">
+<input type="hidden" name="action" value="$$ACTION$$">
+<tr class="$$BUTTONROWCOLOUR$$">
+<td class="center" colspan="2">
+<input type="button" value="Back" onclick="history.go(-1)"><input type="submit" value="Confirm">
+</td></tr>
+</table>
+</div>
Index: tags/2.5/confirmRemove.html
===================================================================
--- tags/2.5/confirmRemove.html	(revision 381)
+++ tags/2.5/confirmRemove.html	(revision 381)
@@ -0,0 +1,19 @@
+<div class="heading">Please confirm:</div>
+<div class="indent">
+<table class="regular" bgcolor="white" cellspacing="1" cellpadding="1">
+<form action="main.cgi" method="POST" class="regular">
+<tr class="color1"><td>IP block:</td><td>$$BLOCK$$<input type=hidden name=block value="$$BLOCK$$"></td></tr>
+<tr class="color2"><td>City:</td><td>$$CITY$$<input type=hidden name=city value="$$CITY$$"></td></tr>
+<tr class="color1"><td>Type:</td><td>$$TYPEFULL$$<input type=hidden name=alloctype value="$$ALLOCTYPE$$"></td></tr>
+<tr class="color2"><td>Customer ID:</td><td>$$CUSTID$$</td></tr>
+<tr class="color2"><td>Circuit ID:</td><td>$$CIRCID$$</td></tr>
+<tr class="color1"><td valign="top">Description/Name:</td><td>$$DESC$$</td></tr>
+<tr class="color2"><td valign="top">Notes:</td><td>$$NOTES$$</td></tr>
+$$PRIVDATA$$
+<!--warn-->
+<input type="hidden" name="action" value="$$ACTION$$">
+<tr class="$$BUTTONROWCOLOUR$$"><td class="center" colspan=2>
+<input type="button" value="Back" onclick="history.go(-1)"><input type="submit" value="Confirm">
+</td></tr>
+</table>
+</div>
Index: tags/2.5/editDisplay.html
===================================================================
--- tags/2.5/editDisplay.html	(revision 381)
+++ tags/2.5/editDisplay.html	(revision 381)
@@ -0,0 +1,33 @@
+<div class="heading"><div class="indent">Edit the information below</div></div>
+
+<table class="indent" size="33%" cellspacing=1 bgcolor="black">
+<form method="POST" action="main.cgi">
+<input type="hidden" name="action" value="update">
+<input type="hidden" name="block" value="$$BLOCK$$">
+
+<tr class="color1"><td class=heading>IP block:</td><td class="regular">$$BLOCK$$</td></tr>
+
+<tr class="color2"><td class=heading>City:</td><td class="regular">$$CITY$$</td></tr>
+
+<tr class="color1"><td class=heading>Type:</td><td class=regular>$$TYPESELECT$$</td></tr>
+
+<tr class="color2"><td class=heading>CustID:</td><td class="regular">$$CUSTID$$</td></tr>
+<tr class="color2"><td class=heading>Old CustID:</td><td class="regular">$$OLDCUSTID$$</td></tr>
+
+<tr class="color2"><td class=heading>SWIPed?:</td><td class=regular>$$SWIP$$</td></tr>
+
+<tr class="color1"><td class=heading>Last modified:</td><td class=regular>$$LASTMOD$$</td></tr>
+
+<tr class="color2"><td class="heading">Circuit ID:</td><td class="regular">$$CIRCID$$</td></tr>
+
+<tr class="color1"><td class="heading">Description/Name:</td><td class="regular">$$DESC$$</td></tr>
+
+<tr class="color2"><td class="heading" valign="top">Notes:</td><td class="regular">$$NOTES$$</td></tr>
+
+$$PRIVDATA$$
+$$UPDOK$$
+$$DELOK$$
+</form>
+
+</table>
+</br>
Index: tags/2.5/fb-assign.html
===================================================================
--- tags/2.5/fb-assign.html	(revision 381)
+++ tags/2.5/fb-assign.html	(revision 381)
@@ -0,0 +1,32 @@
+<div class="indent">
+<div class="heading">Assign IPs</div><br>
+<table class="regular" bgcolor="black" cellspacing="1" cellpadding="1">
+<form method="POST" action="main.cgi" class="regular">
+<tr class="color1"><td>Free block selected for assignment:&nbsp;</td><td>$$BLOCK$$<input type=hidden name=block value="$$BLOCK$$"></td></tr>
+<tr class="color2">
+<td>City:&nbsp;</td><td>
+<select name="city"><option selected>-</option>
+$$ALLCITIES$$
+</select>
+</td>
+</tr>
+<tr class="color1">
+<td>Allocation type:</td><td>
+$$TYPELIST$$
+<input type="button" value=" ? " onclick="helpAllocTypes()" class="regular">
+</td>
+</tr><tr class="color2">
+<td>Customer ID:&nbsp;</td><td><input type="text" name="custid" size="15" maxlength="15"> (Only required for Customer allocations)</td>
+</tr><tr class="color1">
+<td valign="top">Description/Name:&nbsp;</td><td><input name="desc" size=40></td>
+</tr><tr class="color2">
+<td>Notes:&nbsp;</td><td><textarea name="notes" rows="3" cols="40"></textarea></td>
+</tr>
+$$PRIVDATA$$
+<tr class="$$BUTTONROWCOLOUR$$">
+<td class="center" colspan="2"><input type="submit" value="  Assign  "></td>
+<input type="hidden" name="action" value="confirm">
+<input type="hidden" name=fbassign value=y>
+</tr>
+</table>
+</div>
Index: tags/2.5/footer.inc
===================================================================
--- tags/2.5/footer.inc	(revision 381)
+++ tags/2.5/footer.inc	(revision 381)
@@ -0,0 +1,19 @@
+<br><br>
+<table width="100%" border="0" cellspacing="0" cellpadding="0" height="1" bgcolor="#000000">
+  <tr>
+    <td></td>
+  </tr>
+</table>
+<center>
+<table width="98%" border="0" cellspacing="0" cellpadding="1">
+  <tr>
+    <td nowrap="" width="0" height="0" valign="top"> 
+      <div align="right"><font face="Arial, Helvetica, sans-serif" size="1">contact: 
+        <a href="mailto:kdeugau@deepnet.cx">kdeugau@deepnet.cx</a> - (c) 2004 <a href="http://www.deepnet.cx/" target="_blank">deepnet</a><br>
+Written for standards-based browsers (eg <a href="http://www.netscape.com">Netscape</a>/<a href="http://www.mozilla.org">Mozilla</a>)</font></div>
+    </td>
+  </tr>
+</table>
+</center>
+</body>
+</html>
Index: tags/2.5/header.inc
===================================================================
--- tags/2.5/header.inc	(revision 381)
+++ tags/2.5/header.inc	(revision 381)
@@ -0,0 +1,63 @@
+<html>
+<head>
+	<title>[IP Database v2]</title>
+	<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> 
+	<link rel="stylesheet" type="text/css" href="/ip/ipdb.css">
+
+<script language=javascript>
+function openHelp() {
+	window.open("/ip/help.html", "help_window", "toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,copyhistory=no,width=700,height=400")
+}
+function openTables() {
+	window.open("/ip/tables.html", "subnet_tables", "toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,copyhistory=no,width=600,height=250")	
+}
+function helpAllocTypes() {
+	window.open("/ip/alloctypes.html", "alloc_window", "toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,copyhistory=no,width=750,height=550")
+}
+function popNotes(page) {
+	window.open(page, "IPDB_notes", "toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,copyhistory=no,width=400,height=300");
+}
+</script>
+
+</head>
+<body bgcolor="#ffffff" text="#000000" link="#000000" vlink="#000000" alink="#ff0000" leftmargin="0" topmargin="0" marginwidth="0" style="font-family: helvetica">
+
+<table width="98%" border="0" cellspacing="0" cellpadding="0" height="0">
+  <tbody><tr>
+      <td nowrap=""><a href="/ip/index.shtml" target="_blank"><img src="/ip/images/logo.png" width="167" height="60" border="0" alt="[ Logo ]"></a></td>
+    <td nowrap="" valign="bottom"> 
+      <div align="right"><b><font face="Verdana, Arial, Helvetica, sans-serif" size="2">
+	  <a href="/ip/index.shtml">
+	  [IP Database v2 Home]</a></font></b></div>
+    </td>
+  </tr>
+</tbody></table>
+
+<table width="100%" border="0" cellspacing="0" cellpadding="0">
+<tr bgcolor="#000000"><td></td></tr></table>
+<!-- end line and top. -->
+<table width="100%" cellspacing="0">
+<tr class="color1">
+<td width=10></td>
+<form method="POST" action="/ip/cgi-bin/search.cgi">
+<td width=390>Quick Search:
+<input type="text" name="input" size="20" maxlength="50" class="regular">
+<input type=hidden name=page value="1">
+<input type=hidden name=stype value="q">
+<input type=submit value="Go!" class="heading">
+<input type="button" value=" Help? " onclick="openHelp()" class="regular">
+</td><td width=10></td><td><a href="/ip/cgi-bin/search.cgi">Complex Search</a></td>
+<td width=60></td>
+$$EXTRA0$$
+</form>
+</tr>
+</table>
+<!-- start another line -->
+<!-- There has GOT to be a better way to do this...  Ugh.  -->
+<table width="100%" border="0" cellspacing="0" cellpadding="0" height="1" bgcolor="#000000">
+  <tbody><tr>
+   <td></td>
+  </tr>
+</tbody></table>
+<table width="98%" border=0><tr><td align=right><a href="javascript:popNotes('/ip/changes.html')">Recent Changes</a></td></tr></table>
+<br>
Index: tags/2.5/help.html
===================================================================
--- tags/2.5/help.html	(revision 381)
+++ tags/2.5/help.html	(revision 381)
@@ -0,0 +1,65 @@
+<html><head>
+
+<title>IP Database</title><meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
+
+<link rel="stylesheet" type="text/css"
+href="/ip/ipdb.css" />
+
+</head>
+<body bgcolor="#ffffff" text="#000000" link="#000000" vlink="#000000" alink="#ff0000" leftmargin="0" topmargin="0" marginwidth="0">
+
+<table class="regular">
+
+<tr><td class="heading">Quick Searches:</td><tr>
+
+<tr class="color1">
+<td>IP blocks</td><td>209.91.128 or 209.</td>
+<td>Lists all alloctions starting with that set of octets.  Note that matches on the
+first octet MUST include the period to be considered an IP search.</td>
+</tr>
+<tr class="color2">
+<td>CIDR blocks</td><td>209.92.128/30</br>or
+209.92.128.0/30</td><td>Lists all /30's beginning with 209.91.128 or checks for an
+exact match for 209.92.128.0/30 respectively</td>
+</tr>
+<tr class="color1">
+<td>IP address</td><td>209.92.128.30</td><td>Finds the alloction that IP is a
+part of (if any)</td>
+</tr>
+<tr class="color2">
+<td>Customer ID:</td><td>705 or 6137328844 or 503101</td><td>Find all alloctions to that
+customer. Matches on prefix (area code, area code + exchange, etc).  Searches old as well as 
+new CustIDs.
+</td>
+</tr>
+<tr class="color1">
+<td>Description:</td><td>cable or efni</td><td>Find all allocations with the search term in
+the description.  Note that searches for CustIDs with letters will fall under this category
+until CustIDs are all-numeric.</td>
+</tr>
+
+<tr class="regular"><!-- blank row --><td></td></tr>
+<tr><td colspan="3">A blank query will "show all"</td></tr>
+<tr><td colspan="3">The title in the top right hand corner is a link home.</td></tr>
+
+<tr class="regular"><!-- blank row --><td></td></tr>
+<tr class="regular"><!-- blank row --><td></td></tr>
+<tr class="regular"><!-- blank row --><td></td></tr>
+
+<tr class="color1"><td>Subnet Calculator</td>
+<td colspan="2">
+<form method="POST" action="cgi-bin/snCalc.cgi">
+ / <input type="text" size="5" maxlength="10" name="input" class="regular">
+<input type="submit" value="Calculate" class="heading">&nbsp; Show
+<span class="mask">subnet mask</span>, <span class="wildcard">wildcard mask</span>, 
+and possible subnet ranges for the entered mask length.
+</form>
+</tr>
+
+<tr><td>
+
+
+</table>
+
+
+</body></html>
Index: tags/2.5/index.shtml
===================================================================
--- tags/2.5/index.shtml	(revision 381)
+++ tags/2.5/index.shtml	(revision 381)
@@ -0,0 +1,1 @@
+<!--#include virtual="/ip/cgi-bin/main.cgi?action=index" -->
Index: tags/2.5/ipdb.css
===================================================================
--- tags/2.5/ipdb.css	(revision 381)
+++ tags/2.5/ipdb.css	(revision 381)
@@ -0,0 +1,90 @@
+a:active {
+  text-decoration: underline;
+  color: #ff0000;
+}
+
+a:hover {
+  text-decoration: underline;
+  color: #0000ff;
+}
+
+a:link {
+  text-decoration: underline;
+  color: #000000;
+}
+
+a:visited {
+  text-decoration: underline;
+  color: #683080;
+}
+
+tr.color0 {
+	background-color: #A8C4D0;
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+	font-size: 90%;
+}
+
+tr.color1 {
+	background-color: #d0e0e0;
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+	font-size: 90%;
+}
+
+tr.color2 {
+	background-color: #A8C4D0;
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+	font-size: 90%;
+}
+
+tr.warning {
+	background-color: #000000;
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+	font-weight: bold;
+	color: red;
+}
+
+tr.header {
+	background-color: #CCCCCC;
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+}
+
+.indent {
+	margin-left: 5%;
+	font-size: 90%;
+}
+
+.regular { 
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+	font-size: 100%;
+}
+
+.heading {
+	font-size: 110%;
+	font-weight: bold;
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+}
+
+.small {
+	font-size: 60%;
+}
+
+.center {
+	text-align: center;
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+	font-size: 100%;
+}
+
+.red {
+	font-weight: bold;
+	font-family: Verdana, Arial, Helvetica, sans-serif;
+	font-size:100%;
+	color: red;
+}
+
+.mask {
+	background: #00ff00;
+}
+
+.wildcard {
+	background: #ffff00;
+}
Index: tags/2.5/newcity.html
===================================================================
--- tags/2.5/newcity.html	(revision 381)
+++ tags/2.5/newcity.html	(revision 381)
@@ -0,0 +1,26 @@
+<html>
+<head>
+	<title>IP Database - Add new city</title>
+	<link rel="stylesheet" type="text/css" href="/ip/ipdb.css" />
+
+	<SCRIPT language="JavaScript" type="text/javascript">
+	<!-- Hide From Non-Supporting Browsers
+	// snagged from http://www.webxpertz.net/forums/showthread.php?t=24650
+	function redoParent() {
+		opener.location.reload(true);
+	}
+
+	// Done Hiding -->
+	</script>
+
+</head>
+
+<body bgcolor="#ffffff" text="#000000" link="#000000" vlink="#000000" alink="#ff0000" leftmargin="0" topmargin="0" marginwidth="0" onUnload="redoParent()">
+<table>
+<form action="cgi-bin/newcity.cgi" method="POST">
+<tr class="color1"><td>Enter new city:</td><td><input name="city"></td></tr>
+<tr class="color2"><td>POP site?</td><td><input type=checkbox name="pop"></td></tr>
+<tr class="color1"><td colspan=2 align=center><input type=submit value="Add city"></td></tr>
+</table>
+</body>
+</html>
Index: tags/2.5/startsn.html
===================================================================
--- tags/2.5/startsn.html	(revision 381)
+++ tags/2.5/startsn.html	(revision 381)
@@ -0,0 +1,10 @@
+<html>
+<head>
+
+<title>IP Database</title>
+
+<link rel="stylesheet" type="text/css"
+href="/ip/ipdb.css" />
+
+</head>
+<body bgcolor="#ffffff" text="#000000" link="#000000" vlink="#000000" alink="#ff0000" leftmargin="0" topmargin="0" marginwidth="0">
Index: tags/2.5/updated.html
===================================================================
--- tags/2.5/updated.html	(revision 381)
+++ tags/2.5/updated.html	(revision 381)
@@ -0,0 +1,18 @@
+<div class="heading">Allocation info updated:</div>
+<div class="indent">
+<table class="regular" bgcolor="white" cellspacing="1" cellpadding="1">
+<tr class="color1"><td>IP block:</td><td>$$BLOCK$$</td></tr>
+<tr class="color2"><td>City:</td><td>$$CITY$$</td></tr>
+<tr class="color1"><td>Type:</td><td>$$TYPEFULL$$</td></tr> 
+<tr class="color2"><td>Customer ID:</td><td>$$CUSTID$$</td></tr>
+<tr class="color2"><td>SWIPed?:</td><td>$$SWIP$$</td></tr>
+<tr class="color1"><td>Circuit ID:</td><td>$$CIRCID$$</td></tr>
+<tr class="color2"><td valign="top">Description/Name:</td><td>$$DESC$$</td></tr>
+<tr class="color1"><td valign="top">Notes:</td><td>$$NOTES$$</td></tr>
+$$PRIVDATA$$
+</table>
+</div>
+<p>
+<div name="backlink">
+<a href="$$BACKLINK$$">Back to $$BACKBLOCK$$</a>
+</div>
