This commit was generated by cvs2svn to compensate for changes in r11022,
[freeside.git] / FS / FS / part_export / router.pm
1 package FS::part_export::router;
2
3 =head1 FS::part_export::router
4
5 This export connects to a router and transmits commands via telnet or SSH.
6 It requires the following custom router fields:
7
8 =head1 Required custom fields
9
10 =over 4
11
12 =item admin_address - IP address (or hostname) to connect.
13
14 =item admin_user - Username for the router.
15
16 =item admin_password - Password for the  router.
17
18 =item admin_protocol - Protocol to use for the router.  'telnet' or 'ssh'.  The ssh protocol only support password-less (ie. RSA key) authentication.  As such, the admin_password field isn't used if ssh is specified.
19
20 =item admin_timeout - Time in seconds to wait for a connection.
21
22 =item admin_prompt - A regular expression matching the router's prompt.  See Net::Telnet for details.  Only applies to the 'telnet' protocol.
23
24 =item admin_cmd_insert - Insert export command.
25
26 =item admin_cmd_insert_error - Insert export command error pattern.
27
28 =item admin_cmd_delete - Delete export command.
29
30 =item admin_cmd_delete_error - Delete export command error pattern.
31
32 =item admin_cmd_replace - Replace export command.
33
34 =item admin_cmd_replace_error - Replace export command error pattern.
35
36 =item admin_cmd_suspend - Suspend export command.
37
38 =item admin_cmd_suspend_error - Support export command error pattern.
39
40 =item admin_cmd_unsuspend - Unsuspend export command.
41
42 =item admin_cmd_unsuspend_error - Unsuspend export command error pattern.
43
44 The admin_cmd_* virtual fields, if set, will be processed in one of two ways.  After being expanded, they will be run on the router specified by admin_address using the protocol specified by admin_protocol.
45
46 =over 4
47
48 =item Text::Template
49
50 If the export command contains the string [@--, then it will be processed with Text::Template using [@-- and --@] as delimeters.
51
52 =item eval
53
54 If the export command does not contain [@--, it will be double quoted and eval'd.
55
56 =back
57
58 The admin_cmd_*_error virtual fields, if set, define a regular expression that will be matched against the output of the command being run.  If the pattern matches, an error will be raised using the output as the error.
59
60 If any of the required router virtual fields are not defined, then the export silently declines.
61
62 =back
63
64 The export itself takes no options.
65
66 =cut
67
68 use strict;
69 use vars qw(@ISA %info $me $DEBUG);
70 use Tie::IxHash;
71 use Text::Template;
72
73 use FS::Record qw(qsearchs);
74 use FS::part_export;
75
76 @ISA = qw(FS::part_export);
77
78 tie my %options, 'Tie::IxHash',
79   'protocol' => {
80           label=>'Protocol',
81           type =>'select',
82           options => [qw(telnet ssh)],
83           default => 'telnet'},
84 ;
85
86 %info = (
87   'svc'     => 'svc_broadband',
88   'desc'    => 'Send a command to a router.',
89   'options' => \%options,
90   'notes'   => 'Installation of Net::Telnet from CPAN is required for telnet connections.  This export will execute if the following virtual fields are set on the router: admin_user, admin_password, admin_address, admin_timeout, admin_prompt.  Option virtual fields are: admin_cmd_insert, admin_cmd_replace, admin_cmd_delete, admin_cmd_suspend, admin_cmd_unsuspend.  See the module documentation for a full list of required/supported router virtual fields.',
91 );
92
93 $me = '[' . __PACKAGE__ . ']';
94 $DEBUG = 1;
95
96
97 sub rebless { shift; }
98
99 sub _field_prefix { 'admin'; }
100
101 sub _req_router_fields {
102   map {
103     $_[0]->_field_prefix . '_' . $_
104   } (qw(address prompt user));
105 }
106
107 sub _export_insert {
108   my($self) = shift;
109   warn "Running insert for " . ref($self);
110   $self->_export_command('insert', @_);
111 }
112
113 sub _export_delete {
114   my($self) = shift;
115   $self->_export_command('delete', @_);
116 }
117
118 sub _export_suspend {
119   my($self) = shift;
120   $self->_export_command('suspend', @_);
121 }
122
123 sub _export_unsuspend {
124   my($self) = shift;
125   $self->_export_command('unsuspend', @_);
126 }
127
128 sub _export_replace {
129   my($self) = shift;
130   $self->_export_command('replace', @_);
131 }
132
133 sub _export_command {
134   my ($self, $action, $svc_broadband) = (shift, shift, shift);
135   my ($error, $old);
136   
137   if ($action eq 'replace') {
138     $old = shift;
139   }
140
141  warn "[debug]$me Processing action '$action'" if $DEBUG;
142
143   # fetch router info
144   my $router = $self->_get_router($svc_broadband, @_);
145   unless ($router) {
146     return "Unable to lookup router for $action export";
147   }
148
149   unless ($self->_check_router_fields($router)) {
150     # Virtual fields aren't defined.  Exit silently.
151     warn "[debug]$me Required router virtual fields not defined.  Returning..."
152       if $DEBUG;
153     return '';
154   }
155
156   my $args;
157   ($error, $args) = $self->_prepare_args(
158     $action,
159     $router,
160     $svc_broadband,
161     ($old ? $old : ()),
162     @_
163   );
164
165   if ($error) {
166     # Error occured while preparing args.
167     return $error;
168   } elsif (not defined $args) {
169     # Silently decline.
170     warn "[debug]$me Declining '$action' export" if $DEBUG;
171     return '';
172   } # else ... queue the export.
173
174   warn "[debug]$me Queueing with args: " . join(', ', @$args) if $DEBUG;
175
176   return(
177     $self->_queue(
178       $svc_broadband->svcnum,
179       $self->_get_cmd_sub($svc_broadband, $router),
180       @$args
181     )
182   );
183
184 }
185
186 sub _prepare_args {
187
188   my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
189   my $old = shift if ($action eq 'replace');
190   my $error = '';
191
192   my $field_prefix = $self->_field_prefix;
193   my $command = $router->getfield("${field_prefix}_cmd_${action}");
194   unless ($command) {
195     warn "[debug]$me router custom field '${field_prefix}_cmd_$action' "
196       . "is not defined." if $DEBUG;
197     return '';
198   }
199
200   if ($command =~ /\[\@--/) { # Use Text::Template
201
202     my $template_data = {};
203
204     if ($action eq 'replace') {
205       $template_data->{"old_$_"} = $old->getfield($_) foreach $old->fields;
206       $template_data->{"new_$_"} = $svc_broadband->getfield($_)
207         foreach $svc_broadband->fields;
208     } else {
209       $template_data->{$_} = $svc_broadband->getfield($_)
210         foreach $svc_broadband->fields;
211     }
212
213     my $template = new Text::Template (
214       TYPE => 'STRING',
215       SOURCE => $command,
216       DELIMITERS => [ '[@--', '--@]' ],
217     ) or return "Unable to construct template for router command: "
218                 . $Text::Template::ERROR;
219
220     $command = $template->fill_in(
221       HASH => $template_data,
222       BROKEN_ARG => \$error,
223       BROKEN => sub {
224         my %bargs = @_;
225         my $err = $bargs{'arg'};
226         $$err = $bargs{'error'};
227         return undef;
228       },
229     );
230
231     if (not defined $command or $error) {
232       $error ||= $Text::Template::ERROR;
233       return "Unable to fill-in template for router command: $error";
234     }
235
236   } else { # Use eval
237     no strict 'vars';
238     no strict 'refs';
239
240     if ($action eq 'replace') {
241       ${"old_$_"} = $old->getfield($_) foreach $old->fields;
242       ${"new_$_"} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
243       $command = eval(qq("$command"));
244     } else {
245       ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
246       $command = eval(qq("$command"));
247     }
248     return $@ if $@;
249   }
250
251   my $args = [
252     'user' => $router->getfield($field_prefix . '_user'),
253     'password' => $router->getfield($field_prefix . '_password'),
254     'host' => $router->getfield($field_prefix . '_address'),
255     'Timeout' => $router->getfield($field_prefix . '_timeout'),
256     'Prompt' => $router->getfield($field_prefix . '_prompt'),
257     'command' => $command,
258   ];
259
260   my $error_check = $router->getfield("${field_prefix}_cmd_${action}_error");
261   push(@$args, ('error_check' => $error_check)) if ($error_check);
262
263   return('', $args);
264
265 }
266
267 sub _get_cmd_sub {
268
269   my ($self, $svc_broadband, $router) = (shift, shift, shift);
270
271   my $protocol = (
272     $router->getfield($self->_field_prefix . '_protocol') =~ /^(telnet|ssh)$/
273   ) ? $1 : 'telnet';
274
275   return(ref($self)."::".$protocol."_cmd");
276
277 }
278
279 sub _check_router_fields {
280
281   my ($self, $router, $action) = (shift, shift, shift);
282   my @check_fields = $self->_req_router_fields;
283
284   foreach (@check_fields) {
285     if ($router->getfield($_) eq '') {
286       warn "[debug]$me Required field '$_' is unset" if $DEBUG;
287       return 0;
288     } else {
289       return 1;
290     }
291   }
292
293 }
294
295 sub _queue {
296   my( $self, $svcnum, $cmd_sub ) = (shift, shift, shift);
297   my $queue = new FS::queue {
298     'svcnum' => $svcnum,
299   };
300   $queue->job($cmd_sub);
301   $queue->insert(@_);
302 }
303
304 sub _get_router {
305   my ($self, $svc_broadband, %args) = (shift, shift, shift, @_);
306
307   my $router;
308   if ($args{'routernum'}) {
309     $router = qsearchs('router', { routernum => $args{'routernum'}});
310   } else {
311     $router = $svc_broadband->addr_block->router;
312   }
313
314   return($router);
315
316 }
317
318
319 # Subroutines
320 sub ssh_cmd {
321   my %arg = @_;
322
323   eval 'use Net::SSH \'0.08\'';
324   die $@ if $@;
325
326   my @out = &Net::SSH::ssh_cmd( { @_ } );
327   my $error = &_cmd_error_check(\%arg, \@out);
328
329   die ("Error while processing ssh command: $error") if $error;
330
331   return '';
332
333 }
334
335 sub telnet_cmd {
336   my %arg = @_;
337
338   eval 'use Net::Telnet';
339   die $@ if $@;
340
341   my $t = new Net::Telnet (Timeout => $arg{'Timeout'},
342                            Prompt  => $arg{'Prompt'});
343   $t->open($arg{'host'});
344   $t->login($arg{'user'}, $arg{'password'});
345   my @out  = $t->cmd($arg{'command'});
346   my $error = &_cmd_error_check(\%arg, \@out);
347
348   die ("Error while processing telnet command: $error") if $error;
349
350   return '';
351
352 }
353
354 sub _cmd_error_check {
355   my ($arg, $out) = (shift, shift);
356
357   die "_cmd_error_check called without proper arguments"
358     unless (ref($arg) eq 'HASH' and ref($out) eq 'ARRAY');
359
360   unless (exists($arg->{'error_check'}) and $arg->{'error_check'} ne '') {
361     #Preserve default behaviour and return output if a check isn't defined.
362     warn "Output from router command: " . join('', @$out) if $DEBUG;
363     return '';
364   }
365
366   my $error_check = $arg->{'error_check'};
367   foreach (@$out) {
368     return $_ if /$error_check/;
369   }
370
371   return '';
372
373 }
374
375 1;