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