File Coverage

blib/lib/App/Ylastic/CostAgent.pm
Criterion Covered Total %
statement 79 142 55.6
branch 13 24 54.1
condition 7 15 46.6
subroutine 20 29 68.9
pod 0 2 0.0
total 119 212 56.1


line stmt bran cond sub pod time code
1             #
2             # This file is part of App-Ylastic-CostAgent
3             #
4             # This software is Copyright (c) 2011 by David Golden.
5             #
6             # This is free software, licensed under:
7             #
8             # The Apache License, Version 2.0, January 2004
9             #
10 3     3   174799 use 5.010;
  3         11  
  3         106  
11 3     3   22 use strict;
  3         5  
  3         82  
12 3     3   15 use warnings;
  3         6  
  3         134  
13 3     3   2822 use utf8;
  3         32  
  3         18  
14              
15             package App::Ylastic::CostAgent;
16             BEGIN {
17 3     3   287 $App::Ylastic::CostAgent::VERSION = '0.006';
18             }
19             # ABSTRACT: Perl port of the Ylastic Cost Agent for Amazon Web Services
20              
21             # Dependencies
22 3     3   73892 use autodie 2.00;
  3         185277  
  3         27  
23 3     3   396781 use Archive::Zip qw( :CONSTANTS );
  3         303742  
  3         487  
24 3     3   30 use Carp qw/croak/;
  3         8  
  3         155  
25 3     3   3734 use Config::Tiny;
  3         3569  
  3         126  
26 3     3   23 use File::Spec::Functions qw/catfile/;
  3         5  
  3         219  
27 3     3   19 use File::Temp ();
  3         6  
  3         65  
28 3     3   3822 use Log::Dispatchouli 2;
  3         147946  
  3         96  
29 3     3   10458 use Mozilla::CA; # force dependency to trigger SSL validation
  3         1142  
  3         210  
30 3     3   5200 use IO::Socket::SSL; # force dependency to trigger SSL support
  3         316315  
  3         31  
31 3     3   4687 use Time::Piece;
  3         25472  
  3         20  
32 3     3   3180 use Time::Piece::Month;
  3         11179  
  3         105  
33 3     3   4734 use WWW::Mechanize;
  3         550770  
  3         171  
34              
35 3         20 use Object::Tiny qw(
36             accounts
37             config_file
38             dir
39             logger
40             mech
41             upload
42             ylastic_id
43 3     3   3484 );
  3         1142  
44              
45             my %URL = (
46             ylastic_service_list => "http://ylastic.com/cost_services.list",
47             ylastic_upload_form => "http://ylastic.com/usage_upload.html",
48             aws_usage_report_form => "https://aws-portal.amazon.com/gp/aws/developer/account/index.html?ie=UTF8&action=usage-report",
49             );
50              
51             #--------------------------------------------------------------------------#
52             # new -- constructor
53             #
54             # Parameters for new:
55             # * config_file -- path to a .ini style configuration file (required)
56             # * dir -- a directory to hold downloaded data (defaults to a tempdir)
57             # * logger -- a Log::Dispatchouli object (defaults to a null logger)
58             # * upload -- whether to upload data to Ylastic (default is false)
59             #--------------------------------------------------------------------------#
60              
61             sub new {
62 8     8 0 6338 my $class = shift;
63 8         73 my $self = $class->SUPER::new( @_ );
64              
65 8 100 66     319 croak __PACKAGE__ . " requires a valid 'config_file' argument\n"
66             unless $self->config_file && -r $self->config_file;
67              
68 7   33     533 $self->{logger} ||= Log::Dispatchouli->new({ident => __PACKAGE__, to_self => 1});
69 7   33     12743 $self->{dir} ||= File::Temp::tempdir();
70 7         3480 $self->_parse_config;
71              
72 6         31 return $self;
73             }
74              
75             #--------------------------------------------------------------------------#
76             # run -- downloads and possibly uploads data for all accounts from config
77             #--------------------------------------------------------------------------#
78              
79             sub run {
80 0     0 0 0 my $self = shift;
81              
82 0         0 for my $account ( @{ $self->accounts } ) {
  0         0  
83 0         0 my $zipfile = $self->_download_usage( $account );
84 0 0       0 $self->_upload_usage( $account, $zipfile )
85             if $self->upload;
86             }
87              
88 0         0 return 0;
89             }
90              
91             #--------------------------------------------------------------------------#
92             # private
93             #--------------------------------------------------------------------------#
94              
95             sub _do_aws_login {
96 0     0   0 my ($self, $id,$user, $pass) = @_;
97 0         0 $self->mech->get($URL{aws_usage_report_form});
98 0         0 $self->mech->submit_form(
99             form_name => 'signIn',
100             fields => {
101             email => $user,
102             password => $pass,
103             }
104             );
105 0         0 $self->logger->log_debug(["Logged into AWS for account %s as %s", $id, $user]);
106             }
107              
108             sub _download_usage {
109 0     0   0 my ($self, $account) = @_;
110 0         0 my ($id, $user, $pass) = @$account;
111 0         0 $self->_initialize_mech;
112 0         0 $self->_do_aws_login( $id, $user, $pass );
113              
114 0         0 my $zip = Archive::Zip->new;
115              
116 0         0 for my $service ( @{ $self->_service_list } ) {
  0         0  
117 0         0 my $usage = $self->_get_service_usage($id, $service);
118 0 0       0 if ( length $usage > 70 ) {
119 0         0 $self->logger->log_debug("Got $service data for account $id");
120 0         0 my $filename = sprintf("%s_%s_%s\.csv", $self->ylastic_id, $id, $service);
121 0         0 my $member = $zip->addString( $usage => $filename );
122 0         0 $member->desiredCompressionLevel( 9 );
123             }
124             else {
125 0         0 $self->logger->log_debug("No $service data available for account $id");
126             }
127             }
128              
129             # write zipfile
130 0         0 my $zipname = sprintf("%s_%s_aws_usage.zip", $self->ylastic_id, $id);
131 0         0 my $zippath = catfile($self->dir, $zipname);
132 0         0 $zip->writeToFileNamed( $zippath );
133              
134 0         0 $self->logger->log(["Downloaded AWS usage reports for account %s", $id]);
135              
136 0         0 return $zippath;
137             }
138              
139             sub _end_date {
140 0     0   0 state $end_date = Time::Piece::Month->new(
141             Time::Piece->new()
142             )->next_month->start;
143 0         0 return $end_date;
144             }
145              
146             sub _get_service_usage {
147 0     0   0 my ($self, $id, $service) = @_;
148 0         0 my $usage;
149              
150 0         0 ATTEMPT: for ( 0 .. 2 ) {
151 0         0 eval {
152 0         0 $self->mech->get($URL{aws_usage_report_form});
153              
154 0         0 $self->mech->submit_form(
155             form_name => 'usageReportForm',
156             fields => {
157             productCode => $service,
158             }
159             );
160              
161 0         0 my $action = 'download-usage-report-csv';
162 0         0 my $form = $self->mech->form_name('usageReportForm');
163 0 0 0     0 return unless $form && $form->find_input($action);
164              
165 0         0 $self->mech->submit_form(
166             form_name => 'usageReportForm',
167             button => $action,
168             fields => {
169             productCode => $service,
170             timePeriod => 'aws-portal-custom-date-range',
171             startYear => $self->_start_date->year,
172             startMonth => $self->_start_date->mon,
173             startDay => $self->_start_date->mday,
174             endYear => $self->_end_date->year,
175             endMonth => $self->_end_date->mon,
176             endDay => $self->_end_date->mday,
177             periodType => 'days',
178             }
179             );
180             };
181 0 0       0 if ( $@ ) {
182 0         0 $self->logger->log_debug("Error downloading $service for account $id: $@");
183             }
184             else {
185 0         0 $usage = $self->mech->content;
186 0         0 last ATTEMPT;
187             }
188             }
189              
190 0         0 return $usage;
191             }
192              
193             sub _initialize_mech {
194 0     0   0 my $self = shift;
195 0         0 $self->{mech} = WWW::Mechanize->new(
196             quiet => 0,
197             on_error => \&Carp::croak
198             );
199 0         0 $self->mech->agent_alias("Linux Mozilla");
200 0         0 $self->mech->default_header('Accept' => 'text/html, application/xml, */*');
201             }
202              
203             sub _parse_config {
204 7     7   13 my $self = shift;
205 7 50       223 my $config = Config::Tiny->read( $self->config_file )
206             or croak Config::Tiny->errstr;
207              
208 7 100       477243 $self->{ylastic_id} = $config->{_}{ylastic_id}
209             or croak $self->config_file . " does not define 'ylastic_id'";
210              
211 6         13 my @accounts;
212 6         29 for my $k ( keys %$config ) {
213 12 100       33 next if $k eq "_"; # ski config root
214 6 100       32 unless ( $k =~ /^(?:\d{12}|\d{4}-\d{4}-\d{4})$/ ) {
215 1         10 warn "Invalid AWS ID '$k'. Skipping it.";
216 1         53 next;
217             }
218 10 100       39 my ($user, $pass) = map { defined $_ ? $_ : '' }
  10         37  
219 5         14 map { $config->{$k}{$_} } qw/user pass/;
220 5 100 100     38 unless ( length $user && length $pass ) {
221 3         34 warn "Invalid user/password for $k. Skipping it.";
222 3         182 next;
223             }
224 2         8 $k =~ s{-}{}g;
225 2         12 push @accounts, [$k, $user, $pass];
226             }
227 6         19 $self->{accounts} = \@accounts;
228 6         201 $self->logger->log_debug(["Parsed config_file %s", $self->config_file]);
229 6         173 return;
230             }
231              
232             sub _service_list {
233 0     0     my $self = shift;
234 0 0         return $self->{services} if $self->{services};
235 0           my $list = $self->mech->get($URL{ylastic_service_list})->decoded_content;
236 0           chomp $list;
237 0           return $self->{services} = [split q{,}, $list];
238             }
239              
240             sub _start_date {
241 0     0     state $start_date = Time::Piece::Month->new("2010-01-01")->start;
242 0           return $start_date;
243             }
244              
245             sub _upload_usage {
246 0     0     my ($self, $account, $zipfile) = @_;
247 0           $self->_initialize_mech;
248 0           $self->mech->get($URL{ylastic_upload_form});
249 0           $self->mech->submit_form(
250             form_name => 'upload',
251             fields => {
252             file1 => $zipfile,
253             }
254             );
255 0           $self->logger->log(["Uploaded usage reports to Ylastic for account %s",$account->[0]]);
256 0           return;
257             }
258              
259             1;
260              
261              
262              
263             =pod
264              
265             =head1 NAME
266              
267             App::Ylastic::CostAgent - Perl port of the Ylastic Cost Agent for Amazon Web Services
268              
269             =head1 VERSION
270              
271             version 0.006
272              
273             =head1 DESCRIPTION
274              
275             This module contains the internal routines for LEylastic-costagentE. Please
276             see that for end-user documentation.
277              
278             =for Pod::Coverage new
279             run
280             accounts
281             config_file
282             dir
283             logger
284             mech
285             upload
286             ylastic_id
287              
288             =for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders
289              
290             =head1 SUPPORT
291              
292             =head2 Bugs / Feature Requests
293              
294             Please report any bugs or feature requests by email to C, or through
295             the web interface at L. You will be automatically notified of any
296             progress on the request by the system.
297              
298             =head2 Source Code
299              
300             This is open source software. The code repository is available for
301             public review and contribution under the terms of the license.
302              
303             L
304              
305             git clone git://github.com/dagolden/app-ylastic-costagent.git
306              
307             =head1 AUTHOR
308              
309             David Golden
310              
311             =head1 COPYRIGHT AND LICENSE
312              
313             This software is Copyright (c) 2011 by David Golden.
314              
315             This is free software, licensed under:
316              
317             The Apache License, Version 2.0, January 2004
318              
319             =cut
320              
321              
322             __END__