From a472d8ff6bf5c87d7181c4a3f1757bae487a6ddf Mon Sep 17 00:00:00 2001 From: Jonathan Prykop Date: Thu, 24 Mar 2016 21:41:04 -0500 Subject: [PATCH] RT#37912: Service Provisioning Export for ISPConfig 3 --- FS/FS/Schema.pm | 20 +++ FS/FS/cust_svc.pm | 11 ++ FS/FS/export_cust_svc.pm | 134 +++++++++++++++ FS/FS/part_export.pm | 87 ++++++++++ FS/FS/part_export/ispconfig3.pm | 361 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 613 insertions(+) create mode 100644 FS/FS/export_cust_svc.pm create mode 100644 FS/FS/part_export/ispconfig3.pm diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index dadb26d78..bd63af550 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -4453,6 +4453,26 @@ sub tables_hashref { ], }, + 'export_cust_svc' => { + 'columns' => [ + 'exportcustsvcnum', 'serial', '', '', '', '', + 'exportnum', 'int', '', '', '', '', + 'svcnum', 'int', '', '', '', '', + 'remoteid', 'varchar', '', 512, '', '', + ], + 'primary_key' => 'exportcustsvcnum', + 'unique' => [ [ 'exportnum', 'svcnum' ] ], + 'index' => [ [ 'exportnum', 'svcnum' ] ], + 'foreign_keys' => [ + { columns => [ 'exportnum' ], + table => 'part_export', + }, + { columns => [ 'svcnum' ], + table => 'cust_svc', + }, + ], + }, + 'export_device' => { 'columns' => [ 'exportdevicenum' => 'serial', '', '', '', '', diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm index d91fa0d7a..c06b302df 100644 --- a/FS/FS/cust_svc.pm +++ b/FS/FS/cust_svc.pm @@ -169,6 +169,17 @@ sub delete { local $FS::UID::AutoCommit = 0; my $dbh = dbh; + # delete associated export_cust_svc + foreach my $export_cust_svc ( + qsearch('export_cust_svc',{ 'svcnum' => $self->svcnum }) + ) { + my $error = $export_cust_svc->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + my $error = $self->SUPER::delete; if ( $error ) { $dbh->rollback if $oldAutoCommit; diff --git a/FS/FS/export_cust_svc.pm b/FS/FS/export_cust_svc.pm new file mode 100644 index 000000000..7cfdcc6a4 --- /dev/null +++ b/FS/FS/export_cust_svc.pm @@ -0,0 +1,134 @@ +package FS::export_cust_svc; +use base qw(FS::Record); + +use strict; +use FS::Record qw( qsearchs ); + +=head1 NAME + +FS::export_cust_svc - Object methods for export_cust_svc records + +=head1 SYNOPSIS + + use FS::export_cust_svc; + + $record = new FS::export_cust_svc \%hash; + $record = new FS::export_cust_svc { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::export_cust_svc object represents information unique +to a given part_export and cust_svc pair. +FS::export_cust_svc inherits from FS::Record. +The following fields are currently supported: + +=over 4 + +=item exportcustsvcnum - primary key + +=item exportnum - export (see L) + +=item svcnum - service (see L) + +=item remoteid - id for accessing service on export remote system + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new export_cust_svc object. To add the object 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 + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'export_cust_svc'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +sub insert { + my $self = shift; + return "export_cust_svc for exportnum ".$self->exportnum. + " svcnum ".$self->svcnum." already exists" + if qsearchs('export_cust_svc',{ 'exportnum' => $self->exportnum, + 'svcnum' => $self->svcnum }); + $self->SUPER::insert; +} + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=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 + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid export option. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('exportcustsvcnum') + || $self->ut_foreign_key('exportnum', 'part_export', 'exportnum') + || $self->ut_foreign_key('svcnum', 'cust_svc', 'svcnum') + || $self->ut_text('remoteid') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 BUGS + +Possibly. + +=head1 SEE ALSO + +L, L, L + +=cut + +1; + diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm index 182f47608..1b6c55a8b 100644 --- a/FS/FS/part_export.pm +++ b/FS/FS/part_export.pm @@ -10,6 +10,7 @@ use FS::part_svc; use FS::part_export_option; use FS::part_export_machine; use FS::svc_export_machine; +use FS::export_cust_svc; #for export modules, though they should probably just use it themselves use FS::queue; @@ -162,6 +163,17 @@ sub delete { local $FS::UID::AutoCommit = 0; my $dbh = dbh; + # delete associated export_cust_svc + foreach my $export_cust_svc ( + qsearch('export_cust_svc',{ 'exportnum' => $self->exportnum }) + ) { + my $error = $export_cust_svc->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + # clean up export_nas records my $error = $self->process_m2m( 'link_table' => 'export_nas', @@ -637,6 +649,81 @@ sub _export_unsuspend { $self->_export_replace( $svc_x, $old ); } +=item get_remoteid SVC + +Returns the remote id for this export for the given service. + +=cut + +sub get_remoteid { + my ($self, $svc_x) = @_; + + my $export_cust_svc = qsearchs('export_cust_svc',{ + 'exportnum' => $self->exportnum, + 'svcnum' => $svc_x->svcnum + }); + + return $export_cust_svc ? $export_cust_svc->remoteid : ''; +} + +=item set_remoteid SVC VALUE + +Sets the remote id for this export for the given service. +See L. + +If value is true, inserts or updates export_cust_svc record. +If value is false, deletes any existing record. + +Returns error message, blank on success. + +=cut + +sub set_remoteid { + my ($self, $svc_x, $value) = @_; + + my $export_cust_svc = qsearchs('export_cust_svc',{ + 'exportnum' => $self->exportnum, + 'svcnum' => $svc_x->svcnum + }); + + 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 $error = ''; + if ($value) { + if ($export_cust_svc) { + $export_cust_svc->set('remoteid',$value); + $error = $export_cust_svc->replace; + } else { + $export_cust_svc = new FS::export_cust_svc { + 'exportnum' => $self->exportnum, + 'svcnum' => $svc_x->svcnum, + 'remoteid' => $value + }; + $error = $export_cust_svc->insert; + } + } else { + if ($export_cust_svc) { + $error = $export_cust_svc->delete; + } #otherwise, it already doesn't exist + } + + if ($oldAutoCommit) { + $dbh->rollback if $error; + $dbh->commit unless $error; + } + + return $error; +} + =item export_links SVC_OBJECT ARRAYREF Adds a list of web elements to ARRAYREF specific to this export and SVC_OBJECT. diff --git a/FS/FS/part_export/ispconfig3.pm b/FS/FS/part_export/ispconfig3.pm new file mode 100644 index 000000000..2878c51f2 --- /dev/null +++ b/FS/FS/part_export/ispconfig3.pm @@ -0,0 +1,361 @@ +package FS::part_export::ispconfig3; + +use strict; + +use base qw( FS::part_export ); + +use Data::Dumper; +use SOAP::Lite; + +=pod + +=head1 NAME + +FS::part_export::ispconfig3 + +=head1 SYNOPSIS + +ISPConfig 3 integration for Freeside + +=head1 DESCRIPTION + +This export offers basic svc_acct provisioning for ISPConfig 3. +All email accounts will be assigned to a single specified client. + +This module also provides generic methods for working through the L. + +=cut + +use vars qw( %info ); + +my @yesno = ( + options => ['y','n'], + option_labels => { 'y' => 'yes', 'n' => 'no' }, +); + +tie my %options, 'Tie::IxHash', + 'soap_location' => { label => 'SOAP Location' }, + 'username' => { label => 'User Name', + default => '' }, + 'password' => { label => 'Password', + default => '' }, + 'debug' => { type => 'checkbox', + label => 'Enable debug warnings' }, + 'subheading' => { type => 'title', + label => 'Account defaults' }, + 'client_id' => { label => 'Client ID' }, + 'server_id' => { label => 'Server ID' }, + 'maildir' => { label => 'Maildir (substitutions from svc_acct, e.g. /mail/$domain/$username)', }, + 'cc' => { label => 'Cc' }, + 'autoresponder_text' => { label => 'Autoresponder text', + default => 'Out of Office Reply' }, + 'move_junk' => { type => 'select', + options => ['y','n'], + option_labels => { 'y' => 'yes', 'n' => 'no' }, + label => 'Move junk' }, + 'postfix' => { type => 'select', + @yesno, + label => 'Postfix' }, + 'access' => { type => 'select', + @yesno, + label => 'Access' }, + 'disableimap' => { type => 'select', + @yesno, + label => 'Disable IMAP' }, + 'disablepop3' => { type => 'select', + @yesno, + label => 'Disable POP3' }, + 'disabledeliver' => { type => 'select', + @yesno, + label => 'Disable deliver' }, + 'disablesmtp' => { type => 'select', + @yesno, + label => 'Disable SMTP' }, +; + +%info = ( + 'svc' => 'svc_acct', + 'desc' => 'Export email account to ISPConfig 3', + 'options' => \%options, + 'no_machine' => 1, + 'notes' => <<'END', +All email accounts will be assigned to a single specified client and server. +END +); + +sub _mail_user_params { + my ($self, $svc_acct) = @_; + # all available api fields are in comments below, even if we don't use them + return { + #server_id (int(11)) + 'server_id' => $self->option('server_id'), + #email (varchar(255)) + 'email' => $svc_acct->username.'@'.$svc_acct->domain, + #login (varchar(255)) + 'login' => $svc_acct->username.'@'.$svc_acct->domain, + #password (varchar(255)) + 'password' => $svc_acct->_password, + #name (varchar(255)) + 'name' => $svc_acct->finger, + #uid (int(11)) + 'uid' => $svc_acct->uid, + #gid (int(11)) + 'gid' => $svc_acct->gid, + #maildir (varchar(255)) + 'maildir' => $self->_substitute($self->option('maildir'),$svc_acct), + #quota (bigint(20)) + 'quota' => $svc_acct->quota, + #cc (varchar(255)) + 'cc' => $self->option('cc'), + #homedir (varchar(255)) + 'homedir' => $svc_acct->dir, + + ## initializing with autoresponder off, but this could become an export option... + #autoresponder (enum('n','y')) + 'autoresponder' => 'n', + #autoresponder_start_date (datetime) + #autoresponder_end_date (datetime) + #autoresponder_text (mediumtext) + 'autoresponder_text' => $self->option('autoresponder_text'), + + #move_junk (enum('n','y')) + 'move_junk' => $self->option('move_junk'), + #postfix (enum('n','y')) + 'postfix' => $self->option('postfix'), + #access (enum('n','y')) + 'access' => $self->option('access'), + + ## not needed right now, not sure what it is + #custom_mailfilter (mediumtext) + + #disableimap (enum('n','y')) + 'disableimap' => $self->option('disableimap'), + #disablepop3 (enum('n','y')) + 'disablepop3' => $self->option('disablepop3'), + #disabledeliver (enum('n','y')) + 'disabledeliver' => $self->option('disabledeliver'), + #disablesmtp (enum('n','y')) + 'disablesmtp' => $self->option('disablesmtp'), + }; +} + +sub _export_insert { + my ($self, $svc_acct) = @_; + my $params = $self->_mail_user_params($svc_acct); + $self->api_login; + my $remoteid = $self->api_call('mail_user_add',$self->option('client_id'),$params); + return $self->api_error_logout if $self->api_error; + my $error = $self->set_remoteid($svc_acct,$remoteid); + $error = "Remote system updated, but error setting remoteid ($remoteid): $error" + if $error; + $self->api_logout; + $error ||= "Systems updated, but error logging out: ".$self->api_error + if $self->api_error; + return $error; +} + +sub _export_replace { + my ($self, $svc_acct, $svc_acct_old) = @_; + my $remoteid = $self->get_remoteid($svc_acct_old); + return "Could not load remoteid for old service" unless $remoteid; + my $params = $self->_mail_user_params($svc_acct); + #API docs claim "Returns the number of affected rows" + my $success = $self->api_call('mail_user_update',$self->option('client_id'),$remoteid,$params); + return $self->api_error_logout if $self->api_error; + return "Server returned no rows updated, but no other error message" unless $success; + my $error = ''; + unless ($svc_acct->svcnum eq $svc_acct_old->svcnum) { # are these ever not equal? + $error = $self->set_remoteid($svc_acct,$remoteid); + $error = "Remote system updated, but error setting remoteid ($remoteid): $error" + if $error; + } + $self->api_logout; + $error ||= "Systems updated, but error logging out: ".$self->api_error + if $self->api_error; + return $error; +} + +sub _export_delete { + my ($self, $svc_acct) = @_; + my $remoteid = $self->get_remoteid($svc_acct); + return "Could not load remoteid for old service" unless $remoteid; + #API docs claim "Returns the number of deleted records" + my $success = $self->api_call('mail_user_delete',$remoteid); + return $self->api_error_logout if $self->api_error; + my $error = $success ? '' : "Server returned no records deleted"; + $self->api_logout; + $error ||= "Systems updated, but error logging out: ".$self->api_error + if $self->api_error; + return $error; +} + +sub _export_suspend { + my ($self, $svc_acct) = @_; + return ''; +} + +sub _export_unsuspend { + my ($self, $svc_acct) = @_; + return ''; +} + +=head1 ISPConfig3 API + +These methods allow access to the ISPConfig3 API using the credentials +set in the export options. + +=cut + +=head2 api_call + +Accepts I<$method> and I<@params>. Places an api call to the specified +method with the specified params. Returns the result of that call +(empty on failure.) Retrieve error messages using L. + +Do not include session id in list of params; it will be included +automatically. Must run L first. + +=cut + +sub api_call { + my ($self,$method,@params) = @_; + $self->{'__ispconfig_response'} = undef; + # This does get used by api_login, + # to retrieve the session id after it sets the client, + # so we only check for existence of client, + # and we only include session id if we have one + my $client = $self->{'__ispconfig_client'}; + unless ($client) { + $self->{'__ispconfig_error'} = 'Not logged in'; + return; + } + if ($self->{'__ispconfig_session'}) { + unshift(@params,$self->{'__ispconfig_session'}); + } + # Contact server in eval, to trap connection errors + warn "Calling SOAP method $method with params:\n".Dumper(\@params)."\n" + if $self->option('debug'); + my $response = eval { $client->$method(@params) }; + unless ($response) { + $self->{'__ispconfig_error'} = "Error contacting server: $@"; + return; + } + # Set results and return + $self->{'__ispconfig_error'} = $response->fault + ? "Error from server: " . $response->faultstring + : ''; + $self->{'__ispconfig_response'} = $response; + return $response->result; +} + +=head2 api_error + +Returns the error string set by L methods, +or a blank string if most recent call produced no errors. + +=cut + +sub api_error { + my $self = shift; + return $self->{'__ispconfig_error'} || ''; +} + +=head2 api_error_logout + +Attempts L, but returns L message from +before logout was attempted. Useful for logging out +properly after an error. + +=cut + +sub api_error_logout { + my $self = shift; + my $error = $self->api_error; + $self->api_logout; + return $error; +} + +=head2 api_login + +Initializes an api session using the credentials for this export. +Returns true on success, false on failure. +Retrieve error messages using L. + +=cut + +sub api_login { + my $self = shift; + if ($self->{'__ispconfig_session'} || $self->{'__ispconfig_client'}) { + $self->{'__ispconfig_error'} = 'Already logged in'; + return; + } + $self->{'__ispconfig_session'} = undef; + $self->{'__ispconfig_client'} = + SOAP::Lite->proxy($self->option('soap_location'), ssl_opts => [ verify_hostname => 0 ] ) + || undef; + unless ($self->{'__ispconfig_client'}) { + $self->{'__ispconfig_error'} = 'Error creating SOAP client'; + return; + } + $self->{'__ispconfig_session'} = + $self->api_call('login',$self->option('username'),$self->option('password')) + || undef; + return unless $self->{'__ispconfig_session'}; + return 1; +} + +=head2 api_logout + +Ends the current api session established by L. +Returns true on success, false on failure. + +=cut + +sub api_logout { + my $self = shift; + unless ($self->{'__ispconfig_session'}) { + $self->{'__ispconfig_error'} = 'Not logged in'; + return; + } + my $result = $self->api_call('logout'); + # clear these even if there was a failure to logout + $self->{'__ispconfig_client'} = undef; + $self->{'__ispconfig_session'} = undef; + return if $self->api_error; + return 1; +} + +# false laziness with portaone export +sub _substitute { + my ($self, $string, @objects) = @_; + return '' unless $string; + foreach my $object (@objects) { + next unless $object; + my @fields = $object->fields; + push(@fields,'domain') if $object->table eq 'svc_acct'; + foreach my $field (@fields) { + next unless $field; + my $value = $object->$field; + $string =~ s/\$$field/$value/g; + } + } + # strip leading/trailing whitespace + $string =~ s/^\s//g; + $string =~ s/\s$//g; + return $string; +} + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +Jonathan Prykop +jonathan@freeside.biz + +=cut + +1; + + -- 2.11.0