App-FuguVM

 view release on metacpan or  search on metacpan

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


	my $installer = Fugu::File->read($script);
	return if !defined $installer;

	my $generation_file = $self->_generation_file;
	if ( !defined $generation_file ) {
		warn "Cannot locate " . GENERATION_FILE . " for cache key\n";
		return;
	}

	my $generation = Fugu::File->read($generation_file);
	return if !defined $generation;

	# Hash the file contents separately. Thus the joined record
	# stays free of newlines, and an input value cannot forge the
	# delimiter.
	my @inputs = (
		"version=$version",
		"arch=$arch",
		"disk_size=$disk_size",
		'install=' . Digest::SHA::sha256_hex($installer),
		'generation=' . Digest::SHA::sha256_hex($generation),
	);

	my $hash = Digest::SHA::sha256_hex( join( "\n", @inputs ) );

	return "$version-$arch-" . substr( $hash, 0, KEY_HASH_LENGTH );
}

# $self->lookup($key):
#	Return { base => path, meta => hashref, dir => path } for a
#	complete entry, or undef otherwise. A half-written entry is a
#	miss, not an error. The caller falls back to a full
#	installation.
sub lookup ( $self, $key )
{
	return if !defined $key;

	my $dir  = $self->entry_dir($key);
	my $base = "$dir/" . BASE_NAME;
	return if !-f $base;

	my $meta = Fugu::File->read_json( "$dir/" . META_NAME );
	return if !defined $meta;

	return {
		key  => $key,
		dir  => $dir,
		base => $base,
		meta => $meta,
	};
}

# $self->store($key, $disk_path, $meta):
#	Publish $disk_path as the cached base image for $key. The
#	method builds the entry whole in a sibling temporary directory.
#	Then it publishes the entry with a rename of that directory.
#	Thus no reader sees the base image of one installation beside
#	the metadata of another installation. Such a mismatch would
#	look live. It would then wedge every later boot with a root
#	password that does not open the image.
#
#	Entries are write-once: a rename onto a populated directory
#	fails with ENOTEMPTY, and the existing entry wins. Return the
#	base image path, or undef on any failure.
sub store ( $self, $key, $disk_path, $meta = {} )
{
	return if !defined $key;

	if ( !-f $disk_path ) {
		warn "Cannot cache missing disk image: $disk_path\n";
		return;
	}

	Fugu::File->ensure_dir( $self->installed_dir ) or return;

	my $target = $self->entry_dir($key);
	if ( -e "$target/" . BASE_NAME ) {
		Fugu::Log->default->warning(
			'Image cache entry already exists: %s', $key );
		return;
	}

	my $built = Fugu::File->atomic_dir(
		$target,
		sub ($tmp) {
			my $base = "$tmp/" . BASE_NAME;
			return 0 if !_convert( $disk_path, $base );

			chmod 0400, $base or do {
				Fugu::Log->default->warning(
					'Cannot set permissions on %s: %s',
					$base, $! );
				return 0;
			};

			my %record = (
				%$meta,
				key        => $key,
				created_at => time,
			);

			# The record carries the guest root password, so
			# the file gets its mode before its content
			return Fugu::File->write_json( "$tmp/" . META_NAME,
				\%record, mode => 0600 ) ? 1 : 0;
		} );
	return if !defined $built;

	return "$target/" . BASE_NAME;
}

# $self->list:
#	Return every complete entry, newest first, as
#	{ key, dir, base, size, created_at, meta, snapshots }
sub list ($self)
{
	my @entries;
	my $installed = $self->installed_dir;
	return \@entries if !-d $installed;

	opendir my $dh, $installed or return \@entries;
	my @keys = grep { !/^\./ && -d "$installed/$_" } readdir $dh;
	closedir $dh;

	for my $key ( sort @keys ) {
		my $entry = $self->lookup($key) or next;
		$entry->{size} = Fugu::Proxy::Cache->dir_size( $entry->{dir} );
		$entry->{created_at} = $entry->{meta}{created_at};
		$entry->{snapshots}  = $self->_snapshot_names($key);
		push @entries, $entry;
	}

	return [
		sort { ( $b->{created_at} // 0 ) <=> ( $a->{created_at} // 0 ) }
		    @entries
	];
}

# $self->key_for_path($path):
#	Return the cache key whose entry contains $path. The path can
#	point to a base image or to a snapshot. Return undef when $path
#	lies outside the cache. The method lets a caller answer "which
#	cached image is this disk built on?".
sub key_for_path ( $self, $path )
{
	return if !defined $path;

	my $installed = $self->installed_dir . '/';
	return if index( $path, $installed ) != 0;

	my ($key) = split m{/}, substr( $path, length $installed ), 2;
	return if !defined $key || $key eq '';

	return $key;
}

# $self->snapshot_dir($key):
#	Return the directory that holds the named snapshot layers of
#	an entry.
sub snapshot_dir ( $self, $key )
{
	return $self->entry_dir($key) . '/' . SNAPSHOT_DIR;

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

	return 0 if $name !~ /^[A-Za-z0-9][\w.-]*$/;

	return 1;
}

# $self->snapshot_store($key, $name, $disk_path, $meta):
#	Publish the stopped working disk as the named snapshot layer of
#	entry $key.
#
#	The method flattens the disk onto base.qcow2 and does not copy
#	it. A copy would carry the backing-file header of the working
#	disk verbatim. That header is only correct while the disk hangs
#	directly off the base. After a restore the disk hangs off a
#	snapshot. Thus a copy would stack chains without bound. A
#	normal second run saves the same name again. Then a copy would
#	name itself as its own backing file. The flatten operation also
#	keeps every snapshot a direct child of the base. Thus no
#	snapshot is the parent of another snapshot, and the removal of
#	one snapshot cannot orphan another.
#
#	Return the snapshot path, or undef on failure.
sub snapshot_store ( $self, $key, $name, $disk_path, $meta = {} )
{
	if ( !$self->valid_snapshot_name($name) ) {
		warn "Invalid snapshot name: " . ( $name // '(undef)' ) . "\n";
		return;
	}

	my $entry = $self->lookup($key);
	if ( !defined $entry ) {
		warn "No cached image for $key to snapshot against\n";
		return;
	}

	if ( !-f $disk_path ) {
		warn "Cannot snapshot missing disk image: $disk_path\n";
		return;
	}

	my $dir = $self->snapshot_dir($key);
	Fugu::File->ensure_dir($dir) or return;

	# qemu-img writes the image, so the atomic file helpers cannot
	# carry it. It goes through one temporary path and one rename.
	my $target   = $self->snapshot_path( $key, $name );
	my $tmp_disk = "$target." . TEMP_PREFIX . $$;

	unlink $tmp_disk;

	if ( !_convert( $disk_path, $tmp_disk, $entry->{base} ) ) {
		unlink $tmp_disk;
		return;
	}

	chmod 0400, $tmp_disk or do {
		warn "Cannot set permissions on $tmp_disk: $!\n";
		unlink $tmp_disk;
		return;
	};

	# The root password belongs to the base image. Thus the method
	# copies it from there and does not trust the caller.
	my %record = (
		%$meta,
		key           => $key,
		name          => $name,
		root_password => $entry->{meta}{root_password},
		created_at    => time,
	);

	# Metadata first, atomically, with the mode before the content.
	# To save a name again is normal. A reader that catches the
	# window sees the previous image with the new metadata. Those
	# fields describe the base, which did not change.
	if (
		!Fugu::File->write_json(
			"$dir/$name.json", \%record, mode => 0600
		) )
	{
		unlink $tmp_disk;
		return;
	}
	if ( !rename $tmp_disk, $target ) {
		warn "Cannot publish snapshot $name: $!\n";
		unlink $tmp_disk;
		return;
	}

	return $target;
}

# $self->snapshot_lookup($key, $name):
#	Return { key, name, path, meta } for a snapshot whose image,
#	metadata, and backing chain all resolve. Return undef
#	otherwise. A snapshot whose base was removed is a miss. Thus a
#	caller can fall back to a provision from scratch and does not
#	fail hard.
sub snapshot_lookup ( $self, $key, $name )
{
	return if !$self->valid_snapshot_name($name);

	my $path = $self->snapshot_path( $key, $name );
	return if !-f $path;

	my $meta =
	    Fugu::File->read_json( $self->snapshot_dir($key) . "/$name.json" );
	return if !defined $meta;

	my $base = $self->base_path($key);
	return if !-f $base;

	return {
		key  => $key,
		name => $name,
		path => $path,
		base => $base,
		meta => $meta,
	};
}

# $self->snapshot_list($key):
#	Return the sorted snapshots of an entry, as
#	{ name, path, size, created_at }
sub snapshot_list ( $self, $key )
{
	my @snapshots;



( run in 0.822 second using v1.01-cache-2.11-cpan-4ef0a570458 )