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

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

/trunk

Introduce a more "relaxed" layout for listing VRFs and master netblocks -
interleave the lists of master blocks for each VRF with the VRF heading,
instead of formally separating them onto different pages.

Refine and adapt showvrfs.tmpl to show the master blocks instead of
reinventing another wheel.

See #54.

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