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

Last change on this file since 739 was 739, checked in by Kris Deugau, 9 years ago

/dev

Fix a lurking bug before I forget; splitting a largish container with
mostly free space would result in mangled free blocks. Any block larger
than the split size needs to be split as well and distributed between the
appropriate containers. See #7.

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