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