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',
76 'force_export_all_users' => { component => '/elements/popup_link.html',
77 label => 'Force update of all Saisei users from freeside provisioned services',
78 description => 'Will force an update of Saisei users description and map location from freeside provisioned services.',
79 html_label => '<b>Force update of all Saisei users from freeside provisioned services</b>',
80 error_url => '/edit/part_export.cgi?',
81 success_message => 'Export of freeside provisioned services as Saisei users was successful',
83 'force_export_all_virtual_ap' => { component => '/elements/popup_link.html',
84 label => 'Force update of all virtual Access Points',
85 description => 'Will force an update of all virtual access points.',
86 html_label => '<b>Force update of all virtual Access Points</b>',
87 error_url => '/edit/part_export.cgi?',
88 success_message => 'Export of all virtual access points to Saisei was successful',
92 tie my %options, 'Tie::IxHash',
93 'port' => { label => 'Port',
95 'username' => { label => 'Saisei API User Name',
97 'password' => { label => 'Saisei API Password',
99 'interface' => { label => 'Saisei Access Point Interface',
101 'debug' => { type => 'checkbox',
102 label => 'Enable debug warnings' },
106 'svc' => 'svc_broadband',
107 'desc' => 'Export broadband service/account to Saisei',
108 'options' => \%options,
109 'scripts' => \%scripts,
111 This is a customer integration with Saisei. This will set up a rate plan and tie
112 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
113 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.
115 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
116 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
118 To use this export, follow the below instructions:
122 Create a new service definition and set the table to svc_broadband. The service name will become the Saisei rate plan name.
123 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.
124 Attach this Saisei export to this service.
128 Create a tower and add a sector to that tower. The sector name will be the name of the access point,
129 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.
130 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.
131 Each sector will be attached to its tower access point using the Saisei uplink field.
132 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.
136 Create a package for the above created service, and order this package for a customer.
140 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.
141 This provisioned service will then be exported as a host to Saisei.
143 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>.
147 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>.
148 Clicking on this link will export all services attached to this export not currently exported to Saisei.
152 <A HREF="http://www.freeside.biz/mediawiki/index.php/Saisei_provisioning_export" target="_new">Documentation</a>
157 my ($self, $svc_broadband, $force_update) = @_;
159 my $rateplan_name = $self->get_rateplan_name($svc_broadband);
161 # check for existing rate plan
162 my $existing_rateplan;
163 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
165 die ("Please double check your credentials as ".$existing_rateplan->{message}."\n") if $existing_rateplan->{message};
167 # if no existing rate plan create one and modify it.
168 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan->{collection};
169 $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan->{collection});
170 return $self->api_error if $self->{'__saisei_error'};
172 # set rateplan to existing one or newly created one.
173 my $rateplan = $existing_rateplan->{collection} ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
175 my $username = $svc_broadband->{Hash}->{svcnum};
176 my $description = $svc_broadband->{Hash}->{description};
177 my $svc_location = get_svc_location($self, $svc_broadband);
180 $self->{'__saisei_error'} = 'no username - can not export';
181 return $self->api_error;
184 # check for existing user.
186 $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
188 # if no existing user create one.
189 $self->api_create_user($username, $description, $svc_location) unless $existing_user;
190 return $self->api_error if $self->{'__saisei_error'};
192 # set user to existing one or newly created one.
193 my $user = $existing_user ? $existing_user : $self->api_get_user($username);
196 my $tower_sector = FS::Record::qsearchs({
197 'table' => 'tower_sector',
198 'select' => 'tower.towername,
199 tower.up_rate_limit as tower_upratelimit,
200 tower.down_rate_limit as tower_downratelimit,
201 tower_sector.sectorname,
202 tower_sector.towernum,
203 tower_sector.up_rate_limit as sector_upratelimit,
204 tower_sector.down_rate_limit as sector_downratelimit,
207 'addl_from' => 'LEFT JOIN tower USING ( towernum )',
209 'sectornum' => $svc_broadband->{Hash}->{sectornum},
214 $tower_location = $tower_sector->{Hash}->{latitude}.','.$tower_sector->{Hash}->{longitude} if ($tower_sector->{Hash}->{latitude} && $tower_sector->{Hash}->{longitude});
216 my $tower_name = $tower_sector->{Hash}->{towername};
217 $tower_name =~ s/\s/_/g;
220 'tower_name' => $tower_name,
221 'tower_num' => $tower_sector->{Hash}->{towernum},
222 'tower_uprate_limit' => $tower_sector->{Hash}->{tower_upratelimit},
223 'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
225 $tower_opt->{'location'} = $tower_location if $tower_location;
227 my $tower_ap = process_tower($self, $tower_opt);
228 return $self->api_error if $self->{'__saisei_error'};
230 my $sector_name = $tower_sector->{Hash}->{sectorname};
231 $sector_name =~ s/\s/_/g;
234 'tower_name' => $tower_name,
235 'tower_num' => $tower_sector->{Hash}->{towernum},
236 'sector_name' => $sector_name,
237 'sector_uprate_limit' => $tower_sector->{Hash}->{sector_upratelimit},
238 'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
239 'rateplan' => $rateplan_name,
241 $sector_opt->{'location'} = $tower_location if $tower_location;
243 my $accesspoint = process_sector($self, $sector_opt);
244 return $self->api_error if $self->{'__saisei_error'};
246 ## get custnum and pkgpart from cust_pkg for virtual access point
247 my $cust_pkg = FS::Record::qsearchs({
248 'table' => 'cust_pkg',
249 'hashref' => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, },
252 my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up};
253 my $modify_existing_virtual_ap = '1' if $force_update->{'update_virtual_ap'};
255 my $virtual_ap_opt = {
256 'virtual_name' => $virtual_ap_name,
257 'sector_name' => $sector_name,
258 'virtual_uprate_limit' => $svc_broadband->{Hash}->{speed_up},
259 'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down},
260 'location' => $svc_location,
261 'modify_existing' => $modify_existing_virtual_ap,
263 my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt);
264 return $self->api_error if $self->{'__saisei_error'};
266 ## tie host to user add sector name as access point.
268 'user' => $user->{collection}->[0]->{name},
269 'rateplan' => $rateplan->{collection}->[0]->{name},
270 'ip' => $svc_broadband->{Hash}->{ip_addr},
271 'accesspoint' => $virtual_ap->{collection}->[0]->{name},
272 'location' => $svc_location,
274 $self->api_add_host_to_user($host_opt)
275 unless $self->{'__saisei_error'};
278 return $self->api_error;
282 sub _export_replace {
283 my ($self, $svc_broadband) = @_;
284 my $error = $self->_export_insert($svc_broadband);
289 my ($self, $svc_broadband) = @_;
291 my $rateplan_name = $self->get_rateplan_name($svc_broadband);
293 my $username = $svc_broadband->{Hash}->{svcnum};
295 ## untie host to user
296 $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
301 sub _export_suspend {
302 my ($self, $svc_broadband) = @_;
306 sub _export_unsuspend {
307 my ($self, $svc_broadband) = @_;
312 my ($self, $svc_part) = @_;
314 if ( $FS::svc_Common::noexport_hack ) {
315 carp 'export_partsvc() suppressed by noexport_hack'
316 if $self->option('debug');
321 if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
322 for my $type (qw( down up )) {
323 my $speed_type = "broadband_".$type."stream";
324 foreach my $pkg_svc (FS::Record::qsearch({
325 'table' => 'pkg_svc',
326 'select' => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
327 'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
328 'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
329 })) { $fcc_477_speeds->{
330 $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
334 $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
335 $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
338 foreach my $key (keys %$fcc_477_speeds) {
340 $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
341 $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
342 $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
343 $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
345 my $temp_svc = $svc_part->{Hash};
346 my $svc_broadband = {};
347 map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; } } keys %$temp_svc;
349 my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
351 # check for existing rate plan
352 my $existing_rateplan;
353 $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
355 # Modify the existing rate plan with new service data.
356 $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
358 # if no existing rate plan create one and modify it.
359 $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
360 $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
364 return $self->api_error;
368 sub export_tower_sector {
369 my ($self, $tower) = @_;
371 if ( $FS::svc_Common::noexport_hack ) {
372 carp 'export_tower_sector() suppressed by noexport_hack'
373 if $self->option('debug');
378 $tower_location = $tower->{Hash}->{latitude}.','.$tower->{Hash}->{longitude} if ($tower->{Hash}->{latitude} && $tower->{Hash}->{longitude});
380 #modify tower or create it.
381 my $tower_name = $tower->{Hash}->{towername};
382 $tower_name =~ s/\s/_/g;
384 'tower_name' => $tower_name,
385 'tower_num' => $tower->{Hash}->{towernum},
386 'tower_uprate_limit' => $tower->{Hash}->{up_rate_limit},
387 'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
388 'modify_existing' => '1', # modify an existing access point with this info
390 $tower_opt->{'location'} = $tower_location if $tower_location;
392 my $tower_access_point = process_tower($self, $tower_opt);
393 return $tower_access_point if $tower_access_point->{error};
395 #get list of all access points
397 'table' => 'tower_sector',
399 'hashref' => { 'towernum' => $tower->{Hash}->{towernum}, },
402 #for each one modify or create it.
403 foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
404 next if $tower_sector->{Hash}->{sectorname} eq "_default";
405 my $sector_name = $tower_sector->{Hash}->{sectorname};
406 $sector_name =~ s/\s/_/g;
408 'tower_name' => $tower_name,
409 'tower_num' => $tower_sector->{Hash}->{towernum},
410 'sector_name' => $sector_name,
411 'sector_uprate_limit' => $tower_sector->{Hash}->{up_rate_limit},
412 'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
413 'modify_existing' => '1', # modify an existing access point with this info
415 $sector_opt->{'location'} = $tower_location if $tower_location;
417 my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default");
418 return $sector_access_point if $sector_access_point->{error};
421 return { error => $self->api_error, };
425 my ($self, $username, $description, $location) = @_;
427 $self->api_create_user($username, $description, $location);
429 return $self->api_error if $self->{'__saisei_error'};
434 ## creates the rateplan name
435 sub get_rateplan_name {
436 my ($self, $svc_broadband, $svc_name) = @_;
438 my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
439 my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
441 my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
442 $rateplan_name =~ s/\s/_/g; $rateplan_name =~ s/[^A-Za-z0-9\-_]//g;
444 return $rateplan_name;
449 These methods allow access to the Saisei API using the credentials
450 set in the export options.
456 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
457 Places an api call to the specified path and method with the specified params.
458 Returns the decoded json object returned by the api call.
459 Returns empty on failure; retrieve error messages using L</api_error>.
464 my ($self,$method,$path,$params) = @_;
466 $self->{'__saisei_error'} = '';
467 my $auth_info = $self->option('username') . ':' . $self->option('password');
470 warn "Calling $method on http://"
471 .$self->{Hash}->{machine}.':'.$self->option('port')
472 ."/rest/top/configurations/running/$path\n" if $self->option('debug');
474 my $data = encode_json($params) if keys %{ $params };
476 my $client = REST::Client->new();
477 $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
478 $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
479 $client->$method('/rest/top/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
481 warn "Saisei Response Code is ".$client->responseCode()."\n" if $self->option('debug');
485 if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
486 eval { $result = decode_json($client->responseContent()) };
488 $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.";
489 warn "Saisei RC 201 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
493 elsif ($client->responseCode() eq '404') {
494 eval { $result = decode_json($client->responseContent()) };
496 $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.";
497 warn "Saisei RC 404 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
500 ## check if message is for empty hash.
501 my($does_not_exist) = $result->{message} =~ /'(.*)' does not exist$/;
502 $self->{'__saisei_error'} = "Saisei Error: ".$result->{message} unless $does_not_exist;
503 warn "Saisei Response Content is\n".$client->responseContent."\n" if ($self->option('debug') && !$does_not_exist);
506 elsif ($client->responseCode() eq '500') {
507 $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();
508 warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
512 $self->{'__saisei_error'} = "Received Bad response from server during $method $path $data, we received responce code: " . $client->responseCode() . " " . $client->responseContent;
513 warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
523 Returns the error string set by L</Saisei API> methods,
524 or a blank string if most recent call produced no errors.
530 return $self->{'__saisei_error'} || '';
533 =head2 api_get_policies
535 Gets a list of global policies.
539 sub api_get_policies {
542 my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
543 return if $self->api_error;
544 $self->{'__saisei_error'} = "Did not receive any global policies from Saisei."
545 unless $get_policies;
547 return $get_policies->{collection};
550 =head2 api_get_rateplan
552 Gets rateplan info for specific rateplan.
556 sub api_get_rateplan {
558 my $rateplan = shift;
560 my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
561 return if $self->api_error;
563 return $get_rateplan;
568 Gets user info for specific user.
576 my $get_user = $self->api_call("GET", "/users/$user");
577 return if $self->api_error;
582 =head2 api_get_accesspoint
584 Gets user info for specific access point.
588 sub api_get_accesspoint {
590 my $accesspoint = shift;
592 my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
593 return if $self->api_error;
595 return $get_accesspoint;
600 Gets user info for specific host.
608 my $get_host = $self->api_call("GET", "/hosts/$ip");
610 return { message => $self->api_error, } if $self->api_error;
615 =head2 api_create_rateplan
621 sub api_create_rateplan {
622 my ($self, $svc, $rateplan) = @_;
624 $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};
625 $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};
627 my $new_rateplan = $self->api_call(
629 "/rate_plans/$rateplan",
631 'downstream_rate' => $svc->{Hash}->{speed_down},
632 'upstream_rate' => $svc->{Hash}->{speed_up},
634 ) unless $self->{'__saisei_error'};
636 $self->{'__saisei_error'} = "Saisei could not create the rate plan $rateplan."
637 unless ($new_rateplan || $self->{'__saisei_error'});
639 return $new_rateplan;
643 =head2 api_modify_rateplan
645 Modify a new rateplan.
649 sub api_modify_rateplan {
650 my ($self,$svc,$rateplan_name) = @_;
653 my $policies = $self->api_get_policies();
655 foreach my $policy (@$policies) {
656 my $policyname = $policy->{name};
657 my $rate_multiplier = '';
658 if ($policy->{background}) { $rate_multiplier = ".01"; }
659 my $modified_rateplan = $self->api_call(
661 "/rate_plans/$rateplan_name/partitions/$policyname",
663 'restricted' => $policy->{assured}, # policy_assured_flag
664 'rate_multiplier' => $rate_multiplier, # policy_background 0.1
665 'rate' => $policy->{percent_rate}, # policy_percent_rate
669 $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name after it was created."
670 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
678 =head2 api_modify_existing_rateplan
680 Modify a existing rateplan.
684 sub api_modify_existing_rateplan {
685 my ($self,$svc,$rateplan_name) = @_;
687 $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};
688 $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};
690 my $modified_rateplan = $self->api_call(
692 "/rate_plans/$rateplan_name",
694 'downstream_rate' => $svc->{Hash}->{speed_down},
695 'upstream_rate' => $svc->{Hash}->{speed_up},
699 $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name."
700 unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
706 =head2 api_create_user
712 sub api_create_user {
713 my ($self,$user, $description, $location) = @_;
716 'description' => $description,
718 $user_hash->{'map_location'} = $location if $location;
720 my $new_user = $self->api_call(
726 $self->{'__saisei_error'} = "Saisei could not create the user $user"
727 unless ($new_user || $self->{'__saisei_error'}); # should never happen
733 =head2 api_modify_user
739 sub api_modify_user {
740 my ($self,$user, $description, $location) = @_;
743 'description' => $description,
745 $user_hash->{'map_location'} = $location if $location;
747 my $modify_user = $self->api_call(
753 $self->{'__saisei_error'} = "Saisei could not modify the user $user"
754 unless ($modify_user || $self->{'__saisei_error'}); # should never happen
760 =head2 api_create_accesspoint
762 Creates a access point.
766 sub api_create_accesspoint {
767 my ($self,$accesspoint, $upratelimit, $downratelimit, $location) = @_;
770 'downstream_rate_limit' => $downratelimit,
771 'upstream_rate_limit' => $upratelimit,
772 'interface' => $self->option('interface'),
774 $ap_hash->{'map_location'} = $location if $location;
776 my $new_accesspoint = $self->api_call(
778 "/access_points/$accesspoint",
782 $self->{'__saisei_error'} = "Saisei could not create the access point $accesspoint"
783 unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
788 =head2 api_modify_accesspoint
790 Modify a new access point.
794 sub api_modify_accesspoint {
795 my ($self, $accesspoint, $uplink, $location) = @_;
799 'interface' => $self->option('interface'),
801 $ap_hash->{'map_location'} = $location if $location;
803 my $modified_accesspoint = $self->api_call(
805 "/access_points/$accesspoint",
809 $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint after it was created."
810 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
816 =head2 api_modify_existing_accesspoint
818 Modify a existing accesspoint.
822 sub api_modify_existing_accesspoint {
823 my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit, $location) = @_;
826 'downstream_rate_limit' => $downratelimit,
827 'upstream_rate_limit' => $upratelimit,
828 'interface' => $self->option('interface'),
829 # 'uplink' => $uplink, # name of attached access point
831 $ap_hash->{'map_location'} = $location if $location;
833 my $modified_accesspoint = $self->api_call(
835 "/access_points/$accesspoint",
839 $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint."
840 unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
846 =head2 api_add_host_to_user
848 ties host to user, rateplan and default access point.
852 sub api_add_host_to_user {
853 # my ($self,$user, $rateplan, $ip, $accesspoint, $location) = @_;
854 my ($self,$opt) = @_;
855 my $ip = $opt->{'ip'};
856 my $location = $opt->{'location'};
859 'user' => $opt->{'user'},
860 'rate_plan' => $opt->{'rateplan'},
861 'access_point' => $opt->{'accesspoint'},
863 $newhost_hash->{'map_location'} = $location if $location;
865 my $new_host = $self->api_call(
871 $self->{'__saisei_error'} = "Saisei could not create the host $ip"
872 unless ($new_host || $self->{'__saisei_error'}); # should never happen
878 =head2 api_delete_host_to_user
880 unties host from user and rateplan.
881 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
885 sub api_delete_host_to_user {
886 my ($self,$user, $rateplan, $ip) = @_;
888 my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
889 return if $self->api_error;
890 $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."
891 unless $default_rate_plan;
893 my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
895 my $delete_host = $self->api_call(
900 'access_point' => '<none>',
901 'rate_plan' => $default_rateplan_name,
905 $self->{'__saisei_error'} = "Saisei could not delete the host $ip"
906 unless ($delete_host || $self->{'__saisei_error'}); # should never happen
913 my ($self, $opt) = @_;
915 if (!$opt->{tower_uprate_limit} || !$opt->{tower_downrate_limit}) {
916 $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.";
917 return { error => $self->api_error, };
920 my $existing_tower_ap;
921 my $tower_name = $opt->{tower_name};
922 my $location = $opt->{location};
924 #check if tower has been set up as an access point.
925 $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
927 # modify the existing accesspoint if changing tower .
928 $self->api_modify_existing_accesspoint (
930 '', # tower does not have a uplink on sectors.
931 $opt->{tower_uprate_limit},
932 $opt->{tower_downrate_limit},
934 ) if $existing_tower_ap->{collection} && $opt->{modify_existing};
936 #if tower does not exist as an access point create it.
937 $self->api_create_accesspoint(
939 $opt->{tower_uprate_limit},
940 $opt->{tower_downrate_limit},
942 ) unless $existing_tower_ap->{collection};
944 my $accesspoint = $self->api_get_accesspoint($tower_name);
946 return { error => $self->api_error, } if $self->api_error;
951 my ($self, $opt) = @_;
953 if (!$opt->{sector_name} || $opt->{sector_name} eq '_default') {
954 $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.";
955 return { error => $self->api_error, };
958 if (!$opt->{sector_uprate_limit} || !$opt->{sector_downrate_limit}) {
959 $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.";
960 return { error => $self->api_error, };
963 my $existing_sector_ap;
964 my $sector_name = $opt->{sector_name};
965 my $location = $opt->{location};
967 #check if sector has been set up as an access point.
968 $existing_sector_ap = $self->api_get_accesspoint($sector_name);
970 # modify the existing accesspoint if changing sector .
971 $self->api_modify_existing_accesspoint (
974 $opt->{sector_uprate_limit},
975 $opt->{sector_downrate_limit},
977 ) if $existing_sector_ap && $opt->{modify_existing};
979 #if sector does not exist as an access point create it.
980 $self->api_create_accesspoint(
982 $opt->{sector_uprate_limit},
983 $opt->{sector_downrate_limit},
985 ) unless $existing_sector_ap;
987 # Attach newly created sector to it's tower.
988 $self->api_modify_accesspoint($sector_name, $opt->{tower_name}, $location) unless ($self->{'__saisei_error'} || $existing_sector_ap);
990 # set access point to existing one or newly created one.
991 my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
993 return { error => $self->api_error, } if $self->api_error;
997 =head2 get_svc_location
999 sets location to lat and long from service, if no service location gets it from package, if still no location returns null.
1003 sub get_svc_location {
1004 my ($self, $svc) = @_;
1006 my $svc_location = '';
1007 $svc_location = $svc->{Hash}->{latitude}.','.$svc->{Hash}->{longitude} if ($svc->{Hash}->{latitude} && $svc->{Hash}->{longitude});
1009 if (!$svc_location) {
1010 my $pkg_location = FS::Record::qsearchs({
1011 'table' => 'cust_pkg',
1012 'addl_from' => 'LEFT JOIN cust_location USING (locationnum)',
1013 'hashref' => { 'pkgnum' => $svc->{Hash}->{pkgnum} },
1015 $svc_location = $pkg_location->{Hash}->{latitude}.','.$pkg_location->{Hash}->{longitude} if ($pkg_location->{Hash}->{latitude} && $pkg_location->{Hash}->{longitude});
1018 return $svc_location;
1021 =head2 require_tower_and_sector
1023 sets whether the service export requires a sector with it's tower.
1027 sub require_tower_and_sector {
1031 =head2 tower_sector_required_fields
1033 required fields needed for tower and sector export.
1037 sub tower_sector_required_fields {
1040 'up_rate_limit' => '1',
1041 'down_rate_limit' => '1',
1044 'up_rate_limit' => '1',
1045 'down_rate_limit' => '1',
1052 sub required_fields {
1053 my @fields = ('svc_broadband__ip_addr_required', 'svc_broadband__speed_up_required', 'svc_broadband__speed_down_required', 'svc_broadband__sectornum_required');
1057 sub process_virtual_ap {
1058 my ($self, $opt) = @_;
1060 my $existing_virtual_ap;
1061 my $virtual_name = $opt->{virtual_name};
1063 #check if virtual_ap has been set up as an access point.
1064 $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
1066 # modify the existing virtual accesspoint if changing it. this should never happen
1067 $self->api_modify_existing_accesspoint (
1069 $opt->{sector_name},
1070 $opt->{virtual_uprate_limit},
1071 $opt->{virtual_downrate_limit},
1073 ) if $existing_virtual_ap && $opt->{modify_existing};
1075 #if virtual ap does not exist as an access point create it.
1076 $self->api_create_accesspoint(
1078 $opt->{virtual_uprate_limit},
1079 $opt->{virtual_downrate_limit},
1081 ) unless $existing_virtual_ap;
1084 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})) {
1088 # Attach newly created virtual ap to tower sector ap or if sector has changed.
1089 $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}, $opt->{location}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
1091 # set access point to existing one or newly created one.
1092 my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
1094 return $accesspoint;
1097 sub export_provisioned_services {
1100 my $force_update = shift;
1102 my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1103 or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n";
1106 my @svcparts = FS::Record::qsearch({
1107 'table' => 'export_svc',
1108 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ',
1109 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
1111 my $part_count = scalar @svcparts;
1113 my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1115 my @svcs = FS::Record::qsearch({
1116 'table' => 'cust_svc',
1117 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
1118 'extra_sql' => " WHERE svcpart in ('".$parts."')",
1121 my $svc_count = scalar @svcs;
1124 for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
1126 my $process_count=0;
1127 foreach my $svc (@svcs) {
1128 if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1129 ## check if service exists as host if not export it.
1130 my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
1131 die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message};
1132 warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1134 if ($force_update) { $export_error = _export_insert($part_export,$svc,$force_update); }
1135 else { $export_error = _export_insert($part_export,$svc) unless $host->{collection}; }
1136 if ($export_error) {
1137 warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1138 die ("$export_error\n");
1147 sub export_all_towers_sectors {
1151 my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1152 or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n";
1155 my @towers = FS::Record::qsearch({
1158 my $tower_count = scalar @towers;
1161 for (my $c=1; $c <=100; $c=$c+1) { $status{int($tower_count * ($c/100))} = $c; }
1163 my $process_count=0;
1164 foreach my $tower (@towers) {
1165 if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1166 my $export_error = export_tower_sector($part_export,$tower);
1167 if ($export_error->{'error'}) {
1168 warn "Error exporting tower/sector (".$tower->{Hash}->{towername}.")\n" if ($part_export->option('debug'));
1169 die ($export_error->{'error'}."\n");
1178 sub force_export_all_virtual_ap {
1181 my $force_update = { 'update_virtual_ap' => '1', };
1183 export_provisioned_services($job,$param,$force_update);
1188 sub force_export_all_users {
1192 my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
1193 or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}. This export does not exist.\n";
1196 my @svcparts = FS::Record::qsearch({
1197 'table' => 'export_svc',
1198 'addl_from' => 'LEFT JOIN part_svc USING ( svcpart ) ',
1199 'hashref' => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
1201 my $part_count = scalar @svcparts;
1203 my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1205 my @svcs = FS::Record::qsearch({
1206 'table' => 'cust_svc',
1207 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
1208 'extra_sql' => " WHERE svcpart in ('".$parts."')",
1211 my $svc_count = scalar @svcs;
1214 for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
1216 my $process_count=0;
1217 foreach my $svc (@svcs) {
1218 my $description = $svc->{Hash}->{description};
1219 my $user = $svc->{Hash}->{svcnum};
1220 my $svc_location = get_svc_location($job, $svc);
1221 if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
1222 warn "Exporting user ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
1223 my $export_error = export_user($part_export,$user,$description, $svc_location);
1224 if ($export_error) {
1225 warn "Error exporting user ".$svc->{Hash}->{svcnum}."\n" if ($part_export->option('debug'));
1226 die ($export_error->{'error'}."\n");
1235 sub test_export_report {
1236 my ($self, $opts) = @_;
1239 ## check all part services for export errors
1240 my @exports = FS::Record::qsearch('part_export', { 'exporttype' => "saisei", } );
1241 my $export_nums = join "', '", map { $_->{Hash}->{exportnum} } @exports;
1243 my $svc_part_export_error;
1244 my @svcparts = FS::Record::qsearch({
1245 'table' => 'part_svc',
1246 'addl_from' => 'LEFT JOIN export_svc USING ( svcpart ) ',
1247 'extra_sql' => " WHERE export_svc.exportnum in ('".$export_nums."')",
1249 my $part_count = scalar @svcparts;
1252 foreach (@svcparts) {
1253 my $part_error->{'description'} = $_->svc;
1254 $part_error->{'link'} = $opts->{'fsurl'}."/edit/part_svc.cgi?".$_->svcpart;
1256 foreach my $s ('speed_up', 'speed_down') {
1257 my $speed = $_->part_svc_column($s);
1258 if ($speed->columnflag eq "" || $speed->columnflag eq "D") {
1259 $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";
1261 elsif ($speed->columnflag eq "F" || $speed->columnflag eq "S") {
1262 $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;
1264 elsif ($speed->columnflag eq "P") {
1265 my $fcc_speed_name = "broadband_".$speed->columnvalue."stream";
1266 foreach my $part_pkg ( FS::Record::qsearchs({
1267 'table' => 'part_pkg',
1268 'addl_from' => 'LEFT JOIN pkg_svc USING ( pkgpart ) ',
1269 'extra_sql' => " WHERE pkg_svc.svcpart = ".$_->svcpart,
1271 my $pkglink = '<a href="'.$opts->{'fsurl'}.'/edit/part_pkg.cgi?'.$part_pkg->pkgpart.'"><FONT COLOR="red"><B>'.$part_pkg->pkg.'</B></FONT></a>';
1272 $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."
1273 unless $part_pkg->fcc_option($fcc_speed_name);
1277 $part_error->{'errors'}->{'ip_addr'} = "Field IP Address is not set to required" if $_->part_svc_column("ip_addr")->required ne "Y";
1278 $svc_part_error->{$_->svcpart} = $part_error if $part_error->{'errors'};
1281 $svc_part_export_error->{"services"}->{'description'} = "Service definitions";
1282 $svc_part_export_error->{"services"}->{'count'} = $part_count;
1283 $svc_part_export_error->{"services"}->{'errors'} = $svc_part_error if $svc_part_error;
1285 push @export_error, $svc_part_export_error;
1287 ## check all provisioned cust services for export errors
1288 my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1289 my $cust_svc_export_error;
1290 my @svcs = FS::Record::qsearch({
1291 'table' => 'cust_svc',
1292 'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum ) ',
1293 'extra_sql' => " WHERE svcpart in ('".$parts."')",
1295 my $svc_count = scalar @svcs;
1299 my $svc_error->{'description'} = $_->description;
1300 $svc_error->{'link'} = $opts->{'fsurl'}."/edit/svc_broadband.cgi?".$_->svcnum;
1302 foreach my $s ('speed_up', 'speed_down', 'ip_addr') {
1303 $svc_error->{'errors'}->{$s} = "Field ".$s." is not set and is required for this service to be exported to Saisei." unless $_->$s;
1306 my $sector = FS::Record::qsearchs({
1307 'table' => 'tower_sector',
1308 'extra_sql' => " WHERE sectornum = ".$_->sectornum." AND sectorname != '_default'",
1309 }) if $_->sectornum;
1311 $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.";
1314 foreach my $s ('up_rate_limit', 'down_rate_limit') {
1315 $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."
1319 $cust_svc_error->{$_->svcnum} = $svc_error if $svc_error->{'errors'};
1322 $cust_svc_export_error->{"provisioned_services"}->{'description'} = "Provisioned services";
1323 $cust_svc_export_error->{"provisioned_services"}->{'count'} = $svc_count;
1324 $cust_svc_export_error->{"provisioned_services"}->{'errors'} = $cust_svc_error if $cust_svc_error;
1326 push @export_error, $cust_svc_export_error;
1329 ## check all towers and sectors for export errors
1330 my $tower_sector_export_error;
1331 my @towers = FS::Record::qsearch({
1334 my $tower_count = scalar @towers;
1338 my $tower_error->{'description'} = $_->towername;
1339 $tower_error->{'link'} = $opts->{'fsurl'}."/edit/tower.html?".$_->towernum;
1341 foreach my $s ('up_rate_limit', 'down_rate_limit') {
1342 $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;
1345 my @sectors = FS::Record::qsearch({
1346 'table' => 'tower_sector',
1347 'extra_sql' => " WHERE towernum = ".$_->towernum." AND sectorname != '_default' AND (up_rate_limit IS NULL OR down_rate_limit IS NULL)",
1349 foreach my $sector (@sectors) {
1350 foreach my $s ('up_rate_limit', 'down_rate_limit') {
1351 $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."
1355 $towers_error->{$_->towernum} = $tower_error if $tower_error->{'errors'};
1358 $tower_sector_export_error->{"tower_sector"}->{'description'} = "Tower / Sector";
1359 $tower_sector_export_error->{"tower_sector"}->{'count'} = $tower_count;
1360 $tower_sector_export_error->{"tower_sector"}->{'errors'} = $towers_error if $towers_error;
1362 push @export_error, $tower_sector_export_error;
1364 return [@export_error];