RT# 83204 - added link to service or tower causing error.
[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                                       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   my $fcc_477_speeds;
272   if ($svc_part->{Hash}->{svc_broadband__speed_down} eq "down" || $svc_part->{Hash}->{svc_broadband__speed_up} eq "up") {
273     for my $type (qw( down up )) {
274       my $speed_type = "broadband_".$type."stream";
275       foreach my $pkg_svc (FS::Record::qsearch({
276         'table'     => 'pkg_svc',
277         'select'    => 'pkg_svc.*, part_pkg_fcc_option.fccoptionname, part_pkg_fcc_option.optionvalue',
278         'addl_from' => ' LEFT JOIN part_pkg_fcc_option USING (pkgpart) ',
279         'extra_sql' => " WHERE pkg_svc.svcpart = ".$svc_part->{Hash}->{svcpart}." AND pkg_svc.quantity > 0 AND part_pkg_fcc_option.fccoptionname = '".$speed_type."'",
280       })) { $fcc_477_speeds->{
281         $pkg_svc->{Hash}->{pkgpart}}->{$speed_type} = $pkg_svc->{Hash}->{optionvalue} * 1000 unless !$pkg_svc->{Hash}->{optionvalue}; }
282     }
283   }
284   else {
285     $fcc_477_speeds->{1}->{broadband_downstream} = $svc_part->{Hash}->{"svc_broadband__speed_down"};
286     $fcc_477_speeds->{1}->{broadband_upstream} = $svc_part->{Hash}->{"svc_broadband__speed_up"};
287   }
288
289   foreach my $key (keys %$fcc_477_speeds) {
290
291     $svc_part->{Hash}->{speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
292     $svc_part->{Hash}->{speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
293     $svc_part->{Hash}->{svc_broadband__speed_down} = $fcc_477_speeds->{$key}->{broadband_downstream};
294     $svc_part->{Hash}->{svc_broadband__speed_up} = $fcc_477_speeds->{$key}->{broadband_upstream};
295
296     my $temp_svc = $svc_part->{Hash};
297     my $svc_broadband = {};
298     map { if ($_ =~ /^svc_broadband__(.*)$/) { $svc_broadband->{Hash}->{$1} = $temp_svc->{$_}; }  } keys %$temp_svc;
299
300     my $rateplan_name = $self->get_rateplan_name($svc_broadband, $svc_part->{Hash}->{svc});
301
302     # check for existing rate plan
303     my $existing_rateplan;
304     $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
305
306     # Modify the existing rate plan with new service data.
307     $self->api_modify_existing_rateplan($svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || !$existing_rateplan);
308
309     # if no existing rate plan create one and modify it.
310     $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
311     $self->api_modify_rateplan($svc_part, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
312
313   }
314
315   return $self->api_error;
316
317 }
318
319 sub export_tower_sector {
320   my ($self, $tower) = @_;
321
322   #modify tower or create it.
323   my $tower_name = $tower->{Hash}->{towername};
324   $tower_name =~ s/\s/_/g;
325   my $tower_opt = {
326     'tower_name'           => $tower_name,
327     'tower_num'            => $tower->{Hash}->{towernum},
328     'tower_uprate_limit'   => $tower->{Hash}->{up_rate_limit},
329     'tower_downrate_limit' => $tower->{Hash}->{down_rate_limit},
330     'modify_existing'      => '1', # modify an existing access point with this info
331   };
332
333   my $tower_access_point = process_tower($self, $tower_opt);
334     return $tower_access_point if $tower_access_point->{error};
335
336   #get list of all access points
337   my $hash_opt = {
338       'table'     => 'tower_sector',
339       'select'    => '*',
340       'hashref'   => { 'towernum' => $tower->{Hash}->{towernum}, },
341   };
342
343   #for each one modify or create it.
344   foreach my $tower_sector ( FS::Record::qsearch($hash_opt) ) {
345     my $sector_name = $tower_sector->{Hash}->{sectorname};
346     $sector_name =~ s/\s/_/g;
347     my $sector_opt = {
348       'tower_name'            => $tower_name,
349       'tower_num'             => $tower_sector->{Hash}->{towernum},
350       'sector_name'           => $sector_name,
351       'sector_uprate_limit'   => $tower_sector->{Hash}->{up_rate_limit},
352       'sector_downrate_limit' => $tower_sector->{Hash}->{down_rate_limit},
353       'modify_existing'       => '1', # modify an existing access point with this info
354     };
355     my $sector_access_point = process_sector($self, $sector_opt) unless ($sector_name eq "_default");
356       return $sector_access_point if $sector_access_point->{error};
357   }
358
359   return { error => $self->api_error, };
360 }
361
362 ## creates the rateplan name
363 sub get_rateplan_name {
364   my ($self, $svc_broadband, $svc_name) = @_;
365
366   my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } ) unless $svc_name;
367   my $service_name = $svc_name ? $svc_name : $service_part->{Hash}->{svc};
368
369   my $rateplan_name = $service_name . " " . $svc_broadband->{Hash}->{speed_down} . "-" . $svc_broadband->{Hash}->{speed_up};
370   $rateplan_name =~ s/\s/_/g; $rateplan_name =~ s/[^A-Za-z0-9\-_]//g;
371
372   return $rateplan_name;
373 }
374
375 =head1 Saisei API
376
377 These methods allow access to the Saisei API using the credentials
378 set in the export options.
379
380 =cut
381
382 =head2 api_call
383
384 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
385 Places an api call to the specified path and method with the specified params.
386 Returns the decoded json object returned by the api call.
387 Returns empty on failure;  retrieve error messages using L</api_error>.
388
389 =cut
390
391 sub api_call {
392   my ($self,$method,$path,$params) = @_;
393
394   $self->{'__saisei_error'} = '';
395   my $auth_info = $self->option('username') . ':' . $self->option('password');
396   $params ||= {};
397
398   warn "Calling $method on http://"
399     .$self->{Hash}->{machine}.':'.$self->option('port')
400     ."/rest/top/configurations/running/$path\n" if $self->option('debug');
401
402   my $data = encode_json($params) if keys %{ $params };
403
404   my $client = REST::Client->new();
405   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
406   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
407   $client->$method('/rest/top/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
408
409   warn "Saisei Response Code is ".$client->responseCode()."\n" if $self->option('debug');
410
411   my $result;
412
413   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
414     eval { $result = decode_json($client->responseContent()) };
415     unless ($result) {
416       $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.";
417       warn "Saisei RC 201 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
418       return;
419     }
420   }
421   elsif ($client->responseCode() eq '404') {
422     eval { $result = decode_json($client->responseContent()) };
423     unless ($result) {
424       $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.";
425       warn "Saisei RC 404 Response Content is not json\n".$client->responseContent()."\n" if $self->option('debug');
426       return;
427     }
428     ## check if message is for empty hash.
429     my($does_not_exist) = $result->{message} =~ /'(.*)' does not exist$/;
430     $self->{'__saisei_error'} = "Saisei Error: ".$result->{message} unless $does_not_exist;
431     warn "Saisei Response Content is\n".$client->responseContent."\n" if ($self->option('debug') && !$does_not_exist);
432     return;
433   }
434   elsif ($client->responseCode() eq '500') {
435     $self->{'__saisei_error'} = "Could not connect to the Saisei export host machine (".$self->{Hash}->{machine}.':'.$self->option('port').") during $method , we received the responce code: " . $client->responseCode();
436     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
437     return;
438   }
439   else {
440     $self->{'__saisei_error'} = "Received Bad response from server during $method , we received responce code: " . $client->responseCode();
441     warn "Saisei Response Content is\n".$client->responseContent."\n" if $self->option('debug');
442     return; 
443   }
444
445   return $result;
446   
447 }
448
449 =head2 api_error
450
451 Returns the error string set by L</Saisei API> methods,
452 or a blank string if most recent call produced no errors.
453
454 =cut
455
456 sub api_error {
457   my $self = shift;
458   return $self->{'__saisei_error'} || '';
459 }
460
461 =head2 api_get_policies
462
463 Gets a list of global policies.
464
465 =cut
466
467 sub api_get_policies {
468   my $self = shift;
469
470   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
471   return if $self->api_error;
472   $self->{'__saisei_error'} = "Did not receive any global policies from Saisei."
473     unless $get_policies;
474
475   return $get_policies->{collection};
476 }
477
478 =head2 api_get_rateplan
479
480 Gets rateplan info for specific rateplan.
481
482 =cut
483
484 sub api_get_rateplan {
485   my $self = shift;
486   my $rateplan = shift;
487
488   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
489   return if $self->api_error;
490
491   return $get_rateplan;
492 }
493
494 =head2 api_get_user
495
496 Gets user info for specific user.
497
498 =cut
499
500 sub api_get_user {
501   my $self = shift;
502   my $user = shift;
503
504   my $get_user = $self->api_call("GET", "/users/$user");
505   return if $self->api_error;
506
507   return $get_user;
508 }
509
510 =head2 api_get_accesspoint
511
512 Gets user info for specific access point.
513
514 =cut
515
516 sub api_get_accesspoint {
517   my $self = shift;
518   my $accesspoint = shift;
519
520   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
521   return if $self->api_error;
522
523   return $get_accesspoint;
524 }
525
526 =head2 api_get_host
527
528 Gets user info for specific host.
529
530 =cut
531
532 sub api_get_host {
533   my $self = shift;
534   my $ip = shift;
535
536   my $get_host = $self->api_call("GET", "/hosts/$ip");
537
538   return { message => $self->api_error, } if $self->api_error;
539
540   return $get_host;
541 }
542
543 =head2 api_create_rateplan
544
545 Creates a rateplan.
546
547 =cut
548
549 sub api_create_rateplan {
550   my ($self, $svc, $rateplan) = @_;
551
552   $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};
553   $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};
554
555   my $new_rateplan = $self->api_call(
556       "PUT", 
557       "/rate_plans/$rateplan",
558       {
559         'downstream_rate' => $svc->{Hash}->{speed_down},
560         'upstream_rate' => $svc->{Hash}->{speed_up},
561       },
562   ) unless $self->{'__saisei_error'};
563
564   $self->{'__saisei_error'} = "Saisei could not create the rate plan $rateplan."
565     unless ($new_rateplan || $self->{'__saisei_error'});
566
567   return $new_rateplan;
568
569 }
570
571 =head2 api_modify_rateplan
572
573 Modify a new rateplan.
574
575 =cut
576
577 sub api_modify_rateplan {
578   my ($self,$svc,$rateplan_name) = @_;
579
580   # get policy list
581   my $policies = $self->api_get_policies();
582
583   foreach my $policy (@$policies) {
584     my $policyname = $policy->{name};
585     my $rate_multiplier = '';
586     if ($policy->{background}) { $rate_multiplier = ".01"; }
587     my $modified_rateplan = $self->api_call(
588       "PUT", 
589       "/rate_plans/$rateplan_name/partitions/$policyname",
590       {
591         'restricted'      =>  $policy->{assured},         # policy_assured_flag
592         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
593         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
594       },
595     );
596
597     $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name after it was created."
598       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
599     
600   }
601
602   return;
603  
604 }
605
606 =head2 api_modify_existing_rateplan
607
608 Modify a existing rateplan.
609
610 =cut
611
612 sub api_modify_existing_rateplan {
613   my ($self,$svc,$rateplan_name) = @_;
614
615   $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};
616   $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};
617
618   my $modified_rateplan = $self->api_call(
619     "PUT",
620     "/rate_plans/$rateplan_name",
621     {
622       'downstream_rate' => $svc->{Hash}->{speed_down},
623       'upstream_rate' => $svc->{Hash}->{speed_up},
624     },
625   );
626
627     $self->{'__saisei_error'} = "Saisei could not modify the rate plan $rateplan_name."
628       unless ($modified_rateplan || $self->{'__saisei_error'}); # should never happen
629
630   return;
631
632 }
633
634 =head2 api_create_user
635
636 Creates a user.
637
638 =cut
639
640 sub api_create_user {
641   my ($self,$user, $description) = @_;
642
643   my $new_user = $self->api_call(
644       "PUT", 
645       "/users/$user",
646       {
647         'description' => $description,
648       },
649   );
650
651   $self->{'__saisei_error'} = "Saisei could not create the user $user"
652     unless ($new_user || $self->{'__saisei_error'}); # should never happen
653
654   return $new_user;
655
656 }
657
658 =head2 api_create_accesspoint
659
660 Creates a access point.
661
662 =cut
663
664 sub api_create_accesspoint {
665   my ($self,$accesspoint, $upratelimit, $downratelimit) = @_;
666
667   my $new_accesspoint = $self->api_call(
668       "PUT",
669       "/access_points/$accesspoint",
670       {
671          'downstream_rate_limit' => $downratelimit,
672          'upstream_rate_limit' => $upratelimit,
673       },
674   );
675
676   $self->{'__saisei_error'} = "Saisei could not create the access point $accesspoint"
677     unless ($new_accesspoint || $self->{'__saisei_error'}); # should never happen
678   return;
679
680 }
681
682 =head2 api_modify_accesspoint
683
684 Modify a new access point.
685
686 =cut
687
688 sub api_modify_accesspoint {
689   my ($self, $accesspoint, $uplink) = @_;
690
691   my $modified_accesspoint = $self->api_call(
692     "PUT",
693     "/access_points/$accesspoint",
694     {
695       'uplink' => $uplink, # name of attached access point
696     },
697   );
698
699   $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint after it was created."
700     unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
701
702   return;
703
704 }
705
706 =head2 api_modify_existing_accesspoint
707
708 Modify a existing accesspoint.
709
710 =cut
711
712 sub api_modify_existing_accesspoint {
713   my ($self, $accesspoint, $uplink, $upratelimit, $downratelimit) = @_;
714
715   my $modified_accesspoint = $self->api_call(
716     "PUT",
717     "/access_points/$accesspoint",
718     {
719       'downstream_rate_limit' => $downratelimit,
720       'upstream_rate_limit' => $upratelimit,
721 #      'uplink' => $uplink, # name of attached access point
722     },
723   );
724
725     $self->{'__saisei_error'} = "Saisei could not modify the access point $accesspoint."
726       unless ($modified_accesspoint || $self->{'__saisei_error'}); # should never happen
727
728   return;
729
730 }
731
732 =head2 api_add_host_to_user
733
734 ties host to user, rateplan and default access point.
735
736 =cut
737
738 sub api_add_host_to_user {
739   my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
740
741   my $new_host = $self->api_call(
742       "PUT", 
743       "/hosts/$ip",
744       {
745         'user'      => $user,
746         'rate_plan' => $rateplan,
747         'access_point' => $accesspoint,
748       },
749   );
750
751   $self->{'__saisei_error'} = "Saisei could not create the host $ip"
752     unless ($new_host || $self->{'__saisei_error'}); # should never happen
753
754   return $new_host;
755
756 }
757
758 =head2 api_delete_host_to_user
759
760 unties host from user and rateplan.
761 this will set the host entry at Saisei to the default rate plan with the user and access point set to <none>.
762
763 =cut
764
765 sub api_delete_host_to_user {
766   my ($self,$user, $rateplan, $ip) = @_;
767
768   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
769     return if $self->api_error;
770   $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."
771     unless $default_rate_plan;
772
773   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
774
775   my $delete_host = $self->api_call(
776       "PUT",
777       "/hosts/$ip",
778       {
779         'user'          => '<none>',
780         'access_point'  => '<none>',
781         'rate_plan'     => $default_rateplan_name,
782       },
783   );
784
785   $self->{'__saisei_error'} = "Saisei could not delete the host $ip"
786     unless ($delete_host || $self->{'__saisei_error'}); # should never happen
787
788   return $delete_host;
789
790 }
791
792 sub process_tower {
793   my ($self, $opt) = @_;
794
795   if (!$opt->{tower_uprate_limit} || !$opt->{tower_downrate_limit}) {
796     $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.";
797     return { error => $self->api_error, };
798   }
799
800   my $existing_tower_ap;
801   my $tower_name = $opt->{tower_name};
802
803   #check if tower has been set up as an access point.
804   $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};
805
806   # modify the existing accesspoint if changing tower .
807   $self->api_modify_existing_accesspoint (
808     $tower_name,
809     '', # tower does not have a uplink on sectors.
810     $opt->{tower_uprate_limit},
811     $opt->{tower_downrate_limit},
812   ) if $existing_tower_ap->{collection} && $opt->{modify_existing};
813
814   #if tower does not exist as an access point create it.
815   $self->api_create_accesspoint(
816       $tower_name,
817       $opt->{tower_uprate_limit},
818       $opt->{tower_downrate_limit},
819   ) unless $existing_tower_ap->{collection};
820
821   my $accesspoint = $self->api_get_accesspoint($tower_name);
822
823   return { error => $self->api_error, } if $self->api_error;
824   return $accesspoint;
825 }
826
827 sub process_sector {
828   my ($self, $opt) = @_;
829
830   if (!$opt->{sector_name} || $opt->{sector_name} eq '_default') {
831     $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.";
832     return { error => $self->api_error, };
833   }
834
835   if (!$opt->{sector_uprate_limit} || !$opt->{sector_downrate_limit}) {
836     $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.";
837     return { error => $self->api_error, };
838   }
839
840   my $existing_sector_ap;
841   my $sector_name = $opt->{sector_name};
842
843   #check if sector has been set up as an access point.
844   $existing_sector_ap = $self->api_get_accesspoint($sector_name);
845
846   # modify the existing accesspoint if changing sector .
847   $self->api_modify_existing_accesspoint (
848     $sector_name,
849     $opt->{tower_name},
850     $opt->{sector_uprate_limit},
851     $opt->{sector_downrate_limit},
852   ) if $existing_sector_ap && $opt->{modify_existing};
853
854   #if sector does not exist as an access point create it.
855   $self->api_create_accesspoint(
856     $sector_name,
857     $opt->{sector_uprate_limit},
858     $opt->{sector_downrate_limit},
859   ) unless $existing_sector_ap;
860
861   # Attach newly created sector to it's tower.
862   $self->api_modify_accesspoint($sector_name, $opt->{tower_name}) unless ($self->{'__saisei_error'} || $existing_sector_ap);
863
864   # set access point to existing one or newly created one.
865   my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
866
867   return { error => $self->api_error, } if $self->api_error;
868   return $accesspoint;
869 }
870
871 =head2 require_tower_and_sector
872
873 sets whether the service export requires a sector with it's tower.
874
875 =cut
876
877 sub require_tower_and_sector {
878   1;
879 }
880
881 sub required_fields {
882   my @fields = ('svc_broadband__ip_addr_required', 'svc_broadband__speed_up_required', 'svc_broadband__speed_down_required', 'svc_broadband__sectornum_required');
883   return @fields;
884 }
885
886 sub process_virtual_ap {
887   my ($self, $opt) = @_;
888
889   my $existing_virtual_ap;
890   my $virtual_name = $opt->{virtual_name};
891
892   #check if virtual_ap has been set up as an access point.
893   $existing_virtual_ap = $self->api_get_accesspoint($virtual_name);
894
895   # modify the existing virtual accesspoint if changing it. this should never happen
896   $self->api_modify_existing_accesspoint (
897     $virtual_name,
898     $opt->{sector_name},
899     $opt->{virtual_uprate_limit},
900     $opt->{virtual_downrate_limit},
901   ) if $existing_virtual_ap && $opt->{modify_existing};
902
903   #if virtual ap does not exist as an access point create it.
904   $self->api_create_accesspoint(
905     $virtual_name,
906     $opt->{virtual_uprate_limit},
907     $opt->{virtual_downrate_limit},
908   ) unless $existing_virtual_ap;
909
910   my $update_sector;
911   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})) {
912     $update_sector = 1;
913   }
914
915   # Attach newly created virtual ap to tower sector ap or if sector has changed.
916   $self->api_modify_accesspoint($virtual_name, $opt->{sector_name}) unless ($self->{'__saisei_error'} || ($existing_virtual_ap && !$update_sector));
917
918   # set access point to existing one or newly created one.
919   my $accesspoint = $existing_virtual_ap ? $existing_virtual_ap : $self->api_get_accesspoint($virtual_name);
920
921   return $accesspoint;
922 }
923
924 sub export_provisioned_services {
925   my $job = shift;
926   my $param = thaw(decode_base64(shift));
927
928   my $part_export = FS::Record::qsearchs('part_export', { 'exportnum' => $param->{export_provisioned_services_exportnum}, } )
929   or die "You are trying to use an unknown exportnum $param->{export_provisioned_services_exportnum}.  This export does not exist.\n";
930   bless $part_export;
931
932   my @svcparts = FS::Record::qsearch({
933     'table' => 'export_svc',
934     'addl_from' => 'LEFT JOIN part_svc USING ( svcpart  ) ',
935     'hashref'   => { 'exportnum' => $param->{export_provisioned_services_exportnum}, },
936   });
937   my $part_count = scalar @svcparts;
938
939   my $parts = join "', '", map { $_->{Hash}->{svcpart} } @svcparts;
940
941   my @svcs = FS::Record::qsearch({
942     'table' => 'cust_svc',
943     'addl_from' => 'LEFT JOIN svc_broadband USING ( svcnum  ) ',
944     'extra_sql' => " WHERE svcpart in ('".$parts."')",
945   }) unless !$parts;
946
947   my $svc_count = scalar @svcs;
948
949   my %status = {};
950   for (my $c=1; $c <=100; $c=$c+1) { $status{int($svc_count * ($c/100))} = $c; }
951
952   my $process_count=0;
953   foreach my $svc (@svcs) {
954     if ($status{$process_count}) { my $s = $status{$process_count}; $job->update_statustext($s); }
955     ## check if service exists as host if not export it.
956     my $host = api_get_host($part_export, $svc->{Hash}->{ip_addr});
957     die ("Please double check your credentials as ".$host->{message}."\n") if $host->{message};
958     warn "Exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
959     my $export_error = _export_insert($part_export,$svc) unless $host->{collection};
960     if ($export_error) {
961       warn "Error exporting service ".$svc->{Hash}->{ip_addr}."\n" if ($part_export->option('debug'));
962       die ("$export_error\n");
963     }
964     $process_count++;
965   }
966
967   return;
968
969 }
970
971 =head1 SEE ALSO
972
973 L<FS::part_export>
974
975 =cut
976
977 1;