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