RT# 83450 - fixed rateplan export
[freeside.git] / FS / FS / part_export / broadband_snmp.pm
1 package FS::part_export::broadband_snmp;
2
3 use strict;
4 use vars qw(%info $DEBUG);
5 use base 'FS::part_export';
6 use SNMP;
7 use Tie::IxHash;
8
9 $DEBUG = 0;
10
11 my $me = '['.__PACKAGE__.']';
12
13 tie my %snmp_version, 'Tie::IxHash',
14   v1  => '1',
15   v2c => '2c',
16   # v3 unimplemented
17 ;
18
19 #tie my %snmp_type, 'Tie::IxHash',
20 #  i => INTEGER,
21 #  u => UNSIGNED32,
22 #  s => OCTET_STRING,
23 #  n => NULL,
24 #  o => OBJECT_IDENTIFIER,
25 #  t => TIMETICKS,
26 #  a => IPADDRESS,
27 #  # others not implemented yet
28 #;
29
30 tie my %options, 'Tie::IxHash',
31   'version' => { label=>'SNMP version', 
32     type => 'select',
33     options => [ keys %snmp_version ],
34    },
35   'community' => { label=>'Community', default=>'public' },
36
37   'action'  => { multiple=>1 },
38   'oid'     => { multiple=>1 },
39   'value'   => { multiple=>1 },
40   'datatype'=> { multiple=>1 },
41
42   'ip_addr_change_to_new' => { 
43     label=>'Send IP address changes to new address',
44     type=>'checkbox'
45   },
46   'timeout' => { label=>'Timeout (seconds)' },
47 ;
48
49 %info = (
50   'svc'     => 'svc_broadband',
51   'desc'    => 'Send SNMP requests to the service IP address',
52   'config_element' => '/edit/elements/part_export/broadband_snmp.html',
53   'options' => \%options,
54   'no_machine' => 1,
55   'weight'  => 10,
56   'notes'   => <<'END'
57 Send one or more SNMP SET requests to the IP address registered to the service.
58 The value may interpolate fields from svc_broadband, cust_location, or
59 cust_main by prefixing the field name with <b>$</b>. For replace operations,
60 svc_broadband fields may be prefixed with <b>$new_</b> and <b>$old_</b>
61 (e.g. "$old_mac_addr").
62 END
63 );
64
65 sub _export_insert {
66   my $self = shift;
67   $self->export_command('insert', @_);
68 }
69
70 sub _export_delete {
71   my $self = shift;
72   $self->export_command('delete', @_);
73 }
74
75 sub _export_replace {
76   my $self = shift;
77   $self->export_command('replace', @_);
78 }
79
80 sub _export_suspend {
81   my $self = shift;
82   $self->export_command('suspend', @_);
83 }
84
85 sub _export_unsuspend {
86   my $self = shift;
87   $self->export_command('unsuspend', @_);
88 }
89
90 sub export_command {
91   my $self = shift;
92   my ($action, $svc_new, $svc_old) = @_;
93
94   my @a = split("\n", $self->option('action'));
95   my @o = split("\n", $self->option('oid'));
96   my @v = split("\n", $self->option('value'));
97   my @commands;
98   warn "$me parsing $action commands:\n" if $DEBUG;
99   while (@a) {
100     my $oid = shift @o;
101     my $value = shift @v;
102     next unless shift(@a) eq $action; # ignore commands for other actions
103     $value = $self->substitute($value, $svc_new, $svc_old);
104     warn "$me     $oid :=$value\n" if $DEBUG;
105     push @commands, $oid, $value;
106   }
107
108   my $ip_addr = $svc_new->ip_addr;
109   # ip address change: send to old address unless told otherwise
110   if ( defined $svc_old and ! $self->option('ip_addr_change_to_new') ) {
111     $ip_addr = $svc_old->ip_addr;
112   }
113   warn "$me opening session to $ip_addr\n" if $DEBUG;
114
115   my %opt = (
116     DestHost  => $ip_addr,
117     Community => $self->option('community'),
118     Timeout   => ($self->option('timeout') || 20) * 1000,
119   );
120   my $version = $self->option('version');
121   $opt{Version} = $snmp_version{$version} or die 'invalid version';
122   $opt{VarList} = \@commands; # for now
123
124   $self->snmp_queue( $svc_new->svcnum, %opt );
125 }
126
127 sub snmp_queue {
128   my $self = shift;
129   my $svcnum = shift;
130   my $queue = new FS::queue {
131     'svcnum'  => $svcnum,
132     'job'     => 'FS::part_export::broadband_snmp::snmp_request',
133   };
134   $queue->insert(@_);
135 }
136
137 sub snmp_request {
138   my %opt = @_;
139   my $flatvarlist = delete $opt{VarList};
140   my $session = SNMP::Session->new(%opt);
141
142   warn "$me sending SET request\n" if $DEBUG;
143
144   my @varlist;
145   while (@$flatvarlist) {
146     my @this = splice(@$flatvarlist, 0, 2);
147     push @varlist, [ $this[0], 0, $this[1], undef ];
148     # XXX new option to choose the IID (array index) of the object?
149   }
150
151   $session->set(\@varlist);
152   my $error = $session->{ErrorStr};
153
154   if ( $session->{ErrorNum} ) {
155     die "SNMP request failed: $error\n";
156   }
157 }
158
159 sub substitute {
160   # double-quote-ish interpolation of service fields
161   # accepts old_ and new_ for replace actions, like shellcommands
162   my $self = shift;
163   my ($value, $svc_new, $svc_old) = @_;
164
165   my $location = $svc_new->cust_svc->cust_pkg->cust_location;
166   my $cust_main = $location->cust_main;
167
168   foreach my $field ( $svc_new->fields ) {
169     my $new_val = $svc_new->$field;
170     $value =~ s/\$(new_)?$field/$new_val/g;
171     if ( $svc_old ) { # replace only
172       my $old_val = $svc_old->$field;
173       $value =~ s/\$old_$field/$old_val/g;
174     }
175   }
176
177   # we don't yet have export_relocate hooks in here, so there's no old/new
178   # cust_location. do cust_location before cust_main, since cust_main has
179   # a bunch of empty fields with the same names.
180   
181   foreach my $field ( $location->fields ) {
182     my $curr_val = $location->get($field);
183     $value =~ s/\$$field/$curr_val/g;
184   }
185
186   foreach my $field ( $cust_main->fields ) {
187     my $curr_val = $cust_main->get($field);
188     $value =~ s/\$$field/$curr_val/g;
189   }
190
191   $value;
192 }
193
194 sub _upgrade_exporttype {
195   eval 'use FS::Record qw(qsearch qsearchs)';
196   # change from old style with numeric oid, data type flag, and value
197   # on consecutive lines
198   foreach my $export (qsearch('part_export',
199                       { exporttype => 'broadband_snmp' } ))
200   {
201     # for the new options
202     my %new_options = (
203       'action' => [],
204       'oid'    => [],
205       'value'  => [],
206     );
207     foreach my $action (qw(insert replace delete suspend unsuspend)) {
208       my $old_option = qsearchs('part_export_option',
209                       { exportnum   => $export->exportnum,
210                         optionname  => $action.'_command' } );
211       next if !$old_option;
212       my $text = $old_option->optionvalue;
213       my @commands = split("\n", $text);
214       foreach (@commands) {
215         my ($oid, $type, $value) = split /\s/, $_, 3;
216         push @{$new_options{action}}, $action;
217         push @{$new_options{oid}},    $oid;
218         push @{$new_options{value}},   $value;
219       }
220       my $error = $old_option->delete;
221       warn "error migrating ${action}_command option: $error\n" if $error;
222     }
223     foreach (keys(%new_options)) {
224       my $new_option = FS::part_export_option->new({
225           exportnum   => $export->exportnum,
226           optionname  => $_,
227           optionvalue => join("\n", @{ $new_options{$_} })
228       });
229       my $error = $new_option->insert;
230       warn "error inserting '$_' option: $error\n" if $error;
231     }
232   } #foreach $export
233   '';
234 }
235
236 1;