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