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

Last change on this file since 866 was 866, checked in by Kris Deugau, 8 years ago

/trunk

main.cgi:

  • Don't spew warnings and break the UI when retrieval of the zone list for a netblock fails

IPDB.pm, ipdb-rpc.cgi:

  • Fill out rpc_listPool stub in ipdb-rpc.cgi. The first likely consumer may not want the full UI dataset (with description and "deleteme" flag) so the core sub has been extended with an optional flag that defaults to on.

ipdb-rpc.cgi:

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