package Provision::Unix::VirtualOS::Linux::Xen;

our $VERSION = '0.70';

use warnings;
use strict;

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

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

my ( $prov, $log, $vos, $linux, $user, $util );

sub new {
    my $class = shift;

    my %p = validate( @_, { 'vos' => { type => OBJECT }, } );

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

    my $self = { 
        'prov'     => $prov,
        'status'   => undef,
        'exists'   => undef,
        'xen_conf' => undef,
    };
    bless( $self, $class );

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

    $vos->{disk_root} ||= '/home/xen';    # xen default

    return $self;
}

sub create {
    my $self = shift;

    $EUID == 0
        or $log->error( "Create function requires root privileges." );

    my $ctid = $vos->{name} or return $log->error( "VE name missing in request!");

    return $log->error( "VE $ctid already exists", fatal => 0 ) 
        if $self->is_present();

    my $template = $self->is_valid_template() or 
        return $log->error( "no valid template specified", fatal => 0 );

    my $err_count_before = @{ $prov->{errors} };
    my $xm = $util->find_bin( 'xm', debug => 0, fatal => 1 );

    return $log->audit("test mode early exit") if $vos->{test_mode};

    $self->create_swap_image() or return;
    $self->create_disk_image() or return;
    $self->mount() or return;

# make sure we trap any errors here and clean up after ourselves.
    my $r;
    $self->extract_template() or $self->unmount() and return;

    my $fs_root  = $self->get_fs_root();
    eval {
        $linux->install_kernel_modules( 
            fs_root => $fs_root, 
            version => $self->get_kernel_version(),
        );
    };
    $log->error( $@, fatal => 0 ) if $@;

    $self->set_ips();

    eval { $linux->set_rc_local( fs_root => $fs_root ); };
    $log->error( $@, fatal => 0 ) if $@;

    eval { 
        $linux->set_hostname(
            host    => $vos->{hostname},
            fs_root => $fs_root,
            distro  => $template,
        );
    };
    $log->error( $@, fatal => 0 ) if $@;

    $vos->set_nameservers() if $vos->{nameservers};

    eval { $self->create_console_user(); };
    $log->error( $@, fatal => 0 ) if $@;

    if ( $vos->{password} ) {
        eval { $self->set_password('setup');  };
        $log->error( $@, fatal=>0) if $@;
    };

    eval { $self->set_fstab(); };
    $log->error( $@, fatal=>0) if $@;

    eval { $self->set_libc(); };
    $log->error( $@, fatal=>0) if $@;

    eval { $linux->setup_inittab( fs_root => $fs_root, template => $template ); };
    $log->error( $@, fatal=>0) if $@;

    eval { $vos->setup_ssh_host_keys( fs_root => $fs_root ); };
    $log->error( $@, fatal=>0) if $@;
   
    eval { $vos->setup_log_files( fs_root => $fs_root ); };
    $log->error( $@, fatal=>0) if $@;

    $self->unmount();

    $self->gen_config()
        or return $log->error( "unable to install config file", fatal => 0 );

    my $err_count_after = @{ $prov->{errors} };
    $self->start() if ! $vos->{skip_start};

    return if $err_count_after > $err_count_before;
    return 1;

}

sub destroy {

    my $self = shift;

    $EUID == 0
        or $log->error( "Destroy function requires root privileges." );

    my $ctid = $vos->{name};

    return $log->error( "VE $ctid does not exist",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
    ) if !$self->is_present( debug => 0 );

    return $log->audit("\ttest mode early exit") if $vos->{test_mode};

    $self->stop() or return;

# sometimes Xen leaves a disk volume marked as in use, this clears that, so
# the unmount can succeed.
    $self->stop_forcefully();  
    $self->unmount() or return $log->error("could not unmount disk image");

    $log->audit("\tctid '$ctid' is stopped. Nuking it...");
    $self->destroy_snapshot() or return;
    $self->destroy_disk_image() or return;
    $self->destroy_swap_image() or return;

    my $ve_home = $self->get_ve_home() or
        $log->error( "could not deduce the VE home dir" );

    return 1 if ! -d $ve_home;

    $self->destroy_console_user();
    if ( -d $ve_home ) {
        my $rm = $util->find_bin( 'rm', debug => 0 );
        $util->syscmd( "$rm -rf $ve_home",
            debug => 0,
            fatal => $vos->{fatal},
        );
        if ( -d $ve_home ) {
            $log->error( "failed to delete $ve_home" );
        }
    };

    my $ve_name = $self->get_ve_name();
    my $startup = "/etc/xen/auto/$ve_name.cfg";
    unlink $startup if -e $startup;

    return 1;
}

sub start {
    my $self = shift;

    my $ctid  = $vos->{name} or die "name of VE missing!\n";
    my $debug = $vos->{debug};
    my $fatal = $vos->{fatal};

    $log->audit("starting $ctid");

    return $log->error( "ctid $ctid does not exist",
        fatal => $fatal,
        debug => $debug,
    )
    if !$self->is_present( debug => 0 );

    if ( $self->is_running() ) {
        $log->audit("$ctid is already running.");
        return 1;
    };

# disk images often get left mounted, preventing a VE from starting. 
# Try unmounting them, just in case.
    $self->unmount( 'quiet' );

    my $config_file = $self->get_ve_config_path();
    return $log->error( "config file for $ctid at $config_file is missing.")
        if !-e $config_file;

# -c option to xm create will start the vm in a console window. Could be useful
# when doing test VEs to look for errors
    my $cmd = $util->find_bin( 'xm', debug => 0 );
    $cmd .= " create $config_file";
    $util->syscmd( $cmd, debug => 0, fatal => $fatal )
        or $log->error( "unable to start $ctid" );

    foreach ( 1..15 ) {
        return 1 if $self->is_running();
        sleep 1;   # the xm start create returns before the VE is running.
    };
    return 1 if $self->is_running();
    return;
}

sub stop {
    my $self = shift;

    my $ctid = $vos->{name} or die "name of VE missing!\n";

    return $log->error( "$ctid does not exist",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
    ) 
    if !$self->is_present( debug => 0 );

    return $log->audit("$ctid is already shutdown.")
        if ! $self->is_running();

    $log->audit("shutting down $ctid");

    $self->stop_nicely();
    $self->stop_forcefully();

    system "sync";
    foreach ( 1..5 ) {
        last if ! $self->lvm_in_use();  # give the lvm time to sync and detach
        sleep 1;
    };

    return 1 if !$self->is_running();
    $log->error( "failed to stop virtual $ctid", fatal => 0 );
    return;
}

sub stop_nicely {
    my $self = shift;

    my $ve_name = $self->get_ve_name();
    my $xm = $util->find_bin( 'xm', debug => 0 );

    # try a 'friendly' shutdown for 15 seconds
    $util->syscmd( "$xm shutdown -w $ve_name",
        timeout => 15,
        debug   => 0,
        fatal   => 0,
    );

    # xm shutdown may exit before the VE is stopped.
    # wait up to 15 more seconds for VE to shutdown
    foreach ( 1..15 ) {
        last if ! $self->is_running( debug => 0 );
        sleep 1;   
    };
};

sub stop_forcefully {
    my $self = shift;

    my $ctid = $vos->{name} or die "name of VE missing!\n";

    $log->audit("shutting down $ctid");

    my $ve_name = $self->get_ve_name();
    my $xm = $util->find_bin( 'xm', debug => 0 );

    $util->syscmd( "$xm destroy $ve_name",
        timeout => 20,
        fatal => 0,
        debug => 0,
    );

    # xm destroy may exit before the VE is stopped.
    # wait 15 more seconds for it to finish shutting down
    foreach ( 1..15 ) {
        last if ! $self->is_running( debug => 0 );
        sleep 1;   
    };
}

sub restart {
    my $self = shift;

    my $ve_name = $self->get_ve_name();

    $self->stop() or return;
    return $self->start();
}

sub disable {
    my $self = shift;

    my $ctid = $vos->{name};
    $log->audit("disabling $ctid");

    # make sure CTID exists
    return $log->error( "$ctid does not exist",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
    ) if !$self->is_present( debug => 0 );

    # is it already disabled?
    my $config = $self->get_ve_config_path();
    if ( !-e $config && -e "$config.suspend" ) {
        $log->audit( "VE is already disabled." );
        return 1;
    };

    # make sure config file exists
    if ( !-e $config ) {
        return $log->error( "configuration file ($config) for $ctid does not exist",
            fatal => $vos->{fatal},
            debug => $vos->{debug},
        );
    }

    return $log->audit("\ttest mode early exit") if $vos->{test_mode};

    # see if VE is running, and if so, stop it
    if ( $self->is_running() ) {
        $self->stop() or return; 
    };

    move( $config, "$config.suspend" )
        or return $log->error( "\tunable to move file '$config': $!",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
        );

    if ( $vos->{archive} ) {
        my $tar = $util->find_bin( 'tar' );
        my $ve_home = $self->get_ve_home();
        my $fs_root = $self->get_fs_root();
        $self->mount();
        system "$tar -czPf $ve_home/$ctid.tar.gz $fs_root";
        $self->unmount();
        $self->destroy_disk_image();
        $self->destroy_swap_image();
    };

    $log->audit("\tdisabled $ctid.");
    return 1;
}

sub enable {

    my $self = shift;

    my $ctid = $vos->{name};
    $log->audit("enabling $ctid");

    # make sure CTID exists
    return $log->error( "$ctid does not exist",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
    ) if !$self->is_present( debug => 0 );

    # is it already enabled?
    if ( $self->is_enabled() ) {
        $log->audit("\t$ctid is already enabled");
        return $self->start();
    };

    # make sure config file exists
    my $config = $self->get_ve_config_path();
    if ( !-e "$config.suspend" ) {
        return $log->error( "configuration file ($config.suspend) for $ctid does not exist",
            fatal => $vos->{fatal},
            debug => $vos->{debug},
        );
    }

    return $log->audit("\ttest mode early exit") if $vos->{test_mode};

    move( "$config.suspend", $config )
        or return $log->error( "\tunable to move file '$config': $!",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
        );

    if ( $vos->{archive} ) {
#        my $tar = $util->find_bin( 'tar' );
#        my $ve_home = $self->get_ve_home();
#        my $fs_root = $self->get_fs_root();
#        $self->create_disk_image() or return;
#        $self->create_swap_image() or return;
#        $self->mount() or return;
#        system "$tar -xzf $ve_home/$ctid.tar.gz -C $fs_root";
#        $self->unmount();
    };

    return $self->start();
}

sub migrate {
    my $self = shift;

    my $ctid = $vos->{name};
    my $new_node = $vos->{new_node};

    if ( $vos->{connection_test} ) {
        $vos->do_connectivity_test() or return;
        return 1;
    };

    my $status  = $self->get_status();
    my $state   = $status->{state};
    my $running = $state eq 'running' ? 1 : 0;

# mount disk or snapshot
    if ( $running ) {
        $self->create_snapshot() or return;
        $self->mount_snapshot() or do {
            $self->destroy_snapshot();
            return $log->error("failed to mount snapshot", fatal => 0);
        };
    }
    else {
        $self->mount();
    };

# mount remote disk image
    my $ssh = $util->find_bin( 'ssh', debug => 0 );
    my $r_cmd = "$ssh $new_node /usr/bin/prov_virtual --name=$ctid";
    $util->syscmd( "$r_cmd --action=mount", debug => 1 );

# rsync disk contents to new node
    my $fs_root = $self->get_fs_root();
    my $rsync = $util->find_bin( 'rsync', debug => 0 );
    $util->syscmd( "$rsync -a --delete $fs_root/ $new_node:$fs_root/", 
        debug => $vos->{debug}, fatal => 0 ) or return;

    if ( $running ) {
        $self->destroy_snapshot();
        $self->stop();
        $self->mount();
    };

    $util->syscmd( "$rsync -aHAX --delete $fs_root/ $new_node:$fs_root/",
        debug => $vos->{debug}, fatal => 0 ) or return;

# copy over xen config file if it doesn't exist
# TODO: test this before enabling (not used in our environment).
#    my $config  = $self->get_ve_config_path();
#    my $ve_home = $self->get_ve_home();
#    $util->syscmd( "$rsync -av --ignore-existing $config $new_node:$ve_home/",
#        debug => $vos->{debug}, fatal => 0 ) or return;

# restore state to new VE
    if ( $state eq 'running' ) {
        $util->syscmd( "$r_cmd --action=start", debug => 0 );
    }
    elsif ( $state eq 'disabled' ) {
        $util->syscmd( "$r_cmd --action=disable", debug => 0 );
    }
    else {
        $util->syscmd( "$r_cmd --action=unmount", debug => 0 );
    };

    $self->migrate_arp_update();
    $self->unmount();

#   $vos->{archive} = 1;   # tell disable to archive the VPS
    $self->disable();

    $log->audit( "all done" );
    return 1;
};

sub migrate_arp_update {
    my $self = shift;

    my $ctid = $vos->{name};
    my $new_node = $vos->{new_node};

    my $ssh = $util->find_bin( 'ssh', debug => 0 );
    my @ips = grep { /vif$ctid/ } `netstat -rn`;
    my $r_cmd = "$ssh $new_node /usr/bin/prov_virtual";
    foreach my $ip ( @ips ) {
        $util->syscmd( "$r_cmd --name=$ctid --action=publish_arp --ip=$ip" );
    };
};

sub modify {
    my $self = shift;

    my $ctid = $vos->{name};
    $log->audit("modifying $ctid");

    # hostname ips nameservers searchdomain disk_size ram config 

    $self->stop() or return;
    $self->mount() or return;

    my $fs_root = $self->get_fs_root();
    my $hostname = $vos->{hostname};

    $self->set_ips() if $vos->{ip};
    $linux->set_hostname( host => $hostname, fs_root => $fs_root ) 
        if $hostname && ! $vos->{ip};

    $user ||= Provision::Unix::User->new( prov => $prov );

    if ( $user ) {
        $self->set_password_root();
        $self->set_ssh_key();
    };

    $self->gen_config();
    $self->unmount() or return;
    $self->resize_disk_image();
    $self->start() or return;
    return 1;
}

sub reinstall {
    my $self = shift;

    $self->get_xen_config();  # cache value from config file (like mac address)

    $self->destroy()
        or
        return $log->error( "unable to destroy virtual $vos->{name}",
            fatal => $vos->{fatal},
            debug => $vos->{debug},
        );

    return $self->create();
}

sub transition {
    my $self = shift;

# TODO: this method is almost identical to disable. Remove after 10/1/2010
    my $ctid = $vos->{name};
    $log->audit("transitioning $ctid");

    return $log->error( "$ctid does not exist",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
    ) if !$self->is_present( debug => 0 );

    my $config = $self->get_ve_config_path();
    if ( !-e $config && -e "$config.transition" ) {
        $log->audit( "VE is already transitioned." );
        return 1;
    };

    if ( !-e $config ) {
        return $log->error( "configuration file ($config) for $ctid does not exist",
            fatal => $vos->{fatal},
            debug => $vos->{debug},
        );
    }

    if ( $self->is_running() ) {
        $self->stop() or return; 
    };

    move( $config, "$config.transition" )
        or return $log->error( "\tunable to move file '$config': $!",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
        );

    $log->audit("\ttransitioned $ctid.");
    return 1;
}

sub untransition {
    my $self = shift;

# TODO: this method is almost identical to enable. Remove after 10/1/2010
    my $ctid = $vos->{name};
    $log->audit("restoring $ctid");

    return $log->error( "$ctid does not exist",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
    ) if !$self->is_present( debug => 0 );

    if ( $self->is_enabled() ) {
        $log->audit("\t$ctid is already restored");
        return $self->start();
    };

    my $config = $self->get_ve_config_path();
    if ( !-e "$config.transition" ) {
        return $log->error( "configuration file ($config.transition) for $ctid does not exist",
            fatal => $vos->{fatal},
            debug => $vos->{debug},
        );
    }

    move( "$config.transition", $config )
        or return $log->error( "\tunable to move file '$config': $!",
        fatal   => $vos->{fatal},
        debug   => $vos->{debug},
        );

    return $self->start();

    my $arpsend = $util->find_bin( 'arpsend', fatal => 0 );
    if ( -x $arpsend ) {
        my @ips = grep { /vif$ctid/ } `netstat -rn`;
        foreach my $ip ( @ips ) {
            $prov->audit( "$arpsend -U -c2 -i $_ eth0" );
            system "$arpsend -U -c2 -i $_ eth0";
        };

        return 1;
    }
}

sub console {
    my $self = shift;
    my $ve_name = $self->get_ve_name();
    my $cmd = $util->find_bin( 'xm', debug => 0 );
    exec "$cmd console $ve_name";
};

sub create_console_user {
    my $self = shift;

    $user ||= Provision::Unix::User->new( prov => $prov );
    my $username = $self->get_console_username();
    my $ve_home = $self->get_ve_home();
    my $ve_name = $self->get_ve_name();
    my $debug   = $vos->{debug};

    if ( ! $user->exists( $username ) ) { # see if user exists
        $user->create_group( group => $username, debug => $debug );
        $user->create(
            username => $username,
            password => $vos->{password},
            homedir  => $ve_home,
            shell    => -x '/usr/bin/lxxen' ? '/usr/bin/lxxen' : '',
            debug    => $debug,
            gecos    => "System User for $ve_name",
        )
        or return $log->error( "unable to create console user $username", fatal => 0 ); 
        $log->audit("created console user account");
    };   

    my $uid = getpwnam $username;
    if ( $uid ) {
        $util->chown( dir => $ve_home, uid => $uid, fatal => 0 );
    };

    foreach ( qw/ .bashrc .bash_profile / ) {
        $util->file_write( 
            file  => "$ve_home/$_", 
            lines => [ "/usr/bin/sudo /usr/sbin/xm console $ve_name", 'exit' ],
            fatal => 0,
            debug => 0,
        )
        or $log->error( "failed to configure console login script", fatal => 0 );
    }
    $log->audit("installed console login script");

    if ( ! `grep '^$username' /etc/sudoers` ) {
        $util->file_write(
            file   => '/etc/sudoers',
            lines  => [ "$username  ALL=(ALL) NOPASSWD: /usr/sbin/xm console $ve_name" ],
            append => 1,
            mode   => '0440',
            fatal  => 0,
            debug  => 0
        )
        or $log->error( "failed to update sudoers for console login");

        $util->file_write(
            file   => '/etc/sudoers.local',
            lines  => [ "$username  ALL=(ALL) NOPASSWD: /usr/sbin/xm console $ve_name" ],
            append => 1,
            fatal  => 0,
            debug  => 0
        )
        or $log->error( "failed to update sudoers for console login");
        $log->audit("updated sudoers for console account $username");
    };

    $log->audit( "configured remote SSH console" );
    return 1;
};

sub create_disk_image {
    my $self = shift;

    my $image_name = $self->get_disk_image();
    my $image_path = $self->get_disk_image(1);
    my $size = $self->get_disk_size();
    my $ram  = $self->get_ve_ram();

    # create the disk image
    my $cmd = $util->find_bin( 'lvcreate', debug => 0, fatal => 0 ) or return;
    $cmd .= " --size=${size}M --name=${image_name} vol00";
    $util->syscmd( $cmd, debug => 0, fatal => 0 )
        or return $log->error( "unable to create $image_name with: $cmd", fatal => 0 );

    # format as ext3 file system
    my $mkfs = $util->find_bin( 'mkfs.ext3', debug => 0, fatal => 0 ) or return;
    $util->syscmd( "$mkfs $image_path", debug => 0, fatal => 0 )
        or return $log->error( "unable for format disk image", fatal => 0);

    $log->audit("disk image for $vos->{name} created");
    return 1;
}

sub create_snapshot {
    my $self = shift;
    my $ve_name = $self->get_ve_name();

    return $log->error( "VE $ve_name does not exist" )
        if !$self->is_present( debug => 0 );
    my $is_running = $self->is_running();

    my $err;
    my $vol  = $self->get_disk_image();
    my $snap = $vol . '_snap';

    my $xm_bin   = $util->find_bin('xm');
    my $lvremove = $util->find_bin('lvremove');
    my $lvcreate = $util->find_bin('lvcreate');

    # cleanup
    my $snapimg = "/dev/vol00/$snap";
    if ( -e $snapimg ) {
        $self->destroy_snapshot() or return;
    };

    if ( $is_running ) {
        # Sync and pause domain
        my $sync_cmd = "$xm_bin sysrq $ve_name s";
        $log->audit( "syncing domain with: $sync_cmd");
        system $sync_cmd;
        return $log->error( "unable to sync dom $ve_name: $!" ) if $CHILD_ERROR;

        my $pause_cmd = "$xm_bin pause $ve_name";
        $log->audit( "pause VE with: $pause_cmd");
        system $pause_cmd;
        return $log->error( "unable to pause dom $ve_name: $!" ) if $CHILD_ERROR;
    };
    
    # create the snapshot
    my $vol_create = "$lvcreate -l 32 -s -n $snap /dev/vol00/$vol";
    $log->audit( "$vol_create");
    system "$vol_create" and do {
        system "$xm_bin unpause $ve_name";
        return $log->error( "unable to create snapshot of /dev/vol00/$vol: $!" );
    };

    if ( $is_running ) {
        # Unpause domain
        my $resume_cmd = "$xm_bin unpause $ve_name";
        $log->audit( "$resume_cmd" );
        system "$resume_cmd" and $log->error( "unable to unpause domain $ve_name: $!" );
    };
    return 1;
};

sub create_swap_image {
    my $self = shift;

    my $swap_name = $self->get_swap_image();
    my $swap_path = $self->get_swap_image(1);
    my $ram       = $self->get_ve_ram();
    my $size      = $ram * 2;

    # create the swap image
    my $cmd = $util->find_bin( 'lvcreate', debug => 0, fatal => 0 ) or return;
    $cmd .= " --size=${size}M --name=${swap_name} vol00";
    $util->syscmd( $cmd, debug => 0, fatal => 0 )
        or return $log->error( "unable to create $swap_name", fatal => 0 );

    # format the swap file system
    my $mkswap = $util->find_bin( 'mkswap', debug => 0, fatal => 0) or return;
    $util->syscmd( "$mkswap $swap_path", debug => 0, fatal => 0 )
        or return $log->error( "unable to format $swap_name", fatal => 0 );

    $log->audit( "created a $size MB swap partition" );
    return 1;
}

sub destroy_console_user {
    my $self = shift;

    $user ||= Provision::Unix::User->new( prov => $prov );
    my $username = $self->get_console_username();
    my $ve_home = $self->get_ve_home();
    my $ve_name = $self->get_ve_name();

    if ( $user->exists( $username ) ) { # see if user exists
        $user->destroy(
            username => $username,
            homedir  => $ve_home,
            debug    => 0,
            fatal    => 0,
        )
        or return $log->error( "unable to destroy console user $username", fatal => 0 ); 
        $log->audit( "deleted system user $username" );

        $user->destroy_group( group => $username, fatal => 0, debug => 0 );
    };   

    $log->audit( "deleted system user $username" );
    return 1;
};

sub destroy_disk_image {
    my $self = shift;

    my $image_name = $self->get_disk_image();
    my $image_path = $self->get_disk_image(1);

    #$log->audit("checking for presense of disk image $image_name");
    if ( ! -e $image_path ) {
        $log->audit("disk image does not exist: $image_name");
        return 1;
    };

    $log->audit("My name is Inigo Montoya. You killed my father. Prepare to die!");

    my $lvrm  = $util->find_bin( 'lvremove', debug => 0 );
       $lvrm .= " -f vol00/${image_name}";
    my $r = $util->syscmd( $lvrm, debug => 0, fatal => 0 );
    if ( ! $r ) {
        $log->audit("My name is Inigo Montoya. You killed my father. Prepare to die!");
        sleep 3;  # wait a few secs and try again
        $r = $util->syscmd( $lvrm, debug => 0, fatal => 0 )
            and pop @{ $prov->{errors} };  # clear the last error
    };
    $r or return $log->error( "unable to destroy disk image: $image_name" );
    return 1;
}

sub destroy_snapshot {
    my $self = shift;
    my $ve_name = $self->get_ve_name();

    my $vol  = $self->get_disk_image();
    my $snap = $vol . '_snap';

    if ( $self->is_mounted( $snap ) ) {
        # a mounted snapshot cannot be destroyed
        $self->unmount_snapshot() or return;  
    };

    # cleanup
    my $snapimg = "/dev/vol00/$snap";
    if ( -e $snapimg ) {
        my $lvremove = $util->find_bin('lvremove', debug => 0);
        system "$lvremove -f $snapimg";
        return $log->error("could not remove $snapimg") if $CHILD_ERROR;
    }
    return 1;
};

sub destroy_swap_image {
    my $self = shift;

    my $img_name = $self->get_swap_image();
    my $img_path = $self->get_swap_image(1);

    if ( ! -e $img_path ) {
        $log->audit("swap image $img_name does not exist");
        return 1;
    };

    my $lvrm = $util->find_bin( 'lvremove', debug => 0 );
    $util->syscmd( "$lvrm -f vol00/$img_name", debug => 0, fatal => 0 )
        or return $log->error( "unable to destroy swap $img_name", fatal => 0 );
    return 1;
}

sub do_fsck {
    my $self = shift;
    my $image_path = $self->get_disk_image(1);

    my $fsck  = $util->find_bin( 'e2fsck', debug => 0 );
    $util->syscmd( "$fsck -y -f $image_path", debug => 0, fatal => 0 ) or return;
    return 1;
};

sub extract_template {
    my $self = shift;

    my $template = $self->is_valid_template()
        or return $log->error( "no valid template specified", fatal => 0 );

    my $ve_name = $self->get_ve_name();
    my $fs_root = $self->get_fs_root();
    my $template_dir = $self->get_template_dir();

    #tar -zxf $template_dir/$OSTEMPLATE.tar.gz -C /home/xen/$ve_name/mnt

    # untar the template
    my $tar = $util->find_bin( 'tar', debug => 0, fatal => 0 ) or return;
    $tar .= " -zxf $template_dir/$template.tar.gz -C $fs_root";
    $util->syscmd( $tar, debug => 0, fatal => 0 )
        or return $log->error( "unable to extract template $template. Do you have enough disk space?",
            fatal => 0
        );
    return 1;
}

sub gen_config {
    my $self = shift;

    my $ctid        = $vos->{name};
    my $ve_name     = $self->get_ve_name();
    my $config_file = $self->get_ve_config_path();
    #warn "config file: $config_file\n" if $vos->{debug};

    my $ram      = $self->get_ve_ram();
    my $hostname = $vos->{hostname} || $ctid;

    my @ips      = @{ $vos->{ip} };
    my $ip_list  = shift @ips;
    foreach ( @ips ) { $ip_list .= " $_"; };
    my $mac      = $self->get_mac_address();

    my $image_path = $self->get_disk_image(1);
    my $swap_path  = $self->get_swap_image(1);
    my $kernel_dir = $self->get_kernel_dir();
    my $kernel_version = $self->get_kernel_version();

    my ($kernel) = <$kernel_dir/vmlinuz*$kernel_version*>;
    my ($ramdisk) = <$kernel_dir/initrd*$kernel_version*>;
    ($kernel) ||= </boot/vmlinuz-*xen>;
    ($ramdisk) ||= </boot/initrd-*xen.img>;
    my $cpu = $vos->{cpu} || 1;
    my $time_dt = $prov->get_datetime_from_epoch();

    my $config = <<"EOCONF"
# Config file generated by Provision::Unix at $time_dt
kernel     = '$kernel'
ramdisk    = '$ramdisk'
memory     = $ram
name       = '$ve_name'
hostname   = '$hostname'
vif        = ['ip=$ip_list, vifname=vif${ctid},  mac=$mac']
vnc        = 0
vncviewer  = 0
serial     = 'pty'
disk       = ['phy:$image_path,sda1,w', 'phy:$swap_path,sda2,w']
root       = '/dev/sda1 ro'
extra      = 'console=xvc0'
vcpus      = $cpu
EOCONF
;

    # These can also be set in the config file.
    #console    =
    #nics       =
    #dhcp       =

    $util->file_write( 
        file => $config_file, 
        lines => [$config],
        debug => 0,
        fatal => 0,
    ) or return $log->error("unable to install VE config file", fatal => 0);

    link $config_file, "/etc/xen/auto/$ve_name.cfg";
    return 1;
}

sub get_config {
    my $self = shift;

    my $config_file = $self->get_ve_config_path();

    return $util->file_read( 
        file => $config_file, 
        debug => 0,
        fatal => 0,
    ) 
    or return $log->error("unable to read VE config file", fatal => 0);
};

sub get_console_username {
    my $self = shift;
    my $ctid = $vos->{name};
       $ctid .= 'vm';
    return $ctid;
};

sub get_disk_image {
    my $self = shift;
    my $path = shift;
    my $name = $vos->{name} or die "missing VE name!";
    my $image_name = $name . '_rootimg';
    $image_name = '/dev/vol00/' . $image_name if $path;
    return $image_name;
};

sub get_disk_size {
    my $self = shift;
    my $ram = $self->get_ve_ram();
    my $swap = $ram * 2;
    my $allocation = $vos->{disk_size} || 2500;
    return $allocation - $swap; # subtract swap from their disk allotment
};

sub get_disk_usage {
    my $self = shift;
    my $image = shift or return;

    my $cmd = $util->find_bin( 'dumpe2fs', fatal => 0, debug => 0 );
    return if ! -x $cmd;

    $cmd .= " -h $image";
    my $r = `$cmd 2>&1`;
    my ($block_size) = $r =~ /Block size:\s+(\d+)/;
    my ($blocks_tot) = $r =~ /Block count:\s+(\d+)/;
    my ($blocks_free) = $r =~ /Free blocks:\s+(\d+)/; 

    my $disk_total = ( $blocks_tot * $block_size ) / 1024;
    my $disk_free = ( $blocks_free * $block_size ) / 1024;
    my $disk_used = $disk_total - $disk_free;

    return $disk_used;
};

sub get_kernel_dir {
    my $self = shift;
    return '/boot/domU' if -d "/boot/domU";
    return '/boot';
};

sub get_kernel_version {
    my $self = shift;
    return $vos->{kernel_version} if $vos->{kernel_version};
    my $kernel_dir = $self->get_kernel_dir();
    my @kernels = <$kernel_dir/vmlinuz-*xen>;
    my $kernel = $kernels[0];
    my ($version) = $kernel =~ /-([0-9\.\-]+)\./;
    return $log->error("unable to detect a xen kernel (vmlinuz-*xen) in standard locations (/boot, /boot/domU)", fatal => 0) if ! $version;
    $vos->{kernel_version} = $version;
    return $version;
};

sub get_mac_address {
    my $self = shift;
    my $mac = $vos->{mac_address};   # passed in value
    return $mac if $mac;

    # value from VE config file
    my $xen_conf = $self->get_xen_config();
    if ( $xen_conf ) {
        my $vif = $xen_conf->get('vif');
        if ( ref $vif->[0]->{mac} ) {
            return $vif->[0]->{mac}->[0];
        };
        return $vif->[0]->{mac} if $vif->[0]->{mac};
    };

    # both of the previous methods failed, generate a random MAC
    my $i;
    $mac = '00:16:3E';

    while ( ++$i ) {
        last if $i > 6;
        $mac .= ':' if $i % 2;
        $mac .= sprintf "%" . ( qw (X x) [ int( rand(2) ) ] ),
            int( rand(16) );
    }

    # TODO:
    #   make sure random MAC does not conflict with an existing one.

    return $mac;
}

sub get_status {
    my $self = shift;
    my %p = validate( @_, { debug => { type => BOOLEAN, optional => 1 } } );

    my $debug = defined $p{debug} ? $p{debug} : $vos->{debug};

    my $ve_name = $self->get_ve_name();

    $self->{status} = {};    # reset status

    return $self->{status}{$ve_name} if ! $self->is_present( debug => $debug );

    # get IPs and disks from the VE config file
    my ($ips, $vif, $disks, $disk_usage );
    my $config_file = $self->get_ve_config_path();
    if ( ! -e $config_file ) {
        return { state => 'disabled' } if -e "$config_file.suspend";
        return { state => 'transitioned' } if -e "$config_file.transition";

        $log->audit( "\tmissing config file $config_file" );
        return { state => 'broken' };
    };

    my $xen_conf = $self->get_xen_config();
    if ( $xen_conf ) {
        $ips   = $xen_conf->get_ips();
        $disks = $xen_conf->get('disk');
        $vif   = $xen_conf->get('vif');
    };
    foreach ( @$disks ) {
        my ($image) = $_ =~ /phy:(.*?),/;
        next if ! -e $image;
        next if $image =~ /swap/i;
        $disk_usage = $self->get_disk_usage($image);
    };

    my $cmd = $util->find_bin( 'xm', debug => 0 );
    $cmd .= " list $ve_name";
    my $r = `$cmd 2>&1`;

    my $exit_code = $? >> 8;
# xm exit codes: 0=success, 1=fail, 2=unknown

    if ( $r =~ /does not exist/ ) {

        # a Xen VE that is shut down won't show up in the output of 'xm list'
        $self->{status}{$ve_name} = {
            ips      => $ips,
            disks    => $disks,
            state    => 'shutdown',
        };
        return $self->{status}{$ve_name};
    };

    $r =~ /VCPUs/ 
        or $log->error( "could not get valid output from '$cmd'", fatal => 0 );

    foreach my $line ( split /\n/, $r ) {

 # Name                               ID Mem(MiB) VCPUs State   Time(s)
 #test1.vm                            20       63     1 -b----     34.1

        my ( $ctid, $dom_id, $mem, $cpus, $state, $time ) = split /\s+/, $line;
        next unless $ctid;
        next if $ctid eq 'Name';
        next if $ctid eq 'Domain-0';
        next if $ctid ne $ve_name;

        $self->{status}{$ctid} = {
            ips      => $ips,
            disks    => $disks,
            disk_use => $disk_usage,
            dom_id   => $dom_id,
            mem      => $mem + 1,
            cpus     => $cpus,
            state    => _run_state($state),
            cpu_time => $time,
            mac      => $vif->[0]->{mac},
        };
        return $self->{status}{$ctid};
    }

    sub _run_state {
        my $abbr = shift;
        return
              $abbr =~ /r/ ? 'running'
            : $abbr =~ /b/ ? 'running' # blocked is a 'wait' state, poorly named
            : $abbr =~ /p/ ? 'paused'
            : $abbr =~ /s/ ? 'shutdown'
            : $abbr =~ /c/ ? 'crashed'
            : $abbr =~ /d/ ? 'dying'
            :                undef;
    }
}

sub get_swap_image {
    my $self = shift;
    my $path = shift;
    my $name = $vos->{name} or die "missing VE name!";
    my $swap_name = $name . '_vmswap';
    $swap_name = '/dev/vol00/' . $swap_name if $path;
    return $swap_name;
};

sub get_template_dir {
    my $self = shift;

    my $template_dir = $prov->{config}{VirtualOS}{xen_template_dir} || '/templates';
    return $template_dir;
};

sub get_ve_config_path {
    my $self = shift;
    my $ve_name     = $self->get_ve_name();
    my $config_file = "$vos->{disk_root}/$ve_name/$ve_name.cfg";
    return $config_file;
};

sub get_ve_ram {
    my $self = shift;
    return $vos->{ram} if $vos->{ram};  # passed in value

    # value from VE config file
    my $xen_conf = $self->get_xen_config();
    if ( $xen_conf ) {
        my $ram = $xen_conf->get('memory');
        return $ram if $ram;
    };

    return 256;   # default value
};

sub get_fs_root {
    my $self = shift;
    my @caller = caller;
    my $ve_home = $self->get_ve_home( shift )
        or return $log->error( "VE name unset when called by $caller[0] at $caller[2]");
    return "$ve_home/mnt";
};

sub get_snap_root {
    my $self = shift;
    my @caller = caller;
    my $ve_home = $self->get_ve_home( shift )
        or return $log->error( "VE name unset when called by $caller[0] at $caller[2]");
    return "$ve_home/snap";
};

sub get_ve_home {
    my $self = shift;
    my @caller = caller;
    my $ve_name = $self->get_ve_name( shift )
        or return $log->error( "VE name unset when called by $caller[0] at $caller[2]");
    my $homedir = "$vos->{disk_root}/$ve_name";
    return $homedir;
};

sub get_ve_name {
    my $self = shift;
    my @caller = caller;
    my $ctid = shift || $vos->{name}
        or return $log->error( "missing VE name when called by $caller[0] at $caller[2]");
    $ctid .= '.vm';  # TODO: make this a config file option
    return $ctid;
};

sub get_ve_passwd_file {
    my $self = shift;
    my $fs_root = shift || $self->get_fs_root();

    my $pass_file = "$fs_root/etc/shadow";  # SYS 5
    return $pass_file if -f $pass_file;

    $pass_file = "$fs_root/etc/master.passwd";  # BSD
    return $pass_file if -f $pass_file;

    $pass_file = "$fs_root/etc/passwd";
    return $pass_file if -f $pass_file;

    $log->error( "\tcould not find password file", fatal => 0);
    return;
};

sub get_xen_config {
    my $self = shift;
    return $self->{xen_conf} if $self->{xen_conf};   # already got it

    my $config_file = $self->get_ve_config_path();   # no config file, don't bother
    return if ! -f $config_file;

    eval "require Provision::Unix::VirtualOS::Xen::Config";  # won't load
    return if $@;

    my $xen_conf = Provision::Unix::VirtualOS::Xen::Config->new();

    eval { $xen_conf->read_config($config_file); };   # parse the config file
    return if $@;

    $self->{xen_conf} = $xen_conf;                    # cache it
    return $xen_conf;
};

sub is_mounted {
    my $self = shift;
    my $image_name = shift || $self->get_disk_image();

    my $found = `/bin/mount | grep $image_name`; chomp $found;
    if ( $found ) {
        $log->audit( "$image_name is mounted" );
        return 1 
    };
    $log->audit( "$image_name is not mounted" );
    return;
};

sub is_present {
    my $self = shift;
    my %p = validate( @_, { debug => { type => BOOLEAN, optional => 1 } } );

    my $debug   = defined $p{debug} ? $p{debug} : $vos->{debug};
    my $name    = $self->get_ve_name();
    my $ve_home = $self->get_ve_home();

    $log->audit("checking if VE $name exists") if $debug;

    my $image_path = $self->get_disk_image(1);
    my $swap_path = $self->get_swap_image(1);

    my @possible_paths = ( $ve_home, $image_path, $swap_path );

    foreach my $path (@possible_paths) {
        #$log->audit("\tchecking at $path") if $debug;
        if ( -e $path ) {
            $log->audit("\tfound $name at $path") if $debug;
            return $path;
        }
    }

    $self->{status}{$name} = { state => 'non-existent' };

    $log->audit("\tVE $name does not exist");
    return;
}

sub is_running {
    my $self = shift;
    my %p = validate(
        @_, 
        {   refresh => { type => SCALAR,  optional => 1, default => 1 }, 
            debug   => { type => BOOLEAN, optional => 1 },
        }
    );

    my $debug = defined $p{debug} ? $p{debug} : $vos->{debug};
    $self->get_status( debug => $debug ) if $p{refresh};

    my $ve_name = $self->get_ve_name();

    if ( $self->{status}{$ve_name} ) {
        my $state = $self->{status}{$ve_name}{state};
        if ( $state && $state eq 'running' ) {
            $log->audit("$ve_name is running") if $debug;
            return 1;
        };
    }
    $log->audit("$ve_name is not running") if $debug;
    return;
}

sub is_enabled {
    my $self = shift;
    my %p = validate(
        @_,
        {   'name'  => { type => SCALAR,  optional => 1, },
            'debug' => { type => BOOLEAN, optional => 1, default => 1 },
            'fatal' => { type => BOOLEAN, optional => 1, default => 1 },
        }
    );

    my $ve_name     = $p{name} || $self->get_ve_name();
    my $config_file = $self->get_ve_config_path();

    $log->audit("testing if virtual VE $ve_name is enabled");

    if ( -e $config_file ) {
        $log->audit("\tfound $ve_name at $config_file") if $p{debug};
        return 1;
    }

    $log->audit("\tdid not find $config_file");
    return;
}

sub is_valid_template {

    my $self = shift;
    my $template = shift || $vos->{template} or return;

    my $template_dir = $self->get_template_dir();
    return $template if -f "$template_dir/$template.tar.gz";

    # is $template a URL?
    if ( $template =~ /http|rsync/ ) {
        $log->audit("fetching $template");
        my $uri = URI->new($template);
        my @segments = $uri->path_segments;
        my @path_bits = grep { /\w/ } @segments;  # ignore empty fields
        my $file = $segments[-1];

        $prov->audit("fetching $file from " . $uri->host);
        $util->file_get( url => $template, dir => $template_dir, fatal => 0, debug => 0 );

        if ( -f "$template_dir/$file" ) {
            ($file) = $file =~ /^(.*)\.tar\.gz$/;
            return $file;
        };
    }

    return $template if -f "$template_dir/$template.tar.gz";

    return $log->error( "template '$template' does not exist and is not a valid URL",
        debug => $vos->{debug},
        fatal => $vos->{fatal},
    );
}

sub lvm_in_use {
    my $self = shift;
    my $image_name = $self->get_disk_image();
    my $image_path = $self->get_disk_image(1);

    my $lvdisplay = $util->find_bin( 'lvdisplay', debug => 0 );
    my $r = `$lvdisplay $image_path`;
    my ($count) = $r =~ m/# open\s+([0-9])\s+/;

    if ($count) {
        $log->audit( "$image_name is in use" );
        return 1 
    };
    $log->audit( "$image_name is not being used" );
    return;
};

sub mount {
    my $self = shift;

    my $image_name = $self->get_disk_image();
    my $image_path = $self->get_disk_image(1);
    my $ve_name    = $self->get_ve_name();
    my $fs_root    = $self->get_fs_root();

# returns 2 (true) if image is already mounted
    return 2 if $self->is_mounted();

    mkpath $fs_root if !-d $fs_root;

    return $log->error( "unable to create $fs_root", fatal => 0 )
        if ! -d $fs_root;

    sleep 3 if $self->lvm_in_use();

    return $log->error( "LVM is in use, cannot safely mount", fatal => 0 )
        if $self->lvm_in_use();

    $self->do_fsck();

    # mount /dev/vol00/${VE}_rootimg /home/xen/$VE/mnt
    my $mount = $util->find_bin( 'mount', debug => 0, fatal => 0 ) or return;
    $util->syscmd( "$mount $image_path $fs_root", debug => 0, fatal => 0 )
        or return $log->error( "unable to mount $image_name", fatal => 0 );
    return 1;
}

sub mount_snapshot {
    my $self = shift;

    my $image_path = $self->get_disk_image(1);
    my $snap_root  = $self->get_snap_root();
    my $snap_path  = $image_path . '_snap';

    if ( ! -d $snap_root ) {
        mkpath $snap_root 
            or return $log->error( "unable to create snap dir: $snap_root");
    };

    # mount /dev/vol00/${VE}_rootimg_snap /home/xen/$VE/snap
    my $mount = $util->find_bin( 'mount', debug => 0, fatal => 0 ) or return;
    $util->syscmd( "$mount $snap_path $snap_root", debug => 0, fatal => 0 )
        or return $log->error( "unable to mount snapshot", fatal => 0 );

    return 1;
}

sub resize_disk_image {
    my $self = shift;

    my $name = $vos->{name} or die "missing VE name!";

    $self->destroy_swap_image();
    $self->create_swap_image();

    my $image_name = $self->get_disk_image();
    my $image_path = $self->get_disk_image(1);
    my $target_size = $self->get_disk_size();

    # check existing disk size.
    $self->mount() or $log->error( "unable to mount disk image" );
    my $fs_root = $self->get_fs_root();
    my $df_out = qx{/bin/df -m $fs_root | /usr/bin/tail -n1};
    my (undef, $current_size, $df_used, $df_free) = split /\s+/, $df_out;
    $self->unmount();

    my $difference = $target_size - $current_size;

    # return if the same
    return $log->audit( "no disk partition changes required" ) if ! $difference;
    my $percent_diff = abs($difference / $target_size ) * 100;
    return $log->audit( "disk partition is close enough: $current_size vs $target_size" ) 
        if $percent_diff < 5;

    my $pvscan = $util->find_bin( 'pvscan', debug => 0);
    my $resize2fs = $util->find_bin( 'resize2fs', debug => 0 );

    # if new size is bigger
    if ( $target_size > $current_size ) {
# make sure there is sufficient free disk space on the HW node
        my $free = qx{$pvscan};
        $free =~ /(\d+\.\d+)\s+GB\s+free\]/;
        $free = $1 * 1024;
        return $log->error("Not enough disk space on HW node: needs $target_size but only $free MB free. Migrate account and manually increase disk space.") if $free <= $target_size;
        # resize larger
        $log->audit("Extending disk $image_name from $current_size to $target_size");
        my $cmd = "/usr/sbin/lvextend --size=${target_size}M $image_path";
        $log->audit($cmd);
        system $cmd and $log->error( "failed to extend $image_name to $target_size megs");
        $self->do_fsck();
        $cmd = "$resize2fs $image_path";
        $log->audit($cmd);
        system $cmd and $log->error( "unable to resize filesystem $image_name");
        $self->do_fsck();
        return 1;
    }

    if ( $current_size > $target_size ) {
       # see if volume can be safely shrunk - per SA team: Andrew, Ryan, & Ted

# if cannot be safely shrunk, fail.
        return $log->error( "volume has more than $target_size used, failed to shrink" ) 
            if $df_used > $target_size;

        # shrink it
        $log->audit( "Reducing $image_name from $current_size to $target_size MB");
        $self->do_fsck();
        my $cmd = "$resize2fs -f $image_path ${target_size}M";
        $log->audit($cmd);
        system $cmd and $log->error( " Unable to resize filesystem $image_name" );
        $log->audit("reduced file system");

        $cmd  = $util->find_bin( 'lvresize', debug => 0 );
        $cmd .= " --size=${target_size}M $image_path";
        $log->audit($cmd);
        #system $cmd and $log->error( "Error:  Unable to reduce filesystem on $image_name" );
        open(FH, "| $cmd" ) or return $log->error("failed to shrink logical volume");
        print FH "y\n";  # deals with the non-suppressible "Are you sure..." 
        close FH;        # waits for the open process to exit
        $log->audit("completed shrinking logical volume size");
        $self->do_fsck();
        return 1;
    };
};

sub set_fstab {
    my $self = shift;

    my $contents = <<EOFSTAB
/dev/sda1               /                       ext3    defaults,noatime 1 1
/dev/sda2               none                    swap    sw       0 0
none                    /dev/pts                devpts  gid=5,mode=620 0 0
none                    /dev/shm                tmpfs   defaults 0 0
none                    /proc                   proc    defaults 0 0
none                    /sys                    sysfs   defaults 0 0
EOFSTAB
;

    my $ve_home = $self->get_ve_home();
    $util->file_write( 
        file => "$ve_home/mnt/etc/fstab", 
        lines => [ $contents ],
        debug => 0,
        fatal => 0,
    ) or return;
    $log->audit("installed /etc/fstab");
    return 1;
};

sub set_hostname {
    my $self = shift;

    $self->stop() or return;
    $self->mount() or return;

    $linux->set_hostname( 
        host    => $vos->{hostname},
        fs_root => $self->get_fs_root(),
    )
    or $log->error("unable to set hostname", fatal => 0);

    $self->unmount();
    $self->start() or return;
    return 1;
};

sub set_ips {
    my $self     = shift;
    my $fs_root  = $self->get_fs_root();
    my $template = $vos->{template};
    my $ctid     = $vos->{name};

    my %request = (
        hostname => $vos->{hostname},
        ips      => $vos->{ip},
        device   => $vos->{net_device} || 'eth0',
        fs_root  => $fs_root,
    );
    $request{distro} = $vos->{template} if $vos->{template};

    eval { $linux->set_ips( %request ); };
    $log->error( $@, fatal => 0 ) if $@;

    # update the config file, if it exists
    my $config_file = $self->get_ve_config_path() or return;
    return if ! -f $config_file;

    my @ips      = @{ $vos->{ip} };
    my $ip_list  = shift @ips;
    foreach ( @ips ) { $ip_list .= " $_"; };
    my $mac      = $self->get_mac_address();

    my @lines = $util->file_read( file => $config_file, debug => 0, fatal => 0) 
        or return $log->error("could not read $config_file", fatal => 0);

    foreach my $line ( @lines ) {
        next if $line !~ /^vif/;
        $line =~ /mac=([\w\d\:\-]+)\'\]/;
        $mac = $1 if $1;   # use the existing mac if possible
        $line = "vif        = ['ip=$ip_list, vifname=vif${ctid},  mac=$mac']";
    };
    $util->file_write( file => $config_file, lines => \@lines, fatal => 0 )
        or return $log->error( "could not write to $config_file", fatal => 0);

    return 1;
};

sub set_libc {
    my $self = shift;

    my $fs_root = $self->get_fs_root();
    my $libdir  = "/etc/ld.so.conf.d";
    my $libfile = "/etc/ld.so.conf.d/libc6-xen.conf";

    if ( ! -f "$fs_root/$libfile" ) {
        if ( ! -d "$fs_root/$libdir" ) {
            mkpath "$fs_root/$libdir" or return 
                $log->error("unable to create $libdir", fatal => 0);
            $log->audit("created $libdir");
        };
        return $log->error("could not create $libdir", fatal => 0) 
            if ! -d "$fs_root/$libdir";
        $util->file_write( 
            file  => "$fs_root/$libfile", 
            lines => [ 'hwcap 0 nosegneg' ], 
            debug => 0 , fatal => 0 )
            or return $log->error("could not install $libfile", fatal => 0);
        $log->audit("installed $libfile");
    };

    if ( -d "$fs_root/lib/tls" ) {
        move( "$fs_root/lib/tls", "$fs_root/lib/tls.disabled" );
        $log->audit("disabled /lib/tls");
    };
    return 1;
};

sub set_password {
    my $self = shift;
    my $arg = shift;

    my $ve_name = $self->get_ve_name();
    my $pass    = $vos->{password}
        or return $log->error( 'no password provided', fatal => 0 );

    return $log->error( "VE $ve_name does not exist",
        fatal => $vos->{fatal},
        debug => $vos->{debug},
    )
    if !$self->is_present( debug => 0 );

    $log->audit("setting VPS password");

    my $i_stopped;
    my $i_mounted;

    if ( ! $arg || $arg ne 'setup' ) {
        if ( $self->is_running( debug => 0 ) ) {
            $self->stop() or return;
            $i_stopped++;
        };
    
        my $r = $self->mount();
        $i_mounted++ if $r == 1;
    }

    my $errors;

    # set the VE root password
    $self->set_password_root()    or $errors++;
    $self->set_ssh_key()          or $errors++;
    $self->set_password_console() or $errors++;

    if ( ! $arg || $arg ne 'setup' ) {
        $self->unmount() if $i_mounted;
        $self->start() if $i_stopped;
    };
    return 1 if ! $errors;
    return;
};

sub set_password_console {
    my $self = shift;

    my $pass    = $vos->{password} or return 1;
    my $ve_name = $self->get_ve_name();

    $user ||= Provision::Unix::User->new( prov => $prov );

    $log->audit( "creating the console account and password." );

    my $username = $ve_name;
    $username =~ s/\.//g;  # strip the . out of the veid name: NNNNN.vm

    return if ! $user->exists( $username );

    my %request = ( username => $username, password => $pass );

    if ( $vos->{ssh_key} ) {
        $request{ssh_key} = $vos->{ssh_key};
        $request{ssh_restricted} = "sudo /usr/sbin/xm console $ve_name";
    };

    $user->set_password( %request, fatal => 0, debug => 0 ) or return;
    return 1;
};

sub set_password_root {
    my $self = shift;

    my $pass = $vos->{password} or return 1;  # no change requested
    my $debug = $vos->{debug};

    $user ||= Provision::Unix::User->new( prov => $prov );

    my $pass_file = $self->get_ve_passwd_file() or return;
    my @lines     = $util->file_read( file => $pass_file, fatal => 0 );

    grep { /^root:/ } @lines 
        or return $log->error( "\tcould not find root password entry in $pass_file!", fatal => 0);

    my $crypted = $user->get_crypted_password($pass);

    foreach ( @lines ) {
        s/root\:.*?\:/root\:$crypted\:/ if m/^root\:/;
    };

    $util->file_write( file => $pass_file, lines => \@lines, debug => $debug, fatal => 0 ) or return;
    $log->audit( "VE root password set." );
    return 1;
};

sub set_ssh_key {
    my $self = shift;

    return 1 if ! $vos->{ssh_key};

    return $log->error( "VE does not exist" ) 
        if !$self->is_present( debug => 0 );

    $user ||= Provision::Unix::User->new( prov => $prov );

    my $fs_root = $self->get_fs_root();
    eval {
        $user->install_ssh_key(
            homedir => "$fs_root/root",
            ssh_key => $vos->{ssh_key},
            debug   => $vos->{debug},
        );
    };
    return $log->error( $@, fatal => 0 ) if $@;
    $log->audit("installed ssh key");
    return 1;
};

sub unmount {
    my $self = shift;
    my $quiet = shift;

    my $debug = $vos->{debug};
    my $fatal = $vos->{fatal};

    return 1 if ! $self->is_mounted();
    $debug = $fatal = 0 if $quiet;

    # snapshots can interfere with unmount operation, try to remove them
    $self->destroy_snapshot();

    my $image_name = $self->get_disk_image();
    my $image_path = $self->get_disk_image(1);

    my $umount = $util->find_bin( 'umount', debug => 0, fatal => $fatal )
        or return $log->error( "unable to find 'umount' program");

    $util->syscmd( "$umount $image_path", debug => 0, fatal => $fatal );

    sleep 5 if ( $self->is_mounted );
    if ( $self->is_mounted ) { 
        $log->error( "unable to unmount $image_name" ) if ! $quiet;
        return;
    };

    $debug = $fatal = 0 if $quiet;
    $self->do_fsck();
    return 1;
}

sub unmount_snapshot {
    my $self = shift;
    my $image_path = $self->get_disk_image(1);
    my $snap_path  = $image_path . '_snap';

    my $umount = $util->find_bin( 'umount', debug => 0, fatal => 0 ) or return;
    $util->syscmd( "$umount $snap_path", debug => 0, fatal => 0 )
        or return $log->error( "unable to unmount snapshot, is a backup active?", fatal => 0 );
    return 1;
};

sub unmount_inactive_snapshots {
    my $self = shift;

    my $df     = $util->find_bin( 'df', debug => 0, fatal => 0 ) or return;
    my $ps     = $util->find_bin( 'ps', debug => 0, fatal => 0 ) or return;
    my $grep   = $util->find_bin( 'grep', debug => 0, fatal => 0 ) or return;
    my $umount = $util->find_bin( 'umount', debug => 0, fatal => 0 ) or return;

# get a list of snapshots that are mounted and try to unmount them all 
    my @snapshots = `$df | $grep rootimg_snap`; chomp @snapshots;

    foreach my $snap ( @snapshots ) {
        my ($veid) = $snap =~ /\-([0-9]+)_rootimg_snap/;
        if ( ! $veid ) {
            print "unable to parse veid from $snap\n";
            next;
        };
        my @rsync_procs = `$ps ax | $grep rsync | $grep -v grep | $grep $veid`;
        if ( scalar @rsync_procs > 0 ) {
            print "found rsync process for $veid:\n" . join("\t", @rsync_procs) . "\n";
            next;
        };
        system "$umount /dev/vol00/${veid}_rootimg_snap";
    };
};

sub cleanup_inactive_snapshots {
    my $self = shift;

    my $lvremove = $util->find_bin('lvremove', debug => 0) or return;
    my $ps = $util->find_bin('ps',debug=>0);
    my $grep = $util->find_bin('grep',debug=>0);

    foreach my $snap ( </dev/vol00/*_snap> ) {
        my ($veid) = $snap =~ /\/([0-9]+)_rootimg_snap/;
        next if ! $veid;
        my @rsync_procs = `$ps ax | $grep rsync | $grep -v grep | $grep $veid`;
        if ( scalar @rsync_procs > 0 ) {
            print "found rsync process for $veid:\n" . join("\t", @rsync_procs) . "\n";
            next;
        };

        # this will fail if snapshot is in use
        system "$lvremove -f /dev/vol00/${veid}_rootimg_snap";
    };
};

1;

__END__

=head1 NAME

Provision::Unix::VirtualOS::Linux::Xen - Provision Xen VEs

=head1 SYNOPSIS

  use Provision::Unix;
  use Provision::Unix::VirtualOS;

  my $prov = Provision::Unix->new( debug => 0 );
  my $vos  = Provision::Unix::VirtualOS->new( prov => $prov );
 
  $vos->create()
  
General 

=head1 AUTHOR

Matt Simerson, C<< <matt at tnpi.net> >>



=head1 BUGS

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.


=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Provision::Unix::VirtualOS::Linux::Xen


You can also look for information at:

=over 4

=item * RT: CPAN's request tracker

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Provision-Unix>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/Provision-Unix>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/Provision-Unix>

=item * Search CPAN

L<http://search.cpan.org/dist/Provision-Unix>

=back


=head1 ACKNOWLEDGEMENTS


=head1 COPYRIGHT & LICENSE

Copyright (c) 2009 Matt Simerson

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


=cut

