1 package FS::part_export::saisei;
4 use vars qw( @ISA %info );
5 use base qw( FS::part_export );
6 use Date::Format 'time2str';
17 FS::part_export::saisei
21 Saisei integration for Freeside
25 This export offers basic svc_broadband provisioning for Saisei.
27 This is a customer integration with Saisei. This will set up a rate plan and tie
28 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
29 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.
31 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
32 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
34 To use this export, follow the below instructions:
36 Create a new service definition and set the table to svc_broadband. The service name will become the Saisei rate plan name.
37 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
38 Attach this Saisei export to this service.
40 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
41 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.
42 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.
43 Each sector will be attached to its tower access point using the Saisei uplink field.
45 Create a package for the above created service, and order this package for a customer.
47 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.
48 This provisioned service will then be exported as a host to Saisei.
50 Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
52 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.
53 Clicking on this link will export all services attached to this export not currently exported to Saisei.
55 This module also provides generic methods for working through the L</Saisei API>.
59 tie my %scripts, 'Tie::IxHash',
60 'export_provisioned_services' => { component => '/elements/popup_link.html',
61 label => 'Export provisioned services',
62 description => 'will export provisioned services of part service with Saisei export attached.',
63 html_label => '<b>Export provisioned services attached to this export.</b>',
67 tie my %options, 'Tie::IxHash',
68 'port' => { label => 'Port',
70 'username' => { label => 'Saisei API User Name',
72 'password' => { label => 'Saisei API Password',
74 'debug' => { type => 'checkbox',
75 label => 'Enable debug warnings' },
79 'svc' => 'svc_broadband',
80 'desc' => 'Export broadband service/account to Saisei',
81 'options' => \%options,
82 'scripts' => \%scripts,
84 This is a customer integration with Saisei. This will set up a rate plan and tie
85 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
86 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.
88 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
89 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
91 To use this export, follow the below instructions:
95 Create a new service definition and set the table to svc_broadband. The service name will become the Saisei rate plan name.
96 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
97 Attach this Saisei export to this service.
101 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
102 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.
103 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.
104 Each sector will be attached to its tower access point using the Saisei uplink field.
108 Create a package for the above created service, and order this package for a customer.
112 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.
113 This provisioned service will then be exported as a host to Saisei.
115 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>.
119 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>.
120 Clicking on this link will export all services attached to this export not currently exported to Saisei.
129 my ($self, $svc_broadband) = @_;
131 my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
132 my $rateplan_name = $service_part->{Hash}->{svc};
133 $rateplan_name =~ s/\s/_/g;
135 # check for existing rate plan
136 my $existing_rateplan;
137 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
139 # if no existing rate plan create one and modify it.
140 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
141 $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
142 return $self->api_error if $self->{'__saisei_error'};
144 # set rateplan to existing one or newly created one.
145 my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
147 my $username = $svc_broadband->{Hash}->{svcnum};
148 my $description = $svc_broadband->{Hash}->{description};
151 $self->{'__saisei_error'} = 'no username - can not export';
152 return $self->api_error;
155 # check for existing user.
157 $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
159 # if no existing user create one.
160 $self->api_create_user($username, $description) unless $existing_user;
161 return $self->api_error if $self->{'__saisei_error'};
163 # set user to existing one or newly created one.
164 my $user = $existing_user ? $existing_user : $self->api_get_user($username);
167 my $tower_sector = FS::Record::qsearchs({
168 'table' => 'tower_sector',
169 'select' => 'tower.towername,
170 tower.up_rate_limit as tower_upratelimit,
171 tower.down_rate_limit as tower_downratelimit,
172 tower_sector.sectorname,
173 tower_sector.up_rate_limit as sector_upratelimit,
174 tower_sector.down_rate_limit as sector_downratelimit ',
175 'addl_from' => 'LEFT JOIN tower USING ( towernum )',
177 'sectornum' => $svc_broadband->{Hash}->{sectornum},
181 my $tower_name = $tower_sector->{Hash}->{towername};
182 $tower_name =~ s/\s/_/g;
185 'tower_name' => $tower_name,
186 'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit},
187 'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
190 my $tower_ap = process_tower($self, $tower_opt);
191 return $self->api_error if $self->{'__saisei_error'};
193 my $sector_name = $tower_sector->{Hash}->{sectorname};
194 $sector_name =~ s/\s/_/g;
197 'tower_name' => $tower_name,
198 'sector_name' => $sector_name,
199 'sector_uprate_limit' => $tower_sector->{Hash}->{sector_upratelimit},
200 'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
202 my $accesspoint = process_sector($self, $sector_opt);
203 return $self->api_error if $self->{'__saisei_error'};
205 ## tie host to user add sector name as access point.
206 $self->api_add_host_to_user(
207 $user->{collection}->[0]->{name},
208 $rateplan->{collection}->[0]->{name},
209 $svc_broadband->{Hash}->{ip_addr},
210 $accesspoint->{collection}->[0]->{name},
211 ) unless $self->{'__saisei_error'};
214 return $self->api_error;
218 sub _export_replace {
219 my ($self, $svc_broadband) = @_;
224 my ($self, $svc_broadband) = @_;
226 my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
227 my $rateplan_name = $service_part->{Hash}->{svc};
228 $rateplan_name =~ s/\s/_/g;
229 my $username = $svc_broadband->{Hash}->{svcnum};
231 ## untie host to user
232 $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
237 sub _export_suspend {
238 my ($self, $svc_broadband) = @_;
242 sub _export_unsuspend {
243 my ($self, $svc_broadband) = @_;
248 my ($self, $svc_part) = @_;
250 my $rateplan_name = $svc_part->{Hash}->{svc};
251 $rateplan_name =~ s/\s/_/g;
252 my $speeddown = $svc_part->{Hash}->{svc_broadband__speed_down};
253 my $speedup = $svc_part->{Hash}->{svc_broadband__speed_up};
255 my $temp_svc = $svc_part->{Hash};
256 my $svc_broadband = {};
257 map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; } } keys %$temp_svc;
259 # check for existing rate plan
260 my $existing_rateplan;
261 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
263 # Modify the existing rate plan with new service data.
264 $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
266 # if no existing rate plan create one and modify it.
267 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
268 $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
270 return $self->api_error;
274 sub export_tower_sector {
275 my ($self, $tower) = @_;
277 #modify tower or create it.
278 my $tower_name = $tower->{Hash}->{towername};
279 $tower_name =~ s/\s/_/g;
281 'tower_name' => $tower_name,
282 'tower_uprate_limit' => $tower->{Hash}->{up_rate_limit},
283 'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
284 'modify_existing' => '1', # modify an existing access point with this info
287 my $tower_access_point = process_tower($self, $tower_opt);
289 #get list of all access points
291 'table' => 'tower_sector',
293 'hashref' => { 'towernum' => $tower->{Hash}->{towernum}, },
296 #for each one modify or create it.
297 foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
298 my $sector_name = $tower_sector->{Hash}->{sectorname};
299 $sector_name =~ s/\s/_/g;
301 'tower_name' => $tower_name,
302 'sector_name' => $sector_name,
303 'sector_uprate_limit' => $tower_sector->{Hash}->{up_rate_limit},
304 'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
305 'modify_existing' => '1', # modify an existing access point with this info
307 my $sector_access_point = process_sector($self, $sector_opt);
310 return $self->api_error;
315 These methods allow access to the Saisei API using the credentials
316 set in the export options.
322 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
323 Places an api call to the specified path and method with the specified params.
324 Returns the decoded json object returned by the api call.
325 Returns empty on failure; retrieve error messages using L</api_error>.
330 my ($self,$method,$path,$params) = @_;
332 $self->{'__saisei_error'} = '';
333 my $auth_info = $self->option('username') . ':' . $self->option('password');
336 warn "Calling $method on http://"
337 .$self->{Hash}->{machine}.':'.$self->option('port')
338 ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
340 my $data = encode_json($params) if keys %{ $params };
342 my $client = REST::Client->new();
343 $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
344 $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
345 $client->$method('/rest/stm/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
347 warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
351 if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
352 eval { $result = decode_json($client->responseContent()) };
354 $self->{'__saisei_error'} = "Error decoding json: $@";
359 $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent()
360 unless ($method eq "GET");
361 warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
371 Returns the error string set by L</Saisei API> methods,
372 or a blank string if most recent call produced no errors.
378 return $self->{'__saisei_error'} || '';
381 =head2 api_get_policies
383 Gets a list of global policies.
387 sub api_get_policies {
390 my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
391 return if $self->api_error;
392 $self->{'__saisei_error'} = "Did not receive any global policies"
393 unless $get_policies;
395 return $get_policies->{collection};
398 =head2 api_get_rateplan
400 Gets rateplan info for specific rateplan.
404 sub api_get_rateplan {
406 my $rateplan = shift;
408 my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
409 return if $self->api_error;
411 return $get_rateplan;
416 Gets user info for specific user.
424 my $get_user = $self->api_call("GET", "/users/$user");
425 return if $self->api_error;
430 =head2 api_get_accesspoint
432 Gets user info for specific access point.
436 sub api_get_accesspoint {
438 my $accesspoint = shift;
440 my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
441 return if $self->api_error;
443 return $get_accesspoint;
448 Gets user info for specific host.
456 my $get_host = $self->api_call("GET", "/hosts/$ip");
458 return if $self->api_error;
463 =head2 api_create_rateplan
469 sub api_create_rateplan {
470 my ($self, $svc, $rateplan) = @_;
472 $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
473 $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
475 my $new_rateplan = $self->api_call(
477 "/rate_plans/$rateplan",
479 'downstream_rate' => $svc->{Hash}->{speed_down},
480 'upstream_rate' => $svc->{Hash}->{speed_up},
482 ) unless $self->{'__saisei_error'};
484 $self->{'__saisei_error'} = "Rate Plan not created"
485 unless ($new_rateplan || $self->{'__saisei_error'});
487 return $new_rateplan;
491 =head2 api_modify_rateplan
493 Modify a new rateplan.
497 sub api_modify_rateplan {
498 my ($self,$svc,$rateplan_name) = @_;
501 my $policies = $self->api_get_policies();
503 foreach my $policy (@$policies) {
504 my $policyname = $policy->{name};
505 my $rate_multiplier = '';
506 if ($policy->{background}) { $rate_multiplier = ".01"; }
507 my $modified_rateplan = $self->api_call(
509 "/rate_plans/$rateplan_name/partitions/$policyname",
511 'restricted' => $policy->{assured}, # policy_assured_flag
512 'rate_multiplier' => $rate_multiplier, # policy_background 0.1
513 'rate' => $policy->{percent_rate}, # policy_percent_rate
517 $self->{'__saisei_error'} = "Rate Plan not modified after create"
518 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
526 =head2 api_modify_existing_rateplan
528 Modify a existing rateplan.
532 sub api_modify_existing_rateplan {
533 my ($self,$svc,$rateplan_name) = @_;
535 my $modified_rateplan = $self->api_call(
537 "/rate_plans/$rateplan_name",
539 'downstream_rate' => $svc->{Hash}->{speed_down},
540 'upstream_rate' => $svc->{Hash}->{speed_up},
544 $self->{'__saisei_error'} = "Rate Plan not modified"
545 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
551 =head2 api_create_user
557 sub api_create_user {
558 my ($self,$user, $description) = @_;
560 my $new_user = $self->api_call(
564 'description' => $description,
568 $self->{'__saisei_error'} = "User not created"
569 unless ($new_user || $self->{'__saisei_error'}); # should never happen
575 =head2 api_create_accesspoint
577 Creates a access point.
581 sub api_create_accesspoint {
582 my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
584 # this has not been tested, but should work, if needed.
585 my $new_accesspoint = $self->api_call(
587 "/access_points/$accesspoint",
589 'downstream_rate_limit' => $downratelimit,
590 'upstream_rate_limit' => $upratelimit,
594 $self->{'__saisei_error'} = "Access point not created"
595 unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
600 =head2 api_modify_accesspoint
602 Modify a new access point.
606 sub api_modify_accesspoint {
607 my ($self, $accesspoint, $uplink) = @_;
609 my $modified_accesspoint = $self->api_call(
611 "/access_points/$accesspoint",
613 'uplink' => $uplink, # name of attached access point
617 $self->{'__saisei_error'} = "Rate Plan not modified"
618 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
624 =head2 api_modify_existing_accesspoint
626 Modify a existing accesspoint.
630 sub api_modify_existing_accesspoint {
631 my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
633 my $modified_accesspoint = $self->api_call(
635 "/access_points/$accesspoint",
637 'downstream_rate_limit' => $downratelimit,
638 'upstream_rate_limit' => $upratelimit,
639 # 'uplink' => $uplink, # name of attached access point
643 $self->{'__saisei_error'} = "Access point not modified"
644 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
650 =head2 api_add_host_to_user
652 ties host to user, rateplan and default access point.
656 sub api_add_host_to_user {
657 my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
659 my $new_host = $self->api_call(
664 'rate_plan' => $rateplan,
665 'access_point' => $accesspoint,
669 $self->{'__saisei_error'} = "Host not created"
670 unless ($new_host || $self->{'__saisei_error'}); # should never happen
676 =head2 api_delete_host_to_user
678 unties host from user and rateplan.
679 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
683 sub api_delete_host_to_user {
684 my ($self,$user, $rateplan, $ip) = @_;
686 my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
687 return if $self->api_error;
688 $self->{'__saisei_error'} = "Did not receive a default rate plan"
689 unless $default_rate_plan;
691 my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
693 my $delete_host = $self->api_call(
698 'access_point' => '<none>',
699 'rate_plan' => $default_rateplan_name,
703 $self->{'__saisei_error'} = "Host not created"
704 unless ($delete_host || $self->{'__saisei_error'}); # should never happen
711 my ($self, $opt) = @_;
713 my $existing_tower_ap;
714 my $tower_name = $opt->{tower_name};
716 #check if tower has been set up as an access point.
717 $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
719 # modify the existing accesspoint if changing tower .
720 $self->api_modify_existing_accesspoint (
722 '', # tower does not have a uplink on sectors.
723 $opt->{tower_uprate_limit},
724 $opt->{tower_downrate_limit},
725 ) if $existing_tower_ap && $opt->{modify_existing};
727 #if tower does not exist as an access point create it.
728 $self->api_create_accesspoint(
730 $opt->{tower_uprate_limit},
731 $opt->{tower_downrate_limit}
732 ) unless $existing_tower_ap;
734 my $accesspoint = $self->api_get_accesspoint($tower_name);
740 my ($self, $opt) = @_;
742 my $existing_sector_ap;
743 my $sector_name = $opt->{sector_name};
745 #check if sector has been set up as an access point.
746 $existing_sector_ap = $self->api_get_accesspoint($sector_name);
748 # modify the existing accesspoint if changing sector .
749 $self->api_modify_existing_accesspoint (
752 $opt->{sector_uprate_limit},
753 $opt->{sector_downrate_limit},
754 ) if $existing_sector_ap && $opt->{modify_existing};
756 #if sector does not exist as an access point create it.
757 $self->api_create_accesspoint(
759 $opt->{sector_uprate_limit},
760 $opt->{sector_downrate_limit},
761 ) unless $existing_sector_ap;
763 # Attach newly created sector to it's tower.
764 $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
766 # set access point to existing one or newly created one.
767 my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
772 sub export_provisioned_services {
776 my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
777 or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
780 my @svcparts = FS::Record::qsearch({
781 'table' => 'export_svc',
782 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ',
783 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
785 my $part_count = scalar @svcparts;
787 my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
789 my @svcs = FS::Record::qsearch({
790 'table' => 'cust_svc',
791 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
792 'extra_sql' => " WHERE svcpart in ('".$parts."')",
795 my $svc_count = scalar @svcs;
798 for (my $c=10; $c <=100; $c=$c+10) { $status{int($svc_count * ($c/100))} = $c; }
801 foreach my $svc (@svcs) {
802 if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
803 ## check if service exists as host if not export it.
804 _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr});