blob: 254c6ddf02c2df6df6ec047742ab0cb807275924 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <archive.h>
#include <archive_entry.h>
#include <fcntl.h>
#include <unistd.h>
#include <algorithm>
#include <memory>
#include <utility>
#include <base/files/file.h>
#include <base/files/file_util.h>
#include <base/guid.h>
#include <base/memory/ptr_util.h>
#include <base/stl_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/stringprintf.h>
#include "vm_tools/concierge/disk_image.h"
#include "vm_tools/concierge/plugin_vm_helper.h"
#include "vm_tools/concierge/service.h"
#include "vm_tools/concierge/vmplugin_dispatcher_interface.h"
namespace {
constexpr gid_t kPluginVmGid = 20128;
} // namespace
namespace vm_tools {
namespace concierge {
DiskImageOperation::DiskImageOperation()
: uuid_(base::GenerateGUID()),
status_(DISK_STATUS_FAILED),
source_size_(0),
processed_size_(0) {
CHECK(base::IsValidGUID(uuid_));
}
void DiskImageOperation::Run(uint64_t io_limit) {
if (ExecuteIo(io_limit)) {
Finalize();
}
}
int DiskImageOperation::GetProgress() const {
if (status() == DISK_STATUS_IN_PROGRESS) {
if (source_size_ == 0)
return 0; // We do not know any better.
return processed_size_ * 100 / source_size_;
}
// Any other status indicates completed operation (successfully or not)
// so return 100%.
return 100;
}
std::unique_ptr<PluginVmCreateOperation> PluginVmCreateOperation::Create(
base::ScopedFD fd,
const base::FilePath& iso_dir,
uint64_t source_size,
const VmId vm_id,
const std::vector<std::string> params) {
auto op = base::WrapUnique(new PluginVmCreateOperation(
std::move(fd), source_size, std::move(vm_id), std::move(params)));
if (op->PrepareOutput(iso_dir)) {
op->set_status(DISK_STATUS_IN_PROGRESS);
}
return op;
}
PluginVmCreateOperation::PluginVmCreateOperation(
base::ScopedFD in_fd,
uint64_t source_size,
const VmId vm_id,
const std::vector<std::string> params)
: vm_id_(std::move(vm_id)),
params_(std::move(params)),
in_fd_(std::move(in_fd)) {
set_source_size(source_size);
}
bool PluginVmCreateOperation::PrepareOutput(const base::FilePath& iso_dir) {
base::File::Error dir_error;
if (!base::CreateDirectoryAndGetError(iso_dir, &dir_error)) {
set_failure_reason(std::string("failed to create ISO directory: ") +
base::File::ErrorToString(dir_error));
return false;
}
CHECK(output_dir_.Set(iso_dir));
base::FilePath iso_path = iso_dir.Append("install.iso");
out_fd_.reset(open(iso_path.value().c_str(), O_CREAT | O_WRONLY, 0660));
if (!out_fd_.is_valid()) {
PLOG(ERROR) << "Failed to create output ISO file " << iso_path.value();
set_failure_reason("failed to create ISO file");
return false;
}
return true;
}
void PluginVmCreateOperation::MarkFailed(const char* msg, int error_code) {
set_status(DISK_STATUS_FAILED);
if (error_code != 0) {
set_failure_reason(base::StringPrintf("%s: %s", msg, strerror(error_code)));
} else {
set_failure_reason(msg);
}
LOG(ERROR) << vm_id_.name()
<< " PluginVm create operation failed: " << failure_reason();
in_fd_.reset();
out_fd_.reset();
if (output_dir_.IsValid() && !output_dir_.Delete()) {
LOG(WARNING) << "Failed to delete output directory on error";
}
}
bool PluginVmCreateOperation::ExecuteIo(uint64_t io_limit) {
do {
uint8_t buf[65536];
int count = HANDLE_EINTR(read(in_fd_.get(), buf, sizeof(buf)));
if (count == 0) {
// No more data
return true;
}
if (count < 0) {
MarkFailed("failed to read data block", errno);
break;
}
int ret = HANDLE_EINTR(write(out_fd_.get(), buf, count));
if (ret != count) {
MarkFailed("failed to write data block", errno);
break;
}
io_limit -= std::min(static_cast<uint64_t>(count), io_limit);
AccumulateProcessedSize(count);
} while (status() == DISK_STATUS_IN_PROGRESS && io_limit > 0);
// More copying is to be done (or there was a failure).
return false;
}
void PluginVmCreateOperation::Finalize() {
// Close the file descriptors.
in_fd_.reset();
out_fd_.reset();
if (!pvm::helper::CreateVm(vm_id_, std::move(params_))) {
MarkFailed("Failed to create Plugin VM", 0);
return;
}
if (!pvm::helper::AttachIso(vm_id_, "cdrom0", "/iso/install.iso")) {
MarkFailed("Failed to attach install ISO to Plugin VM", 0);
pvm::helper::DeleteVm(vm_id_);
return;
}
if (!pvm::helper::CreateCdromDevice(vm_id_, "/opt/pita/tools/tools.iso")) {
MarkFailed("Failed to attach tools ISO to Plugin VM", 0);
pvm::helper::DeleteVm(vm_id_);
return;
}
// Tell it not to try cleaning directory containing our ISO as we are
// committed to using the image.
output_dir_.Take();
set_status(DISK_STATUS_CREATED);
}
std::unique_ptr<VmExportOperation> VmExportOperation::Create(
const VmId vm_id,
const base::FilePath disk_path,
base::ScopedFD fd,
base::ScopedFD digest_fd,
ArchiveFormat out_fmt) {
auto op = base::WrapUnique(new VmExportOperation(
std::move(vm_id), std::move(disk_path), std::move(fd),
std::move(digest_fd), std::move(out_fmt)));
if (op->PrepareInput() && op->PrepareOutput()) {
op->set_status(DISK_STATUS_IN_PROGRESS);
}
return op;
}
VmExportOperation::VmExportOperation(const VmId vm_id,
const base::FilePath disk_path,
base::ScopedFD out_fd,
base::ScopedFD out_digest_fd,
ArchiveFormat out_fmt)
: vm_id_(std::move(vm_id)),
src_image_path_(std::move(disk_path)),
out_fd_(std::move(out_fd)),
out_digest_fd_(std::move(out_digest_fd)),
copying_data_(false),
out_fmt_(std::move(out_fmt)),
sha256_(crypto::SecureHash::Create(crypto::SecureHash::SHA256)) {
base::File::Info info;
if (GetFileInfo(src_image_path_, &info) && !info.is_directory) {
set_source_size(info.size);
image_is_directory_ = false;
} else {
set_source_size(ComputeDirectorySize(src_image_path_));
image_is_directory_ = true;
}
}
VmExportOperation::~VmExportOperation() {
// Ensure that the archive reader and writers are destroyed first, as these
// can invoke callbacks that rely on data in this object.
in_.reset();
out_.reset();
}
bool VmExportOperation::PrepareInput() {
in_ = ArchiveReader(archive_read_disk_new());
if (!in_) {
set_failure_reason("libarchive: failed to create reader");
return false;
}
// Do not cross mount points and do not archive chattr and xattr attributes.
archive_read_disk_set_behavior(
in_.get(), ARCHIVE_READDISK_NO_TRAVERSE_MOUNTS |
ARCHIVE_READDISK_NO_FFLAGS | ARCHIVE_READDISK_NO_XATTR);
// Do not traverse symlinks.
archive_read_disk_set_symlink_physical(in_.get());
int ret = archive_read_disk_open(in_.get(), src_image_path_.value().c_str());
if (ret != ARCHIVE_OK) {
set_failure_reason("failed to open source directory as an archive");
return false;
}
return true;
}
bool VmExportOperation::PrepareOutput() {
out_ = ArchiveWriter(archive_write_new());
if (!out_) {
set_failure_reason("libarchive: failed to create writer");
return false;
}
int ret;
switch (out_fmt_) {
case ArchiveFormat::ZIP:
ret = archive_write_set_format_zip(out_.get());
if (ret != ARCHIVE_OK) {
set_failure_reason(base::StringPrintf(
"libarchive: failed to initialize zip format: %s",
archive_error_string(out_.get())));
return false;
}
break;
case ArchiveFormat::TAR_GZ:
ret = archive_write_add_filter_gzip(out_.get());
if (ret != ARCHIVE_OK) {
set_failure_reason(base::StringPrintf(
"libarchive: failed to initialize gzip filter: %s",
archive_error_string(out_.get())));
return false;
}
ret = archive_write_set_format_pax_restricted(out_.get());
if (ret != ARCHIVE_OK) {
set_failure_reason(base::StringPrintf(
"libarchive: failed to initialize pax format: %s",
archive_error_string(out_.get())));
return false;
}
break;
}
ret = archive_write_open(out_.get(), reinterpret_cast<void*>(this),
OutputFileOpenCallback, OutputFileWriteCallback,
OutputFileCloseCallback);
if (ret != ARCHIVE_OK) {
set_failure_reason("failed to open output archive");
return false;
}
return true;
}
// static
int VmExportOperation::OutputFileOpenCallback(archive* a, void* data) {
// We expect that we are writing into a regular file, so no padding is needed.
archive_write_set_bytes_in_last_block(a, 1);
return ARCHIVE_OK;
}
// static
ssize_t VmExportOperation::OutputFileWriteCallback(archive* a,
void* data,
const void* buf,
size_t length) {
VmExportOperation* op = reinterpret_cast<VmExportOperation*>(data);
ssize_t bytes_written = HANDLE_EINTR(write(op->out_fd_.get(), buf, length));
if (bytes_written <= 0) {
archive_set_error(a, errno, "Write error");
return -1;
}
op->sha256_->Update(buf, bytes_written);
return bytes_written;
}
// static
int VmExportOperation::OutputFileCloseCallback(archive* a, void* data) {
return ARCHIVE_OK;
}
void VmExportOperation::MarkFailed(const char* msg, struct archive* a) {
set_status(DISK_STATUS_FAILED);
if (a) {
set_failure_reason(
base::StringPrintf("%s: %s", msg, archive_error_string(a)));
} else {
set_failure_reason(msg);
}
LOG(ERROR) << "Vm export failed: " << failure_reason();
// Release resources.
out_.reset();
out_fd_.reset();
out_digest_fd_.reset();
in_.reset();
}
bool VmExportOperation::ExecuteIo(uint64_t io_limit) {
do {
if (!copying_data_) {
struct archive_entry* entry;
int ret = archive_read_next_header(in_.get(), &entry);
if (ret == ARCHIVE_EOF) {
// Successfully copied entire archive.
return true;
}
if (ret < ARCHIVE_OK) {
MarkFailed("failed to read header", in_.get());
break;
}
// Signal our intent to descend into directory (noop if current entry
// is not a directory).
archive_read_disk_descend(in_.get());
const char* c_path = archive_entry_pathname(entry);
if (!c_path || c_path[0] == '\0') {
MarkFailed("archive entry read from disk has empty file name", NULL);
break;
}
base::FilePath path(c_path);
if (image_is_directory_) {
if (path == src_image_path_) {
// Skip the image directory entry itself, as we will be storing
// and restoring relative paths.
continue;
}
// Strip the leading directory data as we want relative path,
// and replace it with <vm_name>.pvm prefix.
base::FilePath dest_path(vm_id_.name() + ".pvm");
if (!src_image_path_.AppendRelativePath(path, &dest_path)) {
MarkFailed("failed to transform archive entry name", NULL);
break;
}
archive_entry_set_pathname(entry, dest_path.value().c_str());
} else {
archive_entry_set_pathname(entry, path.BaseName().value().c_str());
}
ret = archive_write_header(out_.get(), entry);
if (ret != ARCHIVE_OK) {
MarkFailed("failed to write header", out_.get());
break;
}
copying_data_ = archive_entry_size(entry) > 0;
}
if (copying_data_) {
uint64_t bytes_read = CopyEntry(io_limit);
io_limit -= std::min(bytes_read, io_limit);
AccumulateProcessedSize(bytes_read);
}
if (!copying_data_) {
int ret = archive_write_finish_entry(out_.get());
if (ret != ARCHIVE_OK) {
MarkFailed("failed to finish entry", out_.get());
break;
}
}
} while (status() == DISK_STATUS_IN_PROGRESS && io_limit > 0);
// More copying is to be done (or there was a failure).
return false;
}
uint64_t VmExportOperation::CopyEntry(uint64_t io_limit) {
uint64_t bytes_read = 0;
do {
uint8_t buf[16384];
int count = archive_read_data(in_.get(), buf, sizeof(buf));
if (count == 0) {
// No more data
copying_data_ = false;
break;
}
if (count < 0) {
MarkFailed("failed to read data block", in_.get());
break;
}
bytes_read += count;
int ret = archive_write_data(out_.get(), buf, count);
if (ret < ARCHIVE_OK) {
MarkFailed("failed to write data block", out_.get());
break;
}
} while (bytes_read < io_limit);
return bytes_read;
}
void VmExportOperation::Finalize() {
archive_read_close(in_.get());
// Free the input archive.
in_.reset();
int ret = archive_write_close(out_.get());
if (ret != ARCHIVE_OK) {
MarkFailed("libarchive: failed to close writer", out_.get());
return;
}
// Free the output archive structures.
out_.reset();
// Close the file descriptor.
out_fd_.reset();
// Calculate and store the image hash.
if (out_digest_fd_.is_valid()) {
std::vector<uint8_t> digest(sha256_->GetHashLength());
sha256_->Finish(base::data(digest), digest.size());
std::string str = base::StringPrintf(
"%s\n", base::HexEncode(base::data(digest), digest.size()).c_str());
bool written =
base::WriteFileDescriptor(out_digest_fd_.get(), str.data(), str.size());
out_digest_fd_.reset();
if (!written) {
LOG(ERROR) << "Failed to write SHA256 digest of the exported image";
set_status(DISK_STATUS_FAILED);
return;
}
}
set_status(DISK_STATUS_CREATED);
}
std::unique_ptr<PluginVmImportOperation> PluginVmImportOperation::Create(
base::ScopedFD fd,
const base::FilePath disk_path,
uint64_t source_size,
const VmId vm_id,
scoped_refptr<dbus::Bus> bus,
dbus::ObjectProxy* vmplugin_service_proxy) {
auto op = base::WrapUnique(new PluginVmImportOperation(
std::move(fd), source_size, std::move(disk_path), std::move(vm_id),
std::move(bus), vmplugin_service_proxy));
if (op->PrepareInput() && op->PrepareOutput()) {
op->set_status(DISK_STATUS_IN_PROGRESS);
}
return op;
}
PluginVmImportOperation::PluginVmImportOperation(
base::ScopedFD in_fd,
uint64_t source_size,
const base::FilePath disk_path,
const VmId vm_id,
scoped_refptr<dbus::Bus> bus,
dbus::ObjectProxy* vmplugin_service_proxy)
: dest_image_path_(std::move(disk_path)),
vm_id_(std::move(vm_id)),
bus_(std::move(bus)),
vmplugin_service_proxy_(vmplugin_service_proxy),
in_fd_(std::move(in_fd)),
copying_data_(false) {
set_source_size(source_size);
}
PluginVmImportOperation::~PluginVmImportOperation() {
// Ensure that the archive reader and writers are destroyed first, as these
// can invoke callbacks that rely on data in this object.
in_.reset();
out_.reset();
}
bool PluginVmImportOperation::PrepareInput() {
in_ = ArchiveReader(archive_read_new());
if (!in_.get()) {
set_failure_reason("libarchive: failed to create reader");
return false;
}
int ret = archive_read_support_format_zip(in_.get());
if (ret != ARCHIVE_OK) {
set_failure_reason("libarchive: failed to initialize zip format");
return false;
}
ret = archive_read_support_filter_all(in_.get());
if (ret != ARCHIVE_OK) {
set_failure_reason("libarchive: failed to initialize filter");
return false;
}
ret = archive_read_open_fd(in_.get(), in_fd_.get(), 102400);
if (ret != ARCHIVE_OK) {
set_failure_reason("failed to open input archive");
return false;
}
return true;
}
bool PluginVmImportOperation::PrepareOutput() {
// We are not using CreateUniqueTempDirUnderPath() because we want
// to be able to identify images that are being imported, and that
// requires directory name to not be random.
base::FilePath disk_path(dest_image_path_.AddExtension(".tmp"));
if (base::PathExists(disk_path)) {
set_failure_reason("VM with this name is already being imported");
return false;
}
base::File::Error dir_error;
if (!base::CreateDirectoryAndGetError(disk_path, &dir_error)) {
set_failure_reason(std::string("failed to create output directory: ") +
base::File::ErrorToString(dir_error));
return false;
}
CHECK(output_dir_.Set(disk_path));
out_ = ArchiveWriter(archive_write_disk_new());
if (!out_) {
set_failure_reason("libarchive: failed to create writer");
return false;
}
int ret = archive_write_disk_set_options(
out_.get(), ARCHIVE_EXTRACT_SECURE_SYMLINKS |
ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_OWNER);
if (ret != ARCHIVE_OK) {
set_failure_reason("libarchive: failed to initialize filter");
return false;
}
return true;
}
void PluginVmImportOperation::MarkFailed(const char* msg, struct archive* a) {
set_status(DISK_STATUS_FAILED);
if (a) {
set_failure_reason(
base::StringPrintf("%s: %s", msg, archive_error_string(a)));
} else {
set_failure_reason(msg);
}
LOG(ERROR) << "PluginVm import failed: " << failure_reason();
// Release resources.
out_.reset();
if (output_dir_.IsValid() && !output_dir_.Delete()) {
LOG(WARNING) << "Failed to delete output directory on error";
}
in_.reset();
in_fd_.reset();
}
bool PluginVmImportOperation::ExecuteIo(uint64_t io_limit) {
do {
if (!copying_data_) {
struct archive_entry* entry;
int ret = archive_read_next_header(in_.get(), &entry);
if (ret == ARCHIVE_EOF) {
// Successfully copied entire archive.
return true;
}
if (ret < ARCHIVE_OK) {
MarkFailed("failed to read header", in_.get());
break;
}
const char* c_path = archive_entry_pathname(entry);
if (!c_path || c_path[0] == '\0') {
MarkFailed("archive entry has empty file name", NULL);
break;
}
base::FilePath path(c_path);
if (path.empty() || path.IsAbsolute() || path.ReferencesParent()) {
MarkFailed(
"archive entry has invalid/absolute/referencing parent file name",
NULL);
break;
}
// Drop the top level <directory>.pvm prefix, if it is present.
std::vector<std::string> path_parts;
path.GetComponents(&path_parts);
DCHECK(!path_parts.empty());
auto dest_path = output_dir_.GetPath();
auto i = path_parts.begin();
if (base::FilePath(*i).FinalExtension() == ".pvm")
i++;
for (; i != path_parts.end(); i++) {
dest_path = dest_path.Append(*i);
}
archive_entry_set_pathname(entry, dest_path.value().c_str());
archive_entry_set_uid(entry, getuid());
archive_entry_set_gid(entry, kPluginVmGid);
mode_t mode = archive_entry_filetype(entry);
switch (mode) {
case AE_IFREG:
archive_entry_set_perm(entry, 0660);
break;
case AE_IFDIR:
archive_entry_set_perm(entry, 0770);
break;
}
ret = archive_write_header(out_.get(), entry);
if (ret != ARCHIVE_OK) {
MarkFailed("failed to write header", out_.get());
break;
}
copying_data_ = archive_entry_size(entry) > 0;
}
if (copying_data_) {
uint64_t bytes_read = CopyEntry(io_limit);
io_limit -= std::min(bytes_read, io_limit);
AccumulateProcessedSize(bytes_read);
}
if (!copying_data_) {
int ret = archive_write_finish_entry(out_.get());
if (ret != ARCHIVE_OK) {
MarkFailed("failed to finish entry", out_.get());
break;
}
}
} while (status() == DISK_STATUS_IN_PROGRESS && io_limit > 0);
// More copying is to be done (or there was a failure).
return false;
}
// Note that this is extremely similar to VmExportOperation::CopyEntry()
// implementation. The difference is the disk writer supports
// archive_write_data_block() API that handles sparse files, whereas generic
// writer does not, so we have to use separate implementations.
uint64_t PluginVmImportOperation::CopyEntry(uint64_t io_limit) {
uint64_t bytes_read_begin = archive_filter_bytes(in_.get(), -1);
uint64_t bytes_read = 0;
do {
const void* buff;
size_t size;
la_int64_t offset;
int ret = archive_read_data_block(in_.get(), &buff, &size, &offset);
if (ret == ARCHIVE_EOF) {
copying_data_ = false;
break;
}
if (ret != ARCHIVE_OK) {
MarkFailed("failed to read data block", in_.get());
break;
}
bytes_read = archive_filter_bytes(in_.get(), -1) - bytes_read_begin;
ret = archive_write_data_block(out_.get(), buff, size, offset);
if (ret != ARCHIVE_OK) {
MarkFailed("failed to write data block", out_.get());
break;
}
} while (bytes_read < io_limit);
return bytes_read;
}
void PluginVmImportOperation::Finalize() {
archive_read_close(in_.get());
// Free the input archive.
in_.reset();
// Close the file descriptor.
in_fd_.reset();
int ret = archive_write_close(out_.get());
if (ret != ARCHIVE_OK) {
MarkFailed("libarchive: failed to close writer", out_.get());
return;
}
// Free the output archive structures.
out_.reset();
// Make sure resulting image is accessible by the dispatcher process.
if (chown(output_dir_.GetPath().value().c_str(), -1, kPluginVmGid) < 0) {
MarkFailed("failed to change group of the destination directory", NULL);
return;
}
// We are setting setgid bit on the directory to make sure any new files
// created by the plugin will be created with "pluginvm" group ownership.
if (chmod(output_dir_.GetPath().value().c_str(), 02770) < 0) {
MarkFailed("failed to change permissions of the destination directory",
NULL);
return;
}
// Drop the ".tmp" suffix from the directory so that we recognize
// it as a valid Plugin VM image.
if (!base::Move(output_dir_.GetPath(), dest_image_path_)) {
MarkFailed("Unable to rename resulting image directory", NULL);
return;
}
// Tell it not to try cleaning up as we are committed to using the
// image.
output_dir_.Take();
if (!pvm::dispatcher::RegisterVm(bus_, vmplugin_service_proxy_, vm_id_,
dest_image_path_)) {
MarkFailed("Unable to register imported VM image", NULL);
DeletePathRecursively(dest_image_path_);
return;
}
set_status(DISK_STATUS_CREATED);
}
std::unique_ptr<VmResizeOperation> VmResizeOperation::Create(
const VmId vm_id,
StorageLocation location,
const base::FilePath disk_path,
uint64_t disk_size,
ResizeCallback start_resize_cb,
ResizeCallback process_resize_cb) {
DiskImageStatus status = DiskImageStatus::DISK_STATUS_UNKNOWN;
std::string failure_reason;
start_resize_cb.Run(vm_id.owner_id(), vm_id.name(), location, disk_size,
&status, &failure_reason);
auto op = base::WrapUnique(new VmResizeOperation(
std::move(vm_id), std::move(location), std::move(disk_path),
std::move(disk_size), std::move(process_resize_cb)));
op->set_status(status);
op->set_failure_reason(failure_reason);
return op;
}
VmResizeOperation::VmResizeOperation(const VmId vm_id,
StorageLocation location,
const base::FilePath disk_path,
uint64_t disk_size,
ResizeCallback process_resize_cb)
: process_resize_cb_(std::move(process_resize_cb)),
vm_id_(std::move(vm_id)),
location_(std::move(location)),
disk_path_(std::move(disk_path)),
target_size_(std::move(disk_size)) {}
bool VmResizeOperation::ExecuteIo(uint64_t io_limit) {
DiskImageStatus status = DiskImageStatus::DISK_STATUS_UNKNOWN;
std::string failure_reason;
process_resize_cb_.Run(vm_id_.owner_id(), vm_id_.name(), location_,
target_size_, &status, &failure_reason);
set_status(status);
set_failure_reason(failure_reason);
if (status != DISK_STATUS_IN_PROGRESS) {
return true;
}
return false;
}
void VmResizeOperation::Finalize() {}
} // namespace concierge
} // namespace vm_tools