RT# 83203 - made Tower/Sector, speed_up, speed_down required
[freeside.git] / FS / FS / part_export / saisei.pm
1 package FS::part_export::saisei;
2
3 use strict;
4 use vars qw( @ISA %info );
5 use base qw( FS::part_export );
6 use Date::Format 'time2str';
7 use Cpanel::JSON::XS;
8 use Storable qw(thaw);
9 use MIME::Base64;
10 use REST::Client;
11 use Data::Dumper;
12 use FS::Conf;
13
14 =pod
15
16 =head1 NAME
17
18 FS::part_export::saisei
19
20 =head1 SYNOPSIS
21
22 Saisei integration for Freeside
23
24 =head1 DESCRIPTION
25
26 This export offers basic svc_broadband provisioning for Saisei.
27
28 This is a customer integration with Saisei.  This will set up a rate plan and tie
29 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
30 It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
31
32 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
33 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
34
35 To use this export, follow the below instructions:
36
37 Create a new service definition and set the table to svc_broadband.  The service name will become the Saisei rate plan name.
38 Set the upload and download speed for the service. This is required to be able to export the service to Saisei.
39 Attach this Saisei export to this service.
40
41 Create a tower and add a sector to that tower.  The sector name will be the name of the access point,
42 Make sure you have set the up and down rate limit for the tower and the sector.  This is required to be able to export the access point.
43 The tower and sector will be set up as access points at Saisei upon the creation of the tower or sector.  They will be modified at Saisei when modified in freeside.
44 Each sector will be attached to its tower access point using the Saisei uplink field.
45
46 Create a package for the above created service, and order this package for a customer.
47
48 Provision the service, making sure to enter the IP address associated with this service and select the tower and sector for it's access point.
49 This provisioned service will then be exported as a host to Saisei.
50
51 Unprovisioning this service will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
52
53 After this export is set up and attached to a service, you can export the already provisioned services by clicking the link Export provisioned services attached to this export.
54 Clicking on this link will export all services attached to this export not currently exported to Saisei.
55
56 This module also provides generic methods for working through the L</Saisei API>.
57
58 =cut
59
60 tie my %scripts, 'Tie::IxHash',
61   'export_provisioned_services'  => { component => '/elements/popup_link.html',
62                                       label     => 'Export provisioned services',
63                                       description => 'will export provisioned services of part service with Saisei export attached.',
64                                       html_label => '<b>Export provisioned services attached to this export.</b>',
65                                     },
66 ;
67
68 tie my %options, 'Tie::IxHash',
69   'port'             => { label => 'Port',
70                           default => 5000 },
71   'username'         => { label => 'Saisei API User Name',
72                           default => '' },
73   'password'         => { label => 'Saisei API Password',
74                           default => '' },
75   'debug'            => { type => 'checkbox',
76                           label => 'Enable debug warnings' },
77 ;
78
79 %info = (
80   'svc'             => 'svc_broadband',
81   'desc'            => 'Export broadband service/account to Saisei',
82   'options'         => \%options,
83   'scripts'         => \%scripts,
84   'notes'           => <<'END',
85 This is a customer integration with Saisei.  This will set up a rate plan and tie
86 the rate plan to a host and the access point via the Saisei API when the broadband service is provisioned.
87 It will also untie the host from the rate plan, setting it to the default rate plan via the API upon unprovisioning of the broadband service.
88 <P>
89 This will create and modify the rate plans at Saisei as soon as the broadband service attached to this export is created or modified.
90 This will also create and modify an access point at Saisei as soon as the tower is created or modified.
91 <P>
92 To use this export, follow the below instructions:
93 <P>
94 <OL>
95 <LI>
96 Create a new service definition and set the table to svc_broadband.  The service name will become the Saisei rate plan name.
97 Set the upload 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       'rateplan'              => $rateplan_name,
201     };
202     my $accesspoint = process_sector($self, $sector_opt);
203     return $self->api_error if $self->{'__saisei_error'};
204
205 ## get custnum and pkgpart from cust_pkg for virtual access point
206     my $cust_pkg = FS::Record::qsearchs({
207       'table'     => 'cust_pkg',
208       'hashref'   => { 'pkgnum' => $svc_broadband->{Hash}->{pkgnum}, },
209     });
210     my $virtual_ap_name = $cust_pkg->{Hash}->{custnum}.'_'.$cust_pkg->{Hash}->{pkgpart}.'_'.$svc_broadband->{Hash}->{speed_down}.'_'.$svc_broadband->{Hash}->{speed_up};
211
212     my $virtual_ap_opt = {
213       'virtual_name'           => $virtual_ap_name,
214       'sector_name'            => $sector_name,
215       'virtual_uprate_limit'   => $svc_broadband->{Hash}->{speed_up},
216       'virtual_downrate_limit' => $svc_broadband->{Hash}->{speed_down},
217     };
218     my $virtual_ap = process_virtual_ap($self, $virtual_ap_opt);
219     return $self->api_error if $self->{'__saisei_error'};
220
221     ## tie host to user add sector name as access point.
222     $self->api_add_host_to_user(
223       $user->{collection}->[0]->{name},
224       $rateplan->{collection}->[0]->{name},
225       $svc_broadband->{Hash}->{ip_addr},
226       $virtual_ap->{collection}->[0]->{name},
227     ) unless $self->{'__saisei_error'};
228   }
229
230   return $self->api_error;
231
232 }
233
234 sub _export_replace {
235   my ($self, $svc_broadband) = @_;
236   my $error = $self->_export_insert($svc_broadband);
237   return $error;
238 }
239
240 sub _export_delete {
241   my ($self, $svc_broadband) = @_;
242
243   my $rateplan_name = $self->get_rateplan_name($svc_broadband);
244
245   my $username = $svc_broadband->{Hash}->{svcnum};
246
247   ## untie host to user
248   $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
249
250   return '';
251 }
252
253 sub _export_suspend {
254   my ($self, $svc_broadband) = @_;
255   return '';
256 }
257
258 sub _export_unsuspend {
259   my ($self, $svc_broadband) = @_;
260   return '';
261 }
262
263 sub export_partsvc {
264   my ($self, $svc_part) = @_;
265
266   my $fcc_477_speeds;
267   if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
268     for my $type (qw( down up )) {
269       my $speed_type = "broadband_".$type."stream";
270       foreach my $pkg_svc (FS::Record::qsearch({
271         'table'     => 'pkg_svc',
272         'select'    => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
273         'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
274         'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
275       })) { $fcc_477_speeds->{
276         $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
277     }
278   }
279   else {
280     $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
281     $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
282   }
283
284   foreach my $key (keys %$fcc_477_speeds) {
285
286     $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
287     $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
288     $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
289     $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
290
291     my $temp_svc = $svc_part->{Hash};
292     my $svc_broadband = {};
293     map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; }  } keys %$temp_svc;
294
295     my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
296
297     # check for existing rate plan
298     my $existing_rateplan;
299     $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
300
301     # Modify the existing rate plan with new service data.
302     $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
303
304     # if no existing rate plan create one and modify it.
305     $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
306     $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
307
308   }
309
310   return $self->api_error;
311
312 }
313
314 sub export_tower_sector {
315   my ($self, $tower) = @_;
316
317   #modify tower or create it.
318   my $tower_name = $tower->{Hash}->{towername};
319   $tower_name =~ s/\s/_/g;
320   my $tower_opt = {
321     'tower_name'           => $tower_name,
322     'tower_uprate_limit'   => $tower->{Hash}->{up_rate_limit},
323     'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
324     'modify_existing'      => '1', # modify an existing access point with this info
325   };
326
327   my $tower_access_point = process_tower($self, $tower_opt);
328     return $tower_access_point if $tower_access_point->{error};
329
330   #get list of all access points
331   my $hash_opt = {
332       'table'     => 'tower_sector',
333       'select'    => '*',
334       'hashref'   => { 'towernum' => $tower->{Hash}->{towernum}, },
335   };
336
337   #for each one modify or create it.
338   foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
339     my $sector_name = $tower_sector->{Hash}->{sectorname};
340     $sector_name =~ s/\s/_/g;
341     my $sector_opt = {
342       'tower_name'            => $tower_name,
343       'sector_name'           => $sector_name,
344       'sector_uprate_limit'   => $tower_sector->{Hash}->{up_rate_limit},
345       'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
346       'modify_existing'       => '1', # modify an existing access point with this info
347     };
348     my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default");
349       return $sector_access_point if $sector_access_point->{error};
350   }
351
352   return { error => $self->api_error, };
353 }
354
355 ## creates the rateplan name
356 sub get_rateplan_name {
357   my ($self, $svc_broadband, $svc_name) = @_;
358
359   my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
360   my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
361
362   my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
363   $rateplan_name =~ s/\s/_/g; $rateplan_name =~ s/[^A-Za-z0-9\-_]//g;
364
365   return $rateplan_name;
366 }
367
368 =head1 Saisei API
369
370 These methods allow access to the Saisei API using the credentials
371 set in the export options.
372
373 =cut
374
375 =head2 api_call
376
377 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
378 Places an api call to the specified path and method with the specified params.
379 Returns the decoded json object returned by the api call.
380 Returns empty on failure;  retrieve error messages using L</api_error>.
381
382 =cut
383
384 sub api_call {
385   my ($self,$method,$path,$params) = @_;
386
387   $self->{'__saisei_error'} = '';
388   my $auth_info = $self->option('username') . ':' . $self->option('password');
389   $params ||= {};
390
391   warn "Calling $method on http://"
392     .$self->{Hash}->{machine}.':'.$self->option('port')
393     ."/rest/top/configurations/running/$path\n" if $self->option('debug');
394
395   my $data = encode_json($params) if keys %{ $params };
396
397   my $client = REST::Client->new();
398   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
399   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
400   $client->$method('/rest/top/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
401
402   warn "Saisei Response Code is ".$client->responseCode()."\n" if $self->option('debug');
403
404   my $result;
405
406   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
407     eval { $result = decode_json($client->responseContent()) };
408     unless ($result) {
409       $self->{'__saisei_error'} = "There was an error decoding the JSON data from Saisei.  Bad JSON data logged in error log if debug option was set.";
410       warn "Saisei RC 201 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
411       return;
412     }
413   }
414   elsif ($client->responseCode() eq '404') {
415     eval { $result = decode_json($client->responseContent()) };
416     unless ($result) {
417       $self->{'__saisei_error'} = "There was an error decoding the JSON data from Saisei.  Bad JSON data logged in error log if debug option was set.";
418       warn "Saisei RC 404 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
419       return;
420     }
421     ## check if message is for empty hash.
422     my($does_not_exist) = $result->{message} =~ /'(.*)' does not exist$/;
423     $self->{'__saisei_error'} = "Saisei Error: ".$result->{message} unless $does_not_exist;
424     warn "Saisei Response Content is\n".$client->responseContent."\n" if ($self->option('debug') && !$does_not_exist);
425     return;
426   }
427   elsif ($client->responseCode() eq '500') {
428     $self->{'__saisei_error'} = "Could not connect to the host (".$self->{Hash}->{machine}.':'.$self->option('port').") during $method , we received the responce code: " . $client->responseCode();
429     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
430     return;
431   }
432   else {
433     $self->{'__saisei_error'} = "Received Bad response from server during $method , we received responce code: " . $client->responseCode();
434     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
435     return; 
436   }
437
438   return $result;
439   
440 }
441
442 =head2 api_error
443
444 Returns the error string set by L</Saisei API> methods,
445 or a blank string if most recent call produced no errors.
446
447 =cut
448
449 sub api_error {
450   my $self = shift;
451   return $self->{'__saisei_error'} || '';
452 }
453
454 =head2 api_get_policies
455
456 Gets a list of global policies.
457
458 =cut
459
460 sub api_get_policies {
461   my $self = shift;
462
463   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
464   return if $self->api_error;
465   $self->{'__saisei_error'} = "Did not receive any global policies from Saisei."
466     unless $get_policies;
467
468   return $get_policies->{collection};
469 }
470
471 =head2 api_get_rateplan
472
473 Gets rateplan info for specific rateplan.
474
475 =cut
476
477 sub api_get_rateplan {
478   my $self = shift;
479   my $rateplan = shift;
480
481   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
482   return if $self->api_error;
483
484   return $get_rateplan;
485 }
486
487 =head2 api_get_user
488
489 Gets user info for specific user.
490
491 =cut
492
493 sub api_get_user {
494   my $self = shift;
495   my $user = shift;
496
497   my $get_user = $self->api_call("GET", "/users/$user");
498   return if $self->api_error;
499
500   return $get_user;
501 }
502
503 =head2 api_get_accesspoint
504
505 Gets user info for specific access point.
506
507 =cut
508
509 sub api_get_accesspoint {
510   my $self = shift;
511   my $accesspoint = shift;
512
513   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
514   return if $self->api_error;
515
516   return $get_accesspoint;
517 }
518
519 =head2 api_get_host
520
521 Gets user info for specific host.
522
523 =cut
524
525 sub api_get_host {
526   my $self = shift;
527   my $ip = shift;
528
529   my $get_host = $self->api_call("GET", "/hosts/$ip");
530
531   return { message => $self->api_error, } if $self->api_error;
532
533   return $get_host;
534 }
535
536 =head2 api_create_rateplan
537
538 Creates a rateplan.
539
540 =cut
541
542 sub api_create_rateplan {
543   my ($self, $svc, $rateplan) = @_;
544
545   $self->{'__saisei_error'} = "There is no download speed set for the service $rateplan with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a download speed set for them." if !$svc->{Hash}->{speed_down};
546   $self->{'__saisei_error'} = "There is no upload speed set for the service $rateplan with host (".$svc->{Hash}->{ip_addr}."). All services that are to be exported to Saisei need to have a upload speed set for them." if !$svc->{Hash}->{speed_up};
547
548   my $new_rateplan = $self->api_call(
549       "PUT", 
550       "/rate_plans/$rateplan",
551       {
552         'downstream_rate' => $svc->{Hash}->{speed_down},
553         'upstream_rate' => $svc->{Hash}->{speed_up},
554       },
555   ) unless $self->{'__saisei_error'};
556
557   $self->{'__saisei_error'} = "Saisei could not create the rate plan $rateplan."
558     unless ($new_rateplan || $self->{'__saisei_error'});
559
560   return $new_rateplan;
561
562 }
563
564 =head2 api_modify_rateplan
565
566 Modify a new rateplan.
567
568 =cut
569
570 sub api_modify_rateplan {
571   my ($self,$svc,$rateplan_name) = @_;
572
573   # get policy list
574   my $policies = $self->api_get_policies();
575
576   foreach my $policy (@$policies) {
577     my $policyname = $policy->{name};
578     my $rate_multiplier = '';
579     if ($policy->{background}) { $rate_multiplier = ".01"; }
580     my $modified_rateplan = $self->api_call(
581       "PUT", 
582       "/rate_plans/$rateplan_name/partitions/$policyname",
583       {
584         'restricted'      =>  $policy->{assured},         # policy_assured_flag
585         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
586         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
587       },
588     );
589
590     $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name after it was created."
591       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
592     
593   }
594
595   return;
596  
597 }
598
599 =head2 api_modify_existing_rateplan
600
601 Modify a existing rateplan.
602
603 =cut
604
605 sub api_modify_existing_rateplan {
606   my ($self,$svc,$rateplan_name) = @_;
607
608   my $modified_rateplan = $self->api_call(
609     "PUT",
610     "/rate_plans/$rateplan_name",
611     {
612       'downstream_rate' => $svc->{Hash}->{speed_down},
613       'upstream_rate' => $svc->{Hash}->{speed_up},
614     },
615   );
616
617     $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name."
618       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
619
620   return;
621
622 }
623
624 =head2 api_create_user
625
626 Creates a user.
627
628 =cut
629
630 sub api_create_user {
631   my ($self,$user, $description) = @_;
632
633   my $new_user = $self->api_call(
634       "PUT", 
635       "/users/$user",
636       {
637         'description' => $description,
638       },
639   );
640
641   $self->{'__saisei_error'} = "Saisei could not create the user $user"
642     unless ($new_user || $self->{'__saisei_error'}); # should never happen
643
644   return $new_user;
645
646 }
647
648 =head2 api_create_accesspoint
649
650 Creates a access point.
651
652 =cut
653
654 sub api_create_accesspoint {
655   my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
656
657   my $new_accesspoint = $self->api_call(
658       "PUT",
659       "/access_points/$accesspoint",
660       {
661          'downstream_rate_limit' => $downratelimit,
662          'upstream_rate_limit' => $upratelimit,
663       },
664   );
665
666   $self->{'__saisei_error'} = "Saisei could not create the access point $accesspoint"
667     unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
668   return;
669
670 }
671
672 =head2 api_modify_accesspoint
673
674 Modify a new access point.
675
676 =cut
677
678 sub api_modify_accesspoint {
679   my ($self, $accesspoint, $uplink) = @_;
680
681   my $modified_accesspoint = $self->api_call(
682     "PUT",
683     "/access_points/$accesspoint",
684     {
685       'uplink' => $uplink, # name of attached access point
686     },
687   );
688
689   $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint after it was created."
690     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
691
692   return;
693
694 }
695
696 =head2 api_modify_existing_accesspoint
697
698 Modify a existing accesspoint.
699
700 =cut
701
702 sub api_modify_existing_accesspoint {
703   my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
704
705   my $modified_accesspoint = $self->api_call(
706     "PUT",
707     "/access_points/$accesspoint",
708     {
709       'downstream_rate_limit' => $downratelimit,
710       'upstream_rate_limit' => $upratelimit,
711 #      'uplink' => $uplink, # name of attached access point
712     },
713   );
714
715     $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint."
716       unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
717
718   return;
719
720 }
721
722 =head2 api_add_host_to_user
723
724 ties host to user, rateplan and default access point.
725
726 =cut
727
728 sub api_add_host_to_user {
729   my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
730
731   my $new_host = $self->api_call(
732       "PUT", 
733       "/hosts/$ip",
734       {
735         'user'      => $user,
736         'rate_plan' => $rateplan,
737         'access_point' => $accesspoint,
738       },
739   );
740
741   $self->{'__saisei_error'} = "Saisei could not create the host $ip"
742     unless ($new_host || $self->{'__saisei_error'}); # should never happen
743
744   return $new_host;
745
746 }
747
748 =head2 api_delete_host_to_user
749
750 unties host from user and rateplan.
751 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
752
753 =cut
754
755 sub api_delete_host_to_user {
756   my ($self,$user, $rateplan, $ip) = @_;
757
758   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
759     return if $self->api_error;
760   $self->{'__saisei_error'} = "Can not delete the host as Saisei did not return a default rate plan. Please make sure Saisei has a default rateplan setup."
761     unless $default_rate_plan;
762
763   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
764
765   my $delete_host = $self->api_call(
766       "PUT",
767       "/hosts/$ip",
768       {
769         'user'          => '<none>',
770         'access_point'  => '<none>',
771         'rate_plan'     => $default_rateplan_name,
772       },
773   );
774
775   $self->{'__saisei_error'} = "Saisei could not delete the host $ip"
776     unless ($delete_host || $self->{'__saisei_error'}); # should never happen
777
778   return $delete_host;
779
780 }
781
782 sub process_tower {
783   my ($self, $opt) = @_;
784
785   if (!$opt->{tower_uprate_limit} || !$opt->{tower_downrate_limit}) {
786     $self->{'__saisei_error'} = "Could not export tower ".$opt->{tower_name}." because there was no up or down rates attached to the tower.  Saisei requires a up and down rate be attached to each tower.";
787     return { error => $self->api_error, };
788   }
789
790   my $existing_tower_ap;
791   my $tower_name = $opt->{tower_name};
792
793   #check if tower has been set up as an access point.
794   $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
795
796   # modify the existing accesspoint if changing tower .
797   $self->api_modify_existing_accesspoint (
798     $tower_name,
799     '', # tower does not have a uplink on sectors.
800     $opt->{tower_uprate_limit},
801     $opt->{tower_downrate_limit},
802   ) if $existing_tower_ap->{collection} && $opt->{modify_existing};
803
804   #if tower does not exist as an access point create it.
805   $self->api_create_accesspoint(
806       $tower_name,
807       $opt->{tower_uprate_limit},
808       $opt->{tower_downrate_limit},
809   ) unless $existing_tower_ap->{collection};
810
811   my $accesspoint = $self->api_get_accesspoint($tower_name);
812
813   return { error => $self->api_error, } if $self->api_error;
814   return $accesspoint;
815 }
816
817 sub process_sector {
818   my ($self, $opt) = @_;
819
820   if (!$opt->{sector_name} || $opt->{sector_name} eq '_default') {
821     $self->{'__saisei_error'} = "No sector attached to Tower (".$opt->{tower_name}.") for service ".$opt->{'rateplan'}.".  Saisei requires a tower sector to be attached to each service that is exported to Saisei.";
822     return { error => $self->api_error, };
823   }
824
825   if (!$opt->{sector_uprate_limit} || !$opt->{sector_downrate_limit}) {
826     $self->{'__saisei_error'} = "Could not export sector ".$opt->{tower_name}." because there was no up or down rates attached to the sector.  Saisei requires a up and down rate be attached to each sector.";
827     return { error => $self->api_error, };
828   }
829
830   my $existing_sector_ap;
831   my $sector_name = $opt->{sector_name};
832
833   #check if sector has been set up as an access point.
834   $existing_sector_ap = $self->api_get_accesspoint($sector_name);
835
836   # modify the existing accesspoint if changing sector .
837   $self->api_modify_existing_accesspoint (
838     $sector_name,
839     $opt->{tower_name},
840     $opt->{sector_uprate_limit},
841     $opt->{sector_downrate_limit},
842   ) if $existing_sector_ap && $opt->{modify_existing};
843
844   #if sector does not exist as an access point create it.
845   $self->api_create_accesspoint(
846     $sector_name,
847     $opt->{sector_uprate_limit},
848     $opt->{sector_downrate_limit},
849   ) unless $existing_sector_ap;
850
851   # Attach newly created sector to it's tower.
852   $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
853
854   # set access point to existing one or newly created one.
855   my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
856
857   return { error => $self->api_error, } if $self->api_error;
858   return $accesspoint;
859 }
860
861 =head2 require_tower_and_sector
862
863 sets whether the service export requires a sector with it's tower.
864
865 =cut
866
867 sub require_tower_and_sector {
868   1;
869 }
870
871 sub required_fields {
872   my @fields = ('svc_broadband__ip_addr_required', 'svc_broadband__speed_up_required', 'svc_broadband__speed_down_required', 'svc_broadband__sectornum_required');
873   return @fields;
874 }
875
876 sub process_virtual_ap {
877   my ($self, $opt) = @_;
878
879   my $existing_virtual_ap;
880   my $virtual_name = $opt->{virtual_name};
881
882   #check if virtual_ap has been set up as an access point.
883   $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
884
885   # modify the existing virtual accesspoint if changing it. this should never happen
886   $self->api_modify_existing_accesspoint (
887     $virtual_name,
888     $opt->{sector_name},
889     $opt->{virtual_uprate_limit},
890     $opt->{virtual_downrate_limit},
891   ) if $existing_virtual_ap && $opt->{modify_existing};
892
893   #if virtual ap does not exist as an access point create it.
894   $self->api_create_accesspoint(
895     $virtual_name,
896     $opt->{virtual_uprate_limit},
897     $opt->{virtual_downrate_limit},
898   ) unless $existing_virtual_ap;
899
900   my $update_sector;
901   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})) {
902     $update_sector = 1;
903   }
904
905   # Attach newly created virtual ap to tower sector ap or if sector has changed.
906   $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
907
908   # set access point to existing one or newly created one.
909   my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
910
911   return $accesspoint;
912 }
913
914 sub export_provisioned_services {
915   my $job = shift;
916   my $param = thaw(decode_base64(shift));
917
918   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
919   or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
920   bless $part_export;
921
922   my @svcparts = FS::Record::qsearch({
923     'table' => 'export_svc',
924     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
925     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
926   });
927   my $part_count = scalar @svcparts;
928
929   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
930
931   my @svcs = FS::Record::qsearch({
932     'table' => 'cust_svc',
933     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
934     'extra_sql' => " WHERE svcpart in ('".$parts."')",
935   }) unless !$parts;
936
937   my $svc_count = scalar @svcs;
938
939   my %status = {};
940   for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
941
942   my $process_count=0;
943   foreach my $svc (@svcs) {
944     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
945     ## check if service exists as host if not export it.
946     my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
947     die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message};
948     warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
949     my $export_error = _export_insert($part_export,$svc) unless $host->{collection};
950     if ($export_error) {
951       warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
952       die ("$export_error\n");
953     }
954     $process_count++;
955   }
956
957   return;
958
959 }
960
961 =head1 SEE ALSO
962
963 L<FS::part_export>
964
965 =cut
966
967 1;