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