File Coverage

blib/lib/SSH/Command.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package SSH::Command;
2              
3 1     1   811 use strict;
  1         2  
  1         37  
4 1     1   5 use warnings;
  1         2  
  1         38  
5              
6 1     1   14 use Carp;
  1         2  
  1         85  
7 1     1   1095 use Data::Dumper;
  1         6835  
  1         91  
8 1     1   1519 use File::Temp;
  1         23088  
  1         102  
9 1     1   849 use Scope::Guard;
  1         446  
  1         39  
10 1     1   504 use Net::SSH2;
  0            
  0            
11             use Exporter::Lite;
12              
13             our $DEBUG = 0;
14             our $VERSION = '0.07';
15              
16             our @EXPORT = qw/ssh_execute/;
17              
18             =head1 NAME
19              
20             SSH::Command - interface to execute multiple commands
21             on host by SSH protocol without certificates ( only login + password )
22              
23             =head1 SYNOPSIS
24              
25             use SSH::Command;
26              
27             my $result = ssh_execute(
28             host => '127.0.0.1',
29             username => 'suxx',
30             password => 'qwerty',
31             commands =>
32             [
33             {
34             cmd => 'uname -a', # for check connection
35             verify => qr/linux/i, # or => 'linux-....' (check by 'eq')
36             }
37             ]
38             );
39              
40             if ($result) {
41             print "all ok!";
42             } else {
43             print "Command failed!";
44             }
45              
46             Execute one command and get answer:
47              
48             my $command_answer_as_text = ssh_execute(
49             host => '127.0.0.1',
50             username => 'suxx',
51             password => 'qwerty',
52             command => 'uname', # for check connection
53             );
54              
55              
56             =cut
57              
58             #
59             # Module Net::SSH2 have troubles with Perl 5.10
60             # use this patch http://rt.cpan.org/Public/Bug/Display.html?id=36614
61             # and patch Net::SSH2
62             #
63              
64             # Convert server answer in raw ormat to string
65             # unpacking LIBSSH2_HOSTKEY_HASH_MD5 struct
66             sub raw_to_string {
67             my ($raw) = @_;
68             return join '', map { sprintf "%x", ord $_ } split '|', $raw;
69             }
70              
71             # Sub for working with server over ssh / scp
72             sub ssh_execute {
73             my %params = @_;
74              
75             #require_once 'Net::SSH2';
76             my $ssh2 = Net::SSH2->new();
77              
78             print "Start connection\n" if $DEBUG;
79              
80             unless ($params{host} && $ssh2->connect($params{host})) {
81             die "SSH connection failed or host not specified!\n";
82             return '';
83             } else {
84             print "Connection established" if $DEBUG;
85             }
86            
87             unless ( auth_on_ssh($ssh2, { %params }) ) {
88             die "Auth failed!\n";
89             return '';
90             }
91              
92             # check auth result
93             unless ($ssh2->auth_ok) {
94             die "SSH authorization failed!\n";
95             return '';
96             }
97              
98              
99             if ($params{hostkey}) { # check server fingerprint
100             if (raw_to_string($ssh2->hostkey('md5')) ne lc $params{hostkey}) {
101             die "Server digest verification failed!\n";
102             return '';
103             }
104             }
105            
106             my $sg = Scope::Guard->new( sub { $ssh2->disconnect } );
107              
108             if ( ref $params{commands} eq 'ARRAY' ) {
109             foreach my $command (@{ $params{commands} }) {
110              
111             if ( ref $command eq 'HASH' && $command->{cmd} eq 'scp_put' ) {
112              
113             unless ( put_file_to_server($command->{string}, $command->{dest_path}, $ssh2) ) {
114             return '';
115             }
116              
117             } else {
118             my $result = execute_command_and_get_answer($ssh2, $command->{cmd});
119              
120             unless ( verify_answer($result, $command->{verify}) ) {
121             return '';
122             }
123              
124             }
125             }
126             } elsif ($params{command}) {
127             return execute_command_and_get_answer($ssh2, $params{command});
128             }
129              
130             return 1; # all ok
131             }
132              
133              
134             # Try to login to server
135             sub auth_on_ssh {
136             my ($ssh2, $params) = @_;
137              
138             # classical password auth
139             if ($params->{password} && $params->{username}) {
140             $ssh2->auth_password( $params->{username}, $params->{password} );
141             } elsif ($params->{key_path}) {
142             # auth by cert not supported
143             die "Certificate auth in progress!\n";
144             return '';
145             } else {
146             die "Not enought data for auth!\n";
147             return '';
148             }
149              
150             return 1;
151             }
152              
153              
154             # Put file to server via scp
155             sub put_file_to_server {
156             my ($text, $dest_path, $ssh2) = @_;
157              
158             return '' unless $text && $dest_path && $ssh2;
159              
160             my $temp_file = File::Temp->new;
161             $temp_file->printflush($text);
162             $temp_file->seek(0, 0);
163            
164             #print $temp_file->getlines; -- work very unstable!
165             unless ( $dest_path =~ m#^/(?:var|tmp)# ) {
166             die "Danger! Upload only in /var or /tmp\n";
167             return '';
168             }
169              
170             unless($ssh2->scp_put($temp_file, $dest_path)) {
171             die "Scp put failed!\n";
172             return '';
173             }
174              
175             return 1;
176             }
177              
178              
179             # Execute command and get answer as text
180             sub execute_command_and_get_answer {
181             my ($ssh2, $command) = @_;
182              
183             my $chan = $ssh2->channel();
184            
185             $chan->exec($command);
186             $chan->read(my $result, 102400);
187             chomp $result; # remove \n on string tail
188              
189             return $result;
190             }
191              
192              
193             # Check answer
194             sub verify_answer {
195             my ($result, $verify) = @_;
196              
197             if ( ref $verify eq 'Regexp' ) {
198             if ($result !~ /$verify/) {
199             die "Server answer ($result) is not match reg ex!\n";
200             return '';
201             }
202             } elsif ($verify) {
203             if ($result ne $verify) {
204             die "Server answer ($result) is not equal " .
205             "verify string ($verify)!\n";
206             return '';
207             }
208             } else {
209             die "Verify string is null!\n";
210             return '';
211             }
212              
213             return 1;
214             }
215              
216              
217             sub wrapper {
218             # Put config data to YAML config
219             my $user_dir_path = "/var/www/vhosts/test_domain/httpdocs";
220             my $config_path = "user_dir/cfg/config.ini";
221             my $sql_dump_file = "user_dir/install/dump.sql";
222             my $dist_path = '/var/rpanel/r_0.1042_nrg.tar.bz2';
223             my $config = { }; # STUB
224              
225             ssh_execute(
226             host => 'rpanels_ssh_host',
227             username => 'rpanels_ssh_username',
228             password => 'rpanels_ssh_password',
229             hostkey => 'rpanels_ssh_host_digest',
230             commands => [
231             {
232             cmd => 'uname -a', # for connect check
233             verify => qr/linux/i,
234             },
235              
236             {
237             cmd => "tar -xjf $dist_path " .
238             "-C $user_dir_path && echo 'ok'",
239             verify => 'ok'
240             },
241              
242             {
243             cmd => 'scp_put',
244             string => 'some data',
245             dest_path => '/tmp/some_path',
246             },
247              
248             {
249             cmd => "chmod a+rwx $config_path && echo 'ok_chmod'",
250             verify => 'ok_chmod',
251             },
252              
253             {
254             cmd => "zcat $sql_dump_file.gz > " .
255             "$sql_dump_file && echo 'ok_zcat'",
256             verify => 'ok_zcat',
257             },
258              
259             {
260             cmd => "mysql -u$config->{db_user} -p$config->{db_user_password}" .
261             " -D$config->{db_name} < $sql_dump_file && echo 'ok_sql_init'",
262             verify => 'ok_sql_init',
263             },
264              
265             {
266             cmd => 'scp_put',
267             dest_path => "${sql_dump_file}_create_admin.sql",
268             string => "
269             SET NAMES 'cp1251';
270             INSERT INTO admin (email, passwd, first_name,last_name, support_phone, support_icq, support_email)
271             VALUES(
272             '$config->{email}',
273             MD5('$config->{passwd}'),
274             '$config->{first_name}',
275             '$config->{last_name}',
276             '$config->{support_phone}',
277             '$config->{support_icq}',
278             '$config->{support_email}'
279             );
280             "
281             },
282              
283             {
284             cmd => "mysql -u$config->{db_user} -p$config->{db_user_password} " .
285             "-D$config->{db_name} < ${sql_dump_file}_create_admin.sql && echo 'create_admin_ok'",
286             verify => 'create_admin_ok',
287             },
288              
289             {
290             cmd => "rm -rf $user_dir_path/install && echo 'ok_rm'",
291             verify => 'ok_rm',
292             },
293             ],
294             ) or return 'FAIL';
295             }
296              
297              
298             1;
299              
300             __END__