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 FS::part_svc_router;
use NetAddr::IP;
@ISA = qw( FS::svc_Common );
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:
=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
=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
=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.
=cut
+sub table_info {
+ {
+ 'name' => 'Broadband',
+ 'name_plural' => 'Broadband services',
+ 'longname_plural' => 'Fixed (username-less) broadband services',
+ 'display_weight' => 50,
+ 'cancel_weight' => 70,
+ 'fields' => {
+ 'description' => 'Descriptive label for this particular device.',
+ 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
+ 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
+ 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
+ 'blocknum' => { 'label' => 'Address block',
+ 'type' => 'select',
+ 'select_table' => 'addr_block',
+ 'select_key' => 'blocknum',
+ 'select_label' => 'cidr',
+ 'disable_inventory' => 1,
+ },
+ },
+ };
+}
+
sub table { 'svc_broadband'; }
-=item insert
+sub table_dupcheck_fields { ( 'mac_addr' ); }
+
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+sub search_sql {
+ my( $class, $string ) = @_;
+ if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
+ $class->search_sql_field('ip_addr', $string );
+ }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
+ $class->search_sql_field('mac_addr', uc($string));
+ }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
+ $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
+ } else {
+ '1 = 0'; #false
+ }
+}
+
+=item label
+
+Returns the IP address.
+
+=cut
+
+sub label {
+ my $self = shift;
+ $self->ip_addr;
+}
+
+=item insert [ , OPTION => VALUE ... ]
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.
+Currently available options are: I<depend_jobnum>
+
+If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
+jobnums), all provisioning jobs will have a dependancy on the supplied
+jobnum(s) (they will not run until the specific job(s) complete(s)).
+
=cut
-# sub insert {}
# Standard FS::svc_Common::insert
-# (any necessary Deep Magic is handled by exports)
=item delete
=cut
# Standard FS::svc_Common::replace
-# Notice a pattern here?
=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
my $error =
$self->ut_numbern('svcnum')
- || $self->ut_foreign_key('actypenum', 'ac_type', 'actypenum')
+ || $self->ut_numbern('blocknum')
+ || $self->ut_textn('description')
|| $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')
+ || $self->ut_hexn('mac_addr')
+ || $self->ut_hexn('auth_key')
+ || $self->ut_coordn('latitude', -90, 90)
+ || $self->ut_coordn('longitude', -180, 180)
+ || $self->ut_sfloatn('altitude')
+ || $self->ut_textn('vlan_profile')
;
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;
+ my $cust_svc = $self->svcnum
+ ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
+ : '';
+ my $cust_pkg;
+ if ($cust_svc) {
+ $cust_pkg = $cust_svc->cust_pkg;
+ }else{
+ $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
+ return "Invalid pkgnum" unless $cust_pkg;
}
- # 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.
-
- 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';
+
+ if ($self->blocknum) {
+ $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
+ return $error if $error;
}
- # 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 ($cust_pkg && $self->blocknum) {
+ my $addr_agentnum = $self->addr_block->agentnum;
+ if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
+ return "Address block does not service this customer";
}
- if ($block_addr->broadcast->addr eq $self_addr->addr) {
- return 'Address is broadcast address for block '. $block_addr->network;
+ }
+
+ if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
+ return "Must supply either address or block"
+ unless $self->blocknum;
+ my $next_addr = $self->addr_block->next_free_addr;
+ if ($next_addr) {
+ $self->ip_addr($next_addr->addr);
+ } else {
+ return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
}
- if ($block_addr->addr eq $self_addr->addr) {
- return 'Address belongs to the access concentrator: '. $block_addr->addr;
+ }
+
+ if (not($self->blocknum)) {
+ return "Must supply either address or block"
+ unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
+ my @block = grep { $_->NetAddr->contains($self->NetAddr) }
+ map { $_->addr_block }
+ $self->allowed_routers;
+ if (scalar(@block)) {
+ $self->blocknum($block[0]->blocknum);
+ }else{
+ return "Address not with available block.";
}
}
- ''; #no error
+ # 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 $block_addr = $self->addr_block->NetAddr;
+ unless ($block_addr->contains($self_addr)) {
+ return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_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;
+ }
+
+ $self->SUPER::check;
}
-=item ac_block
+sub _check_duplicate {
+ my $self = shift;
-Returns the FS::ac_block record (i.e. the address block) for this broadband service.
+ return "MAC already in use"
+ if ( $self->mac_addr &&
+ scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
+ );
+
+ '';
+}
+
+
+=item NetAddr
+
+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 '';
+ 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 });
+ 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;
+ map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
+}
+
=head1 BUGS
+The business with sb_field has been 'fixed', in a manner of speaking.
+
+allowed_routers isn't agent virtualized because part_svc isn't agent
+virtualized
+
=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::part_svc, schema.html from the base documentation.
=cut