diff --git a/lib/Rex/Function/Common.pm b/lib/Rex/Function/Common.pm new file mode 100644 index 000000000..8c89a7030 --- /dev/null +++ b/lib/Rex/Function/Common.pm @@ -0,0 +1,48 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::Function::Common; + +use strict; +use warnings; + +# VERSION + +require Exporter; +require Rex::Config; +use Data::Dumper; +use base qw(Exporter); +use vars qw(@EXPORT); +use MooseX::Params::Validate; +use Rex::MultiSub::Function; + +@EXPORT = qw(function); + +sub function { + my ( $name, $options, $function ) = @_; + my $name_save = $name; + if ( $name_save !~ m/^[a-zA-Z_][a-zA-Z0-9_]+$/ ) { + Rex::Logger::info( + "Please use only the following characters for function names:", "warn" ); + Rex::Logger::info( " A-Z, a-z, 0-9 and _", "warn" ); + Rex::Logger::info( "Also the function should start with A-Z or a-z", + "warn" ); + die "Wrong function name syntax."; + } + + my $sub = Rex::MultiSub::Function->new( + name => $name_save, + function => $function, + params_list => $options->{params_list}, + test_wantarray => 1, + ); + + my ( $class, $file, @tmp ) = caller; + + $sub->export( $class, $options->{export} ); +} + +1; diff --git a/lib/Rex/MultiSub.pm b/lib/Rex/MultiSub.pm new file mode 100644 index 000000000..b713e66d3 --- /dev/null +++ b/lib/Rex/MultiSub.pm @@ -0,0 +1,115 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::MultiSub; + +use strict; +use warnings; + +# VERSION + +use Moose; +use MooseX::Params::Validate; +use Rex::MultiSub::LookupTable; + +use Data::Dumper; +use Carp; + +has methods => ( + is => 'rw', + isa => 'Rex::MultiSub::LookupTable', + default => sub { + Rex::MultiSub::LookupTable->instance; + } +); + +has name => ( is => 'ro', isa => 'Str' ); +has function => ( is => 'ro', isa => 'CodeRef' ); +has params_list => ( is => 'ro', isa => 'ArrayRef' ); + +sub validate { } +sub error { } + +sub BUILD { + my ($self) = @_; + $self->methods->add( $self->name, $self->params_list, $self->function ); +} + +sub export { + my ( $self, $ns, $global ) = @_; + + my $name = $self->name; + + no strict 'refs'; + no warnings 'redefine'; + + *{"${ns}::$name"} = sub { + $self->dispatch(@_); + }; + + if ($global) { + + # register in caller namespace + push @{ $ns . "::ISA" }, "Rex::Exporter" + unless ( grep { $_ eq "Rex::Exporter" } @{ $ns . "::ISA" } ); + push @{ $ns . "::EXPORT" }, $self->name; + } + + use strict; + use warnings; +} + +sub dispatch { + my ( $self, @args ) = @_; + + my @errors; + my $exec; + my @all_args; + my $found = 0; + + for my $f ( + sort { + scalar( @{ $b->{params_list} } ) <=> + scalar( @{ $a->{params_list} } ) + } @{ $self->methods->data->{ $self->name } } + ) + { + + eval { + @all_args = $self->validate( $f, @args ); + $found = 1; + 1; + } or do { + push @errors, $@; + + # print "Err: $@\n"; + # TODO catch no "X parameter was given" errors + next; + }; + + $exec = $f->{code}; + + last; + } + if ( !$found ) { + my @err_msg; + for my $err (@errors) { + my ($fline) = split( /\n/, $err ); + push @err_msg, $fline; + } + + $self->error(@err_msg); + } + + $self->call( $exec, @all_args ); +} + +sub call { + my ( $self, $code, @args ) = @_; + $code->(@args); +} + +1; diff --git a/lib/Rex/MultiSub/Function.pm b/lib/Rex/MultiSub/Function.pm new file mode 100644 index 000000000..dfc3bd61d --- /dev/null +++ b/lib/Rex/MultiSub/Function.pm @@ -0,0 +1,46 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::MultiSub::Function; + +use strict; +use warnings; + +# VERSION + +use Moose; +use MooseX::Params::Validate; +use Rex::MultiSub::LookupTable; + +use Data::Dumper; +use Carp; + +extends qw(Rex::MultiSub::PosValidatedList); + +has test_wantarray => ( + is => 'ro', + isa => 'Bool', + default => sub { 0 }, +); + +override call => sub { + my ( $self, $code, @args ) = @_; + + my $ret = $code->(@args); + + if ( exists $ret->{value} ) { + if ( $self->test_wantarray && wantarray ) { + return split( /\n/, $ret->{value} ); + } + else { + return $ret->{value}; + } + } + + return undef; +}; + +1; diff --git a/lib/Rex/MultiSub/LookupTable.pm b/lib/Rex/MultiSub/LookupTable.pm new file mode 100644 index 000000000..37498d17c --- /dev/null +++ b/lib/Rex/MultiSub/LookupTable.pm @@ -0,0 +1,31 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::MultiSub::LookupTable; + +use strict; +use warnings; + +# VERSION + +use MooseX::Singleton; + +has data => ( + is => 'ro', + isa => 'HashRef', + default => sub { {} }, +); + +sub add { + my ( $self, $name, $params_list, $code ) = @_; + push @{ $self->data->{$name} }, + { + params_list => $params_list, + code => $code, + }; +} + +1; diff --git a/lib/Rex/MultiSub/PosValidatedList.pm b/lib/Rex/MultiSub/PosValidatedList.pm new file mode 100644 index 000000000..ed4dc6d29 --- /dev/null +++ b/lib/Rex/MultiSub/PosValidatedList.pm @@ -0,0 +1,47 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::MultiSub::PosValidatedList; + +use strict; +use warnings; + +# VERSION + +use Moose; +use MooseX::Params::Validate; +use Rex::MultiSub::LookupTable; + +use Data::Dumper; +use Carp; + +extends qw(Rex::MultiSub); + +override validate => sub { + my ( $self, $func_opts, @args ) = @_; + + my @_x = @{ $func_opts->{params_list} }; + my @order = map { $_x[$_] } grep { $_ & 1 } 1 .. $#_x; + + my @v_args = pos_validated_list( + \@args, @order, + MX_PARAMS_VALIDATE_NO_CACHE => 1, + MX_PARAMS_VALIDATE_ALLOW_EXTRA => 1 + ); + + return @v_args; +}; + +override error => sub { + my ( $self, @err_msg ) = @_; + + my $name = $self->name; + croak "Function $name for provided parameter not found.\nErrors:\n" + . join( "\n", @err_msg ); +}; + +1; + diff --git a/lib/Rex/MultiSub/Resource.pm b/lib/Rex/MultiSub/Resource.pm new file mode 100644 index 000000000..47c38e65e --- /dev/null +++ b/lib/Rex/MultiSub/Resource.pm @@ -0,0 +1,108 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::MultiSub::Resource; + +use strict; +use warnings; + +# VERSION + +use Moose; +use MooseX::Params::Validate; +use Rex::MultiSub::LookupTable; + +use Data::Dumper; +use Carp; +use Clone qw(clone); + +extends qw(Rex::MultiSub::ValidatedHash); + +override validate => sub { + my ( $self, $func_opts, @args ) = @_; + my @modified_args = @args; + my $name = shift @modified_args; + + # some defaults maybe a coderef, so we need to execute this now + my @_x = @{ clone( $func_opts->{params_list} ) }; + my %_x = @_x; + for my $k ( keys %_x ) { + if ( ref $_x{$k}->{default} eq "CODE" ) { + $_x{$k}->{default} = $_x{$k}->{default}->(@args); + } + } + my %args = validated_hash( + \@modified_args, %_x, + MX_PARAMS_VALIDATE_NO_CACHE => 1, + MX_PARAMS_VALIDATE_ALLOW_EXTRA => 1 + ); + + return ( $args[0], %args ); +}; + +override call => sub { + my ( $self, $code, @args ) = @_; + + # TODO check for common parameters like + # * timeout + # * only_notified + # * only_if + # * unless + # * creates + # * on_change + # * on_before_change + # * ensure + + # TODO migrate reporting and error handling from Rex::Resource + # TODO remove Rex::Resource class + # TODO add default values for $args[1] if $args[0] is hash + + my @status_return; + + if ( ref $args[0] eq "HASH" ) { + my @status = (); + for my $k_name ( keys %{ $args[0] } ) { + my $ret = $code->( $k_name, %{ $args[0]->{$k_name} } ); + push @status, $ret; + } + @status_return = @status; + } + elsif ( ref $args[0] eq "ARRAY" ) { + my @status = (); + for my $v_name ( @{ $args[0] } ) { + my $ret = $code->( $v_name, @args[ 1 .. $#args ] ); + push @status, $ret; + } + @status_return = @status; + } + else { + my $ret = $code->(@args); + push @status_return, $ret; + } + + # test if on_change needed + for my $status (@status_return) { + if ( $status->{status} && $status->{status} eq "changed" ) { + my %opts = @args[ 1 .. scalar(@args) - 1 ]; + if ( $opts{on_change} ) { + $opts{on_change}( \@args ); + } + } + } + + if ( scalar(@status_return) == 1 && wantarray ) { + return split( /\n/, $status_return[0]->{value} ); + } + + if ( scalar(@status_return) == 1 ) { + return $status_return[0]->{value}; + } + else { + return @status_return; + } +}; + +1; diff --git a/lib/Rex/MultiSub/ValidatedHash.pm b/lib/Rex/MultiSub/ValidatedHash.pm new file mode 100644 index 000000000..10f8bf2e0 --- /dev/null +++ b/lib/Rex/MultiSub/ValidatedHash.pm @@ -0,0 +1,52 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::MultiSub::ValidatedHash; + +use strict; +use warnings; + +# VERSION + +use Moose; +use MooseX::Params::Validate; +use Rex::MultiSub::LookupTable; + +use Data::Dumper; +use Carp; + +extends qw(Rex::MultiSub); + +override validate => sub { + my ( $self, $func_opts, @args ) = @_; + + # some defaults maybe a coderef, so we need to execute this now + my @_x = @{ $func_opts->{params_list} }; + my %_x = @_x; + for my $k ( keys %_x ) { + if ( ref $_x{$k}->{default} eq "CODE" ) { + $_x{$k}->{default} = $_x{$k}->{default}->(@args); + } + } + + my %args = validated_hash( + \@args, %_x, + MX_PARAMS_VALIDATE_NO_CACHE => 1, + MX_PARAMS_VALIDATE_ALLOW_EXTRA => 1 + ); + + return %args; +}; + +override error => sub { + my ( $self, @err_msg ) = @_; + + my $name = $self->name; + croak "Function $name for provided parameter not found.\nErrors:\n" + . join( "\n", @err_msg ); +}; + +1; diff --git a/lib/Rex/Report/Base.pm b/lib/Rex/Report/Base.pm index 218a78fbc..74fa6befa 100644 --- a/lib/Rex/Report/Base.pm +++ b/lib/Rex/Report/Base.pm @@ -46,6 +46,13 @@ sub report { $self->{__reports__}->{$res}->{changed} ||= $option{changed} || 0; } + $self->{__reports__}->{ $self->{__current_resource__}->[-1] }->{name} = + $option{name}; + $self->{__reports__}->{ $self->{__current_resource__}->[-1] }->{resource} = + $option{resource}; + $self->{__reports__}->{ $self->{__current_resource__}->[-1] }->{status} = + $option{status}; + push @{ $self->{__reports__}->{ $self->{__current_resource__}->[-1] }->{messages} }, @@ -63,6 +70,7 @@ sub report_resource_start { push @{ $self->{__current_resource__} }, $self->_gen_res_name(%option); $self->{__reports__}->{ $self->{__current_resource__}->[-1] } = { changed => 0, + status => 'unknown', messages => [], start_time => time, }; diff --git a/lib/Rex/Resource.pm b/lib/Rex/Resource.pm index e2ef794e0..d22a31316 100644 --- a/lib/Rex/Resource.pm +++ b/lib/Rex/Resource.pm @@ -11,43 +11,47 @@ use warnings; # VERSION +use Moose; + use Rex::Constants; +require Rex::Resource::Common; + our @CURRENT_RES; +has name => ( is => 'ro', isa => 'Str', ); +has display_name => ( is => 'ro', isa => 'Str', ); +has type => ( is => 'ro', isa => 'Str', ); +has cb => ( is => 'ro', isa => 'CodeRef' ); +has __status__ => + ( is => 'ro', isa => 'Str', writer => '_set_status', default => "unchanged" ); +has status_message => ( + is => 'ro', + isa => 'Str', + default => sub { "" }, + writer => "_set_status_message" +); + sub is_inside_resource { ref $CURRENT_RES[-1] ? 1 : 0 } sub get_current_resource { $CURRENT_RES[-1] } -sub new { - my $that = shift; - my $proto = ref($that) || $that; - my $self = {@_}; - - bless( $self, $proto ); - - $self->{__status__} = "unchanged"; - - return $self; -} - -sub name { (shift)->{name}; } -sub display_name { (shift)->{display_name}; } -sub type { (shift)->{type}; } - sub call { - my ( $self, $name, %params ) = @_; + my ( $self, $c, $name, %params ) = @_; - if ( ref $name eq "HASH" ) { + push @CURRENT_RES, $self; - # multiple resource call - for my $n ( keys %{$name} ) { - $self->call( $n, %{ $name->{$n} } ); + #### check and run before hook + eval { + my @new_args = + Rex::Hook::run_hook( $self->type => "before", $name, %params ); + if (@new_args) { + ( $name, %params ) = @new_args; } - - return; - } - - push @CURRENT_RES, $self; + 1; + } or do { + die( "Before hook failed. Cancelling " . $self->type . " resource: $@" ); + }; + ############################## $self->set_all_parameters(%params); @@ -57,13 +61,49 @@ sub call { Rex::get_current_connection()->{reporter} ->report_resource_start( type => $self->display_name, name => $name ); - my $failed = 0; + my $failed = 0; + my $failed_msg = ""; + eval { - $self->{cb}->( \%params ); + my ( $provider, $mod_config ) = $self->cb->( $c, \%params ); + + if ( $provider =~ m/^[a-zA-Z0-9_:]+$/ && ref $mod_config eq "HASH" ) { + + # new resource interface + # old one is already executed via $self->{cb}->(\%params) + $provider->require; + + my $provider_o = $provider->new( + type => $self->type, + config => $mod_config, + name => ( $mod_config->{name} || $name ) + ); + + # TODO add dry-run feature + $provider_o->process; + + #### check and run after hook + Rex::Hook::run_hook( $self->type => "after", $name, %{$mod_config} ); + ############################## + + Rex::Resource::Common::emit( $provider_o->status(), + $provider_o->type . "[" + . $provider_o->name + . "] is now " + . $self->{res_ensure} . "." + . $provider_o->message ); + } + else { + # TODO add deprecation warning + } + 1; } or do { - Rex::Logger::info( $@, "error" ); - Rex::Logger::info( "Resource execution failed: $name", "error" ); + $failed_msg = $@; + Rex::Logger::info( $failed_msg, "error" ); + Rex::Logger::info( + "Resource execution failed: " . $self->display_name . "[$name]", + "error" ); $failed = 1; }; @@ -71,7 +111,7 @@ sub call { Rex::get_current_connection()->{reporter}->report( changed => 1, failed => $failed, - message => $self->message, + message => $self->status_message, ); } else { @@ -83,13 +123,16 @@ sub call { } if ( exists $params{on_change} && $self->was_updated ) { - $params{on_change}->( $self->{__status__} ); + $params{on_change}->( $self->__status__ ); } Rex::get_current_connection()->{reporter} ->report_resource_end( type => $self->display_name, name => $name ); pop @CURRENT_RES; + + # TODO: resource autodie? + die $failed_msg if $failed; } sub was_updated { @@ -106,10 +149,10 @@ sub changed { my ( $self, $changed ) = @_; if ( defined $changed ) { - $self->{__status__} = "changed"; + $self->_set_status("changed"); } else { - return ( $self->{__status__} eq "changed" ? 1 : 0 ); + return ( $self->__status__ eq "changed" ? 1 : 0 ); } } @@ -117,10 +160,10 @@ sub created { my ( $self, $created ) = @_; if ( defined $created ) { - $self->{__status__} = "created"; + $self->_set_status("created"); } else { - return ( $self->{__status__} eq "created" ? 1 : 0 ); + return ( $self->__status__ eq "created" ? 1 : 0 ); } } @@ -128,10 +171,10 @@ sub removed { my ( $self, $removed ) = @_; if ( defined $removed ) { - $self->{__status__} = "removed"; + $self->_set_status("removed"); } else { - return ( $self->{__status__} eq "removed" ? 1 : 0 ); + return ( $self->__status__ eq "removed" ? 1 : 0 ); } } @@ -139,10 +182,10 @@ sub message { my ( $self, $message ) = @_; if ( defined $message ) { - $self->{message} = $message; + $self->_set_status_message($message); } else { - return ( $self->{message} || ( $self->display_name . " changed." ) ); + return ( $self->status_message || ( $self->display_name . " changed." ) ); } } diff --git a/lib/Rex/Resource/Common.pm b/lib/Rex/Resource/Common.pm index 9c4e4e062..641d5dfbe 100644 --- a/lib/Rex/Resource/Common.pm +++ b/lib/Rex/Resource/Common.pm @@ -13,17 +13,34 @@ use warnings; require Exporter; require Rex::Config; +use Rex::Commands::Gather; use Rex::Resource; use Data::Dumper; +use MooseX::Params::Validate; +use Hash::Merge qw/merge/; +use Rex::MultiSub::Resource; + use base qw(Exporter); use vars qw(@EXPORT); +use Carp; -@EXPORT = qw(emit resource resource_name changed created removed); +@EXPORT = + qw(emit resource resource_name changed created removed get_resource_provider + state_good state_changed state_created state_removed state_failed state_timeout + resolve_resource_provider +); sub changed { return "changed"; } sub created { return "created"; } sub removed { return "removed"; } +sub state_good { return "good"; } +sub state_failed { return "failed"; } +sub state_changed { return "changed"; } +sub state_created { return "created"; } +sub state_removed { return "removed"; } +sub state_timeout { return "timeout"; } + sub emit { my ( $type, $message ) = @_; if ( !Rex::Resource->is_inside_resource ) { @@ -57,6 +74,8 @@ sub emit { =cut +my $__lookup_table; + sub resource { my ( $name, $options, $function ) = @_; my $name_save = $name; @@ -77,54 +96,15 @@ sub resource { die "Wrong resource name syntax."; } - my ( $class, $file, @tmp ) = caller; - my $res = Rex::Resource->new( - type => "${class}::$name", - name => $name, - display_name => ( - $options->{name} - || ( $options->{export} ? $name : "${caller_pkg}::${name}" ) - ), - cb => $function + my $sub = Rex::MultiSub::Resource->new( + name => $name_save, + function => $function, + params_list => $options->{params_list}, ); - my $func = sub { - $res->call(@_); - }; - - if (!$class->can($name) - && $name_save =~ m/^[a-zA-Z_][a-zA-Z0-9_]+$/ ) - { - no strict 'refs'; - Rex::Logger::debug("Registering resource: ${class}::$name_save"); - - my $code = $_[-2]; - *{"${class}::$name_save"} = $func; - use strict; - } - elsif ( ( $class ne "main" && $class ne "Rex::CLI" ) - && !$class->can($name_save) - && $name_save =~ m/^[a-zA-Z_][a-zA-Z0-9_]+$/ ) - { - # if not in main namespace, register the task as a sub - no strict 'refs'; - Rex::Logger::debug( - "Registering resource (not main namespace): ${class}::$name_save"); - my $code = $_[-2]; - *{"${class}::$name_save"} = $func; - - use strict; - } - - if ( exists $options->{export} && $options->{export} ) { - no strict 'refs'; + my ( $class, $file, @tmp ) = caller; - # register in caller namespace - push @{ $caller_pkg . "::ISA" }, "Rex::Exporter" - unless ( grep { $_ eq "Rex::Exporter" } @{ $caller_pkg . "::ISA" } ); - push @{ $caller_pkg . "::EXPORT" }, $name_save; - use strict; - } + $sub->export( $class, $options->{export} ); } sub resource_name { @@ -141,6 +121,89 @@ sub current_resource { return $Rex::Resource::CURRENT_RES[-1]; } +sub get_resource_provider { + my ( $os, $os_name ) = @_; + my ($pkg) = caller; + + if ( is_redhat($os_name) ) { + $os_name = "redhat"; + } + + elsif ( is_ubuntu($os_name) ) { + $os_name = "ubuntu"; + } + + elsif ( is_debian($os_name) ) { + $os_name = "debian"; + } + + elsif ( is_gentoo($os_name) ) { + $os_name = "gentoo"; + } + + elsif ( is_suse($os_name) ) { + $os_name = "suse"; + } + + elsif ( is_alt($os_name) ) { + $os_name = "alt"; + } + + elsif ( is_mageia($os_name) ) { + $os_name = "mageia"; + } + + elsif ( is_arch($os_name) ) { + $os_name = "arch"; + } + + elsif ( is_openwrt($os_name) ) { + $os_name = "openwrt"; + } + + else { + die "No module found for $os_name."; + } + + my $try_load = sub { + my @mods = @_; + + my $ret; + for my $mod (@mods) { + Rex::Logger::debug("Try to load provider: $mod"); + eval { + $mod->require; + $ret = $mod; + 1; + } or do { + Rex::Logger::debug("Failed loading provider: $mod\n$@"); + $ret = undef; + }; + + return $ret if ($ret); + } + }; + + my $provider_pkg = $try_load->( + "${pkg}::Provider::" . lc("${os}::${os_name}"), + "${pkg}::Provider::" . lc("${os}::default"), + "${pkg}::Provider::" . lc($os) + ); + + return $provider_pkg; +} + +sub resolve_resource_provider { + my ($provider) = @_; + + if ( $provider =~ m/^::/ ) { + my @call = caller; + return $call[0] . "::Provider$provider"; + } + + return $provider; +} + =back =cut diff --git a/lib/Rex/Resource/Provider.pm b/lib/Rex/Resource/Provider.pm new file mode 100644 index 000000000..f3d81f36c --- /dev/null +++ b/lib/Rex/Resource/Provider.pm @@ -0,0 +1,94 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::Resource::Provider; + +use strict; +use warnings; + +# VERSION + +use Moose; +use Data::Dumper; + +has __version__ => ( + is => 'ro', + isa => 'Str', + default => sub { "1" }, +); + +has name => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has config => ( + is => 'ro', + isa => 'HashRef', + required => 1, +); + +has type => ( + is => 'ro', + isa => 'Str', + required => 1, +); + +has status => ( + is => 'ro', + isa => 'Str', + writer => '_set_status', + default => sub { 'unchanged' }, +); + +has fs => ( + is => 'rw', + isa => 'Rex::Interface::Fs::Base', + lazy => 1, + default => sub { return Rex::Interface::Fs->create }, +); + +has file => ( + is => 'rw', + isa => 'Rex::Interface::File::Base', + lazy => 1, + default => sub { return Rex::Interface::File->create }, +); + +has exec => ( + is => 'rw', + isa => 'Rex::Interface::Exec::Base', + lazy => 1, + default => sub { return Rex::Interface::Exec->create }, +); + +has message => ( + is => 'ro', + isa => 'Str', + writer => '_set_message', + default => sub { '' }, +); + +sub BUILD { + my ($self) = @_; + Rex::get_current_connection()->{reporter} + ->report_resource_start( type => $self->type, name => $self->name ); +} + +sub DEMOLISH { + my ($self) = @_; + $self->report; +} + +sub report { + my ($self) = @_; + + Rex::get_current_connection()->{reporter} + ->report_resource_end( type => $self->type, name => $self->name ); +} + +1; diff --git a/lib/Rex/Resource/Role/EnableDisable.pm b/lib/Rex/Resource/Role/EnableDisable.pm new file mode 100644 index 000000000..f34ac69bc --- /dev/null +++ b/lib/Rex/Resource/Role/EnableDisable.pm @@ -0,0 +1,27 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::Resource::Role::EnableDisable; + +use strict; +use warnings; + +# VERSION + +use Moose::Role; +use Rex::Resource::Common; + +with qw(Rex::Resource::Role::Ensureable); + +has ensure_options => ( + is => 'ro', + isa => 'ArrayRef[Str]', + default => sub { [qw/present absent enable disable/] }, +); + +requires qw(enable disable); + +1; diff --git a/lib/Rex/Resource/Role/Ensureable.pm b/lib/Rex/Resource/Role/Ensureable.pm new file mode 100644 index 000000000..f27443cf1 --- /dev/null +++ b/lib/Rex/Resource/Role/Ensureable.pm @@ -0,0 +1,58 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::Resource::Role::Ensureable; + +use strict; +use warnings; + +# VERSION + +use Moose::Role; +use List::Util qw(first); +use Rex::Resource::Common; + +with qw(Rex::Resource::Role::Testable); + +has ensure_options => ( + is => 'ro', + isa => 'ArrayRef[Str]', + default => sub { [qw/present absent/] }, +); + +requires qw(present absent); + +sub process { + my ($self) = @_; + my $ensure_func = + first { $_ eq $self->config->{ensure} } @{ $self->ensure_options }; + + if ( !$ensure_func ) { + die "Error: " + . $self->config->{ensure} + . " not a valid option for 'ensure'."; + } + + my $res_available = $self->test; + + if ( ( $ensure_func eq "present" && !$res_available ) + || ( $ensure_func eq "absent" && $res_available ) + || ( $ensure_func ne "present" && $ensure_func ne "absent" ) ) + { + return $self->execute_resource_code($ensure_func); + } + else { + # resource is already deployed + return { + value => "", + changed => 0, + status => state_good, + exit_code => 0, + }; + } +} + +1; diff --git a/lib/Rex/Resource/Role/Persistable.pm b/lib/Rex/Resource/Role/Persistable.pm new file mode 100644 index 000000000..5bd78dd58 --- /dev/null +++ b/lib/Rex/Resource/Role/Persistable.pm @@ -0,0 +1,26 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::Resource::Role::Persistable; + +use strict; +use warnings; + +# VERSION + +use Moose::Role; + +with qw(Rex::Resource::Role::Ensureable); + +has ensure_options => ( + is => 'ro', + isa => 'ArrayRef[Str]', + default => sub { [qw/present absent enabled disabled/] }, +); + +requires qw(enabled disabled); + +1; diff --git a/lib/Rex/Resource/Role/Testable.pm b/lib/Rex/Resource/Role/Testable.pm new file mode 100644 index 000000000..144a59d2b --- /dev/null +++ b/lib/Rex/Resource/Role/Testable.pm @@ -0,0 +1,135 @@ +# +# (c) Jan Gehring +# +# vim: set ts=2 sw=2 tw=0: +# vim: set expandtab: + +package Rex::Resource::Role::Testable; + +use strict; +use warnings; +use Time::Out qw(timeout); +use Rex::Resource::Common; + +# VERSION + +use Moose::Role; + +requires qw(test present); + +sub process { + my ($self) = @_; + +# test if the resource is already deployed +# only if test returns false (which means resource is not deployed) we run the resource code. + my $res_available = $self->test; + if ( !$res_available ) { + my $ret = $self->execute_resource_code('present'); + return $ret; + } +} + +# +# this around() also gets called by roles that overrides +# the base process function. +around process => sub { + my $orig = shift; + my $self = shift; + + my $ret = $self->$orig(); + + if ( exists $ret->{status} && exists $ret->{changed} ) { + $self->_set_status( $ret->{status} ); + + Rex::get_current_connection()->{reporter}->report( + changed => $ret->{changed}, + resource => $self->type, + name => $self->name, + status => $ret->{status}, + message => "Resource " + . $self->type + . " status changed to " + . ( $ret->{status} || 'present' ) . ".", + ); + } + else { + die "There is no status or changed return-property for this resource: " + . $self->type . "\n"; + } + + if ( exists $self->config->{auto_die} && $self->config->{auto_die} ) { + if ( $ret->{exit_code} != 0 ) { + die "Calling autodie for " . $self->type . "[" . $self->name . "]"; + } + } + + return $ret; +}; + +# +# this code gets only called if the resource must be deployed. +sub execute_resource_code { + my ( $self, $code ) = @_; + + my $return_data = {}; + + # this new interface deprecates Rex::Hook + #### check and run before_change hook + # Rex::Hook::run_hook( + # $self->type => "before_change", + # $self->name, %{ $self->config } + # ); + ############################## + + # + # TIMEOUT: + # + # This code handles the timeout of a resource. + # timeout is now a global resource option and not limited to + # the run() resource anymore. + if ( exists $self->config->{timeout} && $self->config->{timeout} > 0 ) { + $return_data = $self->_execute_timeout($code); + } + + # + # NO SPECIAL OPTION: + # + # This code handles the execution of the resource code without any + # special options. + else { + $return_data = $self->$code; + } + + #### check and run after_change hook + # Rex::Hook::run_hook( + # $self->type => "after_change", + # $self->name, %{ $self->config } + # ); + ############################## + + # + # return all the gathered data + return $return_data; +} + +sub _execute_timeout { + my ( $self, $code ) = @_; + + my $ret = timeout $self->config->{timeout}, sub { + return $self->$code; + }; + + if ( $ret && ref $ret eq "HASH" ) { + return $ret; + } + + return { + value => $@, + exit_code => $?, + changed => 0, + status => state_timeout, + }; + +} + +1; diff --git a/lib/Rex/Resource/firewall.pm b/lib/Rex/Resource/firewall.pm index 29f4cdb57..c2002a642 100644 --- a/lib/Rex/Resource/firewall.pm +++ b/lib/Rex/Resource/firewall.pm @@ -75,76 +75,56 @@ my $__provider = { default => "Rex::Resource::firewall::Provider::iptables", }; =cut -resource "firewall", { export => 1 }, sub { - my $rule_name = resource_name; - - my $rule_config = { - action => param_lookup("action"), - ensure => param_lookup( "ensure", "present" ), - proto => param_lookup( "proto", undef ), - source => param_lookup( "source", undef ), - destination => param_lookup( "destination", undef ), - port => param_lookup( "port", undef ), - app => param_lookup( "app", undef ), - sport => param_lookup( "sport", undef ), - sapp => param_lookup( "sapp", undef ), - dport => param_lookup( "dport", undef ), - dapp => param_lookup( "dapp", undef ), - tcp_flags => param_lookup( "tcp_falgs", undef ), - chain => param_lookup( "chain", "input" ), - table => param_lookup( "table", "filter" ), - iniface => param_lookup( "iniface", undef ), - outiface => param_lookup( "outiface", undef ), - reject_with => param_lookup( "reject_with", undef ), - logging => param_lookup( "logging", undef ), # overall logging - log => param_lookup( "log", undef ), # logging for rule - log_level => param_lookup( "log_level", undef ), # logging for rule - log_prefix => param_lookup( "log_prefix", undef ), - state => param_lookup( "state", undef ), - ip_version => param_lookup( "ip_version", -4 ), - }; - - my $provider = - param_lookup( "provider", case ( lc(operating_system), $__provider ) ); - - if ( $provider !~ m/::/ ) { - $provider = "Rex::Resource::firewall::Provider::$provider"; - } - +resource "firewall", { + export => 1, + params_list => [ + name => { + isa => 'Str', + default => sub { shift } + }, + ensure => { + isa => 'Str', + default => sub { "present" } + }, + action => { isa => 'Str | Undef', default => undef }, + proto => { isa => 'Str | Undef', default => "tcp" }, + source => { isa => 'Str | Undef', default => undef }, + destination => { isa => 'Str | Undef', default => undef }, + port => { isa => 'Int | Undef', default => undef }, + source => { isa => 'Str | Undef', default => undef }, + app => { isa => 'Str | Undef', default => undef }, + sport => { isa => 'Int | Undef', default => undef }, + dport => { + isa => 'Int | Undef', + default => sub { my ( $name, %p ) = @_; return $p{port}; }, + }, + dapp => { isa => 'Str | Undef', default => undef }, + tcp_flags => { isa => 'ArrayRef | Undef', default => undef }, + chain => { isa => 'Str | Undef', default => "INPUT" }, + table => { isa => 'Str | Undef', default => undef }, + iniface => { isa => 'Str | Undef', default => undef }, + outiface => { isa => 'Str | Undef', default => undef }, + reject_with => { isa => 'Str | Undef', default => undef }, + logging => { isa => 'Str | Undef', default => undef }, + log => { isa => 'Str | Undef', default => undef }, + log_level => { isa => 'Str | Undef', default => undef }, + log_prefix => { isa => 'Str | Undef', default => undef }, + state => { isa => 'Str | Undef', default => undef }, + ip_version => { isa => 'Str | Undef', default => "-4" }, + ], + }, + sub { + my ( $name, %args ) = @_; + + my $provider = resolve_resource_provider( $args{provider} + || case ( lc(operating_system), $__provider ) ); + + # TODO define provider type automatically. $provider->require; - my $provider_o = $provider->new(); - - my $changed = 0; - if ( my $logging = $rule_config->{logging} ) { - if ( $provider_o->logging($logging) ) { - emit changed, "Firewall logging updated."; - } - } - elsif ( $rule_config->{ensure} eq "present" ) { - if ( $provider_o->present($rule_config) ) { - emit created, "Firewall rule created."; - } - } - elsif ( $rule_config->{ensure} eq "absent" ) { - if ( $provider_o->absent($rule_config) ) { - emit removed, "Firewall rule removed."; - } - } - elsif ( $rule_config->{ensure} eq "disabled" ) { - if ( $provider_o->disable($rule_config) ) { - emit changed, "Firewall disabled."; - } - } - elsif ( $rule_config->{ensure} eq "enabled" ) { - if ( $provider_o->enable($rule_config) ) { - emit changed, "Firewall enabled."; - } - } - else { - die "Error: $rule_config->{ensure} not a valid option for 'ensure'."; - } - -}; + my $provider_o = + $provider->new( type => "firewall", name => $name, config => \%args ); + $provider_o->process; + }; =back diff --git a/lib/Rex/Resource/firewall/Provider/base.pm b/lib/Rex/Resource/firewall/Provider/base.pm index 48565ec49..f0a69e0bc 100644 --- a/lib/Rex/Resource/firewall/Provider/base.pm +++ b/lib/Rex/Resource/firewall/Provider/base.pm @@ -13,39 +13,8 @@ use warnings; use Data::Dumper; -sub new { - my $that = shift; - my $proto = ref($that) || $that; - my $self = {@_}; +use Moose; - bless( $self, $proto ); - - return $self; -} - -sub present { - my ( $self, $rule_config ) = @_; - die "Must be implemented by provider."; -} - -sub absent { - my ( $self, $rule_config ) = @_; - die "Must be implemented by provider."; -} - -sub enable { - my ( $self, $rule_config ) = @_; - Rex::Logger::debug("enable: Not implemented by provider."); -} - -sub disable { - my ( $self, $rule_config ) = @_; - Rex::Logger::debug("disable: Not implemented by provider."); -} - -sub logging { - my ( $self, $rule_config ) = @_; - Rex::Logger::debug("logging: Not implemented by provider."); -} +extends qw(Rex::Resource::Provider); 1; diff --git a/lib/Rex/Resource/firewall/Provider/iptables.pm b/lib/Rex/Resource/firewall/Provider/iptables.pm index 93e1fe7e5..2e7236955 100644 --- a/lib/Rex/Resource/firewall/Provider/iptables.pm +++ b/lib/Rex/Resource/firewall/Provider/iptables.pm @@ -11,96 +11,97 @@ use warnings; # VERSION +use Moose; + +extends qw(Rex::Resource::firewall::Provider::base); +with qw(Rex::Resource::Role::Ensureable); + use Rex::Commands::Iptables; use Rex::Helper::Run; +use Rex::Resource::Common; + use Data::Dumper; -use base qw(Rex::Resource::firewall::Provider::base); -sub new { - my $that = shift; - my $proto = ref($that) || $that; - my $self = $proto->SUPER::new(@_); +sub test { + my ($self) = @_; + + my $rule_config = $self->config; + my @iptables_rule = $self->_build_iptables_array("A"); - bless( $self, $proto ); + my $exists = + Rex::Commands::Iptables::_rule_exists( $rule_config->{ip_version}, + @iptables_rule ); - return $self; + if ( $self->config->{ensure} eq "absent" && $exists ) { + return 0; + } + elsif ( $self->config->{ensure} eq "present" && !$exists ) { + return 0; + } + + return 1; } sub present { - my ( $self, $rule_config ) = @_; + my ($self) = @_; - my @iptables_rule = (); + my @iptables_rule = $self->_build_iptables_array("A"); + my $exit_code = 0; + eval { + iptables( $self->config->{ip_version}, @iptables_rule ); + 1; + } or do { + $exit_code = 1; + }; + + return { + value => "", + exit_code => $exit_code, + changed => 1, + status => ( $exit_code == 0 ? state_changed : state_failed ), + }; +} - $rule_config->{dport} ||= $rule_config->{port}; - $rule_config->{proto} ||= 'tcp'; - $rule_config->{chain} ||= 'INPUT'; - $rule_config->{ip_version} ||= -4; +sub absent { + my ($self) = @_; - if ( $rule_config->{source} - && $rule_config->{source} !~ m/\/(\d+)$/ - && $self->_version()->[0] >= 1 - && $self->_version()->[1] >= 4 ) - { - $rule_config->{source} .= "/32"; - } + my @iptables_rule = $self->_build_iptables_array("D"); + my $exit_code = 0; + eval { + iptables( $self->config->{ip_version}, @iptables_rule ); + 1; + } or do { + $exit_code = 1; + }; + + return { + value => "", + exit_code => $exit_code, + changed => 1, + status => ( $exit_code == 0 ? state_changed : state_failed ), + }; +} - push( @iptables_rule, t => $rule_config->{table} ) - if ( defined $rule_config->{table} ); - push( @iptables_rule, A => uc( $rule_config->{chain} ) ) - if ( defined $rule_config->{chain} ); - push( @iptables_rule, p => $rule_config->{proto} ) - if ( defined $rule_config->{proto} ); - push( @iptables_rule, m => $rule_config->{proto} ) - if ( defined $rule_config->{proto} ); - push( @iptables_rule, s => $rule_config->{source} ) - if ( defined $rule_config->{source} ); - push( @iptables_rule, d => $rule_config->{destination} ) - if ( defined $rule_config->{destination} ); - push( @iptables_rule, sport => $rule_config->{sport} ) - if ( defined $rule_config->{sport} ); - push( @iptables_rule, dport => $rule_config->{dport} ) - if ( defined $rule_config->{dport} ); - push( @iptables_rule, "tcp-flags" => $rule_config->{tcp_flags} ) - if ( defined $rule_config->{tcp_flags} ); - push( @iptables_rule, "i" => $rule_config->{iniface} ) - if ( defined $rule_config->{iniface} ); - push( @iptables_rule, "o" => $rule_config->{outiface} ) - if ( defined $rule_config->{outiface} ); - push( @iptables_rule, "reject-with" => $rule_config->{reject_with} ) - if ( defined $rule_config->{reject_with} ); - push( @iptables_rule, "log-level" => $rule_config->{log_level} ) - if ( defined $rule_config->{log_level} ); - push( @iptables_rule, "log-prefix" => $rule_config->{log_prefix} ) - if ( defined $rule_config->{log_prefix} ); - push( @iptables_rule, "state" => $rule_config->{state} ) - if ( defined $rule_config->{state} ); - push( @iptables_rule, j => uc( $rule_config->{action} ) ) - if ( defined $rule_config->{action} ); +sub _version { + my ($self) = @_; + if ( exists $self->{__version__} ) { return $self->{__version__} } - if ( - !Rex::Commands::Iptables::_rule_exists( - $rule_config->{ip_version}, - @iptables_rule - ) - ) - { - iptables( $rule_config->{ip_version}, @iptables_rule ); - return 1; - } + my $version = i_run "iptables --version"; + $version =~ s/^.*\sv(\d+\.\d+\.\d+)/$1/; - return 0; -} + $self->{__version__} = [ split( /\./, $version ) ]; -sub absent { - my ( $self, $rule_config ) = @_; + Rex::Logger::debug( + "Got iptables version: " . join( ", ", @{ $self->{__version__} } ) ); - my @iptables_rule = (); + return $self->{__version__}; +} - $rule_config->{dport} ||= $rule_config->{port}; - $rule_config->{proto} ||= 'tcp'; - $rule_config->{chain} ||= 'INPUT'; +sub _build_iptables_array { + my ( $self, $type ) = @_; + my $rule_config = $self->config; - $rule_config->{ip_version} ||= -4; + my @iptables_rule = (); if ( $rule_config->{source} && $rule_config->{source} !~ m/\/(\d+)$/ @@ -112,14 +113,14 @@ sub absent { push( @iptables_rule, t => $rule_config->{table} ) if ( defined $rule_config->{table} ); - push( @iptables_rule, D => uc( $rule_config->{chain} ) ) + push( @iptables_rule, $type => uc( $rule_config->{chain} ) ) if ( defined $rule_config->{chain} ); - push( @iptables_rule, s => $rule_config->{source} ) - if ( defined $rule_config->{source} ); push( @iptables_rule, p => $rule_config->{proto} ) if ( defined $rule_config->{proto} ); push( @iptables_rule, m => $rule_config->{proto} ) if ( defined $rule_config->{proto} ); + push( @iptables_rule, s => $rule_config->{source} ) + if ( defined $rule_config->{source} ); push( @iptables_rule, d => $rule_config->{destination} ) if ( defined $rule_config->{destination} ); push( @iptables_rule, sport => $rule_config->{sport} ) @@ -143,33 +144,7 @@ sub absent { push( @iptables_rule, j => uc( $rule_config->{action} ) ) if ( defined $rule_config->{action} ); - if ( - Rex::Commands::Iptables::_rule_exists( - $rule_config->{ip_version}, - @iptables_rule - ) - ) - { - iptables( $rule_config->{ip_version}, @iptables_rule ); - return 1; - } - - return 0; -} - -sub _version { - my ($self) = @_; - if ( exists $self->{__version__} ) { return $self->{__version__} } - - my $version = i_run "iptables --version", fail_ok => 1; - $version =~ s/^.*\sv(\d+\.\d+\.\d+)/$1/; - - $self->{__version__} = [ split( /\./, $version ) ]; - - Rex::Logger::debug( - "Got iptables version: " . join( ", ", @{ $self->{__version__} } ) ); - - return $self->{__version__}; + return @iptables_rule; } 1; diff --git a/lib/Rex/Resource/firewall/Provider/ufw.pm b/lib/Rex/Resource/firewall/Provider/ufw.pm index 4970ca9a8..31e04d3dd 100644 --- a/lib/Rex/Resource/firewall/Provider/ufw.pm +++ b/lib/Rex/Resource/firewall/Provider/ufw.pm @@ -12,11 +12,15 @@ use warnings; # VERSION +use Moose; + use Data::Dumper; use Rex::Commands::Run; use Rex::Helper::Run; +use Rex::Resource::Common; -use base qw(Rex::Resource::firewall::Provider::base); +extends qw(Rex::Resource::firewall::Provider::base); +with qw(Rex::Resource::Role::EnableDisable); my %__action_map = ( accept => "allow", @@ -27,41 +31,118 @@ my %__action_map = ( limit => "limit", ); -sub new { - my $that = shift; - my $proto = ref($that) || $that; - my $self = $proto->SUPER::new(@_); +sub test { + my ($self) = @_; - bless( $self, $proto ); + # currently we don't have detection if the rule already exists + # todo: detect if rule exists - return $self; + return 0; } sub present { - my ( $self, $rule_config ) = @_; + my ($self) = @_; + + my $rule_config = $self->config; my @ufw_params = $self->_generate_rule_array($rule_config); - return $self->_ufw_rule( grep { defined } @ufw_params ); + my $changed = 0; + my $exit_code = 0; + my $error = ""; + + eval { + $changed = $self->_ufw_rule( grep { defined } @ufw_params ); + 1; + } or do { + $exit_code = 1; + $error = $@; + }; + + return { + value => $error, + exit_code => $exit_code, + changed => $changed, + status => ( $exit_code == 0 ? state_changed : state_failed ), + }; } sub absent { - my ( $self, $rule_config ) = @_; + my ($self) = @_; + + my $rule_config = $self->config; $rule_config->{delete} = 1; my @ufw_params = $self->_generate_rule_array($rule_config); - return $self->_ufw_rule( grep { defined } @ufw_params ); + my $changed = 0; + my $exit_code = 0; + my $error = ""; + + eval { + $changed = $self->_ufw_rule( grep { defined } @ufw_params ); + 1; + } or do { + $exit_code = 1; + $error = $@; + }; + + return { + value => $error, + exit_code => $exit_code, + changed => $changed, + status => ( $exit_code == 0 ? state_changed : state_failed ), + }; } sub enable { - my ( $self, $rule_config ) = @_; - return $self->_ufw_disable_enable("enable"); + my ($self) = @_; + + my $rule_config = $self->config; + + my $changed = 0; + my $exit_code = 0; + my $error = ""; + + eval { + $changed = $self->_ufw_disable_enable("enable"); + 1; + } or do { + $exit_code = 1; + $error = $@; + }; + + return { + value => $error, + exit_code => $exit_code, + changed => $changed, + status => ( $exit_code == 0 ? state_changed : state_failed ), + }; } sub disable { - my ( $self, $rule_config ) = @_; - return $self->_ufw_disable_enable("disable"); + my ($self) = @_; + + my $rule_config = $self->config; + + my $changed = 0; + my $exit_code = 0; + my $error = ""; + + eval { + $changed = $self->_ufw_disable_enable("disable"); + 1; + } or do { + $exit_code = 1; + $error = $@; + }; + + return { + value => $error, + exit_code => $exit_code, + changed => $changed, + status => ( $exit_code == 0 ? state_changed : state_failed ), + }; } sub logging { @@ -236,7 +317,7 @@ sub _ufw_exec { $cmd = "ufw $cmd"; if ( can_run("ufw") ) { - my ( $output, $err ) = i_run $cmd, sub { @_ }, fail_ok => 1; + my ( $output, $err ) = i_run $cmd, sub { @_ }; if ( $? != 0 ) { Rex::Logger::info( "Error running ufw command: $cmd, received $err", @@ -255,6 +336,8 @@ sub _ufw_exec { sub _generate_rule_array { my ( $self, $rule_config ) = @_; + print STDERR Dumper($rule_config); + my $action = $__action_map{ $rule_config->{action} } or die qq(Unknown action "$rule_config->{action}" for UFW provider); $rule_config->{dport} ||= $rule_config->{port};