File Coverage

lib/Provision/Unix/VirtualOS.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 Provision::Unix::VirtualOS;
2             {
3             $Provision::Unix::VirtualOS::VERSION = '1.07';
4             }
5             # ABSTRACT: Provision virtual computers (VPS,VM,VE,Jail,etc)
6              
7 1     1   2531 use strict;
  1         3  
  1         55  
8 1     1   6 use warnings;
  1         3  
  1         39  
9              
10 1     1   9 use Data::Dumper;
  1         3  
  1         84  
11 1     1   8 use English qw( -no_match_vars );
  1         2  
  1         12  
12 1     1   2957 use LWP::Simple;
  1         128936  
  1         12  
13 1     1   671 use LWP::UserAgent;
  1         3  
  1         38  
14 1     1   479 use Params::Validate qw(:all);
  0            
  0            
15             use Time::Local;
16              
17             our $AUTOLOAD;
18             my ($prov, $util);
19             my @std_opts = qw/ test_mode debug fatal /;
20             my %std_opts = (
21             test_mode => { type => BOOLEAN, optional => 1 },
22             debug => { type => BOOLEAN, optional => 1, default => 1 },
23             fatal => { type => BOOLEAN, optional => 1, default => 1 },
24             );
25              
26             sub new {
27              
28             # Usage : $vos->new( prov => $prov );
29             # Purpose : create a $vos object
30             # Returns : a Provision::Unix::VirtualOS object
31             # Parameters :
32             # Required : prov - a Provision::Unix object
33             # Optional : etc_dir - an etc directory used by some P:U:V classes
34              
35             my $class = shift;
36             my %p = validate(
37             @_,
38             { prov => { type => OBJECT },
39             etc_dir => { type => SCALAR, optional => 1 },
40             debug => { type => BOOLEAN, optional => 1, default => 1 },
41             fatal => { type => BOOLEAN, optional => 1, default => 1 },
42             }
43             );
44              
45             $prov = $p{prov};
46             my $debug = $p{debug};
47             my $fatal = $p{fatal};
48             $util = $prov->get_util;
49              
50             my $self = {
51             prov => $prov,
52             debug => $debug,
53             fatal => $fatal,
54             etc_dir => $p{etc_dir},
55             util => $util,
56             };
57             bless( $self, $class );
58              
59             $prov->audit( $class . sprintf( " loaded by %s, %s, %s", caller ) );
60              
61             $self->{vtype} = $self->_get_virt_type( fatal => $fatal, debug => $debug )
62             or die $prov->{errors}[-1]{errmsg};
63              
64             return $self;
65             }
66              
67             sub create {
68             my $self = shift;
69             my @opt_scalars = qw/ config cpu disk_root disk_size hostname
70             kernel_version mac_address nameservers password ram
71             searchdomain ssh_key template /;
72             my %opt_scalars = map { $_ => { type => SCALAR, optional => 1 } } @opt_scalars;
73             my @opt_bools = qw/ skip_start /;
74             my %opt_bools = map { $_ => { type => BOOLEAN, optional => 1 } } @opt_bools;
75              
76             my %p = validate(
77             @_,
78             { name => { type => SCALAR },
79             ip => { type => SCALAR },
80             %opt_scalars,
81             %opt_bools,
82             %std_opts,
83             }
84             );
85              
86             $prov->audit( "initializing request to create virtual os '$p{name}'");
87              
88             $self->{name} = $self->set_name( $p{name} );
89             $self->{ip} = $self->get_ips( $p{ip} ) or return;
90              
91             foreach ( @opt_scalars, @opt_bools, @std_opts ) {
92             $self->{$_} = $p{$_} if defined $p{$_};
93             };
94              
95             if ( $p{nameservers} ) {
96             $prov->audit( "getting nameserver IP list");
97             $self->{nameservers} = $self->get_ips( $p{nameservers} );
98             };
99              
100             my ($delegate) = $self->{vtype} =~ m/^(.*)=HASH/;
101             $prov->audit("\tdelegating create request to $delegate");
102             $self->{vtype}->create();
103             }
104              
105             sub destroy {
106              
107             my $self = shift;
108             my %p = validate(
109             @_,
110             { 'name' => { type => SCALAR },
111             'disk_root' => { type => SCALAR, optional => 1 },
112             %std_opts
113             }
114             );
115              
116             my $name = $self->set_name( $p{name} );
117             $prov->audit("initializing request to destroy virtual os '$name'");
118              
119             foreach ( @std_opts ) {
120             $self->{$_} = $p{$_} if defined $p{$_};
121             };
122              
123             $self->{vtype}->destroy();
124             }
125              
126             sub start {
127              
128             my $self = shift;
129             my %p = validate(
130             @_,
131             { 'name' => { type => SCALAR },
132             %std_opts
133             }
134             );
135              
136             foreach ( 'name', @std_opts ) {
137             $self->{$_} = $p{$_} if defined $p{$_};
138             };
139              
140             $self->{vtype}->start();
141             }
142              
143             sub stop {
144              
145             my $self = shift;
146              
147             my %p = validate(
148             @_,
149             { 'name' => { type => SCALAR },
150             %std_opts,
151             }
152             );
153              
154             foreach ( 'name', @std_opts ) {
155             $self->{$_} = $p{$_} if defined $p{$_};
156             };
157              
158             $self->{vtype}->stop();
159             }
160              
161             sub restart {
162              
163             my $self = shift;
164              
165             my %p = validate(
166             @_,
167             { 'name' => { type => SCALAR },
168             %std_opts
169             }
170             );
171              
172             foreach ( 'name', @std_opts ) {
173             $self->{$_} = $p{$_} if defined $p{$_};
174             };
175              
176             $self->{vtype}->restart();
177             }
178              
179             sub disable {
180              
181             my $self = shift;
182              
183             my %p = validate(
184             @_,
185             { name => { type => SCALAR },
186             disk_root => { type => SCALAR, optional => 1 },
187             %std_opts
188             }
189             );
190              
191             foreach ( qw/ name disk_root /, @std_opts ) {
192             $self->{$_} = $p{$_} if defined $p{$_};
193             };
194              
195             $self->{vtype}->disable();
196             }
197              
198             sub enable {
199              
200             my $self = shift;
201              
202             my %p = validate(
203             @_,
204             { name => { type => SCALAR },
205             disk_root => { type => SCALAR, optional => 1 },
206             %std_opts
207             }
208             );
209              
210             foreach ( qw/ name disk_root /, @std_opts ) {
211             $self->{$_} = $p{$_} if defined $p{$_};
212             };
213              
214             $self->{vtype}->enable();
215             }
216              
217             sub migrate {
218             my $self = shift;
219             my @req_scalars = qw/ name new_node /;
220             my %req_scalars = map { $_ => { type => SCALAR } } @req_scalars;
221             my @opt_scalars = qw/ /;
222             my %opt_scalars = map { $_ => { type => SCALAR, optional => 1 } } @opt_scalars;
223             my @opt_bools = qw/ connection_test /;
224             my %opt_bools = map { $_ => { type => BOOLEAN, optional => 1 } } @opt_bools;
225              
226             my %p = validate( @_, { %req_scalars, %opt_scalars, %opt_bools, %std_opts, } );
227              
228             my $name = $p{name};
229             my $new_node = $p{new_node};
230              
231             $prov->audit("initializing request to migrate VE '$name' to $new_node");
232              
233             foreach ( @req_scalars, @opt_scalars, @std_opts ) {
234             $self->{$_} = $p{$_} if defined $p{$_};
235             };
236             foreach ( @opt_bools ) {
237             $self->{$_} = defined $p{$_} ? $p{$_} : 0;
238             };
239              
240             $prov->audit("\tdelegating request to $self->{vtype}");
241              
242             $self->{vtype}->migrate();
243             };
244              
245             sub modify {
246              
247             my $self = shift;
248             my @req_scalars = qw/ name disk_size hostname ip ram /;
249             my %req_scalars = map { $_ => { type => SCALAR } } @req_scalars;
250             my @opt_scalars = qw/ config cpu disk_root mac_address nameservers
251             password searchdomain ssh_key template /;
252             my %opt_scalars = map { $_ => { type => SCALAR, optional => 1 } } @opt_scalars;
253              
254             my %p = validate( @_, { %req_scalars, %opt_scalars, %std_opts, } );
255              
256             $prov->audit("initializing request to modify VE '$p{name}'");
257              
258             foreach ( @req_scalars, @opt_scalars, @std_opts ) {
259             $self->{$_} = $p{$_} if defined $p{$_};
260             };
261              
262             $self->{ip} = $self->get_ips( $p{ip} ) if $p{ip};
263             $self->{nameservers} = $self->get_ips( $p{nameservers} ) if $p{nameservers};
264              
265             $prov->audit("\tdelegating request to $self->{vtype}");
266              
267             $self->{vtype}->modify();
268             }
269              
270             sub reinstall {
271              
272             # Usage : $vos->reinstall( name => '42', ip=>'127.0.0.2' );
273             # Purpose : reinstall the OS in virtual machine
274             # Returns : true or undef on failure
275             # Parameters :
276             # Required : name - name/ID of the virtual OS
277             # : template - a 'template' or tarball the OS is patterned after
278             # : ip - IP address(es), space delimited
279             # Optional : hostname - the FQDN of the virtual OS
280             # : disk_root - the root directory of the virt os
281             # : disk_size - disk space allotment (MB)
282             # : ram - (MB)
283             # : config - a config file with virtual specific settings
284             # : password - the root/admin password for the virtual
285             # : nameservers -
286             # : searchdomain -
287              
288             my $self = shift;
289             my @opt_scalars = qw/ config cpu disk_root disk_size hostname
290             kernel_version mac_address nameservers password ram
291             searchdomain ssh_key template /;
292             my %opt_scalars = map { $_ => { type => SCALAR, optional => 1 } } @opt_scalars;
293              
294             my %p = validate(
295             @_,
296             { name => { type => SCALAR },
297             ip => { type => SCALAR },
298             %opt_scalars,
299             %std_opts,
300             }
301             );
302              
303             $prov->audit( "initializing request to reinstall ve '$p{name}'");
304              
305             foreach ( 'name', @opt_scalars, @std_opts ) {
306             $self->{$_} = $p{$_} if defined $p{$_};
307             };
308              
309             $self->{name} = $self->set_name( $p{name} );
310             $self->{ip} = $self->get_ips( $p{ip} ) or return;
311             $self->{nameservers} = $self->get_ips( $p{nameservers} ) if $p{nameservers};
312              
313             $prov->audit("\tdelegating request to $self->{vtype}");
314             $self->{vtype}->reinstall();
315             }
316              
317             sub AUTOLOAD {
318             my $self = shift;
319              
320             # this AUTOLOAD method works for any methods in P:U:V:* whose only required
321             # argument is the VE name.
322             my %p = validate(
323             @_,
324             { name => { type => SCALAR },
325             %std_opts,
326             }
327             );
328             my $fatal = $p{fatal};
329             my $v_type = ref $self->{vtype};
330              
331             foreach ( 'name', @std_opts ) {
332             $self->{$_} = $p{$_} if defined $p{$_};
333             };
334              
335             my $sub = $AUTOLOAD;
336             $sub =~ s/.*://; # strip off everything before the last :
337              
338             $prov->audit( "initializing request to $sub for ve '$p{name}'");
339              
340             ref ($self) || return $prov->error( "invalid call to $sub", fatal => $fatal );
341             return $self->{vtype}->$sub() if $self->{vtype}->can($sub);
342              
343             $prov->error( "The VE platform $v_type does not have support for '$sub'", fatal => $fatal);
344             return;
345             };
346              
347             sub DESTROY {};
348             sub create_snapshot;
349             sub destroy_snapshot;
350             sub mount_snapshot;
351             sub unmount_snapshot;
352             sub get_disk_usage;
353             sub get_mac_address;
354             sub get_config;
355             sub transition;
356             sub untransition;
357              
358             sub mount {
359             my $self = shift;
360             my %p = validate(
361             @_,
362             { name => { type => SCALAR },
363             refresh => { type => BOOLEAN, optional => 1, default => 1 },
364             %std_opts,
365             }
366             );
367              
368             $prov->audit( "initializing request to mount ve '$p{name}'");
369              
370             foreach ( qw/ name refresh /, @std_opts ) {
371             $self->{$_} = $p{$_} if defined $p{$_};
372             };
373              
374             $self->{vtype}->mount();
375             };
376              
377             sub unmount {
378             my $self = shift;
379             my %p = validate(
380             @_,
381             { name => { type => SCALAR },
382             refresh => { type => BOOLEAN, optional => 1, default => 1 },
383             %std_opts,
384             }
385             );
386              
387             $prov->audit( "initializing request to unmount ve '$p{name}'");
388              
389             foreach ( qw/ name refresh /, @std_opts ) {
390             $self->{$_} = $p{$_} if defined $p{$_};
391             };
392              
393             $self->{vtype}->unmount();
394             };
395              
396             sub publish_arp {
397             my $self = shift;
398             my %p = validate(
399             @_,
400             { ip => { type => SCALAR|ARRAYREF },
401             %std_opts,
402             }
403             );
404              
405             my $ip = $p{ip};
406             my @ips = ref $ip ? @$ip : $ip;
407              
408             my $arpsend = $util->find_bin( 'arpsend', fatal => 0 );
409              
410             if ( -x $arpsend ) {
411             foreach ( @ips ) {
412             $prov->audit( "$arpsend -U -c2 -i $_ eth0" );
413             system "$arpsend -U -c2 -i $_ eth0";
414             };
415             return 1;
416             }
417              
418             # TODO: try using arping, but I haven't been able to get it to work
419             return;
420             };
421              
422             sub do_connectivity_test {
423             my $self = shift;
424              
425             return 1 if ! $self->{connection_test};
426              
427             my $new_node = $self->{new_node};
428             my $debug = $self->{debug};
429             my $ssh = $util->find_bin( 'ssh', debug => $debug );
430             my $r = $util->syscmd( "$ssh $new_node /bin/uname -a", debug => $debug, fatal => 0)
431             or return $prov->error("could not validate connectivity to $new_node", fatal => 0);
432             $prov->audit("connectivity to $new_node is good");
433             return 1;
434             };
435              
436             sub gen_config {
437             my $self = shift;
438             my %p = validate(
439             @_,
440             { name => { type => SCALAR },
441             ram => { type => SCALAR },
442             disk_root => { type => SCALAR },
443             disk_size => { type => SCALAR },
444             template => { type => SCALAR },
445             config => { type => SCALAR },
446             hostname => { type => SCALAR },
447             ip => { type => SCALAR },
448             %std_opts,
449             }
450             );
451              
452             foreach ( qw/ name ram disk_size disk_root template config hostname /,
453             @std_opts )
454             {
455             $self->{$_} = $p{$_} if defined $p{$_};
456             };
457              
458             $self->{ip} = $self->get_ips( $p{ip} );
459              
460             $self->{vtype}->gen_config();
461             };
462              
463             sub get_fs_root {
464             my $self = shift;
465             my $name = shift || $self->{name};
466             my $fs_root;
467             if ( $self->{vtype}->can('get_fs_root') ) {
468             return $self->{vtype}->get_fs_root( $name );
469             }
470             return $self->{vtype}->get_ve_home( $name );
471             };
472              
473             sub get_ve_home {
474             my $self = shift;
475             my $name = shift || $self->{name};
476             my $fs_root;
477             if ( $self->{vtype}->can('get_ve_home') ) {
478             return $self->{vtype}->get_ve_home( $name );
479             }
480             return;
481             };
482              
483             sub get_ips {
484             my $self = shift;
485             my $ip_string = shift;
486              
487             $prov->audit("\textracting IPs from string: $ip_string");
488              
489             my @r;
490             my @ips = split / /, $ip_string;
491             foreach my $ip (@ips) {
492             my $ip = $self->is_valid_ip($ip);
493             push @r, $ip if $ip;
494             }
495              
496             my $ips = @r;
497             if ( $ips == 0 ) {
498             return $prov->error( "no valid IPs in request!",
499             debug => $self->{debug},
500             fatal => $self->{fatal},
501             );
502             }
503             my $ip_plural = $ips > 1 ? 'ips' : 'ip';
504             $prov->audit("\tfound $ips valid $ip_plural");
505             return \@r;
506             }
507              
508             sub get_status {
509             my $self = shift;
510             my %p = validate(
511             @_,
512             { name => { type => SCALAR | UNDEF, optional => 1 },
513             %std_opts,
514             }
515             );
516              
517             foreach ( 'name', @std_opts ) {
518             $self->{$_} = $p{$_} if defined $p{$_};
519             };
520              
521             $self->{vtype}->get_status();
522             }
523              
524             sub get_template_dir {
525              
526             my $self = shift;
527             my %p = validate(
528             @_,
529             { v_type => { type => SCALAR },
530             %std_opts,
531             }
532             );
533              
534             my $v_type = $p{v_type};
535              
536             my $dir = $prov->{config}{VirtualOS}{"${v_type}_template_dir"};
537             return $dir if $dir; # they defined it in provision.conf, use it
538              
539             # try to autodetect
540             $dir = -d "/templates" ? '/templates'
541             : -d "/vz/template/cache" ? '/vz/template/cache'
542             : -d "/vz/template" ? '/vz/template'
543             : undef;
544              
545             $dir and return $dir;
546              
547             return $prov->error( 'unable to determine template directory',
548             fatal => $p{fatal},
549             debug => $p{debug},
550             );
551             };
552              
553             sub get_template_list {
554             my $self = shift;
555             my %p = validate(
556             @_,
557             { v_type => { type => SCALAR },
558             url => { type => SCALAR, optional => 1 },
559             %std_opts,
560             }
561             );
562              
563             my $url = $p{url};
564             my $v_type = $p{v_type};
565             my @templates;
566              
567             if ( ! $url ) {
568             my $template_dir = $self->get_template_dir( v_type=> $v_type )
569             or return $prov->error( 'unable to determine template directory',
570             fatal => $p{fatal},
571             debug => $p{debug},
572             );
573              
574             my @template_names = glob("$template_dir/*.tar.gz");
575             foreach my $template ( @template_names ) {
576             ($template) = $template =~ /\/([\w\.\-]+)\.tar\.gz$/;
577             push @templates, { name => $template };
578             };
579              
580             return \@templates if scalar @templates;
581             return;
582             };
583              
584             my $ua = LWP::UserAgent->new( timeout => 10);
585             my $response = $ua->get($url);
586              
587             die $response->status_line if ! $response->is_success;
588              
589             my $content = $response->content;
590             #warn Dumper($content);
591              
592             # >centos-5-i386-plesk-8.6.tar.gz<
593             my @fields = grep { /\-.*?\-/ } split /<.*?>/, $content;
594             while ( scalar @fields ) {
595             my $file = shift @fields or last;
596             next if $file !~ /tar.gz/;
597             my $date = shift @fields;
598             my $timestamp = $self->get_template_timestamp($date);
599             push @templates, { name => $file, date => $date, timestamp => $timestamp };
600             };
601              
602             return \@templates;
603             };
604              
605             sub get_template_timestamp {
606             my ( $self, $time ) = @_;
607            
608             my %months = (
609             'jan' => 1, 'feb' => 2, 'mar' => 3, 'apr' => 4,
610             'may' => 5, 'jun' => 6, 'jul' => 7, 'aug' => 8,
611             'sep' => 9, 'oct' => 10, 'nov' => 11, 'dec' => 12,
612             );
613              
614             my ( $Y, $M, $D, $h, $m, $s )
615             = ( $time =~ /^(\d{4})-(\w{3})-(\d{2})\s+(\d{2})?:?(\d{2})?:?(\d{2})?/ )
616             or die "invalid timestamp format: $time\n";
617              
618             my $txt_m = lc($M);
619             $M = $months{$txt_m}; # convert to an integer
620             $M -= 1;
621             $Y -= 1900;
622             return timelocal( $s, $m, $h, $D, $M, $Y );
623             };
624              
625             sub get_version {
626             return $prov->get_version();
627             };
628              
629             sub probe {
630             my $self = shift;
631             return $self->get_status(@_);
632             };
633              
634             sub set_hostname {
635             my $self = shift;
636             my %p = validate(
637             @_,
638             { 'name' => { type => SCALAR },
639             'hostname' => { type => SCALAR },
640             %std_opts,
641             }
642             );
643              
644             foreach ( qw/ name hostname /, @std_opts ) {
645             $self->{$_} = $p{$_} if defined $p{$_};
646             };
647              
648             $self->{vtype}->set_hostname();
649             }
650              
651             sub set_name {
652             my $self = shift;
653             my $name = shift || $self->{name} || die "unable to set VE name\n";
654             $self->{name} = $name;
655             return $name;
656             };
657              
658             sub set_nameservers {
659             my $self = shift;
660             my %p = validate(
661             @_,
662             { name => { type => SCALAR, optional => 1 },
663             nameservers => { type => SCALAR, optional => 1 },
664             searchdomain => { type => SCALAR, optional => 1 },
665             %std_opts,
666             }
667             );
668              
669             my $name;
670             $name = $self->set_name( $p{name} ) if $p{name};
671             my $searchdomain = $p{searchdomain};
672             $self->{nameservers} = $self->get_ips( $p{nameservers} ) if $p{nameservers};
673             $self->{nameservers} or die 'missing nameservers';
674             $self->{searchdomain} = $searchdomain;
675             $self->{test_mode} = $p{test_mode};
676             my $debug = $self->{debug} = $p{debug};
677             my $fatal = $self->{fatal} = $p{fatal};
678              
679             # if the virtualzation package has the method, call it.
680             if ( $self->{vtype}->can( 'set_nameservers' ) ) {
681             return $self->{vtype}->set_nameservers();
682             };
683              
684             # otherwise, use this default method
685             my $fs_root = $self->get_fs_root();
686             my $nameservers = $self->{nameservers};
687             my $resolv = "$fs_root/etc/resolv.conf";
688              
689             my @new;
690             push @new, "searchdomain $searchdomain" if $searchdomain;
691             if ( -f $resolv ) {
692             my @lines = $util->file_read( $resolv, fatal => $fatal );
693              
694             foreach my $line ( @lines ) {
695             next if $line =~ /^nameserver\s/i;
696             next if $searchdomain && $line =~ /^searchdomain\s/i;
697             push @new, $line;
698             };
699             };
700              
701             foreach ( @$nameservers ) {
702             push @new, "nameserver $_";
703             };
704              
705             return $util->file_write( $resolv, lines => \@new, fatal => $fatal );
706             }
707              
708             sub set_password {
709             my $self = shift;
710             my %p = validate(
711             @_,
712             { name => { type => SCALAR },
713             password => { type => SCALAR },
714             user => { type => SCALAR | UNDEF, optional => 1 },
715             disk_root => { type => SCALAR, optional => 1 },
716             ssh_key => { type => SCALAR, optional => 1 },
717             %std_opts,
718             }
719             );
720              
721             $self->{user} = $p{user} || 'root';
722              
723             foreach ( qw/ name password ssh_key disk_root /, @std_opts ) {
724             $self->{$_} = $p{$_} if defined $p{$_};
725             };
726              
727             return $self->{vtype}->set_password();
728             }
729              
730             sub set_ssh_key {
731             my $self = shift;
732             my %p = validate(
733             @_,
734             { name => { type => SCALAR },
735             ssh_key => { type => SCALAR },
736             user => { type => SCALAR | UNDEF, optional => 1 },
737             disk_root => { type => SCALAR, optional => 1 },
738             %std_opts,
739             }
740             );
741              
742             $self->{user} = $p{user} || 'root';
743              
744             foreach ( qw/ name ssh_key disk_root /, @std_opts ) {
745             $self->{$_} = $p{$_} if defined $p{$_};
746             };
747              
748             return $self->{vtype}->set_ssh_key();
749             }
750              
751             sub setup_log_files {
752             my $self = shift;
753              
754             my %p = validate( @_, { fs_root => { type => SCALAR } } );
755              
756             my $fs_root = $p{fs_root};
757              
758             my @logfiles = `find $fs_root/var/log/ -maxdepth 1 -type f -print`;
759             foreach ( @logfiles ) {
760             chomp $_;
761             $util->file_write( $_, lines => [ '' ], fatal => 0, debug => 0 );
762             };
763             };
764              
765             sub setup_ssh_host_keys {
766             my $self = shift;
767              
768             my %p = validate( @_, { fs_root => { type => SCALAR } } );
769              
770             my $fs_root = $p{fs_root};
771              
772             foreach my $type ( qw/ dsa rsa / ) {
773             my $file_path = "$fs_root/etc/ssh/ssh_host_${type}_key";
774              
775             unlink "$file_path" if -e "$file_path";
776             unlink "$file_path.pub" if -e "$file_path.pub";
777              
778             my $cmd = "/usr/bin/ssh-keygen -q -t $type -f $file_path -N ''";
779             $util->syscmd( $cmd, debug => 0 );
780             };
781             };
782              
783             sub is_mounted {
784             my $self = shift;
785             my %p = validate(
786             @_,
787             { name => { type => SCALAR },
788             refresh => { type => BOOLEAN, optional => 1, default => 1 },
789             %std_opts,
790             }
791             );
792              
793             foreach ( qw/ name refresh /, @std_opts ) {
794             $self->{$_} = $p{$_} if defined $p{$_};
795             };
796              
797             $self->{vtype}->is_mounted();
798             }
799              
800             sub is_valid_ip {
801             my $self = shift;
802             my $ip = shift;
803             my $error = "'$ip' is not a valid IPv4 address";
804              
805             my $r = grep /\./, split( //, $ip ); # need 3 dots
806             return $prov->error( $error, fatal => 0, debug => 0 )
807             if $r != 3;
808              
809             my @octets = split /\./, $ip;
810             return $prov->error( $error, fatal => 0, debug => 0 )
811             if @octets != 4;
812              
813             foreach (@octets) {
814             return unless /^\d{1,3}$/ and $_ >= 0 and $_ <= 255;
815             $_ = 0 + $_;
816             }
817              
818             return $prov->error( $error, fatal => 0, debug => 0 )
819             if $octets[0] == 0; # 0. is invalid
820              
821             return $prov->error( $error, fatal => 0, debug => 0 )
822             if 0 + $octets[0] + $octets[1] + $octets[2] + $octets[3]
823             == 0; # 0.0.0.0 is invalid
824              
825             return $prov->error( $error, fatal => 0, debug => 0 )
826             if grep( $_ eq '255', @octets ) == 4; # 255.255.255.255 is invalid
827              
828             return join( '.', @octets );
829             }
830              
831             sub _get_virt_type {
832             my $self = shift;
833             my %p = validate( @_, { %std_opts });
834              
835             my $debug = $p{debug};
836             my $fatal = $p{fatal};
837             my $prov = $self->{prov};
838              
839             return $self->_get_virt_type_linux( %p ) if lc($OSNAME) eq 'linux';
840              
841             if ( lc( $OSNAME) eq 'freebsd' ) {
842             my $ezjail = $util->find_bin( 'ezjail-admin', fatal => 0, debug => 0 );
843             if ( $ezjail ) {
844             require Provision::Unix::VirtualOS::FreeBSD::Ezjail;
845             return Provision::Unix::VirtualOS::FreeBSD::Ezjail->new( vos => $self );
846             };
847              
848             require Provision::Unix::VirtualOS::FreeBSD::Jail;
849             return Provision::Unix::VirtualOS::FreeBSD::Jail->new( vos => $self );
850             };
851             $prov->error(
852             "No virtualization methods for $OSNAME yet",
853             fatal => $fatal,
854             debug => $debug,
855             );
856             return;
857             }
858              
859             sub _get_virt_type_linux {
860             my $self = shift;
861             my %p = validate( @_, { %std_opts });
862              
863             my $err_before = scalar @{ $prov->{errors} };
864             my $xm = $util->find_bin( 'xm', fatal => 0, debug => 0);
865             my $vzctl = $util->find_bin( 'vzctl', fatal => 0, debug => 0);
866             if ( scalar @{$prov->{errors}} > $err_before ) {
867             delete $prov->{errors}[-1]; # clear the last error
868             delete $prov->{errors}[-1] if scalar @{$prov->{errors}} > $err_before;
869             };
870              
871             require Provision::Unix::VirtualOS::Linux;
872             $self->{linux} = Provision::Unix::VirtualOS::Linux->new( vos => $self );
873              
874             if ( $xm && ! $vzctl ) {
875             require Provision::Unix::VirtualOS::Linux::Xen;
876             return Provision::Unix::VirtualOS::Linux::Xen->new( vos => $self );
877             };
878             if ( $vzctl && ! $xm ) {
879             # this could be Virtuozzo or OpenVZ. The way to tell is by
880             # checking for the presence of /vz/template/cache (OpenVZ only)
881             # also, a Virtuozzo VE will have a cow directory inside the
882             # VE home directory.
883             if ( -d "/vz/template" ) {
884             if ( -d "/vz/template/cache" ) {
885             require Provision::Unix::VirtualOS::Linux::OpenVZ;
886             return Provision::Unix::VirtualOS::Linux::OpenVZ->new( vos => $self );
887             }
888             else {
889             require Provision::Unix::VirtualOS::Linux::Virtuozzo;
890             return Provision::Unix::VirtualOS::Linux::Virtuozzo->new( vos => $self );
891             }
892             }
893             else {
894             # has someone moved the template cache directory from the default location?
895             require Provision::Unix::VirtualOS::Linux::OpenVZ;
896             return Provision::Unix::VirtualOS::Linux::OpenVZ->new( vos => $self );
897             };
898             };
899              
900             $prov->error(
901             "Unable to determine your virtualization method. You need one supported hypervisor (xen, openvz) installed.",
902             fatal => $p{fatal},
903             debug => $p{debug},
904             );
905             };
906              
907             1;
908              
909             __END__
910              
911             =pod
912              
913             =encoding UTF-8
914              
915             =head1 NAME
916              
917             Provision::Unix::VirtualOS - Provision virtual computers (VPS,VM,VE,Jail,etc)
918              
919             =head1 VERSION
920              
921             version 1.07
922              
923             =head1 SYNOPSIS
924              
925             use Provision::Unix;
926             use Provision::Unix::VirtualOS;
927              
928             my $prov = Provision::Unix->new();
929             my $vos = Provision::Unix::VirtualOS->new( prov => $prov );
930              
931             $vos->create(
932             name => 42,
933             password => 't0ps3kretWerD',
934             ip => '10.1.1.43',
935             hostname => 'test_debian_5.example.com',
936             disk_size => 1000,
937             ram => 512,
938             template => 'debian-5-i386-default.tar.gz',
939             nameservers => '10.1.1.2 10.1.1.3',
940             searchdomain => 'example.com',
941             )
942             or $prov->error( "unable to create VE" );
943              
944             =head1 DESCRIPTION
945              
946             Provision::Unix::VirtualOS aims to provide a clean, consistent way to manage virtual machines on a variety of virtualization platforms including Xen, OpenVZ, Vmware, FreeBSD jails, and others. P:U:V provides a command line interface (prov_virtual) and a stable programming interface (API) for provisioning virtual machines on supported platforms. To start a VE on any supported virtualization platform, you run this command:
947              
948             prov_virtual --name=42 --action=start
949              
950             Versus this:
951              
952             xen: xm create /home/xen/42/42.cfg
953             ovz: vzctl start 42
954             ezj: ezjail-admin start 42
955              
956             P:U:V tries very hard to insure that every valid command that can succeed will. There is abundant code for handling common errors, such as unmounting xen volumes before starting a VE, making sure a disk volume is not in use before mounting it, and making sure connectivity to the new HW node exists before attempting to migrate.
957              
958             In addition to the pre-flight checks, there are also post-action checks to determine if the action succeeded. When actions fail, they provide reasonably good error messages geared towards comprehension by sysadmins. Where feasible, actions that fail are rolled back so that when the problem(s) is corrected, the action can be safely retried.
959              
960             =head1 USAGE
961              
962             If you are looking for a command line utility, have a look at the docs for prov_virtual. If you are looking to mate an existing Customer Relationship Manager (CRM) or billing system (like Ubersmith, WHMCS, Modernbill, etc..) with a rack full of hardware nodes, this class is it. There are two existing implementations, the prov_virtual CLI, and an RPC agent. The CLI and remote portion of the RPC agent is included in the distribution as bin/remoteagent.
963              
964             =head2 CLI
965              
966             The best way to interface with P:U:V is using a RPC agent to drive this class directly. However, doing so requires a programmer to write an application that accepts/processes requests from your CRMS system and formats them into P:U:V requests.
967              
968             If you don't have the resources to write your own RPC agent, and your CRM/billing software supports it, you may be able to dispatch the requests to the HW nodes via a terminal connection. If you do this, your CRM software will need to inspect the result code of the script to determine success or failure.
969              
970             P:U calls are quiet by default. If you want to see all the logging, append each CLI request with --verbose. Doing so will dump the audit and error reports to stdout.
971              
972             =head2 API
973              
974             The implementation I use implements RPC over SSH. The billing system we use is rather 'limited' so I wrote a separate request brokering application (maitred). It uses the billing systems API as a trigger to perform basic account management actions (create, destroy, enable, disable). We also provide a control panel so our clients can manage their VEs. The control panel also generates requests (start, stop, install, reinstall, reboot, upgrade, etc). Administrators also have their own control panel which also generates requests.
975              
976             When a request is initiated, the broker allocates any necessary resources (IPs, licences, a slot on a HW node, etc) and then dispatches the request. The dispatcher builds an appropriate SSH invocation that connects to the remote HW node and runs the remoteagent. Once connected to the remoteagent, the P:U:V class is loaded and its methods are invoked directly. The RPC agent checks the result code of each call, as well as the audit and error logs, feeding those request events back. The RPC local agent logs the request events into the request brokers database so there's a complete audit trail.
977              
978             =head2 RPC methods
979              
980             RPC is often implemented over HTTP, using SOAP or XML::RPC. However, our VEs are deployed with local storage. We needed the ability to move a VE from one node to another. In addition to the broker to node relationship, we would have also need temporary trust relationships between the nodes, in order to move files between them with root permissions.
981              
982             The trust relationships are much easier to manage with SSH keys. In our environment, only the request brokers are trusted. In addition to being able to connect to any node, they can also connect from node to node using ssh-agent and key forwarding.
983              
984             =head2 RPC gotcha
985              
986             The $vos->migrate() function expects to be running as a user that has the ability to initiate a SSH connection from the node on which it's running, to the node on which you are moving the VE. Our RPC agent connects to the HW nodes as the maitred user and then invokes the remoteagent using sudo. Our sudo config on the HW nodes looks like this:
987              
988             Cmnd_Alias MAITRED_CMND=/usr/bin/remoteagent, /usr/bin/prov_virtual
989             maitred ALL=NOPASSWD: SETENV: MAITRED_CMND
990              
991             Since the RPC remoteagent is running as root, the request broker has access to a wide variety of tools (tar over ssh pipe, rsync, etc) to move files from one node to another, without the nodes having any sort of trust relationship between them.
992              
993             =head1 FUNCTIONS
994              
995             $vos->create(
996             name => 42,
997             ip => '10.0.0.42',
998             hostname => 'vps.example.com',
999             disk_size => 4096, # 4GB
1000             ram => 512,
1001             template => 'debian-5-i386-default',
1002             password => 't0ps3kretWerD',
1003             nameservers => '10.0.0.2 10.0.0.3',
1004             searchdomain => 'example.com',
1005             );
1006             $vos->start( name => 42 );
1007             $vos->stop( name => 42 );
1008             $vos->restart( name => 42 );
1009              
1010             $vos->enable( name => 42 );
1011             $vos->disable( name => 42 );
1012              
1013             $vos->set_hostname( name => 42, hostname => 'new-host.example.com' );
1014             $vos->set_nameservers( name => 42, nameservers => '10.1.1.3 10.1.1.4' );
1015             $vos->set_password( name => 42, password => 't0ps3kretWerD' );
1016             $vos->set_ssh_key( name => 42, ssh_key => 'ssh-rsa AAAAB3N..' );
1017              
1018             $vos->modify(
1019             name => 42,
1020             disk_size => 4000,
1021             hostname => 'new-host.example.com',
1022             ip => '10.1.1.43 10.1.1.44',
1023             ram => 768,
1024             );
1025              
1026             $vos->get_status( name => 42 );
1027             $vos->migrate( name => 42, new_node => '10.1.1.13' );
1028             $vos->destroy( name => 42 );
1029              
1030             =head2 create
1031              
1032             ##############
1033             # Usage : $vos->create( name => '42', ip=>'127.0.0.2' );
1034             # Purpose : create a virtual OS instance
1035             # Returns : true or undef on failure
1036             # Parameters :
1037             # Required : name - name/ID of the virtual OS
1038             # : ip - IP address(es), space delimited
1039             # Optional : hostname - the FQDN of the virtual OS
1040             # : disk_root - the root directory of the virt os
1041             # : disk_size - disk space allotment in MB
1042             # : ram - in MB
1043             # : cpu - how many CPU cores the VE can use/see
1044             # : template - a 'template' or tarball the OS is patterned after
1045             # : config - a config file with virtual specific settings
1046             # : password - the root/admin password for the virtual
1047             # : ssh_key - ssh public key for root user
1048             # : mac_address - the MAC adress to assign to the vif
1049             # : nameservers -
1050             # : searchdomain -
1051             # : kernel_version -
1052             # : skip_start - do not start the VE after creation
1053              
1054             =head2 start
1055              
1056             # Usage : $vos->start( name => '42' );
1057             # Purpose : start a virtual OS instance
1058             # Returns : true or undef on failure
1059             # Parameters :
1060             # Required : name
1061              
1062             =head2 stop
1063              
1064             # Usage : $vos->stop( name => '42' );
1065             # Purpose : stop a virtual OS instance
1066             # Returns : true or undef on failure
1067             # Parameters :
1068             # Required : name
1069              
1070             =head2 restart
1071              
1072             # Usage : $vos->restart( name => '42' );
1073             # Purpose : restart a virtual OS instance
1074             # Returns : true or undef on failure
1075             # Parameters :
1076             # Required : name
1077              
1078             =head2 enable
1079              
1080             # Usage : $vos->enable( name => '42' );
1081             # Purpose : enable/reactivate/unsuspend a virtual OS instance
1082             # Returns : true or undef on failure
1083             # Parameters :
1084             # Required : name
1085              
1086             =head2 disable
1087              
1088             # Usage : $vos->disable( name => '42' );
1089             # Purpose : disable a virtual OS instance
1090             # Returns : true or undef on failure
1091             # Parameters :
1092             # Required : name
1093              
1094             =head2 set_hostname
1095              
1096             # Usage : $vos->set_hostname(
1097             # name => '42',
1098             # hostname => '42.example.com',
1099             # );
1100             # Purpose : update the hostname of a VE
1101             # Returns : true or undef on failure
1102             # Parameters :
1103             # Required : name
1104             # : hostname - the new FQDN for the virtual OS
1105              
1106             =head2 set_nameservers
1107              
1108             # Usage : $vos->set_nameservers(
1109             # name => '42',
1110             # nameservers => '10.0.1.4 10.0.1.5',
1111             # searchdomain => 'example.com',
1112             # );
1113             # Purpose : update the nameservers in /etc/resolv.conf
1114             # Returns : true or undef on failure
1115             # Parameters :
1116             # Required : name
1117             # : nameservers - space delimited list of IPs
1118             # Optional : searchdomain - space delimited list of domain names
1119              
1120             =head2 set_password
1121              
1122             # Usage : $vos->set_password(
1123             # name => '42',
1124             # password => 't0ps3kretWerD',
1125             # );
1126             # Purpose : update the password of a user inside a VE
1127             # Returns : true or undef on failure
1128             # Parameters :
1129             # Required : name
1130             # : password - the plaintext password to store in /etc/shadow|passwd
1131             # Optional : user - /etc/password user name, defaults to 'root'
1132             # : ssh_key - an ssh public key, to install in ~/.ssh/authorized_keys
1133             # : disk_root- the full to the VE root (ie, / within the VE)
1134              
1135             =head2 set_ssh_key
1136              
1137             # Usage : $vos->set_ssh_key(
1138             # name => '42',
1139             # ssh_key => 'ssh-rsa AAAA.....',
1140             # );
1141             # Purpose : install an SSH key for a user inside a VE
1142             # Returns : true or undef on failure
1143             # Parameters :
1144             # Required : name
1145             # : ssh_key - an ssh public key, to install in ~/.ssh/authorized_keys
1146             # Optional : user - /etc/password user name, defaults to 'root'
1147             # : disk_root- the full to the VE root (ie, / within the VE)
1148              
1149             =head2 modify
1150              
1151             # Usage : $vos->modify( name => '42' );
1152             # Purpose : modify a VE
1153             # Returns : true or undef on failure
1154             # Parameters :
1155             # Required : name
1156             # : disk_size
1157             # : hostname
1158             # : ip
1159             # : ram
1160             # Optional : config
1161             # : cpu
1162             # : disk_root
1163             # : mac_address
1164             # : nameservers
1165             # : password
1166             # : searchdomain
1167             # : ssh_key
1168             # : template
1169              
1170             =head2 get_status
1171              
1172             # Usage : $vos->get_status( name => '42' );
1173             # Purpose : get information about a VE
1174             # Returns : a hashref with state info about a VE
1175             # Parameters :
1176             # Required : name
1177             #
1178             # Example result object:
1179             # {
1180             # 'dom_id' => '42',
1181             # 'disk_use' => 560444,
1182             # 'disks' => [
1183             # 'phy:/dev/vol00/42_rootimg,sda1,w',
1184             # 'phy:/dev/vol00/42_vmswap,sda2,w'
1185             # ],
1186             # 'ips' => '10.0.1.42',
1187             # 'cpu_time' => '2699.9',
1188             # 'mem' => 256,
1189             # 'cpus' => '2',
1190             # 'state' => 'running'
1191             # }
1192              
1193             =head2 migrate
1194              
1195             # Usage : $vos->migrate( name => '42', new_node => 'xen5' );
1196             # Purpose : move a VE from one HW node to another
1197             # Returns : true or undef on failure
1198             # Parameters :
1199             # Required : name
1200             # : new_node - hostname of the new node
1201             # Optional : connection_test - don't migrate, just test SSH connectivity
1202             # between the existing and new HW node
1203              
1204             =head2 destroy
1205              
1206             # Usage : $vos->destroy( name => 42 );
1207             # Purpose : destroy a virtual OS instance
1208             # Returns : true or undef on failure
1209             # Parameters :
1210             # Required : name
1211              
1212             =head2 mount
1213              
1214             =head2 unmount
1215              
1216             =head2 publish_arp
1217              
1218             # Usage : $vos->publish_arp( ip => '10.1.0.42' );
1219             # Purpose : update our neighbors with an ARP request for the provided IP(s)
1220             # Parameters :
1221             # Required : ip, can be a string with one IP, or an arrayref
1222              
1223             =head2 create_snapshot
1224              
1225             Create a snapshot of the VE. Only applies to VEs with logical volumes (LVM)
1226              
1227             =head2 destroy_snapshot
1228              
1229             Create disk snapshots. Opposite of create_snapshot.
1230              
1231             =head2 mount_snapshot
1232              
1233             After a snapshot is created, it can be mounted with this method. For xen VEs, the volume is mounted in ~/mnt, which usually looks like this: /home/xen/42/snap
1234              
1235             =head2 unmount_snapshot
1236              
1237             unmounts a snapshot.
1238              
1239             =head2 get_config
1240              
1241             returns an array representing with each line in the VE config file being an element in the array.
1242              
1243             =head1 BUGS
1244              
1245             Please report any bugs or feature requests to C<bug-unix-provision-virtualos at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Provision-Unix>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
1246              
1247             =head1 SUPPORT
1248              
1249             You can find documentation for this module with the perldoc command.
1250              
1251             perldoc Provision::Unix::VirtualOS
1252              
1253             You can also look for information at:
1254              
1255             =over 4
1256              
1257             =item * RT: CPAN's request tracker
1258              
1259             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Provision-Unix-VirtualOS>
1260              
1261             =item * AnnoCPAN: Annotated CPAN documentation
1262              
1263             L<http://annocpan.org/dist/Provision-Unix-VirtualOS>
1264              
1265             =item * CPAN Ratings
1266              
1267             L<http://cpanratings.perl.org/d/Provision-Unix-VirtualOS>
1268              
1269             =item * Search CPAN
1270              
1271             L<http://search.cpan.org/dist/Provision-Unix-VirtualOS>
1272              
1273             =back
1274              
1275             =head1 AUTHOR
1276              
1277             Matt Simerson <msimerson@cpan.org>
1278              
1279             =head1 COPYRIGHT AND LICENSE
1280              
1281             This software is copyright (c) 2014 by The Network People, Inc..
1282              
1283             This is free software; you can redistribute it and/or modify it under
1284             the same terms as the Perl 5 programming language system itself.
1285              
1286             =cut