view release on metacpan or search on metacpan
lib/App/FuguVM/Autoinstall.pm view on Meta::CPAN
$self->{error} = sprintf 'no free port in %d-%d', @{ +PORTS };
return;
}
my $child = ref $self;
my $result = Fugu::Process->spawn_perl(
code => "use $child; $child->run_child(\@ARGV)",
args => [ $port, $self->{file}, $self->{proxy_url} // '' ],
daemonize => 1,
stdout => $self->{logfile},
stderr => $self->{logfile},
);
unless ( $result->{success} ) {
$self->{error} = "cannot start the responder: $result->{error}";
return;
}
$self->{pidfile}->write_pid( $result->{pid} );
$self->{store}->set( autoinstall_port => $port );
unless ( $self->_wait_ready ) {
lib/App/FuguVM/Autoinstall.pm view on Meta::CPAN
return 1;
}
# $class->run_child($port, $file, $proxy_url):
# The entry point of the spawned child. The child reads the
# response file, renders it, and serves it until a SIGTERM. It
# logs each request line, and it never logs the file content.
sub run_child ( $class, $port, $file, $proxy_url )
{
my $log = Fugu::Log->new( mode => 'stderr', level => 'debug' );
my $bytes = Fugu::File->read($file);
die "Cannot read the response file: $file\n" if !defined $bytes;
my $body = $class->render( $bytes, $proxy_url );
my $listener = IO::Socket::INET->new(
LocalAddr => BIND_ADDRESS,
LocalPort => $port,
Proto => 'tcp',
lib/App/FuguVM/CLI.pm view on Meta::CPAN
sub cmd_ssh ( $self, $cli, @args )
{
my $vm = $self->_load_vm or return $self->{load_exit};
my $remote = $self->_require_remote( $vm, 'ssh_port' );
return EXIT_ERROR if !defined $remote;
if (@args) {
my $result = $remote->run(@args);
print $result->{stdout} if $result->{stdout};
print STDERR $result->{stderr} if $result->{stderr};
return $result->{exit_code};
}
else {
return $remote->interactive;
}
}
# Copy a local file or a local directory into the guest
sub cmd_put ( $self, $cli, @args )
{
lib/App/FuguVM/CLI.pm view on Meta::CPAN
{
my $names = $cli->option('names') // 0;
my $key = $self->_current_cache_key($cache)
or return EXIT_ERROR;
my $snapshots = $cache->snapshot_list($key);
# --names writes bare names to stdout, where a shell can read
# them. The human listing goes through the logger, which writes
# to stderr and prefixes every line.
if ($names) {
say $_->{name} for @$snapshots;
return EXIT_SUCCESS;
}
if ( !@$snapshots ) {
$self->{log}->info("No snapshots for $key");
return EXIT_SUCCESS;
}
lib/App/FuguVM/Disk.pm view on Meta::CPAN
push @cmd, $path;
push @cmd, $size if defined $size;
# The capture also swallows the verbose "Formatting..." line of
# qemu-img, which no caller wants to see
my $result = Fugu::Process->run( cmd => \@cmd );
unless ( $result->{success} ) {
Fugu::Log->default->error( 'Failed to create disk image %s: %s',
$path,
$result->{stderr} || $result->{error} || 'unknown' );
return;
}
return $path;
}
sub path ( $self, $name )
{
return "$self->{state_dir}/$name/disk.qcow2";
}
lib/App/FuguVM/Disk.pm view on Meta::CPAN
my @cmd = ( 'qemu-img', 'convert', '-O', $format );
push @cmd, '-B', $opts{backing}, '-F', 'qcow2'
if defined $opts{backing};
push @cmd, $source, $target;
my $result = Fugu::Process->run( cmd => \@cmd );
unless ( $result->{success} ) {
Fugu::Log->default->error( 'Failed to convert %s to %s: %s',
$source, $target,
$result->{stderr} || $result->{error} || 'unknown' );
return;
}
return $target;
}
# $self->info($name):
# Get the qemu-img report on the disk as a hashref. The method
# returns undef when there is no disk or when it cannot read the
# disk. The inspection is read-only. Thus it asks for shared
lib/App/FuguVM/Disk.pm view on Meta::CPAN
# P5: Check the disk image integrity. The method returns a hashref
# with the 'status' and 'output' keys. The 'status' key is 'ok' or
# 'corrupted'.
sub check ( $self, $name )
{
my $path = $self->path($name);
return if !-f $path;
my $result =
Fugu::Process->run( cmd => [ 'qemu-img', 'check', $path ] );
my $output = $result->{stdout} . $result->{stderr};
return {
status => $result->{success} ? 'ok' : 'corrupted',
output => $output,
path => $path,
};
}
# P5: Repair the disk image. The method returns true on success and
# false on failure.
lib/App/FuguVM/Guest.pm view on Meta::CPAN
# No graphics display (headless)
push @cmd, '-display', 'none';
# Use Fugu::Process to spawn QEMU
my $log_file = $state->vm_state_dir . '/qemu.log';
my $result = Fugu::Process->spawn_command(
cmd => \@cmd,
daemonize => 1,
stdout => $log_file,
stderr => $log_file,
);
return unless $result->{success};
# Wait until QEMU writes the PID file
my $pid = Fugu::Timeout::wait_until(
5, 0.1,
sub {
my $qemu_pid = $state->get_vm_pid;
return $qemu_pid
lib/App/FuguVM/Proxy.pm view on Meta::CPAN
# $class->run_child($port, $cache_dir):
# The entry point of the spawned child. The child builds its own
# cache, warms the metadata, and serves until a SIGTERM.
#
# The spawn passes a fixed argument list, so the distfile cap
# reaches the child through the environment, which it inherits
# from fuguvm. An absent variable means 0, and 0 turns the
# distfile cache off.
sub run_child ( $class, $port, $cache_dir )
{
my $log = Fugu::Log->new( mode => 'stderr', level => 'debug' );
my $limit = $ENV{FUGUVM_DISTFILE_LIMIT} // 0;
$limit = 0 if $limit !~ /\A[0-9]+\z/;
my $self = bless {
cache => App::FuguVM::Proxy::Cache->new( $cache_dir, $limit ),
meta => Fugu::Proxy::Meta->new,
log => $log,
}, $class;
lib/App/FuguVM/Remote.pm view on Meta::CPAN
while (@dirs) {
my @batch = splice @dirs, 0, BATCH_PATHS;
my $result =
$self->{ssh}
->run_command( $self->quote_argv( 'mkdir', '-p', @batch ) );
if ( $result->{exit_code} != 0 ) {
Fugu::Log->default->error(
'Cannot create directories on %s:%d: %s',
$self->{host}, $self->{port},
$result->{stderr} );
return;
}
}
return 1;
}
# $self->_publish(@files):
# Move every temporary file onto its destination, with batched
# mv -f commands. Each mv holds two paths, so one batch holds
lib/App/FuguVM/Remote.pm view on Meta::CPAN
my @batch = splice @pairs, 0, int( BATCH_PATHS / 2 );
my $command = join ' && ',
map { $self->quote_argv( 'mv', '-f', $_->[0], $_->[1] ) }
@batch;
my $result = $self->{ssh}->run_command($command);
if ( $result->{exit_code} != 0 ) {
Fugu::Log->default->error(
'Cannot publish files on %s:%d: %s',
$self->{host}, $self->{port},
$result->{stderr} );
return;
}
}
return 1;
}
# $self->_discard(@temps):
# Remove the temporary files that a failed put wrote, with
# batched rm -f commands. The removal is best effort: the
lib/App/FuguVM/Remote.pod view on Meta::CPAN
each word in single quotes, and it replaces each single quote inside
a word with the C<'\''> form. An empty word becomes C<''>. The words
join with one space. So the remote shell splits the string at the
word boundaries only: it expands nothing, and it globs nothing.
=head2 run
$remote->run(@argv)
Run one argument vector on the guest. The method returns the hash of
C<< Fugu::SSH->run_command >>: C<stdout>, C<stderr> and C<exit_code>.
It dies on an empty vector. A connect failure reads as exit code 1,
with the reason in C<stderr>.
=head2 interactive
$remote->interactive
Open an interactive session. The method returns the exit code of
L<ssh(1)>.
=head2 put
share/fuguvm/expect/autoinstall.exp view on Meta::CPAN
return $env(FUGUVM_TIMEOUT)
}
return $default
}
set timeout [env_timeout 300]
set usage "Usage: autoinstall.exp <host> <port> <url> <arch>"
if {[llength $argv] != 4} {
puts stderr $usage
exit 1
}
set host [lindex $argv 0]
set port [lindex $argv 1]
set url [lindex $argv 2]
set arch [lindex $argv 3]
if {$url eq ""} {
puts stderr $usage
exit 1
}
# The configuration loader of fuguvm validates the value, so this
# exit guards a manual run only.
if {$arch ne "amd64" && $arch ne "arm64"} {
puts stderr $usage
exit 1
}
log_user 1
# Helper proc to send a response after a match
proc respond {response} {
sleep 0.5
send "$response\r"
}
share/fuguvm/expect/command.exp view on Meta::CPAN
#
# Use the environment variable when it exists. Otherwise use 60.
if {[info exists env(FUGUVM_TIMEOUT)]} {
set timeout $env(FUGUVM_TIMEOUT)
} else {
set timeout 60
}
if {[llength $argv] < 3} {
puts stderr "Usage: command.exp <host> <port> <command> \[user\] \[password\]"
exit 1
}
set host [lindex $argv 0]
set port [lindex $argv 1]
set cmd [lindex $argv 2]
set user [lindex $argv 3]
set password [lindex $argv 4]
if {$user eq ""} {
share/fuguvm/expect/command.exp view on Meta::CPAN
expect {
"login:" {
send "$user\r"
expect "Password:"
send "$password\r"
}
"#" {
# Already logged in
}
eof {
puts stderr "telnet could not connect to the VM console"
exit 1
}
timeout {
puts stderr "Timeout waiting for prompt"
exit 1
}
}
# Wait for shell
expect "#" {
send "$cmd\r"
}
# Wait for command to complete
share/fuguvm/expect/install.exp view on Meta::CPAN
return $env(FUGUVM_TIMEOUT)
}
return $default
}
set timeout [env_timeout 300]
set usage "Usage: install.exp <host> <port> <root_password> <proxy_url> <arch> <verify>"
if {[llength $argv] != 6} {
puts stderr $usage
exit 1
}
set host [lindex $argv 0]
set port [lindex $argv 1]
set root_password [lindex $argv 2]
set proxy_url [lindex $argv 3]
set arch [lindex $argv 4]
set verify [lindex $argv 5]
share/fuguvm/expect/install.exp view on Meta::CPAN
set root_password "openbsd"
}
if {$proxy_url eq ""} {
set proxy_url "none"
}
# The configuration loader of fuguvm validates the value, so this
# exit guards a manual run only.
if {$arch ne "amd64" && $arch ne "arm64"} {
puts stderr $usage
exit 1
}
# The configuration loader also normalizes the verify switch.
if {$verify ne "yes" && $verify ne "no"} {
puts stderr $usage
exit 1
}
log_user 1
# Helper proc to send a response after a match
proc respond {response} {
sleep 0.5
send "$response\r"
}
share/fuguvm/expect/login.exp view on Meta::CPAN
global env
if {[info exists env(FUGUVM_TIMEOUT)] && $env(FUGUVM_TIMEOUT) > $default} {
return $env(FUGUVM_TIMEOUT)
}
return $default
}
set timeout [env_timeout 30]
if {[llength $argv] < 2} {
puts stderr "Usage: login.exp <host> <port> \[user\] \[password\]"
exit 1
}
set host [lindex $argv 0]
set port [lindex $argv 1]
set user [lindex $argv 2]
set password [lindex $argv 3]
if {$user eq ""} {
set user "root"
share/fuguvm/expect/login.exp view on Meta::CPAN
expect {
"login:" {
send "$user\r"
}
"#" {
# Already logged in
puts "Already logged in"
exit 0
}
eof {
puts stderr "telnet could not connect to the VM console"
exit 1
}
timeout {
puts stderr "Timeout waiting for login prompt"
exit 1
}
}
# Enter password
expect "Password:" {
send "$password\r"
}
# Wait for shell prompt
expect {
"#" {
puts "Login successful"
exit 0
}
"$" {
puts "Login successful"
exit 0
}
"Login incorrect" {
puts stderr "Login failed"
exit 1
}
timeout {
puts stderr "Timeout waiting for shell"
exit 1
}
}
t/fuguvm/cli.t view on Meta::CPAN
0, 'bare cache clear succeeds');
is(scalar @{ $proxy->list }, 0, 'bare clear empties the proxy too');
}
# The listing also reports the proxy. Thus a user who decides on a
# prune sees both halves of cache_dir.
{
my $project = _cache_project();
_fake_download($project, '7.7/arm64/base77.tgz');
my $err = _capture_stderr($project, 'cache', 'list');
like($err, qr/Proxy downloads/, 'cache list reports the proxy store');
like($err, qr/OpenBSD 7\.7/, 'broken down by version');
like($err, qr/No cached images/,
'and still says the images are empty');
unlike($err, qr/Distfiles/, 'an empty distfile tree has no line');
}
# The distfile line of cache list: the size against the cap, or the
# size with a note that the cap is off. A distfile never joins the
# per-version grouping, because it carries no version.
{
my $project = _cache_project("distfile_cache 4G\n");
_fake_download($project, '7.8/arm64/base78.tgz');
_fake_distfile($project, 'gmake-4.4.1.tar.gz');
my $err = _capture_stderr($project, 'cache', 'list');
like($err, qr/Distfiles: \d+B of 4\.0G/,
'cache list reports the distfile size and the cap');
is(scalar(grep { /OpenBSD -/ } split /\n/, $err), 0,
'no distfile lands in the version grouping');
my $off = _cache_project();
_fake_distfile($off, 'gmake-4.4.1.tar.gz');
$err = _capture_stderr($off, 'cache', 'list');
like($err, qr/Distfiles: \d+B, caching off/,
'a tree with a cap of 0 reads as caching off');
}
# cache clear --stale keeps the distfile tree and re-applies the cap;
# a bare clear removes the tree with everything else
{
my $project = _cache_project("distfile_cache 4G\n");
_fake_download($project, '7.7/arm64/base77.tgz');
my $distfile = _fake_distfile($project, 'gmake-4.4.1.tar.gz');
t/fuguvm/cli.t view on Meta::CPAN
'snapshot', 'restore', 'deps'),
5, 'restore refuses while the VM is running');
unlink "$state_dir/default/vm.pid";
is(App::FuguVM::CLI->run("--project=$project", '--quiet',
'snapshot', 'rm', 'deps'),
0, 'rm succeeds');
}
# --names is the scriptable listing. It writes bare names on stdout,
# where a shell can read them. It does not go through the stderr logger.
SKIP: {
my $has_qemu = `which qemu-img 2>/dev/null`;
skip 'qemu-img not installed', 4 unless $has_qemu;
my $project = _cache_project();
my $cache = App::FuguVM::DiskCache->new("$project/cache");
my $key = $cache->key(App::FuguVM::Config->new($project)->load_vm('default'));
my $source = "$project/source.qcow2";
system('qemu-img', 'create', '-f', 'qcow2', $source, '16M') == 0
t/fuguvm/cli.t view on Meta::CPAN
print $fh "vm \"default\" {\n";
print $fh "\tversion 7.8\n";
print $fh "\tdisk_size 8G\n";
print $fh "}\n";
close $fh;
return $project;
}
# Run a command with both streams captured, and return the exit
# code, the stdout text and the stderr text. Thus scriptable output
# cannot mix with the TAP stream of this test. The capture goes
# through real files, never through an in-memory scalar: a scalar
# handle has no file descriptor, and the output of a child process
# would then land wherever descriptor 1 points at that moment.
sub _run_captured
{
my (@args) = @_;
my $dir = tempdir(CLEANUP => 1);
open my $saved_out, '>&', \*STDOUT or die $!;
t/fuguvm/cli.t view on Meta::CPAN
open STDOUT, '>&', $saved_out or die $!;
close $saved_out;
open STDERR, '>&', $saved_err or die $!;
close $saved_err;
return ($code, _slurp("$dir/out"), _slurp("$dir/err"));
}
# Run a command and capture stdout. This separates the scriptable
# output from the logger's stderr.
sub _capture_stdout
{
my ($project, @args) = @_;
my (undef, $out) =
_run_captured("--project=$project", '--quiet', @args);
return $out;
}
# The logger writes to stderr. Thus this helper captures the human
# listing apart from the scriptable output above.
sub _capture_stderr
{
my ($project, @args) = @_;
my (undef, undef, $err) = _run_captured("--project=$project", @args);
return $err;
}
# _slurp($path):
# The whole file as text.
sub _slurp
t/fuguvm/config.t view on Meta::CPAN
}
# An unrecognized value must not silently mean its opposite
open $fh, '>', "$tmpdir/.fuguvmrc";
print $fh "image_cache maybe\n";
close $fh;
my $diagnostic = '';
my $result;
{
local *STDERR;
open STDERR, '>', \$diagnostic or die "capture stderr: $!";
$result = App::FuguVM::Config->new($tmpdir)->image_cache;
}
is($result, 1, 'an unparseable image_cache falls back to the default');
like($diagnostic, qr/not a yes\/no value: maybe/,
'and it says so instead of meaning the opposite');
}
# The distfile_cache directive: the size grammar, the default, the
# refusal, and the merge
{
t/fuguvm/config.t view on Meta::CPAN
is($write->("cache_dir /tmp\n")->distfile_cache, 0,
'an absent directive is off');
# An unparsable value warns and reads as off: an unrecognized
# spelling must not silently mean its opposite, and off is the
# closed state for a cache.
my $diagnostic = '';
my $result;
{
local *STDERR;
open STDERR, '>', \$diagnostic or die "capture stderr: $!";
$result = $write->("distfile_cache lots\n")->distfile_cache;
}
is($result, 0, 'an unparsable distfile_cache is off');
like($diagnostic, qr/lots/, 'and the warning names the value');
# The project file wins over the global file, for each of the
# three directives of the mirror work
open my $gh, '>', "$homedir/.fuguvmrc" or die $!;
print $gh "distfile_cache 1K\n";
print $gh "verify yes\n";
t/fuguvm/config.t view on Meta::CPAN
open $fh, '>', "$tmpdir/.fuguvmrc";
print $fh "distfile_cache 4G\n";
print $fh "vm test {\n";
print $fh " distfile_cache 8G\n";
print $fh "}\n";
close $fh;
my $warning = '';
my $loaded;
{
local *STDERR;
open STDERR, '>', \$warning or die "capture stderr: $!";
$loaded = App::FuguVM::Config->new($tmpdir)->load_vm('test');
}
is($loaded->{distfile_cache}, 4 * 1024**3,
'the project cap wins over a cap in a VM block');
like($warning, qr/distfile_cache/,
'and the loader warns about the block value');
}
# The parser normalizes image_cache inside a vm block like the global
# directive
t/fuguvm/mirror.t view on Meta::CPAN
'ensure returns the cached path with no fetch');
}
# With verify set to 0, ensure stores the file and logs one warning,
# and the proof methods refuse: a method must not report a proof that
# it did not make.
{
my $cache_dir = tempdir(CLEANUP => 1);
my $mirror = _mirror($cache_dir, verify => 0);
my $warned = _capture_stderr(sub {
no warnings 'redefine';
local *App::FuguVM::Mirror::fetch =
sub ($, $) { _temp_file('unproven bytes') };
is($mirror->ensure('release', 'base78.tgz'),
_cache($cache_dir)->cache_path(
'https://cdn.openbsd.org/pub/OpenBSD/7.8/arm64/base78.tgz'),
'ensure with verify 0 stores the file');
});
like($warned, qr/unproven/i, 'and it logs one warning');
t/fuguvm/mirror.t view on Meta::CPAN
my ($path) = @_;
open my $fh, '<', $path or die "read $path: $!";
local $/;
my $bytes = <$fh>;
close $fh;
return $bytes;
}
# _capture_stderr($code):
# Run the code with the process default logger on standard
# error, capture the stream, and put the quiet default back.
sub _capture_stderr
{
my ($code) = @_;
my $dir = tempdir(CLEANUP => 1);
Fugu::TestLog->stderr;
open my $saved, '>&', \*STDERR or die $!;
open STDERR, '>', "$dir/err" or die $!;
$code->();
open STDERR, '>&', $saved or die $!;
close $saved;
Fugu::TestLog->quiet;
return _slurp("$dir/err");
t/fuguvm/remote.t view on Meta::CPAN
# A mock transport. The batching tests read the commands that put
# would run, without a guest.
package Mock::SSH {
sub new ($class) {
return bless { commands => [], writes => [] }, $class;
}
sub run_command ($self, $command) {
push @{ $self->{commands} }, $command;
return { stdout => '', stderr => '', exit_code => 0 };
}
sub write_file ($self, $path, $content, $mode) {
push @{ $self->{writes} }, [ $path, $mode ];
return 0;
}
}
# ============================================================
# quote_argv