spacing, RT#83503
[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->{Hash}->{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->{Hash}->{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->{Hash}->{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->{Hash}->{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 =head2 tower_sector_required_fields
894
895 required fields needed for tower and sector export.
896
897 =cut
898
899 sub tower_sector_required_fields {
900   my $fields = {
901     'tower' => {
902       'up_rate_limit'   => '1',
903       'down_rate_limit' => '1',
904     },
905     'sector' => {
906       'up_rate_limit'   => '1',
907       'down_rate_limit' => '1',
908       'ip_addr'         => '1',
909     },
910   };
911   return $fields;
912 }
913
914 sub required_fields {
915   my @fields = ('svc_broadband__ip_addr_required', 'svc_broadband__speed_up_required', 'svc_broadband__speed_down_required', 'svc_broadband__sectornum_required');
916   return @fields;
917 }
918
919 sub process_virtual_ap {
920   my ($self, $opt) = @_;
921
922   my $existing_virtual_ap;
923   my $virtual_name = $opt->{virtual_name};
924
925   #check if virtual_ap has been set up as an access point.
926   $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
927
928   # modify the existing virtual accesspoint if changing it. this should never happen
929   $self->api_modify_existing_accesspoint (
930     $virtual_name,
931     $opt->{sector_name},
932     $opt->{virtual_uprate_limit},
933     $opt->{virtual_downrate_limit},
934   ) if $existing_virtual_ap && $opt->{modify_existing};
935
936   #if virtual ap does not exist as an access point create it.
937   $self->api_create_accesspoint(
938     $virtual_name,
939     $opt->{virtual_uprate_limit},
940     $opt->{virtual_downrate_limit},
941   ) unless $existing_virtual_ap;
942
943   my $update_sector;
944   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})) {
945     $update_sector = 1;
946   }
947
948   # Attach newly created virtual ap to tower sector ap or if sector has changed.
949   $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
950
951   # set access point to existing one or newly created one.
952   my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
953
954   return $accesspoint;
955 }
956
957 sub export_provisioned_services {
958   my $job = shift;
959   my $param = shift;
960
961   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
962   or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
963   bless $part_export;
964
965   my @svcparts = FS::Record::qsearch({
966     'table' => 'export_svc',
967     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
968     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
969   });
970   my $part_count = scalar @svcparts;
971
972   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
973
974   my @svcs = FS::Record::qsearch({
975     'table' => 'cust_svc',
976     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
977     'extra_sql' => " WHERE svcpart in ('".$parts."')",
978   }) unless !$parts;
979
980   my $svc_count = scalar @svcs;
981
982   my %status = {};
983   for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
984
985   my $process_count=0;
986   foreach my $svc (@svcs) {
987     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
988     ## check if service exists as host if not export it.
989     my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
990     die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message};
991     warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
992     my $export_error = _export_insert($part_export,$svc) unless $host->{collection};
993     if ($export_error) {
994       warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
995       die ("$export_error\n");
996     }
997     $process_count++;
998   }
999
1000   return;
1001
1002 }
1003
1004 sub test_export_report {
1005   my ($self, $opts) = @_;
1006   my @export_error;
1007
1008   ##  check all part services for export errors
1009   my @exports = FS::Record::qsearch('part_export', { 'exporttype' => "saisei", } );
1010   my $export_nums = join "', '", map { $_->{Hash}->{exportnum} } @exports;
1011
1012   my $svc_part_export_error;
1013   my @svcparts = FS::Record::qsearch({
1014     'table' => 'part_svc',
1015     'addl_from' => 'LEFT JOIN export_svc USING ( svcpart  ) ',
1016     'extra_sql' => " WHERE export_svc.exportnum in ('".$export_nums."')",
1017   });
1018   my $part_count = scalar @svcparts;
1019
1020   my $svc_part_error;
1021   foreach (@svcparts) {
1022     my $part_error->{'description'} = $_->svc;
1023     $part_error->{'link'} = $opts->{'fsurl'}."/edit/part_svc.cgi?".$_->svcpart;
1024
1025     foreach my $s ('speed_up', 'speed_down') {
1026       my $speed = $_->part_svc_column($s);
1027       if ($speed->columnflag eq "" || $speed->columnflag eq "D") {
1028         $part_error->{'errors'}->{$speed->columnname} = "Field ".$speed->columnname." is not set to be required and can be set while provisioning the service." unless $speed->required eq "Y";
1029       }
1030       elsif ($speed->columnflag eq "F" || $speed->columnflag eq "S") {
1031         $part_error->{'errors'}->{$speed->columnname} = "Field ".$speed->columnname." is set to auto fill while provisioning the service but there is no value set." unless $speed->columnvalue;
1032       }
1033       elsif ($speed->columnflag eq "P") {
1034         my $fcc_speed_name = "broadband_".$speed->columnvalue."stream";
1035         foreach my $part_pkg ( FS::Record::qsearchs({
1036                                  'table'   => 'part_pkg',
1037                                  'addl_from' => 'LEFT JOIN pkg_svc USING ( pkgpart  ) ',
1038                                  'extra_sql' => " WHERE pkg_svc.svcpart = ".$_->svcpart,
1039                               })) {
1040           my $pkglink = '<a href="'.$opts->{'fsurl'}.'/edit/part_pkg.cgi?'.$part_pkg->pkgpart.'"><FONT COLOR="red"><B>'.$part_pkg->pkg.'</B></FONT></a>';
1041           $part_error->{'errors'}->{$speed->columnname} = "Field ".$speed->columnname." is set to package FCC 477 information, but package ".$pkglink." does not have FCC ".$fcc_speed_name." set."
1042             unless $part_pkg->fcc_option($fcc_speed_name);
1043         }
1044       }
1045     }
1046     $part_error->{'errors'}->{'ip_addr'}    = "Field IP Address is not set to required" if $_->part_svc_column("ip_addr")->required ne "Y";
1047     $svc_part_error->{$_->svcpart} = $part_error if $part_error->{'errors'};
1048   }
1049
1050   $svc_part_export_error->{"services"}->{'description'} = "Service definitions";
1051   $svc_part_export_error->{"services"}->{'count'} = $part_count;
1052   $svc_part_export_error->{"services"}->{'errors'} = $svc_part_error if $svc_part_error;
1053
1054   push @export_error, $svc_part_export_error;
1055
1056   ##  check all provisioned cust services for export errors
1057   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
1058   my $cust_svc_export_error;
1059   my @svcs = FS::Record::qsearch({
1060     'table' => 'cust_svc',
1061     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
1062     'extra_sql' => " WHERE svcpart in ('".$parts."')",
1063   }) unless !$parts;
1064   my $svc_count = scalar @svcs;
1065
1066   my $cust_svc_error;
1067   foreach (@svcs) {
1068     my $svc_error->{'description'} = $_->description;
1069     $svc_error->{'link'} = $opts->{'fsurl'}."/edit/svc_broadband.cgi?".$_->svcnum;
1070
1071     foreach my $s ('speed_up', 'speed_down', 'ip_addr') {
1072         $svc_error->{'errors'}->{$s} = "Field ".$s." is not set and is required for this service to be exported to Saisei." unless $_->$s;
1073     }
1074
1075     my $sector = FS::Record::qsearchs({
1076         'table' => 'tower_sector',
1077         'extra_sql' => " WHERE sectornum = ".$_->sectornum." AND sectorname != '_default'",
1078     }) if $_->sectornum;
1079     if (!$sector) {
1080       $svc_error->{'errors'}->{'sectornum'} = "No tower sector is set for this service. There needs to be a tower and sector set to be exported to Saisei.";
1081     }
1082     else {
1083       foreach my $s ('up_rate_limit', 'down_rate_limit') {
1084         $svc_error->{'errors'}->{'sectornum'} = "The sector ".$sector->description." does not have a ".$s." set. The sector needs a ".$s." set to be exported to Saisei."
1085           unless $sector->$s;
1086       }
1087     }
1088     $cust_svc_error->{$_->svcnum} = $svc_error if $svc_error->{'errors'};
1089   }
1090
1091   $cust_svc_export_error->{"provisioned_services"}->{'description'} = "Provisioned services";
1092   $cust_svc_export_error->{"provisioned_services"}->{'count'} = $svc_count;
1093   $cust_svc_export_error->{"provisioned_services"}->{'errors'} = $cust_svc_error if $cust_svc_error;
1094
1095   push @export_error, $cust_svc_export_error;
1096
1097
1098   ##  check all towers and sectors for export errors
1099   my $tower_sector_export_error;
1100   my @towers = FS::Record::qsearch({
1101     'table' => 'tower',
1102   });
1103   my $tower_count = scalar @towers;
1104
1105   my $towers_error;
1106   foreach (@towers) {
1107     my $tower_error->{'description'} = $_->towername;
1108     $tower_error->{'link'} = $opts->{'fsurl'}."/edit/tower.html?".$_->towernum;
1109
1110     foreach my $s ('up_rate_limit', 'down_rate_limit') {
1111         $tower_error->{'errors'}->{$s} = "Field ".$s." is not set for the tower, this is required for this tower to be exported to Saisei." unless $_->$s;
1112     }
1113
1114     my @sectors = FS::Record::qsearch({
1115         'table' => 'tower_sector',
1116         'extra_sql' => " WHERE towernum = ".$_->towernum." AND sectorname != '_default' AND (up_rate_limit IS NULL OR down_rate_limit IS NULL)",
1117     }) if $_->towernum;
1118     foreach my $sector (@sectors) {
1119       foreach my $s ('up_rate_limit', 'down_rate_limit') {
1120         $tower_error->{'errors'}->{'sector_'.$s} = "The sector ".$sector->description." does not have a ".$s." set. The sector needs a ".$s." set to be exported to Saisei."
1121           if !$sector->$s;
1122       }
1123     }
1124     $towers_error->{$_->towernum} = $tower_error if $tower_error->{'errors'};
1125   }
1126
1127   $tower_sector_export_error->{"tower_sector"}->{'description'} = "Tower / Sector";
1128   $tower_sector_export_error->{"tower_sector"}->{'count'} = $tower_count;
1129   $tower_sector_export_error->{"tower_sector"}->{'errors'} = $towers_error if $towers_error;
1130
1131   push @export_error, $tower_sector_export_error;
1132
1133   return [@export_error];
1134
1135 }
1136
1137 =head1 SEE ALSO
1138
1139 L<FS::part_export>
1140
1141 =cut
1142
1143 1;