RT# 78356 - Updated documentation
[freeside.git] / FS / FS / part_export / saisei.pm
1 package FS::part_export::saisei;
2
3 use strict;
4 use vars qw( @ISA %info );
5 use base qw( FS::part_export );
6 use Date::Format 'time2str';
7 use Cpanel::JSON::XS;
8 use MIME::Base64;
9 use REST::Client;
10 use Data::Dumper;
11 use FS::Conf;
12
13 =pod
14
15 =head1 NAME
16
17 FS::part_export::saisei
18
19 =head1 SYNOPSIS
20
21 Saisei integration for Freeside
22
23 =head1 DESCRIPTION
24
25 This export offers basic svc_broadband provisioning for Saisei.
26
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.
30
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.
33
34 To use this export, follow the below instructions:
35
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.
39
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.
44
45 Create a package for the above created service, and order this package for a customer.
46
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.
49
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>.
51
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.
54
55 This module also provides generic methods for working through the L</Saisei API>.
56
57 =cut
58
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>',
64                                     },
65 ;
66
67 tie my %options, 'Tie::IxHash',
68   'port'             => { label => 'Port',
69                           default => 5000 },
70   'username'         => { label => 'Saisei API User Name',
71                           default => '' },
72   'password'         => { label => 'Saisei API Password',
73                           default => '' },
74   'debug'            => { type => 'checkbox',
75                           label => 'Enable debug warnings' },
76 ;
77
78 %info = (
79   'svc'             => 'svc_broadband',
80   'desc'            => 'Export broadband service/account to Saisei',
81   'options'         => \%options,
82   'scripts'         => \%scripts,
83   'notes'           => <<'END',
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.
87 <P>
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.
90 <P>
91 To use this export, follow the below instructions:
92 <P>
93 <OL>
94 <LI>
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.
98 </LI>
99 <P>
100 <LI>
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.
105 </LI>
106 <P>
107 <LI>
108 Create a package for the above created service, and order this package for a customer.
109 </LI>
110 <P>
111 <LI>
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.
114 <P>
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>.
116 </LI>
117 <P>
118 <LI>
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.
121 </LI>
122 </OL>
123 <P>
124
125 END
126 );
127
128 sub _export_insert {
129   my ($self, $svc_broadband) = @_;
130
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;
134
135   # check for existing rate plan
136   my $existing_rateplan;
137   $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
138
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'};
143
144   # set rateplan to existing one or newly created one.
145   my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
146
147   my $username = $svc_broadband->{Hash}->{svcnum};
148   my $description = $svc_broadband->{Hash}->{description};
149
150   if (!$username) {
151     $self->{'__saisei_error'} = 'no username - can not export';
152     return $self->api_error;
153   }
154   else {
155     # check for existing user.
156     my $existing_user;
157     $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
158  
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'};
162
163     # set user to existing one or newly created one.
164     my $user = $existing_user ? $existing_user : $self->api_get_user($username);
165
166     ## add access point
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 )',
176       'hashref'   => {
177                         'sectornum' => $svc_broadband->{Hash}->{sectornum},
178                      },
179     });
180
181     my $tower_name = $tower_sector->{Hash}->{towername};
182     $tower_name =~ s/\s/_/g;
183
184     my $tower_opt = {
185       'tower_name'           => $tower_name,
186       'tower_uprate_limit'   => $tower_sector->{Hash}->{tower_upratelimit},
187       'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
188     };
189
190     my $tower_ap = process_tower($self, $tower_opt);
191     return $self->api_error if $self->{'__saisei_error'};
192
193     my $sector_name = $tower_sector->{Hash}->{sectorname};
194     $sector_name =~ s/\s/_/g;
195
196     my $sector_opt = {
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},
201     };
202     my $accesspoint = process_sector($self, $sector_opt);
203     return $self->api_error if $self->{'__saisei_error'};
204
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'};
212   }
213
214   return $self->api_error;
215
216 }
217
218 sub _export_replace {
219   my ($self, $svc_broadband) = @_;
220   return '';
221 }
222
223 sub _export_delete {
224   my ($self, $svc_broadband) = @_;
225
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};
230
231   ## untie host to user
232   $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
233
234   return '';
235 }
236
237 sub _export_suspend {
238   my ($self, $svc_broadband) = @_;
239   return '';
240 }
241
242 sub _export_unsuspend {
243   my ($self, $svc_broadband) = @_;
244   return '';
245 }
246
247 sub export_partsvc {
248   my ($self, $svc_part) = @_;
249
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};
254
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;
258
259   # check for existing rate plan
260   my $existing_rateplan;
261   $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
262
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);
265
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);
269
270   return $self->api_error;
271
272 }
273
274 sub export_tower_sector {
275   my ($self, $tower) = @_;
276
277   #modify tower or create it.
278   my $tower_name = $tower->{Hash}->{towername};
279   $tower_name =~ s/\s/_/g;
280   my $tower_opt = {
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
285   };
286
287   my $tower_access_point = process_tower($self, $tower_opt);
288
289   #get list of all access points
290   my $hash_opt = {
291       'table'     => 'tower_sector',
292       'select'    => '*',
293       'hashref'   => { 'towernum' => $tower->{Hash}->{towernum}, },
294   };
295
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;
300     my $sector_opt = {
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
306     };
307     my $sector_access_point = process_sector($self, $sector_opt);
308   }
309
310   return $self->api_error;
311 }
312
313 =head1 Saisei API
314
315 These methods allow access to the Saisei API using the credentials
316 set in the export options.
317
318 =cut
319
320 =head2 api_call
321
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>.
326
327 =cut
328
329 sub api_call {
330   my ($self,$method,$path,$params) = @_;
331
332   $self->{'__saisei_error'} = '';
333   my $auth_info = $self->option('username') . ':' . $self->option('password');
334   $params ||= {};
335
336   warn "Calling $method on http://"
337     .$self->{Hash}->{machine}.':'.$self->option('port')
338     ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
339
340   my $data = encode_json($params) if keys %{ $params };
341
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'});
346
347   warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
348
349   my $result;
350
351   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
352     eval { $result = decode_json($client->responseContent()) };
353     unless ($result) {
354       $self->{'__saisei_error'} = "Error decoding json: $@";
355       return;
356     }
357   }
358   else {
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');
362     return; 
363   }
364
365   return $result;
366   
367 }
368
369 =head2 api_error
370
371 Returns the error string set by L</Saisei API> methods,
372 or a blank string if most recent call produced no errors.
373
374 =cut
375
376 sub api_error {
377   my $self = shift;
378   return $self->{'__saisei_error'} || '';
379 }
380
381 =head2 api_get_policies
382
383 Gets a list of global policies.
384
385 =cut
386
387 sub api_get_policies {
388   my $self = shift;
389
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;
394
395   return $get_policies->{collection};
396 }
397
398 =head2 api_get_rateplan
399
400 Gets rateplan info for specific rateplan.
401
402 =cut
403
404 sub api_get_rateplan {
405   my $self = shift;
406   my $rateplan = shift;
407
408   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
409   return if $self->api_error;
410
411   return $get_rateplan;
412 }
413
414 =head2 api_get_user
415
416 Gets user info for specific user.
417
418 =cut
419
420 sub api_get_user {
421   my $self = shift;
422   my $user = shift;
423
424   my $get_user = $self->api_call("GET", "/users/$user");
425   return if $self->api_error;
426
427   return $get_user;
428 }
429
430 =head2 api_get_accesspoint
431
432 Gets user info for specific access point.
433
434 =cut
435
436 sub api_get_accesspoint {
437   my $self = shift;
438   my $accesspoint = shift;
439
440   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
441   return if $self->api_error;
442
443   return $get_accesspoint;
444 }
445
446 =head2 api_get_host
447
448 Gets user info for specific host.
449
450 =cut
451
452 sub api_get_host {
453   my $self = shift;
454   my $ip = shift;
455
456   my $get_host = $self->api_call("GET", "/hosts/$ip");
457
458   return if $self->api_error;
459
460   return $get_host;
461 }
462
463 =head2 api_create_rateplan
464
465 Creates a rateplan.
466
467 =cut
468
469 sub api_create_rateplan {
470   my ($self, $svc, $rateplan) = @_;
471
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};
474
475   my $new_rateplan = $self->api_call(
476       "PUT", 
477       "/rate_plans/$rateplan",
478       {
479         'downstream_rate' => $svc->{Hash}->{speed_down},
480         'upstream_rate' => $svc->{Hash}->{speed_up},
481       },
482   ) unless $self->{'__saisei_error'};
483
484   $self->{'__saisei_error'} = "Rate Plan not created"
485     unless ($new_rateplan || $self->{'__saisei_error'});
486
487   return $new_rateplan;
488
489 }
490
491 =head2 api_modify_rateplan
492
493 Modify a new rateplan.
494
495 =cut
496
497 sub api_modify_rateplan {
498   my ($self,$svc,$rateplan_name) = @_;
499
500   # get policy list
501   my $policies = $self->api_get_policies();
502
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(
508       "PUT", 
509       "/rate_plans/$rateplan_name/partitions/$policyname",
510       {
511         'restricted'      =>  $policy->{assured},         # policy_assured_flag
512         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
513         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
514       },
515     );
516
517     $self->{'__saisei_error'} = "Rate Plan not modified after create"
518       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
519     
520   }
521
522   return;
523  
524 }
525
526 =head2 api_modify_existing_rateplan
527
528 Modify a existing rateplan.
529
530 =cut
531
532 sub api_modify_existing_rateplan {
533   my ($self,$svc,$rateplan_name) = @_;
534
535   my $modified_rateplan = $self->api_call(
536     "PUT",
537     "/rate_plans/$rateplan_name",
538     {
539       'downstream_rate' => $svc->{Hash}->{speed_down},
540       'upstream_rate' => $svc->{Hash}->{speed_up},
541     },
542   );
543
544     $self->{'__saisei_error'} = "Rate Plan not modified"
545       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
546
547   return;
548
549 }
550
551 =head2 api_create_user
552
553 Creates a user.
554
555 =cut
556
557 sub api_create_user {
558   my ($self,$user, $description) = @_;
559
560   my $new_user = $self->api_call(
561       "PUT", 
562       "/users/$user",
563       {
564         'description' => $description,
565       },
566   );
567
568   $self->{'__saisei_error'} = "User not created"
569     unless ($new_user || $self->{'__saisei_error'}); # should never happen
570
571   return $new_user;
572
573 }
574
575 =head2 api_create_accesspoint
576
577 Creates a access point.
578
579 =cut
580
581 sub api_create_accesspoint {
582   my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
583
584   # this has not been tested, but should work, if needed.
585   my $new_accesspoint = $self->api_call(
586       "PUT",
587       "/access_points/$accesspoint",
588       {
589          'downstream_rate_limit' => $downratelimit,
590          'upstream_rate_limit' => $upratelimit,
591       },
592   );
593
594   $self->{'__saisei_error'} = "Access point not created"
595     unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
596   return;
597
598 }
599
600 =head2 api_modify_accesspoint
601
602 Modify a new access point.
603
604 =cut
605
606 sub api_modify_accesspoint {
607   my ($self, $accesspoint, $uplink) = @_;
608
609   my $modified_accesspoint = $self->api_call(
610     "PUT",
611     "/access_points/$accesspoint",
612     {
613       'uplink' => $uplink, # name of attached access point
614     },
615   );
616
617   $self->{'__saisei_error'} = "Rate Plan not modified"
618     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
619
620   return;
621
622 }
623
624 =head2 api_modify_existing_accesspoint
625
626 Modify a existing accesspoint.
627
628 =cut
629
630 sub api_modify_existing_accesspoint {
631   my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
632
633   my $modified_accesspoint = $self->api_call(
634     "PUT",
635     "/access_points/$accesspoint",
636     {
637       'downstream_rate_limit' => $downratelimit,
638       'upstream_rate_limit' => $upratelimit,
639 #      'uplink' => $uplink, # name of attached access point
640     },
641   );
642
643     $self->{'__saisei_error'} = "Access point not modified"
644       unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
645
646   return;
647
648 }
649
650 =head2 api_add_host_to_user
651
652 ties host to user, rateplan and default access point.
653
654 =cut
655
656 sub api_add_host_to_user {
657   my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
658
659   my $new_host = $self->api_call(
660       "PUT", 
661       "/hosts/$ip",
662       {
663         'user'      => $user,
664         'rate_plan' => $rateplan,
665         'access_point' => $accesspoint,
666       },
667   );
668
669   $self->{'__saisei_error'} = "Host not created"
670     unless ($new_host || $self->{'__saisei_error'}); # should never happen
671
672   return $new_host;
673
674 }
675
676 =head2 api_delete_host_to_user
677
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>.
680
681 =cut
682
683 sub api_delete_host_to_user {
684   my ($self,$user, $rateplan, $ip) = @_;
685
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;
690
691   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
692
693   my $delete_host = $self->api_call(
694       "PUT",
695       "/hosts/$ip",
696       {
697         'user'          => '<none>',
698         'access_point'  => '<none>',
699         'rate_plan'     => $default_rateplan_name,
700       },
701   );
702
703   $self->{'__saisei_error'} = "Host not created"
704     unless ($delete_host || $self->{'__saisei_error'}); # should never happen
705
706   return $delete_host;
707
708 }
709
710 sub process_tower {
711   my ($self, $opt) = @_;
712
713   my $existing_tower_ap;
714   my $tower_name = $opt->{tower_name};
715
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'};
718
719   # modify the existing accesspoint if changing tower .
720   $self->api_modify_existing_accesspoint (
721     $tower_name,
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};
726
727   #if tower does not exist as an access point create it.
728   $self->api_create_accesspoint(
729       $tower_name,
730       $opt->{tower_uprate_limit},
731       $opt->{tower_downrate_limit}
732   ) unless $existing_tower_ap;
733
734   my $accesspoint = $self->api_get_accesspoint($tower_name);
735
736   return $accesspoint;
737 }
738
739 sub process_sector {
740   my ($self, $opt) = @_;
741
742   my $existing_sector_ap;
743   my $sector_name = $opt->{sector_name};
744
745   #check if sector has been set up as an access point.
746   $existing_sector_ap = $self->api_get_accesspoint($sector_name);
747
748   # modify the existing accesspoint if changing sector .
749   $self->api_modify_existing_accesspoint (
750     $sector_name,
751     $opt->{tower_name},
752     $opt->{sector_uprate_limit},
753     $opt->{sector_downrate_limit},
754   ) if $existing_sector_ap && $opt->{modify_existing};
755
756   #if sector does not exist as an access point create it.
757   $self->api_create_accesspoint(
758     $sector_name,
759     $opt->{sector_uprate_limit},
760     $opt->{sector_downrate_limit},
761   ) unless $existing_sector_ap;
762
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);
765
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);
768
769   return $accesspoint;
770 }
771
772 sub export_provisioned_services {
773   my $job = shift;
774   my $param = shift;
775
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}";
778   bless $part_export;
779
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}, },
784   });
785   my $part_count = scalar @svcparts;
786
787   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
788
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."')",
793   }) unless !$parts;
794
795   my $svc_count = scalar @svcs;
796
797   my %status = {};
798   for (my $c=10; $c <=100; $c=$c+10) { $status{int($svc_count * ($c/100))} = $c; }
799
800   my $process_count=0;
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});
805     $process_count++;
806   }
807
808   return;
809
810 }
811
812 =head1 SEE ALSO
813
814 L<FS::part_export>
815
816 =cut
817
818 1;