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