App-FuguVM

 view release on metacpan or  search on metacpan

lib/App/FuguVM/Guest.pm  view on Meta::CPAN


	# Create the .ssh directory
	my $result =
	    $ssh->run_command('mkdir -p /root/.ssh && chmod 700 /root/.ssh');
	if ( $result->{exit_code} != 0 ) {
		return 0;
	}

	# Write the authorized_keys file
	my $authkeys_content = $ssh_pubkey . "\n";
	if (
		$ssh->write_file(
			'/root/.ssh/authorized_keys', $authkeys_content,
			0600
		) != 0
	    )
	{
		return 0;
	}

	# Store the installed pubkey for a future comparison
	$state->mark_ssh_key_installed($ssh_pubkey);
	return 1;
}

# Graceful shutdown with a filesystem sync: sync through SSH, then
# power off through the ACPI power button, and report the result.
sub _graceful_shutdown ($self)
{
	my $log = $self->{log};

	# Do a best-effort filesystem sync over SSH before the code
	# pulls the power. The sync has a hard time bound. A wedged
	# guest must never stall the shutdown. Also, libssh2 does not
	# reliably obey its own timeout on the connect and handshake. A
	# failure or a timeout here is acceptable. The ACPI powerdown
	# below runs the orderly shutdown of the guest, which syncs. A
	# force stop is the ultimate fallback.
	$self->_bounded(
		Fugu::SSH::DEFAULT_TIMEOUT() + 5,
		sub {
			my $ssh = Fugu::SSH->new(
				host => $self->connect_address,
				port => $self->ssh_port,
				user => 'root',
			);
			return $ssh->run_command('sync; sync; sync');
		} );

	# Ask the guest to power off through the ACPI power button. Then
	# wait.
	if ( $self->_qmp_powerdown && $self->_wait_exit(60) ) {
		$log->info("Shutdown via ACPI powerdown");
		return 1;
	}

	return 0;
}

# $self->_bounded($seconds, $code):
#	Run $code under a hard wall-clock deadline, so a blocked guest
#	interaction cannot stall the caller. The guard itself is
#	Fugu::Timeout; this wrapper adds the log line.
sub _bounded ( $self, $seconds, $code )
{
	my $result = Fugu::Timeout::bounded( $seconds, $code );
	return $result if defined $result;

	$self->{log}->warning("Guest did not respond within ${seconds}s");
	return;
}

# $self->_ensure_proxy:
#	Start the caching proxy if it does not run, and return it. The
#	method returns undef when the proxy cannot start.
#
#	The lifecycle lives here and not in App::FuguVM::State, because a
#	state file must not start a process. That split is what removed
#	the require cycle between the two modules.
sub _ensure_proxy ($self)
{
	my $proxy = $self->_proxy;
	return $proxy if $proxy->is_running;

	# The spawn passes a fixed argument list, so the distfile cap
	# reaches the child through the environment.
	$ENV{FUGUVM_DISTFILE_LIMIT} = $self->{config}{distfile_cache} // 0;

	unless ( defined $proxy->start ) {
		$self->{log}->warning(
			'Proxy did not start: %s',
			$proxy->error // 'unknown'
		);
		return;
	}

	return $proxy;
}

# $self->_stop_proxy:
#	Stop the proxy if it runs. The method returns 1 when it stopped
#	one, and 0 when there was none.
sub _stop_proxy ($self)
{
	my $proxy = $self->_proxy;
	return 0 unless $proxy->is_running;

	$proxy->stop;

	return 1;
}

# $self->_proxy:
#	Build the proxy supervisor over this VM's state.
sub _proxy ($self)
{
	my $state = $self->{state};

	return App::FuguVM::Proxy->new(
		cache => App::FuguVM::Proxy::Cache->new(
			$self->_cache_dir, $self->{config}{distfile_cache} // 0

lib/App/FuguVM/Guest.pm  view on Meta::CPAN


	return;
}

# $self->_free_port($first, $last, $taken):
#	Return the first port of the range that binds on the bind
#	address and that $taken does not hold. Return undef for an
#	exhausted range.
sub _free_port ( $self, $first, $last, $taken )
{
	require IO::Socket::INET;

	for my $port ( $first .. $last ) {
		next if $taken->{$port};

		my $sock = IO::Socket::INET->new(
			LocalAddr => $self->bind_address,
			LocalPort => $port,
			Proto     => 'tcp',
			Listen    => 1,
		);
		next if !defined $sock;

		$sock->close;
		return $port;
	}

	return;
}

# $self->_taken_ports:
#	Return the recorded ports of every guest of the project, as a
#	hash reference keyed by port. The method enumerates the state
#	directory, like App::FuguVM::CLI::_disks_backed_by: a record
#	counts whether a 'vm' block still declares its guest or not.
sub _taken_ports ($self)
{
	my %taken;

	my $state_dir = $self->{state}->state_dir;
	return \%taken if !-d $state_dir;

	opendir my $dh, $state_dir or return \%taken;
	my @names = sort grep { !/^\./ && -d "$state_dir/$_" } readdir $dh;
	closedir $dh;

	for my $name (@names) {
		my $sibling = App::FuguVM::State->new( $state_dir, $name )
		    or next;
		my $runtime = $sibling->get_runtime;
		for my $directive (qw(ssh_port console_port)) {
			my $port = $runtime->{$directive};
			$taken{$port} = 1 if defined $port;
		}
	}

	return \%taken;
}

# $self->_lock_ports:
#	Return the locked handle of ports.lock, or undef on the
#	deadline. The lock file lives in the cache directory, so every
#	project that shares that directory probes one port at a time.
#	The record exclusion of _taken_ports covers this project only.
#	A collision with an other project surfaces when QEMU binds the
#	port, as a reported startup failure. The caller probes without
#	the lock when the deadline elapses: a wedged holder must not
#	fail a run.
sub _lock_ports ($self)
{
	my $dir = $self->_cache_dir;
	Fugu::File->ensure_dir($dir) or return;

	my $path = "$dir/ports.lock";
	open my $fh, '>>', $path or do {
		$self->{log}->warning("Cannot open $path: $!");
		return;
	};

	my $locked = Fugu::Timeout::bounded( PORT_LOCK_TIMEOUT,
		sub { flock $fh, LOCK_EX } );
	return $fh if $locked;

	close $fh;
	$self->{log}->warning("Port lock not acquired, probing without it");

	return;
}

# $self->_check_installed_arch:
#	Report if the configured architecture matches the installed
#	disk, once for up and start. A disk belongs to one
#	architecture, so a changed directive must not start the wrong
#	QEMU on an existing disk. An absent record cannot prove a
#	difference, so the check passes.
sub _check_installed_arch ($self)
{
	my $config = $self->{config};

	my $installed = $self->{state}->get_installed_arch;
	return 1 if !defined $installed || $installed eq $config->{arch};

	$self->{log}->error(
"The disk of '$config->{name}' holds an $installed installation, not $config->{arch}"
	);
	$self->{log}->error("Run 'fuguvm destroy' to rebuild the VM.");

	return 0;
}

# _host_arch():
#	Return the host machine architecture from uname.
sub _host_arch ()
{
	require POSIX;
	my @uname = POSIX::uname();
	return $uname[4] // '';
}

# $self->_find_efi_firmware:
#	Return the firmware of the architecture as { code, vars }, or
#	undef. The vars entry is the variable-store template beside
#	the code file. It is undef when -bios boots the code file
#	alone. A code file without its template is not usable, so the
#	search walks on.
sub _find_efi_firmware ($self)
{
	my $arch = $self->_arch;

	my @candidates = $arch->firmware_paths;
	push @candidates, glob( $arch->firmware_glob );

	for my $code (@candidates) {
		next unless -f $code;

		my $vars = $arch->firmware_vars_path($code);
		next if defined $vars && !-f $vars;

		return { code => $code, vars => $vars };
	}

	return;



( run in 1.162 second using v1.01-cache-2.11-cpan-800906f7e73 )