blob: 696bb512ae92b7ac98178788fd69d4b95ea83d1a [file] [log] [blame] [edit]
// Copyright 2018 The Fuchsia 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 "src/virtualization/bin/linux_runner/guest.h"
#include <arpa/inet.h>
#include <dirent.h>
#include <fcntl.h>
#include <fuchsia/device/cpp/fidl.h>
#include <fuchsia/hardware/block/volume/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <lib/async/default.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fidl/cpp/vector.h>
#include <lib/fit/defer.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/status.h>
#include <netinet/in.h>
#include <sys/mount.h>
#include <unistd.h>
#include <zircon/hw/gpt.h>
#include <zircon/processargs.h>
#include <zircon/status.h>
#include <memory>
#include <fbl/unique_fd.h>
#include "src/virtualization/bin/linux_runner/ports.h"
#include "src/virtualization/lib/grpc/grpc_vsock_stub.h"
#include "src/virtualization/lib/guest_config/guest_config.h"
#include "src/virtualization/third_party/vm_tools/vm_guest.grpc.pb.h"
#include <grpc++/grpc++.h>
#include <grpc++/server_posix.h>
namespace {
constexpr const char kLinuxGuestPackage[] =
"fuchsia-pkg://fuchsia.com/termina_guest#meta/termina_guest.cmx";
constexpr const char kContainerName[] = "penguin";
constexpr const char kContainerImageAlias[] = "debian/bullseye";
constexpr const char kContainerImageServer[] = "https://storage.googleapis.com/cros-containers/96";
constexpr const char kDefaultContainerUser[] = "machina";
constexpr const char kWaylandBridgePackage[] =
"fuchsia-pkg://fuchsia.com/wayland_bridge#meta/wayland_bridge.cmx";
#if defined(USE_VOLATILE_BLOCK)
constexpr bool kForceVolatileWrites = true;
#else
constexpr bool kForceVolatileWrites = false;
#endif
constexpr size_t kNumRetries = 5;
constexpr auto kRetryDelay = zx::msec(100);
constexpr const char kBlockPath[] = "/dev/class/block";
constexpr auto kGuidSize = fuchsia::hardware::block::partition::GUID_LENGTH;
constexpr const char kGuestPartitionName[] = "guest";
constexpr std::array<uint8_t, kGuidSize> kGuestPartitionGuid = {
0x9a, 0x17, 0x7d, 0x2d, 0x8b, 0x24, 0x4a, 0x4c, 0x87, 0x11, 0x1f, 0x99, 0x05, 0xb7, 0x6e, 0xd1,
};
constexpr std::array<uint8_t, kGuidSize> kFvmGuid = GUID_FVM_VALUE;
constexpr std::array<uint8_t, kGuidSize> kGptFvmGuid = GPT_FVM_TYPE_GUID;
using VolumeHandle = fidl::InterfaceHandle<fuchsia::hardware::block::volume::Volume>;
using ManagerHandle = fidl::InterfaceHandle<fuchsia::hardware::block::volume::VolumeManager>;
// Information about a disk image.
struct DiskImage {
const char* path; // Path to the file containing the image
fuchsia::virtualization::BlockFormat format; // Format of the disk image
bool read_only;
};
#ifdef USE_PREBUILT_STATEFUL_IMAGE
constexpr DiskImage kStatefulImage = DiskImage{
.path = "/pkg/data/stateful.qcow2",
.format = fuchsia::virtualization::BlockFormat::QCOW,
.read_only = true,
};
#else
constexpr DiskImage kStatefulImage = DiskImage{
.format = fuchsia::virtualization::BlockFormat::BLOCK,
.read_only = false,
};
#endif
constexpr DiskImage kExtrasImage = DiskImage{
.path = "/pkg/data/extras.img",
.format = fuchsia::virtualization::BlockFormat::FILE,
.read_only = true,
};
// Finds the guest FVM partition, and the FVM GPT partition.
zx::status<std::tuple<VolumeHandle, ManagerHandle>> FindPartitions(DIR* dir) {
VolumeHandle volume;
ManagerHandle manager;
fdio_cpp::UnownedFdioCaller caller(dirfd(dir));
for (dirent* entry; (entry = readdir(dir)) != nullptr;) {
fuchsia::hardware::block::partition::PartitionSyncPtr partition;
zx_status_t status = fdio_service_connect_at(caller.borrow_channel(), entry->d_name,
partition.NewRequest().TakeChannel().release());
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to connect to '" << entry->d_name
<< "': " << zx_status_get_string(status);
return zx::error(status);
}
zx_status_t guid_status;
std::unique_ptr<fuchsia::hardware::block::partition::GUID> guid;
status = partition->GetTypeGuid(&guid_status, &guid);
if (status != ZX_OK || guid_status != ZX_OK || !guid) {
continue;
} else if (std::equal(kGuestPartitionGuid.begin(), kGuestPartitionGuid.end(),
guid->value.begin())) {
// If we find the guest FVM partition, then we can break out of the loop.
// We only need to find the FVM GPT partition if there is no guest FVM
// partition, in order to create the guest FVM partition.
volume.set_channel(partition.Unbind().TakeChannel());
break;
} else if (std::equal(kFvmGuid.begin(), kFvmGuid.end(), guid->value.begin()) ||
std::equal(kGptFvmGuid.begin(), kGptFvmGuid.end(), guid->value.begin())) {
fuchsia::device::ControllerSyncPtr controller;
controller.Bind(partition.Unbind().TakeChannel());
fuchsia::device::Controller_GetTopologicalPath_Result topo_result;
status = controller->GetTopologicalPath(&topo_result);
if (status != ZX_OK || topo_result.is_err()) {
FX_LOGS(ERROR) << "Failed to get topological path for '" << entry->d_name << "'";
return zx::error(ZX_ERR_IO);
}
auto fvm_path = topo_result.response().path + "/fvm";
status = fdio_service_connect(fvm_path.data(), manager.NewRequest().TakeChannel().release());
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to connect to '" << fvm_path
<< "': " << zx_status_get_string(status);
return zx::error(status);
}
}
}
return zx::ok(std::make_tuple(std::move(volume), std::move(manager)));
}
// Waits for the guest partition to be allocated.
//
// TODO(fxbug.dev/90469): Use a directory watcher instead of scanning for
// new partitions.
zx::status<VolumeHandle> WaitForPartition(DIR* dir) {
for (size_t retry = 0; retry != kNumRetries; retry++) {
auto partitions = FindPartitions(dir);
if (partitions.is_error()) {
return partitions.take_error();
}
auto& [volume, manager] = *partitions;
if (volume) {
return zx::ok(std::move(volume));
}
zx::nanosleep(zx::deadline_after(kRetryDelay));
}
FX_LOGS(ERROR) << "Failed to create guest partition";
return zx::error(ZX_ERR_IO);
}
// Locates the FVM partition for a guest block device. If a partition does not
// exist, allocate one.
zx::status<VolumeHandle> FindOrAllocatePartition(std::string_view path, size_t partition_size) {
auto dir = opendir(path.data());
if (dir == nullptr) {
FX_LOGS(ERROR) << "Failed to open directory '" << path << "'";
return zx::error(ZX_ERR_IO);
}
auto defer = fit::defer([dir] { closedir(dir); });
auto partitions = FindPartitions(dir);
if (partitions.is_error()) {
return partitions.take_error();
}
auto& [volume, manager] = *partitions;
if (!volume) {
if (!manager) {
FX_LOGS(ERROR) << "Failed to find FVM";
return zx::error(ZX_ERR_NOT_FOUND);
}
auto sync = manager.BindSync();
zx_status_t info_status = ZX_OK;
// Get the partition slice size.
std::unique_ptr<fuchsia::hardware::block::volume::VolumeManagerInfo> info;
zx_status_t status = sync->GetInfo(&info_status, &info);
if (status != ZX_OK || info_status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to get volume info: " << zx_status_get_string(status) << " and "
<< zx_status_get_string(info_status);
return zx::error(ZX_ERR_IO);
}
size_t slices = partition_size / info->slice_size;
zx_status_t part_status = ZX_OK;
status = sync->AllocatePartition(slices, {.value = kGuestPartitionGuid}, {},
kGuestPartitionName, 0, &part_status);
if (status != ZX_OK || part_status != ZX_OK) {
FX_LOGS(ERROR) << "Failed to allocate partition: " << zx_status_get_string(status) << " and "
<< zx_status_get_string(part_status);
return zx::error(ZX_ERR_IO);
}
return WaitForPartition(dir);
}
return zx::ok(std::move(volume));
}
// Opens the given disk image.
zx::status<fuchsia::io::FileHandle> GetPartition(const DiskImage& image) {
TRACE_DURATION("linux_runner", "GetPartition");
fuchsia::io::OpenFlags flags = fuchsia::io::OpenFlags::RIGHT_READABLE;
if (!image.read_only) {
flags |= fuchsia::io::OpenFlags::RIGHT_WRITABLE;
}
fuchsia::io::FileHandle file;
zx_status_t status = fdio_open(image.path, static_cast<uint32_t>(flags),
file.NewRequest().TakeChannel().release());
if (status) {
return zx::error(status);
}
return zx::ok(std::move(file));
}
// Return the given IPv4 address as a packed uint32_t in network byte
// order (i.e., big endian).
//
// `Ipv4Addr(127, 0, 0, 1)` will generate the loopback address "127.0.0.1".
constexpr uint32_t Ipv4Addr(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
return htonl((static_cast<uint32_t>(a) << 24) | (static_cast<uint32_t>(b) << 16) |
(static_cast<uint32_t>(c) << 8) | (static_cast<uint32_t>(d) << 0));
}
// Run the given command in the guest as a daemon (i.e., in the background and
// automatically restarted on failure).
void MaitredStartDaemon(vm_tools::Maitred::Stub& maitred, std::vector<std::string> args,
std::vector<std::pair<std::string, std::string>> env) {
grpc::ClientContext context;
vm_tools::LaunchProcessRequest request;
vm_tools::LaunchProcessResponse response;
// Set up args / environment.
request.mutable_argv()->Assign(args.begin(), args.end());
request.mutable_env()->insert(env.begin(), env.end());
// Set up as a daemon.
request.set_use_console(true);
request.set_respawn(true);
request.set_wait_for_exit(false);
TRACE_DURATION("linux_runner", "LaunchProcessRPC");
grpc::Status status = maitred.LaunchProcess(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to start daemon in guest: " << status.error_message() << "\n"
<< "Command run: " << request.DebugString();
FX_CHECK(response.status() == vm_tools::ProcessStatus::LAUNCHED)
<< "Process failed to launch, with launch status: "
<< vm_tools::ProcessStatus_Name(response.status());
}
// Run the given command in the guest, blocking until finished.
void MaitredRunCommandSync(vm_tools::Maitred::Stub& maitred, std::vector<std::string> args,
std::vector<std::pair<std::string, std::string>> env) {
grpc::ClientContext context;
vm_tools::LaunchProcessRequest request;
vm_tools::LaunchProcessResponse response;
// Set up args / environment.
request.mutable_argv()->Assign(args.begin(), args.end());
request.mutable_env()->insert(env.begin(), env.end());
// Set the command as synchronous.
request.set_use_console(true);
request.set_respawn(false);
request.set_wait_for_exit(true);
TRACE_DURATION("linux_runner", "LaunchProcessRPC");
grpc::Status status = maitred.LaunchProcess(&context, request, &response);
FX_CHECK(status.ok()) << "Guest command failed: " << status.error_message();
}
// Ask maitre'd to enable the network in the guest.
void MaitredBringUpNetwork(vm_tools::Maitred::Stub& maitred, uint32_t address, uint32_t gateway,
uint32_t netmask) {
grpc::ClientContext context;
vm_tools::NetworkConfigRequest request;
vm_tools::EmptyMessage response;
vm_tools::IPv4Config* config = request.mutable_ipv4_config();
config->set_address(Ipv4Addr(100, 64, 1, 1)); // 100.64.1.1, RFC-6598 address
config->set_gateway(Ipv4Addr(100, 64, 1, 2)); // 100.64.1.2, RFC-6598 address
config->set_netmask(Ipv4Addr(255, 255, 255, 252)); // 30-bit netmask
TRACE_DURATION("linux_runner", "ConfigureNetworkRPC");
grpc::Status status = maitred.ConfigureNetwork(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to configure guest network: " << status.error_message();
}
} // namespace
namespace linux_runner {
// static
zx_status_t Guest::CreateAndStart(sys::ComponentContext* context, GuestConfig config,
GuestInfoCallback callback, std::unique_ptr<Guest>* guest) {
TRACE_DURATION("linux_runner", "Guest::CreateAndStart");
fuchsia::virtualization::ManagerPtr guestmgr;
context->svc()->Connect(guestmgr.NewRequest());
fuchsia::virtualization::RealmPtr guest_env;
guestmgr->Create(config.env_label, guest_env.NewRequest());
*guest = std::make_unique<Guest>(context, config, std::move(callback), std::move(guest_env));
return ZX_OK;
}
Guest::Guest(sys::ComponentContext* context, GuestConfig config, GuestInfoCallback callback,
fuchsia::virtualization::RealmPtr env)
: async_(async_get_default_dispatcher()),
executor_(async_),
config_(config),
callback_(std::move(callback)),
guest_env_(std::move(env)),
wayland_dispatcher_(context, kWaylandBridgePackage) {
guest_env_->GetHostVsockEndpoint(socket_endpoint_.NewRequest());
executor_.schedule_task(Start());
}
Guest::~Guest() {
if (grpc_server_) {
grpc_server_->inner()->Shutdown();
grpc_server_->inner()->Wait();
}
}
fpromise::promise<> Guest::Start() {
TRACE_DURATION("linux_runner", "Guest::Start");
return StartGrpcServer()
.and_then([this](std::unique_ptr<GrpcVsockServer>& server) mutable
-> fpromise::result<void, zx_status_t> {
grpc_server_ = std::move(server);
StartGuest();
return fpromise::ok();
})
.or_else([](const zx_status_t& status) {
FX_LOGS(ERROR) << "Failed to start guest: " << status;
return fpromise::ok();
});
}
fpromise::promise<std::unique_ptr<GrpcVsockServer>, zx_status_t> Guest::StartGrpcServer() {
TRACE_DURATION("linux_runner", "Guest::StartGrpcServer");
fuchsia::virtualization::HostVsockEndpointPtr socket_endpoint;
guest_env_->GetHostVsockEndpoint(socket_endpoint.NewRequest());
GrpcVsockServerBuilder builder(std::move(socket_endpoint));
// CrashListener
builder.AddListenPort(kCrashListenerPort);
builder.RegisterService(&crash_listener_);
// LogCollector
builder.AddListenPort(kLogCollectorPort);
builder.RegisterService(&log_collector_);
// StartupListener
builder.AddListenPort(kStartupListenerPort);
builder.RegisterService(static_cast<vm_tools::StartupListener::Service*>(this));
// TremplinListener
builder.AddListenPort(kTremplinListenerPort);
builder.RegisterService(static_cast<vm_tools::tremplin::TremplinListener::Service*>(this));
// ContainerListener
builder.AddListenPort(kGarconPort);
builder.RegisterService(static_cast<vm_tools::container::ContainerListener::Service*>(this));
return builder.Build();
}
std::vector<fuchsia::virtualization::BlockSpec> Guest::GetBlockDevices(size_t stateful_image_size) {
TRACE_DURATION("linux_runner", "Guest::GetBlockDevices");
std::vector<fuchsia::virtualization::BlockSpec> devices;
// Get/create the stateful partition.
zx::channel stateful;
if (kStatefulImage.format == fuchsia::virtualization::BlockFormat::BLOCK) {
auto handle = FindOrAllocatePartition(kBlockPath, stateful_image_size);
if (handle.is_error()) {
PostContainerFailure("Failed to find or allocate a partition");
return devices;
}
stateful = handle->TakeChannel();
} else {
auto handle = GetPartition(kStatefulImage);
if (handle.is_error()) {
PostContainerFailure("Failed to open or create stateful file");
return devices;
}
stateful = handle->TakeChannel();
}
devices.push_back({
.id = "stateful",
.mode = (kStatefulImage.read_only || kForceVolatileWrites)
? fuchsia::virtualization::BlockMode::VOLATILE_WRITE
: fuchsia::virtualization::BlockMode::READ_WRITE,
.format = kStatefulImage.format,
.client = std::move(stateful),
});
// Drop access to /dev, in order to prevent any further access.
fdio_ns_t* ns;
zx_status_t status = fdio_ns_get_installed(&ns);
FX_CHECK(status == ZX_OK) << "Failed to get installed namespace";
if (fdio_ns_is_bound(ns, "/dev")) {
status = fdio_ns_unbind(ns, "/dev");
FX_CHECK(status == ZX_OK) << "Failed to unbind '/dev' from the installed namespace";
}
// Add the extras partition if it exists.
auto extras = GetPartition(kExtrasImage);
if (extras.is_ok()) {
devices.push_back({
.id = "extras",
.mode = fuchsia::virtualization::BlockMode::VOLATILE_WRITE,
.format = kExtrasImage.format,
.client = extras->TakeChannel(),
});
}
return devices;
}
void Guest::StartGuest() {
TRACE_DURATION("linux_runner", "Guest::StartGuest");
FX_CHECK(!guest_controller_) << "Called StartGuest with an existing instance";
FX_LOGS(INFO) << "Launching guest...";
auto block_devices = GetBlockDevices(config_.stateful_image_size);
if (block_devices.empty()) {
FX_LOGS(ERROR) << "Failed to start guest: missing block device";
return;
}
fuchsia::virtualization::GuestConfig cfg;
cfg.set_virtio_gpu(false);
cfg.set_block_devices(std::move(block_devices));
cfg.mutable_wayland_device()->server = wayland_dispatcher_.NewBinding();
cfg.set_magma_device(fuchsia::virtualization::MagmaDevice());
auto vm_create_nonce = TRACE_NONCE();
TRACE_FLOW_BEGIN("linux_runner", "LaunchInstance", vm_create_nonce);
guest_env_->LaunchInstance(
kLinuxGuestPackage, cpp17::nullopt, std::move(cfg), guest_controller_.NewRequest(),
[this, vm_create_nonce](uint32_t cid) {
TRACE_DURATION("linux_runner", "LaunchInstance Callback");
TRACE_FLOW_END("linux_runner", "LaunchInstance", vm_create_nonce);
FX_LOGS(INFO) << "Guest launched with CID " << cid;
guest_cid_ = cid;
PostContainerStatus(fuchsia::virtualization::ContainerStatus::LAUNCHING_GUEST);
TRACE_FLOW_BEGIN("linux_runner", "TerminaBoot", vm_ready_nonce_);
});
}
void Guest::MountVmTools() {
TRACE_DURATION("linux_runner", "Guest::MountVmTools");
FX_CHECK(maitred_) << "Called MountVmTools without a maitre'd connection";
FX_LOGS(INFO) << "Mounting vm_tools";
grpc::ClientContext context;
vm_tools::MountRequest request;
vm_tools::MountResponse response;
request.mutable_source()->assign("/dev/vdb");
request.mutable_target()->assign("/opt/google/cros-containers");
request.mutable_fstype()->assign("ext4");
request.mutable_options()->assign("");
request.set_mountflags(MS_RDONLY);
{
TRACE_DURATION("linux_runner", "MountRPC");
auto grpc_status = maitred_->Mount(&context, request, &response);
FX_CHECK(grpc_status.ok()) << "Failed to mount vm_tools partition: "
<< grpc_status.error_message();
}
FX_LOGS(INFO) << "Mounted Filesystem: " << response.error();
}
void Guest::MountExtrasPartition() {
TRACE_DURATION("linux_runner", "Guest::MountExtrasPartition");
FX_CHECK(maitred_) << "Called MountExtrasPartition without a maitre'd connection";
FX_LOGS(INFO) << "Mounting Extras Partition";
grpc::ClientContext context;
vm_tools::MountRequest request;
vm_tools::MountResponse response;
request.mutable_source()->assign("/dev/vdd");
request.mutable_target()->assign("/mnt/shared");
request.mutable_fstype()->assign("romfs");
request.mutable_options()->assign("");
request.set_mountflags(0);
{
TRACE_DURATION("linux_runner", "MountRPC");
auto grpc_status = maitred_->Mount(&context, request, &response);
FX_CHECK(grpc_status.ok()) << "Failed to mount extras filesystem: "
<< grpc_status.error_message();
}
FX_LOGS(INFO) << "Mounted Filesystem: " << response.error();
}
void Guest::ConfigureNetwork() {
TRACE_DURATION("linux_runner", "Guest::ConfigureNetwork");
FX_CHECK(maitred_) << "Called ConfigureNetwork without a maitre'd connection";
FX_LOGS(INFO) << "Configuring Guest Network...";
// Perform basic network bring up.
//
// To bring up the network, maitre'd requires an IPv4 address to use for the
// guest's external NIC (even though we are going to replace it with
// a DHCP-acquired address in just a moment).
//
// We use an RFC-6598 (carrier-grade NAT) IP address distinct from the LXD
// subnet, but expect it to be overridden by DHCP later.
MaitredBringUpNetwork(*maitred_,
/*address=*/Ipv4Addr(100, 64, 1, 1), // 100.64.1.1, RFC-6598 address
/*gateway=*/Ipv4Addr(100, 64, 1, 2), // 100.64.1.2, RFC-6598 address
/*netmask=*/Ipv4Addr(255, 255, 255, 252) // 30-bit netmask
);
// Remove the configured IPv4 address from eth0.
MaitredRunCommandSync(*maitred_, /*args=*/{"/bin/ip", "address", "flush", "eth0"}, /*env=*/{});
// Run dhclient.
MaitredStartDaemon(*maitred_,
/*args=*/
{
"/sbin/dhclient",
// Lease file
"-lf",
"/run/dhclient.leases",
// PID file
"-pf",
"/run/dhclient.pid",
// Do not detach, but remain in foreground so maitre'd can monitor.
"-d",
// Interface
"eth0",
},
/*env=*/{{"HOME", "/tmp"}, {"PATH", "/sbin:/bin"}});
FX_LOGS(INFO) << "Network configured.";
}
void Guest::StartTermina() {
TRACE_DURATION("linux_runner", "Guest::StartTermina");
FX_CHECK(maitred_) << "Called StartTermina without a maitre'd connection";
FX_LOGS(INFO) << "Starting Termina...";
PostContainerStatus(fuchsia::virtualization::ContainerStatus::STARTING_VM);
grpc::ClientContext context;
vm_tools::StartTerminaRequest request;
vm_tools::StartTerminaResponse response;
std::string lxd_subnet = "100.115.92.1/24";
request.mutable_lxd_ipv4_subnet()->swap(lxd_subnet);
request.set_stateful_device("/dev/vdc");
{
TRACE_DURATION("linux_runner", "StartTerminaRPC");
auto grpc_status = maitred_->StartTermina(&context, request, &response);
FX_CHECK(grpc_status.ok()) << "Failed to start Termina: " << grpc_status.error_message();
}
}
// This exposes a shell on /dev/hvc0 that can be used to interact with the
// VM.
void Guest::LaunchContainerShell() {
FX_CHECK(maitred_) << "Called LaunchShell without a maitre'd connection";
FX_LOGS(INFO) << "Launching container shell...";
MaitredStartDaemon(
*maitred_,
{"/usr/bin/lxc", "exec", kContainerName, "--", "/bin/login", "-f", kDefaultContainerUser},
{
{"LXD_DIR", "/mnt/stateful/lxd"},
{"LXD_CONF", "/mnt/stateful/lxd_conf"},
{"LXD_UNPRIVILEGED_ONLY", "true"},
});
}
void Guest::AddMagmaDeviceToContainer() {
FX_CHECK(maitred_) << "Called AddMagma without a maitre'd connection";
FX_LOGS(INFO) << "Adding magma device to container";
MaitredRunCommandSync(*maitred_,
{"/usr/bin/lxc", "config", "device", "add", kContainerName, "magma0",
"unix-char", "source=/dev/magma0", "mode=0666"},
{
{"LXD_DIR", "/mnt/stateful/lxd"},
{"LXD_CONF", "/mnt/stateful/lxd_conf"},
{"LXD_UNPRIVILEGED_ONLY", "true"},
});
}
void Guest::SetupGPUDriversInContainer() {
FX_CHECK(maitred_) << "Called SetupGPUDrivers without a maitre'd connection";
FX_LOGS(INFO) << "Setup GPU drivers in container";
MaitredRunCommandSync(
*maitred_,
{"/usr/bin/lxc", "exec", kContainerName, "--", "sh", "-c",
"mkdir -p /usr/share/vulkan/icd.d; /usr/bin/update-alternatives --install "
"/usr/share/vulkan/icd.d/10_magma_intel_icd.x86_64.json vulkan-icd "
"/opt/google/cros-containers/share/vulkan/icd.d/intel_icd.x86_64.json 20; "
"/usr/bin/update-alternatives --install "
"/usr/share/vulkan/icd.d/10_magma_intel_icd.i686.json vulkan-icd32 "
"/opt/google/cros-containers/share/vulkan/icd.d/intel_icd.i686.json 20; "
"echo /opt/google/cros-containers/drivers/lib64=libc6 > /etc/ld.so.conf.d/cros.conf;"
"echo /opt/google/cros-containers/drivers/lib32=libc6 >> /etc/ld.so.conf.d/cros.conf;"
"/sbin/ldconfig; "},
{
{"LXD_DIR", "/mnt/stateful/lxd"},
{"LXD_CONF", "/mnt/stateful/lxd_conf"},
{"LXD_UNPRIVILEGED_ONLY", "true"},
});
}
void Guest::CreateContainer() {
TRACE_DURATION("linux_runner", "Guest::CreateContainer");
FX_CHECK(tremplin_) << "CreateContainer called without a Tremplin connection";
FX_LOGS(INFO) << "Creating Container...";
grpc::ClientContext context;
vm_tools::tremplin::CreateContainerRequest request;
vm_tools::tremplin::CreateContainerResponse response;
request.mutable_container_name()->assign(kContainerName);
request.mutable_image_alias()->assign(kContainerImageAlias);
request.mutable_image_server()->assign(kContainerImageServer);
{
TRACE_DURATION("linux_runner", "CreateContainerRPC");
auto status = tremplin_->CreateContainer(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to create container: " << status.error_message();
}
switch (response.status()) {
case vm_tools::tremplin::CreateContainerResponse::CREATING:
break;
case vm_tools::tremplin::CreateContainerResponse::EXISTS:
FX_LOGS(INFO) << "Container already exists";
StartContainer();
break;
case vm_tools::tremplin::CreateContainerResponse::FAILED:
PostContainerFailure("Failed to create container: " + response.failure_reason());
break;
case vm_tools::tremplin::CreateContainerResponse::UNKNOWN:
default:
PostContainerFailure("Unknown status: " + std::to_string(response.status()));
break;
}
}
void Guest::StartContainer() {
TRACE_DURATION("linux_runner", "Guest::StartContainer");
FX_CHECK(tremplin_) << "StartContainer called without a Tremplin connection";
FX_LOGS(INFO) << "Starting Container...";
PostContainerStatus(fuchsia::virtualization::ContainerStatus::STARTING);
grpc::ClientContext context;
vm_tools::tremplin::StartContainerRequest request;
vm_tools::tremplin::StartContainerResponse response;
request.mutable_container_name()->assign(kContainerName);
request.mutable_host_public_key()->assign("");
request.mutable_container_private_key()->assign("");
request.mutable_token()->assign("container_token");
{
TRACE_DURATION("linux_runner", "StartContainerRPC");
auto status = tremplin_->StartContainer(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to start container: " << status.error_message();
}
switch (response.status()) {
case vm_tools::tremplin::StartContainerResponse::RUNNING:
case vm_tools::tremplin::StartContainerResponse::STARTED:
FX_LOGS(INFO) << "Container started";
break;
case vm_tools::tremplin::StartContainerResponse::STARTING:
FX_LOGS(INFO) << "Container starting";
break;
case vm_tools::tremplin::StartContainerResponse::FAILED:
PostContainerFailure("Failed to start container: " + response.failure_reason());
break;
case vm_tools::tremplin::StartContainerResponse::UNKNOWN:
default:
PostContainerFailure("Unknown status: " + std::to_string(response.status()));
break;
}
}
void Guest::SetupUser() {
FX_CHECK(tremplin_) << "SetupUser called without a Tremplin connection";
FX_LOGS(INFO) << "Creating user '" << kDefaultContainerUser << "'...";
grpc::ClientContext context;
vm_tools::tremplin::SetUpUserRequest request;
vm_tools::tremplin::SetUpUserResponse response;
request.mutable_container_name()->assign(kContainerName);
request.mutable_container_username()->assign(kDefaultContainerUser);
{
TRACE_DURATION("linux_runner", "SetUpUserRPC");
auto status = tremplin_->SetUpUser(&context, request, &response);
FX_CHECK(status.ok()) << "Failed to setup user '" << kDefaultContainerUser
<< "': " << status.error_message();
}
switch (response.status()) {
case vm_tools::tremplin::SetUpUserResponse::EXISTS:
case vm_tools::tremplin::SetUpUserResponse::SUCCESS:
FX_LOGS(INFO) << "User created.";
StartContainer();
break;
case vm_tools::tremplin::SetUpUserResponse::FAILED:
PostContainerFailure("Failed to create user: " + response.failure_reason());
break;
case vm_tools::tremplin::SetUpUserResponse::UNKNOWN:
default:
PostContainerFailure("Unknown status: " + std::to_string(response.status()));
break;
}
}
grpc::Status Guest::VmReady(grpc::ServerContext* context, const vm_tools::EmptyMessage* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::VmReady");
TRACE_FLOW_END("linux_runner", "TerminaBoot", vm_ready_nonce_);
FX_LOGS(INFO) << "VM Ready -- Connecting to Maitre'd...";
auto start_maitred =
[this](fpromise::result<std::unique_ptr<vm_tools::Maitred::Stub>, zx_status_t>& result) {
FX_CHECK(result.is_ok()) << "Failed to connect to Maitre'd";
this->maitred_ = std::move(result.value());
MountVmTools();
MountExtrasPartition();
ConfigureNetwork();
StartTermina();
};
auto task = NewGrpcVsockStub<vm_tools::Maitred>(socket_endpoint_, guest_cid_, kMaitredPort)
.then(start_maitred);
executor_.schedule_task(std::move(task));
return grpc::Status::OK;
}
grpc::Status Guest::TremplinReady(grpc::ServerContext* context,
const ::vm_tools::tremplin::TremplinStartupInfo* request,
vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::TremplinReady");
FX_LOGS(INFO) << "Tremplin Ready.";
auto start_tremplin =
[this](fpromise::result<std::unique_ptr<vm_tools::tremplin::Tremplin::Stub>, zx_status_t>&
result) mutable -> fpromise::result<> {
FX_CHECK(result.is_ok()) << "Failed to connect to Tremplin";
tremplin_ = std::move(result.value());
CreateContainer();
return fpromise::ok();
};
auto task =
NewGrpcVsockStub<vm_tools::tremplin::Tremplin>(socket_endpoint_, guest_cid_, kTremplinPort)
.then(start_tremplin);
executor_.schedule_task(std::move(task));
return grpc::Status::OK;
}
grpc::Status Guest::UpdateCreateStatus(grpc::ServerContext* context,
const vm_tools::tremplin::ContainerCreationProgress* request,
vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateCreateStatus");
switch (request->status()) {
case vm_tools::tremplin::ContainerCreationProgress::CREATED:
FX_LOGS(INFO) << "Container created: " << request->container_name();
SetupUser();
break;
case vm_tools::tremplin::ContainerCreationProgress::DOWNLOADING:
PostContainerDownloadProgress(request->download_progress());
FX_LOGS(INFO) << "Downloading " << request->container_name() << ": "
<< request->download_progress() << "%";
if (request->download_progress() >= 100) {
PostContainerStatus(fuchsia::virtualization::ContainerStatus::EXTRACTING);
FX_LOGS(INFO) << "Extracting " << request->container_name();
}
break;
case vm_tools::tremplin::ContainerCreationProgress::DOWNLOAD_TIMED_OUT:
PostContainerFailure("Download timed out");
break;
case vm_tools::tremplin::ContainerCreationProgress::CANCELLED:
PostContainerFailure("Download cancelled");
break;
case vm_tools::tremplin::ContainerCreationProgress::FAILED:
PostContainerFailure("Download failed: " + request->failure_reason());
break;
case vm_tools::tremplin::ContainerCreationProgress::UNKNOWN:
default:
PostContainerFailure("Unknown download status: " + std::to_string(request->status()));
break;
}
return grpc::Status::OK;
}
grpc::Status Guest::UpdateDeletionStatus(
::grpc::ServerContext* context, const ::vm_tools::tremplin::ContainerDeletionProgress* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateDeletionStatus");
FX_LOGS(INFO) << "Update Deletion Status";
return grpc::Status::OK;
}
grpc::Status Guest::UpdateStartStatus(::grpc::ServerContext* context,
const ::vm_tools::tremplin::ContainerStartProgress* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateStartStatus");
FX_LOGS(INFO) << "Update Start Status";
switch (request->status()) {
case vm_tools::tremplin::ContainerStartProgress::STARTED:
FX_LOGS(INFO) << "Container started";
break;
default:
PostContainerFailure("Unknown start status: " + std::to_string(request->status()));
break;
}
return grpc::Status::OK;
}
grpc::Status Guest::UpdateExportStatus(::grpc::ServerContext* context,
const ::vm_tools::tremplin::ContainerExportProgress* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateExportStatus");
FX_LOGS(INFO) << "Update Export Status";
return grpc::Status::OK;
}
grpc::Status Guest::UpdateImportStatus(::grpc::ServerContext* context,
const ::vm_tools::tremplin::ContainerImportProgress* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateImportStatus");
FX_LOGS(INFO) << "Update Import Status";
return grpc::Status::OK;
}
grpc::Status Guest::ContainerShutdown(::grpc::ServerContext* context,
const ::vm_tools::tremplin::ContainerShutdownInfo* request,
::vm_tools::tremplin::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::ContainerShutdown");
FX_LOGS(INFO) << "Container Shutdown";
return grpc::Status::OK;
}
grpc::Status Guest::ContainerReady(grpc::ServerContext* context,
const vm_tools::container::ContainerStartupInfo* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::ContainerReady");
// Add Magma GPU support to container.
AddMagmaDeviceToContainer();
SetupGPUDriversInContainer();
// Start required user services.
LaunchContainerShell();
// Connect to Garcon service in the container.
// TODO(tjdetwiler): validate token.
auto garcon_port = request->garcon_port();
FX_LOGS(INFO) << "Container Ready; Garcon listening on port " << garcon_port;
auto start_garcon = [this](fpromise::result<std::unique_ptr<vm_tools::container::Garcon::Stub>,
zx_status_t>& result) mutable -> fpromise::result<> {
FX_CHECK(result.is_ok()) << "Failed to connect to Garcon";
garcon_ = std::move(result.value());
DumpContainerDebugInfo();
// Container is now Ready.
PostContainerStatus(fuchsia::virtualization::ContainerStatus::READY);
return fpromise::ok();
};
auto task =
NewGrpcVsockStub<vm_tools::container::Garcon>(socket_endpoint_, guest_cid_, garcon_port)
.then(start_garcon);
executor_.schedule_task(std::move(task));
return grpc::Status::OK;
}
grpc::Status Guest::ContainerShutdown(grpc::ServerContext* context,
const vm_tools::container::ContainerShutdownInfo* request,
vm_tools::EmptyMessage* response) {
FX_LOGS(INFO) << "Container Shutdown";
return grpc::Status::OK;
}
grpc::Status Guest::UpdateApplicationList(
grpc::ServerContext* context, const vm_tools::container::UpdateApplicationListRequest* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateApplicationList");
FX_LOGS(INFO) << "Update Application List";
for (const auto& application : request->application()) {
FX_LOGS(INFO) << "ID: " << application.desktop_file_id();
const auto& name = application.name().values().begin();
if (name != application.name().values().end()) {
FX_LOGS(INFO) << "\tname: " << name->value();
}
const auto& comment = application.comment().values().begin();
if (comment != application.comment().values().end()) {
FX_LOGS(INFO) << "\tcomment: " << comment->value();
}
FX_LOGS(INFO) << "\tno_display: " << application.no_display();
FX_LOGS(INFO) << "\tstartup_wm_class: " << application.startup_wm_class();
FX_LOGS(INFO) << "\tstartup_notify: " << application.startup_notify();
FX_LOGS(INFO) << "\tpackage_id: " << application.package_id();
}
return grpc::Status::OK;
}
grpc::Status Guest::OpenUrl(grpc::ServerContext* context,
const vm_tools::container::OpenUrlRequest* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::OpenUrl");
FX_LOGS(INFO) << "Open URL";
return grpc::Status::OK;
}
grpc::Status Guest::InstallLinuxPackageProgress(
grpc::ServerContext* context,
const vm_tools::container::InstallLinuxPackageProgressInfo* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::InstallLinuxPackageProgress");
FX_LOGS(INFO) << "Install Linux Package Progress";
return grpc::Status::OK;
}
grpc::Status Guest::UninstallPackageProgress(
grpc::ServerContext* context, const vm_tools::container::UninstallPackageProgressInfo* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UninstallPackageProgress");
FX_LOGS(INFO) << "Uninstall Package Progress";
return grpc::Status::OK;
}
grpc::Status Guest::OpenTerminal(grpc::ServerContext* context,
const vm_tools::container::OpenTerminalRequest* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::OpenTerminal");
FX_LOGS(INFO) << "Open Terminal";
return grpc::Status::OK;
}
grpc::Status Guest::UpdateMimeTypes(grpc::ServerContext* context,
const vm_tools::container::UpdateMimeTypesRequest* request,
vm_tools::EmptyMessage* response) {
TRACE_DURATION("linux_runner", "Guest::UpdateMimeTypes");
FX_LOGS(INFO) << "Update Mime Types";
size_t i = 0;
for (const auto& pair : request->mime_type_mappings()) {
FX_LOGS(INFO) << "\t" << pair.first << ": " << pair.second;
if (++i > 10) {
FX_LOGS(INFO) << "\t..." << (request->mime_type_mappings_size() - i) << " more.";
break;
}
}
return grpc::Status::OK;
}
void Guest::DumpContainerDebugInfo() {
FX_CHECK(garcon_) << "Called DumpContainerDebugInfo without a garcon connection";
FX_LOGS(INFO) << "Dumping Container Debug Info...";
grpc::ClientContext context;
vm_tools::container::GetDebugInformationRequest request;
vm_tools::container::GetDebugInformationResponse response;
auto grpc_status = garcon_->GetDebugInformation(&context, request, &response);
if (!grpc_status.ok()) {
FX_LOGS(ERROR) << "Failed to read container debug information: " << grpc_status.error_message();
return;
}
FX_LOGS(INFO) << "Container debug information:";
FX_LOGS(INFO) << response.debug_information();
}
void Guest::PostContainerStatus(fuchsia::virtualization::ContainerStatus container_status) {
callback_(GuestInfo{
.cid = guest_cid_,
.container_status = container_status,
});
}
void Guest::PostContainerDownloadProgress(int32_t download_progress) {
callback_(GuestInfo{
.cid = guest_cid_,
.container_status = fuchsia::virtualization::ContainerStatus::DOWNLOADING,
.download_percent = download_progress,
});
}
void Guest::PostContainerFailure(std::string failure_reason) {
FX_LOGS(ERROR) << failure_reason;
callback_(GuestInfo{
.cid = guest_cid_,
.container_status = fuchsia::virtualization::ContainerStatus::FAILED,
.failure_reason = failure_reason,
});
}
} // namespace linux_runner