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 setup a rate plan and tie
28 the rate plan to a host and access point via the Saisei API when the broadband service is provisioned.
29 It will also untie the 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 a access point at Saisei as soon as the tower is created or modified.
34 To use this export, follow the below instructions:
36 Add a new export and fill out required fields:
38 Hostname or IP - <I>Host name to Saisei API
39 User Name - <I>Saisei API user name
40 Password - <I>Saisei API password
42 Create a broadband service. The broadband service name will become the Saisei rate plan name.
43 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
44 Attach above created Saisei export to this broadband service.
46 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
47 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.
49 Create a package for the above created broadband service, and order this package for a customer.
51 When you provision the service, enter the ip address associated to this service and select the Tower and Sector for it's access point.
52 This provisioned service will then be exported as a host to Saisei.
54 when you un provision this service, the host entry at Saisei will be deleted.
56 When setting this up, if you wish to export your allready provisioned services, make sure the broadband service has this export attached and
57 on export edit screen there will be a link to export Provisioned Services attached to this export. Clicking on that will export all services
58 not currently exported to Saisei.
60 This module also provides generic methods for working through the L</Saisei API>.
64 tie my %scripts, 'Tie::IxHash',
65 'export_provisioned_services' => { component => '/elements/popup_link.html',
66 label => 'Export provisioned services',
67 description => 'will export provisioned services of part service with Saisei export attached.',
68 html_label => '<b>Export Provisioned Services attached to this export.</b>',
72 tie my %options, 'Tie::IxHash',
73 'port' => { label => 'Port',
75 'username' => { label => 'User Name',
77 'password' => { label => 'Password',
79 'debug' => { type => 'checkbox',
80 label => 'Enable debug warnings' },
84 'svc' => 'svc_broadband',
85 'desc' => 'Export broadband service/account to Saisei',
86 'options' => \%options,
87 'scripts' => \%scripts,
89 This is a customer integration with Saisei. This will setup a rate plan and tie
90 the rate plan to a host and access point via the Saisei API when the broadband service is provisioned.
91 It will also untie the rate plan via the API upon unprovisioning of the broadband service.
93 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
94 This will also create and modify a access point at Saisei as soon as the tower is created or modified.
96 To use this export, follow the below instructions:
100 Add a new export and fill out required fields:
102 <LI>Hostname or IP - <I>Host name to Saisei API</I></LI>
103 <LI>Port - <I>Port number to Saisei API</I></LI>
104 <LI>User Name - <I>Saisei API user name</I></LI>
105 <LI>Password - <I>Saisei API password</I></LI>
110 Create a broadband service. The broadband service name will become the Saisei rate plan name.
111 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
112 Attach above created Saisei export to this broadband service.
116 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
117 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.
121 Create a package for the above created broadband service, and order this package for a customer.
125 When you provision the service, enter the ip address associated to this service and select the Tower and Sector for it's access point.
126 This provisioned service will then be exported as a host to Saisei.
128 when you un provision this service, the host entry at Saisei will be deleted.
132 When setting this up, if you wish to export your allready provisioned services, make sure the broadband service has this export attached and
133 on export edit screen there will be a link to export Provisioned Services attached to this export. Clicking on that will export all services
134 not currently exported to Saisei.
139 my ($self, $svc_broadband) = @_;
141 my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
142 my $rateplan_name = $service_part->{Hash}->{svc};
143 $rateplan_name =~ s/\s/_/g;
145 # check for existing rate plan
146 my $existing_rateplan;
147 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
149 # if no existing rate plan create one and modify it.
150 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
151 $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
152 return $self->api_error if $self->{'__saisei_error'};
154 # set rateplan to existing one or newly created one.
155 my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
157 my $username = $svc_broadband->{Hash}->{svcnum};
158 my $description = $svc_broadband->{Hash}->{description};
161 $self->{'__saisei_error'} = 'no username - can not export';
162 return $self->api_error;
165 # check for existing user.
167 $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
169 # if no existing user create one.
170 $self->api_create_user($username, $description) unless $existing_user;
171 return $self->api_error if $self->{'__saisei_error'};
173 # set user to existing one or newly created one.
174 my $user = $existing_user ? $existing_user : $self->api_get_user($username);
177 my $tower_sector = FS::Record::qsearchs({
178 'table' => 'tower_sector',
179 'select' => 'tower.towername,
180 tower.up_rate_limit as tower_upratelimit,
181 tower.down_rate_limit as tower_downratelimit,
182 tower_sector.sectorname,
183 tower_sector.up_rate_limit as sector_upratelimit,
184 tower_sector.down_rate_limit as sector_downratelimit ',
185 'addl_from' => 'LEFT JOIN tower USING ( towernum )',
187 'sectornum' => $svc_broadband->{Hash}->{sectornum},
191 my $tower_name = $tower_sector->{Hash}->{towername};
192 $tower_name =~ s/\s/_/g;
195 'tower_name' => $tower_name,
196 'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit},
197 'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
200 my $tower_ap = process_tower($self, $tower_opt);
201 return $self->api_error if $self->{'__saisei_error'};
203 my $sector_name = $tower_sector->{Hash}->{sectorname};
204 $sector_name =~ s/\s/_/g;
207 'tower_name' => $tower_name,
208 'sector_name' => $sector_name,
209 'sector_uprate_limit' => $tower_sector->{Hash}->{sector_upratelimit},
210 'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
212 my $accesspoint = process_sector($self, $sector_opt);
213 return $self->api_error if $self->{'__saisei_error'};
215 ## tie host to user add sector name as access point.
216 $self->api_add_host_to_user(
217 $user->{collection}->[0]->{name},
218 $rateplan->{collection}->[0]->{name},
219 $svc_broadband->{Hash}->{ip_addr},
220 $accesspoint->{collection}->[0]->{name},
221 ) unless $self->{'__saisei_error'};
224 return $self->api_error;
228 sub _export_replace {
229 my ($self, $svc_broadband) = @_;
234 my ($self, $svc_broadband) = @_;
236 my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
237 my $rateplan_name = $service_part->{Hash}->{svc};
238 $rateplan_name =~ s/\s/_/g;
239 my $username = $svc_broadband->{Hash}->{svcnum};
242 $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
247 sub _export_suspend {
248 my ($self, $svc_broadband) = @_;
252 sub _export_unsuspend {
253 my ($self, $svc_broadband) = @_;
258 my ($self, $svc_part) = @_;
260 my $rateplan_name = $svc_part->{Hash}->{svc};
261 $rateplan_name =~ s/\s/_/g;
262 my $speeddown = $svc_part->{Hash}->{svc_broadband__speed_down};
263 my $speedup = $svc_part->{Hash}->{svc_broadband__speed_up};
265 my $temp_svc = $svc_part->{Hash};
266 my $svc_broadband = {};
267 map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; } } keys %$temp_svc;
269 # check for existing rate plan
270 my $existing_rateplan;
271 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
273 # Modify the existing rate plan with new service data.
274 $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
276 # if no existing rate plan create one and modify it.
277 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
278 $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
280 return $self->api_error;
284 sub export_tower_sector {
285 my ($self, $tower) = @_;
287 #modify tower or create it.
288 my $tower_name = $tower->{Hash}->{towername};
289 $tower_name =~ s/\s/_/g;
291 'tower_name' => $tower_name,
292 'tower_uprate_limit' => $tower->{Hash}->{up_rate_limit},
293 'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
294 'modify_existing' => '1', # modify an existing access point with this info
297 my $tower_access_point = process_tower($self, $tower_opt);
299 #get list of all access points
301 'table' => 'tower_sector',
303 'hashref' => { 'towernum' => $tower->{Hash}->{towernum}, },
306 #for each one modify or create it.
307 foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
308 my $sector_name = $tower_sector->{Hash}->{sectorname};
309 $sector_name =~ s/\s/_/g;
311 'tower_name' => $tower_name,
312 'sector_name' => $sector_name,
313 'sector_uprate_limit' => $tower_sector->{Hash}->{up_rate_limit},
314 'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
315 'modify_existing' => '1', # modify an existing access point with this info
317 my $sector_access_point = process_sector($self, $sector_opt);
320 return $self->api_error;
325 These methods allow access to the Saisei API using the credentials
326 set in the export options.
332 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
333 Places an api call to the specified path and method with the specified params.
334 Returns the decoded json object returned by the api call.
335 Returns empty on failure; retrieve error messages using L</api_error>.
340 my ($self,$method,$path,$params) = @_;
342 $self->{'__saisei_error'} = '';
343 my $auth_info = $self->option('username') . ':' . $self->option('password');
346 warn "Calling $method on http://"
347 .$self->{Hash}->{machine}.':'.$self->option('port')
348 ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
350 my $data = encode_json($params) if keys %{ $params };
352 my $client = REST::Client->new();
353 $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
354 $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
355 $client->$method('/rest/stm/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
357 warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
361 if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
362 eval { $result = decode_json($client->responseContent()) };
364 $self->{'__saisei_error'} = "Error decoding json: $@";
369 $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent()
370 unless ($method eq "GET");
371 warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
381 Returns the error string set by L</Saisei API> methods,
382 or a blank string if most recent call produced no errors.
388 return $self->{'__saisei_error'} || '';
391 =head2 api_get_policies
393 Gets a list of global policies.
397 sub api_get_policies {
400 my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
401 return if $self->api_error;
402 $self->{'__saisei_error'} = "Did not receive any global policies"
403 unless $get_policies;
405 return $get_policies->{collection};
408 =head2 api_get_rateplan
410 Gets rateplan info for specific rateplan.
414 sub api_get_rateplan {
416 my $rateplan = shift;
418 my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
419 return if $self->api_error;
421 return $get_rateplan;
426 Gets user info for specific user.
434 my $get_user = $self->api_call("GET", "/users/$user");
435 return if $self->api_error;
440 =head2 api_get_accesspoint
442 Gets user info for specific access point.
446 sub api_get_accesspoint {
448 my $accesspoint = shift;
450 my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
451 return if $self->api_error;
453 return $get_accesspoint;
458 Gets user info for specific host.
466 my $get_host = $self->api_call("GET", "/hosts/$ip");
468 return if $self->api_error;
473 =head2 api_create_rateplan
479 sub api_create_rateplan {
480 my ($self, $svc, $rateplan) = @_;
482 $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
483 $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
485 my $new_rateplan = $self->api_call(
487 "/rate_plans/$rateplan",
489 'downstream_rate' => $svc->{Hash}->{speed_down},
490 'upstream_rate' => $svc->{Hash}->{speed_up},
492 ) unless $self->{'__saisei_error'};
494 $self->{'__saisei_error'} = "Rate Plan not created"
495 unless ($new_rateplan || $self->{'__saisei_error'});
497 return $new_rateplan;
501 =head2 api_modify_rateplan
503 Modify a new rateplan.
507 sub api_modify_rateplan {
508 my ($self,$svc,$rateplan_name) = @_;
511 my $policies = $self->api_get_policies();
513 foreach my $policy (@$policies) {
514 my $policyname = $policy->{name};
515 my $rate_multiplier = '';
516 if ($policy->{background}) { $rate_multiplier = ".01"; }
517 my $modified_rateplan = $self->api_call(
519 "/rate_plans/$rateplan_name/partitions/$policyname",
521 'restricted' => $policy->{assured}, # policy_assured_flag
522 'rate_multiplier' => $rate_multiplier, # policy_background 0.1
523 'rate' => $policy->{percent_rate}, # policy_percent_rate
527 $self->{'__saisei_error'} = "Rate Plan not modified after create"
528 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
536 =head2 api_modify_existing_rateplan
538 Modify a existing rateplan.
542 sub api_modify_existing_rateplan {
543 my ($self,$svc,$rateplan_name) = @_;
545 my $modified_rateplan = $self->api_call(
547 "/rate_plans/$rateplan_name",
549 'downstream_rate' => $svc->{Hash}->{speed_down},
550 'upstream_rate' => $svc->{Hash}->{speed_up},
554 $self->{'__saisei_error'} = "Rate Plan not modified"
555 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
561 =head2 api_create_user
567 sub api_create_user {
568 my ($self,$user, $description) = @_;
570 my $new_user = $self->api_call(
574 'description' => $description,
578 $self->{'__saisei_error'} = "User not created"
579 unless ($new_user || $self->{'__saisei_error'}); # should never happen
585 =head2 api_create_accesspoint
587 Creates a access point.
591 sub api_create_accesspoint {
592 my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
594 # this has not been tested, but should work, if needed.
595 my $new_accesspoint = $self->api_call(
597 "/access_points/$accesspoint",
599 'downstream_rate_limit' => $downratelimit,
600 'upstream_rate_limit' => $upratelimit,
604 $self->{'__saisei_error'} = "Access point not created"
605 unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
610 =head2 api_modify_accesspoint
612 Modify a new access point.
616 sub api_modify_accesspoint {
617 my ($self, $accesspoint, $uplink) = @_;
619 my $modified_accesspoint = $self->api_call(
621 "/access_points/$accesspoint",
623 'uplink' => $uplink, # name of attached access point
627 $self->{'__saisei_error'} = "Rate Plan not modified"
628 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
634 =head2 api_modify_existing_accesspoint
636 Modify a existing accesspoint.
640 sub api_modify_existing_accesspoint {
641 my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
643 my $modified_accesspoint = $self->api_call(
645 "/access_points/$accesspoint",
647 'downstream_rate_limit' => $downratelimit,
648 'upstream_rate_limit' => $upratelimit,
649 # 'uplink' => $uplink, # name of attached access point
653 $self->{'__saisei_error'} = "Access point not modified"
654 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
660 =head2 api_add_host_to_user
662 ties host to user, rateplan and default access point.
666 sub api_add_host_to_user {
667 my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
669 my $new_host = $self->api_call(
674 'rate_plan' => $rateplan,
675 'access_point' => $accesspoint,
679 $self->{'__saisei_error'} = "Host not created"
680 unless ($new_host || $self->{'__saisei_error'}); # should never happen
686 =head2 api_delete_host_to_user
688 unties host to user and rateplan.
692 sub api_delete_host_to_user {
693 my ($self,$user, $rateplan, $ip) = @_;
695 my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
696 return if $self->api_error;
697 $self->{'__saisei_error'} = "Did not receive a default rate plan"
698 unless $default_rate_plan;
700 my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
702 my $delete_host = $self->api_call(
707 'access_point' => '<none>',
708 'rate_plan' => $default_rateplan_name,
712 $self->{'__saisei_error'} = "Host not created"
713 unless ($delete_host || $self->{'__saisei_error'}); # should never happen
720 my ($self, $opt) = @_;
722 my $existing_tower_ap;
723 my $tower_name = $opt->{tower_name};
725 #check if tower has been set up as an access point.
726 $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
728 # modify the existing accesspoint if changing tower .
729 $self->api_modify_existing_accesspoint (
731 '', # tower does not have a uplink on sectors.
732 $opt->{tower_uprate_limit},
733 $opt->{tower_downrate_limit},
734 ) if $existing_tower_ap && $opt->{modify_existing};
736 #if tower does not exist as an access point create it.
737 $self->api_create_accesspoint(
739 $opt->{tower_uprate_limit},
740 $opt->{tower_downrate_limit}
741 ) unless $existing_tower_ap;
743 my $accesspoint = $self->api_get_accesspoint($tower_name);
749 my ($self, $opt) = @_;
751 my $existing_sector_ap;
752 my $sector_name = $opt->{sector_name};
754 #check if sector has been set up as an access point.
755 $existing_sector_ap = $self->api_get_accesspoint($sector_name);
757 # modify the existing accesspoint if changing sector .
758 $self->api_modify_existing_accesspoint (
761 $opt->{sector_uprate_limit},
762 $opt->{sector_downrate_limit},
763 ) if $existing_sector_ap && $opt->{modify_existing};
765 #if sector does not exist as an access point create it.
766 $self->api_create_accesspoint(
768 $opt->{sector_uprate_limit},
769 $opt->{sector_downrate_limit},
770 ) unless $existing_sector_ap;
772 # Attach newly created sector to it's tower.
773 $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
775 # set access point to existing one or newly created one.
776 my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
781 sub export_provisioned_services {
785 my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
786 or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
789 my @svcparts = FS::Record::qsearch({
790 'table' => 'export_svc',
791 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ',
792 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
794 my $part_count = scalar @svcparts;
796 my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
798 my @svcs = FS::Record::qsearch({
799 'table' => 'cust_svc',
800 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
801 'extra_sql' => " WHERE svcpart in ('".$parts."')",
804 my $svc_count = scalar @svcs;
807 for (my $c=10; $c <=100; $c=$c+10) { $status{int($svc_count * ($c/100))} = $c; }
810 foreach my $svc (@svcs) {
811 if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
812 ## check if service exists as host if not export it.
813 _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr});