1 package FS::part_export::saisei;
4 use vars qw( @ISA %info );
5 use base qw( FS::part_export );
6 use Date::Format 'time2str';
18 FS::part_export::saisei
22 Saisei integration for Freeside
26 This export offers basic svc_broadband provisioning for Saisei.
28 This is a customer integration with Saisei. This will set up a rate plan and tie
29 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
30 It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
32 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
33 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
35 To use this export, follow the below instructions:
37 Create a new service definition and set the table to svc_broadband. The service name will become the Saisei rate plan name.
38 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
39 Attach this Saisei export to this service.
41 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
42 Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point.
43 The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside.
44 Each sector will be attached to its tower access point using the Saisei uplink field.
45 Each access point will be attached to the interface set in the export config. If left blank access point will be attached to the default interface. Most setups can leave this blank.
47 Create a package for the above created service, and order this package for a customer.
49 Provision the service, making sure to enter the IP address associated with this service and select the tower and sector for it's access point.
50 This provisioned service will then be exported as a host to Saisei.
52 Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
54 After this export is set up and attached to a service, you can export the already provisioned services by clicking the link Export provisioned services attached to this export.
55 Clicking on this link will export all services attached to this export not currently exported to Saisei.
57 This module also provides generic methods for working through the L</Saisei API>.
61 tie my %scripts, 'Tie::IxHash',
62 'export_provisioned_services' => { component => '/elements/popup_link.html',
63 label => 'Export provisioned services',
64 description => 'will export provisioned services of part service with Saisei export attached.',
65 html_label => '<b>Export provisioned services attached to this export.</b>',
66 error_url => '/edit/part_export.cgi?',
67 success_message => 'Saisei export of provisioned services successful',
69 'export_all_towers_sectors' => { component => '/elements/popup_link.html',
70 label => 'Export of all towers and sectors',
71 description => 'Will force an export of all towers and sectors to Saisei as access points.',
72 html_label => '<b>Export all towers and sectors.</b>',
73 error_url => '/edit/part_export.cgi?',
74 success_message => 'Saisei export of towers and sectors as access points successful',
78 tie my %options, 'Tie::IxHash',
79 'port' => { label => 'Port',
81 'username' => { label => 'Saisei API User Name',
83 'password' => { label => 'Saisei API Password',
85 'interface' => { label => 'Saisei Access Point Interface',
87 'debug' => { type => 'checkbox',
88 label => 'Enable debug warnings' },
92 'svc' => 'svc_broadband',
93 'desc' => 'Export broadband service/account to Saisei',
94 'options' => \%options,
95 'scripts' => \%scripts,
97 This is a customer integration with Saisei. This will set up a rate plan and tie
98 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
99 It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
101 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
102 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
104 To use this export, follow the below instructions:
108 Create a new service definition and set the table to svc_broadband. The service name will become the Saisei rate plan name.
109 Set the upload speed, download speed, and tower to be required for the service. This is required to be able to export the service to Saisei.
110 Attach this Saisei export to this service.
114 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
115 Make sure you have set the up and down rate limit for the tower and the sector. This is required to be able to export the access point.
116 The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector. They will be modified at Saisei when modified in freeside.
117 Each sector will be attached to its tower access point using the Saisei uplink field.
118 Each access point will be attached to the interface set in the export config. If left blank access point will be attached to the default interface. Most setups can leave this blank.
122 Create a package for the above created service, and order this package for a customer.
126 Provision the service, making sure to enter the IP address associated with this service, the upload and download speed are correct, and select the tower and sector for it's access point.
127 This provisioned service will then be exported as a host to Saisei.
129 Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <i>none</i>.
133 After this export is set up and attached to a service, you can export the already provisioned services by clicking the link <b>Export provisioned services attached to this export</b>.
134 Clicking on this link will export all services attached to this export not currently exported to Saisei.
138 <A HREF="http://www.freeside.biz/mediawiki/index.php/Saisei_provisioning_export" target="_new">Documentation</a>
143 my ($self, $svc_broadband) = @_;
145 my $rateplan_name = $self->get_rateplan_name($svc_broadband);
147 # check for existing rate plan
148 my $existing_rateplan;
149 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
151 # if no existing rate plan create one and modify it.
152 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
153 $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
154 return $self->api_error if $self->{'__saisei_error'};
156 # set rateplan to existing one or newly created one.
157 my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
159 my $username = $svc_broadband->{Hash}->{svcnum};
160 my $description = $svc_broadband->{Hash}->{description};
162 $svc_location = $svc_broadband->{Hash}->{latitude}.','.$svc_broadband->{Hash}->{longitude} if ($svc_broadband->{Hash}->{latitude} && $svc_broadband->{Hash}->{longitude});
165 $self->{'__saisei_error'} = 'no username - can not export';
166 return $self->api_error;
169 # check for existing user.
171 $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
173 # if no existing user create one.
174 $self->api_create_user($username, $description, $svc_location) unless $existing_user;
175 return $self->api_error if $self->{'__saisei_error'};
177 # set user to existing one or newly created one.
178 my $user = $existing_user ? $existing_user : $self->api_get_user($username);
181 my $tower_sector = FS::Record::qsearchs({
182 'table' => 'tower_sector',
183 'select' => 'tower.towername,
184 tower.up_rate_limit as tower_upratelimit,
185 tower.down_rate_limit as tower_downratelimit,
186 tower_sector.sectorname,
187 tower_sector.towernum,
188 tower_sector.up_rate_limit as sector_upratelimit,
189 tower_sector.down_rate_limit as sector_downratelimit,
192 'addl_from' => 'LEFT JOIN tower USING ( towernum )',
194 'sectornum' => $svc_broadband->{Hash}->{sectornum},
199 $tower_location = $tower_sector->{Hash}->{latitude}.','.$tower_sector->{Hash}->{longitude} if ($tower_sector->{Hash}->{latitude} && $tower_sector->{Hash}->{longitude});
201 my $tower_name = $tower_sector->{Hash}->{towername};
202 $tower_name =~ s/\s/_/g;
205 'tower_name' => $tower_name,
206 'tower_num' => $tower_sector->{Hash}->{towernum},
207 'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit},
208 'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
210 $tower_opt->{'location'} = $tower_location if $tower_location;
212 my $tower_ap = process_tower($self, $tower_opt);
213 return $self->api_error if $self->{'__saisei_error'};
215 my $sector_name = $tower_sector->{Hash}->{sectorname};
216 $sector_name =~ s/\s/_/g;
219 'tower_name' => $tower_name,
220 'tower_num' => $tower_sector->{Hash}->{towernum},
221 'sector_name' => $sector_name,
222 'sector_uprate_limit' => $tower_sector->{Hash}->{sector_upratelimit},
223 'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
224 'rateplan' => $rateplan_name,
226 $sector_opt->{'location'} = $tower_location if $tower_location;
228 my $accesspoint = process_sector($self, $sector_opt);
229 return $self->api_error if $self->{'__saisei_error'};
231 ## get custnum and pkgpart from cust_pkg for virtual access point
232 my $cust_pkg = FS::Record::qsearchs({
233 'table' => 'cust_pkg',
234 'hashref' => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, },
236 my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up};
238 my $virtual_ap_opt = {
239 'virtual_name' => $virtual_ap_name,
240 'sector_name' => $sector_name,
241 'virtual_uprate_limit' => $svc_broadband->{Hash}->{speed_up},
242 'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down},
244 my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt);
245 return $self->api_error if $self->{'__saisei_error'};
247 ## tie host to user add sector name as access point.
249 'user' => $user->{collection}->[0]->{name},
250 'rateplan' => $rateplan->{collection}->[0]->{name},
251 'ip' => $svc_broadband->{Hash}->{ip_addr},
252 'accesspoint' => $virtual_ap->{collection}->[0]->{name},
253 'location' => $svc_location,
255 $self->api_add_host_to_user($host_opt)
256 unless $self->{'__saisei_error'};
259 return $self->api_error;
263 sub _export_replace {
264 my ($self, $svc_broadband) = @_;
265 my $error = $self->_export_insert($svc_broadband);
270 my ($self, $svc_broadband) = @_;
272 my $rateplan_name = $self->get_rateplan_name($svc_broadband);
274 my $username = $svc_broadband->{Hash}->{svcnum};
276 ## untie host to user
277 $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
282 sub _export_suspend {
283 my ($self, $svc_broadband) = @_;
287 sub _export_unsuspend {
288 my ($self, $svc_broadband) = @_;
293 my ($self, $svc_part) = @_;
295 if ( $FS::svc_Common::noexport_hack ) {
296 carp 'export_partsvc() suppressed by noexport_hack'
297 if $self->option('debug');
302 if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
303 for my $type (qw( down up )) {
304 my $speed_type = "broadband_".$type."stream";
305 foreach my $pkg_svc (FS::Record::qsearch({
306 'table' => 'pkg_svc',
307 'select' => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
308 'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
309 'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
310 })) { $fcc_477_speeds->{
311 $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
315 $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
316 $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
319 foreach my $key (keys %$fcc_477_speeds) {
321 $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
322 $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
323 $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
324 $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
326 my $temp_svc = $svc_part->{Hash};
327 my $svc_broadband = {};
328 map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; } } keys %$temp_svc;
330 my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
332 # check for existing rate plan
333 my $existing_rateplan;
334 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
336 # Modify the existing rate plan with new service data.
337 $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
339 # if no existing rate plan create one and modify it.
340 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
341 $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
345 return $self->api_error;
349 sub export_tower_sector {
350 my ($self, $tower) = @_;
352 if ( $FS::svc_Common::noexport_hack ) {
353 carp 'export_tower_sector() suppressed by noexport_hack'
354 if $self->option('debug');
359 $tower_location = $tower->{Hash}->{latitude}.','.$tower->{Hash}->{longitude} if ($tower->{Hash}->{latitude} && $tower->{Hash}->{longitude});
361 #modify tower or create it.
362 my $tower_name = $tower->{Hash}->{towername};
363 $tower_name =~ s/\s/_/g;
365 'tower_name' => $tower_name,
366 'tower_num' => $tower->{Hash}->{towernum},
367 'tower_uprate_limit' => $tower->{Hash}->{up_rate_limit},
368 'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
369 'modify_existing' => '1', # modify an existing access point with this info
371 $tower_opt->{'location'} = $tower_location if $tower_location;
373 my $tower_access_point = process_tower($self, $tower_opt);
374 return $tower_access_point if $tower_access_point->{error};
376 #get list of all access points
378 'table' => 'tower_sector',
380 'hashref' => { 'towernum' => $tower->{Hash}->{towernum}, },
383 #for each one modify or create it.
384 foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
385 next if $tower_sector->{Hash}->{sectorname} eq "_default";
386 my $sector_name = $tower_sector->{Hash}->{sectorname};
387 $sector_name =~ s/\s/_/g;
389 'tower_name' => $tower_name,
390 'tower_num' => $tower_sector->{Hash}->{towernum},
391 'sector_name' => $sector_name,
392 'sector_uprate_limit' => $tower_sector->{Hash}->{up_rate_limit},
393 'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
394 'modify_existing' => '1', # modify an existing access point with this info
396 $sector_opt->{'location'} = $tower_location if $tower_location;
398 my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default");
399 return $sector_access_point if $sector_access_point->{error};
402 return { error => $self->api_error, };
405 ## creates the rateplan name
406 sub get_rateplan_name {
407 my ($self, $svc_broadband, $svc_name) = @_;
409 my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
410 my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
412 my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
413 $rateplan_name =~ s/\s/_/g; $rateplan_name =~ s/[^A-Za-z0-9\-_]//g;
415 return $rateplan_name;
420 These methods allow access to the Saisei API using the credentials
421 set in the export options.
427 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
428 Places an api call to the specified path and method with the specified params.
429 Returns the decoded json object returned by the api call.
430 Returns empty on failure; retrieve error messages using L</api_error>.
435 my ($self,$method,$path,$params) = @_;
437 $self->{'__saisei_error'} = '';
438 my $auth_info = $self->option('username') . ':' . $self->option('password');
441 warn "Calling $method on http://"
442 .$self->{Hash}->{machine}.':'.$self->option('port')
443 ."/rest/top/configurations/running/$path\n" if $self->option('debug');
445 my $data = encode_json($params) if keys %{ $params };
447 my $client = REST::Client->new();
448 $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
449 $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
450 $client->$method('/rest/top/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
452 warn "Saisei Response Code is ".$client->responseCode()."\n" if $self->option('debug');
456 if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
457 eval { $result = decode_json($client->responseContent()) };
459 $self->{'__saisei_error'} = "There was an error decoding the JSON data from Saisei. Bad JSON data logged in error log if debug option was set.";
460 warn "Saisei RC 201 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
464 elsif ($client->responseCode() eq '404') {
465 eval { $result = decode_json($client->responseContent()) };
467 $self->{'__saisei_error'} = "There was an error decoding the JSON data from Saisei. Bad JSON data logged in error log if debug option was set.";
468 warn "Saisei RC 404 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
471 ## check if message is for empty hash.
472 my($does_not_exist) = $result->{message} =~ /'(.*)' does not exist$/;
473 $self->{'__saisei_error'} = "Saisei Error: ".$result->{message} unless $does_not_exist;
474 warn "Saisei Response Content is\n".$client->responseContent."\n" if ($self->option('debug') && !$does_not_exist);
477 elsif ($client->responseCode() eq '500') {
478 $self->{'__saisei_error'} = "Could not connect to the Saisei export host machine (".$self->{Hash}->{machine}.':'.$self->option('port').") during $method , we received the responce code: " . $client->responseCode();
479 warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
483 $self->{'__saisei_error'} = "Received Bad response from server during $method $path $data, we received responce code: " . $client->responseCode() . " " . $client->responseContent;
484 warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
494 Returns the error string set by L</Saisei API> methods,
495 or a blank string if most recent call produced no errors.
501 return $self->{'__saisei_error'} || '';
504 =head2 api_get_policies
506 Gets a list of global policies.
510 sub api_get_policies {
513 my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
514 return if $self->api_error;
515 $self->{'__saisei_error'} = "Did not receive any global policies from Saisei."
516 unless $get_policies;
518 return $get_policies->{collection};
521 =head2 api_get_rateplan
523 Gets rateplan info for specific rateplan.
527 sub api_get_rateplan {
529 my $rateplan = shift;
531 my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
532 return if $self->api_error;
534 return $get_rateplan;
539 Gets user info for specific user.
547 my $get_user = $self->api_call("GET", "/users/$user");
548 return if $self->api_error;
553 =head2 api_get_accesspoint
555 Gets user info for specific access point.
559 sub api_get_accesspoint {
561 my $accesspoint = shift;
563 my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
564 return if $self->api_error;
566 return $get_accesspoint;
571 Gets user info for specific host.
579 my $get_host = $self->api_call("GET", "/hosts/$ip");
581 return { message => $self->api_error, } if $self->api_error;
586 =head2 api_create_rateplan
592 sub api_create_rateplan {
593 my ($self, $svc, $rateplan) = @_;
595 $self->{'__saisei_error'} = "There is no download speed set for the service !--service,".$svc->{Hash}->{svcnum}.",".$rateplan."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a download speed set for them." if !$svc->{Hash}->{speed_down};
596 $self->{'__saisei_error'} = "There is no upload speed set for the service !--service,".$svc->{Hash}->{svcnum}.",".$rateplan."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a upload speed set for them." if !$svc->{Hash}->{speed_up};
598 my $new_rateplan = $self->api_call(
600 "/rate_plans/$rateplan",
602 'downstream_rate' => $svc->{Hash}->{speed_down},
603 'upstream_rate' => $svc->{Hash}->{speed_up},
605 ) unless $self->{'__saisei_error'};
607 $self->{'__saisei_error'} = "Saisei could not create the rate plan $rateplan."
608 unless ($new_rateplan || $self->{'__saisei_error'});
610 return $new_rateplan;
614 =head2 api_modify_rateplan
616 Modify a new rateplan.
620 sub api_modify_rateplan {
621 my ($self,$svc,$rateplan_name) = @_;
624 my $policies = $self->api_get_policies();
626 foreach my $policy (@$policies) {
627 my $policyname = $policy->{name};
628 my $rate_multiplier = '';
629 if ($policy->{background}) { $rate_multiplier = ".01"; }
630 my $modified_rateplan = $self->api_call(
632 "/rate_plans/$rateplan_name/partitions/$policyname",
634 'restricted' => $policy->{assured}, # policy_assured_flag
635 'rate_multiplier' => $rate_multiplier, # policy_background 0.1
636 'rate' => $policy->{percent_rate}, # policy_percent_rate
640 $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name after it was created."
641 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
649 =head2 api_modify_existing_rateplan
651 Modify a existing rateplan.
655 sub api_modify_existing_rateplan {
656 my ($self,$svc,$rateplan_name) = @_;
658 $self->{'__saisei_error'} = "There is no download speed set for the service !--service,".$svc->{Hash}->{svcnum}.",".$rateplan_name."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a download speed set for them." if !$svc->{Hash}->{speed_down};
659 $self->{'__saisei_error'} = "There is no upload speed set for the service !--service,".$svc->{Hash}->{svcnum}.",".$rateplan_name."--! with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a upload speed set for them." if !$svc->{Hash}->{speed_up};
661 my $modified_rateplan = $self->api_call(
663 "/rate_plans/$rateplan_name",
665 'downstream_rate' => $svc->{Hash}->{speed_down},
666 'upstream_rate' => $svc->{Hash}->{speed_up},
670 $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name."
671 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
677 =head2 api_create_user
683 sub api_create_user {
684 my ($self,$user, $description, $location) = @_;
687 'description' => $description,
689 $user_hash->{'map_location'} = $location if $location;
691 my $new_user = $self->api_call(
697 $self->{'__saisei_error'} = "Saisei could not create the user $user"
698 unless ($new_user || $self->{'__saisei_error'}); # should never happen
704 =head2 api_create_accesspoint
706 Creates a access point.
710 sub api_create_accesspoint {
711 my ($self,$accesspoint, $upratelimit, $downratelimit, $location) = @_;
714 'downstream_rate_limit' => $downratelimit,
715 'upstream_rate_limit' => $upratelimit,
716 'interface' => $self->option('interface'),
718 $ap_hash->{'map_location'} = $location if $location;
720 my $new_accesspoint = $self->api_call(
722 "/access_points/$accesspoint",
726 $self->{'__saisei_error'} = "Saisei could not create the access point $accesspoint"
727 unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
732 =head2 api_modify_accesspoint
734 Modify a new access point.
738 sub api_modify_accesspoint {
739 my ($self, $accesspoint, $uplink, $location) = @_;
743 'interface' => $self->option('interface'),
745 $ap_hash->{'map_location'} = $location if $location;
747 my $modified_accesspoint = $self->api_call(
749 "/access_points/$accesspoint",
753 $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint after it was created."
754 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
760 =head2 api_modify_existing_accesspoint
762 Modify a existing accesspoint.
766 sub api_modify_existing_accesspoint {
767 my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit, $location) = @_;
770 'downstream_rate_limit' => $downratelimit,
771 'upstream_rate_limit' => $upratelimit,
772 'interface' => $self->option('interface'),
773 # 'uplink' => $uplink, # name of attached access point
775 $ap_hash->{'map_location'} = $location if $location;
777 my $modified_accesspoint = $self->api_call(
779 "/access_points/$accesspoint",
783 $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint."
784 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
790 =head2 api_add_host_to_user
792 ties host to user, rateplan and default access point.
796 sub api_add_host_to_user {
797 # my ($self,$user, $rateplan, $ip, $accesspoint, $location) = @_;
798 my ($self,$opt) = @_;
799 my $ip = $opt->{'ip'};
800 my $location = $opt->{'location'};
803 'user' => $opt->{'user'},
804 'rate_plan' => $opt->{'rateplan'},
805 'access_point' => $opt->{'accesspoint'},
807 $newhost_hash->{'map_location'} = $location if $location;
809 my $new_host = $self->api_call(
815 $self->{'__saisei_error'} = "Saisei could not create the host $ip"
816 unless ($new_host || $self->{'__saisei_error'}); # should never happen
822 =head2 api_delete_host_to_user
824 unties host from user and rateplan.
825 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
829 sub api_delete_host_to_user {
830 my ($self,$user, $rateplan, $ip) = @_;
832 my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
833 return if $self->api_error;
834 $self->{'__saisei_error'} = "Can not delete the host as Saisei did not return a default rate plan. Please make sure Saisei has a default rateplan setup."
835 unless $default_rate_plan;
837 my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
839 my $delete_host = $self->api_call(
844 'access_point' => '<none>',
845 'rate_plan' => $default_rateplan_name,
849 $self->{'__saisei_error'} = "Saisei could not delete the host $ip"
850 unless ($delete_host || $self->{'__saisei_error'}); # should never happen
857 my ($self, $opt) = @_;
859 if (!$opt->{tower_uprate_limit} || !$opt->{tower_downrate_limit}) {
860 $self->{'__saisei_error'} = "Could not export tower !--tower,".$opt->{tower_num}.",".$opt->{tower_name}."--! because there was no up or down rates attached to the tower. Saisei requires a up and down rate be attached to each tower.";
861 return { error => $self->api_error, };
864 my $existing_tower_ap;
865 my $tower_name = $opt->{tower_name};
866 my $location = $opt->{location};
868 #check if tower has been set up as an access point.
869 $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
871 # modify the existing accesspoint if changing tower .
872 $self->api_modify_existing_accesspoint (
874 '', # tower does not have a uplink on sectors.
875 $opt->{tower_uprate_limit},
876 $opt->{tower_downrate_limit},
878 ) if $existing_tower_ap->{collection} && $opt->{modify_existing};
880 #if tower does not exist as an access point create it.
881 $self->api_create_accesspoint(
883 $opt->{tower_uprate_limit},
884 $opt->{tower_downrate_limit},
886 ) unless $existing_tower_ap->{collection};
888 my $accesspoint = $self->api_get_accesspoint($tower_name);
890 return { error => $self->api_error, } if $self->api_error;
895 my ($self, $opt) = @_;
897 if (!$opt->{sector_name} || $opt->{sector_name} eq '_default') {
898 $self->{'__saisei_error'} = "No sector attached to Tower (".$opt->{tower_name}.") for service ".$opt->{'rateplan'}.". Saisei requires a tower sector to be attached to each service that is exported to Saisei.";
899 return { error => $self->api_error, };
902 if (!$opt->{sector_uprate_limit} || !$opt->{sector_downrate_limit}) {
903 $self->{'__saisei_error'} = "Could not export sector !--tower,".$opt->{tower_num}.",".$opt->{sector_name}."--! because there was no up or down rates attached to the sector. Saisei requires a up and down rate be attached to each sector.";
904 return { error => $self->api_error, };
907 my $existing_sector_ap;
908 my $sector_name = $opt->{sector_name};
909 my $location = $opt->{location};
911 #check if sector has been set up as an access point.
912 $existing_sector_ap = $self->api_get_accesspoint($sector_name);
914 # modify the existing accesspoint if changing sector .
915 $self->api_modify_existing_accesspoint (
918 $opt->{sector_uprate_limit},
919 $opt->{sector_downrate_limit},
921 ) if $existing_sector_ap && $opt->{modify_existing};
923 #if sector does not exist as an access point create it.
924 $self->api_create_accesspoint(
926 $opt->{sector_uprate_limit},
927 $opt->{sector_downrate_limit},
929 ) unless $existing_sector_ap;
931 # Attach newly created sector to it's tower.
932 $self->api_modify_accesspoint($sector_name, $opt->{tower_name}, $location) unless ($self->{'__saisei_error'} || $existing_sector_ap);
934 # set access point to existing one or newly created one.
935 my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
937 return { error => $self->api_error, } if $self->api_error;
941 =head2 require_tower_and_sector
943 sets whether the service export requires a sector with it's tower.
947 sub require_tower_and_sector {
951 =head2 tower_sector_required_fields
953 required fields needed for tower and sector export.
957 sub tower_sector_required_fields {
960 'up_rate_limit' => '1',
961 'down_rate_limit' => '1',
964 'up_rate_limit' => '1',
965 'down_rate_limit' => '1',
972 sub required_fields {
973 my @fields = ('svc_broadband__ip_addr_required', 'svc_broadband__speed_up_required', 'svc_broadband__speed_down_required', 'svc_broadband__sectornum_required');
977 sub process_virtual_ap {
978 my ($self, $opt) = @_;
980 my $existing_virtual_ap;
981 my $virtual_name = $opt->{virtual_name};
983 #check if virtual_ap has been set up as an access point.
984 $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
986 # modify the existing virtual accesspoint if changing it. this should never happen
987 $self->api_modify_existing_accesspoint (
990 $opt->{virtual_uprate_limit},
991 $opt->{virtual_downrate_limit},
992 ) if $existing_virtual_ap && $opt->{modify_existing};
994 #if virtual ap does not exist as an access point create it.
995 $self->api_create_accesspoint(
997 $opt->{virtual_uprate_limit},
998 $opt->{virtual_downrate_limit},
999 ) unless $existing_virtual_ap;
1002 if ($existing_virtual_ap && (ref $existing_virtual_ap->{collection}->[0]->{uplink} eq "HASH") && ($existing_virtual_ap->{collection}->[0]->{uplink}->{link}->{name} ne $opt->{sector_name})) {
1006 # Attach newly created virtual ap to tower sector ap or if sector has changed.
1007 $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
1009 # set access point to existing one or newly created one.
1010 my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
1012 return $accesspoint;
1015 sub export_provisioned_services {
1019 my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1020 or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n";
1023 my @svcparts = FS::Record::qsearch({
1024 'table' => 'export_svc',
1025 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ',
1026 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
1028 my $part_count = scalar @svcparts;
1030 my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1032 my @svcs = FS::Record::qsearch({
1033 'table' => 'cust_svc',
1034 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
1035 'extra_sql' => " WHERE svcpart in ('".$parts."')",
1038 my $svc_count = scalar @svcs;
1041 for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
1043 my $process_count=0;
1044 foreach my $svc (@svcs) {
1045 if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1046 ## check if service exists as host if not export it.
1047 my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
1048 die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message};
1049 warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1050 my $export_error = _export_insert($part_export,$svc) unless $host->{collection};
1051 if ($export_error) {
1052 warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1053 die ("$export_error\n");
1062 sub export_all_towers_sectors {
1066 my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1067 or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n";
1070 my @towers = FS::Record::qsearch({
1073 my $tower_count = scalar @towers;
1076 for (my $c=1; $c <=100; $c=$c+1) { $status{int($tower_count * ($c/100))} = $c; }
1078 my $process_count=0;
1079 foreach my $tower (@towers) {
1080 if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1081 my $export_error = export_tower_sector($part_export,$tower);
1082 if ($export_error->{'error'}) {
1083 warn "Error exporting tower/sector (".$tower->{Hash}->{towername}.")\n" if ($part_export->option('debug'));
1084 die ($export_error->{'error'}."\n");
1093 sub test_export_report {
1094 my ($self, $opts) = @_;
1097 ## check all part services for export errors
1098 my @exports = FS::Record::qsearch('part_export', { 'exporttype' => "saisei", } );
1099 my $export_nums = join "', '", map { $_->{Hash}->{exportnum} } @exports;
1101 my $svc_part_export_error;
1102 my @svcparts = FS::Record::qsearch({
1103 'table' => 'part_svc',
1104 'addl_from' => 'LEFT JOIN export_svc USING ( svcpart ) ',
1105 'extra_sql' => " WHERE export_svc.exportnum in ('".$export_nums."')",
1107 my $part_count = scalar @svcparts;
1110 foreach (@svcparts) {
1111 my $part_error->{'description'} = $_->svc;
1112 $part_error->{'link'} = $opts->{'fsurl'}."/edit/part_svc.cgi?".$_->svcpart;
1114 foreach my $s ('speed_up', 'speed_down') {
1115 my $speed = $_->part_svc_column($s);
1116 if ($speed->columnflag eq "" || $speed->columnflag eq "D") {
1117 $part_error->{'errors'}->{$speed->columnname} = "Field ".$speed->columnname." is not set to be required and can be set while provisioning the service." unless $speed->required eq "Y";
1119 elsif ($speed->columnflag eq "F" || $speed->columnflag eq "S") {
1120 $part_error->{'errors'}->{$speed->columnname} = "Field ".$speed->columnname." is set to auto fill while provisioning the service but there is no value set." unless $speed->columnvalue;
1122 elsif ($speed->columnflag eq "P") {
1123 my $fcc_speed_name = "broadband_".$speed->columnvalue."stream";
1124 foreach my $part_pkg ( FS::Record::qsearchs({
1125 'table' => 'part_pkg',
1126 'addl_from' => 'LEFT JOIN pkg_svc USING ( pkgpart ) ',
1127 'extra_sql' => " WHERE pkg_svc.svcpart = ".$_->svcpart,
1129 my $pkglink = '<a href="'.$opts->{'fsurl'}.'/edit/part_pkg.cgi?'.$part_pkg->pkgpart.'"><FONT COLOR="red"><B>'.$part_pkg->pkg.'</B></FONT></a>';
1130 $part_error->{'errors'}->{$speed->columnname} = "Field ".$speed->columnname." is set to package FCC 477 information, but package ".$pkglink." does not have FCC ".$fcc_speed_name." set."
1131 unless $part_pkg->fcc_option($fcc_speed_name);
1135 $part_error->{'errors'}->{'ip_addr'} = "Field IP Address is not set to required" if $_->part_svc_column("ip_addr")->required ne "Y";
1136 $svc_part_error->{$_->svcpart} = $part_error if $part_error->{'errors'};
1139 $svc_part_export_error->{"services"}->{'description'} = "Service definitions";
1140 $svc_part_export_error->{"services"}->{'count'} = $part_count;
1141 $svc_part_export_error->{"services"}->{'errors'} = $svc_part_error if $svc_part_error;
1143 push @export_error, $svc_part_export_error;
1145 ## check all provisioned cust services for export errors
1146 my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1147 my $cust_svc_export_error;
1148 my @svcs = FS::Record::qsearch({
1149 'table' => 'cust_svc',
1150 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
1151 'extra_sql' => " WHERE svcpart in ('".$parts."')",
1153 my $svc_count = scalar @svcs;
1157 my $svc_error->{'description'} = $_->description;
1158 $svc_error->{'link'} = $opts->{'fsurl'}."/edit/svc_broadband.cgi?".$_->svcnum;
1160 foreach my $s ('speed_up', 'speed_down', 'ip_addr') {
1161 $svc_error->{'errors'}->{$s} = "Field ".$s." is not set and is required for this service to be exported to Saisei." unless $_->$s;
1164 my $sector = FS::Record::qsearchs({
1165 'table' => 'tower_sector',
1166 'extra_sql' => " WHERE sectornum = ".$_->sectornum." AND sectorname != '_default'",
1167 }) if $_->sectornum;
1169 $svc_error->{'errors'}->{'sectornum'} = "No tower sector is set for this service. There needs to be a tower and sector set to be exported to Saisei.";
1172 foreach my $s ('up_rate_limit', 'down_rate_limit') {
1173 $svc_error->{'errors'}->{'sectornum'} = "The sector ".$sector->description." does not have a ".$s." set. The sector needs a ".$s." set to be exported to Saisei."
1177 $cust_svc_error->{$_->svcnum} = $svc_error if $svc_error->{'errors'};
1180 $cust_svc_export_error->{"provisioned_services"}->{'description'} = "Provisioned services";
1181 $cust_svc_export_error->{"provisioned_services"}->{'count'} = $svc_count;
1182 $cust_svc_export_error->{"provisioned_services"}->{'errors'} = $cust_svc_error if $cust_svc_error;
1184 push @export_error, $cust_svc_export_error;
1187 ## check all towers and sectors for export errors
1188 my $tower_sector_export_error;
1189 my @towers = FS::Record::qsearch({
1192 my $tower_count = scalar @towers;
1196 my $tower_error->{'description'} = $_->towername;
1197 $tower_error->{'link'} = $opts->{'fsurl'}."/edit/tower.html?".$_->towernum;
1199 foreach my $s ('up_rate_limit', 'down_rate_limit') {
1200 $tower_error->{'errors'}->{$s} = "Field ".$s." is not set for the tower, this is required for this tower to be exported to Saisei." unless $_->$s;
1203 my @sectors = FS::Record::qsearch({
1204 'table' => 'tower_sector',
1205 'extra_sql' => " WHERE towernum = ".$_->towernum." AND sectorname != '_default' AND (up_rate_limit IS NULL OR down_rate_limit IS NULL)",
1207 foreach my $sector (@sectors) {
1208 foreach my $s ('up_rate_limit', 'down_rate_limit') {
1209 $tower_error->{'errors'}->{'sector_'.$s} = "The sector ".$sector->description." does not have a ".$s." set. The sector needs a ".$s." set to be exported to Saisei."
1213 $towers_error->{$_->towernum} = $tower_error if $tower_error->{'errors'};
1216 $tower_sector_export_error->{"tower_sector"}->{'description'} = "Tower / Sector";
1217 $tower_sector_export_error->{"tower_sector"}->{'count'} = $tower_count;
1218 $tower_sector_export_error->{"tower_sector"}->{'errors'} = $towers_error if $towers_error;
1220 push @export_error, $tower_sector_export_error;
1222 return [@export_error];