summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorkhoff <khoff>2003-02-05 23:17:17 +0000
committerkhoff <khoff>2003-02-05 23:17:17 +0000
commit0354f39ed0e74fd2eae1d9da13906625b4f56591 (patch)
tree730b2ac862f4c47c661d91a91ccb8167a4a0ee8f /FS
parentc89aa83639038cc1946fec07a2dda252f64e5144 (diff)
svc_broadband rewrite
Diffstat (limited to 'FS')
-rwxr-xr-xFS/FS/addr_block.pm322
-rwxr-xr-xFS/FS/part_router_field.pm134
-rwxr-xr-xFS/FS/part_sb_field.pm267
-rwxr-xr-xFS/FS/part_svc_router.pm32
-rwxr-xr-xFS/FS/router.pm156
-rwxr-xr-xFS/FS/router_field.pm146
-rwxr-xr-xFS/FS/sb_field.pm148
-rwxr-xr-xFS/FS/svc_broadband.pm225
-rwxr-xr-xFS/bin/freeside-setup89
9 files changed, 1370 insertions, 149 deletions
diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm
new file mode 100755
index 000000000..b671723aa
--- /dev/null
+++ b/FS/FS/addr_block.pm
@@ -0,0 +1,322 @@
+package FS::addr_block;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs qsearch dbh );
+use FS::router;
+use FS::svc_broadband;
+use NetAddr::IP;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::addr_block - Object methods for addr_block records
+
+=head1 SYNOPSIS
+
+ use FS::addr_block;
+
+ $record = new FS::addr_block \%hash;
+ $record = new FS::addr_block { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::addr_block record describes an address block assigned for broadband
+access. FS::addr_block inherits from FS::Record. The following fields are
+currently supported:
+
+=over 4
+
+=item blocknum - primary key, used in FS::svc_broadband to associate
+services to the block.
+
+=item routernum - the router (see FS::router) to which this
+block is assigned.
+
+=item ip_gateway - the gateway address used by customers within this block.
+
+=item ip_netmask - the netmask of the block, expressed as an integer.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record. To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'addr_block'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database. If there is an error, returns the
+error, otherwise returns false.
+
+sub delete {
+ my $self = shift;
+ return 'Block must be deallocated before deletion'
+ if $self->router;
+
+ $self->SUPER::delete;
+}
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is an error,
+returns the error, otherwise returns false. Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_number('routernum')
+ || $self->ut_ip('ip_gateway')
+ || $self->ut_number('ip_netmask')
+ ;
+ return $error if $error;
+
+
+ # A routernum of 0 indicates an unassigned block and is allowed
+ return "Unknown routernum"
+ if ($self->routernum and not $self->router);
+
+ my $self_addr = $self->NetAddr;
+ return "Cannot parse address: ". $self->ip_gateway . '/' . $self->ip_netmask
+ unless $self_addr;
+
+ if (not $self->blocknum) {
+ my @block = grep {
+ my $block_addr = $_->NetAddr;
+ if($block_addr->contains($self_addr)
+ or $self_addr->contains($block_addr)) { $_; };
+ } qsearch( 'addr_block', {});
+ foreach(@block) {
+ return "Block intersects existing block ".$_->ip_gateway."/".$_->ip_netmask;
+ }
+ }
+
+ '';
+}
+
+
+=item router
+
+Returns the FS::router object corresponding to this object. If the
+block is unassigned, returns undef.
+
+=cut
+
+sub router {
+ my $self = shift;
+ return qsearchs('router', { routernum => $self->routernum });
+}
+
+=item svc_broadband
+
+Returns a list of FS::svc_broadband objects associated
+with this object.
+
+=cut
+
+sub svc_broadband {
+ my $self = shift;
+ return qsearch('svc_broadband', { blocknum => $self->blocknum });
+}
+
+=item NetAddr
+
+Returns a NetAddr::IP object for this block's address and netmask.
+
+=cut
+
+sub NetAddr {
+ my $self = shift;
+
+ return new NetAddr::IP ($self->ip_gateway, $self->ip_netmask);
+}
+
+=item next_free_addr
+
+Returns a NetAddr::IP object corresponding to the first unassigned address
+in the block (other than the network, broadcast, or gateway address). If
+there are no free addresses, returns false.
+
+=cut
+
+sub next_free_addr {
+ my $self = shift;
+
+ my @used = map { $_->NetAddr->addr }
+ ($self,
+ qsearch('svc_broadband', { blocknum => $self->blocknum }) );
+
+ my @free = $self->NetAddr->hostenum;
+ while (my $ip = shift @free) {
+ if (not grep {$_ eq $ip->addr;} @used) { return $ip; };
+ }
+
+ '';
+
+}
+
+=item allocate
+
+Allocates this address block to a router. Takes an FS::router object
+as an argument.
+
+At present it's not possible to reallocate a block to a different router
+except by deallocating it first, which requires that none of its addresses
+be assigned. This is probably as it should be.
+
+=cut
+
+sub allocate {
+ my ($self, $router) = @_;
+
+ return 'Block is already allocated'
+ if($self->router);
+
+ return 'Block must be allocated to a router'
+ unless(ref $router eq 'FS::router');
+
+ my @svc = $self->svc_broadband;
+ if (@svc) {
+ return 'Block has assigned addresses: '. join ', ', map {$_->ip_addr} @svc;
+ }
+
+ my $new = new FS::addr_block {$self->hash};
+ $new->routernum($router->routernum);
+ return $new->replace($self);
+
+}
+
+=item deallocate
+
+Deallocates the block (i.e. sets the routernum to 0). If any addresses in the
+block are assigned to services, it fails.
+
+=cut
+
+sub deallocate {
+ my $self = shift;
+
+ my @svc = $self->svc_broadband;
+ if (@svc) {
+ return 'Block has assigned addresses: '. join ', ', map {$_->ip_addr} @svc;
+ }
+
+ my $new = new FS::addr_block {$self->hash};
+ $new->routernum(0);
+ return $new->replace($self);
+}
+
+=item split_block
+
+Splits this address block into two equal blocks, occupying the same space as
+the original block. The first of the two will also have the same blocknum.
+The gateway address of each block will be set to the first usable address, i.e.
+(network address)+1. Since this method is designed for use on unallocated
+blocks, this is probably the correct behavior.
+
+(At present, splitting allocated blocks is disallowed. Anyone who wants to
+implement this is reminded that each split costs three addresses, and any
+customers who were using these addresses will have to be moved; depending on
+how full the block was before being split, they might have to be moved to a
+different block. Anyone who I<still> wants to implement it is asked to tie it
+to a configuration switch so that site admins can disallow it.)
+
+=cut
+
+sub split_block {
+
+ # We should consider using Attribute::Handlers/Aspect/Hook::LexWrap/
+ # something to atomicize functions, so that we can say
+ #
+ # sub split_block : atomic {
+ #
+ # instead of repeating all this AutoCommit verbage in every
+ # sub that does more than one database operation.
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $self = shift;
+ my $error;
+
+ if ($self->router) {
+ return 'Block is already allocated';
+ }
+
+ #TODO: Smallest allowed block should be a config option.
+ if ($self->NetAddr->masklen() ge 30) {
+ return 'Cannot split blocks with a mask length >= 30';
+ }
+
+ my (@new, @ip);
+ $ip[0] = $self->NetAddr;
+ @ip = map {$_->first()} $ip[0]->split($self->ip_netmask + 1);
+
+ foreach (0,1) {
+ $new[$_] = new FS::addr_block {$self->hash};
+ $new[$_]->ip_gateway($ip[$_]->addr);
+ $new[$_]->ip_netmask($ip[$_]->masklen);
+ }
+
+ $new[1]->blocknum('');
+
+ $error = $new[0]->replace($self);
+ if ($error) {
+ $dbh->rollback;
+ return $error;
+ }
+
+ $error = $new[1]->insert;
+ if ($error) {
+ $dbh->rollback;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ return '';
+}
+
+=item merge
+
+To be implemented.
+
+=back
+
+=head1 BUGS
+
+Minimum block size should be a config option. It's hardcoded at /30 right
+now because that's the smallest block that makes any sense at all.
+
+1;
+
diff --git a/FS/FS/part_router_field.pm b/FS/FS/part_router_field.pm
new file mode 100755
index 000000000..73ca50fb6
--- /dev/null
+++ b/FS/FS/part_router_field.pm
@@ -0,0 +1,134 @@
+package FS::part_router_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::router_field;
+use FS::router;
+
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_router_field - Object methods for part_router_field records
+
+=head1 SYNOPSIS
+
+ use FS::part_router_field;
+
+ $record = new FS::part_router_field \%hash;
+ $record = new FS::part_router_field { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+A part_router_field represents an xfield definition for routers. For more
+information on xfields, see L<FS::part_sb_field>.
+
+The following fields are supported:
+
+=over 4
+
+=item routerfieldpart - primary key (assigned automatically)
+
+=item name - name of field
+
+=item length
+
+=item check_block
+
+=item list_source
+
+(See L<FS::part_sb_field> for details on these fields.)
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record. To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'part_router_field'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database. If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is an error,
+returns the error, otherwise returns false. Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+ my $error = '';
+
+ $self->name =~ /^([a-z0-9_\-\.]{1,15})$/i
+ or return "Invalid field name for part_router_field";
+
+ ''; #no error
+}
+
+=item list_values
+
+Equivalent to "eval($part_router_field->list_source)".
+
+=cut
+
+sub list_values {
+ my $self = shift;
+ return () unless $self->list_source;
+ my @opts = eval($self->list_source);
+ if($@) {
+ warn $@;
+ return ();
+ } else {
+ return @opts;
+ }
+}
+
+=back
+
+=head1 VERSION
+
+$Id:
+
+=head1 BUGS
+
+Needless duplication of much of FS::part_sb_field, with the result that most of
+the warnings about it apply here also.
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::router, FS::router_field, schema.html
+from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_sb_field.pm b/FS/FS/part_sb_field.pm
new file mode 100755
index 000000000..8dca946b5
--- /dev/null
+++ b/FS/FS/part_sb_field.pm
@@ -0,0 +1,267 @@
+package FS::part_sb_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_sb_field - Object methods for part_sb_field records
+
+=head1 SYNOPSIS
+
+ use FS::part_sb_field;
+
+ $record = new FS::part_sb_field \%hash;
+ $record = new FS::part_sb_field { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_sb_field object represents an extended field (xfield) definition
+for svc_broadband's sb_field mechanism (see L<FS::svc_broadband>).
+FS::part_sb_field inherits from FS::Record. The following fields are
+currently supported:
+
+=over 2
+
+=item sbfieldpart - primary key (assigned automatically)
+
+=item name - name of the field
+
+=item svcpart - service type for which this field is available (see L<FS::part_svc>)
+
+=item length - length of the contents of the field (see note #1)
+
+=item check_block - validation routine (see note #2)
+
+=item list_source - enumeration routine (see note #3)
+
+=back
+
+=head1 BACKGROUND
+
+Broadband services, unlike dialup services, are provided over a wide
+variety of physical media (DSL, wireless, cable modems, digital circuits)
+and network architectures (Ethernet, PPP, ATM). For many of these access
+mechanisms, adding a new customer requires knowledge of some properties
+of the physical connection (circuit number, the type of CPE in use, etc.).
+It is unreasonable to expect ISPs to alter Freeside's schema (and the
+associated library and UI code) to make each of these parameters a field in
+svc_broadband.
+
+Hence sb_field and part_sb_field. They allow the Freeside administrator to
+define 'extended fields' ('xfields') associated with svc_broadband records.
+These are I<not> processed in any way by Freeside itself; they exist solely for
+use by exports (see L<FS::part_export>) and technical support staff.
+
+For a parallel mechanism (at the per-router level rather than per-service),
+see L<FS::part_router_field>.
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record. To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'part_sb_field'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database. If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is an error,
+returns the error, otherwise returns false. Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+ my $error = '';
+
+ $error = $self->ut_numbern('svcpart');
+ return $error if $error;
+
+ unless (qsearchs('part_svc', { svcpart => $self->svcpart }))
+ { return "Unknown svcpart: " . $self->svcpart;}
+
+ $self->name =~ /^([a-z0-9_\-\.]{1,15})$/i
+ or return "Invalid field name for part_sb_field";
+
+ #How to check input_block, display_block, and check_block?
+
+ ''; #no error
+}
+
+=item list_values
+
+If the I<list_source> field is set, this method eval()s it and
+returns its output. If the field is empty, list_values returns
+an empty list.
+
+Any arguments passed to this method will be received by the list_source
+code, but this behavior is a fortuitous accident and may be removed in
+the future.
+
+=cut
+
+sub list_values {
+ my $self = shift;
+ return () unless $self->list_source;
+
+ my @opts = eval($self->list_source);
+ if($@) {
+ warn $@;
+ return ();
+ } else {
+ return @opts;
+ }
+}
+
+=item part_svc
+
+Returns the FS::part_svc object associated with this field definition.
+
+=cut
+
+sub part_svc {
+ my $self = shift;
+ return qsearchs('part_svc', { svcpart => $self->svcpart });
+}
+
+=back
+
+=head1 VERSION
+
+$Id:
+
+=head1 NOTES
+
+=over
+
+=item 1.
+
+The I<length> field is not enforced. It provides a hint to UI
+code about how to display the field on a form. If you want to enforce a
+minimum or maximum length for a field, use a I<check_block>.
+
+=item 2.
+
+The check_block mechanism used here as well as in
+FS::part_router_field allows the user to define validation rules.
+
+When FS::sb_field::check is called, the proposed value of the xfield is
+assigned to $_. The check_block is then eval()'d and its return value
+captured. If the return value is false (empty/zero/undef), $_ is then assigned
+back into the field and stored in the database.
+
+Therefore a check_block can do three different things with the value: allow
+it, allow it with a modification, or reject it. This is very flexible, but
+somewhat dangerous. Some warnings:
+
+=over 2
+
+=item *
+
+Assume that $_ has had I<no> error checking prior to the
+check_block. That's what the check_block is for, after all. It could
+contain I<anything>: evil shell commands in backquotes, 100kb JPEG images,
+the Klez virus, whatever.
+
+=item *
+
+If your check_block modifies the input value, it should probably
+produce a value that wouldn't be modified by going through the same
+check_block again. (That is, it should map input values into its own
+eigenspace.) The reason is that if someone calls $new->replace($old),
+where $new and $old contain the same value for the field, they probably
+want the field to keep its old value, not to get transformed by the
+check_block again. So don't do silly things like '$_++' or
+'tr/A-Za-z/a-zA-Z/'.
+
+=item *
+
+Don't alter the contents of the database. I<Reading> the database
+is perfectly reasonable, but writing to it is a bad idea. Remember that
+check() might get called more than once, as described above.
+
+=item *
+
+The check_block probably won't even get called if the user submits
+an I<empty> sb_field. So at present, you can't set up a default value with
+something like 's/^$/foo/'. Conversely, don't replace the submitted value
+with an empty string. It probably will get stored, but might be deleted at
+any time.
+
+=back
+
+=item 3.
+
+The list_source mechanism is a UI hint (like length) to generate
+drop-down or list boxes. If list_source contains a value, the UI code can
+eval() it and use the results as the options on the list.
+
+Note 'can'. This is not a substitute for check_block. The HTML interface
+currently requires that the user pick one of the options on the list
+because that's the way HTML drop-down boxes work, but in the future the UI
+code might add an 'Other (please specify)' option and a text box so that
+the user can enter something else. Or it might ignore list_source and just
+generate a text box. Or the interface might be rewritten in MS Access,
+where drop-down boxes have text boxes built in. Data validation is the job
+of check(), not the front end.
+
+Note also that a list of literals evaluates to itself, so a list_source
+like
+
+C<('Windows', 'MacOS', 'Linux')>
+
+or
+
+C<qw(Windows MacOS Linux)>
+
+means exactly what you'd think.
+
+=head1 BUGS
+
+The lack of any way to do default values. We might add this as another UI
+hint (since, for the most part, it's the UI's job to figure out which fields
+have had values entered into them). In fact, there are lots of things we
+should add as UI hints.
+
+Oh, and the documentation is probably full of lies.
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::sb_field, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_svc_router.pm b/FS/FS/part_svc_router.pm
new file mode 100755
index 000000000..0b23ab580
--- /dev/null
+++ b/FS/FS/part_svc_router.pm
@@ -0,0 +1,32 @@
+package FS::part_svc_router;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw(qsearchs);
+use FS::router;
+use FS::part_svc;
+
+@ISA = qw(FS::Record);
+
+sub table { 'part_svc_router'; }
+
+sub check {
+ my $self = shift;
+ my $error =
+ $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart')
+ || $self->ut_foreign_key('routernum', 'router', 'routernum');
+ return $error if $error;
+ ''; #no error
+}
+
+sub router {
+ my $self = shift;
+ return qsearchs('router', { routernum => $self->routernum });
+}
+
+sub part_svc {
+ my $self = shift;
+ return qsearchs('part_svc', { svcpart => $self->svcpart });
+}
+
+1;
diff --git a/FS/FS/router.pm b/FS/FS/router.pm
new file mode 100755
index 000000000..3f9459a01
--- /dev/null
+++ b/FS/FS/router.pm
@@ -0,0 +1,156 @@
+package FS::router;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs qsearch );
+use FS::addr_block;
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::router - Object methods for router records
+
+=head1 SYNOPSIS
+
+ use FS::router;
+
+ $record = new FS::router \%hash;
+ $record = new FS::router { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::router record describes a broadband router, such as a DSLAM or a wireless
+ access point. FS::router inherits from FS::Record. The following
+fields are currently supported:
+
+=over 4
+
+=item routernum - primary key
+
+=item routername - descriptive name for the router
+
+=item svcnum - svcnum of the owning FS::svc_broadband, if appropriate
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record. To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'router'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database. If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is an error,
+returns the error, otherwise returns false. Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('routernum')
+ || $self->ut_text('routername');
+ return $error if $error;
+
+ '';
+}
+
+=item addr_block
+
+Returns a list of FS::addr_block objects (address blocks) associated
+with this object.
+
+=cut
+
+sub addr_block {
+ my $self = shift;
+ return qsearch('addr_block', { routernum => $self->routernum });
+}
+
+=item router_field
+
+Returns a list of FS::router_field objects assigned to this object.
+
+=cut
+
+sub router_field {
+ my $self = shift;
+
+ return qsearch('router_field', { routernum => $self->routernum });
+}
+
+=item part_svc_router
+
+Returns a list of FS::part_svc_router objects associated with this
+object. This is unlikely to be useful for any purpose other than retrieving
+the associated FS::part_svc objects. See below.
+
+=cut
+
+sub part_svc_router {
+ my $self = shift;
+ return qsearch('part_svc_router', { routernum => $self->routernum });
+}
+
+=item part_svc
+
+Returns a list of FS::part_svc objects associated with this object.
+
+=cut
+
+sub part_svc {
+ my $self = shift;
+ return map { qsearchs('part_svc', { svcpart => $_->svcpart }) }
+ $self->part_svc_router;
+}
+
+=back
+
+=head1 VERSION
+
+$Id:
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::router, FS::addr_block, FS::router_field, FS::part_svc,
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/router_field.pm b/FS/FS/router_field.pm
new file mode 100755
index 000000000..eee21ab89
--- /dev/null
+++ b/FS/FS/router_field.pm
@@ -0,0 +1,146 @@
+package FS::router_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::part_router_field;
+use FS::router;
+
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::router_field - Object methods for router_field records
+
+=head1 SYNOPSIS
+
+ use FS::router_field;
+
+ $record = new FS::router_field \%hash;
+ $record = new FS::router_field { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+FS::router_field contains values of router xfields. See FS::part_sb_field
+for details on the xfield mechanism.
+
+=over 4
+
+=item routerfieldpart - Type of router_field as defined by
+FS::part_router_field
+
+=item routernum - The FS::router to which this value belongs.
+
+=item value - The contents of the field.
+
+=back
+
+=head1 METHODS
+
+
+=over 4
+
+=item new HASHREF
+
+Create a new record. To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'router_field'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database. If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is an error,
+returns the error, otherwise returns false. Called by the insert and replace
+methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ return "routernum must be defined" unless $self->routernum;
+ return "routerfieldpart must be defined" unless $self->routerfieldpart;
+
+ my $part_router_field = $self->part_router_field;
+ $_ = $self->value;
+
+ my $check_block = $part_router_field->check_block;
+ if ($check_block) {
+ $@ = '';
+ my $error = (eval($check_block) or $@);
+ return $error if $error;
+ $self->setfield('value' => $_);
+ }
+
+ ''; #no error
+}
+
+=item part_router_field
+
+Returns a reference to the FS:part_router_field that defines this
+FS::router_field
+
+=cut
+
+sub part_router_field {
+ my $self = shift;
+
+ return qsearchs('part_router_field',
+ { routerfieldpart => $self->routerfieldpart });
+}
+
+=item router
+
+Returns a reference to the FS::router to which this FS::router_field
+belongs.
+
+=cut
+
+sub router {
+ my $self = shift;
+
+ return qsearchs('router', { routernum => $self->routernum });
+}
+
+=back
+
+=head1 VERSION
+
+$Id:
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+FS::svc_broadband, FS::router, FS::router_block, FS::router_field,
+schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/sb_field.pm b/FS/FS/sb_field.pm
new file mode 100755
index 000000000..d4eb37844
--- /dev/null
+++ b/FS/FS/sb_field.pm
@@ -0,0 +1,148 @@
+package FS::sb_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs );
+use FS::part_sb_field;
+
+use UNIVERSAL qw( can );
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::sb_field - Object methods for sb_field records
+
+=head1 SYNOPSIS
+
+ use FS::sb_field;
+
+ $record = new FS::sb_field \%hash;
+ $record = new FS::sb_field { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+See L<FS::part_sb_field> for details on this table's mission in life.
+FS::sb_field contains the actual values of the xfields defined in
+part_sb_field.
+
+The following fields are supported:
+
+=over 4
+
+=item sbfieldpart - Type of sb_field as defined by FS::part_sb_field
+
+=item svcnum - The svc_broadband to which this value belongs.
+
+=item value - The contents of the field.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record. To add the record to the database, see L<"insert">.
+
+=cut
+
+sub table { 'sb_field'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database. If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks the value against the check_block of the corresponding part_sb_field.
+Returns whatever the check_block returned (unless the check_block dies, in
+which case check returns the die message). Therefore, if the check_block
+wants to allow the value to be stored, it must return false. See
+L<FS::part_sb_field> for details.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ return "svcnum must be defined" unless $self->svcnum;
+ return "sbfieldpart must be defined" unless $self->sbfieldpart;
+
+ my $part_sb_field = $self->part_sb_field;
+
+ $_ = $self->value;
+
+ my $check_block = $self->part_sb_field->check_block;
+ if ($check_block) {
+ $@ = '';
+ my $error = (eval($check_block) or $@); # treat fatal errors as errors
+ return $error if $error;
+ $self->setfield('value' => $_);
+ }
+
+ ''; #no error
+}
+
+=item part_sb_field
+
+Returns a reference to the FS::part_sb_field that defines this FS::sb_field.
+
+=cut
+
+sub part_sb_field {
+ my $self = shift;
+
+ return qsearchs('part_sb_field', { sbfieldpart => $self->sbfieldpart });
+}
+
+=back
+
+=item svc_broadband
+
+Returns a reference to the FS::svc_broadband to which this value is attached.
+Nobody's ever going to use this function, but here it is anyway.
+
+=cut
+
+sub svc_broadband {
+ my $self = shift;
+
+ return qsearchs('svc_broadband', { svcnum => $self->svcnum });
+}
+
+=head1 VERSION
+
+$Id:
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_broadband>, schema.html
+from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm
index ab92fb3d7..45f6c3601 100755
--- a/FS/FS/svc_broadband.pm
+++ b/FS/FS/svc_broadband.pm
@@ -2,10 +2,10 @@ package FS::svc_broadband;
use strict;
use vars qw(@ISA $conf);
-#use FS::Record qw( qsearch qsearchs );
use FS::Record qw( qsearchs qsearch dbh );
use FS::svc_Common;
use FS::cust_svc;
+use FS::addr_block;
use NetAddr::IP;
@ISA = qw( FS::svc_Common );
@@ -45,25 +45,6 @@ An FS::svc_broadband object represents a 'broadband' Internet connection, such
as a DSL, cable modem, or fixed wireless link. These services are assumed to
have the following properties:
-=over 2
-
-=item
-The network consists of one or more 'Access Concentrators' (ACs), such as
-DSLAMs or wireless access points. (See L<FS::ac>.)
-
-=item
-Each AC provides connectivity to one or more contiguous blocks of IP addresses,
-each described by a gateway address and a netmask. (See L<FS::ac_block>.)
-
-=item
-Each connection has one or more static IP addresses within one of these blocks.
-
-=item
-The details of configuring routers and other devices are to be handled by a
-site-specific L<FS::part_export> subclass.
-
-=back
-
FS::svc_broadband inherits from FS::svc_Common. The following fields are
currently supported:
@@ -71,14 +52,7 @@ currently supported:
=item svcnum - primary key
-=item
-actypenum - access concentrator type; see L<FS::ac_type>. This is included here
-so that a part_svc can specifically be a 'wireless' or 'DSL' service by
-designating actypenum as a fixed field. It does create a redundant functional
-dependency between this table and ac_type, in that the matching ac_type could
-be found by looking up the IP address in ac_block and then finding the block's
-AC, but part_svc can't do that, and we don't feel like hacking it so that it
-can.
+=item blocknum - see FS::addr_block
=item
speed_up - maximum upload speed, in bits per second. If set to zero, upload
@@ -89,28 +63,12 @@ connection.
=item
speed_down - maximum download speed, as above
-=item
-ip_addr - the customer's IP address. If the customer needs more than one IP
-address, set this to the address of the customer's router. As a result, the
-customer's router will have the same address for both it's internal and external
+=item ip_addr - the customer's IP address. If the customer needs more than one
+IP address, set this to the address of the customer's router. As a result, the
+customer's router will have the same address for both its internal and external
interfaces thus saving address space. This has been found to work on most NAT
routers available.
-=item
-ip_netmask - the customer's netmask, as a single integer in the range 0-32.
-(E.g. '24', not '255.255.255.0'. We assume that address blocks are contiguous.)
-This should be 32 unless the customer has multiple IP addresses.
-
-=item
-mac_addr - the MAC address of the customer's router or other device directly
-connected to the network, if needed. Some systems (e.g. DHCP, MAC address-based
-access control) may need this. If not, you may leave it blank.
-
-=item
-location - a human-readable description of the location of the connected site,
-such as its address. This should not be used for billing or contact purposes;
-that information is stored in L<FS::cust_main>.
-
=back
=head1 METHODS
@@ -120,7 +78,7 @@ that information is stored in L<FS::cust_main>.
=item new HASHREF
Creates a new svc_broadband. To add the record to the database, see
-L<"insert">.
+"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.
@@ -134,14 +92,12 @@ sub table { 'svc_broadband'; }
Adds this record to the database. If there is an error, returns the error,
otherwise returns false.
-The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be
+The additional fields pkgnum and svcpart (see FS::cust_svc) should be
defined. An FS::cust_svc record will be created and inserted.
=cut
-# sub insert {}
# Standard FS::svc_Common::insert
-# (any necessary Deep Magic is handled by exports)
=item delete
@@ -159,19 +115,62 @@ returns the error, otherwise returns false.
=cut
# Standard FS::svc_Common::replace
-# Notice a pattern here?
+
+=item sb_field
+
+Returns a list of FS::sb_field objects assigned to this object.
+
+=cut
+
+sub sb_field {
+ my $self = shift;
+
+ return qsearch( 'sb_field', { svcnum => $self->svcnum } );
+}
+
+=item sb_field_hashref
+
+Returns a hashref of the FS::sb_field key/value pairs for this object.
+
+Deprecated. Please don't use it.
+
+=cut
+
+# Kristian wrote this, but don't hold it against him. He was under a powerful
+# distracting influence whom he evidently found much more interesting than
+# svc_broadband.pm. I can't say I blame him.
+
+sub sb_field_hashref {
+ my $self = shift;
+ my $svcpart = shift;
+
+ if ((not $svcpart) && ($self->cust_svc)) {
+ $svcpart = $self->cust_svc->svcpart;
+ }
+
+ my $hashref = {};
+
+ map {
+ my $sb_field = qsearchs('sb_field', { sbfieldpart => $_->sbfieldpart,
+ svcnum => $self->svcnum });
+ $hashref->{$_->getfield('name')} = $sb_field ? $sb_field->getfield('value') : '';
+ } qsearch('part_sb_field', { svcpart => $svcpart });
+
+ return $hashref;
+
+}
=item suspend
-Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
=item unsuspend
-Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
=item cancel
-Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
=item check
@@ -189,105 +188,99 @@ sub check {
my $error =
$self->ut_numbern('svcnum')
- || $self->ut_foreign_key('actypenum', 'ac_type', 'actypenum')
+ || $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum')
|| $self->ut_number('speed_up')
|| $self->ut_number('speed_down')
- || $self->ut_ip('ip_addr')
- || $self->ut_numbern('ip_netmask')
- || $self->ut_textn('mac_addr')
- || $self->ut_textn('location')
+ || $self->ut_ipn('ip_addr')
;
return $error if $error;
if($self->speed_up < 0) { return 'speed_up must be positive'; }
if($self->speed_down < 0) { return 'speed_down must be positive'; }
- # This should catch errors in the ip_addr and ip_netmask. If it doesn't,
- # they'll almost certainly not map into a valid block anyway.
- my $self_addr = new NetAddr::IP ($self->ip_addr, $self->ip_netmask);
- return 'Cannot parse address: ' . $self->ip_addr . '/' . $self->ip_netmask unless $self_addr;
-
- my @block = grep {
- my $block_addr = new NetAddr::IP ($_->ip_gateway, $_->ip_netmask);
- if ($block_addr->contains($self_addr)) { $_ };
- } qsearch( 'ac_block', { acnum => $self->acnum });
-
- if(scalar @block == 0) {
- return 'Block not found for address '.$self->ip_addr.' in actype '.$self->actypenum;
- } elsif(scalar @block > 1) {
- return 'ERROR: Intersecting blocks found for address '.$self->ip_addr.' :'.
- join ', ', map {$_->ip_addr . '/' . $_->ip_netmask} @block;
+ if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
+ $self->ip_addr($self->addr_block->next_free_addr->addr);
+ if (not $self->ip_addr) {
+ return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
+ }
}
- # OK, we've found a valid block. We don't actually _do_ anything with it, though; we
- # just take comfort in the knowledge that it exists.
- # A simple qsearchs won't work here. Since we can assign blocks to customers,
- # we have to make sure the new address doesn't fall within someone else's
- # block. Ugh.
+ # This should catch errors in the ip_addr. If it doesn't,
+ # they'll almost certainly not map into the block anyway.
+ my $self_addr = $self->NetAddr; #netmask is /32
+ return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
- my @conflicts = grep {
- my $cust_addr = new NetAddr::IP($_->ip_addr, $_->ip_netmask);
- if (($cust_addr->contains($self_addr)) and
- ($_->svcnum ne $self->svcnum)) { $_; };
- } qsearch('svc_broadband', {});
-
- if (scalar @conflicts > 0) {
- return 'Address in use by existing service';
+ my $block_addr = $self->addr_block->NetAddr;
+ unless ($block_addr->contains($self_addr)) {
+ return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
}
- # Are we trying to use a network, broadcast, or the AC's address?
- foreach (qsearch('ac_block', { acnum => $self->acnum })) {
- my $block_addr = new NetAddr::IP($_->ip_gateway, $_->ip_netmask);
- if ($block_addr->network->addr eq $self_addr->addr) {
- return 'Address is network address for block '. $block_addr->network;
- }
- if ($block_addr->broadcast->addr eq $self_addr->addr) {
- return 'Address is broadcast address for block '. $block_addr->network;
- }
- if ($block_addr->addr eq $self_addr->addr) {
- return 'Address belongs to the access concentrator: '. $block_addr->addr;
- }
+ my $router = $self->addr_block->router
+ or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
+ if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
+ } # do nothing
+ else {
+ return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
}
+
''; #no error
}
-=item ac_block
+=item NetAddr
-Returns the FS::ac_block record (i.e. the address block) for this broadband service.
+Returns a NetAddr::IP object containing the IP address of this service. The netmask
+is /32.
=cut
-sub ac_block {
+sub NetAddr {
my $self = shift;
- my $self_addr = new NetAddr::IP ($self->ip_addr, $self->ip_netmask);
-
- foreach my $block (qsearch( 'ac_block', {} )) {
- my $block_addr = new NetAddr::IP ($block->ip_addr, $block->ip_netmask);
- if($block_addr->contains($self_addr)) { return $block; }
- }
- return '';
+ return new NetAddr::IP ($self->ip_addr);
}
-=item ac_type
+=item addr_block
-Returns the FS::ac_type record for this broadband service.
+Returns the FS::addr_block record (i.e. the address block) for this broadband service.
=cut
-sub ac_type {
+sub addr_block {
my $self = shift;
- return qsearchs('ac_type', { actypenum => $self->actypenum });
+
+ return qsearchs('addr_block', { blocknum => $self->blocknum });
}
=back
+=item allowed_routers
+
+Returns a list of allowed FS::router objects.
+
+=cut
+
+sub allowed_routers {
+ my $self = shift;
+
+ return map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
+}
+
=head1 BUGS
+I think there's one place in the code where we actually use sb_field_hashref.
+That's a bug in itself.
+
+The real problem with it is that we're still grappling with the question of how
+tightly xfields should be integrated with real fields. There are a few
+different directions we could go with it--we I<could> override several
+functions in Record so that xfields behave almost exactly like real fields (can
+be set with setfield(), appear in fields() and hash(), used as criteria in
+qsearch(), etc.).
+
=head1 SEE ALSO
-L<FS::svc_Common>, L<FS::Record>, L<FS::ac_type>, L<FS::ac_block>,
-L<FS::part_svc>, schema.html from the base documentation.
+FS::svc_Common, FS::Record, FS::addr_block, FS::sb_field,
+FS::part_svc, schema.html from the base documentation.
=cut
diff --git a/FS/bin/freeside-setup b/FS/bin/freeside-setup
index 0ef3fc81b..19483765e 100755
--- a/FS/bin/freeside-setup
+++ b/FS/bin/freeside-setup
@@ -1012,76 +1012,99 @@ sub tables_hash_hack {
'index' => [],
},
- 'ac_type' => {
+ 'router' => {
'columns' => [
- 'actypenum', 'serial', '', '',
- 'actypename', 'varchar', '', $char_d,
+ 'routernum', 'serial', '', '',
+ 'routername', 'varchar', '', $char_d,
+ 'svcnum', 'int', '0', '',
],
- 'primary_key' => 'actypenum',
+ 'primary_key' => 'routernum',
'unique' => [],
'index' => [],
},
- 'ac' => {
+ 'part_svc_router' => {
'columns' => [
- 'acnum', 'serial', '', '',
- 'actypenum', 'int', '', '',
- 'acname', 'varchar', '', $char_d,
- ],
- 'primary_key' => 'acnum',
+ 'svcpart', 'int', '', '',
+ 'routernum', 'int', '', '',
+ ];
+ 'primary_key' => '',
'unique' => [],
- 'index' => [ [ 'actypenum' ] ],
+ 'index' => [],
},
- 'part_ac_field' => {
+ 'part_router_field' => {
'columns' => [
- 'acfieldpart', 'serial', '', '',
- 'actypenum', 'int', '', '',
+ 'routerfieldpart', 'serial', '', '',
'name', 'varchar', '', $char_d,
- 'ut_type', 'varchar', '', $char_d,
+ 'length', 'int', '', '',
+ 'check_block', 'text', 'NULL', '',
+ 'list_source', 'text', 'NULL', '',
],
- 'primary_key' => 'acfieldpart',
+ 'primary_key' => 'routerfieldpart',
'unique' => [],
- 'index' => [ [ 'actypenum' ] ],
+ 'index' => [],
},
- 'ac_field' => {
+ 'router_field' => {
'columns' => [
- 'acfieldpart', 'int', '', '',
- 'acnum', 'int', '', '',
- 'value', 'text', '', '',
+ 'routerfieldpart', 'int', '', '',
+ 'routernum', 'int', '', '',
+ 'value', 'varchar', '', 128,
],
'primary_key' => '',
- 'unique' => [ [ 'acfieldpart', 'acnum' ] ],
- 'index' => [ [ 'acnum' ] ],
+ 'unique' => [ [ 'routerfieldpart', 'routernum' ] ],
+ 'index' => [],
},
- 'ac_block' => {
+ 'addr_block' => {
'columns' => [
- 'acnum', 'int', '', '',
+ 'blocknum', 'int', '', '',
+ 'routernum', 'int', '', '',
'ip_gateway', 'varchar', '', 15,
'ip_netmask', 'int', '', '',
],
+ 'primary_key' => 'blocknum',
+ 'unique' => [ [ 'blocknum', 'routernum' ] ],
+ 'index' => [],
+ },
+
+ 'part_sb_field' => {
+ 'columns' => [
+ 'sbfieldpart', 'int', '', '',
+ 'svcpart', 'int', '', '',
+ 'name', 'varchar', '', $char_d,
+ 'length', 'int', '', '',
+ 'check_block', 'text', 'NULL', '',
+ 'list_source', 'text', 'NULL', '',
+ ],
+ 'primary_key' => 'sbfieldpart',
+ 'unique' => [ [ 'sbfieldpart', 'svcpart' ] ],
+ 'index' => [],
+ },
+
+ 'sb_field' => {
+ 'columns' => [
+ 'sbfieldpart', 'int', '', '',
+ 'svcnum', 'int', '', '',
+ 'value', 'varchar', '', 128,
+ ],
'primary_key' => '',
- 'unique' => [],
- 'index' => [ [ 'acnum' ] ],
+ 'unique' => [ [ 'sbfieldpart', 'svcnum' ] ],
+ 'index' => [],
},
'svc_broadband' => {
'columns' => [
'svcnum', 'int', '', '',
- 'actypenum', 'int', '', '',
+ 'blocknum', 'int', '', '',
'speed_up', 'int', '', '',
'speed_down', 'int', '', '',
- 'acnum', 'int', '', '',
'ip_addr', 'varchar', '', 15,
- 'ip_netmask', 'int', '', '',
- 'mac_addr', 'char', '', 17,
- 'location', 'varchar', '', $char_d,
],
'primary_key' => 'svcnum',
'unique' => [],
- 'index' => [ [ 'actypenum' ] ],
+ 'index' => [],
},
);