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