FS::part_export::router
[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.  See below.
25
26 =item admin_cmd_delete - Delete export command.  See below.
27
28 =item admin_cmd_replace - Replace export command.  See below.
29
30 =item admin_cmd_suspend - Suspend export command.  See below.
31
32 =item admin_cmd_unsuspend - Unsuspend export command.  See below.
33
34 The admin_cmd_* virtual fields, if set, will be double quoted, eval'd, and executed on the router specified.
35
36 If any of the required router virtual fields are not defined, then the export silently declines.
37
38 =back
39
40 The export itself takes no options.
41
42 =cut
43
44 use strict;
45 use vars qw(@ISA %info $me $DEBUG);
46 use Tie::IxHash;
47 use String::ShellQuote;
48 use FS::Record qw(qsearchs);
49 use FS::part_export;
50
51 @ISA = qw(FS::part_export);
52
53 tie my %options, 'Tie::IxHash',
54   'protocol' => {
55           label=>'Protocol',
56           type =>'select',
57           options => [qw(telnet ssh)],
58           default => 'telnet'},
59 ;
60
61 %info = (
62   'svc'     => 'svc_broadband',
63   'desc'    => 'Send a command to a router.',
64   'options' => \%options,
65   '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.',
66 );
67
68 $me = '[' . __PACKAGE__ . ']';
69 $DEBUG = 1;
70
71
72 sub rebless { shift; }
73
74 sub _field_prefix { 'admin'; }
75
76 sub _req_router_fields {
77   map {
78     $_[0]->_field_prefix . '_' . $_
79   } (qw(address prompt user));
80 }
81
82 sub _export_insert {
83   my($self) = shift;
84   warn "Running insert for " . ref($self);
85   $self->_export_command('insert', @_);
86 }
87
88 sub _export_delete {
89   my($self) = shift;
90   $self->_export_command('delete', @_);
91 }
92
93 sub _export_suspend {
94   my($self) = shift;
95   $self->_export_command('suspend', @_);
96 }
97
98 sub _export_unsuspend {
99   my($self) = shift;
100   $self->_export_command('unsuspend', @_);
101 }
102
103 sub _export_replace {
104   my($self) = shift;
105   $self->_export_command('replace', @_);
106 }
107
108 sub _export_command {
109   my ($self, $action, $svc_broadband) = (shift, shift, shift);
110   my ($error, $old);
111   
112   if ($action eq 'replace') {
113     $old = shift;
114   }
115
116  warn "[debug]$me Processing action '$action'" if $DEBUG;
117
118   # fetch router info
119   my $router = $self->_get_router($svc_broadband, @_);
120   unless ($router) {
121     return "Unable to lookup router for $action export";
122   }
123
124   unless ($self->_check_router_fields($router)) {
125     # Virtual fields aren't defined.  Exit silently.
126     warn "[debug]$me Required router virtual fields not defined.  Returning...";
127     return '';
128   }
129
130   my $args;
131   ($error, $args) = $self->_prepare_args(
132     $action,
133     $router,
134     $svc_broadband,
135     ($old ? $old : ()),
136     @_
137   );
138
139   if ($error) {
140     # Error occured while preparing args.
141     return $error;
142   } elsif (not defined $args) {
143     # Silently decline.
144     warn "[debug]$me Declining '$action' export";
145     return '';
146   } # else ... queue the export.
147
148   warn "[debug]$me Queueing with args: " . join(', ', @$args) if $DEBUG;
149
150   return(
151     $self->_queue(
152       $svc_broadband->svcnum,
153       $self->_get_cmd_sub($svc_broadband, $router),
154       @$args
155     )
156   );
157
158 }
159
160 sub _prepare_args {
161
162   my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
163   my $old = shift if ($action eq 'replace');
164
165   my $field_prefix = $self->_field_prefix;
166   my $command = $router->getfield("${field_prefix}_cmd_${action}");
167   unless ($command) {
168     warn "[debug]$me router custom field '${field_prefix}_cmd_$action' "
169       . "is not defined." if $DEBUG;
170     return '';
171   }
172
173   {
174     no strict 'vars';
175     no strict 'refs';
176
177     if ($action eq 'replace') {
178       ${"old_$_"} = $old->getfield($_) foreach $old->fields;
179       ${"new_$_"} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
180       $command = eval(qq("$command"));
181     } else {
182       ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
183       $command = eval(qq("$command"));
184     }
185     return $@ if $@;
186   }
187
188   my $args = [
189     'user' => $router->getfield($field_prefix . '_user'),
190     'password' => $router->getfield($field_prefix . '_password'),
191     'host' => $router->getfield($field_prefix . '_address'),
192     'Timeout' => $router->getfield($field_prefix . '_timeout'),
193     'Prompt' => $router->getfield($field_prefix . '_prompt'),
194     'command' => $command,
195   ];
196
197   return('', $args);
198
199 }
200
201 sub _get_cmd_sub {
202
203   my ($self, $svc_broadband, $router) = (shift, shift, shift);
204
205   my $protocol = (
206     $router->getfield($self->_field_prefix . '_protocol') =~ /^(telnet|ssh)$/
207   ) ? $1 : 'telnet';
208
209   return(ref($self)."::".$protocol."_cmd");
210
211 }
212
213 sub _check_router_fields {
214
215   my ($self, $router, $action) = (shift, shift, shift);
216   my @check_fields = $self->_req_router_fields;
217
218   foreach (@check_fields) {
219     if ($router->getfield($_) eq '') {
220       warn "[debug]$me Required field '$_' is unset";
221       return 0;
222     } else {
223       return 1;
224     }
225   }
226
227 }
228
229 sub _queue {
230   #warn join ':', @_;
231   my( $self, $svcnum, $cmd_sub ) = (shift, shift, shift);
232   my $queue = new FS::queue {
233     'svcnum' => $svcnum,
234   };
235   $queue->job($cmd_sub);
236   $queue->insert(@_);
237 }
238
239 sub _get_router {
240   my ($self, $svc_broadband, %args) = (shift, shift, shift, @_);
241
242   my $router;
243   if ($args{'routernum'}) {
244     $router = qsearchs('router', { routernum => $args{'routernum'}});
245   } else {
246     $router = $svc_broadband->addr_block->router;
247   }
248
249   return($router);
250
251 }
252
253
254 # Subroutines
255 sub ssh_cmd {
256   use Net::SSH '0.08';
257   &Net::SSH::ssh_cmd( { @_ } );
258 }
259
260 sub telnet_cmd {
261   eval 'use Net::Telnet;';
262   die $@ if $@;
263
264   warn join(', ', @_);
265
266   my %arg = @_;
267
268   my $t = new Net::Telnet (Timeout => $arg{Timeout},
269                            Prompt  => $arg{Prompt});
270   $t->open($arg{host});
271   $t->login($arg{user}, $arg{password});
272   my @error = $t->cmd($arg{command});
273   die @error if (grep /^ERROR/, @error);
274 }
275
276 1;