summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--FS/FS/IP_Mixin.pm11
-rw-r--r--FS/FS/Mason.pm1
-rw-r--r--FS/FS/Schema.pm12
-rw-r--r--FS/FS/addr_range.pm264
-rw-r--r--FS/MANIFEST2
-rw-r--r--FS/t/addr_range.t5
-rw-r--r--httemplate/browse/addr_range.html66
-rw-r--r--httemplate/edit/addr_range.html27
-rw-r--r--httemplate/edit/elements/edit.html21
-rw-r--r--httemplate/edit/process/addr_range.html22
-rw-r--r--httemplate/elements/menu.html4
-rw-r--r--httemplate/misc/delete-addr_range.html21
12 files changed, 455 insertions, 1 deletions
diff --git a/FS/FS/IP_Mixin.pm b/FS/FS/IP_Mixin.pm
index fdeb51d..b3c1052 100644
--- a/FS/FS/IP_Mixin.pm
+++ b/FS/FS/IP_Mixin.pm
@@ -200,12 +200,21 @@ sub check_ip_addr {
return '' if $addr eq '';
my $na = $self->NetAddr
or return "Can't parse address '$addr'";
+ # if there's a chosen address block, check that the address is in it
if ( my $block = $self->addr_block ) {
if ( !$block->NetAddr->contains($na) ) {
return "Address $addr not in block ".$block->cidr;
}
}
- # this returns '' if the address is in use by $self.
+ # if the address is in any designated ranges, check that they don't
+ # disallow use
+ foreach my $range (FS::addr_range->any_contains($addr)) {
+ if ( !$range->allow_use ) {
+ return "Address $addr is in ".$range->desc." range ".$range->as_string;
+ }
+ }
+ # check that nobody else is sitting on the address
+ # (this returns '' if the address is in use by $self)
if ( my $dup = $self->is_used($self->ip_addr) ) {
return "Address $addr in use by $dup";
}
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index fc25a86..fefa1bc 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -358,6 +358,7 @@ if ( -e $addl_handler_use_file ) {
use FS::cable_provider;
use FS::cust_credit_void;
use FS::discount_class;
+ use FS::addr_range;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 6403782..647e2b1 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -4278,6 +4278,18 @@ sub tables_hashref {
],
},
+ 'addr_range' => {
+ 'columns' => [
+ 'rangenum', 'serial', '', '', '', '',
+ 'start', 'varchar', '', 15, '', '',
+ 'length', 'int', '', '', '', '',
+ 'status', 'varchar', 'NULL', 32, '', '',
+ ],
+ 'primary_key' => 'rangenum',
+ 'unique' => [],
+ 'index' => [],
+ },
+
'svc_broadband' => {
'columns' => [
'svcnum', 'int', '', '', '', '',
diff --git a/FS/FS/addr_range.pm b/FS/FS/addr_range.pm
new file mode 100644
index 0000000..5faa443
--- /dev/null
+++ b/FS/FS/addr_range.pm
@@ -0,0 +1,264 @@
+package FS::addr_range;
+
+use strict;
+use base qw( FS::Record );
+use vars qw( %status_desc
+ %status_allow_auto
+ %status_allow_use
+ );
+use FS::Record qw( qsearch qsearchs );
+use NetAddr::IP;
+
+# metadata about status strings:
+# how to describe them
+%status_desc = (
+ '' => '',
+ 'unavailable' => 'unavailable',
+);
+
+# whether addresses in this range are available for use
+%status_allow_use = (
+ '' => 1,
+ 'unavailable' => 0,
+);
+
+=head1 NAME
+
+FS::addr_range - Object methods for addr_range records
+
+=head1 SYNOPSIS
+
+ use FS::addr_range;
+
+ $record = new FS::addr_range \%hash;
+ $record = new FS::addr_range { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::addr_range object represents a contiguous range of IP
+addresses assigned to a certain purpose. Unlike L<FS::addr_block>,
+this isn't a routing block; the range doesn't have to be aligned on
+a subnet boundary, and doesn't have a gateway or broadcast address.
+It's just a range.
+
+=over 4
+
+=item rangenum - primary key
+
+=item start - starting address of the range, as a dotted quad
+
+=item length - number of addresses in the range, including start
+
+=item status - what to do with the addresses in this range; currently can
+only be "unavailable", which makes the addresses unavailable for assignment
+to any kind of service.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new range. To add the example to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+sub table { 'addr_range'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('rangenum')
+ || $self->ut_ip('start')
+ || $self->ut_number('length')
+ || $self->ut_textn('status')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=item end [ IPADDR ]
+
+Get/set the end IP address in the range. This isn't actually part of the
+record but it's convenient.
+
+=cut
+
+sub end {
+ my $self = shift;
+ # if there's no start address, just return nothing
+ my $start = NetAddr::IP->new($self->start, 0) or return '';
+
+ my $new = shift;
+ if ( $new ) {
+ my $end = NetAddr::IP->new($new, 0)
+ or die "bad end address $new";
+ if ( $end < $start ) {
+ $self->set('start', $end);
+ ($end, $start) = ($start, $end);
+ }
+ $self->set('length', $end - $start + 1);
+ return $end->addr;
+ }
+ my $end = $start + $self->get('length') - 1;
+ $end->addr;
+}
+
+=item contains IPADDR
+
+Checks whether IPADDR (a dotted-quad IPv4 address) is within the range.
+
+=cut
+
+sub contains {
+ my $self = shift;
+ my $addr = shift;
+ $addr = NetAddr::IP->new($addr, 0)
+ unless ref($addr) and UNIVERSAL::isa($addr, 'NetAddr::IP');
+ return 0 unless $addr;
+
+ my $start = NetAddr::IP->new($self->start, 0);
+
+ return ($addr >= $start and $addr - $start < $self->length) ? 1 : 0;
+}
+
+=item as_string
+
+Returns a readable string showing the address range.
+
+=cut
+
+sub as_string {
+ my $self = shift;
+ my $start = NetAddr::IP->new($self->start, 0);
+ my $end = $start + $self->length;
+
+ if ( $self->length == 1 ) {
+ # then just the address
+ return $self->start;
+ } else { # we have to get tricksy
+ my @end_octets = split('\.', $end->addr);
+ $start = ($start->numeric)[0] + 0;
+ $end = ($end->numeric)[0] + 0;
+ # which octets are different between start and end?
+ my $delta = $end ^ $start;
+ foreach (0xffffff, 0xffff, 0xff) {
+ if ( $delta <= $_ ) {
+ # then they are identical in the first 8/16/24 bits
+ shift @end_octets;
+ }
+ }
+ return $self->start . '-' . join('.', @end_octets);
+ }
+}
+
+=item desc
+
+Returns a semi-friendly description of the block status.
+
+=item allow_use
+
+Returns true if addresses in this range can be used by services, etc.
+
+=cut
+
+sub desc {
+ my $self = shift;
+ $status_desc{ $self->status };
+}
+
+sub allow_auto {
+ my $self = shift;
+ $status_allow_auto{ $self->status };
+}
+
+sub allow_use {
+ my $self = shift;
+ $status_allow_use{ $self->status };
+}
+
+=back
+
+=head1 CLASS METHODS
+
+=sub any_contains IPADDR
+
+Returns all address ranges that contain IPADDR.
+
+=cut
+
+sub any_contains {
+ my $self = shift;
+ my $addr = shift;
+ return grep { $_->contains($addr) } qsearch('addr_range', {});
+}
+
+=head1 DEVELOPER NOTE
+
+L<NetAddr::IP> objects have netmasks. When using them to represent
+range endpoints, be sure to set the netmask to I<zero> so that math on
+the address doesn't stop at the subnet boundary. (The default is /32,
+which doesn't work very well. Address ranges ignore subnet boundaries.
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_IP_Mixin>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
index e635831..0b36e24 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -729,3 +729,5 @@ FS/cust_credit_void.pm
t/cust_credit_void.t
FS/discount_class.pm
t/discount_class.t
+FS/addr_range.pm
+t/addr_range.t
diff --git a/FS/t/addr_range.t b/FS/t/addr_range.t
new file mode 100644
index 0000000..6747d67
--- /dev/null
+++ b/FS/t/addr_range.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::addr_range;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/browse/addr_range.html b/httemplate/browse/addr_range.html
new file mode 100644
index 0000000..d657f32
--- /dev/null
+++ b/httemplate/browse/addr_range.html
@@ -0,0 +1,66 @@
+<& elements/browse.html,
+ 'title' => 'Address Ranges',
+ 'name_singular' => 'address range',
+ 'html_init' => $html_init,
+ 'html_foot' => $html_foot,
+ 'query' => { 'table' => 'addr_range',
+ 'order_by' => $order_by,
+ },
+ 'count_query' => "SELECT count(*) from addr_range",
+ 'header' => [ 'From',
+ '', # the dash
+ 'To',
+ 'Status',
+ # would be nice to show whether any addresses in the
+ # range are assigned, but that's ugly
+ ],
+ 'fields' => [ 'start',
+ sub { '&ndash;' },
+ 'end',
+ 'desc',
+ ],
+ 'links' => [
+ [ '#' ],
+ '',
+ [ '#' ],
+ ],
+ 'link_onclicks' => [ $edit_link,
+ '',
+ $edit_link,
+ '',
+ ],
+&>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Broadband global configuration');
+# addr_ranges are ALWAYS global, else there will be chaos
+
+my $order_by = "ORDER BY inet(start)"; # Pg-ism
+# though we could also make the field itself inet-type...
+# this would simplify a lot of things.
+
+my $html_init = include('/elements/error.html');
+
+my $edit_link = sub {
+ my $addr_range = shift;
+ include('/elements/popup_link_onclick.html',
+ action => $p.'edit/addr_range.html?rangenum='.
+ $addr_range->rangenum,
+ actionlabel => 'Edit address range',
+ width => 650,
+ height => 420,
+ );
+};
+
+my $add_link = include('/elements/popup_link_onclick.html',
+ action => $p.'edit/addr_range.html',
+ actionlabel => 'Edit address range',
+ width => 650,
+ height => 420,
+);
+
+my $html_foot = qq!<A HREF="#" onclick="$add_link">
+<I>Add a new address range</I></A>!;
+
+</%init>
diff --git a/httemplate/edit/addr_range.html b/httemplate/edit/addr_range.html
new file mode 100644
index 0000000..68efa5d
--- /dev/null
+++ b/httemplate/edit/addr_range.html
@@ -0,0 +1,27 @@
+<& elements/edit.html,
+ 'name_singular' => 'address range',
+ 'popup' => 1,
+ 'table' => 'addr_range',
+ 'labels' => { 'start' => 'From',
+ 'end' => 'To',
+ 'status' => 'Status',
+ 'rangenum'=> 'Range',
+ },
+ 'fields' => [ 'start',
+ 'end',
+ { field => 'status',
+ type => 'select',
+ labels => \%FS::addr_range::status_desc,
+ options => [ sort { $a cmp $b }
+ keys(%FS::addr_range::status_desc) ],
+ disable_empty => 1,
+ },
+ ],
+ 'delete_url' => $p.'misc/delete-addr_range.html',
+&>
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Broadband global configuration');
+
+</%init>
diff --git a/httemplate/edit/elements/edit.html b/httemplate/edit/elements/edit.html
index 9e27f2a..16d0817 100644
--- a/httemplate/edit/elements/edit.html
+++ b/httemplate/edit/elements/edit.html
@@ -108,6 +108,9 @@ Example:
# overrides default popurl(1)."process/$table.html"
'post_url' => popurl(1).'process/something',
+ # optional link to delete this object; primary key will be appended
+ 'delete_url' => $p.'misc/delete-something.html?',
+
#we're in a popup (no title/menu/searchboxes)
'popup' => 1,
@@ -211,6 +214,7 @@ Example:
% );
% }
+
<% include('/elements/header'. ( $opt{popup} ? '-popup' : '' ). '.html',
$title,
include( '/elements/menubar.html', @menubar ),
@@ -773,6 +777,23 @@ Example:
: "Add ". ($opt{'name'} || $opt{'name_singular'})
%>"
>
+% if ( $opt{'delete_url'} and $object->get($pkey) ) {
+% my $delete_msg = 'Delete this '.
+% ($opt{'name_singular'} || $opt{'name'});
+% my $delete_url = $opt{'delete_url'};
+% $delete_url .= '?' unless $delete_url =~ /\?/;
+% $delete_url .= $object->get($pkey);
+ <SCRIPT TYPE="text/javascript">
+ function confirm_delete() {
+ if(confirm(<% $delete_msg . '?' |js_string %>)) {
+ window.location.href = <% $delete_url |js_string %>;
+ }
+ }
+ </SCRIPT>
+ <INPUT TYPE = "button"
+ VALUE = "<% $delete_msg |h %>"
+ onclick = "confirm_delete()">
+% }
% }
</FORM>
diff --git a/httemplate/edit/process/addr_range.html b/httemplate/edit/process/addr_range.html
new file mode 100644
index 0000000..6b05d23
--- /dev/null
+++ b/httemplate/edit/process/addr_range.html
@@ -0,0 +1,22 @@
+<& elements/process.html,
+ 'table' => 'addr_range',
+ 'popup_reload' => 'Address range changed',
+ 'precheck_callback' => sub {
+ my ($cgi) = @_;
+ my $start = NetAddr::IP->new($cgi->param('start'), 0)
+ or return 'Illegal or empty (IP address) start: '.$cgi->param('start');
+ if ( length($cgi->param('end')) ) {
+ my $end = NetAddr::IP->new($cgi->param('end'), 0)
+ or return 'Illegal or empty (IP address) end: '.$cgi->param('end');
+ if ( $end < $start ) {
+ ($start, $end) = ($end, $start);
+ $cgi->param('end', $end->addr);
+ $cgi->param('start', $start->addr);
+ }
+ $cgi->param('length', $end - $start + 1);
+ } else {
+ $cgi->param('length', 1);
+ }
+ '';
+ },
+&>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index b4fff22..2ae216c 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -508,6 +508,10 @@ tie my %config_broadband, 'Tie::IxHash',
'Routers' => [ $fsurl.'browse/router.cgi', 'Broadband access routers' ],
'Address blocks' => [ $fsurl.'browse/addr_block.cgi', 'Manage address blocks and block assignments to broadband routers' ],
;
+if ( $curuser->access_right('Broadband global configuration') ) {
+ $config_broadband{'Address ranges'} =
+ [ $fsurl.'browse/addr_range.html', 'Designate special address ranges' ];
+}
tie my %config_phone, 'Tie::IxHash',
'View/Edit phone device types' => [ $fsurl.'browse/part_device.html', 'Phone device types' ],
diff --git a/httemplate/misc/delete-addr_range.html b/httemplate/misc/delete-addr_range.html
new file mode 100644
index 0000000..c6310e9
--- /dev/null
+++ b/httemplate/misc/delete-addr_range.html
@@ -0,0 +1,21 @@
+% if ( $error ) {
+<& /elements/errorpage-popup.html, $error &>
+% } else {
+<& /elements/header-popup.html, "Address range deleted" &>
+ <SCRIPT TYPE="text/javascript">
+ window.top.location.reload();
+ </SCRIPT>
+</BODY>
+</HTML>
+% }
+<%init>
+
+die "access denied"
+ unless $FS::CurrentUser::CurrentUser->access_right('Broadband global configuration');
+
+my ($rangenum) = $cgi->keywords;
+$rangenum =~ /^\d+$/ or die "bad rangenum '$rangenum'";
+my $addr_range = FS::addr_range->by_key($rangenum);
+die "unknown rangenum $rangenum" unless $addr_range;
+my $error = $addr_range->delete;
+</%init>