RT# 82087 - added logging for decode json error
[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     return $tower_access_point if $tower_access_point->{error};
328
329   #get list of all access points
330   my $hash_opt = {
331       'table'     => 'tower_sector',
332       'select'    => '*',
333       'hashref'   => { 'towernum' => $tower->{Hash}->{towernum}, },
334   };
335
336   #for each one modify or create it.
337   foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
338     my $sector_name = $tower_sector->{Hash}->{sectorname};
339     $sector_name =~ s/\s/_/g;
340     my $sector_opt = {
341       'tower_name'            => $tower_name,
342       'sector_name'           => $sector_name,
343       'sector_uprate_limit'   => $tower_sector->{Hash}->{up_rate_limit},
344       'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
345       'modify_existing'       => '1', # modify an existing access point with this info
346     };
347     my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default");
348       return $sector_access_point if $sector_access_point->{error};
349   }
350
351   return { error => $self->api_error, };
352 }
353
354 ## creates the rateplan name
355 sub get_rateplan_name {
356   my ($self, $svc_broadband, $svc_name) = @_;
357
358   my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
359   my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
360
361   my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
362   $rateplan_name =~ s/\s/_/g; $rateplan_name =~ s/[^A-Za-z0-9\-_]//g;
363
364   return $rateplan_name;
365 }
366
367 =head1 Saisei API
368
369 These methods allow access to the Saisei API using the credentials
370 set in the export options.
371
372 =cut
373
374 =head2 api_call
375
376 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
377 Places an api call to the specified path and method with the specified params.
378 Returns the decoded json object returned by the api call.
379 Returns empty on failure;  retrieve error messages using L</api_error>.
380
381 =cut
382
383 sub api_call {
384   my ($self,$method,$path,$params) = @_;
385
386   $self->{'__saisei_error'} = '';
387   my $auth_info = $self->option('username') . ':' . $self->option('password');
388   $params ||= {};
389
390   warn "Calling $method on http://"
391     .$self->{Hash}->{machine}.':'.$self->option('port')
392     ."/rest/top/configurations/running/$path\n" if $self->option('debug');
393
394   my $data = encode_json($params) if keys %{ $params };
395
396   my $client = REST::Client->new();
397   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
398   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
399   $client->$method('/rest/top/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
400
401   warn "Saisei Response Code is ".$client->responseCode()."\n" if $self->option('debug');
402
403   my $result;
404
405   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
406     eval { $result = decode_json($client->responseContent()) };
407     unless ($result) {
408       $self->{'__saisei_error'} = "Error decoding json from Saisei";
409       warn "Saisei RC 201 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
410       return;
411     }
412   }
413   elsif ($client->responseCode() eq '404') {
414     eval { $result = decode_json($client->responseContent()) };
415     unless ($result) {
416       $self->{'__saisei_error'} = "Error decoding json from Saisei";
417       warn "Saisei RC 404 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
418       return;
419     }
420     ## check if message is for empty hash.
421     my($does_not_exist) = $result->{message} =~ /'(.*)' does not exist$/;
422     $self->{'__saisei_error'} = "Error ".$result->{message} unless $does_not_exist;
423     warn "Saisei Response Content is\n".$client->responseContent."\n" if ($self->option('debug') && !$does_not_exist);
424     return;
425   }
426   elsif ($client->responseCode() eq '500') {
427     $self->{'__saisei_error'} = "Can't connect to host during $method , received responce code: " . $client->responseCode() . " and message: " . $client->responseContent();
428     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
429     return;
430   }
431   else {
432     $self->{'__saisei_error'} = "Bad response from server during $method , received responce code: " . $client->responseCode() . " and message: " . $client->responseContent();
433     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
434     return; 
435   }
436
437   return $result;
438   
439 }
440
441 =head2 api_error
442
443 Returns the error string set by L</Saisei API> methods,
444 or a blank string if most recent call produced no errors.
445
446 =cut
447
448 sub api_error {
449   my $self = shift;
450   return $self->{'__saisei_error'} || '';
451 }
452
453 =head2 api_get_policies
454
455 Gets a list of global policies.
456
457 =cut
458
459 sub api_get_policies {
460   my $self = shift;
461
462   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
463   return if $self->api_error;
464   $self->{'__saisei_error'} = "Did not receive any global policies"
465     unless $get_policies;
466
467   return $get_policies->{collection};
468 }
469
470 =head2 api_get_rateplan
471
472 Gets rateplan info for specific rateplan.
473
474 =cut
475
476 sub api_get_rateplan {
477   my $self = shift;
478   my $rateplan = shift;
479
480   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
481   return if $self->api_error;
482
483   return $get_rateplan;
484 }
485
486 =head2 api_get_user
487
488 Gets user info for specific user.
489
490 =cut
491
492 sub api_get_user {
493   my $self = shift;
494   my $user = shift;
495
496   my $get_user = $self->api_call("GET", "/users/$user");
497   return if $self->api_error;
498
499   return $get_user;
500 }
501
502 =head2 api_get_accesspoint
503
504 Gets user info for specific access point.
505
506 =cut
507
508 sub api_get_accesspoint {
509   my $self = shift;
510   my $accesspoint = shift;
511
512   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
513   return if $self->api_error;
514
515   return $get_accesspoint;
516 }
517
518 =head2 api_get_host
519
520 Gets user info for specific host.
521
522 =cut
523
524 sub api_get_host {
525   my $self = shift;
526   my $ip = shift;
527
528   my $get_host = $self->api_call("GET", "/hosts/$ip");
529
530   return { message => $self->api_error, } if $self->api_error;
531
532   return $get_host;
533 }
534
535 =head2 api_create_rateplan
536
537 Creates a rateplan.
538
539 =cut
540
541 sub api_create_rateplan {
542   my ($self, $svc, $rateplan) = @_;
543
544   $self->{'__saisei_error'} = "No downrate listed for service $rateplan" if !$svc->{Hash}->{speed_down};
545   $self->{'__saisei_error'} = "No uprate listed for service $rateplan" if !$svc->{Hash}->{speed_up};
546
547   my $new_rateplan = $self->api_call(
548       "PUT", 
549       "/rate_plans/$rateplan",
550       {
551         'downstream_rate' => $svc->{Hash}->{speed_down},
552         'upstream_rate' => $svc->{Hash}->{speed_up},
553       },
554   ) unless $self->{'__saisei_error'};
555
556   $self->{'__saisei_error'} = "Rate Plan not created"
557     unless ($new_rateplan || $self->{'__saisei_error'});
558
559   return $new_rateplan;
560
561 }
562
563 =head2 api_modify_rateplan
564
565 Modify a new rateplan.
566
567 =cut
568
569 sub api_modify_rateplan {
570   my ($self,$svc,$rateplan_name) = @_;
571
572   # get policy list
573   my $policies = $self->api_get_policies();
574
575   foreach my $policy (@$policies) {
576     my $policyname = $policy->{name};
577     my $rate_multiplier = '';
578     if ($policy->{background}) { $rate_multiplier = ".01"; }
579     my $modified_rateplan = $self->api_call(
580       "PUT", 
581       "/rate_plans/$rateplan_name/partitions/$policyname",
582       {
583         'restricted'      =>  $policy->{assured},         # policy_assured_flag
584         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
585         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
586       },
587     );
588
589     $self->{'__saisei_error'} = "Rate Plan not modified after create"
590       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
591     
592   }
593
594   return;
595  
596 }
597
598 =head2 api_modify_existing_rateplan
599
600 Modify a existing rateplan.
601
602 =cut
603
604 sub api_modify_existing_rateplan {
605   my ($self,$svc,$rateplan_name) = @_;
606
607   my $modified_rateplan = $self->api_call(
608     "PUT",
609     "/rate_plans/$rateplan_name",
610     {
611       'downstream_rate' => $svc->{Hash}->{speed_down},
612       'upstream_rate' => $svc->{Hash}->{speed_up},
613     },
614   );
615
616     $self->{'__saisei_error'} = "Rate Plan not modified"
617       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
618
619   return;
620
621 }
622
623 =head2 api_create_user
624
625 Creates a user.
626
627 =cut
628
629 sub api_create_user {
630   my ($self,$user, $description) = @_;
631
632   my $new_user = $self->api_call(
633       "PUT", 
634       "/users/$user",
635       {
636         'description' => $description,
637       },
638   );
639
640   $self->{'__saisei_error'} = "User not created"
641     unless ($new_user || $self->{'__saisei_error'}); # should never happen
642
643   return $new_user;
644
645 }
646
647 =head2 api_create_accesspoint
648
649 Creates a access point.
650
651 =cut
652
653 sub api_create_accesspoint {
654   my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
655
656   my $new_accesspoint = $self->api_call(
657       "PUT",
658       "/access_points/$accesspoint",
659       {
660          'downstream_rate_limit' => $downratelimit,
661          'upstream_rate_limit' => $upratelimit,
662       },
663   );
664
665   $self->{'__saisei_error'} = "Access point not created"
666     unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
667   return;
668
669 }
670
671 =head2 api_modify_accesspoint
672
673 Modify a new access point.
674
675 =cut
676
677 sub api_modify_accesspoint {
678   my ($self, $accesspoint, $uplink) = @_;
679
680   my $modified_accesspoint = $self->api_call(
681     "PUT",
682     "/access_points/$accesspoint",
683     {
684       'uplink' => $uplink, # name of attached access point
685     },
686   );
687
688   $self->{'__saisei_error'} = "Rate Plan not modified"
689     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
690
691   return;
692
693 }
694
695 =head2 api_modify_existing_accesspoint
696
697 Modify a existing accesspoint.
698
699 =cut
700
701 sub api_modify_existing_accesspoint {
702   my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
703
704   my $modified_accesspoint = $self->api_call(
705     "PUT",
706     "/access_points/$accesspoint",
707     {
708       'downstream_rate_limit' => $downratelimit,
709       'upstream_rate_limit' => $upratelimit,
710 #      'uplink' => $uplink, # name of attached access point
711     },
712   );
713
714     $self->{'__saisei_error'} = "Access point not modified"
715       unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
716
717   return;
718
719 }
720
721 =head2 api_add_host_to_user
722
723 ties host to user, rateplan and default access point.
724
725 =cut
726
727 sub api_add_host_to_user {
728   my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
729
730   my $new_host = $self->api_call(
731       "PUT", 
732       "/hosts/$ip",
733       {
734         'user'      => $user,
735         'rate_plan' => $rateplan,
736         'access_point' => $accesspoint,
737       },
738   );
739
740   $self->{'__saisei_error'} = "Host not created"
741     unless ($new_host || $self->{'__saisei_error'}); # should never happen
742
743   return $new_host;
744
745 }
746
747 =head2 api_delete_host_to_user
748
749 unties host from user and rateplan.
750 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
751
752 =cut
753
754 sub api_delete_host_to_user {
755   my ($self,$user, $rateplan, $ip) = @_;
756
757   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
758     return if $self->api_error;
759   $self->{'__saisei_error'} = "Did not receive a default rate plan"
760     unless $default_rate_plan;
761
762   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
763
764   my $delete_host = $self->api_call(
765       "PUT",
766       "/hosts/$ip",
767       {
768         'user'          => '<none>',
769         'access_point'  => '<none>',
770         'rate_plan'     => $default_rateplan_name,
771       },
772   );
773
774   $self->{'__saisei_error'} = "Host not created"
775     unless ($delete_host || $self->{'__saisei_error'}); # should never happen
776
777   return $delete_host;
778
779 }
780
781 sub process_tower {
782   my ($self, $opt) = @_;
783
784   if (!$opt->{tower_uprate_limit} || !$opt->{tower_downrate_limit}) {
785     $self->{'__saisei_error'} = "Can not export tower, no up or down rates attached to tower";
786     return { error => $self->api_error, };
787   }
788
789   my $existing_tower_ap;
790   my $tower_name = $opt->{tower_name};
791
792   #check if tower has been set up as an access point.
793   $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
794
795   # modify the existing accesspoint if changing tower .
796   $self->api_modify_existing_accesspoint (
797     $tower_name,
798     '', # tower does not have a uplink on sectors.
799     $opt->{tower_uprate_limit},
800     $opt->{tower_downrate_limit},
801   ) if $existing_tower_ap->{collection} && $opt->{modify_existing};
802
803   #if tower does not exist as an access point create it.
804   $self->api_create_accesspoint(
805       $tower_name,
806       $opt->{tower_uprate_limit},
807       $opt->{tower_downrate_limit},
808   ) unless $existing_tower_ap->{collection};
809
810   my $accesspoint = $self->api_get_accesspoint($tower_name);
811
812   return { error => $self->api_error, } if $self->api_error;
813   return $accesspoint;
814 }
815
816 sub process_sector {
817   my ($self, $opt) = @_;
818
819   if (!$opt->{sector_uprate_limit} || !$opt->{sector_downrate_limit}) {
820     $self->{'__saisei_error'} = "Can not export sector, no up or down rates attached to sector";
821     return { error => $self->api_error, };
822   }
823
824   my $existing_sector_ap;
825   my $sector_name = $opt->{sector_name};
826
827   #check if sector has been set up as an access point.
828   $existing_sector_ap = $self->api_get_accesspoint($sector_name);
829
830   # modify the existing accesspoint if changing sector .
831   $self->api_modify_existing_accesspoint (
832     $sector_name,
833     $opt->{tower_name},
834     $opt->{sector_uprate_limit},
835     $opt->{sector_downrate_limit},
836   ) if $existing_sector_ap && $opt->{modify_existing};
837
838   #if sector does not exist as an access point create it.
839   $self->api_create_accesspoint(
840     $sector_name,
841     $opt->{sector_uprate_limit},
842     $opt->{sector_downrate_limit},
843   ) unless $existing_sector_ap;
844
845   # Attach newly created sector to it's tower.
846   $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
847
848   # set access point to existing one or newly created one.
849   my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
850
851   return { error => $self->api_error, } if $self->api_error;
852   return $accesspoint;
853 }
854
855 sub process_virtual_ap {
856   my ($self, $opt) = @_;
857
858   my $existing_virtual_ap;
859   my $virtual_name = $opt->{virtual_name};
860
861   #check if virtual_ap has been set up as an access point.
862   $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
863
864   # modify the existing virtual accesspoint if changing it. this should never happen
865   $self->api_modify_existing_accesspoint (
866     $virtual_name,
867     $opt->{sector_name},
868     $opt->{virtual_uprate_limit},
869     $opt->{virtual_downrate_limit},
870   ) if $existing_virtual_ap && $opt->{modify_existing};
871
872   #if virtual ap does not exist as an access point create it.
873   $self->api_create_accesspoint(
874     $virtual_name,
875     $opt->{virtual_uprate_limit},
876     $opt->{virtual_downrate_limit},
877   ) unless $existing_virtual_ap;
878
879   my $update_sector;
880   if ($existing_virtual_ap && (ref $existing_virtual_ap->{collection}->[0]->{uplink} eq "HASH") && ($existing_virtual_ap->{collection}->[0]->{uplink}->{link}->{name} ne $opt->{sector_name})) {
881     $update_sector = 1;
882   }
883
884   # Attach newly created virtual ap to tower sector ap or if sector has changed.
885   $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
886
887   # set access point to existing one or newly created one.
888   my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
889
890   return $accesspoint;
891 }
892
893 sub export_provisioned_services {
894   my $job = shift;
895   my $param = thaw(decode_base64(shift));
896
897   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
898   or die "unknown exportnum $param->{export_provisioned_services_exportnum}";
899   bless $part_export;
900
901   my @svcparts = FS::Record::qsearch({
902     'table' => 'export_svc',
903     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
904     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
905   });
906   my $part_count = scalar @svcparts;
907
908   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
909
910   my @svcs = FS::Record::qsearch({
911     'table' => 'cust_svc',
912     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
913     'extra_sql' => " WHERE svcpart in ('".$parts."')",
914   }) unless !$parts;
915
916   my $svc_count = scalar @svcs;
917
918   my %status = {};
919   for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
920
921   my $process_count=0;
922   foreach my $svc (@svcs) {
923     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
924     ## check if service exists as host if not export it.
925     my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
926     die $host->{message} if $host->{message};
927     warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
928     my $export_error = _export_insert($part_export,$svc) unless $host->{collection};
929     die $export_error if $export_error;
930     $process_count++;
931   }
932
933   return;
934
935 }
936
937 =head1 SEE ALSO
938
939 L<FS::part_export>
940
941 =cut
942
943 1;