diff options
author | khoff <khoff> | 2002-09-09 23:05:30 +0000 |
---|---|---|
committer | khoff <khoff> | 2002-09-09 23:05:30 +0000 |
commit | 44398c83f25bf4e43838df9f39331c29fdeff19d (patch) | |
tree | 963837373a3b621ee2140adad0eb0b44e12c75e6 /FS | |
parent | 91292eadb6254740a9b72e5dc95f575593f6a35d (diff) |
svc_broadband merge
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/ac.pm | 148 | ||||
-rwxr-xr-x | FS/FS/ac_block.pm | 148 | ||||
-rwxr-xr-x | FS/FS/ac_field.pm | 138 | ||||
-rwxr-xr-x | FS/FS/ac_type.pm | 128 | ||||
-rw-r--r-- | FS/FS/cust_svc.pm | 4 | ||||
-rwxr-xr-x | FS/FS/part_ac_field.pm | 102 | ||||
-rw-r--r-- | FS/FS/part_export.pm | 3 | ||||
-rwxr-xr-x | FS/FS/svc_broadband.pm | 295 |
8 files changed, 965 insertions, 1 deletions
diff --git a/FS/FS/ac.pm b/FS/FS/ac.pm new file mode 100644 index 0000000..5a2b360 --- /dev/null +++ b/FS/FS/ac.pm @@ -0,0 +1,148 @@ +package FS::ac; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearchs qsearch ); +use FS::ac_type; +use FS::ac_block; + +@ISA = qw( FS::Record ); + +=head1 NAME + +FS::ac - Object methods for ac records + +=head1 SYNOPSIS + + use FS::ac; + + $record = new FS::ac \%hash; + $record = new FS::ac { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::ac record describes a broadband Access Concentrator, such as a DSLAM +or a wireless access point. FS::ac inherits from FS::Record. The following +fields are currently supported: + +narf + +=over 4 + +=item acnum - primary key + +=item actypenum - AC type, see L<FS::ac_type> + +=item acname - descriptive name for the AC + +=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 { 'ac'; } + +=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('acnum') + || $self->ut_number('actypenum') + || $self->ut_text('acname'); + return $error if $error; + + return "Unknown actypenum" + unless $self->ac_type; + ''; +} + +=item ac_type + +Returns the L<FS::ac_type> object corresponding to this object. + +=cut + +sub ac_type { + my $self = shift; + return qsearchs('ac_type', { actypenum => $self->actypenum }); +} + +=item ac_block + +Returns a list of L<FS::ac_block> objects (address blocks) associated +with this object. + +=cut + +sub ac_block { + my $self = shift; + return qsearch('ac_block', { acnum => $self->acnum }); +} + +=item ac_field + +Returns a hash of L<FS::ac_field> objects assigned to this object. + +=cut + +sub ac_field { + my $self = shift; + + return qsearch('ac_field', { acnum => $self->acnum }); +} + +=back + +=head1 VERSION + +$Id: + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::svc_broadband>, L<FS::ac>, L<FS::ac_block>, L<FS::ac_field>, schema.html +from the base documentation. + +=cut + +1; + diff --git a/FS/FS/ac_block.pm b/FS/FS/ac_block.pm new file mode 100755 index 0000000..09de6a4 --- /dev/null +++ b/FS/FS/ac_block.pm @@ -0,0 +1,148 @@ +package FS::ac_block; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearchs qsearch ); +use FS::ac_type; +use FS::ac; +use FS::svc_broadband; +use NetAddr::IP; + +@ISA = qw( FS::Record ); + +=head1 NAME + +FS::ac - Object methods for ac records + +=head1 SYNOPSIS + + use FS::ac_block; + + $record = new FS::ac_block \%hash; + $record = new FS::ac_block { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::ac_block record describes an address block assigned for broadband +access. FS::ac_block inherits from FS::Record. The following fields are +currently supported: + +=over 4 + +=item acnum - the access concentrator (see L<FS::ac_type>) to which this +block is assigned. + +=item ip_gateway - the gateway address used by customers within this block. +This functions as the primary key. + +=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 L<"insert">. + +=cut + +sub table { 'ac_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. + +=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('acnum') + || $self->ut_ip('ip_gateway') + || $self->ut_number('ip_netmask') + ; + return $error if $error; + + return "Unknown acnum" + unless $self->ac; + + my $self_addr = new NetAddr::IP ($self->ip_gateway, $self->ip_netmask); + return "Cannot parse address: ". $self->ip_gateway . '/' . $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) + or $self_addr->contains($block_addr)) { $_; }; + } qsearch( 'ac_block', {}); + + foreach(@block) { + return "Block intersects existing block ".$_->ip_gateway."/".$_->ip_netmask; + } + + ''; +} + + +=item ac + +Returns the L<FS::ac> object corresponding to this object. + +=cut + +sub ac { + my $self = shift; + return qsearchs('ac', { acnum => $self->acnum }); +} + +=item svc_broadband + +Returns a list of L<FS::svc_broadband> objects associated +with this object. + +=cut + +#sub svc_broadband { +# my $self = shift; +# my @svc = qsearch('svc_broadband', { actypenum => $self->ac->ac_type->actypenum }); +# return grep { +# my $svc_addr = new NetAddr::IP($_->ip_addr, $_->ip_netmask); +# $self_addr->contains($svc_addr); +# } @svc; +#} + +=back + +=cut + +1; + diff --git a/FS/FS/ac_field.pm b/FS/FS/ac_field.pm new file mode 100755 index 0000000..f601119 --- /dev/null +++ b/FS/FS/ac_field.pm @@ -0,0 +1,138 @@ +package FS::ac_field; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearchs ); +use FS::part_ac_field; +use FS::ac; + +use UNIVERSAL qw( can ); + +@ISA = qw( FS::Record ); + +=head1 NAME + +FS::ac_field - Object methods for ac_field records + +=head1 SYNOPSIS + + use FS::ac_field; + + $record = new FS::ac_field \%hash; + $record = new FS::ac_field { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +L<FS::ac_field> contains values of fields defined by L<FS::part_ac_field> +for an L<FS::ac>. Values must be of the data type defined by ut_type in +L<FS::part_ac_field>. +Supported fields as follows: + +=over 4 + +=item acfieldpart - Type of ac_field as defined by L<FS::part_ac_field> + +=item acnum - The L<FS::ac> 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 { 'ac_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 "acnum must be defined" unless $self->acnum; + return "acfieldpart must be defined" unless $self->acfieldpart; + + my $ut_func = $self->can("ut_" . $self->part_ac_field->ut_type); + my $error = $self->$ut_func('value'); + + return $error if $error; + + ''; #no error +} + +=item part_ac_field + +Returns a reference to the L<FS:part_ac_field> that defines this L<FS::ac_field> + +=cut + +sub part_ac_field { + my $self = shift; + + return qsearchs('part_ac_field', { acfieldpart => $self->acfieldpart }); +} + +=item ac + +Returns a reference to the L<FS::ac> to which this L<FS::ac_field> belongs. + +=cut + +sub ac { + my $self = shift; + + return qsearchs('ac', { acnum => $self->acnum }); +} + +=back + +=head1 VERSION + +$Id: + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::svc_broadband>, L<FS::ac>, L<FS::ac_block>, L<FS::ac_field>, schema.html +from the base documentation. + +=cut + +1; + diff --git a/FS/FS/ac_type.pm b/FS/FS/ac_type.pm new file mode 100755 index 0000000..e83c5c5 --- /dev/null +++ b/FS/FS/ac_type.pm @@ -0,0 +1,128 @@ +package FS::ac_type; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearchs ); +use FS::ac; + +@ISA = qw( FS::Record ); + +=head1 NAME + +FS::ac_type - Object methods for ac_type records + +=head1 SYNOPSIS + + use FS::ac_type; + + $record = new FS::ac_type \%hash; + $record = new FS::ac_type { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +L<FS::ac_type> refers to a type of access concentrator. L<FS::svc_broadband> +records refer to a specific L<FS::ac_type> limiting the choice of access +concentrator to one of the chosen type. This should be set as a fixed +default in part_svc to prevent provisioning the wrong type of service for +a given package or service type. Supported fields as follows: + +=over 4 + +=item actypenum - Primary key. see L<FS::ac> + +=item actypename - Text identifier for access concentrator type. + +=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 { 'ac_type'; } + +=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; + + # What do we check? + + ''; #no error +} + +=item ac + +Returns a list of all L<FS::ac> records of this type. + +=cut + +sub ac { + my $self = shift; + + return qsearch('ac', { actypenum => $self->actypenum }); +} + +=item part_ac_field + +Returns a list of all L<FS::part_ac_field> records of this type. + +=cut + +sub part_ac_field { + my $self = shift; + + return qsearch('part_ac_field', { actypenum => $self->actypenum }); +} + +=back + +=head1 VERSION + +$Id: + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::svc_broadband>, L<FS::ac>, L<FS::ac_block>, L<FS::ac_field>, schema.html +from the base documentation. + +=cut + +1; + diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index c7cc4b3..d54fb2d 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -295,6 +295,8 @@ sub label { } elsif ( $svcdb eq 'svc_www' ) { my $domain = qsearchs( 'domain_record', { 'recnum' => $svc_x->recnum } ); $tag = $domain->reczone; + } elsif ( $svcdb eq 'svc_broadband' ) { + $tag = $svc_x->ip_addr . '/' . $svc_x->ip_netmask; } else { cluck "warning: asked for label of unsupported svcdb; using svcnum"; $tag = $svc_x->getfield('svcnum'); @@ -344,7 +346,7 @@ sub seconds_since { =head1 VERSION -$Id: cust_svc.pm,v 1.15 2002-05-22 12:17:06 ivan Exp $ +$Id: cust_svc.pm,v 1.16 2002-09-09 23:01:35 khoff Exp $ =head1 BUGS diff --git a/FS/FS/part_ac_field.pm b/FS/FS/part_ac_field.pm new file mode 100755 index 0000000..dcb4452 --- /dev/null +++ b/FS/FS/part_ac_field.pm @@ -0,0 +1,102 @@ +package FS::part_ac_field; + +use strict; +use vars qw( @ISA ); +use FS::Record qw( qsearchs ); +use FS::ac_field; +use FS::ac; + + +@ISA = qw( FS::Record ); + +=head1 NAME + +FS::part_ac_field - Object methods for part_ac_field records + +=head1 SYNOPSIS + + use FS::part_ac_field; + + $record = new FS::part_ac_field \%hash; + $record = new FS::part_ac_field { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + + +=over 4 + +=item blank + +=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 { 'part_ac_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_ac_field"; + + ''; #no error +} + + +=back + +=head1 VERSION + +$Id: + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::svc_broadband>, L<FS::ac>, L<FS::ac_block>, L<FS::ac_field>, schema.html +from the base documentation. + +=cut + +1; + diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm index bc6a4d7..69cd805 100644 --- a/FS/FS/part_export.pm +++ b/FS/FS/part_export.pm @@ -839,6 +839,9 @@ tie my %sqlmail_options, 'Tie::IxHash', }, + 'svc_broadband' => { + }, + ); =back diff --git a/FS/FS/svc_broadband.pm b/FS/FS/svc_broadband.pm new file mode 100755 index 0000000..ab92fb3 --- /dev/null +++ b/FS/FS/svc_broadband.pm @@ -0,0 +1,295 @@ +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 NetAddr::IP; + +@ISA = qw( FS::svc_Common ); + +$FS::UID::callback{'FS::svc_broadband'} = sub { + $conf = new FS::Conf; +}; + +=head1 NAME + +FS::svc_broadband - Object methods for svc_broadband records + +=head1 SYNOPSIS + + use FS::svc_broadband; + + $record = new FS::svc_broadband \%hash; + $record = new FS::svc_broadband { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + + $error = $record->suspend; + + $error = $record->unsuspend; + + $error = $record->cancel; + +=head1 DESCRIPTION + +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: + +=over 4 + +=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 +speed_up - maximum upload speed, in bits per second. If set to zero, upload +speed will be unlimited. Exports that do traffic shaping should handle this +correctly, and not blindly set the upload speed to zero and kill the customer's +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 +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 + +=over 4 + +=item new HASHREF + +Creates a new svc_broadband. To add the record 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 { 'svc_broadband'; } + +=item insert + +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 +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 + +Delete this record from the database. + +=cut + +# Standard FS::svc_Common::delete + +=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 + +# 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>). + +=item unsuspend + +Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>). + +=item cancel + +Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>). + +=item check + +Checks all fields to make sure this is a valid broadband service. 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 $x = $self->setfixed; + + return $x unless ref($x); + + my $error = + $self->ut_numbern('svcnum') + || $self->ut_foreign_key('actypenum', 'ac_type', 'actypenum') + || $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') + ; + 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; + } + # 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'; + } + + # 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; + } + } + + ''; #no error +} + +=item ac_block + +Returns the FS::ac_block record (i.e. the address block) for this broadband service. + +=cut + +sub ac_block { + 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 ''; +} + +=item ac_type + +Returns the FS::ac_type record for this broadband service. + +=cut + +sub ac_type { + my $self = shift; + return qsearchs('ac_type', { actypenum => $self->actypenum }); +} + +=back + +=head1 BUGS + +=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. + +=cut + +1; + |