diff options
-rw-r--r-- | FS/FS/Schema.pm | 5 | ||||
-rw-r--r-- | FS/FS/part_export/saisei.pm | 522 | ||||
-rw-r--r-- | FS/FS/part_svc.pm | 17 | ||||
-rw-r--r-- | FS/FS/tower.pm | 10 | ||||
-rw-r--r-- | FS/FS/tower_sector.pm | 35 | ||||
-rw-r--r-- | httemplate/edit/part_export.cgi | 14 | ||||
-rw-r--r-- | httemplate/edit/process/elements/process.html | 8 | ||||
-rw-r--r-- | httemplate/edit/process/tower.html | 2 | ||||
-rw-r--r-- | httemplate/edit/tower.html | 7 | ||||
-rw-r--r-- | httemplate/elements/tower_sector.html | 2 | ||||
-rw-r--r-- | httemplate/elements/tr-tower_sectors.html | 310 | ||||
-rw-r--r-- | httemplate/view/svc_export/run_script.cgi | 31 |
12 files changed, 872 insertions, 91 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index d0322545c..1567b0030 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -3342,6 +3342,8 @@ sub tables_hashref { 'height', 'decimal', 'NULL', '', '', '', 'veg_height', 'decimal', 'NULL', '', '', '', 'color', 'varchar', 'NULL', 6, '', '', + 'up_rate_limit', 'int', 'NULL', '', '', '', + 'down_rate_limit', 'int', 'NULL', '', '', '', ], 'primary_key' => 'towernum', 'unique' => [ [ 'towername' ] ], # , 'agentnum' ] ], @@ -3367,6 +3369,9 @@ sub tables_hashref { 'east', 'decimal', 'NULL', '10,7', '', '', 'south', 'decimal', 'NULL', '10,7', '', '', 'north', 'decimal', 'NULL', '10,7', '', '', + 'title', 'varchar', 'NULL', $char_d,'', '', + 'up_rate_limit', 'int', 'NULL', '', '', '', + 'down_rate_limit', 'int', 'NULL', '', '', '', ], 'primary_key' => 'sectornum', 'unique' => [ [ 'towernum', 'sectorname' ], [ 'ip_addr' ], ], diff --git a/FS/FS/part_export/saisei.pm b/FS/FS/part_export/saisei.pm index c7ee6f638..922a347b6 100644 --- a/FS/FS/part_export/saisei.pm +++ b/FS/FS/part_export/saisei.pm @@ -24,23 +24,51 @@ Saisei integration for Freeside This export offers basic svc_broadband provisioning for Saisei. -This is a customer integration with Saisei. This will setup a rate plan and tie -the rate plan to a host via the Saisei API when the broadband service is provisioned. -It will also untie the rate plan via the API upon unprovisioning of the broadband service. +This is a customer integration with Saisei. This will setup a rate plan and tie +the rate plan to a host and access point via the Saisei API when the broadband service is provisioned. +It will also untie the rate plan via the API upon unprovisioning of the broadband service. + +This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified. +This will also create and modify a access point at Saisei as soon as the tower is created or modified. + +To use this export, follow the below instructions: -This export will use the broadband service descriptive label for the Saisei rate plan name and -will use the email from the first contact for the Saisei username that will be -attached to this rate plan. It will use the Saisei default Access Point. +Add a new export and fill out required fields: -Hostname or IP - Host name to Saisei API -Port - <I>Port number to Saisei API +Hostname or IP - <I>Host name to Saisei API User Name - <I>Saisei API user name Password - <I>Saisei API password +Create a broadband service. The broadband service name will become the Saisei rate plan name. +Set the upload and download speed for the service. This is required to be able to export the service to Saisei. +Attach above created Saisei export to this broadband service. + +Create a tower and add a sector to that tower. The sector name will be the name of the access point, +Make sure you have set the up and down rate limit for the Tower and Sector. This is required to be able to export the access point. + +Create a package for the above created broadband service, and order this package for a customer. + +When you provision the service, enter the ip address associated to this service and select the Tower and Sector for it's access point. +This provisioned service will then be exported as a host to Saisei. + +when you un provision this service, the host entry at Saisei will be deleted. + +When setting this up, if you wish to export your allready provisioned services, make sure the broadband service has this export attached and +on export edit screen there will be a link to export Provisioned Services attached to this export. Clicking on that will export all services +not currently exported to Saisei. + This module also provides generic methods for working through the L</Saisei API>. =cut +tie my %scripts, 'Tie::IxHash', + 'export_provisioned_services' => { component => '/elements/popup_link.html', + label => 'Export provisioned services', + description => 'will export provisioned services of part service with Saisei export attached.', + html_label => '<b>Export Provisioned Services attached to this export.</b>', + }, +; + tie my %options, 'Tie::IxHash', 'port' => { label => 'Port', default => 5000 }, @@ -56,37 +84,63 @@ tie my %options, 'Tie::IxHash', 'svc' => 'svc_broadband', 'desc' => 'Export broadband service/account to Saisei', 'options' => \%options, + 'scripts' => \%scripts, 'notes' => <<'END', This is a customer integration with Saisei. This will setup a rate plan and tie -the rate plan to a host via the Saisei API when the broadband service is provisioned. -It will also untie the rate plan via the API upon unprovisioning of the broadband service. -<P>This export will use the broadband service descriptive label for the Saisei rate plan name and -will use the email from the first contact for the Saisei username that will be -attached to this rate plan. It will use the Saisei default Access Point. +the rate plan to a host and access point via the Saisei API when the broadband service is provisioned. +It will also untie the rate plan via the API upon unprovisioning of the broadband service. +<P> +This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified. +This will also create and modify a access point at Saisei as soon as the tower is created or modified. +<P> +To use this export, follow the below instructions: <P> -Required Fields: +<OL> +<LI> +Add a new export and fill out required fields: <UL> <LI>Hostname or IP - <I>Host name to Saisei API</I></LI> <LI>Port - <I>Port number to Saisei API</I></LI> <LI>User Name - <I>Saisei API user name</I></LI> <LI>Password - <I>Saisei API password</I></LI> </UL> +</LI> +<P> +<LI> +Create a broadband service. The broadband service name will become the Saisei rate plan name. +Set the upload and download speed for the service. This is required to be able to export the service to Saisei. +Attach above created Saisei export to this broadband service. +</LI> +<P> +<LI> +Create a tower and add a sector to that tower. The sector name will be the name of the access point, +Make sure you have set the up and down rate limit for the Tower and Sector. This is required to be able to export the access point. +</LI> +<P> +<LI> +Create a package for the above created broadband service, and order this package for a customer. +</LI> +<P> +<LI> +When you provision the service, enter the ip address associated to this service and select the Tower and Sector for it's access point. +This provisioned service will then be exported as a host to Saisei. +<P> +when you un provision this service, the host entry at Saisei will be deleted. +</LI> +</OL> +<P> +When setting this up, if you wish to export your allready provisioned services, make sure the broadband service has this export attached and +on export edit screen there will be a link to export Provisioned Services attached to this export. Clicking on that will export all services +not currently exported to Saisei. END ); sub _export_insert { my ($self, $svc_broadband) = @_; - my $rateplan_name = $svc_broadband->{Hash}->{description}; - $rateplan_name =~ s/\s/_/g; - - # load needed info from our end - my $cust_main = $svc_broadband->cust_main; - return "Could not load service customer" unless $cust_main; - my $conf = new FS::Conf; - - # get policy list - my $policies = $self->api_get_policies(); + my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ); + my $rateplan_name = $service_part->{Hash}->{svc}; + $rateplan_name =~ s/\s/_/g; # check for existing rate plan my $existing_rateplan; @@ -94,24 +148,18 @@ sub _export_insert { # if no existing rate plan create one and modify it. $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan; - $self->api_modify_rateplan($policies->{collection}, $svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan); + $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan); + return $self->api_error if $self->{'__saisei_error'}; # set rateplan to existing one or newly created one. my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name); - my @email = map { $_->emailaddress } FS::Record::qsearch({ - 'table' => 'contact', - 'select' => 'emailaddress', - 'addl_from' => ' JOIN contact_email USING (contactnum)', - 'hashref' => { 'custnum' => $cust_main->{Hash}->{custnum}, }, - }); - my $username = $email[0]; - my $description = $cust_main->{Hash}->{first}." ".$cust_main->{Hash}->{last}; + my $username = $svc_broadband->{Hash}->{svcnum}; + my $description = $svc_broadband->{Hash}->{description}; if (!$username) { $self->{'__saisei_error'} = 'no username - can not export'; - warn "No email found $username\n" if $self->option('debug'); - return; + return $self->api_error; } else { # check for existing user. @@ -120,42 +168,75 @@ sub _export_insert { # if no existing user create one. $self->api_create_user($username, $description) unless $existing_user; + return $self->api_error if $self->{'__saisei_error'}; # set user to existing one or newly created one. my $user = $existing_user ? $existing_user : $self->api_get_user($username); - ## add access point ? - - ## tie host to user - $self->api_add_host_to_user($user->{collection}->[0]->{name}, $rateplan->{collection}->[0]->{name}, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'}; + ## add access point + my $tower_sector = FS::Record::qsearchs({ + 'table' => 'tower_sector', + 'select' => 'tower.towername, + tower.up_rate_limit as tower_upratelimit, + tower.down_rate_limit as tower_downratelimit, + tower_sector.sectorname, + tower_sector.up_rate_limit as sector_upratelimit, + tower_sector.down_rate_limit as sector_downratelimit ', + 'addl_from' => 'LEFT JOIN tower USING ( towernum )', + 'hashref' => { + 'sectornum' => $svc_broadband->{Hash}->{sectornum}, + }, + }); + + my $tower_name = $tower_sector->{Hash}->{towername}; + $tower_name =~ s/\s/_/g; + + my $tower_opt = { + 'tower_name' => $tower_name, + 'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit}, + 'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit}, + }; + + my $tower_ap = process_tower($self, $tower_opt); + return $self->api_error if $self->{'__saisei_error'}; + + my $sector_name = $tower_sector->{Hash}->{sectorname}; + $sector_name =~ s/\s/_/g; + + my $sector_opt = { + 'tower_name' => $tower_name, + 'sector_name' => $sector_name, + 'sector_uprate_limit' => $tower_sector->{Hash}->{sector_upratelimit}, + 'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit}, + }; + my $accesspoint = process_sector($self, $sector_opt); + return $self->api_error if $self->{'__saisei_error'}; + + ## tie host to user add sector name as access point. + $self->api_add_host_to_user( + $user->{collection}->[0]->{name}, + $rateplan->{collection}->[0]->{name}, + $svc_broadband->{Hash}->{ip_addr}, + $accesspoint->{collection}->[0]->{name}, + ) unless $self->{'__saisei_error'}; } - return ''; + return $self->api_error; } sub _export_replace { - my ($self, $svc_phone) = @_; + my ($self, $svc_broadband) = @_; return ''; } sub _export_delete { my ($self, $svc_broadband) = @_; - my $cust_main = $svc_broadband->cust_main; - return "Could not load service customer" unless $cust_main; - my $conf = new FS::Conf; - - my $rateplan_name = $svc_broadband->{Hash}->{description}; + my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ); + my $rateplan_name = $service_part->{Hash}->{svc}; $rateplan_name =~ s/\s/_/g; - - my @email = map { $_->emailaddress } FS::Record::qsearch({ - 'table' => 'contact', - 'select' => 'emailaddress', - 'addl_from' => ' JOIN contact_email USING (contactnum)', - 'hashref' => { 'custnum' => $cust_main->{Hash}->{custnum}, }, - }); - my $username = $email[0]; + my $username = $svc_broadband->{Hash}->{svcnum}; ## tie host to user $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'}; @@ -164,15 +245,81 @@ sub _export_delete { } sub _export_suspend { - my ($self, $svc_phone) = @_; + my ($self, $svc_broadband) = @_; return ''; } sub _export_unsuspend { - my ($self, $svc_phone) = @_; + my ($self, $svc_broadband) = @_; return ''; } +sub export_partsvc { + my ($self, $svc_part) = @_; + + my $rateplan_name = $svc_part->{Hash}->{svc}; + $rateplan_name =~ s/\s/_/g; + my $speeddown = $svc_part->{Hash}->{svc_broadband__speed_down}; + my $speedup = $svc_part->{Hash}->{svc_broadband__speed_up}; + + my $temp_svc = $svc_part->{Hash}; + my $svc_broadband = {}; + map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; } } keys %$temp_svc; + + # check for existing rate plan + my $existing_rateplan; + $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'}; + + # Modify the existing rate plan with new service data. + $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan); + + # if no existing rate plan create one and modify it. + $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan; + $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan); + + return $self->api_error; + +} + +sub export_tower_sector { + my ($self, $tower) = @_; + + #modify tower or create it. + my $tower_name = $tower->{Hash}->{towername}; + $tower_name =~ s/\s/_/g; + my $tower_opt = { + 'tower_name' => $tower_name, + 'tower_uprate_limit' => $tower->{Hash}->{up_rate_limit}, + 'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit}, + 'modify_existing' => '1', # modify an existing access point with this info + }; + + my $tower_access_point = process_tower($self, $tower_opt); + + #get list of all access points + my $hash_opt = { + 'table' => 'tower_sector', + 'select' => '*', + 'hashref' => { 'towernum' => $tower->{Hash}->{towernum}, }, + }; + + #for each one modify or create it. + foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) { + my $sector_name = $tower_sector->{Hash}->{sectorname}; + $sector_name =~ s/\s/_/g; + my $sector_opt = { + 'tower_name' => $tower_name, + 'sector_name' => $sector_name, + 'sector_uprate_limit' => $tower_sector->{Hash}->{up_rate_limit}, + 'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit}, + 'modify_existing' => '1', # modify an existing access point with this info + }; + my $sector_access_point = process_sector($self, $sector_opt); + } + + return $self->api_error; +} + =head1 Saisei API These methods allow access to the Saisei API using the credentials @@ -191,6 +338,7 @@ Returns empty on failure; retrieve error messages using L</api_error>. sub api_call { my ($self,$method,$path,$params) = @_; + $self->{'__saisei_error'} = ''; my $auth_info = $self->option('username') . ':' . $self->option('password'); $params ||= {}; @@ -218,7 +366,8 @@ sub api_call { } } else { - $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent(); + $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent() + unless ($method eq "GET"); warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug'); return; } @@ -229,7 +378,7 @@ sub api_call { =head2 api_error -Returns the error string set by L</PortaOne API> methods, +Returns the error string set by L</Saisei API> methods, or a blank string if most recent call produced no errors. =cut @@ -253,7 +402,7 @@ sub api_get_policies { $self->{'__saisei_error'} = "Did not receive any global policies" unless $get_policies; - return $get_policies; + return $get_policies->{collection}; } =head2 api_get_rateplan @@ -268,8 +417,6 @@ sub api_get_rateplan { my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan"); return if $self->api_error; - $self->{'__saisei_error'} = "Did not receive any rateplan info" - unless $get_rateplan; return $get_rateplan; } @@ -286,8 +433,6 @@ sub api_get_user { my $get_user = $self->api_call("GET", "/users/$user"); return if $self->api_error; - $self->{'__saisei_error'} = "Did not receive any user info" - unless $get_user; return $get_user; } @@ -300,14 +445,29 @@ Gets user info for specific access point. sub api_get_accesspoint { my $self = shift; - my $accesspoint; + my $accesspoint = shift; my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint"); return if $self->api_error; - $self->{'__saisei_error'} = "Did not receive any user info" - unless $get_accesspoint; - return; + return $get_accesspoint; +} + +=head2 api_get_host + +Gets user info for specific host. + +=cut + +sub api_get_host { + my $self = shift; + my $ip = shift; + + my $get_host = $self->api_call("GET", "/hosts/$ip"); + + return if $self->api_error; + + return $get_host; } =head2 api_create_rateplan @@ -319,6 +479,9 @@ Creates a rateplan. sub api_create_rateplan { my ($self, $svc, $rateplan) = @_; + $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down}; + $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up}; + my $new_rateplan = $self->api_call( "PUT", "/rate_plans/$rateplan", @@ -326,22 +489,26 @@ sub api_create_rateplan { 'downstream_rate' => $svc->{Hash}->{speed_down}, 'upstream_rate' => $svc->{Hash}->{speed_up}, }, - ); + ) unless $self->{'__saisei_error'}; $self->{'__saisei_error'} = "Rate Plan not created" - unless $new_rateplan; # should never happen + unless ($new_rateplan || $self->{'__saisei_error'}); + return $new_rateplan; } =head2 api_modify_rateplan -Modify a rateplan. +Modify a new rateplan. =cut sub api_modify_rateplan { - my ($self,$policies,$svc,$rateplan_name) = @_; + my ($self,$svc,$rateplan_name) = @_; + + # get policy list + my $policies = $self->api_get_policies(); foreach my $policy (@$policies) { my $policyname = $policy->{name}; @@ -357,8 +524,8 @@ sub api_modify_rateplan { }, ); - $self->{'__saisei_error'} = "Rate Plan not modified" - unless $modified_rateplan; # should never happen + $self->{'__saisei_error'} = "Rate Plan not modified after create" + unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen } @@ -366,6 +533,31 @@ sub api_modify_rateplan { } +=head2 api_modify_existing_rateplan + +Modify a existing rateplan. + +=cut + +sub api_modify_existing_rateplan { + my ($self,$svc,$rateplan_name) = @_; + + my $modified_rateplan = $self->api_call( + "PUT", + "/rate_plans/$rateplan_name", + { + 'downstream_rate' => $svc->{Hash}->{speed_down}, + 'upstream_rate' => $svc->{Hash}->{speed_up}, + }, + ); + + $self->{'__saisei_error'} = "Rate Plan not modified" + unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen + + return; + +} + =head2 api_create_user Creates a user. @@ -384,7 +576,7 @@ sub api_create_user { ); $self->{'__saisei_error'} = "User not created" - unless $new_user; # should never happen + unless ($new_user || $self->{'__saisei_error'}); # should never happen return $new_user; @@ -397,19 +589,70 @@ Creates a access point. =cut sub api_create_accesspoint { - my ($self,$accesspoint) = @_; + my ($self,$accesspoint, $upratelimit, $downratelimit) = @_; # this has not been tested, but should work, if needed. - #my $new_accesspoint = $self->api_call( - # "PUT", - # "/access_points/$accesspoint", - # { - # 'description' => 'my description', - # }, - #); - - #$self->{'__saisei_error'} = "Access point not created" - # unless $new_accesspoint; # should never happen + my $new_accesspoint = $self->api_call( + "PUT", + "/access_points/$accesspoint", + { + 'downstream_rate_limit' => $downratelimit, + 'upstream_rate_limit' => $upratelimit, + }, + ); + + $self->{'__saisei_error'} = "Access point not created" + unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen + return; + +} + +=head2 api_modify_accesspoint + +Modify a new access point. + +=cut + +sub api_modify_accesspoint { + my ($self, $accesspoint, $uplink) = @_; + + my $modified_accesspoint = $self->api_call( + "PUT", + "/access_points/$accesspoint", + { + 'uplink' => $uplink, # name of attached access point + }, + ); + + $self->{'__saisei_error'} = "Rate Plan not modified" + unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen + + return; + +} + +=head2 api_modify_existing_accesspoint + +Modify a existing accesspoint. + +=cut + +sub api_modify_existing_accesspoint { + my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_; + + my $modified_accesspoint = $self->api_call( + "PUT", + "/access_points/$accesspoint", + { + 'downstream_rate_limit' => $downratelimit, + 'upstream_rate_limit' => $upratelimit, +# 'uplink' => $uplink, # name of attached access point + }, + ); + + $self->{'__saisei_error'} = "Access point not modified" + unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen + return; } @@ -421,7 +664,7 @@ ties host to user, rateplan and default access point. =cut sub api_add_host_to_user { - my ($self,$user, $rateplan, $ip) = @_; + my ($self,$user, $rateplan, $ip, $accesspoint) = @_; my $new_host = $self->api_call( "PUT", @@ -429,11 +672,12 @@ sub api_add_host_to_user { { 'user' => $user, 'rate_plan' => $rateplan, + 'access_point' => $accesspoint, }, ); $self->{'__saisei_error'} = "Host not created" - unless $new_host; # should never happen + unless ($new_host || $self->{'__saisei_error'}); # should never happen return $new_host; @@ -466,12 +710,114 @@ sub api_delete_host_to_user { ); $self->{'__saisei_error'} = "Host not created" - unless $delete_host; # should never happen + unless ($delete_host || $self->{'__saisei_error'}); # should never happen return $delete_host; } +sub process_tower { + my ($self, $opt) = @_; + + my $existing_tower_ap; + my $tower_name = $opt->{tower_name}; + + #check if tower has been set up as an access point. + $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'}; + + # modify the existing accesspoint if changing tower . + $self->api_modify_existing_accesspoint ( + $tower_name, + '', # tower does not have a uplink on sectors. + $opt->{tower_uprate_limit}, + $opt->{tower_downrate_limit}, + ) if $existing_tower_ap && $opt->{modify_existing}; + + #if tower does not exist as an access point create it. + $self->api_create_accesspoint( + $tower_name, + $opt->{tower_uprate_limit}, + $opt->{tower_downrate_limit} + ) unless $existing_tower_ap; + + my $accesspoint = $self->api_get_accesspoint($tower_name); + + return $accesspoint; +} + +sub process_sector { + my ($self, $opt) = @_; + + my $existing_sector_ap; + my $sector_name = $opt->{sector_name}; + + #check if sector has been set up as an access point. + $existing_sector_ap = $self->api_get_accesspoint($sector_name); + + # modify the existing accesspoint if changing sector . + $self->api_modify_existing_accesspoint ( + $sector_name, + $opt->{tower_name}, + $opt->{sector_uprate_limit}, + $opt->{sector_downrate_limit}, + ) if $existing_sector_ap && $opt->{modify_existing}; + + #if sector does not exist as an access point create it. + $self->api_create_accesspoint( + $sector_name, + $opt->{sector_uprate_limit}, + $opt->{sector_downrate_limit}, + ) unless $existing_sector_ap; + + # Attach newly created sector to it's tower. + $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap); + + # set access point to existing one or newly created one. + my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name); + + return $accesspoint; +} + +sub export_provisioned_services { + my $job = shift; + my $param = shift; + + my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } ) + or die "unknown exportnum $param->{export_provisioned_services_exportnum}"; + bless $part_export; + + my @svcparts = FS::Record::qsearch({ + 'table' => 'export_svc', + 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ', + 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, }, + }); + my $part_count = scalar @svcparts; + + my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts; + + my @svcs = FS::Record::qsearch({ + 'table' => 'cust_svc', + 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ', + 'extra_sql' => " WHERE svcpart in ('".$parts."')", + }) unless !$parts; + + my $svc_count = scalar @svcs; + + my %status = {}; + for (my $c=10; $c <=100; $c=$c+10) { $status{int($svc_count * ($c/100))} = $c; } + + my $process_count=0; + foreach my $svc (@svcs) { + if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); } + ## check if service exists as host if not export it. + _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr}); + $process_count++; + } + + return; + +} + =head1 SEE ALSO L<FS::part_export> diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm index 60889c6af..81652c115 100644 --- a/FS/FS/part_svc.pm +++ b/FS/FS/part_svc.pm @@ -514,6 +514,18 @@ sub part_export_dsl_pull { grep $_->can('dsl_pull'), $self->part_export; } +=item part_export_partsvc + +Returns a list of any exports (see L<FS::part_export>) for this service that +are capable of pushing a change after part svc is changed. + +=cut + +sub part_export_partsvc { + my $self = shift; + grep $_->can('export_partsvc'), $self->part_export; +} + =item cust_svc [ PKGPART ] Returns a list of associated customer services (FS::cust_svc records). @@ -905,6 +917,11 @@ sub process { ); die "$error\n" if $error; + + foreach my $part_svc_export ( $new->part_export_partsvc ) { + $error = $part_svc_export->export_partsvc($new); + } + return $error if $error; } =item process_bulk_cust_svc diff --git a/FS/FS/tower.pm b/FS/FS/tower.pm index 5497c7217..d835f7bc3 100644 --- a/FS/FS/tower.pm +++ b/FS/FS/tower.pm @@ -44,6 +44,14 @@ Tower name Disabled flag, empty or 'Y' +=item up_rate_limit + +Up Rate limit for towner + +=item down_rate_limit + +Down Rate limit for tower + =back =head1 METHODS @@ -97,6 +105,8 @@ sub check { || $self->ut_floatn('height') || $self->ut_floatn('veg_height') || $self->ut_alphan('color') + || $self->ut_numbern('up_rate_limit') + || $self->ut_numbern('down_rate_limit') ; return $error if $error; diff --git a/FS/FS/tower_sector.pm b/FS/FS/tower_sector.pm index 6ccfe553d..350fce1fd 100644 --- a/FS/FS/tower_sector.pm +++ b/FS/FS/tower_sector.pm @@ -2,7 +2,7 @@ package FS::tower_sector; use strict; use base qw( FS::Record ); -use FS::Record qw( qsearch qsearchs ); +use FS::Record qw( dbh qsearch qsearchs ); use FS::tower; use FS::svc_broadband; use Class::Load qw(load_class); @@ -93,6 +93,18 @@ The coverage map, as a PNG. The coordinate boundaries of the coverage map. +=item title + +The sector title. + +=item up_rate_limit + +Up rate limit for sector. + +=item down_rate_limit + +down rate limit for sector. + =back =head1 METHODS @@ -155,6 +167,8 @@ sub check { || $self->ut_numbern('downtilt') || $self->ut_floatn('sector_range') || $self->ut_numbern('margin') + || $self->ut_numbern('up_rate_limit') + || $self->ut_numbern('down_rate_limit') || $self->ut_anything('image') || $self->ut_sfloatn('west') || $self->ut_sfloatn('east') @@ -260,6 +274,25 @@ sub queue_generate_coverage { =over 4 +=item part_export_svc_broadband + +Returns all svc_broadband exports. + +=cut + +sub part_export_svc_broadband { + my $info = $FS::part_export::exports{'svc_broadband'} or return; + my @exporttypes = map { dbh->quote($_) } keys %$info or return; + qsearch({ + 'table' => 'part_export', + 'extra_sql' => 'WHERE exporttype IN(' . join(',', @exporttypes) . ')' + }); +} + +=back + +=over 4 + =item process_generate_coverage JOB, PARAMS Queueable routine to fetch the sector coverage map from the tower mapping diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi index 5411feb5f..f6ec208be 100644 --- a/httemplate/edit/part_export.cgi +++ b/httemplate/edit/part_export.cgi @@ -290,6 +290,20 @@ my $widget = new HTML::Widgets::SelectLayers( $html .= ' CHECKED' if $part_export->no_suspend eq 'Y'; $html .= '></TD></TR>'; + foreach my $script ( keys %{$exports->{$layer}{scripts}} ) { + $html .= '<TR><TD ALIGN="left" COLSPAN=2>' . + include('/elements/progress-init.html', + $part_export->exporttype, + [ $script.'_exportnum', $script.'_script' ], + rooturl().'view/svc_export/run_script.cgi', + rooturl().'edit/part_export.cgi?'.$part_export->{Hash}->{exportnum}, + $script, + ) . + '<INPUT TYPE="hidden" NAME="'.$script.'_exportnum" VALUE="'.$part_export->{Hash}->{exportnum}.'"> + <INPUT TYPE="hidden" NAME="'.$script.'_script" VALUE="'.$script.'"> + <A HREF="#" onClick="'.$script.'process();">'.$exports->{$layer}{scripts}{$script}->{html_label}.'</A></TD></TR>'; + } + $html .= '</TABLE>'; # false laziness with config_element above diff --git a/httemplate/edit/process/elements/process.html b/httemplate/edit/process/elements/process.html index 60aaf749a..e96fa0c4f 100644 --- a/httemplate/edit/process/elements/process.html +++ b/httemplate/edit/process/elements/process.html @@ -459,6 +459,14 @@ foreach my $value ( @values ) { } +if ($class eq "FS::tower") { + foreach my $part_svc_broadband_export ( FS::tower_sector->part_export_svc_broadband ) { + if ($part_svc_broadband_export and $part_svc_broadband_export->can('export_tower_sector')) { + $error = $part_svc_broadband_export->export_tower_sector($new); + } + } +} + # set up redirect URLs my $redirect; diff --git a/httemplate/edit/process/tower.html b/httemplate/edit/process/tower.html index d14ac56f8..ba7309c99 100644 --- a/httemplate/edit/process/tower.html +++ b/httemplate/edit/process/tower.html @@ -5,7 +5,7 @@ 'fields' => [qw( sectorname ip_addr height freq_mhz direction width downtilt v_width margin - sector_range + sector_range up_rate_limit down_rate_limit )], }, &> diff --git a/httemplate/edit/tower.html b/httemplate/edit/tower.html index 377a33e9b..b9fea779f 100644 --- a/httemplate/edit/tower.html +++ b/httemplate/edit/tower.html @@ -12,6 +12,8 @@ 'altitude', 'height', 'veg_height', + 'up_rate_limit', + 'down_rate_limit', { field => 'sectornum', type => 'tower_sector', o2m_table => 'tower_sector', @@ -30,6 +32,8 @@ 'height' => 'Height (feet)', 'veg_height' => 'Vegetation height (feet)', 'color' => 'Color', + 'up_rate_limit' => 'Up Rate Limit(Kbps)', + 'down_rate_limit' => 'Down Rate Limit(Kbps)', }, &> <%init> @@ -38,7 +42,8 @@ my $m2_error_callback = sub { # reconstruct the list my ($cgi, $object) = @_; my @fields = qw( - sectorname ip_addr height freq_mhz direction width tilt v_width margin sector_range + sectorname ip_addr height freq_mhz direction width tilt v_width margin + sector_range up_rate_limit down_rate_limit ); map { diff --git a/httemplate/elements/tower_sector.html b/httemplate/elements/tower_sector.html index 987177582..722c5d74e 100644 --- a/httemplate/elements/tower_sector.html +++ b/httemplate/elements/tower_sector.html @@ -61,6 +61,8 @@ tie my %label, 'Tie::IxHash', 'v_width' => 'Vert. width', 'sector_range' => 'Range', 'margin' => 'Signal margin (dB)', + 'up_rate_limit' => 'Up rate limit', + 'down_rate_limit' => 'Down rate limit', ; my @fields = keys %label; diff --git a/httemplate/elements/tr-tower_sectors.html b/httemplate/elements/tr-tower_sectors.html new file mode 100644 index 000000000..8acedb84b --- /dev/null +++ b/httemplate/elements/tr-tower_sectors.html @@ -0,0 +1,310 @@ +<%shared> +# kind of a hack... +my ($export) = FS::tower_sector->part_export; +my $antenna_types; # will be an ordered hash +if ($export and $export->can('get_antenna_types')) { + $antenna_types = $export->get_antenna_types; +} +</%shared> +<%init> +my %opt = @_; +my $tower = $opt{'object'}; +my $towernum = $tower->towernum; +my $cgi = $opt{'cgi'}; + +my $tabcounter = 0; + +my @fields = qw( + sectorname ip_addr height freq_mhz direction width downtilt v_width + db_high db_low sector_range + power line_loss antenna_gain hardware_typenum up_rate_limit down_rate_limit +); + +my @sectors; +if ( $cgi->param('error') ) { + foreach my $k ($cgi->param) { + if ($k =~ /^sectornum\d+$/) { + my $sectornum = $cgi->param($k); + my $sector = FS::tower_sector->new({ + 'sectornum' => $sectornum, + 'towernum' => $towernum, + map { $_ => scalar($cgi->param($k.'_'.$_)) } @fields, + }); + push @sectors, $sector if length($sector->sectorname); + } + } +} elsif ( $towernum ) { + @sectors = $tower->tower_sector; +} # else new mode, no sectors yet + +my $id = $opt{id} || $opt{field} || 'sectornum'; + +</%init> +<& tablebreak-tr-title.html, value => 'Sectors' &> + +<style> + .ui-tabs-nav a { + padding: 6px 9px; + font-weight: bold; + } + .ui-tabs-nav li { + border-top-left-radius: 0.5em; + border-top-right-radius: 0.5em; + } + .ui-tabs-active li { + border-bottom-color: #fff; + } + .ui-tabs { + font-weight: bold; + } + .ui-tabs label { + padding-top: 3px; + width: 140px; + display: inline-block; + text-align: right; + } + .ui-tabs input, .ui-spinner { + border: 1px solid #666; + border-radius: 2px; + font-size: 13.3px; + text-align: right; + font-weight: normal; + padding: 1px; + } + .ui-tabs input { /* but not spinner, messes it up */ + margin-left: 1px; + margin-right: 1px; + } + .ui-tabs input:focus { + border-color: #7e0079; + background-color: #ffffdd; + } + .ui-spinner input { /* use the spinner's border and padding */ + border: none; + text-align: left; + } + .ui-tabs p { + margin-top: 8px; + margin-bottom: 8px; + } + +</style> + + +<tr> + <td colspan=2> +%# prototypes + <div style="display: none"> +<& .tab, id => $id . '_P' &> +<& .panel, id => $id . '_P' &> + </div> + +%# main container + <div id="<% $id %>_tabs"> + <ul> +% foreach my $sector (@sectors) { +<& .tab, sector => $sector, id => $id . $tabcounter &> +% $tabcounter++; +% } + </ul> + +% $tabcounter = 0; +% foreach my $sector (@sectors) { +<& .panel, sector => $sector, id => $id . $tabcounter &> +% $tabcounter++; +% } + </div> + </td> +</tr> +<script> +$(function() { + var tabcounter = <% $tabcounter %>; + var id = <% $id |js_string %>; + //create tab bar + var tabs = $( '#'+id+'_tabs' ).tabs(); + + function changedSectorName() { + var this_panel = $(this).closest('div'); + var this_tab = tabs.find('#' + this_panel.prop('id') + '_tab'); + // if this is the last panel, make a new one + if (this_panel.next().length == 0) { + addSector(); + } + // and update the current tab's text with the sector name + this_tab.find('a').text($(this).val()); + } + + var tab_proto = $('#'+id+'_P_tab'); + var panel_proto = $('#'+id+'_P'); + + function addSector() { + var new_tab = tab_proto.clone(); + var new_panel = panel_proto.clone(); + // replace proto placeholder with the counter value, in all id and + // name properties in new_panel and its children + new_panel.add( new_panel.find('*') ).each(function() { + this.id = this.id.replace('_P', tabcounter); + if (this.name) { + this.name = this.name.replace('_P', tabcounter); + } + }); + tabcounter++; + // and set the handler up on it + new_panel.find('.input-sectorname').on('change', changedSectorName); + + // also update the tab itself + new_tab.find('a').prop('href', '#' + new_panel.prop('id')); + new_tab.prop('id', new_panel.prop('id') + '_tab'); + + tabs.append(new_panel); + tabs.children('ul:first').append(new_tab); + + tabs.tabs('refresh'); + } + + $('.dbspinner').spinner({ step: 5 }); + + $('.input-sectorname').on('change', changedSectorName); + addSector(); + +}); +</script> +<%def .tab> +% my %opt = @_; +% my $sector = $opt{sector}; +% my $id = $opt{id}; +% my $title = $sector ? $sector->sectorname : mt('Add new'); + <li id="<% $id %>_tab"> + <a href="#<% $id %>"><% $title |h %></a> + </li> +</%def> +<%def .panel> +% my %opt = @_; +% my $sector = $opt{sector} || FS::tower_sector->new({}); +% my $id = $opt{id}; # sectornumX +<div id="<% $id %>"> +% # no id on this one, the panel gets the "sectornumX" id + <input type="hidden" name="<% $id %>" value="<% $sector->sectornum |h %>"> + <p> + <label><% emt('Sector name') %></label> + <input style="text-align: left" + class="input-sectorname" + id="<% $id %>_sectorname" + name="<% $id %>_sectorname" + value="<% $sector->sectorname |h %>"> + + <label><% emt('IP address') %></label> + <input style="text-align: left" + id="<% $id %>_ip_addr" + name="<% $id %>_ip_addr" + value="<% $sector->ip_addr |h %>"> + </p> + <p> + <label for="<% $id %>_height"><% emt('Antenna height') %></label> + <input size="3" + id="<% $id %>_height" + name="<% $id %>_height" + value="<% $sector->height |h %>"> + <% emt('feet above ground') %> + </p> + <p> + <label for="<% $id %>_direction"><% emt('Azimuth') %></label> + <input size="3" + id="<% $id %>_direction" + name="<% $id %>_direction" + value="<% $sector->direction |h %>">° + <label for="<% $id %>_downtilt"><% emt('Down tilt') %></label> + <input size="2" + id="<% $id %>_downtilt" + name="<% $id %>_downtilt" + value="<% $sector->downtilt |h %>">° + </p> + + <p> + <label for="<% $id %>_freq_mhz"><% emt('Frequency') %></label> + <input size="4" + id="<% $id %>_freq_mhz" + name="<% $id %>_freq_mhz" + value="<% $sector->freq_mhz |h %>"> + <% emt('MHz') %> + </p> + + <p> + <label for="<% $id %>_power"><% emt('Transmit power') %></label> + <input size="3" + id="<% $id %>_power" + name="<% $id %>_power" + value="<% $sector->power |h %>"> + <% emt('dBm') %><br> + <label for="<% $id %>_antenna_gain">+ </label> + <input size="3" + id="<% $id %>_antenna_gain" + name="<% $id %>_antenna_gain" + value="<% $sector->antenna_gain |h %>"> + <% emt('dB antenna gain') %><br> + <label for="<% $id %>_line_loss">– </label> + <input size="3" + id="<% $id %>_line_loss" + name="<% $id %>_line_loss" + value="<% $sector->line_loss |h %>"> + <% emt('dB line loss') %> + +% if ( $antenna_types ) { + <p> + <label for="<% $id %>_hardware_typenum"><% emt('Antenna type') %></label> + <& /elements/select.html, + field => $id.'_hardware_typenum', + options => [ '', keys %$antenna_types ], + labels => $antenna_types, + curr_value => $sector->hardware_typenum, + &> + </p> +% } +% # this next section might not be necessary if you enter an antenna type + <p> + <label for="<% $id %>_width"><% emt('Horizontal beam') %></label> + <input size="3" + id="<% $id %>_width" + name="<% $id %>_width" + value="<% $sector->width |h %>">° + <label for="<% $id %>_v_width"><% emt('Vertical beam') %></label> + <input size="2" + id="<% $id %>_v_width" + name="<% $id %>_v_width" + value="<% $sector->v_width |h %>">° + </p> + + <label><% emt('Signal margin') %></label> + <div style="display: inline-block; vertical-align: top"> + <input class="dbspinner" + size="4" + id="<% $id %>_db_high" + name="<% $id %>_db_high" + value="<% $sector->db_high |h %>"> + <% emt('dB (high quality)') %> + <br> + + <input class="dbspinner" + size="4" + id="<% $id %>_db_low" + name="<% $id %>_db_low" + value="<% $sector->db_low |h %>"> + <% emt('dB (low quality)') %> + </div> + <p> + <label><% emt('Up Rate (Kbps)') %></label> + <input style="text-align: left" + id="<% $id %>_up_rate_limit" + name="<% $id %>_up_rate_limit" + value="<% $sector->up_rate_limit |h %>"> + </p> + <p> + <label><% emt('Down Rate (Kbps)') %></label> + <input style="text-align: left" + id="<% $id %>_down_rate_limit" + name="<% $id %>_down_rate_limit" + value="<% $sector->down_rate_limit |h %>"> + </p> + +</div> +</%def> diff --git a/httemplate/view/svc_export/run_script.cgi b/httemplate/view/svc_export/run_script.cgi new file mode 100644 index 000000000..ba58bbdd7 --- /dev/null +++ b/httemplate/view/svc_export/run_script.cgi @@ -0,0 +1,31 @@ +<% $server->process %> +<%init> + +my @args = $cgi->param('arg'); +my %param = (); + while ( @args ) { + my( $field, $value ) = splice(@args, 0, 2); + unless ( exists( $param{$field} ) ) { + $param{$field} = $value; + } elsif ( ! ref($param{$field}) ) { + $param{$field} = [ $param{$field}, $value ]; + } else { + push @{$param{$field}}, $value; + } + } + +my $exportnum; +my $method; +for (grep /^*_script$/, keys %param) { + $exportnum = $param{$param{$_}.'_exportnum'}; + $method = $param{$param{$_}.'_script'}; +} + +my $part_export = qsearchs('part_export', { 'exportnum'=> $exportnum, } ) + or die "unknown exportnum $exportnum"; + +my $class = 'FS::part_export::'.$part_export->{Hash}->{exporttype}.'::'.$method; + +my $server = new FS::UI::Web::JSRPC $class, $cgi; + +</%init>
\ No newline at end of file |