The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

#
# (c) Jan Gehring <jan.gehring@gmail.com>
#
=head1 NAME
Rex::Commands::Box - Functions / Class to manage Virtual Machines
=head1 DESCRIPTION
This is a Module to manage Virtual Machines or Cloud Instances in a simple way. Currently it supports Amazon, KVM and VirtualBox.
Version <= 1.0: All these functions will not be reported.
=head1 SYNOPSIS
use Rex::Commands::Box;
set box => "VBox";
group all_my_boxes => map { get_box($_->{name})->{ip} } list_boxes;
task mytask => sub {
box {
my ($box) = @_;
$box->name("boxname");
$box->network(1 => {
type => "nat",
});
$box->network(1 => {
type => "bridged",
bridge => "eth0",
});
$box->forward_port(ssh => [2222, 22]);
$box->share_folder(myhome => "/home/myuser");
$box->auth(
user => "root",
password => "box",
);
$box->setup(qw/task_to_customize_box/);
};
};
=head1 EXPORTED FUNCTIONS
=cut
use v5.12.5;
our $VERSION = '1.16.0'; # VERSION
use YAML;
use Rex::Commands -no => [qw/auth/];
$|++;
################################################################################
# BEGIN of class methods
################################################################################
require Exporter;
use base qw(Exporter);
use vars qw(@EXPORT %vm_infos $VM_STRUCT);
#@EXPORT = qw(box $box);
@EXPORT = qw(box list_boxes get_box boxes);
Rex::Config->register_set_handler(
"box",
sub {
my ( $type, @data ) = @_;
Rex::Config->set( "box_type", $type );
if ( ref( $data[0] ) ) {
Rex::Config->set( "box_options", $data[0] );
}
else {
Rex::Config->set( "box_options", {@data} );
}
}
);
=head2 new(name => $box_name)
Constructor if used in OO mode.
my $box = Rex::Commands::Box->new(name => "box_name");
=cut
sub new {
my $class = shift;
return Rex::Box->create(@_);
}
=head2 box(sub {})
With this function you can create a new Rex/Box. The first parameter of this function is the Box object. With this object you can define your box.
box {
my ($box) = @_;
$box->name("boxname");
$box->network(1 => {
type => "nat",
});
$box->network(1 => {
type => "bridged",
bridge => "eth0",
});
$box->forward_port(ssh => [2222, 22]);
$box->share_folder(myhome => "/home/myuser");
$box->auth(
user => "root",
password => "box",
);
$box->setup(qw/task_to_customize_box/);
};
=cut
sub box(&) {
my $code = shift;
#### too much black magic...
#my ($caller_box) = do {
# my $pkg = caller();
# no strict 'refs';
# \*{ $pkg . "::box" };
#};
my $self = Rex::Box->create;
#local( *$caller_box );
#*$caller_box = \$self;
$code->($self);
#*$caller_box = \{}; # undef $box
$self->import_vm();
$self->provision_vm();
return $self->ip;
}
=head2 list_boxes
This function returns an array of hashes containing all information that can be gathered from the hypervisor about the Rex/Box. This function doesn't start a Rex/Box.
use Data::Dumper;
task "get_infos", sub {
my @all_boxes = list_boxes;
print Dumper(\@all_boxes);
};
=cut
sub list_boxes {
my $box = Rex::Box->create;
my @ret = $box->list_boxes;
my $ref = LOCAL {
if ( -f ".box.cache" ) {
my $yaml_str = eval { local ( @ARGV, $/ ) = (".box.cache"); <>; };
$yaml_str .= "\n";
my $yaml_ref = Load($yaml_str);
for my $box ( keys %{$yaml_ref} ) {
my ($found_box) = grep { $_->{name} eq $box } @ret;
if ( !$found_box ) {
$yaml_ref->{$box} = undef;
delete $yaml_ref->{$box};
}
}
open( my $fh, ">", ".box.cache" ) or die($!);
print $fh Dump($yaml_ref);
close($fh);
}
if (wantarray) {
return @ret;
}
return \@ret;
};
return @{$ref};
}
=head2 get_box($box_name)
This function tries to gather all information of a Rex/Box. This function also starts a Rex/Box to gather all information of the running system.
use Data::Dumper;
task "get_box_info", sub {
my $data = get_box($box_name);
print Dumper($data);
};
=cut
sub get_box {
my ($box_name) = @_;
my $box = Rex::Box->create( name => $box_name );
$box->info;
if ( $box->status eq "stopped" ) {
$box->start;
$box->wait_for_ssh;
}
my $box_ip = $box->ip;
my $box_info = $box->info;
return LOCAL {
if ( -f ".box.cache" ) {
Rex::Logger::debug("Loading box information of cache file: .box.cache.");
my $yaml_str = eval { local ( @ARGV, $/ ) = (".box.cache"); <>; };
$yaml_str .= "\n";
my $yaml_ref = Load($yaml_str);
%vm_infos = %{$yaml_ref};
}
if ( exists $vm_infos{$box_name} ) {
return $vm_infos{$box_name};
}
my $pid = fork;
if ( $pid == 0 ) {
print
"Gathering system information from $box_name.\nThis may take a while..";
while (1) {
print ".";
sleep 1;
}
CORE::exit;
}
my $old_q = $::QUIET;
$::QUIET = 1;
eval {
$vm_infos{$box_name} = run_task "Commands:Box:get_sys_info",
on => $box_ip;
} or do {
$::QUIET = $old_q;
print STDERR "\n";
Rex::Logger::info(
"There was an error connecting to your Box. Please verify the login credentials.\nERROR: $@\n",
"warn"
);
Rex::Logger::debug(
"You have to define login credentials before calling get_box()");
# cleanup
kill 9, $pid;
CORE::exit(1);
};
$::QUIET = $old_q;
for my $key ( keys %{$box_info} ) {
$vm_infos{$box_name}->{$key} = $box_info->{$key};
}
open( my $fh, ">", ".box.cache" ) or die($!);
print $fh Dump( \%vm_infos );
close($fh);
kill 9, $pid;
print "\n";
return $vm_infos{$box_name};
};
}
=head2 boxes($action, @data)
With this function you can control your boxes. Currently there are 3 actions.
=over 4
=item init
This action can only be used if you're using a YAML file to describe your Rex/Boxes.
task "prepare_boxes", sub {
boxes "init";
};
=item start
This action start one or more Rex/Boxes.
task "start_boxes", sub {
boxes "start", "box1", "box2";
};
=item stop
This action stop one or more Rex/Boxes.
task "stop_boxes", sub {
boxes "stop", "box1", "box2";
};
=back
=cut
sub boxes {
my ( $action, @data ) = @_;
if ( substr( $action, 0, 1 ) eq "-" ) {
$action = substr( $action, 1 );
}
if ( $action eq "init" ) {
if ( -f ".box.cache" ) {
unlink ".box.cache";
}
my $yaml_ref = $VM_STRUCT;
my @vms;
if ( ref $yaml_ref->{vms} eq "HASH" ) {
for my $vm ( keys %{ $yaml_ref->{vms} } ) {
push(
@vms,
{
name => $vm,
%{ $yaml_ref->{vms}->{$vm} }
}
);
}
}
else {
@vms = @{ $yaml_ref->{vms} };
}
my $box_vms = {};
for my $vm_ref (@vms) {
my $vm = $vm_ref->{name};
box {
my ($box) = @_;
$box->name($vm);
for my $key ( keys %{$vm_ref} ) {
if ( ref( $vm_ref->{$key} ) eq "HASH" ) {
$box->$key( %{ $vm_ref->{$key} } );
}
elsif ( ref( $vm_ref->{$key} ) eq "ARRAY" ) {
$box->$key( @{ $vm_ref->{$key} } );
}
else {
$box->$key( $vm_ref->{$key} );
}
}
$box_vms->{$vm} = $box;
};
}
return $box_vms;
}
if ( $action eq "stop" ) {
for my $box (@data) {
my $o = Rex::Commands::Box->new( name => $box );
$o->stop;
}
}
if ( $action eq "start" ) {
for my $box (@data) {
my $o = Rex::Commands::Box->new( name => $box );
$o->start;
}
}
}
task 'get_sys_info', sub {
return { get_system_information() };
}, { dont_register => 1, exit_on_connect_fail => 0 };
sub load_init_file {
my ( $class, $file ) = @_;
if ( !-f $file ) {
die("Error: Wrong configuration file: $file.");
}
my $yaml_str = eval { local ( @ARGV, $/ ) = ($file); <>; };
$yaml_str .= "\n";
my $yaml_ref = Load($yaml_str);
if ( !exists $yaml_ref->{type} ) {
die("You have to define a type.");
}
my $type = ucfirst $yaml_ref->{type};
set box_type => $type;
# set special box options, like amazon out
if ( exists $yaml_ref->{"\L$type"} ) {
set box_options => $yaml_ref->{"\L$type"};
}
elsif ( exists $yaml_ref->{$type} ) {
set box_options => $yaml_ref->{$type};
}
$VM_STRUCT = $yaml_ref;
}
sub import {
my ( $class, %option ) = @_;
if ( $option{init_file} ) {
my $file = $option{init_file};
$class->load_init_file($file);
@_ = ($class);
}
__PACKAGE__->export_to_level( 1, @_ );
}
1;