File Coverage

blib/lib/DracPerl/Client.pm
Criterion Covered Total %
statement 74 91 81.3
branch 15 28 53.5
condition 1 2 50.0
subroutine 12 14 85.7
pod 4 6 66.6
total 106 141 75.1


line stmt bran cond sub pod time code
1             package DracPerl::Client;
2              
3             our $VERSION = "0.10";
4              
5 2     2   19674 use Log::Any ();
  2         41151  
  2         84  
6 2     2   2649 use Log::Any::Adapter;
  2         883  
  2         12  
7              
8 2     2   2103 use LWP::UserAgent;
  2         88047  
  2         68  
9 2     2   1343 use Moose;
  2         918096  
  2         22  
10 2     2   18878 use XML::Simple qw(XMLin);
  2         21871  
  2         29  
11              
12             has 'ua' => ( lazy => 1, is => 'ro', builder => '_build_ua' );
13              
14             has 'url' => ( is => 'ro', isa => 'Str', required => 1 );
15             has 'user' => ( is => 'ro', isa => 'Str', required => 1 );
16             has 'password' => ( is => 'ro', isa => 'Str', required => 1 );
17              
18             has 'max_retries' => ( is => 'ro', isa => 'Int', default => 5 );
19             has 'token' => ( is => 'rw', isa => 'Str', default => 0 );
20              
21             has 'log' => (
22             is => 'ro',
23             default => sub {
24             Log::Any::Adapter->set('Stdout');
25             return Log::Any->get_logger;
26             }
27             );
28              
29             sub _build_ua {
30 1     1   12 my $ua = LWP::UserAgent->new();
31 1         2845 $ua->ssl_opts( verify_hostname => 0 );
32 1         29 $ua->cookie_jar( {} );
33              
34             #Thanks Spotify for theses two headers.
35             #https://github.com/spotify/moob/blob/master/lib/moob/idrac7.rb#L23
36 1         20057 $ua->default_header( 'Accept-Encoding' => 'gzip,deflate,sdch' );
37 1         82 $ua->default_header( 'Accept-Language' => 'en-US,en;q=0.8,sv;q=0.6' );
38              
39 1         35 $ua->default_header( 'Accept' => '*/*' );
40              
41 1         69 return $ua;
42             }
43              
44             sub openSession {
45 1     1 1 3368 my ( $self, $saved_session ) = @_;
46              
47 1 50       4 unless ($saved_session) {
48 1         36 $self->log->debug("Opening new session");
49 1         53 return $self->_login;
50             }
51              
52 0         0 $self->log->debug("Resuming opened session");
53 0         0 $self->token( $saved_session->{token} );
54 0         0 $self->ua->cookie_jar( $saved_session->{cookie_jar} );
55 0         0 $self->ua->default_header( "ST2", $self->token );
56 0         0 return 1;
57             }
58              
59             sub closeSession {
60 0     0 1 0 my ($self) = @_;
61              
62 0         0 my $logout_page = $self->ua->post( $self->url . "/data/logout" );
63              
64 0         0 $self->token(0);
65 0         0 $self->ua->default_header( "ST2", $self->token );
66 0         0 $self->log->debug( "Logging out : " . $logout_page->decoded_content );
67 0         0 return 1;
68             }
69              
70             sub saveSession {
71 1     1 1 4 my ($self) = @_;
72 1         4 my %saved_session;
73              
74 1 50       55 return 0 unless $self->token;
75              
76 1         50 $self->log->info("Saving the session");
77 1         228 $saved_session{token} = $self->token;
78 1         47 $saved_session{cookie_jar} = $self->ua->cookie_jar;
79              
80 1         17 return \%saved_session;
81             }
82              
83             sub isAlive {
84 1     1 1 2 my ($self) = @_;
85 2     2   1543 use Data::Dumper;
  2         5  
  2         1707  
86 1         39 my $response = $self->ua->get( $self->url . "TreeList.xml" );
87              
88 1 50       309 return 0 unless $response->is_success;
89              
90 1         20 my $treelist = XMLin( $response->decoded_content );
91              
92 0 0       0 return 0 unless $treelist->{TreeNode};
93 0         0 return 1;
94              
95             }
96              
97             sub _login {
98 1     1   4 my $self = shift;
99              
100 1         50 my $login_form = $self->ua->get( $self->url . "/login.html" );
101              
102 1 50       7964 if ( $login_form->is_success ) {
103 1         124 $self->log->info("Login Step 0 success");
104             }
105             else {
106 0         0 $self->log->error( "iDrac login page is unreacheable : "
107             . $self->url
108             . "/login.html" );
109 0         0 die();
110             }
111              
112 1         555 my $response_raw;
113             my $response_xml;
114 1         39 my $need_to_retry = 1;
115 1         4 my $current_tries = 1;
116 1         2 my $logged;
117              
118 1         6 while ($need_to_retry) {
119              
120 3         167 $response_raw = $self->ua->post(
121             $self->url . "/data/login",
122             { user => $self->user,
123             password => $self->password
124             }
125             );
126              
127 3 50       2855 if ( $response_raw->is_success ) {
128 3         66 $response_xml = XMLin( $response_raw->decoded_content );
129 3         155513 $logged = !$response_xml->{authResult};
130             }
131              
132 3 100       23 $need_to_retry = 0 if $logged;
133 3 50       209 $need_to_retry = 0 if $current_tries > $self->max_retries - 1;
134              
135 3 100       13 if ($logged) {
136 1         54 $self->log->info( "Sucessfully performed login step 1 ( Attempt "
137             . $current_tries . "/"
138             . $self->max_retries
139             . ")" );
140             }
141             else {
142 2         104 $self->log->error( "Failed login step 1. ( Attempt "
143             . $current_tries . "/"
144             . $self->max_retries
145             . ")" );
146             }
147              
148 3         1142 $current_tries++;
149             }
150              
151 1 50       7 die( "Logging failed after " . $self->max_retries . " attempts" )
152             unless $logged;
153              
154 1         55 $self->log->debug(
155             "Login Step 1 response : " . $response_raw->decoded_content );
156              
157 1         769 my @tokens_parts = reverse split( "=", $response_xml->{forwardUrl} );
158              
159 1         55 $self->log->info( "Success while opening session / " . $tokens_parts[0] );
160              
161 1 50       240 $self->token( $tokens_parts[0] ) if $tokens_parts[0];
162 1         51 $self->ua->default_header( "ST2", $self->token );
163 1         142 return 1;
164             }
165              
166             sub get {
167 1     1 0 4 my ( $self, $query ) = @_;
168              
169 1 50       55 $self->openSession() unless $self->token;
170              
171 1         47 my $response = $self->ua->post( $self->url . "/data?get=" . $query );
172              
173 1 50       572 if ( $response->is_success ) {
174 1         67 $self->log->info("Sucessfully fetched $query");
175             }
176             else {
177 0         0 $self->log->error("Error while fetching $query");
178             }
179              
180 1         185 my $parsed_response;
181              
182 1 50       7 $parsed_response = XMLin( $response->decoded_content )
183             if $response->is_success;
184              
185 1   50     25691 return $parsed_response || 0;
186             }
187              
188             sub set {
189 0     0 0   die("Not implemented yet");
190             }
191              
192             =head1 NAME
193              
194             DracPerl::Client - API Client for Dell's management interface (iDRAC)
195              
196             =head1 AUTHOR
197              
198             Jules Decol (@Apcros)
199              
200             =head1 SYNOPSIS
201              
202             A client to interact with the iDRAC API on Dell Poweredge servers
203              
204             # Create the client
205             my $drac_client = DracPerl::Client->new({
206             user => "username",
207             password => "password",
208             url => "https://dracip",
209             });
210              
211             # Get what you're interested in
212             # Login is done implicitly, you can save and resume sessions. See below
213             my $parsed_xml = $drac_client->get("fans");
214              
215             =head1 DESCRIPTION
216              
217             =head2 WHY ?
218              
219             This been created because I find the web interface of iDrac slow and far from being easy to use.
220             I have the project of creating a full new iDrac front-end, but of course that project required an API Client.
221             Because this is something that seem to be quite lacking in the PowerEdge community, I made a standalone repo/project for that :)
222              
223             =head2 TODO
224              
225             What's to come ?
226              
227             - Better error handling
228              
229             - Integration with Log4Perl
230              
231             - Full list of supported Method
232              
233             - Few method to abstract commons operations
234              
235             =head1 OBJECT ARGUMENTS
236              
237              
238             =head2 max_retries
239              
240             Login can be extremely capricious, Max retries avoid being too
241             annoyed by that. Defaulted to 5.
242              
243             =head1 METHODS
244              
245             =head2 openSession
246              
247             Can be called explicitly or is called by default if get is called and no session is available
248             You can pass it a saved session in order to restore it.
249              
250             $drac_client->openSession($saved_session) #Will restore a session
251             $drac_client->openSession() #Will open a new one
252              
253             =head2 saveSession
254              
255             This will return the current session. (Basically the token and the cookie jar).
256              
257             =head2 closeSession
258              
259             Invalidate the current session
260              
261             =head2 isAlive
262              
263             Check with a quick api call if your current session is still useable.
264              
265              
266             =cut
267              
268             1;