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 setup a rate plan and tie
29 the rate plan to a host and access point via the Saisei API when the broadband service is provisioned.
30 It will also untie the 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 a access point at Saisei as soon as the tower is created or modified.
35 To use this export, follow the below instructions:
37 Add a new export and fill out required fields:
39 Hostname or IP - <I>Host name to Saisei API
40 User Name - <I>Saisei API user name
41 Password - <I>Saisei API password
43 Create a broadband service. The broadband service name will become the Saisei rate plan name.
44 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
45 Attach above created Saisei export to this broadband service.
47 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
48 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.
50 Create a package for the above created broadband service, and order this package for a customer.
52 When you provision the service, enter the ip address associated to this service and select the Tower and Sector for it's access point.
53 This provisioned service will then be exported as a host to Saisei.
55 when you un provision this service, the host entry at Saisei will be deleted.
57 When setting this up, if you wish to export your allready provisioned services, make sure the broadband service has this export attached and
58 on export edit screen there will be a link to export Provisioned Services attached to this export. Clicking on that will export all services
59 not currently exported to Saisei.
61 This module also provides generic methods for working through the L</Saisei API>.
65 tie my %scripts, 'Tie::IxHash',
66 'export_provisioned_services' => { component => '/elements/popup_link.html',
67 label => 'Export provisioned services',
68 description => 'will export provisioned services of part service with Saisei export attached.',
69 html_label => '<b>Export Provisioned Services attached to this export.</b>',
73 tie my %options, 'Tie::IxHash',
74 'port' => { label => 'Port',
76 'username' => { label => 'User Name',
78 'password' => { label => 'Password',
80 'debug' => { type => 'checkbox',
81 label => 'Enable debug warnings' },
85 'svc' => 'svc_broadband',
86 'desc' => 'Export broadband service/account to Saisei',
87 'options' => \%options,
88 'scripts' => \%scripts,
90 This is a customer integration with Saisei. This will setup a rate plan and tie
91 the rate plan to a host and access point via the Saisei API when the broadband service is provisioned.
92 It will also untie the rate plan via the API upon unprovisioning of the broadband service.
94 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
95 This will also create and modify a access point at Saisei as soon as the tower is created or modified.
97 To use this export, follow the below instructions:
101 Add a new export and fill out required fields:
103 <LI>Hostname or IP - <I>Host name to Saisei API</I></LI>
104 <LI>Port - <I>Port number to Saisei API</I></LI>
105 <LI>User Name - <I>Saisei API user name</I></LI>
106 <LI>Password - <I>Saisei API password</I></LI>
111 Create a broadband service. The broadband service name will become the Saisei rate plan name.
112 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
113 Attach above created Saisei export to this broadband service.
117 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
118 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.
122 Create a package for the above created broadband service, and order this package for a customer.
126 When you provision the service, enter the ip address associated to this service 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 when you un provision this service, the host entry at Saisei will be deleted.
133 When setting this up, if you wish to export your allready provisioned services, make sure the broadband service has this export attached and
134 on export edit screen there will be a link to export Provisioned Services attached to this export. Clicking on that will export all services
135 not currently exported to Saisei.
140 my ($self, $svc_broadband) = @_;
142 my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
143 my $rateplan_name = $service_part->{Hash}->{svc};
144 $rateplan_name =~ s/\s/_/g;
146 # check for existing rate plan
147 my $existing_rateplan;
148 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
150 # if no existing rate plan create one and modify it.
151 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
152 $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
153 return $self->api_error if $self->{'__saisei_error'};
155 # set rateplan to existing one or newly created one.
156 my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
158 my $username = $svc_broadband->{Hash}->{svcnum};
159 my $description = $svc_broadband->{Hash}->{description};
162 $self->{'__saisei_error'} = 'no username - can not export';
163 return $self->api_error;
166 # check for existing user.
168 $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
170 # if no existing user create one.
171 $self->api_create_user($username, $description) unless $existing_user;
172 return $self->api_error if $self->{'__saisei_error'};
174 # set user to existing one or newly created one.
175 my $user = $existing_user ? $existing_user : $self->api_get_user($username);
178 my $tower_sector = FS::Record::qsearchs({
179 'table' => 'tower_sector',
180 'select' => 'tower.towername,
181 tower.up_rate_limit as tower_upratelimit,
182 tower.down_rate_limit as tower_downratelimit,
183 tower_sector.sectorname,
184 tower_sector.up_rate_limit as sector_upratelimit,
185 tower_sector.down_rate_limit as sector_downratelimit ',
186 'addl_from' => 'LEFT JOIN tower USING ( towernum )',
188 'sectornum' => $svc_broadband->{Hash}->{sectornum},
192 my $tower_name = $tower_sector->{Hash}->{towername};
193 $tower_name =~ s/\s/_/g;
196 'tower_name' => $tower_name,
197 'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit},
198 'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
201 my $tower_ap = process_tower($self, $tower_opt);
202 return $self->api_error if $self->{'__saisei_error'};
204 my $sector_name = $tower_sector->{Hash}->{sectorname};
205 $sector_name =~ s/\s/_/g;
208 'tower_name' => $tower_name,
209 'sector_name' => $sector_name,
210 'sector_uprate_limit' => $tower_sector->{Hash}->{sector_upratelimit},
211 'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
213 my $accesspoint = process_sector($self, $sector_opt);
214 return $self->api_error if $self->{'__saisei_error'};
216 ## tie host to user add sector name as access point.
217 $self->api_add_host_to_user(
218 $user->{collection}->[0]->{name},
219 $rateplan->{collection}->[0]->{name},
220 $svc_broadband->{Hash}->{ip_addr},
221 $accesspoint->{collection}->[0]->{name},
222 ) unless $self->{'__saisei_error'};
225 return $self->api_error;
229 sub _export_replace {
230 my ($self, $svc_broadband) = @_;
235 my ($self, $svc_broadband) = @_;
237 my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
238 my $rateplan_name = $service_part->{Hash}->{svc};
239 $rateplan_name =~ s/\s/_/g;
240 my $username = $svc_broadband->{Hash}->{svcnum};
243 $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
248 sub _export_suspend {
249 my ($self, $svc_broadband) = @_;
253 sub _export_unsuspend {
254 my ($self, $svc_broadband) = @_;
259 my ($self, $svc_part) = @_;
261 my $rateplan_name = $svc_part->{Hash}->{svc};
262 $rateplan_name =~ s/\s/_/g;
263 my $speeddown = $svc_part->{Hash}->{svc_broadband__speed_down};
264 my $speedup = $svc_part->{Hash}->{svc_broadband__speed_up};
266 my $temp_svc = $svc_part->{Hash};
267 my $svc_broadband = {};
268 map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; } } keys %$temp_svc;
270 # check for existing rate plan
271 my $existing_rateplan;
272 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
274 # Modify the existing rate plan with new service data.
275 $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
277 # if no existing rate plan create one and modify it.
278 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
279 $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
281 return $self->api_error;
285 sub export_tower_sector {
286 my ($self, $tower) = @_;
288 #modify tower or create it.
289 my $tower_name = $tower->{Hash}->{towername};
290 $tower_name =~ s/\s/_/g;
292 'tower_name' => $tower_name,
293 'tower_uprate_limit' => $tower->{Hash}->{up_rate_limit},
294 'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
295 'modify_existing' => '1', # modify an existing access point with this info
298 my $tower_access_point = process_tower($self, $tower_opt);
300 #get list of all access points
302 'table' => 'tower_sector',
304 'hashref' => { 'towernum' => $tower->{Hash}->{towernum}, },
307 #for each one modify or create it.
308 foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
309 my $sector_name = $tower_sector->{Hash}->{sectorname};
310 $sector_name =~ s/\s/_/g;
312 'tower_name' => $tower_name,
313 'sector_name' => $sector_name,
314 'sector_uprate_limit' => $tower_sector->{Hash}->{up_rate_limit},
315 'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
316 'modify_existing' => '1', # modify an existing access point with this info
318 my $sector_access_point = process_sector($self, $sector_opt);
321 return $self->api_error;
326 These methods allow access to the Saisei API using the credentials
327 set in the export options.
333 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
334 Places an api call to the specified path and method with the specified params.
335 Returns the decoded json object returned by the api call.
336 Returns empty on failure; retrieve error messages using L</api_error>.
341 my ($self,$method,$path,$params) = @_;
343 $self->{'__saisei_error'} = '';
344 my $auth_info = $self->option('username') . ':' . $self->option('password');
347 warn "Calling $method on http://"
348 .$self->{Hash}->{machine}.':'.$self->option('port')
349 ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
351 my $data = encode_json($params) if keys %{ $params };
353 my $client = REST::Client->new();
354 $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
355 $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
356 $client->$method('/rest/stm/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
358 warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
362 if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
363 eval { $result = decode_json($client->responseContent()) };
365 $self->{'__saisei_error'} = "Error decoding json: $@";
370 $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent()
371 unless ($method eq "GET");
372 warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
382 Returns the error string set by L</Saisei API> methods,
383 or a blank string if most recent call produced no errors.
389 return $self->{'__saisei_error'} || '';
392 =head2 api_get_policies
394 Gets a list of global policies.
398 sub api_get_policies {
401 my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
402 return if $self->api_error;
403 $self->{'__saisei_error'} = "Did not receive any global policies"
404 unless $get_policies;
406 return $get_policies->{collection};
409 =head2 api_get_rateplan
411 Gets rateplan info for specific rateplan.
415 sub api_get_rateplan {
417 my $rateplan = shift;
419 my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
420 return if $self->api_error;
422 return $get_rateplan;
427 Gets user info for specific user.
435 my $get_user = $self->api_call("GET", "/users/$user");
436 return if $self->api_error;
441 =head2 api_get_accesspoint
443 Gets user info for specific access point.
447 sub api_get_accesspoint {
449 my $accesspoint = shift;
451 my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
452 return if $self->api_error;
454 return $get_accesspoint;
459 Gets user info for specific host.
467 my $get_host = $self->api_call("GET", "/hosts/$ip");
469 return if $self->api_error;
474 =head2 api_create_rateplan
480 sub api_create_rateplan {
481 my ($self, $svc, $rateplan) = @_;
483 $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
484 $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
486 my $new_rateplan = $self->api_call(
488 "/rate_plans/$rateplan",
490 'downstream_rate' => $svc->{Hash}->{speed_down},
491 'upstream_rate' => $svc->{Hash}->{speed_up},
493 ) unless $self->{'__saisei_error'};
495 $self->{'__saisei_error'} = "Rate Plan not created"
496 unless ($new_rateplan || $self->{'__saisei_error'});
498 return $new_rateplan;
502 =head2 api_modify_rateplan
504 Modify a new rateplan.
508 sub api_modify_rateplan {
509 my ($self,$svc,$rateplan_name) = @_;
512 my $policies = $self->api_get_policies();
514 foreach my $policy (@$policies) {
515 my $policyname = $policy->{name};
516 my $rate_multiplier = '';
517 if ($policy->{background}) { $rate_multiplier = ".01"; }
518 my $modified_rateplan = $self->api_call(
520 "/rate_plans/$rateplan_name/partitions/$policyname",
522 'restricted' => $policy->{assured}, # policy_assured_flag
523 'rate_multiplier' => $rate_multiplier, # policy_background 0.1
524 'rate' => $policy->{percent_rate}, # policy_percent_rate
528 $self->{'__saisei_error'} = "Rate Plan not modified after create"
529 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
537 =head2 api_modify_existing_rateplan
539 Modify a existing rateplan.
543 sub api_modify_existing_rateplan {
544 my ($self,$svc,$rateplan_name) = @_;
546 my $modified_rateplan = $self->api_call(
548 "/rate_plans/$rateplan_name",
550 'downstream_rate' => $svc->{Hash}->{speed_down},
551 'upstream_rate' => $svc->{Hash}->{speed_up},
555 $self->{'__saisei_error'} = "Rate Plan not modified"
556 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
562 =head2 api_create_user
568 sub api_create_user {
569 my ($self,$user, $description) = @_;
571 my $new_user = $self->api_call(
575 'description' => $description,
579 $self->{'__saisei_error'} = "User not created"
580 unless ($new_user || $self->{'__saisei_error'}); # should never happen
586 =head2 api_create_accesspoint
588 Creates a access point.
592 sub api_create_accesspoint {
593 my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
595 # this has not been tested, but should work, if needed.
596 my $new_accesspoint = $self->api_call(
598 "/access_points/$accesspoint",
600 'downstream_rate_limit' => $downratelimit,
601 'upstream_rate_limit' => $upratelimit,
605 $self->{'__saisei_error'} = "Access point not created"
606 unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
611 =head2 api_modify_accesspoint
613 Modify a new access point.
617 sub api_modify_accesspoint {
618 my ($self, $accesspoint, $uplink) = @_;
620 my $modified_accesspoint = $self->api_call(
622 "/access_points/$accesspoint",
624 'uplink' => $uplink, # name of attached access point
628 $self->{'__saisei_error'} = "Rate Plan not modified"
629 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
635 =head2 api_modify_existing_accesspoint
637 Modify a existing accesspoint.
641 sub api_modify_existing_accesspoint {
642 my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
644 my $modified_accesspoint = $self->api_call(
646 "/access_points/$accesspoint",
648 'downstream_rate_limit' => $downratelimit,
649 'upstream_rate_limit' => $upratelimit,
650 # 'uplink' => $uplink, # name of attached access point
654 $self->{'__saisei_error'} = "Access point not modified"
655 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
661 =head2 api_add_host_to_user
663 ties host to user, rateplan and default access point.
667 sub api_add_host_to_user {
668 my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
670 my $new_host = $self->api_call(
675 'rate_plan' => $rateplan,
676 'access_point' => $accesspoint,
680 $self->{'__saisei_error'} = "Host not created"
681 unless ($new_host || $self->{'__saisei_error'}); # should never happen
687 =head2 api_delete_host_to_user
689 unties host to user and rateplan.
693 sub api_delete_host_to_user {
694 my ($self,$user, $rateplan, $ip) = @_;
696 my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
697 return if $self->api_error;
698 $self->{'__saisei_error'} = "Did not receive a default rate plan"
699 unless $default_rate_plan;
701 my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
703 my $delete_host = $self->api_call(
708 'access_point' => '<none>',
709 'rate_plan' => $default_rateplan_name,
713 $self->{'__saisei_error'} = "Host not created"
714 unless ($delete_host || $self->{'__saisei_error'}); # should never happen
721 my ($self, $opt) = @_;
723 my $existing_tower_ap;
724 my $tower_name = $opt->{tower_name};
726 #check if tower has been set up as an access point.
727 $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
729 # modify the existing accesspoint if changing tower .
730 $self->api_modify_existing_accesspoint (
732 '', # tower does not have a uplink on sectors.
733 $opt->{tower_uprate_limit},
734 $opt->{tower_downrate_limit},
735 ) if $existing_tower_ap && $opt->{modify_existing};
737 #if tower does not exist as an access point create it.
738 $self->api_create_accesspoint(
740 $opt->{tower_uprate_limit},
741 $opt->{tower_downrate_limit}
742 ) unless $existing_tower_ap;
744 my $accesspoint = $self->api_get_accesspoint($tower_name);
750 my ($self, $opt) = @_;
752 my $existing_sector_ap;
753 my $sector_name = $opt->{sector_name};
755 #check if sector has been set up as an access point.
756 $existing_sector_ap = $self->api_get_accesspoint($sector_name);
758 # modify the existing accesspoint if changing sector .
759 $self->api_modify_existing_accesspoint (
762 $opt->{sector_uprate_limit},
763 $opt->{sector_downrate_limit},
764 ) if $existing_sector_ap && $opt->{modify_existing};
766 #if sector does not exist as an access point create it.
767 $self->api_create_accesspoint(
769 $opt->{sector_uprate_limit},
770 $opt->{sector_downrate_limit},
771 ) unless $existing_sector_ap;
773 # Attach newly created sector to it's tower.
774 $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
776 # set access point to existing one or newly created one.
777 my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
782 sub export_provisioned_services {
784 my $param = thaw(decode_base64(shift));
786 my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
787 or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
790 my @svcparts = FS::Record::qsearch({
791 'table' => 'export_svc',
792 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ',
793 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
795 my $part_count = scalar @svcparts;
797 my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
799 my @svcs = FS::Record::qsearch({
800 'table' => 'cust_svc',
801 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
802 'extra_sql' => " WHERE svcpart in ('".$parts."')",
805 my $svc_count = scalar @svcs;
808 for (my $c=10; $c <=100; $c=$c+10) { $status{int($svc_count * ($c/100))} = $c; }
811 foreach my $svc (@svcs) {
812 if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
813 ## check if service exists as host if not export it.
814 _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr});