Merge branch 'master' of ssh://git.freeside.biz/home/git/freeside
[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 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 This export will use the broadband service descriptive label for the Saisei rate plan name and
32 will use the email from the first contact for the Saisei username that will be
33 attached to this rate plan.  It will use the Saisei default Access Point.
34
35 Hostname or IP - Host name to Saisei API
36 Port - <I>Port number to Saisei API
37 User Name -  <I>Saisei API user name
38 Password - <I>Saisei API password
39
40 This module also provides generic methods for working through the L</Saisei API>.
41
42 =cut
43
44 tie my %options, 'Tie::IxHash',
45   'port'             => { label => 'Port',
46                           default => 5000 },
47   'username'         => { label => 'User Name',
48                           default => '' },
49   'password'         => { label => 'Password',
50                           default => '' },
51   'debug'            => { type => 'checkbox',
52                           label => 'Enable debug warnings' },
53 ;
54
55 %info = (
56   'svc'             => 'svc_broadband',
57   'desc'            => 'Export broadband service/account to Saisei',
58   'options'         => \%options,
59   'notes'           => <<'END',
60 This is a customer integration with Saisei.  This will setup a rate plan and tie 
61 the rate plan to a host via the Saisei API when the broadband service is provisioned.  
62 It will also untie the rate  plan via the API upon unprovisioning of the broadband service.
63 <P>This export will use the broadband service descriptive label for the Saisei rate plan name and
64 will use the email from the first contact for the Saisei username that will be
65 attached to this rate plan.  It will use the Saisei default Access Point.
66 <P>
67 Required Fields:
68 <UL>
69 <LI>Hostname or IP - <I>Host name to Saisei API</I></LI>
70 <LI>Port - <I>Port number to Saisei API</I></LI>
71 <LI>User Name -  <I>Saisei API user name</I></LI>
72 <LI>Password - <I>Saisei API password</I></LI>
73 </UL>
74 END
75 );
76
77 sub _export_insert {
78   my ($self, $svc_broadband) = @_;
79   my $rateplan_name = $svc_broadband->{Hash}->{description};
80    $rateplan_name =~ s/\s/_/g;
81
82
83   # load needed info from our end
84   my $cust_main = $svc_broadband->cust_main;
85   return "Could not load service customer" unless $cust_main;
86   my $conf = new FS::Conf;
87
88   # get policy list
89   my $policies = $self->api_get_policies();
90
91   # check for existing rate plan
92   my $existing_rateplan;
93   $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
94
95   # if no existing rate plan create one and modify it.
96   $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
97   $self->api_modify_rateplan($policies->{collection}, $svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
98
99   # set rateplan to existing one or newly created one.
100   my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
101
102   my @email = map { $_->emailaddress } FS::Record::qsearch({
103         'table'     => 'cust_contact',
104         'select'    => 'emailaddress',
105         'addl_from' => ' JOIN contact_email USING (contactnum)',
106         'hashref'   => { 'custnum' => $cust_main->{Hash}->{custnum}, },
107     });
108   my $username = $email[0];
109   my $description = $cust_main->{Hash}->{first}." ".$cust_main->{Hash}->{last};
110
111   if (!$username) {
112     $self->{'__saisei_error'} = 'no username - can not export';
113     warn "No email found $username\n" if $self->option('debug');
114     return;
115   }
116   else {
117     # check for existing user.
118     my $existing_user;
119     $existing_user = $self->api_get_user($username) unless $self->{'__saisei_error'};
120  
121     # if no existing user create one.
122     $self->api_create_user($username, $description) unless $existing_user;
123
124     # set user to existing one or newly created one.
125     my $user = $existing_user ? $existing_user : $self->api_get_user($username);
126
127     ## add access point ?
128  
129     ## tie host to user
130     $self->api_add_host_to_user($user->{collection}->[0]->{name}, $rateplan->{collection}->[0]->{name}, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
131   }
132
133   return '';
134
135 }
136
137 sub _export_replace {
138   my ($self, $svc_phone) = @_;
139   return '';
140 }
141
142 sub _export_delete {
143   my ($self, $svc_broadband) = @_;
144
145   my $cust_main = $svc_broadband->cust_main;
146   return "Could not load service customer" unless $cust_main;
147   my $conf = new FS::Conf;
148
149   my $rateplan_name = $svc_broadband->{Hash}->{description};
150   $rateplan_name =~ s/\s/_/g;
151
152   my @email = map { $_->emailaddress } FS::Record::qsearch({
153         'table'     => 'cust_contact',
154         'select'    => 'emailaddress',
155         'addl_from' => ' JOIN contact_email USING (contactnum)',
156         'hashref'   => { 'custnum' => $cust_main->{Hash}->{custnum}, },
157     });
158   my $username = $email[0]; 
159
160   ## tie host to user
161   $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
162
163   return '';
164 }
165
166 sub _export_suspend {
167   my ($self, $svc_phone) = @_;
168   return '';
169 }
170
171 sub _export_unsuspend {
172   my ($self, $svc_phone) = @_;
173   return '';
174 }
175
176 =head1 Saisei API
177
178 These methods allow access to the Saisei API using the credentials
179 set in the export options.
180
181 =cut
182
183 =head2 api_call
184
185 Accepts I<$method>, I<$path>, I<$params> hashref and optional.
186 Places an api call to the specified path and method with the specified params.
187 Returns the decoded json object returned by the api call.
188 Returns empty on failure;  retrieve error messages using L</api_error>.
189
190 =cut
191
192 sub api_call {
193   my ($self,$method,$path,$params) = @_;
194   $self->{'__saisei_error'} = '';
195   my $auth_info = $self->option('username') . ':' . $self->option('password');
196   $params ||= {};
197
198   warn "Calling $method on http://"
199     .$self->{Hash}->{machine}.':'.$self->option('port')
200     ."/rest/stm/configurations/running/$path\n" if $self->option('debug');
201
202   my $data = encode_json($params) if keys %{ $params };
203
204   my $client = REST::Client->new();
205   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
206   $client->setHost('http://'.$self->{Hash}->{machine}.':'.$self->option('port'));
207   $client->$method('/rest/stm/configurations/running'.$path, $data, { "Content-type" => 'application/json'});
208
209   warn "Response Code is ".$client->responseCode()."\n" if $self->option('debug');
210
211   my $result;
212
213   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
214     eval { $result = decode_json($client->responseContent()) };
215     unless ($result) {
216       $self->{'__saisei_error'} = "Error decoding json: $@";
217       return;
218     }
219   }
220   else {
221     $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent();
222     warn "Response Content is\n".$client->responseContent."\n" if $self->option('debug');
223     return; 
224   }
225
226   return $result;
227   
228 }
229
230 =head2 api_error
231
232 Returns the error string set by L</PortaOne API> methods,
233 or a blank string if most recent call produced no errors.
234
235 =cut
236
237 sub api_error {
238   my $self = shift;
239   return $self->{'__saisei_error'} || '';
240 }
241
242 =head2 api_get_policies
243
244 Gets a list of global policies.
245
246 =cut
247
248 sub api_get_policies {
249   my $self = shift;
250
251   my $get_policies = $self->api_call("GET", '/policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
252   return if $self->api_error;
253   $self->{'__saisei_error'} = "Did not receive any global policies"
254     unless $get_policies;
255
256   return $get_policies;
257 }
258
259 =head2 api_get_rateplan
260
261 Gets rateplan info for specific rateplan.
262
263 =cut
264
265 sub api_get_rateplan {
266   my $self = shift;
267   my $rateplan = shift;
268
269   my $get_rateplan = $self->api_call("GET", "/rate_plans/$rateplan");
270   return if $self->api_error;
271   $self->{'__saisei_error'} = "Did not receive any rateplan info"
272     unless $get_rateplan;
273
274   return $get_rateplan;
275 }
276
277 =head2 api_get_user
278
279 Gets user info for specific user.
280
281 =cut
282
283 sub api_get_user {
284   my $self = shift;
285   my $user = shift;
286
287   my $get_user = $self->api_call("GET", "/users/$user");
288   return if $self->api_error;
289   $self->{'__saisei_error'} = "Did not receive any user info"
290     unless $get_user;
291
292   return $get_user;
293 }
294
295 =head2 api_get_accesspoint
296
297 Gets user info for specific access point.
298
299 =cut
300
301 sub api_get_accesspoint {
302   my $self = shift;
303   my $accesspoint;
304
305   my $get_accesspoint = $self->api_call("GET", "/access_points/$accesspoint");
306   return if $self->api_error;
307   $self->{'__saisei_error'} = "Did not receive any user info"
308     unless $get_accesspoint;
309
310   return;
311 }
312
313 =head2 api_create_rateplan
314
315 Creates a rateplan.
316
317 =cut
318
319 sub api_create_rateplan {
320   my ($self, $svc, $rateplan) = @_;
321
322   my $new_rateplan = $self->api_call(
323       "PUT", 
324       "/rate_plans/$rateplan",
325       {
326         'downstream_rate' => $svc->{Hash}->{speed_down},
327         'upstream_rate' => $svc->{Hash}->{speed_up},
328       },
329   );
330
331   $self->{'__saisei_error'} = "Rate Plan not created"
332     unless $new_rateplan; # should never happen
333   return $new_rateplan;
334
335 }
336
337 =head2 api_modify_rateplan
338
339 Modify a rateplan.
340
341 =cut
342
343 sub api_modify_rateplan {
344   my ($self,$policies,$svc,$rateplan_name) = @_;
345
346   foreach my $policy (@$policies) {
347     my $policyname = $policy->{name};
348     my $rate_multiplier = '';
349     if ($policy->{background}) { $rate_multiplier = ".01"; }
350     my $modified_rateplan = $self->api_call(
351       "PUT", 
352       "/rate_plans/$rateplan_name/partitions/$policyname",
353       {
354         'restricted'      =>  $policy->{assured},         # policy_assured_flag
355         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
356         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
357       },
358     );
359
360     $self->{'__saisei_error'} = "Rate Plan not modified"
361       unless $modified_rateplan; # should never happen
362     
363   }
364
365   return;
366  
367 }
368
369 =head2 api_create_user
370
371 Creates a user.
372
373 =cut
374
375 sub api_create_user {
376   my ($self,$user, $description) = @_;
377
378   my $new_user = $self->api_call(
379       "PUT", 
380       "/users/$user",
381       {
382         'description' => $description,
383       },
384   );
385
386   $self->{'__saisei_error'} = "User not created"
387     unless $new_user; # should never happen
388
389   return $new_user;
390
391 }
392
393 =head2 api_create_accesspoint
394
395 Creates a access point.
396
397 =cut
398
399 sub api_create_accesspoint {
400   my ($self,$accesspoint) = @_;
401
402   # this has not been tested, but should work, if needed.
403   #my $new_accesspoint = $self->api_call(
404   #    "PUT", 
405   #    "/access_points/$accesspoint",
406   #    {
407   #      'description' => 'my description',
408   #    },
409   #);
410
411   #$self->{'__saisei_error'} = "Access point not created"
412   #  unless $new_accesspoint; # should never happen
413   return;
414
415 }
416
417 =head2 api_add_host_to_user
418
419 ties host to user, rateplan and default access point.
420
421 =cut
422
423 sub api_add_host_to_user {
424   my ($self,$user, $rateplan, $ip) = @_;
425
426   my $new_host = $self->api_call(
427       "PUT", 
428       "/hosts/$ip",
429       {
430         'user'      => $user,
431         'rate_plan' => $rateplan,
432       },
433   );
434
435   $self->{'__saisei_error'} = "Host not created"
436     unless $new_host; # should never happen
437
438   return $new_host;
439
440 }
441
442 =head2 api_delete_host_to_user
443
444 unties host to user and rateplan.
445
446 =cut
447
448 sub api_delete_host_to_user {
449   my ($self,$user, $rateplan, $ip) = @_;
450
451   my $default_rate_plan = $self->api_call("GET", '?token=1&select=default_rate_plan');
452     return if $self->api_error;
453   $self->{'__saisei_error'} = "Did not receive a default rate plan"
454     unless $default_rate_plan;
455
456   my $default_rateplan_name = $default_rate_plan->{collection}->[0]->{default_rate_plan}->{link}->{name};
457
458   my $delete_host = $self->api_call(
459       "PUT",
460       "/hosts/$ip",
461       {
462         'user'          => '<none>',
463         'access_point'  => '<none>',
464         'rate_plan'     => $default_rateplan_name,
465       },
466   );
467
468   $self->{'__saisei_error'} = "Host not created"
469     unless $delete_host; # should never happen
470
471   return $delete_host;
472
473 }
474
475 =head1 SEE ALSO
476
477 L<FS::part_export>
478
479 =cut
480
481 1;