83cb4ffd9999f2ba8d2b871a780f570ff1702722
[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 Storable qw(thaw);
9 use MIME::Base64;
10 use REST::Client;
11 use Data::Dumper;
12 use FS::Conf;
13
14 =pod
15
16 =head1 NAME
17
18 FS::part_export::saisei
19
20 =head1 SYNOPSIS
21
22 Saisei integration for Freeside
23
24 =head1 DESCRIPTION
25
26 This export offers basic svc_broadband provisioning for Saisei.
27
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.
31
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.
34
35 To use this export, follow the below instructions:
36
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.
40
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
46 Create a package for the above created service, and order this package for a customer.
47
48 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.
49 This provisioned service will then be exported as a host to Saisei.
50
51 Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
52
53 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.
54 Clicking on this link will export all services attached to this export not currently exported to Saisei.
55
56 This module also provides generic methods for working through the L</Saisei API>.
57
58 =cut
59
60 tie my %scripts, 'Tie::IxHash',
61   'export_provisioned_services'  => { component => '/elements/popup_link.html',
62                                       label     => 'Export provisioned services',
63                                       description => 'will export provisioned services of part service with Saisei export attached.',
64                                       html_label => '<b>Export provisioned services attached to this export.</b>',
65                                     },
66 ;
67
68 tie my %options, 'Tie::IxHash',
69   'port'             => { label => 'Port',
70                           default => 5000 },
71   'username'         => { label => 'Saisei API User Name',
72                           default => '' },
73   'password'         => { label => 'Saisei API Password',
74                           default => '' },
75   'debug'            => { type => 'checkbox',
76                           label => 'Enable debug warnings' },
77 ;
78
79 %info = (
80   'svc'             => 'svc_broadband',
81   'desc'            => 'Export broadband service/account to Saisei',
82   'options'         => \%options,
83   'scripts'         => \%scripts,
84   'notes'           => <<'END',
85 This is a customer integration with Saisei.  This will set up a rate plan and tie
86 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
87 It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
88 <P>
89 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
90 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
91 <P>
92 To use this export, follow the below instructions:
93 <P>
94 <OL>
95 <LI>
96 Create a new service definition and set the table to svc_broadband.  The service name will become the Saisei rate plan name.
97 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
98 Attach this Saisei export to this service.
99 </LI>
100 <P>
101 <LI>
102 Create a tower and add a sector to that tower.  The sector name will be the name of the access point,
103 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.
104 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.
105 Each sector will be attached to its tower access point using the Saisei uplink field.
106 </LI>
107 <P>
108 <LI>
109 Create a package for the above created service, and order this package for a customer.
110 </LI>
111 <P>
112 <LI>
113 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.
114 This provisioned service will then be exported as a host to Saisei.
115 <P>
116 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>.
117 </LI>
118 <P>
119 <LI>
120 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>.
121 Clicking on this link will export all services attached to this export not currently exported to Saisei.
122 </LI>
123 </OL>
124 <P>
125
126 END
127 );
128
129 sub _export_insert {
130   my ($self, $svc_broadband) = @_;
131
132   my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
133   my $rateplan_name = $service_part->{Hash}->{svc};
134   $rateplan_name =~ s/\s/_/g;
135
136   # check for existing rate plan
137   my $existing_rateplan;
138   $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
139
140   # if no existing rate plan create one and modify it.
141   $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
142   $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
143   return $self->api_error if $self->{'__saisei_error'};
144
145   # set rateplan to existing one or newly created one.
146   my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
147
148   my $username = $svc_broadband->{Hash}->{svcnum};
149   my $description = $svc_broadband->{Hash}->{description};
150
151   if (!$username) {
152     $self->{'__saisei_error'} = 'no username - can not export';
153     return $self->api_error;
154   }
155   else {
156     # check for existing user.
157     my $existing_user;
158     $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
159  
160     # if no existing user create one.
161     $self->api_create_user($username, $description) unless $existing_user;
162     return $self->api_error if $self->{'__saisei_error'};
163
164     # set user to existing one or newly created one.
165     my $user = $existing_user ? $existing_user : $self->api_get_user($username);
166
167     ## add access point
168     my $tower_sector = FS::Record::qsearchs({
169       'table'     => 'tower_sector',
170       'select'    => 'tower.towername,
171                       tower.up_rate_limit as tower_upratelimit,
172                       tower.down_rate_limit as tower_downratelimit,
173                       tower_sector.sectorname,
174                       tower_sector.up_rate_limit as sector_upratelimit,
175                       tower_sector.down_rate_limit as sector_downratelimit ',
176       'addl_from' => 'LEFT JOIN tower USING ( towernum )',
177       'hashref'   => {
178                         'sectornum' => $svc_broadband->{Hash}->{sectornum},
179                      },
180     });
181
182     my $tower_name = $tower_sector->{Hash}->{towername};
183     $tower_name =~ s/\s/_/g;
184
185     my $tower_opt = {
186       'tower_name'           => $tower_name,
187       'tower_uprate_limit'   => $tower_sector->{Hash}->{tower_upratelimit},
188       'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
189     };
190
191     my $tower_ap = process_tower($self, $tower_opt);
192     return $self->api_error if $self->{'__saisei_error'};
193
194     my $sector_name = $tower_sector->{Hash}->{sectorname};
195     $sector_name =~ s/\s/_/g;
196
197     my $sector_opt = {
198       'tower_name'            => $tower_name,
199       'sector_name'           => $sector_name,
200       'sector_uprate_limit'   => $tower_sector->{Hash}->{sector_upratelimit},
201       'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
202     };
203     my $accesspoint = process_sector($self, $sector_opt);
204     return $self->api_error if $self->{'__saisei_error'};
205
206     ## tie host to user add sector name as access point.
207     $self->api_add_host_to_user(
208       $user->{collection}->[0]->{name},
209       $rateplan->{collection}->[0]->{name},
210       $svc_broadband->{Hash}->{ip_addr},
211       $accesspoint->{collection}->[0]->{name},
212     ) unless $self->{'__saisei_error'};
213   }
214
215   return $self->api_error;
216
217 }
218
219 sub _export_replace {
220   my ($self, $svc_broadband) = @_;
221   return '';
222 }
223
224 sub _export_delete {
225   my ($self, $svc_broadband) = @_;
226
227   my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
228   my $rateplan_name = $service_part->{Hash}->{svc};
229   $rateplan_name =~ s/\s/_/g;
230   my $username = $svc_broadband->{Hash}->{svcnum};
231
232   ## untie host to user
233   $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
234
235   return '';
236 }
237
238 sub _export_suspend {
239   my ($self, $svc_broadband) = @_;
240   return '';
241 }
242
243 sub _export_unsuspend {
244   my ($self, $svc_broadband) = @_;
245   return '';
246 }
247
248 sub export_partsvc {
249   my ($self, $svc_part) = @_;
250
251   my $rateplan_name = $svc_part->{Hash}->{svc};
252   $rateplan_name =~ s/\s/_/g;
253   my $speeddown = $svc_part->{Hash}->{svc_broadband__speed_down};
254   my $speedup = $svc_part->{Hash}->{svc_broadband__speed_up};
255
256   my $temp_svc = $svc_part->{Hash};
257   my $svc_broadband = {};
258   map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; }  } keys %$temp_svc;
259
260   # check for existing rate plan
261   my $existing_rateplan;
262   $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
263
264   # Modify the existing rate plan with new service data.
265   $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
266
267   # if no existing rate plan create one and modify it.
268   $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
269   $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
270
271   return $self->api_error;
272
273 }
274
275 sub export_tower_sector {
276   my ($self, $tower) = @_;
277
278   #modify tower or create it.
279   my $tower_name = $tower->{Hash}->{towername};
280   $tower_name =~ s/\s/_/g;
281   my $tower_opt = {
282     'tower_name'           => $tower_name,
283     'tower_uprate_limit'   => $tower->{Hash}->{up_rate_limit},
284     'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
285     'modify_existing'      => '1', # modify an existing access point with this info
286   };
287
288   my $tower_access_point = process_tower($self, $tower_opt);
289
290   #get list of all access points
291   my $hash_opt = {
292       'table'     => 'tower_sector',
293       'select'    => '*',
294       'hashref'   => { 'towernum' => $tower->{Hash}->{towernum}, },
295   };
296
297   #for each one modify or create it.
298   foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
299     my $sector_name = $tower_sector->{Hash}->{sectorname};
300     $sector_name =~ s/\s/_/g;
301     my $sector_opt = {
302       'tower_name'            => $tower_name,
303       'sector_name'           => $sector_name,
304       'sector_uprate_limit'   => $tower_sector->{Hash}->{up_rate_limit},
305       'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
306       'modify_existing'       => '1', # modify an existing access point with this info
307     };
308     my $sector_access_point = process_sector($self, $sector_opt);
309   }
310
311   return $self->api_error;
312 }
313
314 =head1 Saisei API
315
316 These methods allow access to the Saisei API using the credentials
317 set in the export options.
318
319 =cut
320
321 =head2 api_call
322
323 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
324 Places an api call to the specified path and method with the specified params.
325 Returns the decoded json object returned by the api call.
326 Returns empty on failure;  retrieve error messages using L</api_error>.
327
328 =cut
329
330 sub api_call {
331   my ($self,$method,$path,$params) = @_;
332
333   $self->{'__saisei_error'} = '';
334   my $auth_info = $self->option('username') . ':' . $self->option('password');
335   $params ||= {};
336
337   warn "Calling $method on http://"
338     .$self->{Hash}->{machine}.':'.$self->option('port')
339     ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
340
341   my $data = encode_json($params) if keys %{ $params };
342
343   my $client = REST::Client->new();
344   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
345   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
346   $client->$method('/rest/stm/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
347
348   warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
349
350   my $result;
351
352   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
353     eval { $result = decode_json($client->responseContent()) };
354     unless ($result) {
355       $self->{'__saisei_error'} = "Error decoding json: $@";
356       return;
357     }
358   }
359   else {
360     $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent()
361     unless ($method eq "GET");
362     warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
363     return; 
364   }
365
366   return $result;
367   
368 }
369
370 =head2 api_error
371
372 Returns the error string set by L</Saisei API> methods,
373 or a blank string if most recent call produced no errors.
374
375 =cut
376
377 sub api_error {
378   my $self = shift;
379   return $self->{'__saisei_error'} || '';
380 }
381
382 =head2 api_get_policies
383
384 Gets a list of global policies.
385
386 =cut
387
388 sub api_get_policies {
389   my $self = shift;
390
391   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
392   return if $self->api_error;
393   $self->{'__saisei_error'} = "Did not receive any global policies"
394     unless $get_policies;
395
396   return $get_policies->{collection};
397 }
398
399 =head2 api_get_rateplan
400
401 Gets rateplan info for specific rateplan.
402
403 =cut
404
405 sub api_get_rateplan {
406   my $self = shift;
407   my $rateplan = shift;
408
409   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
410   return if $self->api_error;
411
412   return $get_rateplan;
413 }
414
415 =head2 api_get_user
416
417 Gets user info for specific user.
418
419 =cut
420
421 sub api_get_user {
422   my $self = shift;
423   my $user = shift;
424
425   my $get_user = $self->api_call("GET", "/users/$user");
426   return if $self->api_error;
427
428   return $get_user;
429 }
430
431 =head2 api_get_accesspoint
432
433 Gets user info for specific access point.
434
435 =cut
436
437 sub api_get_accesspoint {
438   my $self = shift;
439   my $accesspoint = shift;
440
441   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
442   return if $self->api_error;
443
444   return $get_accesspoint;
445 }
446
447 =head2 api_get_host
448
449 Gets user info for specific host.
450
451 =cut
452
453 sub api_get_host {
454   my $self = shift;
455   my $ip = shift;
456
457   my $get_host = $self->api_call("GET", "/hosts/$ip");
458
459   return if $self->api_error;
460
461   return $get_host;
462 }
463
464 =head2 api_create_rateplan
465
466 Creates a rateplan.
467
468 =cut
469
470 sub api_create_rateplan {
471   my ($self, $svc, $rateplan) = @_;
472
473   $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
474   $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
475
476   my $new_rateplan = $self->api_call(
477       "PUT", 
478       "/rate_plans/$rateplan",
479       {
480         'downstream_rate' => $svc->{Hash}->{speed_down},
481         'upstream_rate' => $svc->{Hash}->{speed_up},
482       },
483   ) unless $self->{'__saisei_error'};
484
485   $self->{'__saisei_error'} = "Rate Plan not created"
486     unless ($new_rateplan || $self->{'__saisei_error'});
487
488   return $new_rateplan;
489
490 }
491
492 =head2 api_modify_rateplan
493
494 Modify a new rateplan.
495
496 =cut
497
498 sub api_modify_rateplan {
499   my ($self,$svc,$rateplan_name) = @_;
500
501   # get policy list
502   my $policies = $self->api_get_policies();
503
504   foreach my $policy (@$policies) {
505     my $policyname = $policy->{name};
506     my $rate_multiplier = '';
507     if ($policy->{background}) { $rate_multiplier = ".01"; }
508     my $modified_rateplan = $self->api_call(
509       "PUT", 
510       "/rate_plans/$rateplan_name/partitions/$policyname",
511       {
512         'restricted'      =>  $policy->{assured},         # policy_assured_flag
513         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
514         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
515       },
516     );
517
518     $self->{'__saisei_error'} = "Rate Plan not modified after create"
519       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
520     
521   }
522
523   return;
524  
525 }
526
527 =head2 api_modify_existing_rateplan
528
529 Modify a existing rateplan.
530
531 =cut
532
533 sub api_modify_existing_rateplan {
534   my ($self,$svc,$rateplan_name) = @_;
535
536   my $modified_rateplan = $self->api_call(
537     "PUT",
538     "/rate_plans/$rateplan_name",
539     {
540       'downstream_rate' => $svc->{Hash}->{speed_down},
541       'upstream_rate' => $svc->{Hash}->{speed_up},
542     },
543   );
544
545     $self->{'__saisei_error'} = "Rate Plan not modified"
546       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
547
548   return;
549
550 }
551
552 =head2 api_create_user
553
554 Creates a user.
555
556 =cut
557
558 sub api_create_user {
559   my ($self,$user, $description) = @_;
560
561   my $new_user = $self->api_call(
562       "PUT", 
563       "/users/$user",
564       {
565         'description' => $description,
566       },
567   );
568
569   $self->{'__saisei_error'} = "User not created"
570     unless ($new_user || $self->{'__saisei_error'}); # should never happen
571
572   return $new_user;
573
574 }
575
576 =head2 api_create_accesspoint
577
578 Creates a access point.
579
580 =cut
581
582 sub api_create_accesspoint {
583   my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
584
585   # this has not been tested, but should work, if needed.
586   my $new_accesspoint = $self->api_call(
587       "PUT",
588       "/access_points/$accesspoint",
589       {
590          'downstream_rate_limit' => $downratelimit,
591          'upstream_rate_limit' => $upratelimit,
592       },
593   );
594
595   $self->{'__saisei_error'} = "Access point not created"
596     unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
597   return;
598
599 }
600
601 =head2 api_modify_accesspoint
602
603 Modify a new access point.
604
605 =cut
606
607 sub api_modify_accesspoint {
608   my ($self, $accesspoint, $uplink) = @_;
609
610   my $modified_accesspoint = $self->api_call(
611     "PUT",
612     "/access_points/$accesspoint",
613     {
614       'uplink' => $uplink, # name of attached access point
615     },
616   );
617
618   $self->{'__saisei_error'} = "Rate Plan not modified"
619     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
620
621   return;
622
623 }
624
625 =head2 api_modify_existing_accesspoint
626
627 Modify a existing accesspoint.
628
629 =cut
630
631 sub api_modify_existing_accesspoint {
632   my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
633
634   my $modified_accesspoint = $self->api_call(
635     "PUT",
636     "/access_points/$accesspoint",
637     {
638       'downstream_rate_limit' => $downratelimit,
639       'upstream_rate_limit' => $upratelimit,
640 #      'uplink' => $uplink, # name of attached access point
641     },
642   );
643
644     $self->{'__saisei_error'} = "Access point not modified"
645       unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
646
647   return;
648
649 }
650
651 =head2 api_add_host_to_user
652
653 ties host to user, rateplan and default access point.
654
655 =cut
656
657 sub api_add_host_to_user {
658   my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
659
660   my $new_host = $self->api_call(
661       "PUT", 
662       "/hosts/$ip",
663       {
664         'user'      => $user,
665         'rate_plan' => $rateplan,
666         'access_point' => $accesspoint,
667       },
668   );
669
670   $self->{'__saisei_error'} = "Host not created"
671     unless ($new_host || $self->{'__saisei_error'}); # should never happen
672
673   return $new_host;
674
675 }
676
677 =head2 api_delete_host_to_user
678
679 unties host from user and rateplan.
680 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
681
682 =cut
683
684 sub api_delete_host_to_user {
685   my ($self,$user, $rateplan, $ip) = @_;
686
687   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
688     return if $self->api_error;
689   $self->{'__saisei_error'} = "Did not receive a default rate plan"
690     unless $default_rate_plan;
691
692   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
693
694   my $delete_host = $self->api_call(
695       "PUT",
696       "/hosts/$ip",
697       {
698         'user'          => '<none>',
699         'access_point'  => '<none>',
700         'rate_plan'     => $default_rateplan_name,
701       },
702   );
703
704   $self->{'__saisei_error'} = "Host not created"
705     unless ($delete_host || $self->{'__saisei_error'}); # should never happen
706
707   return $delete_host;
708
709 }
710
711 sub process_tower {
712   my ($self, $opt) = @_;
713
714   my $existing_tower_ap;
715   my $tower_name = $opt->{tower_name};
716
717   #check if tower has been set up as an access point.
718   $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
719
720   # modify the existing accesspoint if changing tower .
721   $self->api_modify_existing_accesspoint (
722     $tower_name,
723     '', # tower does not have a uplink on sectors.
724     $opt->{tower_uprate_limit},
725     $opt->{tower_downrate_limit},
726   ) if $existing_tower_ap && $opt->{modify_existing};
727
728   #if tower does not exist as an access point create it.
729   $self->api_create_accesspoint(
730       $tower_name,
731       $opt->{tower_uprate_limit},
732       $opt->{tower_downrate_limit}
733   ) unless $existing_tower_ap;
734
735   my $accesspoint = $self->api_get_accesspoint($tower_name);
736
737   return $accesspoint;
738 }
739
740 sub process_sector {
741   my ($self, $opt) = @_;
742
743   my $existing_sector_ap;
744   my $sector_name = $opt->{sector_name};
745
746   #check if sector has been set up as an access point.
747   $existing_sector_ap = $self->api_get_accesspoint($sector_name);
748
749   # modify the existing accesspoint if changing sector .
750   $self->api_modify_existing_accesspoint (
751     $sector_name,
752     $opt->{tower_name},
753     $opt->{sector_uprate_limit},
754     $opt->{sector_downrate_limit},
755   ) if $existing_sector_ap && $opt->{modify_existing};
756
757   #if sector does not exist as an access point create it.
758   $self->api_create_accesspoint(
759     $sector_name,
760     $opt->{sector_uprate_limit},
761     $opt->{sector_downrate_limit},
762   ) unless $existing_sector_ap;
763
764   # Attach newly created sector to it's tower.
765   $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
766
767   # set access point to existing one or newly created one.
768   my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
769
770   return $accesspoint;
771 }
772
773 sub export_provisioned_services {
774   my $job = shift;
775   my $param = thaw(decode_base64(shift));
776
777   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
778   or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
779   bless $part_export;
780
781   my @svcparts = FS::Record::qsearch({
782     'table' => 'export_svc',
783     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
784     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
785   });
786   my $part_count = scalar @svcparts;
787
788   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
789
790   my @svcs = FS::Record::qsearch({
791     'table' => 'cust_svc',
792     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
793     'extra_sql' => " WHERE svcpart in ('".$parts."')",
794   }) unless !$parts;
795
796   my $svc_count = scalar @svcs;
797
798   my %status = {};
799   for (my $c=10; $c <=100; $c=$c+10) { $status{int($svc_count * ($c/100))} = $c; }
800
801   my $process_count=0;
802   foreach my $svc (@svcs) {
803     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
804     ## check if service exists as host if not export it.
805     _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr});
806     $process_count++;
807   }
808
809   return;
810
811 }
812
813 =head1 SEE ALSO
814
815 L<FS::part_export>
816
817 =cut
818
819 1;