RT# 81131 - updated Saisei export to create a virtual ap for multiple ips per customer
[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 $rateplan_name = $self->get_rateplan_name($svc_broadband);
133
134   # check for existing rate plan
135   my $existing_rateplan;
136   $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
137
138   # if no existing rate plan create one and modify it.
139   $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
140   $self->api_modify_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
141   return $self->api_error if $self->{'__saisei_error'};
142
143   # set rateplan to existing one or newly created one.
144   my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
145
146   my $username = $svc_broadband->{Hash}->{svcnum};
147   my $description = $svc_broadband->{Hash}->{description};
148
149   if (!$username) {
150     $self->{'__saisei_error'} = 'no username - can not export';
151     return $self->api_error;
152   }
153   else {
154     # check for existing user.
155     my $existing_user;
156     $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
157  
158     # if no existing user create one.
159     $self->api_create_user($username, $description) unless $existing_user;
160     return $self->api_error if $self->{'__saisei_error'};
161
162     # set user to existing one or newly created one.
163     my $user = $existing_user ? $existing_user : $self->api_get_user($username);
164
165     ## add access point
166     my $tower_sector = FS::Record::qsearchs({
167       'table'     => 'tower_sector',
168       'select'    => 'tower.towername,
169                       tower.up_rate_limit as tower_upratelimit,
170                       tower.down_rate_limit as tower_downratelimit,
171                       tower_sector.sectorname,
172                       tower_sector.up_rate_limit as sector_upratelimit,
173                       tower_sector.down_rate_limit as sector_downratelimit ',
174       'addl_from' => 'LEFT JOIN tower USING ( towernum )',
175       'hashref'   => {
176                         'sectornum' => $svc_broadband->{Hash}->{sectornum},
177                      },
178     });
179
180     my $tower_name = $tower_sector->{Hash}->{towername};
181     $tower_name =~ s/\s/_/g;
182
183     my $tower_opt = {
184       'tower_name'           => $tower_name,
185       'tower_uprate_limit'   => $tower_sector->{Hash}->{tower_upratelimit},
186       'tower_downrate_limit' => $tower_sector->{Hash}->{tower_downratelimit},
187     };
188
189     my $tower_ap = process_tower($self, $tower_opt);
190     return $self->api_error if $self->{'__saisei_error'};
191
192     my $sector_name = $tower_sector->{Hash}->{sectorname};
193     $sector_name =~ s/\s/_/g;
194
195     my $sector_opt = {
196       'tower_name'            => $tower_name,
197       'sector_name'           => $sector_name,
198       'sector_uprate_limit'   => $tower_sector->{Hash}->{sector_upratelimit},
199       'sector_downrate_limit' => $tower_sector->{Hash}->{sector_downratelimit},
200     };
201     my $accesspoint = process_sector($self, $sector_opt);
202     return $self->api_error if $self->{'__saisei_error'};
203
204 ## get custnum and pkgpart from cust_pkg for virtual access point
205     my $cust_pkg = FS::Record::qsearchs({
206       'table'     => 'cust_pkg',
207       'hashref'   => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, },
208     });
209     my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up};
210
211     my $virtual_ap_opt = {
212       'virtual_name'           => $virtual_ap_name,
213       'sector_name'            => $sector_name,
214       'virtual_uprate_limit'   => $svc_broadband->{Hash}->{speed_up},
215       'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down},
216     };
217     my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt);
218     return $self->api_error if $self->{'__saisei_error'};
219
220     ## tie host to user add sector name as access point.
221     $self->api_add_host_to_user(
222       $user->{collection}->[0]->{name},
223       $rateplan->{collection}->[0]->{name},
224       $svc_broadband->{Hash}->{ip_addr},
225       $virtual_ap->{collection}->[0]->{name},
226     ) unless $self->{'__saisei_error'};
227   }
228
229   return $self->api_error;
230
231 }
232
233 sub _export_replace {
234   my ($self, $svc_broadband) = @_;
235   my $error = $self->_export_insert($svc_broadband);
236   return $error;
237 }
238
239 sub _export_delete {
240   my ($self, $svc_broadband) = @_;
241
242   my $rateplan_name = $self->get_rateplan_name($svc_broadband);
243
244   my $username = $svc_broadband->{Hash}->{svcnum};
245
246   ## untie host to user
247   $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
248
249   return '';
250 }
251
252 sub _export_suspend {
253   my ($self, $svc_broadband) = @_;
254   return '';
255 }
256
257 sub _export_unsuspend {
258   my ($self, $svc_broadband) = @_;
259   return '';
260 }
261
262 sub export_partsvc {
263   my ($self, $svc_part) = @_;
264
265   my $fcc_477_speeds;
266   if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
267     for my $type (qw( down up )) {
268       my $speed_type = "broadband_".$type."stream";
269       foreach my $pkg_svc (FS::Record::qsearch({
270         'table'     => 'pkg_svc',
271         'select'    => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
272         'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
273         'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
274       })) { $fcc_477_speeds->{
275         $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
276     }
277   }
278   else {
279     $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
280     $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
281   }
282
283   foreach my $key (keys %$fcc_477_speeds) {
284
285     $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
286     $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
287     $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
288     $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
289
290     my $temp_svc = $svc_part->{Hash};
291     my $svc_broadband = {};
292     map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; }  } keys %$temp_svc;
293
294     my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
295
296     # check for existing rate plan
297     my $existing_rateplan;
298     $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
299
300     # Modify the existing rate plan with new service data.
301     $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
302
303     # if no existing rate plan create one and modify it.
304     $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
305     $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
306
307   }
308
309   return $self->api_error;
310
311 }
312
313 sub export_tower_sector {
314   my ($self, $tower) = @_;
315
316   #modify tower or create it.
317   my $tower_name = $tower->{Hash}->{towername};
318   $tower_name =~ s/\s/_/g;
319   my $tower_opt = {
320     'tower_name'           => $tower_name,
321     'tower_uprate_limit'   => $tower->{Hash}->{up_rate_limit},
322     'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
323     'modify_existing'      => '1', # modify an existing access point with this info
324   };
325
326   my $tower_access_point = process_tower($self, $tower_opt);
327
328   #get list of all access points
329   my $hash_opt = {
330       'table'     => 'tower_sector',
331       'select'    => '*',
332       'hashref'   => { 'towernum' => $tower->{Hash}->{towernum}, },
333   };
334
335   #for each one modify or create it.
336   foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
337     my $sector_name = $tower_sector->{Hash}->{sectorname};
338     $sector_name =~ s/\s/_/g;
339     my $sector_opt = {
340       'tower_name'            => $tower_name,
341       'sector_name'           => $sector_name,
342       'sector_uprate_limit'   => $tower_sector->{Hash}->{up_rate_limit},
343       'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
344       'modify_existing'       => '1', # modify an existing access point with this info
345     };
346     my $sector_access_point = process_sector($self, $sector_opt);
347   }
348
349   return $self->api_error;
350 }
351
352 ## creates the rateplan name
353 sub get_rateplan_name {
354   my ($self, $svc_broadband, $svc_name) = @_;
355
356   my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
357   my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
358
359   my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
360   $rateplan_name =~ s/\s/_/g;
361
362   return $rateplan_name;
363 }
364
365 =head1 Saisei API
366
367 These methods allow access to the Saisei API using the credentials
368 set in the export options.
369
370 =cut
371
372 =head2 api_call
373
374 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
375 Places an api call to the specified path and method with the specified params.
376 Returns the decoded json object returned by the api call.
377 Returns empty on failure;  retrieve error messages using L</api_error>.
378
379 =cut
380
381 sub api_call {
382   my ($self,$method,$path,$params) = @_;
383
384   $self->{'__saisei_error'} = '';
385   my $auth_info = $self->option('username') . ':' . $self->option('password');
386   $params ||= {};
387
388   warn "Calling $method on http://"
389     .$self->{Hash}->{machine}.':'.$self->option('port')
390     ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
391
392   my $data = encode_json($params) if keys %{ $params };
393
394   my $client = REST::Client->new();
395   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
396   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
397   $client->$method('/rest/stm/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
398
399   warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
400
401   my $result;
402
403   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
404     eval { $result = decode_json($client->responseContent()) };
405     unless ($result) {
406       $self->{'__saisei_error'} = "Error decoding json: $@";
407       return;
408     }
409   }
410   else {
411     $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent()
412     unless ($method eq "GET");
413     warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
414     return; 
415   }
416
417   return $result;
418   
419 }
420
421 =head2 api_error
422
423 Returns the error string set by L</Saisei API> methods,
424 or a blank string if most recent call produced no errors.
425
426 =cut
427
428 sub api_error {
429   my $self = shift;
430   return $self->{'__saisei_error'} || '';
431 }
432
433 =head2 api_get_policies
434
435 Gets a list of global policies.
436
437 =cut
438
439 sub api_get_policies {
440   my $self = shift;
441
442   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
443   return if $self->api_error;
444   $self->{'__saisei_error'} = "Did not receive any global policies"
445     unless $get_policies;
446
447   return $get_policies->{collection};
448 }
449
450 =head2 api_get_rateplan
451
452 Gets rateplan info for specific rateplan.
453
454 =cut
455
456 sub api_get_rateplan {
457   my $self = shift;
458   my $rateplan = shift;
459
460   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
461   return if $self->api_error;
462
463   return $get_rateplan;
464 }
465
466 =head2 api_get_user
467
468 Gets user info for specific user.
469
470 =cut
471
472 sub api_get_user {
473   my $self = shift;
474   my $user = shift;
475
476   my $get_user = $self->api_call("GET", "/users/$user");
477   return if $self->api_error;
478
479   return $get_user;
480 }
481
482 =head2 api_get_accesspoint
483
484 Gets user info for specific access point.
485
486 =cut
487
488 sub api_get_accesspoint {
489   my $self = shift;
490   my $accesspoint = shift;
491
492   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
493   return if $self->api_error;
494
495   return $get_accesspoint;
496 }
497
498 =head2 api_get_host
499
500 Gets user info for specific host.
501
502 =cut
503
504 sub api_get_host {
505   my $self = shift;
506   my $ip = shift;
507
508   my $get_host = $self->api_call("GET", "/hosts/$ip");
509
510   return if $self->api_error;
511
512   return $get_host;
513 }
514
515 =head2 api_create_rateplan
516
517 Creates a rateplan.
518
519 =cut
520
521 sub api_create_rateplan {
522   my ($self, $svc, $rateplan) = @_;
523
524   $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
525   $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
526
527   my $new_rateplan = $self->api_call(
528       "PUT", 
529       "/rate_plans/$rateplan",
530       {
531         'downstream_rate' => $svc->{Hash}->{speed_down},
532         'upstream_rate' => $svc->{Hash}->{speed_up},
533       },
534   ) unless $self->{'__saisei_error'};
535
536   $self->{'__saisei_error'} = "Rate Plan not created"
537     unless ($new_rateplan || $self->{'__saisei_error'});
538
539   return $new_rateplan;
540
541 }
542
543 =head2 api_modify_rateplan
544
545 Modify a new rateplan.
546
547 =cut
548
549 sub api_modify_rateplan {
550   my ($self,$svc,$rateplan_name) = @_;
551
552   # get policy list
553   my $policies = $self->api_get_policies();
554
555   foreach my $policy (@$policies) {
556     my $policyname = $policy->{name};
557     my $rate_multiplier = '';
558     if ($policy->{background}) { $rate_multiplier = ".01"; }
559     my $modified_rateplan = $self->api_call(
560       "PUT", 
561       "/rate_plans/$rateplan_name/partitions/$policyname",
562       {
563         'restricted'      =>  $policy->{assured},         # policy_assured_flag
564         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
565         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
566       },
567     );
568
569     $self->{'__saisei_error'} = "Rate Plan not modified after create"
570       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
571     
572   }
573
574   return;
575  
576 }
577
578 =head2 api_modify_existing_rateplan
579
580 Modify a existing rateplan.
581
582 =cut
583
584 sub api_modify_existing_rateplan {
585   my ($self,$svc,$rateplan_name) = @_;
586
587   my $modified_rateplan = $self->api_call(
588     "PUT",
589     "/rate_plans/$rateplan_name",
590     {
591       'downstream_rate' => $svc->{Hash}->{speed_down},
592       'upstream_rate' => $svc->{Hash}->{speed_up},
593     },
594   );
595
596     $self->{'__saisei_error'} = "Rate Plan not modified"
597       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
598
599   return;
600
601 }
602
603 =head2 api_create_user
604
605 Creates a user.
606
607 =cut
608
609 sub api_create_user {
610   my ($self,$user, $description) = @_;
611
612   my $new_user = $self->api_call(
613       "PUT", 
614       "/users/$user",
615       {
616         'description' => $description,
617       },
618   );
619
620   $self->{'__saisei_error'} = "User not created"
621     unless ($new_user || $self->{'__saisei_error'}); # should never happen
622
623   return $new_user;
624
625 }
626
627 =head2 api_create_accesspoint
628
629 Creates a access point.
630
631 =cut
632
633 sub api_create_accesspoint {
634   my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
635
636   # this has not been tested, but should work, if needed.
637   my $new_accesspoint = $self->api_call(
638       "PUT",
639       "/access_points/$accesspoint",
640       {
641          'downstream_rate_limit' => $downratelimit,
642          'upstream_rate_limit' => $upratelimit,
643       },
644   );
645
646   $self->{'__saisei_error'} = "Access point not created"
647     unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
648   return;
649
650 }
651
652 =head2 api_modify_accesspoint
653
654 Modify a new access point.
655
656 =cut
657
658 sub api_modify_accesspoint {
659   my ($self, $accesspoint, $uplink) = @_;
660
661   my $modified_accesspoint = $self->api_call(
662     "PUT",
663     "/access_points/$accesspoint",
664     {
665       'uplink' => $uplink, # name of attached access point
666     },
667   );
668
669   $self->{'__saisei_error'} = "Rate Plan not modified"
670     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
671
672   return;
673
674 }
675
676 =head2 api_modify_existing_accesspoint
677
678 Modify a existing accesspoint.
679
680 =cut
681
682 sub api_modify_existing_accesspoint {
683   my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
684
685   my $modified_accesspoint = $self->api_call(
686     "PUT",
687     "/access_points/$accesspoint",
688     {
689       'downstream_rate_limit' => $downratelimit,
690       'upstream_rate_limit' => $upratelimit,
691 #      'uplink' => $uplink, # name of attached access point
692     },
693   );
694
695     $self->{'__saisei_error'} = "Access point not modified"
696       unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
697
698   return;
699
700 }
701
702 =head2 api_add_host_to_user
703
704 ties host to user, rateplan and default access point.
705
706 =cut
707
708 sub api_add_host_to_user {
709   my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
710
711   my $new_host = $self->api_call(
712       "PUT", 
713       "/hosts/$ip",
714       {
715         'user'      => $user,
716         'rate_plan' => $rateplan,
717         'access_point' => $accesspoint,
718       },
719   );
720
721   $self->{'__saisei_error'} = "Host not created"
722     unless ($new_host || $self->{'__saisei_error'}); # should never happen
723
724   return $new_host;
725
726 }
727
728 =head2 api_delete_host_to_user
729
730 unties host from user and rateplan.
731 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
732
733 =cut
734
735 sub api_delete_host_to_user {
736   my ($self,$user, $rateplan, $ip) = @_;
737
738   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
739     return if $self->api_error;
740   $self->{'__saisei_error'} = "Did not receive a default rate plan"
741     unless $default_rate_plan;
742
743   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
744
745   my $delete_host = $self->api_call(
746       "PUT",
747       "/hosts/$ip",
748       {
749         'user'          => '<none>',
750         'access_point'  => '<none>',
751         'rate_plan'     => $default_rateplan_name,
752       },
753   );
754
755   $self->{'__saisei_error'} = "Host not created"
756     unless ($delete_host || $self->{'__saisei_error'}); # should never happen
757
758   return $delete_host;
759
760 }
761
762 sub process_tower {
763   my ($self, $opt) = @_;
764
765   my $existing_tower_ap;
766   my $tower_name = $opt->{tower_name};
767
768   #check if tower has been set up as an access point.
769   $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
770
771   # modify the existing accesspoint if changing tower .
772   $self->api_modify_existing_accesspoint (
773     $tower_name,
774     '', # tower does not have a uplink on sectors.
775     $opt->{tower_uprate_limit},
776     $opt->{tower_downrate_limit},
777   ) if $existing_tower_ap && $opt->{modify_existing};
778
779   #if tower does not exist as an access point create it.
780   $self->api_create_accesspoint(
781       $tower_name,
782       $opt->{tower_uprate_limit},
783       $opt->{tower_downrate_limit}
784   ) unless $existing_tower_ap;
785
786   my $accesspoint = $self->api_get_accesspoint($tower_name);
787
788   return $accesspoint;
789 }
790
791 sub process_sector {
792   my ($self, $opt) = @_;
793
794   my $existing_sector_ap;
795   my $sector_name = $opt->{sector_name};
796
797   #check if sector has been set up as an access point.
798   $existing_sector_ap = $self->api_get_accesspoint($sector_name);
799
800   # modify the existing accesspoint if changing sector .
801   $self->api_modify_existing_accesspoint (
802     $sector_name,
803     $opt->{tower_name},
804     $opt->{sector_uprate_limit},
805     $opt->{sector_downrate_limit},
806   ) if $existing_sector_ap && $opt->{modify_existing};
807
808   #if sector does not exist as an access point create it.
809   $self->api_create_accesspoint(
810     $sector_name,
811     $opt->{sector_uprate_limit},
812     $opt->{sector_downrate_limit},
813   ) unless $existing_sector_ap;
814
815   # Attach newly created sector to it's tower.
816   $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
817
818   # set access point to existing one or newly created one.
819   my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
820
821   return $accesspoint;
822 }
823
824 sub process_virtual_ap {
825   my ($self, $opt) = @_;
826
827   my $existing_virtual_ap;
828   my $virtual_name = $opt->{virtual_name};
829
830   #check if sector has been set up as an access point.
831   $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
832
833   # modify the existing virtual accesspoint if changing it. this should never happen
834   $self->api_modify_existing_accesspoint (
835     $virtual_name,
836     $opt->{sector_name},
837     $opt->{virtual_uprate_limit},
838     $opt->{virtual_downrate_limit},
839   ) if $existing_virtual_ap && $opt->{modify_existing};
840
841   #if virtual ap does not exist as an access point create it.
842   $self->api_create_accesspoint(
843     $virtual_name,
844     $opt->{virtual_uprate_limit},
845     $opt->{virtual_downrate_limit},
846   ) unless $existing_virtual_ap;
847
848 my $update_sector;
849 if ($existing_virtual_ap && ($existing_virtual_ap->{collection}->[0]->{uplink}->{link}->{name} ne $opt->{sector_name})) {
850   $update_sector = 1;
851 }
852
853   # Attach newly created virtual ap to tower sector ap or if sector has changed.
854   $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
855
856   # set access point to existing one or newly created one.
857   my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
858
859   return $accesspoint;
860 }
861
862 sub export_provisioned_services {
863   my $job = shift;
864   my $param = thaw(decode_base64(shift));
865
866   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
867   or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
868   bless $part_export;
869
870   my @svcparts = FS::Record::qsearch({
871     'table' => 'export_svc',
872     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
873     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
874   });
875   my $part_count = scalar @svcparts;
876
877   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
878
879   my @svcs = FS::Record::qsearch({
880     'table' => 'cust_svc',
881     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
882     'extra_sql' => " WHERE svcpart in ('".$parts."')",
883   }) unless !$parts;
884
885   my $svc_count = scalar @svcs;
886
887   my %status = {};
888   for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
889
890   my $process_count=0;
891   foreach my $svc (@svcs) {
892     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
893     ## check if service exists as host if not export it.
894     _export_insert($part_export,$svc) unless api_get_host($part_export, $svc->{Hash}->{ip_addr});
895     $process_count++;
896   }
897
898   return;
899
900 }
901
902 =head1 SEE ALSO
903
904 L<FS::part_export>
905
906 =cut
907
908 1;