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