source: trunk/cgi-bin/IPDB.pm@ 745

Last change on this file since 745 was 745, checked in by Kris Deugau, 10 years ago

/trunk

Add a flag to _toPool() to indicate whether to return all of the
allocations converted. Defaults to "off", but required so that rDNS
deletes can be called with the complete list of leaf blocks converted.
Also return type code of merged/returned blocks for further internal
processing.
See #8.

  • Property svn:keywords set to Date Rev Author
File size: 112.6 KB
RevLine 
[8]1# ipdb/cgi-bin/IPDB.pm
[66]2# Contains functions for IPDB - database access, subnet mangling, block allocation, etc
[8]3###
4# SVN revision info
5# $Date: 2015-06-16 21:47:30 +0000 (Tue, 16 Jun 2015) $
6# SVN revision $Rev: 745 $
7# Last update by $Author: kdeugau $
8###
[417]9# Copyright (C) 2004-2010 - Kris Deugau
[8]10
[4]11package IPDB;
12
13use strict;
14use warnings;
15use Exporter;
[77]16use DBI;
[66]17use Net::SMTP;
[573]18use NetAddr::IP qw(:lower Compact );
[582]19use Frontier::Client;
[68]20use POSIX;
[4]21use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
22
[417]23$VERSION = 2; ##VERSION##
[4]24@ISA = qw(Exporter);
[106]25@EXPORT_OK = qw(
[541]26 %disp_alloctypes %list_alloctypes %def_custids @citylist @poplist
[733]27 %IPDBacl %merge_display %aclmsg %rpcacl $maxfcgi
[660]28 $errstr
[523]29 &initIPDBGlobals &connectDB &finish &checkDBSanity
[547]30 &addMaster &touchMaster
[720]31 &listSummary &listSubs &listContainers &listAllocations &listForMerge &listFree &listPool
[541]32 &getMasterList &getTypeList &getPoolSelect &findAllocateFrom
[697]33 &ipParent &subParent &blockParent &getBreadCrumbs &getRoutedCity
[728]34 &allocateBlock &updateBlock &splitBlock &shrinkBlock &mergeBlocks &deleteBlock &getBlockData
[705]35 &getBlockRDNS &getRDNSbyIP
[530]36 &getNodeList &getNodeName &getNodeInfo
[519]37 &mailNotify
[106]38 );
[4]39
40@EXPORT = (); # Export nothing by default.
[106]41%EXPORT_TAGS = ( ALL => [qw(
[167]42 %disp_alloctypes %list_alloctypes %def_custids @citylist @poplist
[733]43 %IPDBacl %merge_display %aclmsg %rpcacl $maxfcgi
[660]44 $errstr
[523]45 &initIPDBGlobals &connectDB &finish &checkDBSanity
[547]46 &addMaster &touchMaster
[720]47 &listSummary &listSubs &listContainers &listAllocations &listForMerge &listFree &listPool
[541]48 &getMasterList &getTypeList &getPoolSelect &findAllocateFrom
[697]49 &ipParent &subParent &blockParent &getBreadCrumbs &getRoutedCity
[728]50 &allocateBlock &updateBlock &splitBlock &shrinkBlock &mergeBlocks &deleteBlock &getBlockData
[705]51 &getBlockRDNS &getRDNSbyIP
[530]52 &getNodeList &getNodeName &getNodeInfo
[519]53 &mailNotify
[106]54 )]
55 );
[4]56
[77]57##
58## Global variables
59##
60our %disp_alloctypes;
61our %list_alloctypes;
[167]62our %def_custids;
[96]63our @citylist;
64our @poplist;
[233]65our %IPDBacl;
[66]66
[725]67# Mapping hash for pooltype -> poolip-as-netblock conversions
68my %poolmap = (sd => 'en', cd => 'cn', dp => 'cn', mp => 'cn', wp => 'cn', ld => 'in', ad => 'in', bd => 'in');
69
[733]70# Friendly display strings for merge scopes
71our %merge_display = (
72 keepall => "Keep mergeable allocations as suballocations of new block",
73 mergepeer => "Keep suballocations of mergeable allocations",
74 clearpeer => "Keep only suballocations of the selected block",
75 clearall => "Clear all suballocations"
76 );
77
[517]78# mapping table for functional-area => error message
79our %aclmsg = (
80 addmaster => 'add a master block',
81 addblock => 'add an allocation',
82 updateblock => 'update a block',
83 delblock => 'delete an allocation',
84 );
85
[660]86our %rpcacl;
[661]87our $maxfcgi = 3;
[660]88
[585]89# error reporting
90our $errstr = '';
91
[417]92our $org_name = 'Example Corp';
[416]93our $smtphost = 'smtp.example.com';
94our $domain = 'example.com';
[417]95our $defcustid = '5554242';
[681]96our $smtpsender = 'ipdb@example.com';
[417]97# mostly for rwhois
98##fixme: leave these blank by default?
[420]99our $rwhoisDataPath = '/usr/local/rwhoisd/etc/rwhoisd'; # to match ./configure defaults from rwhoisd-1.5.9.6
[417]100our $org_street = '123 4th Street';
101our $org_city = 'Anytown';
102our $org_prov_state = 'ON';
103our $org_pocode = 'H0H 0H0';
104our $org_country = 'CA';
105our $org_phone = '000-555-1234';
106our $org_techhandle = 'ISP-ARIN-HANDLE';
[434]107our $org_email = 'noc@example.com';
[437]108our $hostmaster = 'dns@example.com';
[416]109
[417]110our $syslog_facility = 'local2';
111
[582]112our $rpc_url = '';
[585]113our $revgroup = 1; # should probably be configurable somewhere
114our $rpccount = 0;
[582]115
[674]116# Largest inverse CIDR mask length to show per-IP rDNS list
117# (eg, NetAddr::IP->bits - NetAddr::IP->masklen)
118our $maxrevlist = 5; # /27
119
[682]120# UI layout for subblocks/containers
121our $sublistlayout = 1;
122
[691]123# VLAN validation mode. Set to 0 to allow alphanumeric vlan names instead of using the vlan number.
124our $numeric_vlan = 1;
125
126
[585]127##
128## Internal utility functions
129##
130
131## IPDB::_rpc
132# Make an RPC call for DNS changes
133sub _rpc {
134 return if !$rpc_url; # Just In Case
135 my $rpcsub = shift;
136 my %args = @_;
137
138 # Make an object to represent the XML-RPC server.
139 my $server = Frontier::Client->new(url => $rpc_url, debug => 0);
140 my $result;
141
142 my %rpcargs = (
143 rpcsystem => 'ipdb',
[640]144# must be provided by caller's caller
145# rpcuser => $args{user},
146 %args,
[585]147 );
148
149 eval {
[640]150 $result = $server->call("dnsdb.$rpcsub", %rpcargs);
[585]151 };
152 if ($@) {
153 $errstr = $@;
[678]154 $errstr =~ s/\s*$//;
[585]155 $errstr =~ s/Fault returned from XML RPC Server, fault code 4: error executing RPC `dnsdb.$rpcsub'\.\s//;
156 }
157 $rpccount++;
158
159 return $result if $result;
160} # end _rpc()
161
162
[715]163## IPDB::_compactFree()
164# Utility sub to compact a set of free block entries down to the minimum possible set of CIDR entries
165# Not to be called outside of an eval{}!
166sub _compactFree {
167 my $dbh = shift;
168 my $parent = shift;
169
170 # Rather than having the caller provide all the details
171 my $pinfo = getBlockData($dbh, $parent);
[722]172 my $ftype = (split //, $pinfo->{type})[0];
[715]173
174# NetAddr::IP->compact() attempts to produce the smallest inclusive block
175# from the caller and the passed terms.
176# EG: if you call $cidr->compact($ip1,$ip2,$ip3) when $cidr, $ip1, $ip2,
177# and $ip3 are consecutive /27's starting on .0 (.0-.31, .32-.63,
178# .64-.95, and .96-.128), you will get an array containing a single
179# /25 as element 0 (.0-.127). Order is not important; you could have
180# $cidr=.32/27, $ip1=.96/27, $ip2=.0/27, and $ip3=.64/27.
181
182##fixme: vrf
183##fixme: simplify since all containers now represent different "layers"/"levels"?
184
185 # set up the query to get the list of blocks to try to merge.
186 my $sth = $dbh->prepare(q{
187 SELECT cidr,id FROM freeblocks
188 WHERE parent_id = ?
189 ORDER BY masklen(cidr) DESC
190 });
191 $sth->execute($parent);
192
193 my (@rawfb, @combinelist, %rawid);
194 my $i=0;
195 # for each free block under $parent, push a NetAddr::IP object into one list, and
196 # continuously use NetAddr::IP->compact to automagically merge netblocks as possible.
197 while (my ($fcidr, $fid) = $sth->fetchrow_array) {
198 my $testIP = new NetAddr::IP $fcidr;
199 push @rawfb, $testIP;
200 $rawid{"$testIP"} = $fid; # $data[0] vs "$testIP" *does* make a difference for v6
201 @combinelist = $testIP->compact(@combinelist);
202 }
203
204 # now that we have the full list of "compacted" freeblocks, go back over
205 # the list of raw freeblocks, and delete the ones that got merged.
206 $sth = $dbh->prepare("DELETE FROM freeblocks WHERE id = ?");
207 foreach my $rawfree (@rawfb) {
208 next if grep { $rawfree == $_ } @combinelist; # skip if the raw block is in the compacted list
209 $sth->execute($rawid{$rawfree});
210 }
211
212 # now we walk the new list of compacted blocks, and see which ones we need to insert
[723]213 $sth = $dbh->prepare("INSERT INTO freeblocks (cidr,city,routed,vrf,parent_id,master_id) VALUES (?,?,?,?,?,?)");
[715]214 foreach my $cme (@combinelist) {
215 next if grep { $cme == $_ } @rawfb; # skip if the combined block was in the raw list
[722]216 $sth->execute($cme, $pinfo->{city}, $ftype, $pinfo->{vrf}, $parent, $pinfo->{master_id});
[715]217 }
218
219} # end _compactFree()
220
221
[736]222## IPDB::_toPool()
223# Convert an allocation or allocation tree to entries in an IP pool
224# Assumes an incomplete/empty pool
225# Takes a parent ID for the pool, CIDR range descriptor for the allocation(s) to convert, and the pool type
226sub _toPool {
227 my $dbh = shift;
228 my $poolparent = shift;
229 my $convblock = shift; # May be smaller than the block referenced by $poolparent
230 my $pooltype = shift;
[745]231 my $retall = shift || 0;
[736]232
233 # there is probably a way to avoid the temporary $foo here
234 my $foo = $dbh->selectall_arrayref("SELECT master_id,parent_id FROM allocations WHERE id = ?", undef, $poolparent);
235 my ($master,$mainparent) = @{$foo->[0]};
236
237 my @retlist;
238
239 my $iptype = $pooltype;
240 $iptype =~ s/[pd]$/i/;
241 my $poolclass = (split //, $iptype)[0];
242
243 my $cidrpool = new NetAddr::IP $convblock;
244
245 my $asth = $dbh->prepare(q{
246 SELECT id, cidr, type, parent_id, city, description, notes, circuitid,
247 createstamp, modifystamp, privdata, custid, vrf, vlan, rdns
248 FROM allocations
249 WHERE cidr <<= ? AND master_id = ?
250 ORDER BY masklen(cidr) DESC
251 });
252 my $inssth = $dbh->prepare(q{
253 INSERT INTO poolips (
254 ip,type,parent_id,available,
255 city,description,notes,circuitid,createstamp,modifystamp,privdata,custid,vrf,vlan,rdns
256 )
257 VALUES (?,?,?,'n',?,?,?,?,?,?,?,?,?,?,?)
258 });
259 my $updsth = $dbh->prepare("UPDATE poolips SET parent_id = ?, type = ? WHERE parent_id = ?");
260 my $delsth = $dbh->prepare("DELETE FROM allocations WHERE id = ?");
261 my $fbdelsth = $dbh->prepare("DELETE FROM freeblocks WHERE parent_id = ?");
262
263 $asth->execute($convblock, $master);
264 my %poolcounter;
265 while (my ($oldid, $oldcidr, $oldtype, $oldparent, @oldalloc) = $asth->fetchrow_array) {
266 if ($oldtype =~ /.[enr]/) {
267 # Convert leaf allocations to block of pool IP assignments
268 my $tmpcidr = new NetAddr::IP $oldcidr;
269 my $newtype = $poolclass.'i';
270 # set up the gateway IP in case we need it
271 my $gw = $cidrpool+1;
272 foreach my $newip ($tmpcidr->split(32)) {
273 my $baseip = $newip->addr;
274 # skip .0 and .255, they are prefectly legitimate but some systems behave
275 # poorly talking to a client using them.
276 next if $baseip =~ /\.(?:0|255)$/;
277 # skip the network, broadcast, and gateway IPs if we're creating a "normal netblock" pool
278 if ($pooltype =~ /d$/) {
279 next if $newip->addr eq $cidrpool->network->addr;
280 next if $newip->addr eq $cidrpool->broadcast->addr;
281 next if $newip->addr eq $gw->addr;
282 }
283 $inssth->execute($newip, $newtype, $poolparent, @oldalloc) if !$poolcounter{"$newip"};
284 $poolcounter{"$newip"}++;
285 }
[745]286 } elsif ($oldtype =~ /.[dp]/) {
[736]287 # Reparent IPs in an existing pool, and rewrite their type
288 $updsth->execute($poolparent, $poolclass.'i', $oldid);
289 } else {
290 # Containers are mostly "not interesting" in this context since they're
291 # equivalent to the pool allocation on .[dp] types. Clean up the lingering free block(s).
292 $fbdelsth->execute($oldid);
293 }
294 # Clean up - remove the converted block unless it is the "primary"
295 $delsth->execute($oldid) unless $oldid == $poolparent;
296 # Return the converted blocks, but only the immediate peers, not the entire tree
[745]297 push @retlist, { block => $oldcidr, mdisp => $disp_alloctypes{$oldtype}, mtype => $oldtype }
298 if (($oldparent == $mainparent) || $retall) && $oldid != $poolparent;
299 } # while $asth->fetch
300
[736]301 return \@retlist;
302} # end _toPool()
303
304
[725]305## IPDB::_poolToAllocations
306# Convert pool IPs into allocations, and free IPs into free blocks
307# Takes a pool ID, original pool CIDR (in case the allocation has been updated before the call here)
308# and hashref to data for the new parent container for the IPs,
309# and an optional hash with the new parent ID and allocation type
310sub _poolToAllocations {
311 my $dbh = shift;
312 my $oldpool = shift;
313 my $parentinfo = shift;
314 my %args = @_;
315
316 # Default to converting the pool to a container
317 $args{newparent} = $oldpool->{id} if !$args{newparent};
318
319 my ($containerclass) = ($parentinfo->{type} =~ /(.)./);
320
321 # Default type mapping
322 $args{newtype} = $poolmap{$oldpool->{type}} if !$args{newtype};
323
324 # Convert a bunch of pool IP allocations into "normal" netblock allocations
325 my $pool2alloc = $dbh->prepare(q{
326 INSERT INTO allocations (
327 cidr,type,city, description, notes, circuitid, createstamp, modifystamp,
328 privdata, custid, vrf, vlan, rdns, parent_id, master_id
329 )
330 SELECT
331 ip, ? AS type, city, description, notes, circuitid, createstamp, modifystamp,
332 privdata, custid, vrf, vlan, rdns, ? AS parent_id, master_id
333 FROM poolips
334 WHERE parent_id = ? AND available = 'n'
335 });
336 $pool2alloc->execute($args{newtype}, $args{newparent}, $oldpool->{id});
337
338 # Snag the whole list of pool IPs
339 my @freeips = @{$dbh->selectall_arrayref("SELECT ip,available FROM poolips WHERE parent_id = ?",
340 undef, $oldpool->{id})};
341 my @iplist;
342 my %usedips;
343 # Filter out the ones that were used...
344 foreach my $ip (@freeips) {
345 $$ip[0] =~ s{/32$}{};
346 push @iplist, NetAddr::IP->new($$ip[0]) if $$ip[1] eq 'y';
347 $usedips{$$ip[0]}++ if $$ip[1] eq 'n';
348 }
349 # ... so that we can properly decide whether the net, gw, and bcast IPs need to be added to the free list.
350 my $tmpblock = new NetAddr::IP $oldpool->{block};
351 push @iplist, NetAddr::IP->new($tmpblock->network->addr)
352 if !$usedips{$tmpblock->network->addr} || $tmpblock->network->addr =~ /\.0$/;
353 push @iplist, NetAddr::IP->new($tmpblock->broadcast->addr)
354 if !$usedips{$tmpblock->broadcast->addr} || $tmpblock->broadcast->addr =~ /\.255$/;
355 # only "DHCP"-ish pools have a gw ip removed from the pool
356 if ($oldpool->{type} =~ /.d/) {
357 $tmpblock++;
358 push @iplist, NetAddr::IP->new($tmpblock->addr);
359 }
360
361 # take the list of /32 IPs, and see what CIDR ranges we get back as free, then insert them.
362 @iplist = Compact(@iplist);
363 my $insfbsth = $dbh->prepare("INSERT INTO freeblocks (cidr,city,routed,vrf,parent_id,master_id) VALUES (?,?,?,?,?,?)");
364 foreach (@iplist) {
365 $insfbsth->execute($_, $parentinfo->{city}, $containerclass, $parentinfo->{vrf},
366 $args{newparent}, $parentinfo->{master_id});
367 }
368
369 # and finally delete the poolips entries
370 $dbh->do("DELETE FROM poolips WHERE parent_id = ?", undef, $oldpool->{id});
371
372} # end _poolToAllocations()
373
374
[716]375## IPDB::_deleteCascade()
376# Internal sub. Deletes an allocation and all subcomponents
377sub _deleteCascade {
378 my $dbh = shift;
379 my $id = shift;
[724]380 my $createfb = shift; # may be null at this point
[716]381
382 my $binfo = getBlockData($dbh, $id);
383
[724]384 # Decide if we're going to add a free block.
385
386 # Caller is normal block delete -> add freeblock under $binfo->{parent_id} -> pass nothing
387 # Caller is delete for merge to leaf -> do not add freeblock -> pass 0
388 # Caller is normal master delete -> do not add freeblock -> pass nothing
389 # Caller is merge master -> add freeblock under alternate parent -> pass parent ID
390 if ($binfo->{type} ne 'mm') {
391 # Deleting a non-master block
392 if (!defined($createfb)) {
393 # No createfb flag passed; assuming normal block delete. Add the freeblock
394 # under the parent of the block we're deleting.
395 $createfb = $binfo->{parent_id};
396 #} else {
397 # Don't need to actually do anything here. The caller has given us an ID,
398 # which is either 0 (causing no free block) or (theoretically) a valid block
399 # ID to add the free block under.
400 }
401 #} else {
402 # Deleting a master block
403 # Don't need to actually do anything here. If the caller passed a parent ID,
404 # that parent will get the new free block. if the caller didn't pass anything,
405 # no free block will be added.
406 }
407
[716]408##fixme: special-case master blocks up here and quickly delete based on master_id,
409# instead of wasting time tracing parent relations
410
411 # grab all allocations in the master within the CIDR of the block to be deleted
412 my %parents;
413 my %cidrlist;
414##fixme: limit by VRF?
415 my $sth = $dbh->prepare("SELECT cidr,id,parent_id FROM allocations WHERE cidr <<= ? AND master_id = ?");
416 $sth->execute($binfo->{block}, $binfo->{master_id});
417 while (my ($cidr, $cid, $pid) = $sth->fetchrow_array) {
418 $parents{$cid} = $pid;
419 $cidrlist{$cid} = $cidr;
420 }
421
422 # Trace the parent relations up the tree until we either hit parent ID 0 (we've found a master block
423 # but not the parent we're looking for - arguably this is already an error) or the parent ID matches
424 # the passed ID. If the latter, push the whole set into a second flag hash, so we can terminate
425 # further tree-tracing early.
426 my %found;
427 foreach my $cid (keys %parents) {
428 my @tmp;
429 if ($cid == $id) {
430 # "child" is the ID we've been asked to cascade-delete.
431 $found{$cid}++;
432 } elsif ($found{$cid}) {
433 # ID already seen and the chain terminates in our parent.
434 } elsif ($parents{$cid} == $id) {
435 # Immediate parent is the target parent
436 $found{$cid}++;
437 } else {
438 # Immediate parent isn't the one we're looking for. Walk the chain up until we hit our parent,
439 # the nonexistent parent id 0, or undefined (ID is not a child of the target ID at all)
440 # There are probably better ways to structure this loop.
441 while (1) {
442 # cache the ID
443 push @tmp, $cid;
444 # some very particularly defined loop ending conditions
445 if (!defined($parents{$cid}) || $parents{$cid} == $id || $parents{$cid} == 0) {
446 last;
447 } else {
448 # if we haven't found either the desired parent or another limiting condition,
449 # reset the ID to the parent next up the tree
450 $cid = $parents{$cid};
451 }
452 }
453 # if the current chain of relations ended with our target parent, shuffle the cached IDs into a flag hash
454 if (defined($parents{$cid}) && $parents{$cid} == $id) {
455 foreach (@tmp) { $found{$_}++; }
456 }
457 } # else
458 } # foreach my $cid
459
460 # Use the keys in the flag hash to determine which allocations to actually delete.
461 # Delete matching freeblocks and pool IPs; their parents are going away so we want
462 # to make sure we don't leave orphaned records lying around loose.
463 my @dellist = keys %found;
464 push @dellist, $id; # Just In Case the target ID didn't make the list earlier.
465 my $b = '?'. (',?' x $#dellist);
466 $dbh->do("DELETE FROM allocations WHERE id IN ($b)", undef, (@dellist) );
467 $dbh->do("DELETE FROM freeblocks WHERE parent_id IN ($b)", undef, (@dellist) );
468 $dbh->do("DELETE FROM poolips WHERE parent_id IN ($b)", undef, (@dellist) );
469
[724]470 # Insert a new free block if needed
471 if ($createfb) {
472 my $pinfo = getBlockData($dbh, $createfb);
[716]473 my $pt = (split //, $pinfo->{type})[1];
474 $dbh->do("INSERT INTO freeblocks (cidr,city,routed,parent_id,vrf,master_id) VALUES (?,?,?,?,?,?)", undef,
[724]475 $binfo->{block}, $pinfo->{city}, $pt, $createfb, $pinfo->{vrf}, $binfo->{master_id});
[716]476 }
477
478##todo: and hey! bonus! we can return @dellist, or something (%cidrlist{@dellist})
[724]479
[716]480} # end _deleteCascade()
481
482
[715]483##
484## Public subs
485##
486
487
[77]488## IPDB::initIPDBGlobals()
489# Initialize all globals. Takes a database handle, returns a success or error code
490sub initIPDBGlobals {
491 my $dbh = $_[0];
492 my $sth;
493
[106]494 # Initialize alloctypes hashes
[167]495 $sth = $dbh->prepare("select type,listname,dispname,listorder,def_custid from alloctypes order by listorder");
[77]496 $sth->execute;
497 while (my @data = $sth->fetchrow_array) {
[106]498 $disp_alloctypes{$data[0]} = $data[2];
[167]499 $def_custids{$data[0]} = $data[4];
[106]500 if ($data[3] < 900) {
501 $list_alloctypes{$data[0]} = $data[1];
502 }
[77]503 }
[96]504
505 # City and POP listings
[157]506 $sth = $dbh->prepare("select city,routing from cities order by city");
[96]507 $sth->execute;
508 return (undef,$sth->errstr) if $sth->err;
509 while (my @data = $sth->fetchrow_array) {
[106]510 push @citylist, $data[0];
[96]511 if ($data[1] eq 'y') {
[106]512 push @poplist, $data[0];
[96]513 }
514 }
515
[233]516 # Load ACL data. Specific username checks are done at a different level.
517 $sth = $dbh->prepare("select username,acl from users");
518 $sth->execute;
[106]519 return (undef,$sth->errstr) if $sth->err;
[233]520 while (my @data = $sth->fetchrow_array) {
521 $IPDBacl{$data[0]} = $data[1];
522 }
[106]523
[517]524##fixme: initialize HTML::Template env var for template path
525# something like $self->path().'/templates' ?
526# $ENV{HTML_TEMPLATE_ROOT} = 'foo/bar';
527
[77]528 return (1,"OK");
529} # end initIPDBGlobals
530
531
532## IPDB::connectDB()
[4]533# Creates connection to IPDB.
[77]534# Requires the database name, username, and password.
[4]535# Returns a handle to the db.
[77]536# Set up for a PostgreSQL db; could be any transactional DBMS with the
537# right changes.
[4]538sub connectDB {
[432]539 my $dbname = shift;
540 my $user = shift;
541 my $pass = shift;
542 my $dbhost = shift;
543
[4]544 my $dbh;
[432]545 my $DSN = "DBI:Pg:".($dbhost ? "host=$dbhost;" : '')."dbname=$dbname";
[4]546
547# Note that we want to autocommit by default, and we will turn it off locally as necessary.
[77]548# We may not want to print gobbledygook errors; YMMV. Have to ponder that further.
549 $dbh = DBI->connect($DSN, $user, $pass, {
550 AutoCommit => 1,
551 PrintError => 0
552 })
553 or return (undef, $DBI::errstr) if(!$dbh);
[4]554
[77]555# Return here if we can't select. Note that this indicates a
556# problem executing the select.
[183]557 my $sth = $dbh->prepare("select type from alloctypes");
[77]558 $sth->execute();
559 return (undef,$DBI::errstr) if ($sth->err);
560
561# See if the select returned anything (or null data). This should
562# succeed if the select executed, but...
563 $sth->fetchrow();
564 return (undef,$DBI::errstr) if ($sth->err);
565
566# If we get here, we should be OK.
567 return ($dbh,"DB connection OK");
[4]568} # end connectDB
569
[77]570
571## IPDB::finish()
572# Cleans up after database handles and so on.
573# Requires a database handle
574sub finish {
575 my $dbh = $_[0];
[517]576 $dbh->disconnect if $dbh;
[77]577} # end finish
578
579
[106]580## IPDB::checkDBSanity()
[4]581# Quick check to see if the db is responding. A full integrity
582# check will have to be a separate tool to walk the IP allocation trees.
583sub checkDBSanity {
[106]584 my ($dbh) = $_[0];
[4]585
586 if (!$dbh) {
[106]587 print "No database handle, or connection has been closed.";
588 return -1;
[4]589 } else {
590 # it connects, try a stmt.
[184]591 my $sth = $dbh->prepare("select type from alloctypes");
[4]592 my $err = $sth->execute();
593
594 if ($sth->fetchrow()) {
595 # all is well.
596 return 1;
597 } else {
[16]598 print "Connected to the database, but could not execute test statement. ".$sth->errstr();
[106]599 return -1;
[4]600 }
601 }
602 # Clean up after ourselves.
[106]603# $dbh->disconnect;
[4]604} # end checkDBSanity
605
[66]606
[371]607## IPDB::addMaster()
608# Does all the magic necessary to sucessfully add a master block
609# Requires database handle, block to add
610# Returns failure code and error message or success code and "message"
611sub addMaster {
612 my $dbh = shift;
[591]613 # warning! during testing, this somehow generated a "Bad file descriptor" error. O_o
[371]614 my $cidr = new NetAddr::IP shift;
[582]615 my %args = @_;
[371]616
[582]617 $args{vrf} = '' if !$args{vrf};
618 $args{rdns} = '' if !$args{rdns};
619 $args{defloc} = '' if !$args{defloc};
620 $args{rwhois} = 'n' if !$args{rwhois}; # fail "safe", sort of.
621 $args{rwhois} = 'n' if $args{rwhois} ne 'n' and $args{rwhois} ne 'y';
622
[629]623 my $mid;
624
[371]625 # Allow transactions, and raise an exception on errors so we can catch it later.
626 # Use local to make sure these get "reset" properly on exiting this block
627 local $dbh->{AutoCommit} = 0;
628 local $dbh->{RaiseError} = 1;
629
630 # Wrap all the SQL in a transaction
631 eval {
[628]632 # First check - does the master exist? Ignore VRFs until we can see a sane UI
633 my ($mcontained) = $dbh->selectrow_array("SELECT cidr FROM allocations WHERE cidr >>= ? AND type = 'mm'",
634 undef, ($cidr) );
635 die "Master block $mcontained already exists and entirely contains $cidr\n"
636 if $mcontained;
[371]637
[628]638 # Second check - does the new master contain an existing one or ones?
639 my ($mexist) = $dbh->selectrow_array("SELECT cidr FROM allocations WHERE cidr <<= ? AND type = 'mm'",
640 undef, ($cidr) );
641
[518]642 if (!$mexist) {
[371]643 # First case - master is brand-spanking-new.
644##fixme: rwhois should be globally-flagable somewhere, much like a number of other things
645## maybe a db table called "config"?
[628]646 $dbh->do("INSERT INTO allocations (cidr,type,swip,vrf,rdns) VALUES (?,?,?,?,?)", undef,
647 ($cidr, 'mm', 'y', $args{vrf}, $args{rdns}) );
648 ($mid) = $dbh->selectrow_array("SELECT currval('allocations_id_seq')");
[371]649
650# Unrouted blocks aren't associated with a city (yet). We don't rely on this
651# elsewhere though; legacy data may have traps and pitfalls in it to break this.
652# Thus the "routed" flag.
[628]653 $dbh->do("INSERT INTO freeblocks (cidr,city,routed,parent_id,vrf,master_id) VALUES (?,?,?,?,?,?)", undef,
654 ($cidr, '<NULL>', 'm', $mid, $args{vrf}, $mid) );
[371]655
[628]656 # master should be its own master, so deletes directly at the master level work
657 $dbh->do("UPDATE allocations SET master_id = ? WHERE id = ?", undef, ($mid, $mid) );
658
[371]659 # If we get here, everything is happy. Commit changes.
660 $dbh->commit;
661
[518]662 } # done new master does not contain existing master(s)
[371]663 else {
664
665 # collect the master(s) we're going to absorb, and snag the longest netmask while we're at it.
666 my $smallmask = $cidr->masklen;
[628]667 my $sth = $dbh->prepare("SELECT cidr,id FROM allocations WHERE cidr <<= ? AND type='mm' AND parent_id=0");
[518]668 $sth->execute($cidr);
[371]669 my @cmasters;
[628]670 my @oldmids;
[371]671 while (my @data = $sth->fetchrow_array) {
672 my $master = new NetAddr::IP $data[0];
673 push @cmasters, $master;
[628]674 push @oldmids, $data[1];
[371]675 $smallmask = $master->masklen if $master->masklen > $smallmask;
676 }
677
678 # split the new master, and keep only those blocks not part of an existing master
679 my @blocklist;
680 foreach my $seg ($cidr->split($smallmask)) {
681 my $contained = 0;
682 foreach my $master (@cmasters) {
683 $contained = 1 if $master->contains($seg);
684 }
685 push @blocklist, $seg if !$contained;
686 }
687
[628]688##fixme: master_id
[371]689 # collect the unrouted free blocks within the new master
[556]690 $sth = $dbh->prepare("SELECT cidr FROM freeblocks WHERE masklen(cidr) <= ? AND cidr <<= ? AND routed = 'm'");
[518]691 $sth->execute($smallmask, $cidr);
[371]692 while (my @data = $sth->fetchrow_array) {
693 my $freeblock = new NetAddr::IP $data[0];
694 push @blocklist, $freeblock;
695 }
696
697 # combine the set of free blocks we should have now.
698 @blocklist = Compact(@blocklist);
699
[628]700 # master
701 $dbh->do("INSERT INTO allocations (cidr,type,swip,vrf,rdns) VALUES (?,?,?,?,?)", undef,
702 ($cidr, 'mm', 'y', $args{vrf}, $args{rdns}) );
703 ($mid) = $dbh->selectrow_array("SELECT currval('allocations_id_seq')");
704
705 # master should be its own master, so deletes directly at the master level work
706 $dbh->do("UPDATE allocations SET master_id = ? WHERE id = ?", undef, ($mid, $mid) );
707
[371]708 # and now insert the new data. Make sure to delete old masters too.
709
710 # freeblocks
[628]711 $sth = $dbh->prepare("DELETE FROM freeblocks WHERE cidr <<= ? AND parent_id IN (".join(',', @oldmids).")");
712 my $sth2 = $dbh->prepare("INSERT INTO freeblocks (cidr,city,routed,parent_id,vrf,master_id)".
713 " VALUES (?,'<NULL>','m',?,?,?)");
[371]714 foreach my $newblock (@blocklist) {
[518]715 $sth->execute($newblock);
[628]716 $sth2->execute($newblock, $mid, $args{vrf}, $mid);
[371]717 }
718
[628]719 # Update immediate allocations, and remove the old parents
720 $sth = $dbh->prepare("UPDATE allocations SET parent_id = ? WHERE parent_id = ?");
721 $sth2 = $dbh->prepare("DELETE FROM allocations WHERE id = ?");
722 foreach my $old (@oldmids) {
723 $sth->execute($mid, $old);
724 $sth2->execute($old);
725 }
[556]726
[371]727 # *whew* If we got here, we likely suceeded.
728 $dbh->commit;
[628]729
[371]730 } # new master contained existing master(s)
731 }; # end eval
732
733 if ($@) {
734 my $msg = $@;
735 eval { $dbh->rollback; };
736 return ('FAIL',$msg);
737 } else {
[582]738
739 # Only attempt rDNS if the IPDB side succeeded
740 if ($rpc_url) {
741
742# Note *not* splitting reverse zones negates any benefit from caching the exported data.
743# IPv6 address space is far too large to split usefully, and in any case (also due to
744# the large address space) doesn't support the iterated template records v4 zones do
745# that causes the bulk of the slowdown that needs the cache anyway.
746
747 my @zonelist;
748# allow splitting reverse zones to be disabled, maybe, someday
749#if ($splitrevzones && !$cidr->{isv6}) {
750 if (1 && !$cidr->{isv6}) {
751 my $splitpoint = ($cidr->masklen <= 16 ? 16 : 24); # hack pthui
752 @zonelist = $cidr->split($splitpoint);
753 } else {
754 @zonelist = ($cidr);
755 }
756 my @fails;
757 ##fixme: remove hardcoding where possible
758 foreach my $subzone (@zonelist) {
759 my %rpcargs = (
760 rpcuser => $args{user},
761 revzone => "$subzone",
762 revpatt => $args{rdns},
763 defloc => $args{defloc},
[585]764 group => $revgroup, # not sure how these two could sanely be exposed, tbh...
[582]765 state => 1, # could make them globally configurable maybe
766 );
[587]767 if ($rpc_url && !_rpc('addRDNS', %rpcargs)) {
[585]768 push @fails, ("$subzone" => $errstr);
[582]769 }
770 }
771 if (@fails) {
[628]772 $errstr = "Warning(s) adding $cidr to reverse DNS:\n".join("\n", @fails);
773 return ('WARN',$mid);
[582]774 }
775 }
[628]776 return ('OK',$mid);
[371]777 }
778} # end addMaster
779
780
[547]781## IPDB::touchMaster()
782# Update last-changed timestamp on a master block.
783sub touchMaster {
784 my $dbh = shift;
785 my $master = shift;
786
787 local $dbh->{AutoCommit} = 0;
788 local $dbh->{RaiseError} = 1;
789
790 eval {
[652]791 $dbh->do("UPDATE allocations SET modifystamp=now() WHERE id = ?", undef, ($master));
[547]792 $dbh->commit;
793 };
794
795 if ($@) {
796 my $msg = $@;
797 eval { $dbh->rollback; };
798 return ('FAIL',$msg);
799 }
800 return ('OK','OK');
801} # end touchMaster()
802
803
[523]804## IPDB::listSummary()
805# Get summary list of all master blocks
806# Returns an arrayref to a list of hashrefs containing the master block, routed count,
807# allocated count, free count, and largest free block masklength
808sub listSummary {
809 my $dbh = shift;
810
[625]811 my $mlist = $dbh->selectall_arrayref("SELECT cidr AS master,id,vrf FROM allocations ".
812 "WHERE type='mm' ORDER BY cidr",
813 { Slice => {} });
[523]814
815 foreach (@{$mlist}) {
[625]816 my ($rcnt) = $dbh->selectrow_array("SELECT count(*) FROM allocations WHERE cidr <<= ? AND type='rm' AND master_id = ?",
817 undef, ($$_{master}, $$_{id}));
[523]818 $$_{routed} = $rcnt;
[625]819 my ($acnt) = $dbh->selectrow_array("SELECT count(*) FROM allocations WHERE cidr <<= ? ".
820 "AND NOT type='rm' AND NOT type='mm' AND master_id = ?",
821 undef, ($$_{master}, $$_{id}));
[523]822 $$_{allocated} = $acnt;
[625]823 my ($fcnt) = $dbh->selectrow_array("SELECT count(*) FROM freeblocks WHERE cidr <<= ? AND master_id = ?",
824 undef, ($$_{master}, $$_{id}));
[523]825 $$_{free} = $fcnt;
[560]826 my ($bigfree) = $dbh->selectrow_array("SELECT masklen(cidr) AS maskbits FROM freeblocks WHERE cidr <<= ?".
[625]827 " AND master_id = ? ORDER BY masklen(cidr) LIMIT 1", undef, ($$_{master}, $$_{id}));
[523]828##fixme: should find a way to do this without having to HTMLize the <>
829 $bigfree = "/$bigfree" if $bigfree;
[525]830 $bigfree = '<NONE>' if !$bigfree;
[523]831 $$_{bigfree} = $bigfree;
832 }
833 return $mlist;
834} # end listSummary()
835
836
[561]837## IPDB::listSubs()
838# Get list of subnets within a specified CIDR block, on a specified VRF.
839# Returns an arrayref to a list of hashrefs containing the CIDR block, customer location or
840# city it's routed to, block type, SWIP status, and description
841sub listSubs {
842 my $dbh = shift;
843 my %args = @_;
844
845 # Just In Case
846 $args{vrf} = '' if !$args{vrf};
847
848 # Snag the allocations for this block
[691]849 my $sth = $dbh->prepare("SELECT cidr,city,type,custid,swip,description,vrf,id,master_id".
[627]850 " FROM allocations WHERE parent_id = ? ORDER BY cidr");
851 $sth->execute($args{parent});
[561]852
853 # hack hack hack
854 # set up to flag swip=y records if they don't actually have supporting data in the customers table
855 my $custsth = $dbh->prepare("SELECT count(*) FROM customers WHERE custid = ?");
856
[653]857 # snag some more details
[664]858 my $substh = $dbh->prepare("SELECT count(*) FROM allocations WHERE cidr <<= ? ".
[663]859 "AND type ~ '[mc]\$' AND master_id = ? AND NOT cidr = ? ");
[653]860 my $alsth = $dbh->prepare("SELECT count(*) FROM allocations WHERE cidr <<= ? ".
[712]861 "AND NOT type='rm' AND NOT type='mm' AND master_id = ? AND NOT id = ?");
[653]862 my $freesth = $dbh->prepare("SELECT count(*) FROM freeblocks WHERE cidr <<= ? AND master_id = ?");
863 my $lfreesth = $dbh->prepare("SELECT masklen(cidr) AS maskbits FROM freeblocks WHERE cidr <<= ?".
864 " AND master_id = ? ORDER BY masklen(cidr) LIMIT 1");
865
[561]866 my @blocklist;
[691]867 while (my ($cidr,$city,$type,$custid,$swip,$desc,$vrf,$id,$mid) = $sth->fetchrow_array()) {
868 $desc .= " - vrf:$vrf" if $desc && $vrf;
869 $desc = "vrf:$vrf" if !$desc && $vrf;
[561]870 $custsth->execute($custid);
871 my ($ncust) = $custsth->fetchrow_array();
[653]872 $substh->execute($cidr, $mid, $cidr);
873 my ($cont) = $substh->fetchrow_array();
[712]874 $alsth->execute($cidr, $mid, $id);
[653]875 my ($alloc) = $alsth->fetchrow_array();
876 $freesth->execute($cidr, $mid);
877 my ($free) = $freesth->fetchrow_array();
878 $lfreesth->execute($cidr, $mid);
879 my ($lfree) = $lfreesth->fetchrow_array();
880 $lfree = "/$lfree" if $lfree;
881 $lfree = '<NONE>' if !$lfree;
[561]882 my %row = (
883 block => $cidr,
[653]884 subcontainers => $cont,
885 suballocs => $alloc,
886 subfree => $free,
887 lfree => $lfree,
[561]888 city => $city,
889 type => $disp_alloctypes{$type},
890 custid => $custid,
891 swip => ($swip eq 'y' ? 'Yes' : 'No'),
892 partswip => ($swip eq 'y' && $ncust == 0 ? 1 : 0),
893 desc => $desc,
894 hassubs => ($type eq 'rm' || $type =~ /.c/ ? 1 : 0),
[627]895 id => $id,
[561]896 );
897# $row{subblock} = ($type =~ /^.r$/); # hmf. wonder why these won't work in the hash declaration...
898 $row{listpool} = ($type =~ /^.[pd]$/);
899 push (@blocklist, \%row);
900 }
901 return \@blocklist;
902} # end listSubs()
903
904
[663]905## IPDB::listContainers()
906# List all container-type allocations in a given parent
907# Takes a database handle and a hash:
908# - parent is the ID of the parent block
909# Returns an arrayref to a list of hashrefs with the CIDR block, location, type,
910# description, block ID, and counts for the nmber uf suballocations (all types),
911# free blocks, and the CIDR size of the largest free block
912sub listContainers {
913 my $dbh = shift;
914 my %args = @_;
915
916 # Just In Case
917 $args{vrf} = '' if !$args{vrf};
918
919 # Snag the allocations for this block
[691]920 my $sth = $dbh->prepare("SELECT cidr,city,type,custid,swip,description,vrf,id,master_id".
[663]921 " FROM allocations WHERE parent_id = ? AND type ~ '[mc]\$' ORDER BY cidr");
922 $sth->execute($args{parent});
923
924 my $alsth = $dbh->prepare("SELECT count(*) FROM allocations WHERE cidr <<= ? ".
[712]925 "AND NOT type='rm' AND NOT type='mm' AND master_id = ? AND NOT id = ?");
[663]926 my $freesth = $dbh->prepare("SELECT count(*) FROM freeblocks WHERE cidr <<= ? AND master_id = ?");
927 my $lfreesth = $dbh->prepare("SELECT masklen(cidr) AS maskbits FROM freeblocks WHERE cidr <<= ?".
928 " AND master_id = ? ORDER BY masklen(cidr) LIMIT 1");
929
930 my @blocklist;
[691]931 while (my ($cidr,$city,$type,$custid,$swip,$desc,$vrf,$id,$mid) = $sth->fetchrow_array()) {
932 $desc .= " - vrf:$vrf" if $desc && $vrf;
933 $desc = "vrf:$vrf" if !$desc && $vrf;
[712]934 $alsth->execute($cidr, $mid, $id);
[663]935 my ($alloc) = $alsth->fetchrow_array();
936 $freesth->execute($cidr, $mid);
937 my ($free) = $freesth->fetchrow_array();
938 $lfreesth->execute($cidr, $mid);
939 my ($lfree) = $lfreesth->fetchrow_array();
940 $lfree = "/$lfree" if $lfree;
941 $lfree = '<NONE>' if !$lfree;
942 my %row = (
943 block => $cidr,
944 suballocs => $alloc,
945 subfree => $free,
946 lfree => $lfree,
947 city => $city,
948 type => $disp_alloctypes{$type},
949 desc => $desc,
950 id => $id,
951 );
952 push (@blocklist, \%row);
953 }
954 return \@blocklist;
955} # end listContainers()
956
957
958## IPDB::listAllocations()
959# List all end-use allocations in a given parent
960# Takes a database handle and a hash:
961# - parent is the ID of the parent block
962# Returns an arrayref to a list of hashrefs with the CIDR block, location, type,
963# custID, SWIP flag, description, block ID, and master ID
964sub listAllocations {
965 my $dbh = shift;
966 my %args = @_;
967
968 # Snag the allocations for this block
[691]969 my $sth = $dbh->prepare("SELECT cidr,city,type,custid,swip,description,vrf,id,master_id".
[663]970 " FROM allocations WHERE parent_id = ? AND type !~ '[mc]\$' ORDER BY cidr");
971 $sth->execute($args{parent});
972
973 # hack hack hack
974 # set up to flag swip=y records if they don't actually have supporting data in the customers table
975 my $custsth = $dbh->prepare("SELECT count(*) FROM customers WHERE custid = ?");
976
977 my @blocklist;
[691]978 while (my ($cidr,$city,$type,$custid,$swip,$desc,$vrf,$id,$mid) = $sth->fetchrow_array()) {
979 $desc .= " - vrf:$vrf" if $desc && $vrf;
980 $desc = "vrf:$vrf" if !$desc && $vrf;
[663]981 $custsth->execute($custid);
982 my ($ncust) = $custsth->fetchrow_array();
983 my %row = (
984 block => $cidr,
985 city => $city,
986 type => $disp_alloctypes{$type},
987 custid => $custid,
988 swip => ($swip eq 'y' ? 'Yes' : 'No'),
989 partswip => ($swip eq 'y' && $ncust == 0 ? 1 : 0),
990 desc => $desc,
991 id => $id,
992 );
993# $row{subblock} = ($type =~ /^.r$/); # hmf. wonder why these won't work in the hash declaration...
994 $row{listpool} = ($type =~ /^.[pd]$/);
995 push (@blocklist, \%row);
996 }
997 return \@blocklist;
998} # end listAllocations()
999
1000
[720]1001## IPDB::listForMerge()
1002# Get a list of blocks targetted in a proposed merge
1003sub listForMerge {
1004 my $dbh = shift;
1005 my $parent = shift;
1006 my $newblock = shift;
1007 my $btype = shift || 'a';
1008 $btype = 'a' if $btype !~/^[af]$/;
[733]1009 my $incsub = shift;
1010 $incsub = 1 if !defined($incsub);
[720]1011
1012 my $sql;
1013 if ($btype eq 'a') {
1014 my $ret = $dbh->selectall_arrayref(q(
1015 SELECT a.cidr,a.id,t.dispname FROM allocations a
1016 JOIN alloctypes t ON a.type=t.type
1017 WHERE a.parent_id = ? AND a.cidr <<= ?
1018 ORDER BY a.cidr
1019 ),
1020 { Slice => {} }, $parent, $newblock);
1021 return $ret;
1022 } else {
1023##fixme: Not sure about the casting hackery in "SELECT ?::integer AS id", but it works as intended
[733]1024 my @dbargs = ($parent, "$newblock");
1025 push @dbargs, $parent, $newblock if $incsub;
1026 my $ret = $dbh->selectall_arrayref(q{
1027 SELECT cidr,id FROM freeblocks
1028 WHERE parent_id IN (
1029 }.($incsub ? "SELECT id FROM allocations WHERE parent_id = ? AND cidr <<= ? UNION " : '').q{
1030 SELECT ?::integer AS id
1031 ) AND cidr <<= ?
1032 ORDER BY cidr
1033 },
1034 { Slice => {} }, @dbargs);
[720]1035 return $ret;
1036 }
[733]1037 return;
[720]1038} # end listForMerge()
1039
1040
[524]1041## IPDB::listFree()
[562]1042# Gets a list of free blocks in the requested parent/master and VRF instance in both CIDR and range notation
[630]1043# Takes a parent/master ID and an optional VRF specifier that defaults to empty.
[524]1044# Returns an arrayref to a list of hashrefs containing the CIDR and range-notation blocks
[527]1045# Returns some extra flags in the hashrefs for routed blocks, since those can have several subtypes
[524]1046sub listFree {
1047 my $dbh = shift;
1048
[562]1049 my %args = @_;
1050 # Just In Case
1051 $args{vrf} = '' if !$args{vrf};
1052
[692]1053 my $sth = $dbh->prepare(q(
1054 SELECT f.cidr,f.id,allocations.cidr
1055 FROM freeblocks f
1056 LEFT JOIN allocations ON f.reserve_for = allocations.id
1057 WHERE f.parent_id = ?
1058 ORDER BY f.cidr
1059 ) );
[630]1060# $sth->execute($args{parent}, $args{vrf});
1061 $sth->execute($args{parent});
[524]1062 my @flist;
[692]1063 while (my ($cidr,$id,$resv) = $sth->fetchrow_array()) {
[524]1064 $cidr = new NetAddr::IP $cidr;
[527]1065 my %row = (
1066 fblock => "$cidr",
1067 frange => $cidr->range,
[630]1068 fbid => $id,
1069 fbparent => $args{parent},
[692]1070 resv => $resv,
[527]1071 );
[524]1072 push @flist, \%row;
1073 }
1074 return \@flist;
[527]1075} # end listFree()
[524]1076
1077
[528]1078## IPDB::listPool()
[740]1079# List the IPs in an IP pool.
1080# Takes a pool/parent ID
1081# Returns
1082# Returns an arrayref to a list of hashrefs containing the IP, customer ID, availability flag,
1083# description, backreference to the pool/parent, and the IP ID in the pool.
1084# Also includes a "may be deleted" metaflag mainly useful for allowing the return to be passed
1085# directly to HTML::Template for UI display.
[528]1086sub listPool {
1087 my $dbh = shift;
1088 my $pool = shift;
1089
[630]1090 my $sth = $dbh->prepare("SELECT ip,custid,available,description,type,id".
1091 " FROM poolips WHERE parent_id = ? ORDER BY ip");
[528]1092 $sth->execute($pool);
1093 my @poolips;
[630]1094 while (my ($ip,$custid,$available,$desc,$type,$id) = $sth->fetchrow_array) {
[528]1095 my %row = (
1096 ip => $ip,
1097 custid => $custid,
1098 available => $available,
1099 desc => $desc,
[563]1100 delme => $available eq 'n',
[630]1101 parent => $pool,
1102 id => $id,
[528]1103 );
1104 push @poolips, \%row;
1105 }
1106 return \@poolips;
1107} # end listPool()
1108
1109
[541]1110## IPDB::getMasterList()
1111# Get a list of master blocks, optionally including last-modified timestamps
1112# Takes an optional flag to indicate whether to include timestamps;
1113# 'm' includes ctime, all others (suggest 'c') do not.
1114# Returns an arrayref to a list of hashrefs
1115sub getMasterList {
1116 my $dbh = shift;
1117 my $stampme = shift || 'm'; # optional but should be set by caller for clarity
1118
[632]1119 my $mlist = $dbh->selectall_arrayref("SELECT id,vrf,cidr AS master".($stampme eq 'm' ? ',modifystamp AS mtime' : '').
1120 " FROM allocations WHERE type='mm' ORDER BY cidr", { Slice => {} });
[541]1121 return $mlist;
1122} # end getMasterList()
1123
1124
[529]1125## IPDB::getTypeList()
1126# Get an alloctype/description pair list suitable for dropdowns
1127# Takes a flag to determine which general groups of types are returned
1128# Returns an reference to an array of hashrefs
1129sub getTypeList {
1130 my $dbh = shift;
1131 my $tgroup = shift || 'a'; # technically optional, like this, but should
1132 # really be specified in the call for clarity
[714]1133 my $seltype = shift || '';
1134
1135 my $sql = "SELECT type,listname,type=? AS sel FROM alloctypes WHERE listorder <= 500";
[564]1136 if ($tgroup eq 'n') {
1137 # grouping 'p' - all netblock types. These include routed blocks, containers (_c)
1138 # and contained (_r) types, dynamic-allocation ranges (_e), static IP pools (_d and _p),
1139 # and the "miscellaneous" cn, in, and en types.
[714]1140 $sql .= " AND type NOT LIKE '_i'";
[564]1141 } elsif ($tgroup eq 'p') {
1142 # grouping 'p' - primary allocation types. As with 'n' above but without the _r contained types.
[714]1143 $sql .= " AND type NOT LIKE '_i' AND type NOT LIKE '_r'";
[529]1144 } elsif ($tgroup eq 'c') {
1145 # grouping 'c' - contained types. These include all static IPs and all _r types.
[714]1146 $sql .= " AND (type LIKE '_i' OR type LIKE '_r')";
[632]1147 } elsif ($tgroup eq 'i') {
1148 # grouping 'i' - static IP types.
[714]1149 $sql .= " AND type LIKE '_i'";
[529]1150 } else {
1151 # grouping 'a' - all standard allocation types. This includes everything
1152 # but mm (present only as a formality). Make this the default.
[714]1153 # ... whee! no extra WHERE clauses
[529]1154 }
[714]1155 $sql .= " ORDER BY listorder";
1156 my $tlist = $dbh->selectall_arrayref($sql, { Slice => {} }, $seltype);
[529]1157 return $tlist;
1158}
1159
1160
[532]1161## IPDB::getPoolSelect()
1162# Get a list of pools matching the passed city and type that have 1 or more free IPs
[740]1163# Returns an arrayref to a list of hashrefs containing the number of available IPs, the CIDR pool,
1164# and the city it's nominally in.
[532]1165sub getPoolSelect {
1166 my $dbh = shift;
1167 my $iptype = shift;
1168 my $pcity = shift;
1169
1170 my ($ptype) = ($iptype =~ /^(.)i$/);
1171 return if !$ptype;
1172 $ptype .= '_';
1173
[663]1174 my $plist = $dbh->selectall_arrayref( q(
1175 SELECT count(*) AS poolfree,p.pool AS poolblock, a.city AS poolcit
1176 FROM poolips p
1177 JOIN allocations a ON p.parent_id=a.id
1178 WHERE p.available='y' AND a.city = ? AND p.type LIKE ?
1179 GROUP BY p.pool,a.city
1180 ),
[572]1181 { Slice => {} }, ($pcity, $ptype) );
[532]1182 return $plist;
1183} # end getPoolSelect()
1184
1185
[533]1186## IPDB::findAllocateFrom()
1187# Find free block to add a new allocation from. (CIDR block version of pool select above, more or less)
1188# Takes
1189# - mask length
1190# - allocation type
1191# - POP city "parent"
1192# - optional master-block restriction
1193# - optional flag to allow automatic pick-from-private-network-ranges
[740]1194# Returns a 3-element list with the free block ID, CIDR, and parent ID matching the criteria, if any
[533]1195sub findAllocateFrom {
1196 my $dbh = shift;
1197 my $maskbits = shift;
1198 my $type = shift;
1199 my $city = shift;
1200 my $pop = shift;
1201 my %optargs = @_;
1202
1203 my $failmsg = "No suitable free block found\n";
1204
[633]1205 my @vallist;
1206 my $sql;
1207
1208 # Free pool IPs should be easy.
1209 if ($type =~ /^.i$/) {
1210 # User may get an IP from the wrong VRF. User should not be using admin tools to allocate static IPs.
1211 $sql = "SELECT id, ip, parent_id FROM poolips WHERE ip = ?";
1212 @vallist = ($optargs{gimme});
1213 } else {
1214
[533]1215## Set up the SQL to find out what freeblock we can (probably) use for an allocation.
1216## Very large systems will require development of a reserve system (possibly an extension
1217## of the reserve-for-expansion concept in https://secure.deepnet.cx/trac/ipdb/ticket/24?)
1218## Also populate a value list for the DBI call.
1219
[633]1220 @vallist = ($maskbits);
1221 $sql = "SELECT id,cidr,parent_id FROM freeblocks WHERE masklen(cidr) <= ?";
[533]1222
[572]1223# cases, strict rules
1224# .c -> container type
1225# requires a routing container, fbtype r
1226# .d -> DHCP/"normal-routing" static pool
1227# requires a routing container, fbtype r
1228# .e -> Dynamic-assignment connectivity
1229# requires a routing container, fbtype r
1230# .i -> error, can't allocate static IPs this way?
1231# mm -> error, master block
1232# rm -> routed block
1233# requires master block, fbtype m
1234# .n -> Miscellaneous usage
1235# requires a routing container, fbtype r
1236# .p -> PPP(oE) static pool
1237# requires a routing container, fbtype r
1238# .r -> contained type
1239# requires a matching container, fbtype $1
1240##fixme: strict-or-not flag
1241
[633]1242##fixme: config or UI flag for "Strict" mode
1243# if ($strictmode) {
1244if (0) {
[572]1245 if ($type =~ /^(.)r$/) {
1246 push @vallist, $1;
1247 $sql .= " AND routed = ?";
1248 } elsif ($type eq 'rm') {
1249 $sql .= " AND routed = 'm'";
1250 } else {
1251 $sql .= " AND routed = 'r'";
1252 }
[633]1253}
[572]1254
[633]1255 # for PPP(oE) and container types, the POP city is the one attached to the pool.
1256 # individual allocations get listed with the customer city site.
1257 ##fixme: chain cities to align roughly with a full layer-2 node graph
1258 $city = $pop if $type !~ /^.[pc]$/;
1259 if ($type ne 'rm' && $city) {
1260 $sql .= " AND city = ?";
1261 push @vallist, $city;
[533]1262 }
[633]1263 # Allow specifying an arbitrary full block, instead of a master
1264 if ($optargs{gimme}) {
1265 $sql .= " AND cidr >>= ?";
1266 push @vallist, $optargs{gimme};
1267 }
1268 # if a specific master was requested, allow the requestor to self->shoot(foot)
1269 if ($optargs{master} && $optargs{master} ne '-') {
1270 $sql .= " AND master_id = ?";
1271# if $optargs{master} ne '-';
1272 push @vallist, $optargs{master};
1273 } else {
1274 # if a specific master was NOT requested, filter out the RFC 1918 private networks
1275 if (!$optargs{allowpriv}) {
1276 $sql .= " AND NOT (cidr <<= '192.168.0.0/16' OR cidr <<= '10.0.0.0/8' OR cidr <<= '172.16.0.0/12')";
1277 }
1278 }
[693]1279 # Keep "reserved" blocks out of automatic assignment.
1280##fixme: needs a UI flag or a config knob
1281 $sql .= " AND reserve_for = 0";
[633]1282 # Sorting and limiting, since we don't (currently) care to provide a selection of
1283 # blocks to carve up. This preserves something resembling optimal usage of the IP
1284 # space by forcing contiguous allocations and free blocks as much as possible.
1285 $sql .= " ORDER BY masklen(cidr) DESC,cidr LIMIT 1";
1286 } # done setting up SQL for free CIDR block
[533]1287
[633]1288 my ($fbid,$fbfound,$fbparent) = $dbh->selectrow_array($sql, undef, @vallist);
1289 return $fbid,$fbfound,$fbparent;
[533]1290} # end findAllocateFrom()
1291
1292
[536]1293## IPDB::ipParent()
1294# Get an IP's parent pool's details
1295# Takes a database handle and IP
1296# Returns a hashref to the parent pool block, if any
1297sub ipParent {
1298 my $dbh = shift;
1299 my $block = shift;
1300
1301 my $pinfo = $dbh->selectrow_hashref("SELECT cidr,custid,type,city,description FROM allocations".
[565]1302 " WHERE cidr >>= ? AND (type LIKE '_p' OR type LIKE '_d')", undef, ($block) );
[536]1303 return $pinfo;
1304} # end ipParent()
1305
1306
1307## IPDB::subParent()
[529]1308# Get a block's parent's details
1309# Takes a database handle and CIDR block
[536]1310# Returns a hashref to the parent container block, if any
1311sub subParent {
[529]1312 my $dbh = shift;
1313 my $block = shift;
1314
1315 my $pinfo = $dbh->selectrow_hashref("SELECT cidr,custid,type,city,description FROM allocations".
1316 " WHERE cidr >>= ?", undef, ($block) );
1317 return $pinfo;
[536]1318} # end subParent()
[529]1319
1320
[536]1321## IPDB::blockParent()
1322# Get a block's parent's details
1323# Takes a database handle and CIDR block
1324# Returns a hashref to the parent container block, if any
1325sub blockParent {
1326 my $dbh = shift;
1327 my $block = shift;
1328
1329 my $pinfo = $dbh->selectrow_hashref("SELECT cidr,city FROM routed".
1330 " WHERE cidr >>= ?", undef, ($block) );
1331 return $pinfo;
1332} # end blockParent()
1333
1334
[697]1335## IPDB::getBreadCrumbs()
1336# Retrieve the ID and CIDR of a block's parent(s) up to the master block
1337# Returns an arrayref to a list of hashrefs with CIDR and block ID
1338sub getBreadCrumbs {
1339 my $dbh = shift;
1340 my $parent = shift;
1341 my @result;
1342
1343 my $sth = $dbh-> prepare("SELECT cidr,type,id,parent_id FROM allocations WHERE id=?");
1344
1345 while ($parent != 0) {
1346 $sth->execute($parent);
1347 my ($cidr,$type,$id,$pid) = $sth->fetchrow_array;
1348 push @result, {cidr => $cidr, link => $id, ispool => ($type =~ /^.[dp]$/ ? 1 : 0)};
1349 $parent = $pid;
1350 }
1351
1352 return \@result;
1353} # end getBread()
1354
1355
[527]1356## IPDB::getRoutedCity()
1357# Get the city for a routed block.
1358sub getRoutedCity {
1359 my $dbh = shift;
1360 my $block = shift;
1361
1362 my ($rcity) = $dbh->selectrow_array("SELECT city FROM routed WHERE cidr = ?", undef, ($block) );
1363 return $rcity;
1364} # end getRoutedCity()
1365
1366
[77]1367## IPDB::allocateBlock()
[66]1368# Does all of the magic of actually allocating a netblock
[554]1369# Requires a database handle, and a hash containing the block to allocate, routing depth, custid,
1370# type, city, block to allocate from, and optionally a description, notes, circuit ID,
1371# and private data
[77]1372# Returns a success code and optional error message.
1373sub allocateBlock {
[554]1374 my $dbh = shift;
[284]1375
[554]1376 my %args = @_;
1377
1378 $args{cidr} = new NetAddr::IP $args{cidr};
1379
1380 $args{desc} = '' if !$args{desc};
1381 $args{notes} = '' if !$args{notes};
1382 $args{circid} = '' if !$args{circid};
1383 $args{privdata} = '' if !$args{privdata};
1384 $args{vrf} = '' if !$args{vrf};
[691]1385 $args{vlan} = '' if !$args{vlan};
[585]1386 $args{rdns} = '' if !$args{rdns};
[554]1387
[690]1388 # Could arguably allow this for eg /120 allocations, but end users who get a single v4 IP are
1389 # usually given a v6 /64, and most v6 addressing schemes need at least half that address space
1390 if ($args{cidr}->{isv6} && $args{rdns} =~ /\%/) {
1391 return ('FAIL','Reverse DNS template patterns are not supported for IPv6 allocations');
1392 }
1393
[77]1394 my $sth;
[66]1395
[633]1396 # Snag the "type" of the freeblock and its CIDR
1397 my ($alloc_from_type, $alloc_from, $fbparent, $fcity, $fbmaster) =
1398 $dbh->selectrow_array("SELECT routed,cidr,parent_id,city,master_id FROM freeblocks WHERE id = ?",
1399 undef, $args{fbid});
1400 $alloc_from = new NetAddr::IP $alloc_from;
[692]1401 return ('FAIL',"Failed to allocate $args{cidr}; intended free block was used by another allocation.")
[741]1402 if ($args{type} !~ /.i/ && !$fbparent);
[692]1403##fixme: fail here if !$alloc_from
1404# also consider "lock for allocation" due to multistep allocation process
[349]1405
[79]1406 # To contain the error message, if any.
[554]1407 my $msg = "Unknown error allocating $args{cidr} as '$disp_alloctypes{$args{type}}'";
[79]1408
[77]1409 # Enable transactions and error handling
1410 local $dbh->{AutoCommit} = 0; # These need to be local so we don't
1411 local $dbh->{RaiseError} = 1; # step on our toes by accident.
[66]1412
[554]1413 if ($args{type} =~ /^.i$/) {
1414 $msg = "Unable to assign static IP $args{cidr} to $args{custid}";
[77]1415 eval {
[554]1416 if ($args{cidr}) { # IP specified
1417 my ($isavail) = $dbh->selectrow_array("SELECT available FROM poolips WHERE ip=?", undef, ($args{cidr}) );
1418 die "IP is not in an IP pool.\n"
1419 if !$isavail;
1420 die "IP already allocated. Deallocate and reallocate, or update the entry\n"
1421 if $isavail eq 'n';
1422 } else { # IP not specified, take first available
1423 ($args{cidr}) = $dbh->selectrow_array("SELECT ip FROM poolips WHERE pool=? AND available='y' ORDER BY ip",
1424 undef, ($args{alloc_from}) );
[545]1425 }
[633]1426 $dbh->do("UPDATE poolips SET custid = ?, city = ?,available='n', description = ?, notes = ?, ".
1427 "circuitid = ?, privdata = ?, vrf = ?, rdns = ? ".
1428 "WHERE ip = ? AND parent_id = ?", undef,
1429 ($args{custid}, $args{city}, $args{desc}, $args{notes},
1430 $args{circid}, $args{privdata}, $args{vrf}, $args{rdns},
1431 $args{cidr}, $args{parent}) );
[157]1432
[397]1433# node hack
[554]1434 if ($args{nodeid} && $args{nodeid} ne '') {
1435 $dbh->do("INSERT INTO noderef (block,node_id) VALUES (?,?)", undef, ($args{cidr}, $args{nodeid}) );
[397]1436 }
1437# end node hack
[545]1438
[691]1439 $dbh->commit; # Allocate IP from pool
[77]1440 };
1441 if ($@) {
[545]1442 $msg .= ": $@";
[78]1443 eval { $dbh->rollback; };
[578]1444 return ('FAIL', $msg);
[77]1445 } else {
[585]1446 _rpc('addOrUpdateRevRec', cidr => "$args{cidr}", name => $args{rdns}, rpcuser => $args{user});
[578]1447 return ('OK', $args{cidr});
[77]1448 }
1449
1450 } else { # end IP-from-pool allocation
1451
[633]1452 if ($args{cidr} == $alloc_from) {
[77]1453 # Easiest case- insert in one table, delete in the other, and go home. More or less.
1454
1455 eval {
[554]1456 $msg = "Unable to allocate $args{cidr} as '$disp_alloctypes{$args{type}}'";
1457
[633]1458 # Insert the allocations entry
1459 $dbh->do("INSERT INTO allocations ".
[691]1460 "(cidr,parent_id,master_id,vrf,vlan,custid,type,city,description,notes,circuitid,privdata,rdns)".
1461 " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", undef,
1462 ($args{cidr}, $fbparent, $fbmaster, $args{vrf}, $args{vlan}, $args{custid}, $args{type}, $args{city},
[633]1463 $args{desc}, $args{notes}, $args{circid}, $args{privdata}, $args{rdns}) );
1464 my ($bid) = $dbh->selectrow_array("SELECT currval('allocations_id_seq')");
[555]1465
[554]1466 # Munge freeblocks
1467 if ($args{type} =~ /^(.)[mc]$/) {
1468 # special case - block is a routed or container/"reserve" block
1469 my $rtype = $1;
[633]1470 $dbh->do("UPDATE freeblocks SET routed = ?,city = ?,parent_id = ? WHERE id = ?",
1471 undef, ($rtype, $args{city}, $bid, $args{fbid}) );
[77]1472 } else {
[554]1473 # "normal" case
[633]1474 $dbh->do("DELETE FROM freeblocks WHERE id = ?", undef, ($args{fbid}) );
[554]1475 }
[77]1476
[554]1477 # And initialize the pool, if necessary
1478 # PPPoE pools (currently dialup, DSL, and WiFi) get all IPs made available
1479 # "DHCP" or "real-subnet" pools have the net, gw, and bcast IPs removed.
1480 if ($args{type} =~ /^.p$/) {
1481 $msg = "Could not initialize IPs in new $disp_alloctypes{$args{type}} $args{cidr}";
[633]1482 my ($code,$rmsg) = initPool($dbh, $args{cidr}, $args{type}, $args{city}, "all", $bid);
[554]1483 die $rmsg if $code eq 'FAIL';
1484 } elsif ($args{type} =~ /^.d$/) {
1485 $msg = "Could not initialize IPs in new $disp_alloctypes{$args{type}} $args{cidr}";
[633]1486 my ($code,$rmsg) = initPool($dbh, $args{cidr}, $args{type}, $args{city}, "normal", $bid);
[554]1487 die $rmsg if $code eq 'FAIL';
1488 }
[79]1489
[397]1490# node hack
[554]1491 if ($args{nodeid} && $args{nodeid} ne '') {
1492 $dbh->do("INSERT INTO noderef (block,node_id) VALUES (?,?)", undef, ($args{cidr}, $args{nodeid}) );
[397]1493 }
1494# end node hack
[574]1495
[691]1496 $dbh->commit; # Simple block allocation
[78]1497 }; # end of eval
[77]1498 if ($@) {
[157]1499 $msg .= ": ".$@;
[77]1500 eval { $dbh->rollback; };
[157]1501 return ('FAIL',$msg);
[78]1502 }
[77]1503
1504 } else { # cidr != alloc_from
1505
1506 # Hard case. Allocation is smaller than free block.
[633]1507
1508 # make sure new allocation is in fact within freeblock. *sigh*
1509 return ('FAIL',"Requested allocation $args{cidr} is not within $alloc_from")
1510 if !$alloc_from->contains($args{cidr});
[554]1511 my $wantmaskbits = $args{cidr}->masklen;
[633]1512 my $maskbits = $alloc_from->masklen;
[77]1513
1514 my @newfreeblocks; # Holds free blocks generated from splitting the source freeblock.
1515
1516 # This determines which blocks will be left "free" after allocation. We take the
1517 # block we're allocating from, and split it in half. We see which half the wanted
1518 # block is in, and repeat until the wanted block is equal to one of the halves.
1519 my $i=0;
[633]1520 my $tmp_from = $alloc_from; # So we don't munge $args{alloc_from}
[77]1521 while ($maskbits++ < $wantmaskbits) {
1522 my @subblocks = $tmp_from->split($maskbits);
[554]1523 $newfreeblocks[$i++] = (($args{cidr}->within($subblocks[0])) ? $subblocks[1] : $subblocks[0]);
1524 $tmp_from = ( ($args{cidr}->within($subblocks[0])) ? $subblocks[0] : $subblocks[1] );
[77]1525 } # while
1526
1527 # Begin SQL transaction block
1528 eval {
[554]1529 $msg = "Unable to allocate $args{cidr} as '$disp_alloctypes{$args{type}}'";
[79]1530
[77]1531 # Delete old freeblocks entry
[633]1532 $dbh->do("DELETE FROM freeblocks WHERE id = ?", undef, ($args{fbid}) );
[77]1533
[633]1534 # Insert the allocations entry
1535 $dbh->do("INSERT INTO allocations ".
[691]1536 "(cidr,parent_id,master_id,vrf,vlan,custid,type,city,description,notes,circuitid,privdata,rdns)".
1537 " VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", undef,
1538 ($args{cidr}, $fbparent, $fbmaster, $args{vrf}, $args{vlan}, $args{custid}, $args{type}, $args{city},
[633]1539 $args{desc}, $args{notes}, $args{circid}, $args{privdata}, $args{rdns}) );
1540 my ($bid) = $dbh->selectrow_array("SELECT currval('allocations_id_seq')");
1541
[692]1542 # Insert new list of smaller free blocks left over. Flag the one that matches the
1543 # masklength of the new allocation, if a reserve block was requested.
1544 $sth = $dbh->prepare("INSERT INTO freeblocks (cidr,city,routed,vrf,parent_id,master_id,reserve_for) ".
1545 "VALUES (?,?,?,?,?,?,?)");
1546 foreach my $block (@newfreeblocks) {
1547 $sth->execute($block, $fcity, $alloc_from_type, $args{vrf}, $fbparent, $fbmaster,
[721]1548 ($args{reserve} && $block->masklen == $wantmaskbits ? $bid : 0));
[692]1549 }
1550
[554]1551 # For routed/container types, add a freeblock within the allocated block so we can subdivide it further
1552 if ($args{type} =~ /(.)[mc]/) { # rm and .c types - containers
1553 my $rtype = $1;
[698]1554 $sth->execute($args{cidr}, $args{city}, $rtype, $args{vrf}, $bid, $fbmaster, 0);
[554]1555 }
[79]1556
[554]1557 # And initialize the pool, if necessary
1558 # PPPoE pools (currently dialup, DSL, and WiFi) get all IPs made available
1559 # "DHCP" or "real-subnet" pools have the net, gw, and bcast IPs removed.
1560 if ($args{type} =~ /^.p$/) {
1561 $msg = "Could not initialize IPs in new $disp_alloctypes{$args{type}} $args{cidr}";
[633]1562 my ($code,$rmsg) = initPool($dbh, $args{cidr}, $args{type}, $args{city}, "all", $bid);
[554]1563 die $rmsg if $code eq 'FAIL';
1564 } elsif ($args{type} =~ /^.d$/) {
1565 $msg = "Could not initialize IPs in new $disp_alloctypes{$args{type}} $args{cidr}";
[633]1566 my ($code,$rmsg) = initPool($dbh, $args{cidr}, $args{type}, $args{city}, "normal", $bid);
[554]1567 die $rmsg if $code eq 'FAIL';
1568 }
[77]1569
[397]1570# node hack
[554]1571 if ($args{nodeid} && $args{nodeid} ne '') {
1572 $dbh->do("INSERT INTO noderef (block,node_id) VALUES (?,?)", undef, ($args{cidr}, $args{nodeid}) );
[397]1573 }
1574# end node hack
[554]1575
[691]1576 $dbh->commit; # Complex block allocation
[77]1577 }; # end eval
1578 if ($@) {
[256]1579 $msg .= ": ".$@;
[77]1580 eval { $dbh->rollback; };
[78]1581 return ('FAIL',$msg);
[77]1582 }
1583
1584 } # end fullcidr != alloc_from
1585
[590]1586 # now we do the DNS dance for netblocks, if we have an RPC server to do it with and a pattern to use.
1587 _rpc('addOrUpdateRevRec', cidr => "$args{cidr}", name => $args{rdns}, rpcuser => $args{user})
1588 if $args{rdns};
[585]1589
[677]1590 # and the per-IP set, if there is one.
1591 _rpc('updateRevSet', %{$args{iprev}}, rpcuser => $args{user});
1592
[585]1593 return ('OK', 'OK');
1594
[77]1595 } # end static-IP vs netblock allocation
1596
1597} # end allocateBlock()
1598
1599
1600## IPDB::initPool()
1601# Initializes a pool
1602# Requires a database handle, the pool CIDR, type, city, and a parameter
1603# indicating whether the pool should allow allocation of literally every
1604# IP, or if it should reserve network/gateway/broadcast IPs
[78]1605# Note that this is NOT done in a transaction, that's why it's a private
1606# function and should ONLY EVER get called from allocateBlock()
[77]1607sub initPool {
[633]1608 my ($dbh,undef,$type,$city,$class,$parent) = @_;
[77]1609 my $pool = new NetAddr::IP $_[1];
1610
[574]1611 # IPv6 does not lend itself to IP pools as supported
1612 return ('FAIL',"Refusing to create IPv6 static IP pool") if $pool->{isv6};
1613 # IPv4 pools don't make much sense beyond even /24. Allow up to 4096-host footshooting anyway.
1614 # NetAddr::IP won't allow more than a /16 (65k hosts).
1615 return ('FAIL',"Refusing to create oversized static IP pool") if $pool->masklen <= 20;
1616
[691]1617 # Retrieve some odds and ends for defaults on the IPs
[633]1618 my ($pcustid) = $dbh->selectrow_array("SELECT def_custid FROM alloctypes WHERE type=?", undef, ($type) );
[699]1619 my ($vrf,$vlan,$master) = $dbh->selectrow_array("SELECT vrf,vlan,master_id FROM allocations WHERE id = ?",
[691]1620 undef, ($parent) );
1621
[157]1622 $type =~ s/[pd]$/i/;
[77]1623 my $sth;
[157]1624 my $msg;
[77]1625
[157]1626 eval {
1627 # have to insert all pool IPs into poolips table as "unallocated".
[699]1628 $sth = $dbh->prepare("INSERT INTO poolips (ip,custid,city,type,parent_id,master_id) VALUES (?,?,?,?,?,?)");
[695]1629
1630 # in case of pool extension by some means, we need to see what IPs were already inserted
1631 my $tmp1 = $dbh->selectall_arrayref("SELECT ip FROM poolips WHERE parent_id = ?", undef, $parent);
1632 my %foundips;
1633 foreach (@{$tmp1}) {
1634 $foundips{$_->[0]} = 1;
1635 }
1636
[696]1637# Dodge an edge case - pool where IPs have been "stolen" and turned into a netblock assignment.
1638# We can't just "get all the current IPs, and add the missing ones", because some IPs are
1639# legitimately missing (for stretchy values of "legitimately").
1640
1641 my $pdata = getBlockData($dbh, $parent);
1642 my $pcidr = new NetAddr::IP $pdata->{block};
1643
1644 if ($pcidr != $pool) {
1645 # enumerate the IPs from the *old* pool, flag them as "found", so we can iterate the entire
1646 # requested pool and still make sure we skip the IPs in the old pool - even if they've been
1647 # "stolen" by legacy netblocks.
1648 my @oldips = $pcidr->hostenum;
1649 # decide whether to start excluding existing IPs at the "gateway" or "gateway+1"
1650 my $ostart = ($pdata->{type} =~ /^.d$/ ? 1 : 0);
1651 for (my $i = $ostart; $i<= $#oldips; $i++) {
1652 $foundips{$oldips[$i]} = 1;
1653 }
1654 }
1655
[695]1656 # enumerate the hosts in the IP range - everything except the first (net) and last (bcast) IP
[157]1657 my @poolip_list = $pool->hostenum;
[695]1658
1659 # always check/add IPs from gw+1 through bcast-1:
1660 # (but the set won't be in oooorderrrrr! <pout>)
1661 for (my $i=1; $i<=$#poolip_list; $i++) {
[696]1662 my $baseip = $poolip_list[$i]->addr;
1663 if ($baseip !~ /\.(?:0|255)$/ && !$foundips{$poolip_list[$i]}) {
[699]1664 $sth->execute($baseip, $pcustid, $city, $type, $parent, $master);
[696]1665 }
[695]1666 }
1667
1668 # now do the special case - DSL/PPP blocks can use the "net", "gw", and "bcast" IPs.
1669 # we exclude .0 and .255 anyway, since while they'll mostly work, they *will* behave badly here and there.
[157]1670 if ($class eq 'all') { # (DSL-ish block - *all* IPs available
[246]1671 if ($pool->addr !~ /\.0$/) { # .0 causes weirdness.
[734]1672 $sth->execute($pool->addr, $pcustid, $city, $type, $parent, $master) unless $foundips{$pool->addr."/32"};
[246]1673 }
[699]1674 $sth->execute($poolip_list[0]->addr, $pcustid, $city, $type, $parent, $master) unless $foundips{$poolip_list[0]};
[157]1675 $pool--;
[246]1676 if ($pool->addr !~ /\.255$/) { # .255 can cause weirdness.
[734]1677 $sth->execute($pool->addr, $pcustid, $city, $type, $parent, $master) unless $foundips{$pool->addr."/32"};
[246]1678 }
[77]1679 }
[633]1680# don't commit here! the caller may not be done.
1681# $dbh->commit;
[157]1682 };
1683 if ($@) {
[574]1684 $msg = $@;
[633]1685# Don't roll back! It's up to the caller to handle this.
1686# eval { $dbh->rollback; };
[157]1687 return ('FAIL',$msg);
1688 } else {
1689 return ('OK',"OK");
[77]1690 }
1691} # end initPool()
1692
1693
[531]1694## IPDB::updateBlock()
1695# Update an allocation
1696# Takes all allocation fields in a hash
1697sub updateBlock {
1698 my $dbh = shift;
1699 my %args = @_;
1700
1701 return ('FAIL', 'Missing block to update') if !$args{block};
1702
[634]1703 # Spaces don't show up well in lots of places. Make sure they don't get into the DB.
1704 $args{custid} =~ s/^\s+//;
1705 $args{custid} =~ s/\s+$//;
1706
[531]1707 # do it all in a transaction
1708 local $dbh->{AutoCommit} = 0;
1709 local $dbh->{RaiseError} = 1;
1710
1711 my @fieldlist;
1712 my @vallist;
[691]1713 foreach ('custid', 'city', 'description', 'notes', 'circuitid', 'privdata', 'rdns', 'vrf', 'vlan') {
[531]1714 if ($args{$_}) {
1715 push @fieldlist, $_;
1716 push @vallist, $args{$_};
1717 }
1718 }
1719
[634]1720 my $binfo;
[531]1721 my $updtable = 'allocations';
[634]1722 my $keyfield = 'id';
[535]1723 if ($args{type} =~ /^(.)i$/) {
[531]1724 $updtable = 'poolips';
[634]1725 $binfo = getBlockData($dbh, $args{block}, 'i');
[531]1726 } else {
1727## fixme: there's got to be a better way...
[634]1728 $binfo = getBlockData($dbh, $args{block});
[531]1729 if ($args{swip}) {
1730 if ($args{swip} eq 'on' || $args{swip} eq '1' || $args{swip} eq 'y') {
1731 $args{swip} = 'y';
1732 } else {
1733 $args{swip} = 'n';
1734 }
1735 }
1736 foreach ('type', 'swip') {
1737 if ($args{$_}) {
1738 push @fieldlist, $_;
1739 push @vallist, $args{$_};
1740 }
1741 }
1742 }
1743
1744 return ('FAIL', 'No fields to update') if !@fieldlist;
1745
1746 my $sql = "UPDATE $updtable SET ";
1747 $sql .= join " = ?, ", @fieldlist;
1748
1749 eval {
[695]1750 # check for block merge first...
1751 if ($args{fbmerge}) {
1752 my $cidr = NetAddr::IP->new($binfo->{block});
1753 my $newblock = NetAddr::IP->new($cidr->addr, $cidr->masklen - 1)->network;
1754 # safety net? make sure mergeable block passed in is really one or both of
1755 # a) reserved for expansion of the block and
1756 # b) confirmed CIDR-combinable
1757 # "safety? SELECT foo FROM freeblocks WHERE cidr << ? AND masklen(cidr) = ?, $newblock, ".$cidr->masklen."\n";
1758 $dbh->do("DELETE FROM freeblocks WHERE id=?", undef, $args{fbmerge});
1759 # ... so we can append the change in the stored CIDR field to extend the allocation.
1760 $sql .= " = ?, cidr";
1761 push @vallist, $newblock;
1762 # if we have an IP pool, call initPool to fill in any missing entries in the pool
1763 if ($binfo->{type} =~ /^.p$/) {
1764 my ($code,$rmsg) = initPool($dbh, "$newblock", $binfo->{type}, $binfo->{city}, 'all', $args{block});
1765 die $rmsg if $code eq 'FAIL';
1766 } elsif ($binfo->{type} =~ /^.d$/) {
1767 my ($code,$rmsg) = initPool($dbh, "$newblock", $binfo->{type}, $binfo->{city}, 'normal', $args{block});
1768 die $rmsg if $code eq 'FAIL';
1769 }
1770 }
1771
1772 # append another SQL fragment
1773 push @vallist, $args{block};
1774 $sql .= " = ? WHERE $keyfield = ?";
1775
[531]1776 # do the update
1777 $dbh->do($sql, undef, @vallist);
1778
1779 if ($args{node}) {
1780 # done with delete/insert so we don't have to worry about funkyness updating a node ref that isn't there
[634]1781 $dbh->do("DELETE FROM noderef WHERE block = ?", undef, ($binfo->{block}) );
1782 $dbh->do("INSERT INTO noderef (block,node_id) VALUES (?,?)", undef, ($binfo->{block}, $args{node}) )
1783 if $args{node} ne '--';
[531]1784 }
1785
1786 $dbh->commit;
1787 };
1788 if ($@) {
1789 my $msg = $@;
1790 $dbh->rollback;
1791 return ('FAIL', $msg);
1792 }
[588]1793
[686]1794 # In case of any container (mainly master block), only update freeblocks so we don't stomp subs
1795 # (which would be the wrong thing in pretty much any case except "DELETE ALL EVARYTHING!!1!oneone!")
1796 if ($binfo->{type} =~ '.[mc]') {
1797 # Not using listFree() as it doesn't return quite all of the blocks wanted.
1798 # Retrieve the immediate free blocks
1799 my $sth = $dbh->prepare(q(
1800 SELECT cidr FROM freeblocks WHERE parent_id = ?
1801 UNION
1802 SELECT cidr FROM freeblocks f WHERE
1803 cidr = (SELECT cidr FROM allocations a WHERE f.cidr = a.cidr)
1804 AND master_id = ?
1805 ) );
1806 $sth->execute($args{block}, $binfo->{master_id});
1807 my %fbset;
1808 while (my ($fb) = $sth->fetchrow_array) {
1809 $fbset{"host_$fb"} = $args{rdns};
1810 }
1811 # We use this RPC call instead of multiple addOrUpdateRevRec calls, since we don't
1812 # know how many records we'll be updating and more than 3-4 is far too slow. This
1813 # should be safe to call unconditionally.
1814 # Requires dnsadmin >= r678
1815 _rpc('updateRevSet', %fbset, rpcuser => $args{user});
[677]1816
[686]1817 } else {
1818 $binfo->{block} =~ s|/32$||;
1819 _rpc('addOrUpdateRevRec', cidr => $binfo->{block}, name => $args{rdns}, rpcuser => $args{user});
[677]1820
[686]1821 # and the per-IP set, if there is one.
1822 _rpc('updateRevSet', %{$args{iprev}}, rpcuser => $args{user}) if keys (%{$args{iprev}});
1823 }
1824
[588]1825 return ('OK','OK');
[531]1826} # end updateBlock()
1827
1828
[702]1829## IPDB::splitBlock()
1830# Splits an existing allocation into two or more smaller allocations based on a passed netmask
1831# Duplicates all other data
1832# Returns an arrayref to a list of hashrefs with ID and CIDR keys for the list of new allocations.
1833# Should probably commit DNS magic to realign DNS data
[713]1834# Mostly works but may return Strange Things(TM) if used on a master block
[702]1835sub splitBlock {
1836 my $dbh = shift;
[707]1837 my %args = @_;
1838# my $id = shift;
1839# my $basetype = shift;
1840# my $newmask = shift;
[702]1841
1842##fixme: set errstr on errors so caller can suitably clue-by-four the user
[707]1843 return if $args{basetype} ne 'b'; # only netblocks allowed!
[702]1844
[707]1845 my $binfo = getBlockData($dbh, $args{id});
[702]1846 return if !$binfo;
1847
[707]1848 return if $args{newmask} !~ /^\d+$/;
[702]1849
1850 my @ret;
1851 my $block = new NetAddr::IP $binfo->{block};
1852 my $oldmask = $block->masklen;
1853
1854 # Fail if the block to split is "too small" - eg, can't split a v4 /32 at all
1855 # failure modes:
1856 # difference between $oldmask and $newmask is negative or 0
[707]1857 if ($args{newmask} - $oldmask <= 0) {
1858 $errstr = "Can't split a /$oldmask allocation into /$args{newmask} pieces";
[702]1859 return;
1860 }
1861# # difference between $oldmask and $newmask is > n, for arbitrary n?
1862# if ($newmask - $oldmask > 42) { # because 42
1863# }
1864 # $oldmask > n, for arbitrary n? At least check limits of data type.
1865 if ($block->{isv6}) {
[707]1866 if ($args{newmask} - $oldmask > 128) {
1867 $errstr = "Impossible IPv6 mask length /$args{newmask} requested";
[702]1868 return;
1869 }
1870 } else {
[707]1871 if ($args{newmask} - $oldmask > 32) {
1872 $errstr = "Impossible IPv4 mask length /$args{newmask} requested";
[702]1873 return;
1874 }
1875 }
1876
[707]1877 my @newblocks = $block->split($args{newmask});
[702]1878
1879 local $dbh->{AutoCommit} = 0;
1880 local $dbh->{RaiseError} = 1;
1881
1882 eval {
1883 # line up a list of fields and values. Be nice if there was a handy way to do,
1884 # direct in SQL, something like
1885 # "INSERT INTO foo (f1,f2,f3) VALUES (newf1,(SELECT oldf2,oldf3 FROM foo WHERE baz))"
1886 my @fieldlist = qw(type city description notes circuitid privdata custid swip vrf vlan rdns parent_id master_id);
1887 my $fields_sql = join(',', @fieldlist);
1888 my @vals;
1889 foreach (@fieldlist) {
1890 push @vals, $binfo->{$_};
1891 }
1892 # note the first block in the split for return
[707]1893 push @ret, {nid => $args{id}, nblock => "$newblocks[0]"};
[702]1894
1895 # prepare
1896 my $idsth = $dbh->prepare("SELECT currval('allocations_id_seq')");
[713]1897 my $allocsth = $dbh->prepare("INSERT INTO allocations (cidr, $fields_sql)".
[702]1898 " VALUES (?".',?'x(scalar(@fieldlist)).")");
1899 my $nbsth = $dbh->prepare("DELETE FROM poolips WHERE parent_id = ? AND ip = ?");
[713]1900 my $upd_psth = $dbh->prepare("UPDATE allocations SET parent_id = ? WHERE parent_id = ? AND cidr <<= ?");
1901 my $upd_msth = $dbh->prepare("UPDATE allocations SET master_id = ? WHERE master_id = ? AND cidr <<= ?");
1902 my $fb_psth = $dbh->prepare("UPDATE freeblocks SET parent_id = ? WHERE parent_id = ? AND cidr <<= ?");
1903 my $fb_msth = $dbh->prepare("UPDATE freeblocks SET master_id = ? WHERE master_id = ? AND cidr <<= ?");
1904 my $pool_psth = $dbh->prepare("UPDATE poolips SET parent_id = ? WHERE parent_id = ? AND ip << ?");
1905 my $pool_msth = $dbh->prepare("UPDATE poolips SET master_id = ? WHERE master_id = ? AND ip <<= ?");
[702]1906
1907 # set up update of existing block
[707]1908 $dbh->do("UPDATE allocations SET cidr = ? WHERE id = ?", undef, ("$newblocks[0]", $args{id}) );
[702]1909
[713]1910 # axe the new bcast IP from the smaller pool at the "base" block, if it's a "normal" pool
[702]1911 if ($binfo->{type} =~ /.d/) {
1912 $newblocks[0]--;
[707]1913 $nbsth->execute($args{id}, $newblocks[0]->addr);
[702]1914 }
1915
[739]1916 # Holder for freeblocks-to-delete. Should be impossible to have more than one...
1917 my %fbdel;
1918
[702]1919 # Loop over the new blocks that are not the base block
1920 for (my $i = 1; $i <= $#newblocks; $i++) {
[713]1921 # add the new allocation
1922 $allocsth->execute($newblocks[$i], @vals);
[702]1923 # fetch the ID of the entry we just added...
1924 $idsth->execute();
1925 my ($nid) = $idsth->fetchrow_array();
1926 # ... so we can pass back the list of blocks and IDs...
1927 push @ret, {nid => $nid, nblock => "$newblocks[$i]"};
1928 # axe the net, gw, and bcast IPs as necessary when splitting a "normal" pool
1929 if ($binfo->{type} =~ /.d/) {
1930 # net
[707]1931 $nbsth->execute($args{id}, $newblocks[$i]->addr);
[702]1932 $newblocks[$i]++;
1933 # gw
[707]1934 $nbsth->execute($args{id}, $newblocks[$i]->addr);
[702]1935 $newblocks[$i]--;
1936 $newblocks[$i]--;
1937 # bcast
[707]1938 $nbsth->execute($args{id}, $newblocks[$i]->addr);
[702]1939 $newblocks[$i]++;
[739]1940 } # $binfo->{type} =~ /.d/
[702]1941
[739]1942 # Check for free blocks larger than the new mask length, and split those as needed.
1943 if ($binfo->{type} =~ /.[cm]/) {
1944 # get a "list" of freeblocks bigger than the allocation in the parent. there's only one, right?
1945 my $fblist = $dbh->selectall_arrayref("SELECT id FROM freeblocks WHERE cidr >> ? AND parent_id = ? ",
1946 {Slice=>{}}, $newblocks[$i], $args{id});
1947 if (@$fblist) {
1948 # create a new freeblock for the new block we created earlier
1949 $dbh->do(q{
1950 INSERT INTO freeblocks (cidr, parent_id, master_id, city, routed,vrf)
1951 SELECT ? AS cidr, ? AS parent_id, master_id, city, routed, vrf FROM freeblocks
1952 WHERE id = ?
1953 }, undef, ($newblocks[$i], $nid, $fblist->[0]->{id}) );
1954 $fbdel{$fblist->[0]->{id}}++;
1955 }
1956 } # $binfo->{type} =~ /.[cm]/
1957
[713]1958 # Reparent allocations, freeblocks, and pool IPs.
1959 $upd_psth->execute($nid, $args{id}, $newblocks[$i]);
1960 $fb_psth->execute($nid, $args{id}, $newblocks[$i]);
1961 $pool_psth->execute($nid, $args{id}, $newblocks[$i]);
1962
1963 # Update master if we've split a master block
1964 if ($binfo->{type} eq 'mm') {
1965 $upd_msth->execute($nid, $args{id}, $newblocks[$i]);
1966 $fb_msth->execute($nid, $args{id}, $newblocks[$i]);
1967 $pool_msth->execute($nid, $args{id}, $newblocks[$i]);
1968 }
1969
[739]1970##fixme:
1971# Still missing one edge case - megasplitting a large block such that "many" children also need to be split.
1972# I'm going to call this "unsupported" because I really can't imagine a sane reason for doing this.
1973# Should probably check and error out at least
1974
[713]1975 } # for (... @newblocks)
1976
[739]1977 $dbh->do("DELETE FROM freeblocks WHERE id IN (".join(',', keys %fbdel).")");
1978
[702]1979 $dbh->commit;
1980 };
1981 if ($@) {
1982 $errstr = "Error splitting $binfo->{block}: $@";
1983 $dbh->rollback;
1984 return;
1985 }
[707]1986##fixme: RPC return code?
1987# particularly useful in this case as there may be arbitrary combinations of pass, warn, fail
1988# results from each of the update and add(s), which could get the user to prod dnsadmin to see what exploded
1989 my $foo = _rpc('splitTemplate', cidr => $binfo->{block}, newmask => $args{newmask}, rpcuser => $args{user});
[702]1990
1991 return \@ret;
1992} # end splitBlock()
1993
1994
[705]1995## IPDB::shrinkBlock()
1996# Shrink an allocation to the passed CIDR block
1997# Takes an allocation ID and a new CIDR
1998# Returns an arrayref to a list of hashrefs with the ID and CIDR of the freed block(s)
1999# Refuses to shrink "real netblock" pool types below /30
2000sub shrinkBlock {
2001 my $dbh = shift;
2002 my $id = shift;
2003
2004 # just take the new CIDR spec; this way we can shrink eg .16/28 to .20/30 without extra contortions
2005 my $newblock = new NetAddr::IP shift;
2006
2007 if (!$newblock) {
2008 $errstr = "Can't shrink something that's not a netblock";
2009 return;
2010 }
2011
2012 my $binfo = getBlockData($dbh, $id);
2013 my $pinfo = getBlockData($dbh, $binfo->{parent_id});
2014
2015 if ($binfo->{type} =~ /.d/ && $newblock->masklen > ($newblock->bits+2) ) {
2016 $errstr = "Can't shrink a non-PPP pool smaller than ".($newblock->{isv6} ? '/124' : '/30');
2017 return;
2018 }
2019
2020 my $oldblock = new NetAddr::IP $binfo->{block};
2021
2022 # Don't try to shrink the block outside of itself, Bad Things (probably) Happen.
2023 if (!$oldblock->contains($newblock)) {
2024 $errstr = "Can't shrink an allocation outside of itself";
2025 return;
2026 }
2027
2028 local $dbh->{AutoCommit} = 0;
2029 local $dbh->{RaiseError} = 1;
2030
[713]2031 my $addfbsth = $dbh->prepare("INSERT INTO freeblocks (cidr,city,routed,vrf,parent_id,master_id) VALUES (?,?,?,?,?,?)");
[705]2032 my $idsth = $dbh->prepare("SELECT currval('freeblocks_id_seq')");
2033 my $poolsth = $dbh->prepare("DELETE FROM poolips WHERE parent_id = ? AND ip << ?");
2034 my $netsth = $dbh->prepare("DELETE FROM poolips WHERE parent_id = ? AND ip = ?");
[713]2035 my $allocsth = $dbh->prepare("DELETE FROM allocations WHERE parent_id = ? AND cidr <<= ?");
2036 my $delfbsth = $dbh->prepare("DELETE FROM freeblocks WHERE parent_id = ? AND cidr <<= ?");
[705]2037
[713]2038##fixme: turn this into a public/top-level sub?
2039 sub getchildren {
2040 my $dbh = shift;
2041 my $id = shift;
2042 my $master = shift;
2043 my $retlist = shift; # better than trying to return complex structures recursively. Ow.
2044 my $cidr = shift;
2045
2046 if (!$cidr) {
2047 my $bd = getBlockData($dbh, $id);
2048 $cidr = $bd->{cidr};
2049 }
2050
2051 my $sth = $dbh->prepare(q(
2052 SELECT id,cidr,type FROM allocations
2053 WHERE parent_id = ? AND master_id = ? AND cidr <<= ?
2054 ) );
2055 $sth->execute($id, $master, $cidr);
2056 while (my $row = $sth->fetchrow_hashref) {
2057 push @$retlist, $row;
2058 getchildren($dbh, $row->{id}, $master, $retlist, $cidr);
2059 }
2060 }
2061
[705]2062 my @ret;
[743]2063 my @newfreelist;
[705]2064 eval {
2065 $dbh->do("UPDATE allocations SET cidr = ? WHERE id = ?", undef, $newblock, $id);
2066
2067 # find the netblock(s) that are now free
2068 my @workingblocks = $oldblock->split($newblock->masklen);
2069 foreach my $newsub (@workingblocks) {
2070 next if $newsub == $newblock;
[743]2071 push @newfreelist, $newsub;
[705]2072 }
[743]2073 @newfreelist = Compact(@newfreelist);
[705]2074
2075 # set new freeblocks, and clean up any IP pool entries if needed.
[743]2076 foreach my $newfree (@newfreelist) {
[713]2077 my @clist;
2078 # the block we're munging
2079 push @clist, { id => $id, type => $binfo->{type}, cidr => $binfo->{block} };
2080 getchildren($dbh, $id, $binfo->{master_id}, \@clist, $newfree);
2081
2082 foreach my $goner (@clist) {
2083 $poolsth->execute($goner->{id}, $newfree) if $goner->{type} =~ /.[dp]/;
2084 $allocsth->execute($goner->{id}, $newfree);
2085 $delfbsth->execute($goner->{id}, $newfree);
[705]2086 }
2087
[713]2088 # No pinfo means we're shrinking a master block, which means the free space is returned outside of IPDB.
2089 if ($pinfo) {
2090 $addfbsth->execute($newfree, $pinfo->{city}, 'm', $pinfo->{vrf}, $binfo->{parent_id}, $pinfo->{master_id});
2091 $idsth->execute;
2092 my ($nid) = $idsth->fetchrow_array();
2093 # add to return list
2094 push @ret, {fbid => $nid, newfree => "$newfree", fbparent => $binfo->{parent_id} };
2095 }
2096
[743]2097 } # $newfree (@newfreelist)
[713]2098
[705]2099 # additional cleanup on net/gw/bcast IPs in pool
2100 if ($binfo->{type} =~ /.d/) {
2101 $netsth->execute($id, $newblock->addr);
2102 $newblock++;
2103 $netsth->execute($id, $newblock->addr);
2104 $newblock--;
2105 $newblock--;
2106 $netsth->execute($id, $newblock->addr);
2107 }
2108
2109 $dbh->commit;
2110 };
2111 if ($@) {
2112 $errstr = "Error splitting $binfo->{block}: $@";
2113 $dbh->rollback;
2114 return;
2115 }
2116
2117 return \@ret;
2118} # end shrinkBlock()
2119
2120
[728]2121## IPDB::mergeBlocks()
2122# Merges two or more adjacent allocations, optionally including relevant
2123# free space, into one allocation.
2124# Takes a "base" block ID and a hash with a mask length and a scope argument to decide
2125# how much existing allocation data to delete.
[733]2126# Returns a list starting with the new merged block, then the merged allocations with comment
[728]2127## Merge scope:
2128# Merge to container
2129# keepall
2130# Move all mergeable allocations into the new block
2131# Move all mergeable free blocks into the new block
2132# mergepeer
2133# Move subs of mergeable containers into the updated primary.
2134# Reparent free blocks in mergeable containers to the updated primary.
2135# Convert assigned IPs from pools into subs.
2136# Convert unused IPs from pools into free blocks.
2137# Convert leaf allocations into free blocks.
2138# clearpeer
2139# Keep subs of the original
2140# Convert assigned IPs from pools into subs.
2141# Convert unused IPs from pools into free blocks.
2142# Convert leaf allocations into free blocks.
2143# clearall
2144# Delete all peers, subs and IPs.
2145# Add single free block for new container.
2146# Merge to pool
2147# keepall
2148# Convert all leaf allocations in the merge range to groups of used IPs
2149# mergepeer
2150# Effectively equal to keepall
2151# clearpeer
2152# Only convert IPs from the original allocation to used IPs
2153# clearall
2154# Delete any existing IPs, and reinitialize the new pool entirely
2155# Merge to leaf type
2156# Remove all subs
2157sub mergeBlocks {
2158 my $dbh = shift;
2159 my $prime = shift; # "base" block ID to use as a starting point
2160 if (!$prime) {
2161 $errstr = "Missing block ID to base merge on";
2162 return;
2163 }
2164
2165 my %args = @_;
2166
2167 # check key arguments.
2168 if (!$args{scope} || $args{scope} !~ /^(keepall|mergepeer|clearpeer|clearall)$/) {
2169 $errstr = "Bad or missing merge scope";
2170 return;
2171 }
2172 if (!$args{newmask} || $args{newmask} !~ /^\d+$/) {
2173 $errstr = "Bad or missing new netmask";
2174 return;
2175 }
2176
2177 # Retrieve info about the base allocation we're munging
2178 my $binfo = getBlockData($dbh, $prime);
2179 my $block = new NetAddr::IP $binfo->{block};
2180 my ($basetype) = ($binfo->{type} =~ /^.(.)$/);
2181 $binfo->{id} = $prime; # preserve for later, just in case
2182
2183 # proposed block
2184 my $newblock = new NetAddr::IP $block->addr."/$args{newmask}";
2185 $newblock = $newblock->network;
2186 $args{newtype} = $binfo->{type} if !$args{newtype};
2187 # if the "primary" block being changed is a master, it must remain one.
[733]2188 # Also force the scope, since otherwise things get ugly.
2189 if ($binfo->{type} eq 'mm') {
2190 $args{newtype} = 'mm';
2191 # don't want to make a peer master a sub of the existing one; too many special cases go explodey,
2192 # but want to retain all other allocations
2193 $args{scope} = 'mergepeer';
2194 }
[728]2195 my ($newcontainerclass) = ($args{newtype} =~ /^(.).$/);
2196
2197 # build an info hash for the "new" allocation we're creating
2198 my $pinfo = {
2199 id => $prime,
2200 block => "$newblock",
2201 type => $args{newtype},
2202 parent_id =>
2203 $binfo->{parent_id},
2204 city => $binfo->{city},
2205 vrf => $binfo->{vrf},
2206 master_id => $binfo->{master_id}
2207 };
2208
2209 my @retlist;
2210
[729]2211 local $dbh->{AutoCommit} = 0;
2212 local $dbh->{RaiseError} = 1;
2213
[728]2214 # Want to do all of the DB stuff in a transaction, to minimize data changing underfoot
2215 eval {
2216
2217 # We always update the "prime" block passed in...
2218 $dbh->do("UPDATE allocations SET cidr = ?, type = ? WHERE id = ?", undef,
2219 ($newblock, $args{newtype}, $prime) )
2220 # ... but only on existing container or pool types. Leaf -> container conversions
2221 # may need a new parent inserted instead.
2222 if $basetype =~ /[cm]/;
2223
2224 # For leaf blocks, we may need to create a new parent as the "primary" instead
2225 # of updating the existing block
2226 my $newparent = $dbh->prepare(q{
2227 INSERT INTO allocations (
2228 cidr, type, city, description, notes, circuitid, createstamp, modifystamp,
2229 privdata, custid, swip, vrf, vlan, rdns, parent_id, master_id
2230 )
2231 SELECT
2232 ? AS cidr, ? AS type, city, description, notes, circuitid, createstamp, modifystamp,
2233 privdata, custid, swip, vrf, vlan, rdns, parent_id, master_id
2234 FROM allocations
2235 WHERE id = ?
2236 });
2237
2238 # Common actions
2239 my $peersth = $dbh->prepare("SELECT cidr,id,type,master_id FROM allocations WHERE parent_id = ? AND cidr <<= ?");
2240 $peersth->execute($binfo->{parent_id}, "$newblock");
2241 my $reparentsth = $dbh->prepare("UPDATE allocations SET parent_id = ?, master_id = ? WHERE id = ?");
2242 my $insfbsth = $dbh->prepare("INSERT INTO freeblocks (cidr,city,routed,vrf,parent_id,master_id) VALUES (?,?,?,?,?,?)");
[737]2243 my $delsth = $dbh->prepare("DELETE FROM allocations WHERE id = ?");
[728]2244
2245 my $fbreparentsth = $dbh->prepare(q{
2246 UPDATE freeblocks
2247 SET parent_id = ?, master_id = ?, city = ?, routed = ?, vrf = ?
2248 WHERE parent_id = ? AND cidr <<= ?
2249 });
2250
2251 if ($args{newtype} =~ /.[cm]/) {
2252 ## Container
2253
[733]2254 # In case of merging a master block. Somewhat redundant with calls to $fbreparentsth,
2255 # but not *quite* entirely.
[729]2256 my $mfbsth = $dbh->prepare("UPDATE freeblocks SET master_id = ? WHERE master_id = ?");
2257
2258 if ($args{scope} eq 'keepall') {
[730]2259 # Create a new parent with the same info as the passed "primary".
2260 $newparent->execute($newblock, $args{newtype}, $prime);
2261 # and now retrieve the new parent ID
2262 ($prime) = $dbh->selectrow_array("SELECT currval('allocations_id_seq')");
[733]2263 # snag the new parent info for the return list
2264 push @retlist, { block => "$newblock", type => $disp_alloctypes{$args{newtype}}, id => $prime };
[730]2265 # Reparent the free blocks in the new block
2266 $fbreparentsth->execute($prime, $binfo->{master_id}, $binfo->{city}, $newcontainerclass, $binfo->{vrf},
2267 $binfo->{parent_id}, $newblock);
2268 # keep existing allocations (including the original primary), just push them down a level
[733]2269 while (my ($peercidr, $peer_id, $peertype, $m_id) = $peersth->fetchrow_array) {
[730]2270 $reparentsth->execute($prime, $binfo->{master_id}, $peer_id);
2271 # Fix up master_id on free blocks if we're merging a master block
[733]2272 $mfbsth->execute($binfo->{master_id}, $m_id) if $peertype eq 'mm';
2273 # capture block for return
2274 push @retlist, { block => $peercidr, mtype => $disp_alloctypes{$peertype} };
[730]2275 }
2276
[729]2277 } elsif ($args{scope} =~ /^clear/) {
[731]2278 # clearpeer and clearall share a starting point
[733]2279 # snag the new parent info for the return list
2280 push @retlist, { block => "$newblock", type => $disp_alloctypes{$args{newtype}}, id => $prime };
[731]2281 # update the primary allocation info
2282 $dbh->do("UPDATE allocations SET cidr = ?, type = ? WHERE id = ?", undef, ($newblock, $args{newtype}, $prime) );
2283 # Reparent the free blocks in the new block
2284 $fbreparentsth->execute($prime, $binfo->{master_id}, $binfo->{city}, $newcontainerclass, $binfo->{vrf},
2285 $binfo->{parent_id}, $newblock);
2286 # Insert a free block if $prime is a leaf
2287 if ($binfo->{type} =~ /.[enr]/) {
2288 $insfbsth->execute($binfo->{block}, $binfo->{city}, $newcontainerclass, $binfo->{vrf}, $prime,
2289 $binfo->{master_id});
2290 }
[733]2291 # delete the peers.
2292 while (my ($peercidr, $peer_id, $peertype, $m_id) = $peersth->fetchrow_array) {
[731]2293 next if $peer_id == $prime;
2294 # push existing allocations down a level before deleting,
2295 # so that when they're deleted the parent info is correct
2296 $reparentsth->execute($prime, $binfo->{master_id}, $peer_id);
2297 _deleteCascade($dbh, $peer_id);
2298 # insert the freeblock _deleteCascade() (deliberately) didn't when deleting a master block.
2299 # aren't special cases fun?
2300 $dbh->do("INSERT INTO freeblocks (cidr,routed,parent_id,master_id) values (?,?,?,?)",
[733]2301 undef, ($peercidr, 'm', $prime, $prime) ) if $binfo->{type} eq 'mm';
2302 # capture block for return
2303 push @retlist, { block => $peercidr, mtype => $disp_alloctypes{$peertype} };
[731]2304 }
2305 if ($args{scope} eq 'clearall') {
2306 # delete any subs of $prime as well
2307 my $substh = $dbh->prepare("SELECT cidr,id FROM allocations WHERE parent_id = ?");
2308 $substh->execute($prime);
2309 while (my ($scidr, $s_id) = $substh->fetchrow_array) {
2310 _deleteCascade($dbh, $s_id);
2311 }
2312 } else {
2313 # clearpeer
2314 if ($basetype =~ /[dp]/) {
2315 # Convert active IP pool entries to allocations if the original was an IP pool
2316 _poolToAllocations($dbh, $binfo, $pinfo, newtype => $poolmap{$binfo->{type}});
2317 }
2318 } # clearall or clearpeer
2319
[729]2320 } elsif ($args{scope} eq 'mergepeer') { # should this just be an else?
[732]2321 # Default case. Merge "peer" blocks, but keep all suballocations
[733]2322 # snag the new parent info for the return list
2323 push @retlist, {block => "$newblock", type => $disp_alloctypes{$args{newtype}}, id => $prime};
[732]2324 my $substh = $dbh->prepare("UPDATE allocations SET parent_id = ? WHERE parent_id = ?");
2325 my $delsth = $dbh->prepare("DELETE FROM allocations WHERE id = ?");
2326 # Reparent freeblocks in parent
2327 $fbreparentsth->execute($prime, $binfo->{master_id}, $binfo->{city}, $newcontainerclass, $binfo->{vrf},
2328 $binfo->{parent_id}, $newblock);
2329 # Loop over "peer" allocations to be merged
2330 while (my ($peercidr, $peer_id, $peertype, $m_id) = $peersth->fetchrow_array) {
2331 # Snag existing peer data since we may need it
2332 my $peerfull = getBlockData($dbh, $peer_id);
2333 # Reparent free blocks from existing containers
2334 $fbreparentsth->execute($prime, $binfo->{master_id}, $binfo->{city}, $newcontainerclass,
2335 $binfo->{vrf}, $peer_id, $newblock);
2336 # Reparent any subblocks from existing containers
2337 $substh->execute($prime, $peer_id);
2338 # Delete the old container
2339 $delsth->execute($peer_id) unless $peer_id == $prime;
2340 # Add new freeblocks for merged leaf blocks
2341 $insfbsth->execute($peercidr, $binfo->{city}, $newcontainerclass, $binfo->{vrf}, $binfo->{id},
2342 $binfo->{master_id}) if $peertype =~ /.[enr]/;
2343 # Convert pool IPs into allocations or aggregated free blocks
[733]2344 _poolToAllocations($dbh, $peerfull, $pinfo, newparent => $prime) if $peertype =~ /.[dp]/;
[732]2345 # Fix up master_id on free blocks if we're merging a master block
2346 $mfbsth->execute($binfo->{master_id}, $m_id) if $peertype eq 'mm';
[733]2347 # capture block for return
2348 push @retlist, { block => $peercidr, mtype => $disp_alloctypes{$peertype} };
[732]2349 } # merge peers
2350 # update the primary allocation info. Do this last so we don't stomp extra data-retrieval in the loop above
2351 $dbh->do("UPDATE allocations SET cidr = ?, type = ? WHERE id = ?", undef, ($newblock, $args{newtype}, $prime) );
2352
[729]2353 } # scope
2354
2355 # Clean up free blocks
2356 _compactFree($dbh, $prime);
2357
[728]2358 } elsif ($args{newtype} =~ /.[dp]/) {
2359 ## Pool
[737]2360 # Snag the new parent info for the return list
2361 push @retlist, { block => "$newblock", type => $disp_alloctypes{$args{newtype}}, id => $prime };
[728]2362
[737]2363 if ($args{scope} eq 'keepall') {
2364 # Convert all mergeable allocations and subs to chunks of pool IP assignments
2365 push @retlist, @{ _toPool($dbh, $prime, $newblock, $args{newtype}) };
2366
2367 } elsif ($args{scope} =~ /^clear/) {
2368 # Clear it all out for a fresh (mostly?) empty IP pool
2369 while (my ($peercidr, $peer_id, $peertype, $m_id) = $peersth->fetchrow_array) {
2370 next if $peer_id == $prime;
2371 # Push existing allocations down a level before deleting,
2372 # so that when they're deleted the parent info is correct
2373 $reparentsth->execute($prime, $binfo->{master_id}, $peer_id);
2374 _deleteCascade($dbh, $peer_id, 0);
2375 # Capture block for return
2376 push @retlist, { block => $peercidr, mtype => $disp_alloctypes{$peertype} };
2377 }
2378 if ($args{scope} eq 'clearall') {
2379 # Delete any subs of $prime as well
2380 my $substh = $dbh->prepare("SELECT cidr,id FROM allocations WHERE parent_id = ?");
2381 $substh->execute($prime);
2382 while (my ($scidr, $s_id) = $substh->fetchrow_array) {
2383 _deleteCascade($dbh, $s_id);
2384 }
2385 } else {
2386 # Convert (subs of) self if not a leaf.
2387 _toPool($dbh, $prime, $newblock, $args{newtype}) unless $binfo->{type} =~ /.[enr]/;
2388 } # scope ne 'clearall'
2389
2390 } elsif ($args{scope} eq 'mergepeer') {
2391 # Try to match behaviour from (target type == container) by deleting immediate peer leaf allocations
2392 while (my ($peercidr, $peer_id, $peertype, $m_id) = $peersth->fetchrow_array) {
2393 next if $peer_id == $prime; # don't delete the block we're turning into the pool allocation
2394 # Capture block for return
2395 push @retlist, { block => $peercidr, mtype => $disp_alloctypes{$peertype} };
2396 next unless $peertype =~ /.[enr]/;
2397 # Don't need _deleteCascade(), since we'll just be deleting the freshly
2398 # added free block a little later anyway
2399 $delsth->execute($peer_id);
2400 }
2401 # Convert self if not a leaf, to match behaviour with a container type as target
2402 _toPool($dbh, $prime, $newblock, $args{newtype}) unless $binfo->{type} =~ /.[enr]/;
2403 }
2404 # Update the primary allocation info.
2405 $dbh->do("UPDATE allocations SET cidr = ?, type = ? WHERE id = ?", undef, ($newblock, $args{newtype}, $prime) );
2406 # Delete any lingering free blocks
2407 $dbh->do("DELETE FROM freeblocks WHERE parent_id = ? AND cidr <<= ?", undef, $binfo->{parent_id}, $newblock);
2408 # Fix up the rest of the pool IPs
2409 my ($code,$msg) = initPool($dbh, $newblock, $args{newtype}, $binfo->{city},
2410 ($args{newtype} =~ /.p/ ? 'all' : 'normal'), $prime);
2411
[728]2412 } elsif ($args{newtype} =~ /.[enr]/) {
2413 ## Leaf
[738]2414 # Merging to a leaf type of any kind is, pretty much be definition, scope == 'clearall'.
2415 # keepall, mergepeer, and clearpeer all imply keeping suballocations, where leaf allocations
2416 # by definition do not have suballocations.
2417 # Snag the new parent info for the return list
2418 push @retlist, {block => "$newblock", type => $disp_alloctypes{$args{newtype}}, id => $prime};
2419 while (my ($peercidr, $peer_id, $peertype, $m_id) = $peersth->fetchrow_array) {
2420 next if $peer_id == $prime;
2421 # Push existing allocations down a level before deleting,
2422 # so that when they're deleted the parent info is correct
2423 $reparentsth->execute($prime, $binfo->{master_id}, $peer_id);
2424 _deleteCascade($dbh, $peer_id, 0);
2425 # Capture block for return
2426 push @retlist, { block => $peercidr, mtype => $disp_alloctypes{$peertype} };
2427 }
2428 # Delete any subs of $prime as well
2429 my $substh = $dbh->prepare("SELECT cidr,id FROM allocations WHERE parent_id = ?");
2430 $substh->execute($prime);
2431 while (my ($scidr, $s_id) = $substh->fetchrow_array) {
2432 _deleteCascade($dbh, $s_id);
2433 }
2434 # Clean up lingering free blocks and pool IPs
2435 $dbh->do("DELETE FROM freeblocks WHERE cidr <<= ? AND (parent_id = ? OR parent_id = ?)", undef,
2436 $newblock, $binfo->{parent_id}, $prime);
2437 $dbh->do("DELETE FROM poolips WHERE parent_id = ? AND ip <<= ? ", undef,
2438 $prime, $newblock);
[728]2439
[738]2440 } # $args{newtype} if()
[728]2441
2442 $dbh->commit;
2443 };
2444 if ($@) {
2445 my $msg = $@;
2446 $errstr = $msg;
2447 $dbh->rollback;
2448 return ('FAIL',$msg);
2449 }
2450
[744]2451# Make the assumption that any change crossing /24 or /16 boundaries will not come out right. Reverse DNS
2452# updates for this operation are already complex enough without handling those edge cases.
2453# ... er, how do we detect this?
2454
2455 if ($args{newtype} =~ /.[cm]/) {
2456
2457 if ($args{scope} eq 'keepall') {
2458 # Add new rDNS for new container
2459 _rpc('addOrUpdateRevRec', cidr => $binfo->{block}, name => $binfo->{rdns}, rpcuser => $args{user});
2460
2461 } else {
2462 # Resize rDNS template for $prime
2463 _rpc('resizeTemplate', oldcidr => "$binfo->{block}", newcidr => $newblock->network.'', rpcuser => $args{user});
2464
2465 # Assemble a list of blocks to delete...
2466 my $cidrlist;
2467 foreach my $mblock (@retlist) {
2468 $cidrlist .= $mblock->{block}."," unless $mblock->{block} =~ $newblock;
2469 }
2470
2471 # ... then make slight variant batch delete calls depending on the merge scope
2472 if ($args{scope} eq 'mergepeer') {
2473 # Delete separate rDNS for other peers
2474 $cidrlist =~ s/,$//;
2475 _rpc('delRevSet', cidrlist => $cidrlist, rpcuser => $args{user}, delforward => 'y', delsubs => 'n',
2476 parpatt => $pinfo->{rdns});
2477
2478 } elsif ($args{scope} eq 'clearpeer') {
2479 # Delete all rDNS within other peers
2480 $cidrlist =~ s/,$//;
2481 _rpc('delRevSet', cidrlist => $cidrlist, rpcuser => $args{user}, delforward => 'y', delsubs => 'y',
2482 parpatt => $pinfo->{rdns})
2483
2484 } elsif ($args{scope} eq 'clearall') {
2485 # Delete all other records within the new block
2486 $cidrlist .= $binfo->{block};
2487 _rpc('delRevSet', cidrlist => $cidrlist, rpcuser => $args{user}, delforward => 'y', delsubs => 'y',
2488 parpatt => $pinfo->{rdns});
2489
2490 } # scope, second level
2491 } # scope, !keepall
2492
2493 } # type grouping for rDNS calls
2494
2495
[728]2496 return \@retlist;
2497
2498} # end mergeBlocks()
2499
2500
[93]2501## IPDB::deleteBlock()
2502# Removes an allocation from the database, including deleting IPs
2503# from poolips and recombining entries in freeblocks if possible
2504# Also handles "deleting" a static IP allocation, and removal of a master
[558]2505# Requires a database handle, the block to delete, the routing depth (if applicable),
[590]2506# the VRF ID, and a flag to indicate whether to delete associated forward DNS entries
2507# as well as the reverse entry
[93]2508sub deleteBlock {
[638]2509 my ($dbh,$id,$basetype,$delfwd,$user) = @_;
[93]2510
[638]2511 # Collect info about the block we're going to delete
2512 my $binfo = getBlockData($dbh, $id, $basetype);
2513 my $cidr = new NetAddr::IP $binfo->{block};
2514
[558]2515# For possible auto-VRF-ignoring (since public IPs shouldn't usually be present in more than one VRF)
2516# is_rfc1918 requires NetAddr::IP >= 4.059
2517# rather than doing this over and over and over.....
2518 my $tmpnum = $cidr->numeric;
2519# 192.168.0.0/16 -> 192.168.255.255 => 3232235520 -> 3232301055
2520# 172.16.0.0/12 -> 172.31.255.255 => 2886729728 -> 2887778303
2521# 10.0.0.0/8 -> 10.255.255.255 => 167772160 -> 184549375
2522 my $isprivnet = (3232235520 <= $tmpnum && $tmpnum <= 3232301055) ||
2523 (2886729728 <= $tmpnum && $tmpnum <= 2887778303) ||
2524 (167772160 <= $tmpnum && $tmpnum <= 184549375);
2525
[93]2526 my $sth;
2527
[349]2528 # Magic variables used for odd allocation cases.
2529 my $container;
2530 my $con_type;
2531
[558]2532
2533 # temporarily forced null, until a sane UI for VRF tracking can be found.
[638]2534# $vrf = '';# if !$vrf; # as with SQL, the null value is not equal to ''. *sigh*
[558]2535
[93]2536 # To contain the error message, if any.
[558]2537 my $msg = "Unknown error deallocating $binfo->{type} $cidr";
2538 my $goback; # to put the parent in so we can link back where the deallocate started
2539
[93]2540 # Enable transactions and exception-on-errors... but only for this sub
2541 local $dbh->{AutoCommit} = 0;
2542 local $dbh->{RaiseError} = 1;
2543
[558]2544 if ($binfo->{type} =~ /^.i$/) {
[638]2545 # First case. The "block" is a static IP
2546 # Note that we still need some additional code in the odd case
2547 # of a netblock-aligned contiguous group of static IPs
[93]2548
2549 eval {
[558]2550 $msg = "Unable to deallocate $disp_alloctypes{$binfo->{type}} $cidr";
[638]2551 my $pinfo = getBlockData($dbh, $binfo->{parent_id}, 'b');
[558]2552##fixme: VRF and rdepth
[638]2553 $dbh->do("UPDATE poolips SET custid = ?, available = 'y',".
2554 "city = (SELECT city FROM allocations WHERE id = ?),".
2555 "description = '', notes = '', circuitid = '', vrf = ? WHERE id = ?", undef,
2556 ($pinfo->{custid}, $binfo->{parent_id}, $pinfo->{vrf}, $id) );
[93]2557 $dbh->commit;
2558 };
2559 if ($@) {
[558]2560 $msg .= ": $@";
[93]2561 eval { $dbh->rollback; };
2562 return ('FAIL',$msg);
2563 } else {
[590]2564##fixme: RPC return code?
[638]2565 _rpc('delByCIDR', cidr => "$cidr", user => $user, delforward => $delfwd, rpcuser => $user);
[93]2566 return ('OK',"OK");
2567 }
2568
[558]2569 } elsif ($binfo->{type} eq 'mm') { # end alloctype =~ /.i/
[638]2570 # Second case. The block is a full master block
[93]2571
[558]2572##fixme: VRF limit
[93]2573 $msg = "Unable to delete master block $cidr";
2574 eval {
[638]2575 $dbh->do("DELETE FROM allocations WHERE cidr <<= ? AND master_id = ?", undef, ($cidr, $binfo->{master_id}) );
2576 $dbh->do("DELETE FROM freeblocks WHERE cidr <<= ? AND master_id = ?", undef, ($cidr, $binfo->{master_id}) );
[93]2577 $dbh->commit;
2578 };
2579 if ($@) {
[558]2580 $msg .= ": $@";
[93]2581 eval { $dbh->rollback; };
2582 return ('FAIL', $msg);
[591]2583 }
2584
2585 # Have to handle potentially split reverse zones. Assume they *are* split,
2586 # since if we added them here, they would have been added split.
2587# allow splitting reverse zones to be disabled, maybe, someday
2588#if ($splitrevzones && !$cidr->{isv6}) {
2589 my @zonelist;
2590 if (1 && !$cidr->{isv6}) {
2591 my $splitpoint = ($cidr->masklen <= 16 ? 16 : 24); # hack pthui
2592 @zonelist = $cidr->split($splitpoint);
[93]2593 } else {
[591]2594 @zonelist = ($cidr);
[93]2595 }
[591]2596 my @fails;
2597 foreach my $subzone (@zonelist) {
[638]2598 if ($rpc_url && !_rpc('delZone', zone => "$subzone", revrec => 'y', rpcuser => $user, delforward => $delfwd) ) {
[591]2599 push @fails, ("$subzone" => $errstr);
2600 }
2601 }
2602 if (@fails) {
2603 return ('WARN',"Warning(s) deleting $cidr from reverse DNS:\n".join("\n", @fails));
2604 }
2605 return ('OK','OK');
[93]2606
2607 } else { # end alloctype master block case
2608
2609 ## This is a big block; but it HAS to be done in a chunk. Any removal
2610 ## of a netblock allocation may result in a larger chunk of free
2611 ## contiguous IP space - which may in turn be combined into a single
2612 ## netblock rather than a number of smaller netblocks.
2613
[558]2614 my $retcode = 'OK';
[638]2615 my ($ptype,$pcity,$ppatt,$p_id);
[558]2616
[93]2617 eval {
2618
[558]2619##fixme: add recursive flag to allow "YES DAMMIT DELETE ALL EVARYTHING!!1!!" without
2620# explicitly deleting any suballocations of the block to be deleted.
[93]2621
[638]2622 # get parent info of the block we're deleting
2623 my $pinfo = getBlockData($dbh, $binfo->{parent_id});
2624 $ptype = $pinfo->{type};
2625 $pcity = $pinfo->{city};
2626 $ppatt = $pinfo->{rdns};
2627 $p_id = $binfo->{parent_id};
[93]2628
[558]2629 # Delete the block
[638]2630 $dbh->do("DELETE FROM allocations WHERE id = ?", undef, ($id) );
[349]2631
[558]2632 # munge the parent type a little
[638]2633 $ptype = (split //, $ptype)[1];
[93]2634
[558]2635##fixme: you can't... CAN NOT.... assign the same public IP to multiple things.
2636# 'Net don't work like that, homey. Restrict VRF-uniqueness to private IPs?
2637# -> $isprivnet flag from start of sub
[93]2638
[558]2639 # check to see if any container allocations could be the "true" parent
[638]2640 my ($tparent,$tpar_id,$trtype,$tcity);
2641 $tpar_id = 0;
[404]2642
[638]2643##fixme: this is far simpler in the strict VRF case; we "know" that any allocation
2644# contained by a container is a part of the same allocation tree when the VRF fields are equal.
[558]2645
[638]2646# logic:
2647# For each possible container of $cidr
2648# note the parent id
2649# walk the chain up the parents
2650# if we intersect $cidr's current parent, break
2651# if we've intersected $cidr's current parent
2652# set some variables to track that block
2653# break
[558]2654
[638]2655# Set up part of "is it in the middle of a pool?" check
2656 my $wuzpool = $dbh->selectrow_hashref("SELECT cidr,parent_id,type,city,custid,id FROM allocations ".
2657 "WHERE (type LIKE '_d' OR type LIKE '_p') AND cidr >> ? AND master_id = ?", { Slice => {} },
2658 ($cidr, $binfo->{master_id}) );
[558]2659
[638]2660##fixme?
2661# edge cases not handled, or handled badly:
2662# -> $cidr managed to get to be the entirety of an IP pool
[558]2663
[638]2664 if ($wuzpool && $wuzpool->{id} != $id) {
2665 # we have legacy goo to be purified
2666 # going to ignore nested pools; not possible to create them via API and no current legacy data includes any.
2667
2668 # for convenience
2669 my $poolid = $wuzpool->{id};
2670 my $pool = $wuzpool->{cidr};
2671 my $poolcity = $wuzpool->{city};
2672 my $pooltype = $wuzpool->{type};
2673 my $poolcustid = $wuzpool->{custid};
2674
2675 $retcode = 'WARNPOOL';
2676 $goback = "$poolid,$pool";
2677 # We've already deleted the block, now we have to stuff its IPs into the pool.
2678 $pooltype =~ s/[dp]$/i/; # change type to static IP
2679 my $sth2 = $dbh->prepare("INSERT INTO poolips (ip,city,type,custid,parent_id) VALUES ".
2680 "(?,'$poolcity','$pooltype','$poolcustid',$poolid)");
[655]2681
[429]2682##fixme: need to not insert net, gateway, and bcast on "real netblock" pools (DHCPish)
[638]2683 # don't insert .0
2684 $sth2->execute($cidr->addr) unless $cidr->addr =~ m|\.0$|;
[655]2685 $cidr++;
2686 my $bcast = $cidr->broadcast;
2687 while ($cidr != $bcast) {
2688 $sth2->execute($cidr->addr);
2689 $cidr++;
[638]2690 }
2691 # don't insert .255
2692 $sth2->execute($cidr->addr) unless $cidr->addr =~ m|\.255$|;
[655]2693
2694# Weirdness Happens. $cidr goes read-only somewhere (this is a thing?!?),
2695# causing ->split, ->hostenum, and related methods to explode. O_o
2696# foreach my $ip ($cidr->hostenum) {
2697# $sth2->execute($ip);
2698# }
2699
[638]2700 }
[93]2701
[638]2702## important!
2703# ... or IS IT?
2704# we may have undef'ed $wuzpool above, if the allocation tree $cidr is in doesn't intersect the pool we found
2705#if (!$wuzpool) {
2706
2707 else {
2708
[655]2709# Edge case: Block is the same size as more than one parent level. Should be rare.
2710# - mainly master + first routing. Sorting on parent_id hides the problem pretty well,
2711# but it's likely still possible to fail in particularly well-mangled databases.
2712# The ultimate fix for this may be to resurrect the "routing depth" atrocity. :/
[638]2713 # Get all possible (and probably a number of impossible) containers for $cidr
2714 $sth = $dbh->prepare("SELECT cidr,parent_id,type,city,id FROM allocations ".
2715 "WHERE (type LIKE '_m' OR type LIKE '_c') AND cidr >>= ? AND master_id = ? ".
[655]2716 "ORDER BY masklen(cidr) DESC,parent_id DESC");
[638]2717 $sth->execute($cidr, $binfo->{master_id});
2718
2719 # Quickly get certain fields (simpler than getBlockData()
2720 my $sth2 = $dbh->prepare("SELECT cidr,parent_id,type,city FROM allocations ".
2721 "WHERE (type LIKE '_m' OR type LIKE '_c') AND id = ? AND master_id = ?");
2722
2723 # For each possible container of $cidr...
2724 while (my @data = $sth->fetchrow_array) {
2725 my $i = 0;
2726 # Save some state and set a start point - parent ID of container we're checking
2727 $tparent = $data[0];
2728 my $ppid = $data[1];
2729 $trtype = $data[2];
2730 $tcity = $data[3];
2731 $tpar_id = $data[4];
2732 last if $data[4] == $binfo->{parent_id}; # Preemptively break if we're already in the right place
2733 last if $ppid == $binfo->{parent_id}; # ... or if the parent of the container is the block's parent
2734 while (1) {
2735 # Retrieve bits on that parent ID
2736 $sth2->execute($ppid, $binfo->{master_id});
2737 my @container = $sth2->fetchrow_array;
2738 $ppid = $container[1];
2739 last if $container[1] == 0; # Break if we've hit a master block
2740 last if $ppid == $binfo->{parent_id}; # Break if we've reached the block $cidr is currently in
2741 }
2742 last if $ppid == $binfo->{parent_id};
2743 }
2744
2745 # found an alternate parent; reset some parent-info bits
2746 if ($tpar_id != $binfo->{parent_id}) {
2747 $ptype = (split //, $trtype)[1];
2748 $pcity = $tcity;
2749 $retcode = 'WARNMERGE'; # may be redundant
2750 $p_id = $tpar_id;
2751 }
2752
2753 $goback = "$p_id,$tparent"; # breadcrumb, currently only used in case of live-parent-is-not-true-parent
2754
2755 # Special case - delete pool IPs
2756 if ($binfo->{type} =~ /^.[pd]$/) {
[655]2757 # We have to delete the IPs from the pool listing.
2758##fixme: rdepth? vrf?
[638]2759 $dbh->do("DELETE FROM poolips WHERE parent_id = ?", undef, ($id) );
2760 }
2761
2762 $pinfo = getBlockData($dbh, $p_id);
2763
[558]2764 # If the block wasn't legacy goo embedded in a static pool, we check the
2765 # freeblocks in the identified parent to see if we can combine any of them.
[93]2766
[559]2767 # if the block to be deleted is a container, move its freeblock(s) up a level, and reset their parenting info
2768 if ($binfo->{type} =~ /^.[mc]/) {
2769 # move the freeblocks into the parent
2770 # we don't insert a new freeblock because there could be a live reparented sub.
[638]2771 $dbh->do("UPDATE freeblocks SET parent_id = ?, routed = ?, city = ? WHERE parent_id = ?", undef,
2772 ($p_id, $ptype, $pcity, $id) );
[559]2773 } else {
2774 # ... otherwise, add the freeblock
[638]2775 $dbh->do("INSERT INTO freeblocks (cidr, city, routed, parent_id, master_id) VALUES (?,?,?,?,?)", undef,
2776 ($cidr, $pcity, $ptype, $p_id, $binfo->{master_id}) );
[559]2777 }
2778
[715]2779 # Walk the free blocks in the parent and reduce them to the minimal set of CIDR ranges necessary
2780 _compactFree($dbh, $p_id);
[428]2781
2782 } # done returning IPs to the appropriate place
[404]2783
[93]2784 # If we got here, we've succeeded. Whew!
2785 $dbh->commit;
2786 }; # end eval
2787 if ($@) {
[558]2788 $msg .= ": $@";
[93]2789 eval { $dbh->rollback; };
2790 return ('FAIL', $msg);
2791 } else {
[590]2792##fixme: RPC return code?
[638]2793 _rpc('delByCIDR', cidr => "$cidr", rpcuser => $user, delforward => $delfwd, delsubs => 'y', parpatt => $ppatt);
[558]2794 return ($retcode, $goback);
[93]2795 }
2796
2797 } # end alloctype != netblock
2798
2799} # end deleteBlock()
2800
2801
[370]2802## IPDB::getBlockData()
[557]2803# Get CIDR or IP, custid, type, city, circuit ID, description, notes, modification time,
2804# private/restricted data, for a CIDR block or pool IP
2805# Also returns SWIP status flag for CIDR blocks or pool netblock for IPs
[636]2806# Takes the block ID or IP to look up and an optional flag to indicate a pool IP lookup
2807# instead of a netblock.
[557]2808# Returns a hashref to the block data
[370]2809sub getBlockData {
2810 my $dbh = shift;
[636]2811 my $id = shift;
2812 my $type = shift || 'b'; # default to netblock for lazy callers
[370]2813
[636]2814 # netblocks are in the allocations table; pool IPs are in the poolips table.
2815 # If we try to look up a CIDR in an integer field we should just get back nothing.
2816 my ($btype) = $dbh->selectrow_array("SELECT type FROM allocations WHERE id=?", undef, ($id) );
[534]2817
[692]2818 # Note city, vrf, parent_id and master_id removed due to JOIN uncertainty for block allocations
2819 my $commonfields = q(custid, type, circuitid, description, notes, modifystamp AS lastmod,
2820 privdata, vlan, rdns);
2821
[636]2822 if ($type eq 'i') {
[692]2823 my $binfo = $dbh->selectrow_hashref(qq(
[726]2824 SELECT id, ip AS block, city, vrf, parent_id, master_id, $commonfields
[692]2825 FROM poolips WHERE id = ?
2826 ), undef, ($id) );
[557]2827 return $binfo;
2828 } else {
[692]2829 my $binfo = $dbh->selectrow_hashref(qq(
[726]2830 SELECT a.id, a.cidr AS block, a.city, a.vrf, a.parent_id, a.master_id, swip, $commonfields,
[692]2831 f.cidr AS reserve, f.id as reserve_id
2832 FROM allocations a LEFT JOIN freeblocks f ON a.id=f.reserve_for
2833 WHERE a.id = ?
2834 ), undef, ($id) );
[557]2835 return $binfo;
[534]2836 }
[370]2837} # end getBlockData()
2838
2839
[585]2840## IPDB::getBlockRDNS()
2841# Gets reverse DNS pattern for a block or IP. Note that this will also
2842# retrieve any default pattern following the parent chain up, and check via
2843# RPC (if available) to see what the narrowest pattern for the requested block is
2844# Returns the current pattern for the block or IP.
2845sub getBlockRDNS {
2846 my $dbh = shift;
2847 my %args = @_;
2848
[637]2849 $args{type} = 'b' if !$args{type};
[675]2850 my $cached = 1;
[585]2851
[637]2852 # snag entry from database
2853 my ($rdns,$rfrom,$pid);
2854 if ($args{type} =~ /.i/) {
2855 ($rdns, $rfrom, $pid) = $dbh->selectrow_array("SELECT rdns,ip,parent_id FROM poolips WHERE id = ?",
2856 undef, ($args{id}) );
2857 } else {
2858 ($rdns, $rfrom, $pid) = $dbh->selectrow_array("SELECT rdns,cidr,parent_id FROM allocations WHERE id = ?",
2859 undef, ($args{id}) );
2860 }
[585]2861
[637]2862 # Can't see a way this could end up empty, for any case I care about. If the caller
2863 # doesn't know an allocation ID to request, then they don't know anything else anyway.
2864 my $selfblock = $rfrom;
[585]2865
[637]2866 my $type;
2867 while (!$rdns && $pid) {
2868 ($rdns, $rfrom, $pid, $type) = $dbh->selectrow_array(
2869 "SELECT rdns,cidr,parent_id,type FROM allocations WHERE id = ?",
2870 undef, ($pid) );
2871 last if $type eq 'mm'; # break loops in unfortunate legacy data
2872 }
2873
2874 # use the actual allocation to check against the DNS utility; we don't want
2875 # to always go chasing up the chain to the master... which may (usually won't)
2876 # be present directly in DNS anyway
2877 my $cidr = new NetAddr::IP $selfblock;
2878
[585]2879 if ($rpc_url) {
2880 # Use the first /16 or /24, rather than dithering over which sub-/14 /16
2881 # or sub-/19 /24 to retrieve - it's the least-wrong way to do things.
2882
[586]2883 my ($rpcblock) = ($cidr->masklen <= 24 ? $cidr->split( ($cidr->masklen <= 16 ? 16 : 24) ) : $cidr);
[585]2884 my %rpcargs = (
2885 rpcuser => $args{user},
2886 group => $revgroup, # not sure how this could sanely be exposed, tbh...
2887 cidr => "$rpcblock",
2888 );
2889
[637]2890 my $remote_rdns = _rpc('getRevPattern', %rpcargs);
2891 $rdns = $remote_rdns if $remote_rdns;
[675]2892 $cached = 0;
[585]2893 }
2894
2895 # hmm. do we care about where it actually came from?
[675]2896 return $rdns, $cached;
[585]2897} # end getBlockRDNS()
2898
2899
[675]2900## IPDB::getRDNSbyIP()
2901# Get individual reverse entries for the IP or CIDR IP range passed. Sort of looking the
2902# opposite direction down the netblock tree compared to getBlockRDNS() above.
2903sub getRDNSbyIP {
2904 my $dbh = shift;
2905 my %args = @_; # We want to accept a variety of call types
2906
2907 # key arguments: allocation ID, type
2908 unless ($args{id} || $args{type}) {
2909 $errstr = 'Missing allocation ID or type';
2910 return;
2911 }
2912
2913 my @ret = ();
2914 # special case: single IP. Check if it's an allocation or in a pool, then do the RPC call for fresh data.
2915 if ($args{type} =~ /^.i$/) {
2916 my ($ip, $localrev) = $dbh->selectrow_array("SELECT ip, rdns FROM poolips WHERE id = ?", undef, ($args{id}) );
2917 push @ret, { 'r_ip' => $ip, 'iphost' => $localrev };
2918 } else {
2919 if ($rpc_url) {
2920 my %rpcargs = (
2921 rpcuser => $args{user},
2922 group => $revgroup, # not sure how this could sanely be exposed, tbh...
2923 cidr => $args{range},
2924 );
2925
2926 my $remote_rdns = _rpc('getRevSet', %rpcargs);
2927 return $remote_rdns;
2928# $rdns = $remote_rdns if $remote_rdns;
2929# $cached = 0;
2930 }
2931 }
2932 return \@ret;
2933} # end getRDNSbyIP()
2934
2935
[519]2936## IPDB::getNodeList()
2937# Gets a list of node ID+name pairs as an arrayref to a list of hashrefs
2938sub getNodeList {
2939 my $dbh = shift;
2940
2941 my $ret = $dbh->selectall_arrayref("SELECT node_id, node_name FROM nodes ORDER BY node_type,node_id",
2942 { Slice => {} });
2943 return $ret;
2944} # end getNodeList()
2945
2946
[530]2947## IPDB::getNodeName()
2948# Get node name from the ID
2949sub getNodeName {
2950 my $dbh = shift;
2951 my $nid = shift;
2952
2953 my ($nname) = $dbh->selectrow_array("SELECT node_name FROM nodes WHERE node_id = ?", undef, ($nid) );
2954 return $nname;
2955} # end getNodeName()
2956
2957
2958## IPDB::getNodeInfo()
2959# Get node name and ID associated with a block
2960sub getNodeInfo {
2961 my $dbh = shift;
2962 my $block = shift;
2963
2964 my ($nid, $nname) = $dbh->selectrow_array("SELECT nodes.node_id,node_name FROM nodes INNER JOIN noderef".
2965 " ON nodes.node_id=noderef.node_id WHERE noderef.block = ?", undef, ($block) );
2966 return ($nid, $nname);
2967} # end getNodeInfo()
2968
2969
[77]2970## IPDB::mailNotify()
[66]2971# Sends notification mail to recipients regarding an IPDB operation
[416]2972sub mailNotify {
2973 my $dbh = shift;
2974 my ($action,$subj,$message) = @_;
[66]2975
[462]2976 return if $smtphost eq 'smtp.example.com'; # do nothing if still using default SMTP host.
2977
[422]2978##fixme: need to redesign the breakdown/processing for $action for proper handling of all cases
2979
[416]2980# split action into parts for fiddlement. nb: there are almost certainly better ways to do this.
[422]2981 my @actionbits = split //, $action;
[416]2982
2983 # want to notify anyone who has specifically requested notify on *this* type ($action as passed),
2984 # on "all static IP types" or "all pool types" (and other last-char-in-type groupings), on eg "all DSL types",
2985 # and "all events with this action"
2986 my @actionsets = ($action);
2987##fixme: ick, eww. really gotta find a better way to handle this...
2988 push @actionsets, ($actionbits[0].'.'.$actionbits[2],
2989 $actionbits[0].$actionbits[1].'.', $actionbits[0].'a') if $action =~ /^.{3}$/;
2990
2991 my $mailer = Net::SMTP->new($smtphost, Hello => "ipdb.$domain");
2992
2993 # get recip list from db
2994 my $sth = $dbh->prepare("SELECT reciplist FROM notify WHERE action=?");
2995
[443]2996 my %reciplist;
[416]2997 foreach (@actionsets) {
[426]2998 $sth->execute($_);
[416]2999##fixme - need to handle db errors
3000 my ($recipsub) = $sth->fetchrow_array;
3001 next if !$recipsub;
3002 foreach (split(/,/, $recipsub)) {
[443]3003 $reciplist{$_}++;
[416]3004 }
3005 }
3006
[443]3007 return if !%reciplist;
[420]3008
[443]3009 foreach my $recip (keys %reciplist) {
[681]3010 $mailer->mail($smtpsender);
[416]3011 $mailer->to($recip);
[681]3012 $mailer->data("From: \"$org_name IP Database\" <$smtpsender>\n",
[135]3013 "To: $recip\n",
[69]3014 "Date: ".strftime("%a, %d %b %Y %H:%M:%S %z",localtime)."\n",
3015 "Subject: {IPDB} $subj\n",
3016 "X-Mailer: IPDB Notify v".sprintf("%.1d",$IPDB::VERSION)."\n",
[417]3017 "Organization: $org_name\n",
[69]3018 "\n$message\n");
[416]3019 }
[66]3020 $mailer->quit;
3021}
3022
[4]3023# Indicates module loaded OK. Required by Perl.
30241;
Note: See TracBrowser for help on using the repository browser.