From: jeff Date: Mon, 8 Feb 2010 15:37:29 +0000 (+0000) Subject: grandstream device configuration support #4220 X-Git-Tag: root_of_svc_elec_features~483 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=d0221fabd4656b3a04251ca6168cc45f54d23574 grandstream device configuration support #4220 --- diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index f20ea647a..eb26dde5d 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -174,6 +174,7 @@ if ( -e $addl_handler_use_file ) { use FS::part_export; use FS::part_export_option; use FS::export_svc; + use FS::export_device; use FS::msgcat; use FS::rate; use FS::rate_region; diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 19c2e8e58..fdb4a94e6 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -1851,6 +1851,17 @@ sub tables_hashref { 'index' => [ [ 'exportnum' ], [ 'svcpart' ] ], }, + 'export_device' => { + 'columns' => [ + 'exportdevicenum' => 'serial', '', '', '', '', + 'exportnum' => 'int', '', '', '', '', + 'devicepart' => 'int', '', '', '', '', + ], + 'primary_key' => 'exportdevicenum', + 'unique' => [ [ 'exportnum', 'devicepart' ] ], + 'index' => [ [ 'exportnum' ], [ 'devicepart' ] ], + }, + 'part_export' => { 'columns' => [ 'exportnum', 'serial', '', '', '', '', diff --git a/FS/FS/export_device.pm b/FS/FS/export_device.pm new file mode 100644 index 000000000..69e382649 --- /dev/null +++ b/FS/FS/export_device.pm @@ -0,0 +1,136 @@ +package FS::export_device; + +use strict; +use base qw( FS::Record ); +use FS::Record qw( qsearch qsearchs dbh ); +use FS::part_export; +use FS::part_device; + +=head1 NAME + +FS::export_device - Object methods for export_device records + +=head1 SYNOPSIS + + use FS::export_device; + + $record = new FS::export_device \%hash; + $record = new FS::export_device { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::export_device object links a device definition (see L) +to an export (see L). FS::export_device inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item exportdevicenum - primary key + +=item exportnum - export (see L) + +=item devicepart - device definition (see L) + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new record. To add the record to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I method. + +=cut + +sub table { 'export_device'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# may want to check for duplicates against either services or devices +# cf FS::export_svc + +=item delete + +Delete this record from the database. + +=cut + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +=item check + +Checks all fields to make sure this is a valid record. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +sub check { + my $self = shift; + + $self->ut_numbern('exportdevicenum') + || $self->ut_number('exportnum') + || $self->ut_foreign_key('exportnum', 'part_export', 'exportnum') + || $self->ut_number('devicepart') + || $self->ut_foreign_key('devicepart', 'part_device', 'devicepart') + || $self->SUPER::check + ; +} + +=item part_export + +Returns the FS::part_export object (see L). + +=cut + +sub part_export { + my $self = shift; + qsearchs( 'part_export', { 'exportnum' => $self->exportnum } ); +} + +=item part_device + +Returns the FS::part_device object (see L). + +=cut + +sub part_device { + my $self = shift; + qsearchs( 'part_device', { 'svcpart' => $self->devicepart } ); +} + +=back + +=head1 BUGS + +=head1 SEE ALSO + +L, L, L, schema.html from the base +documentation. + +=cut + +1; + diff --git a/FS/FS/part_device.pm b/FS/FS/part_device.pm index 79a534ae7..49635841e 100644 --- a/FS/FS/part_device.pm +++ b/FS/FS/part_device.pm @@ -1,8 +1,10 @@ package FS::part_device; use strict; -use base qw( FS::Record ); -use FS::Record; # qw( qsearch qsearchs ); +use base qw( FS::Record FS::m2m_Common ); +use FS::Record qw( qsearch qsearchs ); +use FS::part_export; +use FS::export_device; =head1 NAME @@ -107,6 +109,18 @@ sub check { $self->SUPER::check; } +=item part_export + +Returns a list of all exports (see L) for this device. + +=cut + +sub part_export { + my $self = shift; + map { qsearchs( 'part_export', { 'exportnum' => $_->exportnum } ) } + qsearch( 'export_device', { 'devicepart' => $self->devicepart } ); +} + sub process_batch_import { my $job = shift; diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm index 16aad6dcd..d533db88b 100644 --- a/FS/FS/part_export.pm +++ b/FS/FS/part_export.pm @@ -226,6 +226,17 @@ sub export_svc { qsearch('export_svc', { 'exportnum' => $self->exportnum } ); } +=item export_device + +Returns a list of associated FS::export_device records. + +=cut + +sub export_device { + my $self = shift; + qsearch('export_device', { 'exportnum' => $self->exportnum } ); +} + =item part_export_option Returns all options as FS::part_export_option objects (see diff --git a/FS/FS/part_export/grandstream.pm b/FS/FS/part_export/grandstream.pm new file mode 100644 index 000000000..162fb8c1d --- /dev/null +++ b/FS/FS/part_export/grandstream.pm @@ -0,0 +1,242 @@ +package FS::part_export::grandstream; + +use vars qw(@ISA $me %info $GAPSLITE_HOME $JAVA_HOME); +use URI; +use MIME::Base64; +use Tie::IxHash; +use FS::part_export; +use FS::CGI qw(rooturl); + +@ISA = qw(FS::part_export); +$me = '[' . __PACKAGE__ . ']'; +$GAPSLITE_HOME = '/usr/local/src/GS/GS_CFG_GEN/'; +$JAVA_HOME = '/usr/lib/jvm/java-6-sun/'; +$JAVA_HOME = '/usr/lib/jvm/java-1.4.2-gcj-4.1-1.4.2.0/'; + +tie my %options, 'Tie::IxHash', + 'upload' => { label=>'Enable upload to tftpserver', + type=>'checkbox', + }, + 'user' => { label=>'User name for ssh to tftp server' }, + 'tftproot' => { label=>'Directory in which to upload configuration' }, + 'java_home' => { label=>'Path to java to be used', + default=>$JAVA_HOME, + }, + 'gapslite_home' => { label=>'Path to grandstream configuration tool', + default=>$GAPSLITE_HOME, + }, + 'template' => { label=>'Configuration template', + type=>'textarea', + notes=>'Type or paste the configuration template here', + }, +; + +%info = ( + 'svc' => [ qw( part_device ) ], # svc_phone + 'desc' => 'Provision phone numbers to Grandstream Networks phones', + 'options' => \%options, + 'notes' => '', +); + +sub rebless { shift; } + +sub gs_create_config { + my($self, $mac, %opt) = (@_); + + eval "use Net::SCP;"; + die $@ if $@; + + warn "gs_create_config called with mac of $mac\n"; + $mac = sprintf('%012s', lc($mac)); + my $dir = '%%%FREESIDE_CONF%%%/cache.'. $FS::UID::datasrc; + + my $fh = new File::Temp( + TEMPLATE => "grandstream.$mac.XXXXXXXX", + DIR => $dir, + UNLINK => 0, + ); + + my $filename = $fh->filename; + + #my $template = new Text::Template ( + # TYPE => 'ARRAY', + # SOURCE => $self->option('template'), + # DELIMITERS => $delimiters, + # OUTPUT => $fh, + #); + + #$template->compile or die "Can't compile template: $Text::Template::ERROR\n"; + + #my $config = $template->fill_in( HASH => { mac_addr => $mac } ); + + print $fh $self->option('template') or die "print failed: $!"; + close $fh; + + system( "export GAPSLITE_HOME=$GAPSLITE_HOME; export JAVA_HOME=$JAVA_HOME; ". + "cd $dir; $GAPSLITE_HOME/bin/encode.sh $mac $filename $dir/cfg$mac" + ) == 0 + or die "grandstream encode failed: $!"; + + unlink $filename; + + open my $encoded, "$dir/cfg$mac" or die "open cfg$mac failed: $!"; + + my $content; + + if ($opt{upload}) { + if ($self->option('upload')) { + my $scp = new Net::SCP ( { + 'host' => $self->machine, + 'user' => $self->option('user'), + 'cwd' => $self->option('tftproot'), + } ); + + $scp->put( "$dir/cfg$mac" ) or die "upload failed: ". $scp->errstr; + } + } else { + local $/; + $content = <$encoded>; + } + + close $encoded; + unlink "$dir/cfg$mac"; + + $content; +} + +sub gs_create { + my($self, $mac) = (shift, shift); + + return unless $mac; # be more alarmed? Or check upstream? + + $self->gs_create_config($mac, 'upload' => 1); + ''; +} + +sub gs_delete { + my($self, $mac) = (shift, shift); + + $mac = sprintf('%012s', lc($mac)); + + ssh_cmd( user => $self->option('user'), + host => $self->machine, + command => 'rm', + args => [ '-f', $self->option(tftproot). "/cfg$mac" ], + ); + ''; + +} + +sub ssh_cmd { #subroutine, not method + use Net::SSH '0.08'; + &Net::SSH::ssh_cmd( { @_ } ); +} + +sub _export_insert { +# my( $self, $svc_phone ) = (shift, shift); +# $self->gs_create($svc_phone->mac_addr); + ''; +} + +sub _export_replace { +# my( $self, $new_svc, $old_svc ) = (shift, shift, shift); +# $self->gs_delete($old_svc->mac_addr); +# $self->gs_create($new_svc->mac_addr); + ''; +} + +sub _export_delete { +# my( $self, $svc_phone ) = (shift, shift); +# $self->gs_delete($svc_phone->mac_addr); + ''; +} + +sub _export_suspend { + ''; +} + +sub _export_unsuspend { + ''; +} + +sub export_device_insert { + my( $self, $svc_phone, $phone_device ) = (shift, shift, shift); + $self->gs_create($phone_device->mac_addr); + ''; +} + +sub export_device_delete { + my( $self, $svc_phone, $phone_device ) = (shift, shift, shift); + $self->gs_delete($phone_device->mac_addr); + ''; +} + +sub export_device_config { + my( $self, $svc_phone, $phone_device ) = (shift, shift, shift); + + my $mac; +# if ($phone_device) { + $mac = $phone_device->mac_addr; +# } else { +# $mac = $svc_phone->mac_addr; +# } + + return '' unless $mac; # be more alarmed? Or check upstream? + + $self->gs_create_config($mac); +} + + +sub export_device_replace { + my( $self, $svc_phone, $new_svc_or_device, $old_svc_or_device ) = + (shift, shift, shift, shift); + + $self->gs_delete($old_svc_or_device->mac_addr); + $self->gs_create($new_svc_or_device->mac_addr); + ''; +} + +# bad overloading? +sub export_links { + my($self, $svc_phone, $arrayref) = (shift, shift, shift); + + return; # remove if we actually support being an export for svc_phone; + + my @deviceparts = map { $_->devicepart } $self->export_device; + my @devices = grep { my $part = $_->devicepart; + scalar( grep { $_ == $part } @deviceparts ); + } $svc_phone->phone_device; + + my $export = $self->exportnum; + my $fsurl = rooturl(); + if (@devices) { + foreach my $device ( @devices ) { + next unless $device->mac_addr; + my $num = $device->devicenum; + push @$arrayref, + qq!!. + qq! Phone config !; + } + } elsif ($svc_phone->mac_addr) { + my $num = $svc_phone->svcnum; + push @$arrayref, + qq!!. + qq! Phone config !; + } #else + ''; +} + +sub export_device_links { + my($self, $svc_phone, $device, $arrayref) = (shift, shift, shift, shift); + + return unless $device && $device->mac_addr; + my $export = $self->exportnum; + my $fsurl = rooturl(); + my $num = $device->devicenum; + push @$arrayref, + qq!!. + qq! Phone config !; + ''; +} + +1; diff --git a/FS/FS/part_export/netsapiens.pm b/FS/FS/part_export/netsapiens.pm index 332edccc0..b8068940b 100644 --- a/FS/FS/part_export/netsapiens.pm +++ b/FS/FS/part_export/netsapiens.pm @@ -21,7 +21,7 @@ tie my %options, 'Tie::IxHash', ; %info = ( - 'svc' => 'svc_phone', + 'svc' => [ 'svc_phone', ], # 'part_device', 'desc' => 'Provision phone numbers to NetSapiens', 'options' => \%options, 'notes' => <<'END' diff --git a/FS/FS/phone_device.pm b/FS/FS/phone_device.pm index 914f735b6..ba765e026 100644 --- a/FS/FS/phone_device.pm +++ b/FS/FS/phone_device.pm @@ -97,7 +97,7 @@ sub insert { return $error; } - $self->svc_phone->export('device_insert', $self); #call device export + $self->export('device_insert'); $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -124,7 +124,7 @@ sub delete { local $FS::UID::AutoCommit = 0; my $dbh = dbh; - $self->svc_phone->export('device_delete', $self); #call device export + $self->export('device_delete'); my $error = $self->SUPER::delete; if ( $error ) { @@ -167,7 +167,7 @@ sub replace { return $error; } - $new->svc_phone->export('device_replace', $new, $old); #call device export + $new->export('device_replace', $old); $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -227,6 +227,64 @@ sub svc_phone { qsearchs( 'svc_phone', { 'svcnum' => $self->svcnum } ); } +=item export HOOK [ EXPORT_ARGS ] + +Runs the provided export hook (i.e. "device_insert") for this service. + +=cut + +sub export { + my( $self, $method ) = ( shift, shift ); + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + my $svc_phone = $self->svc_phone; + my $error = $svc_phone->export($method, $self, @_); #call device export + if ( $error ) { #netsapiens at least + $dbh->rollback if $oldAutoCommit; + return "error exporting $method event to svc_phone ". $svc_phone->svcnum. + " (transaction rolled back): $error"; + } + + $method = "export_$method" unless $method =~ /^export_/; + + foreach my $part_export ( $self->part_device->part_export ) { + next unless $part_export->can($method); + my $error = $part_export->$method($svc_phone, $self, @_); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error exporting $method event to ". $part_export->exporttype. + " (transaction rolled back): $error"; + } + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + ''; + +} + +=item export_links + +Returns a list of html elements associated with this device's exports. + +=cut + +sub export_links { + my $self = shift; + my $return = []; + $self->export('export_device_links', $return); + $return; +} + =back =head1 BUGS diff --git a/FS/MANIFEST b/FS/MANIFEST index 71523458a..f3d2a6909 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -87,6 +87,7 @@ FS/h_svc_www.pm FS/part_bill_event.pm FS/payinfo_Mixin.pm FS/export_svc.pm +FS/export_device.pm FS/part_export.pm FS/part_export_option.pm FS/part_export/acct_sql.pm @@ -231,6 +232,7 @@ t/domain_record.t t/nas.t t/part_bill_event.t t/export_svc.t +t/export_device.t t/part_export.t t/part_export_option.t t/part_export-acct_sql.t diff --git a/FS/t/export_device.t b/FS/t/export_device.t new file mode 100644 index 000000000..4688326a7 --- /dev/null +++ b/FS/t/export_device.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::export_device; +$loaded=1; +print "ok 1\n"; diff --git a/Makefile b/Makefile index 2f696dc21..8bfc56aa0 100644 --- a/Makefile +++ b/Makefile @@ -204,6 +204,7 @@ perl-modules: s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\ " blib/lib/FS/Cron/*.pm;\ perl -p -i -e "\ + s|%%%FREESIDE_CONF%%%|${FREESIDE_CONF}|g;\ s|%%%FREESIDE_EXPORT%%%|${FREESIDE_EXPORT}|g;\ s|%%%FREESIDE_LOG%%%|${FREESIDE_LOG}|g;\ " blib/lib/FS/part_export/*.pm;\ diff --git a/httemplate/edit/part_device.html b/httemplate/edit/part_device.html index 4f2fe93b4..aa626b379 100644 --- a/httemplate/edit/part_device.html +++ b/httemplate/edit/part_device.html @@ -6,6 +6,7 @@ 'devicename' => 'Device name', }, 'viewall_dir' => 'browse', + 'html_bottom' => $html_bottom_sub, ) %> <%init> @@ -13,4 +14,33 @@ die "access denied" unless $FS::CurrentUser::CurrentUser->access_right('Configuration'); +my $extra_sql = + join( ' OR ', map { "exporttype = '$_'" } + keys %{FS::part_export::export_info('part_device')} + ); +$extra_sql = $extra_sql ? " WHERE ( $extra_sql ) " : " WHERE 0 = 1 "; + +my $html_bottom_sub = sub { + my $part_device = shift; + + '
'. + 'Exports
'. + + ''. + '
'. + include( '/elements/checkboxes-table.html', + 'source_obj' => $part_device, + 'link_table' => 'export_device', + 'target_table' => 'part_export', + 'extra_sql' => $extra_sql, + 'name_callback' => sub { my $o = shift; + $o->exporttype. ' to '. $o->machine; + }, + ). + '
'. + '
'; + +}; + + diff --git a/httemplate/edit/process/part_device.html b/httemplate/edit/process/part_device.html index 2b7e1da49..399991fc8 100644 --- a/httemplate/edit/process/part_device.html +++ b/httemplate/edit/process/part_device.html @@ -1,6 +1,10 @@ <% include( 'elements/process.html', 'table' => 'part_device', 'viewall_dir' => 'browse', + 'process_m2m' => { + 'link_table' => 'export_device', + 'target_table' => 'part_export', + }, ) %> <%init> diff --git a/httemplate/elements/checkboxes-table.html b/httemplate/elements/checkboxes-table.html index b6b04d111..a31bdb919 100644 --- a/httemplate/elements/checkboxes-table.html +++ b/httemplate/elements/checkboxes-table.html @@ -46,7 +46,7 @@ % % my $hashref = $opt{'hashref'} || {}; % -% my $extra_sql = ''; +% my $extra_sql = $opt{'extra_sql'} || ''; % % if ( $opt{'agent_virt'} ) { % $extra_sql .= ' AND' . $FS::CurrentUser::CurrentUser->agentnums_sql( diff --git a/httemplate/misc/phone_device_config.html b/httemplate/misc/phone_device_config.html new file mode 100644 index 000000000..9ea0d0d1c --- /dev/null +++ b/httemplate/misc/phone_device_config.html @@ -0,0 +1,57 @@ +%if ($config) { +<% $config %> +%}else{ +<% include("/elements/errorpage.html", "No configuration data produced.") %> +%} +<%init> + +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('View customer services'); + +my $exportnum; +if ( $cgi->param('exportnum') ) { + $cgi->param('exportnum') =~ /^(\d+)$/ or die "unparsable exportnum"; + $exportnum = $1; +} + +die "no export provided" + unless $exportnum; + +my $svcnum; +if ( $cgi->param('svcnum') ) { + $cgi->param('svcnum') =~ /^(\d+)$/ or die "unparsable svcnum"; + $svcnum = $1; +} + +my $devicenum; +if ( $cgi->param('devicenum') ) { + $cgi->param('devicenum') =~ /^(\d+)$/ or die "unparsable devicenum"; + $devicenum = $1; +} + +die "no device or service provided" + unless $svcnum || $devicenum; + +my $part_export = qsearchs('part_export', { 'exportnum' => $exportnum }) + or die "Unknown exportnum $exportnum\n"; + +my $phone_device; +my $svc_phone; +if ($devicenum) { + $phone_device = qsearchs('phone_device', { 'devicenum' => $devicenum }) + or die "Unknown device $devicenum\n"; + $svc_phone = $phone_device->svc_phone; +} else { + $svc_phone = qsearchs('svc_phone', { 'svcnum' => $svcnum }) + or die "Unknown svc_phone $svcnum\n"; +} + +my $config = $part_export->export_device_config($svc_phone, $phone_device); + +if ($config) { + http_header('Content-Type' => 'application/octet-stream'); + http_header('Content-Disposition' => 'attachment;filename="config"'); + http_header('Content-Length' => length($config)); +} + + diff --git a/httemplate/view/svc_phone.cgi b/httemplate/view/svc_phone.cgi index 59ee2d516..2733e258e 100644 --- a/httemplate/view/svc_phone.cgi +++ b/httemplate/view/svc_phone.cgi @@ -58,6 +58,7 @@ my $html_foot = sub { 'Type'. 'MAC Addr'. ''. + ''. ''; my $bgcolor1 = '#eeeeee'; my $bgcolor2 = '#ffffff'; @@ -73,10 +74,12 @@ my $html_foot = sub { my $td = qq(); my $devicenum = $phone_device->devicenum; + my $export_links = join( '
', @{ $phone_device->export_links } ); $devices .= ''. $td. $phone_device->part_device->devicename. ''. $td. $phone_device->mac_addr. ''. + $td. $export_links. ''. "$td( ". qq(edit | ). qq(delete).