diff --git a/Build.PL b/Build.PL index 2386e5bf..cb4b5cdf 100644 --- a/Build.PL +++ b/Build.PL @@ -5,11 +5,16 @@ my $builder = Module::Build->new( dist_author => 'Lars Dieckow ', dist_version_from => 'lib/NGCP/Schema.pm', requires => { + 'aliased' => 0, 'DBIx::Class::Schema::Loader' => 0, 'File::Path' => 0, + 'MooseX::FileAttribute' => 0, 'MooseX::NonMoose' => 0, 'NGCP' => 0, 'Quantum::Superpositions' => 0, + 'Regexp::Common' => 0, + 'Regexp::IPv6' => 0, + 'Throwable::Error' => 0, }, add_to_cleanup => ['NGCP-Schema-*'], ); diff --git a/lib/NGCP/Schema.pm b/lib/NGCP/Schema.pm index 9ba93de2..c610016d 100644 --- a/lib/NGCP/Schema.pm +++ b/lib/NGCP/Schema.pm @@ -1,7 +1,76 @@ package NGCP::Schema; use Sipwise::Base; +use aliased 'NGCP::Schema::Exception'; +use NGCP::Schema::Config qw(); +use Regexp::Common qw(net); +use Regexp::IPv6 qw($IPv6_re); + our $VERSION = '1.000'; +has('config', is => 'ro', lazy => 1, default => sub { NGCP::Schema::Config->new->config }); + +method validate($data, $mandatory_params, $optional_params?) { + Exception->throw({ + description => 'Client.Syntax.MissingParam', + message => q(missing parameter 'data'), + }) unless defined $data; + Exception->throw({ + description => 'Client.Syntax.MalformedParam', + message => q(parameter 'data' should be an object/hash, but is '%s')->sprintf(ref $data) + }) unless defined eval { %$data }; + my %check_data = %$data; + for my $param (@$mandatory_params) { + Exception->throw({ + description => 'Client.Syntax.MissingParam', + message => "missing parameter '$param' in request" + }) unless exists $check_data{$param}; + delete $check_data{$param}; + } + foreach my $key (keys %check_data) { + next if grep { $key eq $_ } @$optional_params; + Exception->throw({ + description => 'Client.Syntax.UnknownParam', + message => "unknown parameter '$key'", + }); + } + return; +} + +method check_domain($data) { + $self->validate($data, ['domain']); + my $domain = $data->{domain}; + return 1 if $domain =~ /^(?:[a-z0-9]+(?:-[a-z0-9]+)*\.)+[a-z]+$/i; + return 1 if $self->config->{allow_ip_as_domain} + and ($self->check_ip4({ip4 => $domain}) || $self->check_ip6_brackets({ip6brackets => $domain})); + return; +} + +method check_ip4($data) { + $self->validate($data, ['ip4']); + return $self->_check_ip_generic($data->{ip4}, 1); +} + +method check_ip6($data) { + $self->validate($data, ['ip6']); + return $self->_check_ip_generic($data->{ip6}, 2); +} + +method check_ip6_brackets($data) { + $self->validate($data, ['ip6brackets']); + return $self->_check_ip_generic($data->{ip6brackets}, 4); +} + +method _check_ip_generic($ip, $flags) { + for ($flags) { + when ($_ & 1) {return $ip =~ /^$RE{net}{IPv4}$/;} + when ($_ & 2) {return $ip =~ /^$IPv6_re$/;} + when ($_ & 4) {return $ip =~ /^\[$IPv6_re\]$/;} + } + return; +} + +$CLASS->meta->make_immutable; + __END__ =encoding UTF-8 diff --git a/lib/NGCP/Schema/Config.pm b/lib/NGCP/Schema/Config.pm new file mode 100644 index 00000000..ceb251c2 --- /dev/null +++ b/lib/NGCP/Schema/Config.pm @@ -0,0 +1,122 @@ +package NGCP::Schema::Config; +use Sipwise::Base; +use Log::Log4perl qw(); +use MooseX::FileAttribute qw(has_file); +use XML::Simple qw(); + +our $VERSION = '1.000'; + +has_file('config_file', is => 'rw', required => 1, default => '/etc/ngcp-ossbss/provisioning.conf'); +has('config', isa => 'HashRef', is => 'rw', lazy => 1, default => method { + return $self->check_config(XML::Simple->new->XMLin($self->config_file->stringify, ForceArray => 0)); +}); + +method BUILD { + die q(can't find config file %s)->sprintf($self->config_file) + unless -e $self->config_file; + Log::Log4perl->init_once($self->config->{logconf}); + Log::Log4perl->get_logger($self)->info('using config file "%s"'->sprintf($self->config_file)); +} + +method check_config($config) { + $config->{vsc}{actions} = [$config->{vsc}{actions}] + if defined $config->{vsc} + and defined $config->{vsc}{actions} + and not defined eval {@{$config->{vsc}{actions}}}; + $config->{credit_warnings} = [$config->{credit_warnings}] + if defined $config->{credit_warnings} and not defined eval {@{$config->{credit_warnings}}}; + foreach my $warning (eval {@{$config->{credit_warnings}}}) { + $warning->{recipients} = [$warning->{recipients}] + if defined $warning->{recipients} and not defined eval {@{$warning->{recipients}}}; + } + $config->{reserved_usernames} = [$config->{reserved_usernames}] + if defined $config->{reserved_usernames} and not defined eval {@{$config->{reserved_usernames}}}; + $config->{backends_enabled} = [$config->{backends_enabled}] + if defined $config->{backends_enabled} and not defined eval {@{$config->{backends_enabled}}}; + $config->{carrier_prov}{backends} = [$config->{carrier_prov}{backends}] + if defined $config->{carrier_prov} + and defined $config->{carrier_prov}{backends} + and not defined eval {@{$config->{carrier_prov}{backends}}}; + return $config; +} + +$CLASS->meta->make_immutable; + +__END__ + +=encoding UTF-8 + +=head1 NAME + +NGCP::Schema::Config - configuration class + +=head1 VERSION + +This document describes NGCP::Schema::Config version 1.000 + +=head1 SYNOPSIS + + use NGCP::Schema::Config qw(); + my $c_hashref = NGCP::Schema::Config->new; + +=head1 DESCRIPTION + +Reads a configuration file, initialises the logger, provides configuration as structured data. + +=head1 INTERFACE + +=head2 Attributes + +=head3 C + +Type C, B attribute, location of the configuration file. Default is +C. + +=head3 C + +Type C, supplies configuration from the L. + +=head2 Methods + +=head3 C + +Takes hashref as just parsed by the configuration file reader and makes sure it is formatted correctly. At the moment, +it only ensures that arrays are arrays even if there is only one member. + +=head1 DIAGNOSTICS + +=over + +=item C + +The configuration file does not exist. + +=back + +=head1 CONFIGURATION AND ENVIRONMENT + +See L. + +The meaning of the configuration items are detailed in the manual. + +=head1 DEPENDENCIES + +See meta file in the source distribution. + +=head1 INCOMPATIBILITIES + +None reported. + +=head1 BUGS AND LIMITATIONS + +L + +No known limitations. + +=head1 AUTHOR + +Lars Dieckow C<< >> + +=head1 LICENCE + +restricted diff --git a/lib/NGCP/Schema/Exception.pm b/lib/NGCP/Schema/Exception.pm new file mode 100644 index 00000000..5d70002a --- /dev/null +++ b/lib/NGCP/Schema/Exception.pm @@ -0,0 +1,96 @@ +package NGCP::Schema::Exception; +use Sipwise::Base; +use namespace::sweep; + +our $VERSION = '1.000'; + +extends 'Throwable::Error'; +has('description', is => 'ro', isa => 'Str', required => 1); +has('context', is => 'ro', isa => 'HashRef', documentation => 'extra data to pass along'); + +$CLASS->meta->make_immutable(inline_constructor => 0); + +__END__ + +=encoding UTF-8 + +=head1 NAME + +NGCP::Schema::Exception - exceptions that work like ossbss mydie + +=head1 VERSION + +This document describes NGCP::Schema::Exception version 1.000 + +=head1 SYNOPSIS + + use aliased 'NGCP::Schema::Exception'; + Exception->throw({ + description => 'Client.Auth.Failed', + message => 'authentication failed', + }); + +=head1 DESCRIPTION + +This is a stop-gap measure to port ossbss code. + +=head1 INTERFACE + +=head2 Composition + + NGCP::Schema::Exception + ISA Throwable::Error + +All methods and attributes not mentioned here are inherited from L. + +=head2 Attributes + +=head3 C + +Type C, B attribute, designates the ossbss error type. + +=head3 C + +Type C, extra data to pass along to the error handler. + +=head2 Exports + +None. + +=head1 DIAGNOSTICS + +None. + +=head1 CONFIGURATION AND ENVIRONMENT + +NGCP::Schema::Exception requires no configuration files or environment variables. + +=head1 DEPENDENCIES + +See meta file in the source distribution. + +=head1 INCOMPATIBILITIES + +None reported. + +=head1 BUGS AND LIMITATIONS + +L + +No known limitations. + +=head1 TO DO + +This becomes obsolete with a hierarchy of exception classes. + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +Lars Dieckow C<< >> + +=head1 LICENCE + +restricted diff --git a/lib/NGCP/Schema/billing.pm b/lib/NGCP/Schema/billing.pm index ada30c91..3169c5c3 100644 --- a/lib/NGCP/Schema/billing.pm +++ b/lib/NGCP/Schema/billing.pm @@ -13,7 +13,374 @@ __PACKAGE__->load_namespaces; # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-02-05 17:12:46 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:sx/D2d16t8N9+cdwKfYW5g +use NGCP::Schema qw(); +use NGCP::Schema::provisioning qw(); +use aliased 'NGCP::Schema::Exception'; -# You can replace this text with custom code or comments, and it will be preserved on regeneration -__PACKAGE__->meta->make_immutable(inline_constructor => 0); -1; +method get_domain($reseller_id, $domain) { + my %return; + $return{domain} = $domain; + $return{id} = $self->resultset('domains')->search( + { + domain => $domain, + defined($reseller_id) + ? ( + 'domain_resellers.domain_id' => {-ident => 'me.id'}, + 'domain_resellers.reseller_id' => $reseller_id, + ) + : () + }, + { + join => 'domain_resellers', + select => [{distinct => ['me.id']}], + as => ['id'], + } + )->single->id; + Exception->throw({ + description => 'Client.Voip.NoSuchDomain', + message => "unknown domain '$domain'", + context => {object => $domain}, + }) unless defined $return{id} and $return{id}; + $return{resellers} = [ + map { $_->reseller_id } + $self->resultset('domain_resellers')->search( + { + domain_id => $return{id}, + reseller_id => {-ident => 'reseller.id'}, + status => {q{!=} => 'terminated'}, + }, + { + join => 'reseller', + columns => ['reseller_id'], + } + )->all + ] unless $reseller_id; + $return{subscribers} = { + map { $_->status => $_->get_column('count') } + $self->resultset('voip_subscribers')->search({ + domain_id => $return{id}, + contract_id => {-ident => 'contract.id'}, + defined($reseller_id) + ? ('contract.reseller_id' => $reseller_id) + : () + }, + { + select => ['status', {count => '*', -as => 'count'},], + join => 'contract', + group_by => ['status'], + } + )->all + }; + for my $status qw(active locked terminated) { + $return{subscribers}{$status} = 0 unless exists $return{subscribers}{$status}; + } + return \%return; +} + +method create_domain($data, $reseller_id?) { +# FIXME MXMS validation should throw objects + Exception->throw({ + description => 'Client.Syntax.MissingParam', + message => "missing parameter 'domain' in request", + }) unless exists $data->{domain}; + Exception->throw({ + description => 'Client.Syntax.MalformedDomain', + message => "malformed domain '$data->{domain}' in request", + }) unless NGCP::Schema->new->check_domain({domain => $data->{domain}}); +# /FIXME + $self->txn_do(λ{ + # just to verify the domain does not exist for the reseller + if (defined $self->get_domain($reseller_id, $data->{domain})) { + Exception->throw({ + description => 'Client.Voip.ExistingDomain', + message => "domain '$data->{domain}' already exists in the billing database", + context => {object => $data->{domain}}, + }); + } + # see if the domain exists at all + # this forces domains to be unique! + # we need this to simplify our overall data structure a bit + # at least our voip_dom_preferences are partially depending on it + my $dbdom = $self->get_domain(undef, $data->{domain}); + if (defined $dbdom) { + Exception->throw({ + description => 'Client.Voip.ExistingDomain', + message => "domain '$data->{domain}' already in use by another reseller", + context => {object => $data->{domain}}, + }); + } + # FIXME why does it test for $dbdom again? didn't we just above leave flow control? + if (defined $dbdom) { + $self->resultset('domain_resellers')->create({ + domain_id => $dbdom->{id}, + reseller_id => $reseller_id, + }) if defined $reseller_id; + } else { + # FIXME see docs about create: "keyed on the relationship name" + my $row = $self->resultset('domains')->create({domain => $data->{domain}}); + $self->resultset('domain_resellers')->create({ + domain_id => $row->id, + reseller_id => $reseller_id, + }) if defined $reseller_id; + } + # domain may already exist in provisioning DB if another reseller uses it + my $provisioning = NGCP::Schema::provisioning->connect; + if ($provisioning->get_domain({domain => $data->{domain}})) { + $provisioning->update_domain($data); + } else { + $provisioning->create_domain($data); + } + }); + return; +} + +method delete_domain($data, $reseller_id) { + Exception->throw({ + description => 'Client.Syntax.MissingParam', + message => "missing parameter 'domain' in request", + }) unless exists $data->{domain}; + Exception->throw({ + description => 'Client.Syntax.MalformedDomain', + message => "malformed domain '$data->{domain}' in request", + }) unless NGCP::Schema->new->check_domain({domain => $data->{domain}}); + $self->txn_do(λ{ + my $remaining_resellers = $self->delete_domain($reseller_id, $data->{domain}); + unless ($remaining_resellers) { + # just to verify the domain exists + my $dbdom = $self->get_domain($reseller_id, $data->{domain}); + $self->resultset('domain_resellers')->search( + { + domain_id => $dbdom->{id}, + defined($reseller_id) + ? (reseller => $reseller_id) + : (), + } + )->delete_all; + my $dc = $self->resultset('domain_resellers')->search( + { + domain_id => $dbdom->{id}, + } + )->count; + if ($dc == 0) { + # remove contracts and subscribers first + $self->_delete_domain_contracts_and_subscribers($dbdom->{id}, $reseller_id); + if ($reseller_id) { + # reseller admin, see if another reseller has subscribers + # within the domain (as dc==0, they should be terminated ones) + my $osc = $self->resultset('voip_subscribers')->search( + { + contract_id => {-ident => 'contracts.id'}, + domain_id => $dbdom->{id}, + reseller_id => {q{!=} => $reseller_id}, + }, + { + join => 'contracts', + } + )->count; + if($osc == 0) { + # no "foreign" data, delete the complete domain + $self->resultset('domains')->find($dbdom->{id})->delete_all; + } + } else { + # superuser, delete complete domain + $self->resultset('domains')->find($dbdom->{id})->delete_all; + } + } else { + # paranoia + Exception->throw({ + description => 'Server.Internal', + message => 'reseller_id empty in domain delete, still we have a persisting domain reseller.', + }) unless $reseller_id; + # Oooops! Breaks data encapsulation heinously! :o/ + $self->storage->dbh_do(λ{ + my (undef, $dbh, @bind) = @_; + $dbh->do( + 'DELETE FROM provisioning.pvs + USING provisioning.voip_subscribers pvs + INNER JOIN billing.voip_subscribers bvs + INNER JOIN billing.contracts bc + WHERE pvs.uuid = bvs.uuid + AND bvs.contract_id = bc.id + AND bvs.domain_id = ? + AND bc.reseller_id = ?', + {}, + @bind + ); + }, $dbdom->{id}, $reseller_id); + $self->_delete_domain_contracts_and_subscribers($dbdom->{id}, $reseller_id); + } + return $dc; + } + }); + return; +} + +method _delete_domain_contracts_and_subscribers($domain_id, $reseller_id) { + # remove contracts which only have subscribers within + # the domain that is to be deleted + $self->resultset('contracts')->search( + { + 'voip_subscribers.contract_id' => {-ident => 'me.id'}, + 'voip_subscribers.domain_id' => $domain_id, + 'voip_subscribers_2.contract_id' => {-ident => 'me.id'}, + 'voip_subscribers_2.domain_id' => {q{!=} => $domain_id}, + 'voip_subscribers_2.domain_id' => undef, + defined($reseller_id) + ? (reseller_id => $reseller_id) + : () + }, + { + join => ['voip_subscribers', 'voip_subscribers'], + } + )->delete_all; + # delete remaining subscribers within the domain that + # is to be deleted + if ($reseller_id) { + $self->resultset('voip_subscribers')->search( + { + contract_id => {-ident => 'id'}, + 'contracts.reseller_id' => $reseller_id, + domain_id => $domain_id, + }, + { + join => 'contracts' + } + )->delete_all; + } else { + $self->resultset('voip_subscribers')->search( + { + domain_id => $domain_id + } + )->delete_all; + } + return; +} + +$CLASS->meta->make_immutable(inline_constructor => 0); + +__END__ + +=encoding UTF-8 + +=head1 NAME + +NGCP::Schema::billing - billing schema + +=head1 VERSION + +This document describes NGCP::Schema::billing version 1.000 + +=head1 SYNOPSIS + + use NGCP::Schema::billing qw(); + +=head1 DESCRIPTION + +This is a port of F and +F in F. + +=head1 INTERFACE + +=head2 Composition + + NGCP::Schema::billing + ISA DBIx::Class::Schema + +All methods and attributes not mentioned here are inherited from +L. + +=head2 Attributes + +None. + +=head2 Methods + +=head3 C + + get_domain($reseller_id, $domain) + +This method retrieves a domain from the DB. Returns a fault if the +domain can not be found in the database. + +=head3 C + + create_domain($data, $reseller_id?) + +This function creates a new domain in the database. + +=head3 C + + delete_domain($data, $reseller_id) + +This function deletes a domain from the database. Will delete all +subscribers that use this domain too! +Returns a fault if the domain can not be found in the database. On +success, returns the number of remaining domain resellers for the +domain. + +=head2 Exports + +None. + +=head1 DIAGNOSTICS + +All exceptions are of type L. They are listed by +their C attribute. + +=head2 C + +C attribute is C + +=head2 C + +C attribute is C + +=head2 C + +C attribute is C + +=head2 C + +C attribute is C + +=head2 C + +C attribute is C + +=head2 C + +C attribute is C + +=head1 CONFIGURATION AND ENVIRONMENT + +See L. + +=head1 DEPENDENCIES + +See meta file in the source distribution. + +=head1 INCOMPATIBILITIES + +None reported. + +=head1 BUGS AND LIMITATIONS + +L + +No known limitations. + +=head1 TO DO + +Nothing so far. + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +Lars Dieckow C<< >> + +=head1 LICENCE + +restricted diff --git a/lib/NGCP/Schema/provisioning.pm b/lib/NGCP/Schema/provisioning.pm index f74eb960..790c48e3 100644 --- a/lib/NGCP/Schema/provisioning.pm +++ b/lib/NGCP/Schema/provisioning.pm @@ -13,7 +13,221 @@ __PACKAGE__->load_namespaces; # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-02-05 17:13:35 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ZXzIpOFDxVoY7UfJoB1ddg +use NGCP::Schema qw(); +use aliased 'NGCP::Schema::Exception'; -# You can replace this text with custom code or comments, and it will be preserved on regeneration -__PACKAGE__->meta->make_immutable(inline_constructor => 0); -1; +method _get_domain_id($domain) { + my $domainid = $self->resultset('voip_domains')->search(id => $domain)->first->id; + Exception->throw({ + description => 'Client.Voip.NoSuchDomain', + message => "domain '$domain' does not exist", + context => {object => $domain} + }) unless defined $domainid; + return $domainid; +} + +method create_domain($data) { +# FIXME MXMS validation should throw objects + Exception->throw({ + description => 'Client.Syntax.MissingParam', + message => "missing parameter 'domain' in request", + }) unless exists $data->{domain}; + Exception->throw({ + description => 'Client.Syntax.MalformedDomain', + message => "malformed domain '$data->{domain}' in request", + }) unless NGCP::Schema->new->check_domain({domain => $data->{domain}}); +# /FIXME + + $self->txn_do(λ{ + # FIXME ack-basswards exception check: abused for control flow + eval { + $self->_get_domain_id($data->{domain}); + 1; + } and Exception->throw({ + description => 'Client.Voip.ExistingDomain', + message => "domain '$data->{domain}' already exists in the operations database", + context => {object => $data->{domain}}, + }); + $self->resultset('voip_domains')->create({domain => $data->{domain}}); + }); + $self->_sip_domain_reload; + return; +} + +method get_domain($data) { + Exception->throw({ + description => 'Client.Syntax.MissingParam', + message => "missing parameter 'domain' in request", + }) unless exists $$data{domain}; + Exception->throw({ + description => 'Client.Syntax.MalformedDomain', + message => "malformed domain '$data->{domain}' in request", + }) unless NGCP::Schema->new->check_domain({domain => $data->{domain}}); + return $self->resultset('voip_domains')->search(id => $self->_get_domain_id($data->{domain})); +} + +method update_domain($data) { + Exception->throw({ + description => 'Client.Syntax.MissingParam', + message => "missing parameter 'domain' in request", + }) unless exists $data->{domain}; + Exception->throw({ + description => 'Client.Syntax.MalformedDomain', + message => "malformed domain '$data->{domain}' in request", + }) unless NGCP::Schema->new->check_domain({domain => $data->{domain}}); + $self->txn_do(λ{ + my $domainid = $self->_get_domain_id($data->{domain}); + }); + $self->_sip_domain_reload; + return; +} + +method delete_domain($data) { + Exception->throw({ + description => 'Client.Syntax.MissingParam', + message => "missing parameter 'domain' in request", + }) unless exists $data->{domain}; + Exception->throw({ + description => 'Client.Syntax.MalformedDomain', + message => "malformed domain '$data->{domain}' in request", + }) unless NGCP::Schema->new->check_domain({domain => $data->{domain}}); + }) unless NGCP::Schema->new->check_domain({domain => $data->{domain}}); + $self->txn_do(λ{ + my $domainid = $self->_get_domain_id($data->{domain}); + $self->resultset('voip_domains')->search( + {}, + { + id => $domainid + } + )->delete_all; + }); + $self->_sip_domain_reload; + return; +} + +$CLASS->meta->make_immutable(inline_constructor => 0); + +__END__ + +=encoding UTF-8 + +=head1 NAME + +NGCP::Schema::provisioning - provisioning schema + +=head1 VERSION + +This document describes NGCP::Schema::provisioning version 1.000 + +=head1 SYNOPSIS + + use NGCP::Schema::provisioning qw(); + +=head1 DESCRIPTION + +This is a port of F and +F in F. + +=head1 INTERFACE + +=head2 Composition + + NGCP::Schema::provisioning + ISA DBIx::Class::Schema + +All methods and attributes not mentioned here are inherited from +L. + +=head2 Attributes + +None. + +=head2 Methods + +=head3 C + + get_domain($data) + +This method retrieves a domain from the DB. Returns a fault if the +domain can not be found in the database. + +=head3 C + + create_domain($data) + +This function creates a new domain in the database. + +=head3 C + + update_domain($data) + +This function modifies a domain in the database. As there is no domain +data at the moment, this function does nothing but check whether the +domain exists. + +=head3 C + + delete_domain($data) + +This function deletes a domain from the database. Will delete all +subscribers that use this domain too! +Returns a fault if the domain can not be found in the database. + +=head2 Exports + +None. + +=head1 DIAGNOSTICS + +All exceptions are of type L. They are listed by +their C attribute. + +=head2 C + +C attribute is C + +=head2 C + +C attribute is C + +=head2 C + +C attribute is C + +=head2 C + +C attribute is C + +=head1 CONFIGURATION AND ENVIRONMENT + +See L. + +=head1 DEPENDENCIES + +See meta file in the source distribution. + +=head1 INCOMPATIBILITIES + +None reported. + +=head1 BUGS AND LIMITATIONS + +L + +No known limitations. + +=head1 TO DO + +Nothing so far. + +=head1 SEE ALSO + +L + +=head1 AUTHOR + +Lars Dieckow C<< >> + +=head1 LICENCE + +restricted