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