RT# 78356 - broadband svc export to saisei
[freeside.git] / FS / FS / part_export / saisei.pm
1 package FS::part_export::saisei;
2
3 use strict;
4 use base qw( FS::part_export );
5 use vars qw( @ISA %info );
6 use Date::Format 'time2str';
7 use Cpanel::JSON::XS;
8 use Net::HTTPS::Any qw(https_post);
9 use MIME::Base64;
10 use REST::Client;
11 use Data::Dumper;
12
13 use FS::Conf;
14
15 #@ISA = qw( FS::part_export::http );
16
17 =pod
18
19 =head1 NAME
20
21 FS::part_export::saisei
22
23 =head1 SYNOPSIS
24
25 Saisei integration for Freeside
26
27 =head1 DESCRIPTION
28
29 This export offers basic svc_broadband provisioning for Saisei.
30
31 This module also provides generic methods for working through the L</Saisei API>.
32
33 =cut
34
35 tie my %options, 'Tie::IxHash',
36   'username'         => { label => 'User Name',
37                           default => '' },
38   'password'         => { label => 'Password',
39                           default => '' },
40   'host'             => { label => 'Host',
41                           default => 'STM IP ADDRESS' },
42   'port'             => { label => 'Port',
43                           default => 5000 },
44   'customer_name'    => { label => 'Customer Name',
45                           default => 'FREESIDE CUST $custnum' },
46   'account_id'       => { label => 'Account ID',
47                           default => 'SVC$svcnum' },
48   'product_id'       => { label => 'Account Product ID' },
49   'debug'            => { type => 'checkbox',
50                           label => 'Enable debug warnings' },
51 ;
52
53 %info = (
54   'svc'             => 'svc_broadband',
55   'desc'            => 'Export broadband service/account to Saisei',
56   'options'         => \%options,
57   'notes'           => <<'END',
58 This is customer integration with Saisei.
59 END
60 );
61
62 #"/STM_IP:5000/rest/top/configurations/running/" is for http 5029 for https
63
64 #Creating User Names
65 #Users are tracked by their name which gives access to the internal slice data which in turn allows the viewing of  Applications and Geo-Locations.
66 #Creating a user name requires a command of the following format: -
67 #'put', 'users/USER_NAME', {'description':description}
68 #When creating a user name it is usual to add a description and since a user attribute set does not normally contain the users plan name it is best to encode it into the description field.
69
70 sub _export_insert {
71   my ($self, $svc_broadband) = @_;
72   my $rateplan_name = $svc_broadband->{Hash}->{description};
73    $rateplan_name =~ s/\s/_/g;
74
75
76   # load needed info from our end
77   my $cust_main = $svc_broadband->cust_main;
78   return "Could not load service customer" unless $cust_main;
79   my $conf = new FS::Conf;
80
81   # get policy list
82   my $policies = $self->api_get_policies();
83
84   # check for existing rate plan
85   my $existing_rateplan;
86   $existing_rateplan = $self->api_get_rateplan($rateplan_name) unless $self->{'__saisei_error'};
87
88   # if no existing rate plan create one and modify it.
89   $self->api_create_rateplan($svc_broadband, $rateplan_name) unless $existing_rateplan;
90   $self->api_modify_rateplan($policies->{collection}, $svc_broadband, $rateplan_name) unless ($self->{'__saisei_error'} || $existing_rateplan);
91
92   # set rateplan to existing one or newly created one.
93   my $rateplan = $existing_rateplan ? $existing_rateplan : $self->api_get_rateplan($rateplan_name);
94
95   my @email = map { $_->emailaddress } FS::Record::qsearch({
96         'table'     => 'cust_contact',
97         'select'    => 'emailaddress',
98         'addl_from' => ' JOIN contact_email USING (contactnum)',
99         'hashref'   => { 'custnum' => $cust_main->{Hash}->{custnum}, },
100     });
101   my $username = $email[0];
102   my $description = $cust_main->{Hash}->{first}." ".$cust_main->{Hash}->{last};
103
104   # check for existing user.
105   my $existing_user;
106   $existing_user = $self->api_get_user($username) unless ( $self->{'__saisei_error'} || !$username);
107  
108   # if no existing user create one.
109   $self->api_create_user($username, $description) unless $existing_user;
110
111   # set user to existing one or newly created one.
112   my $user = $existing_user ? $existing_user : $self->api_get_user($username);
113
114   ## add access point ?
115  
116   ## tie host to user
117   $self->api_add_host_to_user($user->{collection}->[0]->{name}, $rateplan->{collection}->[0]->{name}, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
118
119   #die('ending for testing');
120   return '';
121
122 }
123
124 sub _export_replace {
125   my ($self, $svc_phone) = @_;
126   return '';
127 }
128
129 sub _export_delete {
130   my ($self, $svc_broadband) = @_;
131
132   my $cust_main = $svc_broadband->cust_main;
133   return "Could not load service customer" unless $cust_main;
134   my $conf = new FS::Conf;
135
136   my $rateplan_name = $svc_broadband->{Hash}->{description};
137   $rateplan_name =~ s/\s/_/g;
138
139   my @email = map { $_->emailaddress } FS::Record::qsearch({
140         'table'     => 'cust_contact',
141         'select'    => 'emailaddress',
142         'addl_from' => ' JOIN contact_email USING (contactnum)',
143         'hashref'   => { 'custnum' => $cust_main->{Hash}->{custnum}, },
144     });
145   my $username = $email[0]; 
146
147   ## tie host to user
148   $self->api_delete_host_to_user($username, $rateplan_name, $svc_broadband->{Hash}->{ip_addr}) unless $self->{'__saisei_error'};
149
150   return '';
151 }
152
153 sub _export_suspend {
154   my ($self, $svc_phone) = @_;
155   return '';
156 }
157
158 sub _export_unsuspend {
159   my ($self, $svc_phone) = @_;
160   return '';
161 }
162
163 =head1 Saisei API
164
165 These methods allow access to the Saisei API using the credentials
166 set in the export options.
167
168 =cut
169
170 =head2 api_call
171
172 Accepts I<$service>, I<$method>, I<$params> hashref and optional
173 I<$returnfield>.  Places an api call to the specified service
174 and method with the specified params.  Returns the decoded json
175 object returned by the api call.  If I<$returnfield> is specified,
176 returns only that field of the decoded object, and errors out if
177 that field does not exist.  Returns empty on failure;  retrieve
178 error messages using L</api_error>.
179
180 Must run L</api_login> first.
181
182 =cut
183
184 sub api_call {
185   my ($self,$method,$path,$params) = @_;
186   $self->{'__saisei_error'} = '';
187   my $auth_info = $self->option('username') . ':' . $self->option('password');
188   $params ||= {};
189
190   print "Calling /$method\n" if $self->option('debug');
191
192   my $data = encode_json($params) if keys %{ $params };
193
194   my $client = REST::Client->new();
195   $client->addHeader("Authorization", "Basic ".encode_base64($auth_info));
196   $client->setHost('http://'.$self->option('host').':'.$self->option('port'));
197   $client->$method('/rest/stm/configurations/running/'.$path, $data, { "Content-type" => 'application/json'});
198
199   my $result;
200
201   if ($client->responseCode() eq '200' || $client->responseCode() eq '201') {
202     eval { $result = decode_json($client->responseContent()) };
203     unless ($result) {
204       $self->{'__saisei_error'} = "Error decoding json: $@";
205       return;
206     }
207   }
208   else {
209     $self->{'__saisei_error'} = "Bad response from server during $method: " . $client->responseContent();
210     print "My response content fo /$method\n". Dumper($client->responseContent) if $self->option('debug');
211     return; 
212   }
213
214   return $result;
215   
216 }
217
218 =head2 api_error
219
220 Returns the error string set by L</PortaOne API> methods,
221 or a blank string if most recent call produced no errors.
222
223 =cut
224
225 sub api_error {
226   my $self = shift;
227   return $self->{'__saisei_error'} || '';
228 }
229
230 =head2 api_get_policies
231
232 Gets a list of global policies.
233
234 =cut
235
236 sub api_get_policies {
237   my $self = shift;
238
239   my $get_policies = $self->api_call("GET", 'policies/?token=1&order=name&start=0&limit=20&select=name%2Cpercent_rate%2Cassured%2C');
240   return if $self->api_error;
241   $self->{'__saisei_error'} = "Did not receive any global policies"
242     unless $get_policies;
243
244   return $get_policies;
245 }
246
247 =head2 api_get_rateplan
248
249 Gets rateplan info for specific rateplan.
250
251 =cut
252
253 sub api_get_rateplan {
254   my $self = shift;
255   my $rateplan = shift;
256
257   my $get_rateplan = $self->api_call("GET", "rate_plans/$rateplan");
258   return if $self->api_error;
259   $self->{'__saisei_error'} = "Did not receive any rateplan info"
260     unless $get_rateplan;
261
262   return $get_rateplan;
263 }
264
265 =head2 api_get_user
266
267 Gets user info for specific user.
268
269 =cut
270
271 sub api_get_user {
272   my $self = shift;
273   my $user = shift;
274
275   my $get_user = $self->api_call("GET", "users/$user");
276   return if $self->api_error;
277   $self->{'__saisei_error'} = "Did not receive any user info"
278     unless $get_user;
279
280   return $get_user;
281 }
282
283 =head2 api_get_accesspoint
284
285 Gets user info for specific access point.
286
287 =cut
288
289 sub api_get_accesspoint {
290   my $self = shift;
291   my $accesspoint;
292
293   my $get_accesspoint = $self->api_call("GET", "access_points/$accesspoint");
294   return if $self->api_error;
295   $self->{'__saisei_error'} = "Did not receive any user info"
296     unless $get_accesspoint;
297
298   return;
299 }
300
301 =head2 api_create_rateplan
302
303 Creates a rateplan.
304
305 =cut
306
307 sub api_create_rateplan {
308   my ($self, $svc, $rateplan) = @_;
309
310   my $new_rateplan = $self->api_call(
311       "PUT", 
312       "rate_plans/$rateplan",
313       {
314         'downstream_rate' => $svc->{Hash}->{speed_down},
315         'upstream_rate' => $svc->{Hash}->{speed_up},
316       },
317   );
318
319   $self->{'__saisei_error'} = "Rate Plan not created"
320     unless $new_rateplan; # should never happen
321   return $new_rateplan;
322
323 }
324
325 =head2 api_modify_rateplan
326
327 Modify a rateplan.
328
329 =cut
330
331 sub api_modify_rateplan {
332   my ($self,$policies,$svc,$rateplan_name) = @_;
333
334   foreach my $policy (@$policies) {
335     my $policyname = $policy->{name};
336     my $rate_multiplier = '';
337     if ($policy->{background}) { $rate_multiplier = ".01"; }
338     my $modified_rateplan = $self->api_call(
339       "PUT", 
340       "rate_plans/$rateplan_name/partitions/$policyname",
341       {
342         'restricted'      =>  $policy->{assured},         # policy_assured_flag
343         'rate_multiplier' => $rate_multiplier,           # policy_background 0.1
344         'rate'            =>  $policy->{percent_rate}, # policy_percent_rate
345       },
346     );
347
348     $self->{'__saisei_error'} = "Rate Plan not modified"
349       unless $modified_rateplan; # should never happen
350     
351   }
352
353   return;
354  
355 }
356
357 =head2 api_create_user
358
359 Creates a rateplan.
360
361 =cut
362
363 sub api_create_user {
364   my ($self,$user, $description) = @_;
365
366   my $new_user = $self->api_call(
367       "PUT", 
368       "users/$user",
369       {
370         'description' => $description,
371       },
372   );
373
374   $self->{'__saisei_error'} = "User not created"
375     unless $new_user; # should never happen
376
377   return $new_user;
378
379 }
380
381 =head2 api_create_accesspoint
382
383 Creates a access point.
384
385 =cut
386
387 sub api_create_accesspoint {
388   my ($self,$accesspoint) = @_;
389
390   my $new_accesspoint = $self->api_call(
391       "PUT", 
392       "access_points/$accesspoint",
393       {
394         'description' => 'my description',
395       },
396   );
397
398   $self->{'__saisei_error'} = "Access point not created"
399     unless $new_accesspoint; # should never happen
400   return;
401
402 }
403
404 =head2 api_add_host_to_user
405
406 ties host to user and rateplan.
407
408 =cut
409
410 sub api_add_host_to_user {
411   my ($self,$user, $rateplan, $ip) = @_;
412
413   my $new_host = $self->api_call(
414       "PUT", 
415       "hosts/$ip",
416       {
417         'user'      => $user,
418         'rate_plan' => $rateplan,
419       },
420   );
421
422   $self->{'__saisei_error'} = "Host not created"
423     unless $new_host; # should never happen
424
425   return $new_host;
426
427 }
428
429 =head2 api_add_host_to_user
430
431 ties host to user and rateplan.
432
433 =cut
434
435 sub api_delete_host_to_user {
436   my ($self,$user, $rateplan, $ip) = @_;
437
438   my $delete_host = $self->api_call("DELETE", "hosts/$ip");
439
440   $self->{'__saisei_error'} = "Host not created"
441     unless $delete_host; # should never happen
442
443   return $delete_host;
444
445 }
446
447 =head1 SEE ALSO
448
449 L<FS::part_export>
450
451 =cut
452
453 1;