The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Provision::Unix::VirtualOS::Linux;
# ABSTRACT: a framework for building Linux virtual machines

use strict;
use warnings;

our $VERSION = '0.27';

use File::Copy;
use File::Path;
#use English qw( -no_match_vars );
use Params::Validate qw(:all);

use lib 'lib';
use Provision::Unix;

my ($prov, $vos, $util);
my %std_opts = ( debug => 0, fatal => 0 );

sub new {
    my $class = shift;
    my %p = validate(@_, { vos => { type => OBJECT } } );

    $vos  = $p{vos};
    $prov = $vos->{prov};
    $util = $vos->{util};

    my $self = { 
        vos  => $vos, 
        util => $util,
    };
    bless $self, $class;

    $prov->audit( $class . sprintf( " loaded by %s, %s, %s", caller ) );

    return $self;
}

sub get_distro {
    my $self = shift;
    my $fs_root = shift;
    return if ! $fs_root;
    my $etc = "$fs_root/etc";
    return if ! -d "$fs_root/etc";

# credit to Max Vohra for distro detection logic 
    return 
        -e "$etc/debian_version"    ? 'debian'
      : -e "$etc/redhat-release"    ? 'redhat'      
      : -e "$etc/SuSE-release"      ? 'suse'
      : -e "$etc/slackware-version" ? 'slackware'
      : -e "$etc/gentoo-release"    ? 'gentoo'
      : -e "$etc/arch-release"      ? 'arch'
      : undef;
};

sub get_package_manager {
    my $distro = shift or return;
    return $distro eq 'debian'    ? 'apt'
         : $distro eq 'redhat'    ? 'yum'
         : $distro eq 'suse'      ? 'zypper'
         : $distro eq 'slackware' ? undef
         : $distro eq 'gentoo'    ? 'emerge'
         : $distro eq 'arch'      ? 'packman'
         : return;
};

sub install_kernel_modules {
    my $self = shift;
    my %p = validate(@_,
        {   fs_root   => { type => SCALAR, },
            url       => { type => SCALAR, optional => 1 },
            version   => { type => SCALAR, optional => 1 },
            test_mode => { type => BOOLEAN, optional => 1 },
        },
    );

    my $fs_root = $p{fs_root};
    my $url     = $p{url} || 'http://mirror.vpslink.com/xen';
    my $version = $p{version} = `uname -r`; chomp $version;

    return 1 if $p{test_mode};

    if ( -d "/boot/domU" ) {
        my ($modules) = glob("/boot/domU/modules*$version*");
        $modules or return $prov->error( 
            "unable to find kernel modules in /boot/domU", %std_opts);
        my $module_dir = "$fs_root/lib/modules";
        if ( ! -d $module_dir ) {
            mkpath $module_dir 
                or return $prov->error("unable to create $module_dir", %std_opts);
        };
        my $cmd = "tar -zxpf $modules -C $module_dir";
        $util->syscmd( $cmd, %std_opts ) or return;
    }
    else {
        chdir $fs_root;
        foreach my $mod ( qw/ modules headers / ) {
#    foreach my $mod ( qw/ modules module-fuse headers / ) {
            next if $mod eq 'headers' && ! "$fs_root/usr/src";
            my $file = "xen-$mod-$version.tar.gz";
            $util->get_url( "$url/$file", %std_opts ) or return;
            $util->syscmd( "tar -zxpf $file -C $fs_root", %std_opts ) or return;
            unlink $file;
        };
        chdir "/home/xen";
    };

    # clean up behind template authors
    unlink "$fs_root/.bash_history" if -e "$fs_root/.bash_history";
    unlink "$fs_root/root/.bash_history" if -e "$fs_root/root/.bash_history";
    return 1;
};

sub set_rc_local {
    my $self = shift;
    my %p = validate(@_, { fs_root => { type => SCALAR } } );

    my $fs_root = $p{fs_root};

    my $rc_local = "$fs_root/etc/conf.d/local.start"; # gentoo
    if ( ! -f $rc_local ) {
        $rc_local = "$fs_root/etc/rc.local";  # everything else
    };

    return $util->file_write( $rc_local, 
        lines  => [ 'pkill -9 -f nash', 
                    'ldconfig > /dev/null', 
                    'depmod -a > /dev/null', 
                    'exit 0',
                  ],
        mode   => '0755',
        append => 0,
        %std_opts,
    );
};

sub set_ips {
    my $self = shift;
    my %p = validate(@_,
        {   ips       => { type => ARRAYREF },
            fs_root   => { type => SCALAR },
            distro    => { type => SCALAR,  optional => 1 },
            device    => { type => SCALAR,  optional => 1 },
            hostname  => { type => SCALAR,  optional => 1 },
            test_mode => { type => BOOLEAN, optional => 1 },
        }
    );

    my $distro = delete $p{distro};
    $distro ||= $self->get_distro( $p{fs_root} );

    return 1 if $p{test_mode};

    return $self->set_ips_debian(%p) if $distro =~ /debian|ubuntu/i;
    return $self->set_ips_redhat(%p) if $distro =~ /redhat|fedora|centos/i;
    return $self->set_ips_gentoo(%p) if $distro =~ /gentoo/i;

    $prov->error( "unable to set up networking on distro $distro", %std_opts );
    return;
};

sub set_ips_debian {
    my $self = shift;
    my %p = validate(@_,
        {   ips       => { type => ARRAYREF },
            fs_root   => { type => SCALAR },
            device    => { type => SCALAR,  optional => 1 },
            hostname  => { type => SCALAR,  optional => 1 },
            test_mode => { type => BOOLEAN, optional => 1 },
        }
    );

    my $device = $p{device} || 'eth0';
    my @ips = @{ $p{ips} };
    my $test_mode = $p{test_mode};
    my $hostname = $p{hostname};
    my $fs_root  = $p{fs_root};

    my $ip = shift @ips;
    my @octets = split /\./, $ip;
    my $gw  = "$octets[0].$octets[1].$octets[2].1";
    my $net = "$octets[0].$octets[1].$octets[2].0";

    my $config = <<EO_FIRST_IP
# This configuration file is generated by Provision::Unix.
# WARNING: Do not edit this file, else your changes will be lost.

# Auto generated interfaces
auto $device lo
iface lo inet loopback
iface $device inet static
    address $ip
    netmask 255.255.255.0
    up route add -net $net netmask 255.255.255.0 dev $device
    up route add default gw $gw
EO_FIRST_IP
;

    my $alias_count = 0;
    foreach ( @ips ) {
        $config .= <<EO_ADDTL_IPS

auto $device:$alias_count
iface $device:$alias_count inet static
    address $_
    netmask 255.255.255.255
    broadcast 0.0.0.0
EO_ADDTL_IPS
;
        $alias_count++;
    };
    #return $config;

    my $config_file = "/etc/network/interfaces";
    return $config if $test_mode;

    if ( $util->file_write( "$fs_root/$config_file", 
            lines => [ $config ], 
            %std_opts,
            ) 
        ) 
    {
        $prov->audit( "updated debian $config_file with network settings");
    }
    else {
        $prov->error( "failed to update $config_file with network settings", %std_opts);
    };

    if ( $hostname) {
        $self->set_hostname_debian( host => $hostname, fs_root => $fs_root );
    };
    return $config;
};

sub set_ips_gentoo {
    my $self = shift;
    my %p = validate(@_,
        {   ips       => { type => ARRAYREF },
            fs_root   => { type => SCALAR },
            device    => { type => SCALAR,  optional => 1 },
            gw_octet  => { type => SCALAR,  optional => 1 },
            hostname  => { type => SCALAR,  optional => 1 },
            test_mode => { type => BOOLEAN, optional => 1 },
        }
    );

    my $device   = $p{device} || 'eth0';
    my @ips      = @{ $p{ips} };
    my $test_mode= $p{test_mode};
    my $hostname = $p{hostname};
    my $fs_root  = $p{fs_root};

    my $ip       = shift @ips;
    my @octets   = split /\./, $ip;
    my $gw_octet = $p{gw_octet} || 1;
    my $gw       = "$octets[0].$octets[1].$octets[2].$gw_octet";

    my $conf_dir = "$fs_root/etc/conf.d";
    my $net_conf = "$conf_dir/net";

    my (@lines, @new_lines);
    if ( -r $net_conf ) {
        @lines = $util->file_read( $net_conf, %std_opts )
            or $prov->error("error trying to read /etc/conf.d/net", %std_opts);
    };
    foreach ( @lines ) {
        next if $_ =~ /^config_$device/;
        next if $_ =~ /^routes_$device/;
        push @new_lines, $_;
    };
    my $ip_string = "config_$device=( \n\t\"$ip/24\"";
    foreach ( @ips ) { $ip_string .= "\n\t\"$_/32\""; };
    $ip_string .= ")";
    push @new_lines, $ip_string;
    push @new_lines, "routes_$device=(\n\t\"default via $gw\"\n)";
    $prov->audit("net config: $ip_string");
    $util->file_write( $net_conf, lines => \@new_lines, %std_opts )
        or return $prov->error(
        "error setting up networking, unable to write to $net_conf", %std_opts);

    return 1;
    #my $script = "/etc/runlevels/default/net.$device";
};

sub set_ips_redhat {
    my $self = shift;
    my %p = validate(@_,
        {   ips       => { type => ARRAYREF },
            fs_root   => { type => SCALAR },
            device    => { type => SCALAR,  optional => 1 },
            gw_octet  => { type => SCALAR,  optional => 1 },
            hostname  => { type => SCALAR,  optional => 1 },
            test_mode => { type => BOOLEAN, optional => 1 },
        }
    );

    my $etc       = "$p{fs_root}/etc";
    my $device    = $p{device} || 'eth0';
    my @ips       = @{ $p{ips} };
    my $hostname  = $p{hostname} || 'localhost';
    my $test_mode = $p{test_mode};

    my $ip = shift @ips;
    my @octets = split /\./, $ip;
    my $gw_octet = $p{gw_octet} || 1;
    my $gw  = "$octets[0].$octets[1].$octets[2].$gw_octet";
    my $net = "$octets[0].$octets[1].$octets[2].0";

    my $netfile = "sysconfig/network";
    my $if_file = "sysconfig/network-scripts/ifcfg-$device";
    my $route_f = "sysconfig/network-scripts/route-$device";
    my $errors_before = scalar @{ $prov->{errors} };

    # cleanup any existing files that may no longer be valid
    unlink <$etc/$if_file*>;

    my $contents = <<EO_NETFILE
NETWORKING="yes"
GATEWAY="$gw"
HOSTNAME="$hostname"
EO_NETFILE
;
    return $contents if $test_mode;
    my $r = $util->file_write( "$etc/$netfile", lines => [ $contents ], %std_opts );
    $r ? $prov->audit("updated /etc/$netfile with hostname $hostname and gateway $gw")
       : $prov->error("failed to update $netfile", fatal => 0);

    $contents = <<EO_IF_FILE
DEVICE=$device
BOOTPROTO=static
ONBOOT=yes
IPADDR=$ip
NETMASK=255.255.255.0
EO_IF_FILE
;
    $r = $util->file_write( "$etc/$if_file", lines => [ $contents ], %std_opts );
    $r ? $prov->audit("updated /etc/$if_file with ip $ip")
       : $prov->error("failed to update $if_file", %std_opts);

    $contents = <<EO_ROUTE_FILE
$net/24 dev $device scope host
default via $gw
EO_ROUTE_FILE
;
    $r = $util->file_write( "$etc/$route_f", lines => [ $contents ], %std_opts );
    $r ? $prov->audit("updated /etc/$route_f with net $net and gw $gw")
       : $prov->error("failed to update $route_f", %std_opts);

    my $alias_count = 0;
    foreach ( @ips ) {
        $if_file = "sysconfig/network-scripts/ifcfg-$device:$alias_count";
        $contents = <<EO_IF_FILE
DEVICE=$device:$alias_count
BOOTPROTO=static
ONBOOT=yes
IPADDR=$_
NETMASK=255.255.255.0
EO_IF_FILE
;
        $alias_count++;
        $r = $util->file_write( "$etc/$if_file", lines => [ $contents ], %std_opts );
        $r ? $prov->audit("updated /etc/$if_file with device $device and ip $_")
           : $prov->error("failed to update $if_file", %std_opts);
    };
    return if scalar @{ $prov->{errors}} > $errors_before;
    return 1;
};

sub set_hostname {
    my $self = shift;
    my %p = validate(@_,
        {   host    => { type => SCALAR },
            fs_root => { type => SCALAR },
            distro  => { type => SCALAR, optional => 1 },
        }
    );

    my $distro = delete $p{distro} || $self->get_distro( $p{fs_root} );
    return $self->set_hostname_debian(%p) if $distro =~ /debian|ubuntu/i;
    return $self->set_hostname_redhat(%p) if $distro =~ /redhat|fedora|centos/i;
    return $self->set_hostname_gentoo(%p) if $distro =~ /gentoo/i;

    $prov->error( "unable to set hostname on distro $distro", %std_opts );
    return;
};

sub set_hostname_debian {
    my $self = shift;
    my %p = validate(@_,
        {   host    => { type => SCALAR },
            fs_root => { type => SCALAR },
        }
    );

    my $host    = $p{host};
    my $fs_root = $p{fs_root};

    $util->file_write( "$fs_root/etc/hostname" , 
        lines => [ $host ], 
        %std_opts,
    )
    or return $prov->error("unable to set hostname", %std_opts );

    $prov->audit("wrote hostname to /etc/hostname");
    return 1;
};

sub set_hostname_gentoo {
    my $self = shift;
    my %p = validate(@_,
        {   host    => { type => SCALAR },
            fs_root => { type => SCALAR },
        }
    );

    my $host    = $p{host};
    my $fs_root = $p{fs_root};

    mkpath "$fs_root/etc/conf.d" if ! "$fs_root/etc/conf.d";

    $util->file_write( "$fs_root/etc/conf.d/hostname" , 
        lines => [ "HOSTNAME=$host" ],
        %std_opts,
    )
    or return $prov->error("error setting hostname", %std_opts);

    $prov->audit("updated /etc/conf.d/hostname with $host");
    return 1;
};

sub set_hostname_redhat {
    my $self = shift;
    my %p = validate(@_,
        {   host    => { type => SCALAR },
            fs_root => { type => SCALAR },
        }
    );

    my $fs_root = $p{fs_root};
    my $host    = $p{host};

    my $config = "$fs_root/etc/sysconfig/network";
    my @new;
    if ( -r $config ) {
        my @lines = $util->file_read( $config, %std_opts );
        foreach ( @lines ) {
            next if $_ =~ /^HOSTNAME/;
            push @new, $_;
        };
    };
    push @new, "HOSTNAME=$host";

    $util->file_write( $config, lines => \@new, %std_opts )
    or return $prov->error("failed to update $config with hostname $host", %std_opts);

    $prov->audit("updated $config with hostname $host");
    return 1;
};

sub set_upstart_console {
    my $self = shift;
    my ($fs_root, $getty_cmd) = @_;

    my $contents = <<EO_INITTAB
#
# This service maintains a getty on xvc0 from the point the system is
# started until it is shut down again.

start on runlevel 2
start on runlevel 3

stop on runlevel 0
stop on runlevel 1
stop on runlevel 4
stop on runlevel 5
stop on runlevel 6

respawn
exec $getty_cmd

EO_INITTAB
;

    $util->file_write( "$fs_root/etc/event.d/xvc0", 
        lines => [ $contents ],
        %std_opts,
    ) or return;
    $prov->audit( "installed /etc/event.d/xvc0" );

    my $serial = "$fs_root/etc/event.d/serial";
    return if ! -e $serial;

    my @lines = $util->file_read( $serial, %std_opts );
    my @new;
    foreach my $line ( @lines ) {
        if ( $line =~ /^[start|stop]/ ) {
            push @new, "#$line";
            next;
        };
        push @new, $line;
    }
    $util->file_write( "$fs_root/etc/event.d/serial", 
        lines => \@new,
        %std_opts,
    ) or return;
    $prov->audit("updated /etc/event.d/serial");
    return;
}

sub setup_inittab {
    my $self = shift;
    my %p = validate(@_, 
        {   fs_root  => { type => SCALAR }, 
            template => { type => SCALAR },
        } 
    );

    my $fs_root = $p{fs_root};
    my $template = $p{template};
    my $login;
    my $tty_dev = 'xvc0';
#    $tty_dev = 'console' 
#        if ( -e "$fs_root/dev/console" && ! -e "$fs_root/dev/xvc0" );

    if ( $template !~ /debian|ubuntu/i ) {
        $login = $self->setup_autologin( fs_root => $fs_root );
    };
    if ( $template =~ /redhat|fedora|centos/ ) {
        $tty_dev = 'console';
    };
    $login ||= -e "$fs_root/bin/bash" ? '/bin/bash' : '/bin/sh';

    my $getty_cmd = -e "$fs_root/sbin/getty" ?
        "/sbin/getty -n -l $login 38400 $tty_dev"
                  : -e "$fs_root/sbin/agetty" ?
        "/sbin/agetty -n -i -l $login $tty_dev 38400"
                  : '/bin/sh';

    # check for upstart
    if ( -e "$fs_root/etc/event.d" ) {   
        $self->set_upstart_console( $fs_root, $getty_cmd );
    };

    my $inittab = "$fs_root/etc/inittab";
    my @lines = $util->file_read( $inittab, %std_opts );
    my @new;
    foreach ( @lines ) {
        next if $_ =~ /^1:/;
        push @new, $_;
    }
    push @new, "1:2345:respawn:$getty_cmd";
    copy $inittab, "$inittab.dist";
    $util->file_write( $inittab, lines => \@new, %std_opts )
        or return $prov->error( "unable to write $inittab", %std_opts);

    $prov->audit("updated /etc/inittab ");
    return 1;
};

sub setup_autologin {
    my $self = shift;
    my %p = validate(@_, { fs_root => { type => SCALAR, } } );

    my $fs_root = $p{fs_root};

    my $auto = <<'EO_C_CODE'
#include <unistd.h>
/* 
  http://wiki.archlinux.org/index.php/Automatically_login_some_user_to_a_virtual_console_on_startup
*/
int main() {
    printf( "%s \n", "Logging on to VPS console. Press Ctrl-] to Quit.");
    printf( "%s \n", "Press Enter to start." );
    execlp( "login", "login", "-f", "root", 0);
}
EO_C_CODE
;

    $util->file_write( "$fs_root/tmp/autologin.c", 
        lines => [ $auto ], 
        %std_opts 
    ) or return;

    my $chroot = $util->find_bin( 'chroot', %std_opts ) or return;
    my $gcc = $util->find_bin( 'gcc', %std_opts ) or return;
    my $cmd = "$chroot $fs_root $gcc -m32 -o /bin/autologin /tmp/autologin.c";
    $util->syscmd( $cmd, %std_opts ) or return;
    unlink "$fs_root/tmp/autologin.c";
    return if ! -x "$fs_root/bin/autologin";
    return '/bin/autologin';
}


1;


__END__
=pod

=head1 NAME

Provision::Unix::VirtualOS::Linux - a framework for building Linux virtual machines

=head1 VERSION

version 1.05

=head1 AUTHOR

Matt Simerson <msimerson@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by The Network People, Inc..

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut