File Coverage

blib/lib/AnsibleModule.pm
Criterion Covered Total %
statement 42 112 37.5
branch 3 76 3.9
condition 0 21 0.0
subroutine 14 21 66.6
pod 3 3 100.0
total 62 233 26.6


line stmt bran cond sub pod time code
1             package AnsibleModule;
2              
3 1     1   593 use Mojo::Base -base;
  1         3  
  1         6  
4              
5             our $VERSION = '0.4';
6              
7             =for comment
8              
9             We want JSON
10             WANT_JSON
11              
12             =cut
13              
14 1     1   200 use Mojo::JSON qw/decode_json encode_json/;
  1         2  
  1         49  
15 1     1   6 use Mojo::File qw/path/;
  1         1  
  1         37  
16 1     1   289 use POSIX qw/locale_h/;
  1         4717  
  1         6  
17 1     1   1184 use Carp qw/croak/;
  1         2  
  1         1666  
18              
19              
20             has argument_spec => sub { +{} };
21             has bypass_checks => sub {0};
22             has no_log => sub {0};
23             has check_invalid_arguments => sub {1};
24             has mutually_exclusive => sub { [] };
25             has required_together => sub { [] };
26             has required_one_of => sub { [] };
27             has supports_check_mode => sub {0};
28             has required_if => sub { [] };
29             has aliases => sub { {} };
30              
31              
32             has params => sub {
33             my $self = shift;
34             return {} unless @ARGV;
35             my $args = path($ARGV[0])->slurp;
36             my $json = decode_json($args);
37             return $json if defined $json;
38             my $params = {};
39             for my $arg (split $args) {
40             my ($k, $v) = split '=', $arg;
41             $self->fail_json(
42             {msg => 'This module requires key=value style argument: ' . $arg})
43             unless defined $v;
44             $self->fail_json({msg => "Duplicate parameter: $k"})
45             if exists $params->{$k};
46             $params->{$k} = $v;
47             }
48             return $params;
49             };
50              
51             has _legal_inputs => sub {
52             {CHECKMODE => 1, NO_LOG => 1};
53             };
54              
55             has check_mode => sub {0};
56              
57             sub new {
58 1     1 1 526 my $self = shift->SUPER::new(@_);
59 1         35 setlocale(LC_ALL, "");
60 1         3 $self->_check_argument_spec();
61 1         3 $self->_check_params();
62 1 50       3 unless ($self->bypass_checks) {
63 1         3 $self->_check_arguments();
64 1         2 $self->_check_required_together();
65 1         3 $self->_check_required_one_of();
66 1         3 $self->_check_required_if();
67             }
68 1 50       3 $self->_log_invocation() unless $self->no_log();
69 1         2 $self->_set_cwd();
70 1         3 return $self;
71             }
72              
73             sub exit_json {
74 0     0 1 0 my $self = shift;
75 0 0       0 my $args = ref $_[0] ? $_[0] : {@_};
76 0   0     0 $args->{changed} //= 0;
77 0         0 print encode_json($args);
78 0         0 exit 0;
79             }
80              
81             sub fail_json {
82 0     0 1 0 my $self = shift;
83 0 0       0 my $args = ref $_[0] ? $_[0] : {@_};
84             croak("Implementation error -- msg to explain the error is required")
85 0 0       0 unless defined($args->{'msg'});
86 0         0 $args->{failed} = 1;
87 0         0 print encode_json($args);
88 0         0 exit 1;
89             }
90              
91             sub _check_argument_spec {
92 1     1   2 my $self = shift;
93 1         2 for my $arg (keys(%{$self->argument_spec})) {
  1         3  
94 0         0 $self->_legal_inputs->{$arg}++;
95 0         0 my $spec = $self->argument_spec->{$arg};
96              
97             # Check required
98             $self->fail_json(msg =>
99             "internal error: required and default are mutually exclusive for $arg")
100 0 0 0     0 if defined $spec->{default} && $spec->{required};
101              
102             # Check aliases
103 0   0     0 $spec->{aliases} //= [];
104             $self->fail_json({msg => "internal error: aliases must be an arrayref"})
105 0 0 0     0 unless ref $spec->{aliases} && ref $spec->{aliases} eq 'ARRAY';
106              
107             # Set up aliases
108 0         0 for my $alias (@{$spec->{aliases}}) {
  0         0  
109 0         0 $self->_legal_inputs->{$alias}++;
110 0         0 $self->aliases->{$alias} = $arg;
111             $self->params->{$arg} = $self->params->{$alias}
112 0 0       0 if exists $self->params->{$alias};
113             }
114              
115             # Fallback to default value
116 0 0 0     0 $self->params->{$arg} //= $spec->{default} if exists $spec->{default};
117             }
118             }
119              
120             sub _check_arguments {
121 1     1   2 my $self = shift;
122              
123             # Check for missing required params
124 1         3 my @missing = ();
125 1         1 for my $arg (keys(%{$self->argument_spec})) {
  1         3  
126 0         0 my $spec = $self->argument_spec->{$arg};
127 0 0 0     0 push(@missing, $arg) if $spec->{required} && !$self->params->{$arg};
128 0   0     0 my $choices = $spec->{choices} || [];
129 0 0       0 $self->fail_json(msg => 'error: choices must be a list of values')
130             unless ref $choices eq 'ARRAY';
131 0 0 0     0 if ($self->params->{$arg} && @{$choices}) {
  0         0  
132 0 0       0 if (!grep { $self->params->{$arg} eq $_ } $choices) {
  0         0  
133             $self->fail_json(msg => "value of $arg must be one of: "
134             . join(", ", $choices)
135             . ", got: "
136 0         0 . $self->params->{$arg});
137             }
138             }
139              
140             # Try to wrangle types. We don't care as much as python does about diff scalars.
141 0 0       0 if ($spec->{type}) {
142 0 0       0 if ($spec->{type} eq 'dict') {
    0          
    0          
143             $self->fail_json(msg => "Could not serialize $arg to dict")
144             unless defined(
145 0 0       0 $self->params->{$arg} = $self->_to_dict($self->params->{$arg})
146             );
147              
148             }
149             elsif ($spec->{type} eq 'list') {
150             $self->fail_json(msg => "Could not serialize $arg to list")
151             unless defined(
152 0 0       0 $self->params->{$arg} = $self->_to_list($self->params->{$arg})
153             );
154              
155             }
156             elsif ($spec->{type} eq 'bool') {
157             $self->fail_json(msg => "Could not serialize $arg to bool")
158             unless defined(
159 0 0       0 $self->params->{$arg} = $self->_to_list($self->params->{$arg})
160             );
161             }
162             else {
163             $self->fail_json(msg => "Could not serialize $arg to bool")
164 0 0       0 if ref $self->params->{$arg}
165              
166             }
167             }
168             }
169 1 50       7 $self->fail_json(msg => "missing required arguments: " . join(" ", @missing))
170             if @missing;
171             }
172              
173              
174             sub _to_dict {
175 0     0   0 my ($self, $val) = @_;
176              
177             # if it's a ref we only accept hashes.
178 0 0       0 if (ref $val) {
    0          
    0          
179 0 0       0 return $val if ref $val eq 'HASH';
180 0         0 return;
181              
182             # json literal
183             }
184             elsif ($val =~ /^{/) {
185 0         0 my $res = decode_json($val);
186 0 0       0 return $res if defined $res;
187             }
188             elsif ($val =~ /=/) {
189 0         0 my @lines = split(/\s*,\s*/, $val);
190 0         0 return {map split(/\s*=\s*/), @lines};
191             }
192 0         0 return;
193             }
194              
195             sub _to_list {
196 0     0   0 my ($self, $val) = @_;
197              
198             # if it's a ref we only accept arrays.
199 0 0       0 if (ref $val) {
200 0 0       0 return $val if ref $val eq 'ARRAY';
201 0         0 return;
202             }
203              
204             # single element or split if comma separated
205 0         0 return [split /[\s,]+/, $val];
206              
207             }
208              
209             sub _to_bool {
210 0     0   0 my ($self, $val) = @_;
211 0 0       0 return 1 if grep { lc($val) eq lc($_) } qw/yes on true 1/;
  0         0  
212 0 0       0 return 0 if grep { lc($val) eq lc($_) } qw/no off false 1/;
  0         0  
213 0         0 return;
214             }
215              
216              
217             sub _check_required_together {
218 1     1   13 my $self = shift;
219             }
220              
221             sub _check_required_one_of {
222 1     1   8 my $self = shift;
223             }
224              
225             sub _check_required_if {
226 1     1   2 my $self = shift;
227             }
228              
229             sub _check_argument_types {
230 0     0   0 my $self = shift;
231             }
232              
233       1     sub _log_invocation {
234             }
235              
236             sub _set_cwd {
237 1     1   1 my $self = shift;
238             }
239              
240             sub _check_params {
241 1     1   2 my $self = shift;
242 1         1 for my $param (keys %{$self->params}) {
  1         3  
243 0 0         if ($self->check_invalid_arguments) {
244             $self->fail_json(msg => "unsupported parameter for module: $param")
245 0 0         unless $self->_legal_inputs->{$param};
246             }
247 0           my $val = $self->params->{$param};
248 0 0         $self->no_log(!!$val) if $param eq 'NO_LOG';
249 0 0         if ($param eq 'CHECKMODE') {
250 0 0         $self->exit_json(
251             skipped => 1,
252             msg => "remote module does not support check mode"
253             ) unless $self->supports_check_mode;
254 0           $self->check_mode(1);
255             }
256              
257 0 0         $self->no_log(!!$val) if $param eq '_ansible_no_log';
258             }
259             }
260              
261             sub _count_terms {
262 0     0     my ($self, $terms) = @_;
263 0           my $count;
264 0           for my $term (@$terms) {
265 0 0         $count++ if $self->params->{$terms};
266             }
267             }
268              
269             1;
270              
271             =head1 NAME
272              
273             AnsibleModule - Port of AnsibleModule helper from Ansible distribution
274              
275             =head1 SYNOPSIS
276              
277             my $pkg_mod=AnsibleModule->new(argument_spec=> {
278             name => { aliases => 'pkg' },
279             state => { default => 'present', choices => [ 'present', 'absent'],
280             list => {}
281             },
282             required_one_of => [ qw/ name list / ],
283             mutually_exclusive => [ qw/ name list / ],
284             supports_check_mode => 1,
285             );
286             ...
287             $pkg_mod->exit_json(changed => 1, foo => 'bar');
288              
289             =head1 DESCRIPTION
290              
291             This is a helper class for building ansible modules in Perl. It's a straight port of the AnsibleModule class
292             that ships with the ansible distribution.
293              
294             =head1 ATTRIBUTES
295              
296             =head2 argument_spec
297              
298             Argument specification. Takes a hashref of arguments, along with a set of parameters for each.
299              
300             The argument specification for your module.
301              
302             =head2 bypass_checks
303              
304             =head2 no_log
305              
306             =head2 check_invalid_arguments
307              
308             =head2 mutually_exclusive
309              
310             =head2 required_together
311              
312             =head2 required_one_of
313              
314             =head2 add_file_common_args
315              
316             =head2 supports_check_mode
317              
318             =head2 required_if
319              
320             =head1 METHODS
321              
322             =head2 exit_json $args
323              
324             Exit with a json msg. changed will default to false.
325              
326             =head2 fail_json $args
327              
328             Exit with a failure. msg is required.
329              
330             =cut