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

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

/trunk

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

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