handle domains

agranig/2.004-ramoptimized
Lars Dieckow 13 years ago
parent 3c448229bb
commit a081dddc42

@ -5,11 +5,16 @@ my $builder = Module::Build->new(
dist_author => 'Lars Dieckow <ldieckow@sipwise.com>', dist_author => 'Lars Dieckow <ldieckow@sipwise.com>',
dist_version_from => 'lib/NGCP/Schema.pm', dist_version_from => 'lib/NGCP/Schema.pm',
requires => { requires => {
'aliased' => 0,
'DBIx::Class::Schema::Loader' => 0, 'DBIx::Class::Schema::Loader' => 0,
'File::Path' => 0, 'File::Path' => 0,
'MooseX::FileAttribute' => 0,
'MooseX::NonMoose' => 0, 'MooseX::NonMoose' => 0,
'NGCP' => 0, 'NGCP' => 0,
'Quantum::Superpositions' => 0, 'Quantum::Superpositions' => 0,
'Regexp::Common' => 0,
'Regexp::IPv6' => 0,
'Throwable::Error' => 0,
}, },
add_to_cleanup => ['NGCP-Schema-*'], add_to_cleanup => ['NGCP-Schema-*'],
); );

@ -1,7 +1,76 @@
package NGCP::Schema; package NGCP::Schema;
use Sipwise::Base; 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'; 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__ __END__
=encoding UTF-8 =encoding UTF-8

@ -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<config_file>
Type C<MooseX::Types::Path::Class>, B<required> attribute, location of the configuration file. Default is
C</etc/ngcp-ossbss/provisioning.conf>.
=head3 C<config>
Type C<HashRef>, supplies configuration from the L</config_file>.
=head2 Methods
=head3 C<check_config>
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<can't find config file %s>
The configuration file does not exist.
=back
=head1 CONFIGURATION AND ENVIRONMENT
See L</config_file>.
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<https://bugtracker.sipwise.com>
No known limitations.
=head1 AUTHOR
Lars Dieckow C<< <ldieckow@sipwise.com> >>
=head1 LICENCE
restricted

@ -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<Throwable::Error>.
=head2 Attributes
=head3 C<description>
Type C<Str>, B<required> attribute, designates the ossbss error type.
=head3 C<context>
Type C<HashRef>, 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<https://bugtracker.sipwise.com>
No known limitations.
=head1 TO DO
This becomes obsolete with a hierarchy of exception classes.
=head1 SEE ALSO
L<Sipwise::Provisioning/mydie>
=head1 AUTHOR
Lars Dieckow C<< <ldieckow@sipwise.com> >>
=head1 LICENCE
restricted

@ -13,7 +13,374 @@ __PACKAGE__->load_namespaces;
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-02-05 17:12:46 # 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 # 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 method get_domain($reseller_id, $domain) {
__PACKAGE__->meta->make_immutable(inline_constructor => 0); my %return;
1; $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<lib/Sipwise/Provisioning/Billing.pm> and
F<lib/Sipwise/Provisioning/Billing/DB.pm> in F<svn/dev/ngcp/ossbss>.
=head1 INTERFACE
=head2 Composition
NGCP::Schema::billing
ISA DBIx::Class::Schema
All methods and attributes not mentioned here are inherited from
L<DBIx::Class::Schema>.
=head2 Attributes
None.
=head2 Methods
=head3 C<get_domain>
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>
create_domain($data, $reseller_id?)
This function creates a new domain in the database.
=head3 C<delete_domain>
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<NGCP::Schema::Exception>. They are listed by
their C<message> attribute.
=head2 C<unknown domain '%s'>
C<description> attribute is C<Client.Voip.NoSuchDomain>
=head2 C<missing parameter 'domain' in request>
C<description> attribute is C<Client.Syntax.MissingParam>
=head2 C<malformed domain '%s' in request>
C<description> attribute is C<Client.Syntax.MalformedDomain>
=head2 C<domain '%s' already exists in the billing database>
C<description> attribute is C<Client.Voip.ExistingDomain>
=head2 C<domain '%s' already in use by another reseller>
C<description> attribute is C<Client.Voip.ExistingDomain>
=head2 C<reseller_id empty in domain delete, still we have a persisting domain reseller.>
C<description> attribute is C<Server.Internal>
=head1 CONFIGURATION AND ENVIRONMENT
See L<DBI/"DBI ENVIRONMENT VARIABLES">.
=head1 DEPENDENCIES
See meta file in the source distribution.
=head1 INCOMPATIBILITIES
None reported.
=head1 BUGS AND LIMITATIONS
L<https://bugtracker.sipwise.com>
No known limitations.
=head1 TO DO
Nothing so far.
=head1 SEE ALSO
L<MooseX::Method::Signatures/"SIGNATURE SYNTAX">
=head1 AUTHOR
Lars Dieckow C<< <ldieckow@sipwise.com> >>
=head1 LICENCE
restricted

@ -13,7 +13,221 @@ __PACKAGE__->load_namespaces;
# Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-02-05 17:13:35 # Created by DBIx::Class::Schema::Loader v0.07033 @ 2013-02-05 17:13:35
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ZXzIpOFDxVoY7UfJoB1ddg # 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 method _get_domain_id($domain) {
__PACKAGE__->meta->make_immutable(inline_constructor => 0); my $domainid = $self->resultset('voip_domains')->search(id => $domain)->first->id;
1; 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<lib/Sipwise/Provisioning/Voip.pm> and
F<lib/Sipwise/Provisioning/Voip/DB.pm> in F<svn/dev/ngcp/ossbss>.
=head1 INTERFACE
=head2 Composition
NGCP::Schema::provisioning
ISA DBIx::Class::Schema
All methods and attributes not mentioned here are inherited from
L<DBIx::Class::Schema>.
=head2 Attributes
None.
=head2 Methods
=head3 C<get_domain>
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>
create_domain($data)
This function creates a new domain in the database.
=head3 C<update_domain>
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>
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<NGCP::Schema::Exception>. They are listed by
their C<message> attribute.
=head2 C<domain '%s' does not exist>
C<description> attribute is C<Client.Voip.NoSuchDomain>
=head2 C<missing parameter 'domain' in request>
C<description> attribute is C<Client.Syntax.MissingParam>
=head2 C<malformed domain '%s' in request>
C<description> attribute is C<Client.Syntax.MalformedDomain>
=head2 C<missing parameter 'domain' in request>
C<description> attribute is C<Client.Syntax.MissingParam>
=head1 CONFIGURATION AND ENVIRONMENT
See L<DBI/"DBI ENVIRONMENT VARIABLES">.
=head1 DEPENDENCIES
See meta file in the source distribution.
=head1 INCOMPATIBILITIES
None reported.
=head1 BUGS AND LIMITATIONS
L<https://bugtracker.sipwise.com>
No known limitations.
=head1 TO DO
Nothing so far.
=head1 SEE ALSO
L<MooseX::Method::Signatures/"SIGNATURE SYNTAX">
=head1 AUTHOR
Lars Dieckow C<< <ldieckow@sipwise.com> >>
=head1 LICENCE
restricted

Loading…
Cancel
Save