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

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

/trunk

Second page in merge sequence; show main allocations and free blocks
that would be affected by the merge, along with reminders as
appropriate about data that may be lost with the combination of merge
scope and target type selected for the new allocation.
See #8.

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