RT# 78356 - updated documentation and added ability to create access points as Saisei...
[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
13 =pod
14
15 =head1 NAME
16
17 FS::part_export::saisei
18
19 =head1 SYNOPSIS
20
21 Saisei integration for Freeside
22
23 =head1 DESCRIPTION
24
25 This export offers basic svc_broadband provisioning for Saisei.
26
27 This is a customer integration with Saisei.  This will setup a rate plan and tie 
28 the rate plan to a host and access point via the Saisei API when the broadband service is provisioned.  
29 It will also untie the rate plan via the API upon unprovisioning of the broadband service.
30
31 Add a new export and fill out required fields:
32 <UL>
33 <LI>Hostname or IP - <I>Host name to Saisei API</I></LI>
34 <LI>Port - <I>Port number to Saisei API</I></LI>
35 <LI>User Name -  <I>Saisei API user name</I></LI>
36 <LI>Password - <I>Saisei API password</I></LI>
37 </UL>
38 Create a broadband service.  The broadband service name will become the Saisei rate plan name.
39 Set the upload and download speed, and set the modifier to fixed.
40 Set IP Address to required.
41 Attach Saisei export to service
42
43 Create a tower and add a sector to that tower.  The sector name will be the name of the access point,
44 Make sure you have set an up and down rate for the Tower and Sector.
45
46 When you provision the service, enter the ip address associated to this service.
47 Select the Tower and Sector for it's access point.
48
49 When the service is provisioned it will auto setup the rate plan.
50
51 This module also provides generic methods for working through the L</Saisei API>.
52
53 =cut
54
55 tie my %options, 'Tie::IxHash',
56   'port'             => { label => 'Port',
57                           default => 5000 },
58   'username'         => { label => 'User Name',
59                           default => '' },
60   'password'         => { label => 'Password',
61                           default => '' },
62   'debug'            => { type => 'checkbox',
63                           label => 'Enable debug warnings' },
64 ;
65
66 %info = (
67   'svc'             => 'svc_broadband',
68   'desc'            => 'Export broadband service/account to Saisei',
69   'options'         => \%options,
70   'notes'           => <<'END',
71 This is a customer integration with Saisei.  This will setup a rate plan and tie 
72 the rate plan to a host and access point via the Saisei API when the broadband service is provisioned.  
73 It will also untie the rate plan via the API upon unprovisioning of the broadband service.
74 <P>
75 Add a new export and fill out required fields:
76 <UL>
77 <LI>Hostname or IP - <I>Host name to Saisei API</I></LI>
78 <LI>Port - <I>Port number to Saisei API</I></LI>
79 <LI>User Name -  <I>Saisei API user name</I></LI>
80 <LI>Password - <I>Saisei API password</I></LI>
81 </UL>
82 Create a broadband service.  The broadband service name will become the Saisei rate plan name.
83 Set the upload and download speed, and set the modifier to fixed.
84 Set IP Address to required.
85 Attach Saisei export to service
86 <P>
87 Create a tower and add a sector to that tower.  The sector name will be the name of the access point,
88 Make sure you have set an up and down rate for the Tower and Sector.
89 <P>
90 When you provision the service, enter the ip address associated to this service.
91 Select the Tower and Sector for it's access point.
92 <P>
93 When the service is provisioned it will auto setup the rate plan.
94 END
95 );
96
97 sub _export_insert {
98   my ($self, $svc_broadband) = @_;
99
100   my $service_part = FS::Record::qsearchs( 'part_svc', { 'svcpart' => $svc_broadband->{Hash}->{svcpart} } );
101   my $rateplan_name = $service_part->{Hash}->{svc};
102   $rateplan_name =~ s/\s/_/g;
103
104   # load needed info from our end
105   my $cust_main = $svc_broadband->cust_main;
106   return "Could not load service customer" unless $cust_main;
107   my $conf = new FS::Conf;
108
109   # get policy list
110   my $policies = $self->api_get_policies();
111
112   # check for existing rate plan
113   my $existing_rateplan;
114   $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
115
116   # if no existing rate plan create one and modify it.
117   $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
118   $self->api_modify_rateplan($policies->{collection}, $svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
119
120   # set rateplan to existing one or newly created one.
121   my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
122
123   my $username = $svc_broadband->{Hash}->{svcnum};
124   my $description = $svc_broadband->{Hash}->{description};
125
126   if (!$username) {
127     $self->{'__saisei_error'} = 'no username - can not export';
128     warn "No user $username\n" if $self->option('debug');
129     return $self->api_error;
130   }
131   else {
132     # check for existing user.
133     my $existing_user;
134     $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
135  
136     # if no existing user create one.
137     $self->api_create_user($username, $description) unless $existing_user;
138
139     # set user to existing one or newly created one.
140     my $user = $existing_user ? $existing_user : $self->api_get_user($username);
141
142     ## add access point ?
143     my $tower_sector = FS::Record::qsearchs({
144       'table'     => 'tower_sector',
145       'select'    => 'tower.towername,
146                       tower.up_rate as toweruprate,
147                       tower.down_rate as towerdownrate,
148                       tower_sector.sectorname,
149                       tower_sector.up_rate as sectoruprate,
150                       tower_sector.down_rate as sectordownrate ',
151       'addl_from' => 'LEFT JOIN tower USING ( towernum )',
152       'hashref'   => {
153                         'sectornum' => $svc_broadband->{Hash}->{sectornum},
154                      },
155     });
156
157     my $existing_tower_ap;
158     my $tower_name = $tower_sector->{Hash}->{towername};
159     $tower_name =~ s/\s/_/g;
160
161     #check if tower has been set up as an access point.
162     $existing_tower_ap = $self->api_get_accesspoint($tower_name) unless $self->{'__saisei_error'};;
163
164     #if tower does not exist as an access point create it.
165     $self->api_create_accesspoint(
166         $tower_name,
167         $tower_sector->{Hash}->{toweruprate},
168         $tower_sector->{Hash}->{towerdownrate}
169     ) unless $existing_tower_ap;
170
171     my $existing_sector_ap;
172     my $sector_name = $tower_sector->{Hash}->{sectorname};
173     $sector_name =~ s/\s/_/g;
174
175     #check if sector has been set up as an access point.
176     $existing_sector_ap = $self->api_get_accesspoint($sector_name);
177
178     #if sector does not exist as an access point create it.
179     $self->api_create_accesspoint(
180         $sector_name,
181         $tower_sector->{Hash}->{sectoruprate},
182         $tower_sector->{Hash}->{sectordownrate},
183         $tower_name,
184     ) unless $existing_sector_ap;
185
186     # Attach newly created sector to it's tower.
187     $self->api_modify_accesspoint($sector_name, $tower_name) unless ($self->{'__saisei_error'} || $existing_sector_ap);
188
189     # set access point to existing one or newly created one.
190     my $accesspoint = $existing_sector_ap ? $existing_sector_ap : $self->api_get_accesspoint($sector_name);
191
192     ## tie host to user add sector name as access point.
193     $self->api_add_host_to_user(
194       $user->{collection}->[0]->{name},
195       $rateplan->{collection}->[0]->{name},
196       $svc_broadband->{Hash}->{ip_addr},
197       $accesspoint->{collection}->[0]->{name},
198     ) unless $self->{'__saisei_error'};
199   }
200
201   return $self->api_error;
202
203 }
204
205 sub _export_replace {
206   my ($self, $svc_phone) = @_;
207   return '';
208 }
209
210 sub _export_delete {
211   my ($self, $svc_broadband) = @_;
212
213   my $cust_main = $svc_broadband->cust_main;
214   return "Could not load service customer" unless $cust_main;
215   my $conf = new FS::Conf;
216
217   my $rateplan_name = $svc_broadband->{Hash}->{description};
218   $rateplan_name =~ s/\s/_/g;
219
220   my @email = map { $_->emailaddress } FS::Record::qsearch({
221         'table'     => 'cust_contact',
222         'select'    => 'emailaddress',
223         'addl_from' => ' JOIN contact_email USING (contactnum)',
224         'hashref'   => { 'custnum' => $cust_main->{Hash}->{custnum}, },
225     });
226   my $username = $email[0]; 
227
228   ## tie host to user
229   $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
230
231   return '';
232 }
233
234 sub _export_suspend {
235   my ($self, $svc_phone) = @_;
236   return '';
237 }
238
239 sub _export_unsuspend {
240   my ($self, $svc_phone) = @_;
241   return '';
242 }
243
244 =head1 Saisei API
245
246 These methods allow access to the Saisei API using the credentials
247 set in the export options.
248
249 =cut
250
251 =head2 api_call
252
253 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
254 Places an api call to the specified path and method with the specified params.
255 Returns the decoded json object returned by the api call.
256 Returns empty on failure;  retrieve error messages using L</api_error>.
257
258 =cut
259
260 sub api_call {
261   my ($self,$method,$path,$params) = @_;
262   $self->{'__saisei_error'} = '';
263   my $auth_info = $self->option('username') . ':' . $self->option('password');
264   $params ||= {};
265
266   warn "Calling $method on http://"
267     .$self->{Hash}->{machine}.':'.$self->option('port')
268     ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
269
270   my $data = encode_json($params) if keys %{ $params };
271
272   my $client = REST::Client->new();
273   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
274   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
275   $client->$method('/rest/stm/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
276
277   warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
278
279   my $result;
280
281   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
282     eval { $result = decode_json($client->responseContent()) };
283     unless ($result) {
284       $self->{'__saisei_error'} = "Error decoding json: $@";
285       return;
286     }
287   }
288   else {
289     $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent();
290     warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
291     return; 
292   }
293
294   return $result;
295   
296 }
297
298 =head2 api_error
299
300 Returns the error string set by L</Saisei API> methods,
301 or a blank string if most recent call produced no errors.
302
303 =cut
304
305 sub api_error {
306   my $self = shift;
307   return $self->{'__saisei_error'} || '';
308 }
309
310 =head2 api_get_policies
311
312 Gets a list of global policies.
313
314 =cut
315
316 sub api_get_policies {
317   my $self = shift;
318
319   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
320   return if $self->api_error;
321   $self->{'__saisei_error'} = "Did not receive any global policies"
322     unless $get_policies;
323
324   return $get_policies;
325 }
326
327 =head2 api_get_rateplan
328
329 Gets rateplan info for specific rateplan.
330
331 =cut
332
333 sub api_get_rateplan {
334   my $self = shift;
335   my $rateplan = shift;
336
337   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
338   return if $self->api_error;
339   $self->{'__saisei_error'} = "Did not receive any rateplan info"
340     unless $get_rateplan;
341
342   return $get_rateplan;
343 }
344
345 =head2 api_get_user
346
347 Gets user info for specific user.
348
349 =cut
350
351 sub api_get_user {
352   my $self = shift;
353   my $user = shift;
354
355   my $get_user = $self->api_call("GET", "/users/$user");
356   return if $self->api_error;
357   $self->{'__saisei_error'} = "Did not receive any user info"
358     unless $get_user;
359
360   return $get_user;
361 }
362
363 =head2 api_get_accesspoint
364
365 Gets user info for specific access point.
366
367 =cut
368
369 sub api_get_accesspoint {
370   my $self = shift;
371   my $accesspoint = shift;
372
373   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
374   return if $self->api_error;
375   $self->{'__saisei_error'} = "Did not receive any access point info"
376     unless $get_accesspoint;
377
378   return $get_accesspoint;
379 }
380
381 =head2 api_create_rateplan
382
383 Creates a rateplan.
384
385 =cut
386
387 sub api_create_rateplan {
388   my ($self, $svc, $rateplan) = @_;
389
390   my $new_rateplan = $self->api_call(
391       "PUT", 
392       "/rate_plans/$rateplan",
393       {
394         'downstream_rate' => $svc->{Hash}->{speed_down},
395         'upstream_rate' => $svc->{Hash}->{speed_up},
396       },
397   );
398
399   $self->{'__saisei_error'} = "Rate Plan not created"
400     unless $new_rateplan; # should never happen
401   return $new_rateplan;
402
403 }
404
405 =head2 api_modify_rateplan
406
407 Modify a rateplan.
408
409 =cut
410
411 sub api_modify_rateplan {
412   my ($self,$policies,$svc,$rateplan_name) = @_;
413
414   foreach my $policy (@$policies) {
415     my $policyname = $policy->{name};
416     my $rate_multiplier = '';
417     if ($policy->{background}) { $rate_multiplier = ".01"; }
418     my $modified_rateplan = $self->api_call(
419       "PUT", 
420       "/rate_plans/$rateplan_name/partitions/$policyname",
421       {
422         'restricted'      =>  $policy->{assured},         # policy_assured_flag
423         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
424         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
425       },
426     );
427
428     $self->{'__saisei_error'} = "Rate Plan not modified"
429       unless $modified_rateplan; # should never happen
430     
431   }
432
433   return;
434  
435 }
436
437 =head2 api_create_user
438
439 Creates a user.
440
441 =cut
442
443 sub api_create_user {
444   my ($self,$user, $description) = @_;
445
446   my $new_user = $self->api_call(
447       "PUT", 
448       "/users/$user",
449       {
450         'description' => $description,
451       },
452   );
453
454   $self->{'__saisei_error'} = "User not created"
455     unless $new_user; # should never happen
456
457   return $new_user;
458
459 }
460
461 =head2 api_create_accesspoint
462
463 Creates a access point.
464
465 =cut
466
467 sub api_create_accesspoint {
468   my ($self,$accesspoint, $uprate, $downrate) = @_;
469
470   # this has not been tested, but should work, if needed.
471   my $new_accesspoint = $self->api_call(
472       "PUT",
473       "/access_points/$accesspoint",
474       {
475          'downstream_rate_limit' => $downrate,
476          'upstream_rate_limit' => $uprate,
477       },
478   );
479
480   $self->{'__saisei_error'} = "Access point not created"
481     unless $new_accesspoint; # should never happen
482   return;
483
484 }
485
486 =head2 api_modify_accesspoint
487
488 Modify a access point.
489
490 =cut
491
492 sub api_modify_accesspoint {
493   my ($self, $accesspoint, $uplink) = @_;
494
495   my $modified_rateplan = $self->api_call(
496     "PUT",
497     "/access_points/$accesspoint",
498     {
499       'uplink' => $uplink, # name of attached access point
500     },
501   );
502
503   $self->{'__saisei_error'} = "Rate Plan not modified"
504     unless $modified_rateplan; # should never happen
505
506   return;
507
508 }
509
510 =head2 api_add_host_to_user
511
512 ties host to user, rateplan and default access point.
513
514 =cut
515
516 sub api_add_host_to_user {
517   my ($self,$user, $rateplan, $ip, $accesspoint) = @_;
518
519   my $new_host = $self->api_call(
520       "PUT", 
521       "/hosts/$ip",
522       {
523         'user'      => $user,
524         'rate_plan' => $rateplan,
525         'access_point' => $accesspoint,
526       },
527   );
528
529   $self->{'__saisei_error'} = "Host not created"
530     unless $new_host; # should never happen
531
532   return $new_host;
533
534 }
535
536 =head2 api_delete_host_to_user
537
538 unties host to user and rateplan.
539
540 =cut
541
542 sub api_delete_host_to_user {
543   my ($self,$user, $rateplan, $ip) = @_;
544
545   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
546     return if $self->api_error;
547   $self->{'__saisei_error'} = "Did not receive a default rate plan"
548     unless $default_rate_plan;
549
550   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
551
552   my $delete_host = $self->api_call(
553       "PUT",
554       "/hosts/$ip",
555       {
556         'user'          => '<none>',
557         'access_point'  => '<none>',
558         'rate_plan'     => $default_rateplan_name,
559       },
560   );
561
562   $self->{'__saisei_error'} = "Host not created"
563     unless $delete_host; # should never happen
564
565   return $delete_host;
566
567 }
568
569 =head1 SEE ALSO
570
571 L<FS::part_export>
572
573 =cut
574
575 1;