blob: c1a9c19d3d0ed1ea68cf4499c80858b6758103f9 [file] [log] [blame]
// 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/tests/enclosed_guest.h"
#include <dirent.h>
#include <fcntl.h>
#include <fuchsia/kernel/cpp/fidl.h>
#include <fuchsia/net/virtualization/cpp/fidl.h>
#include <fuchsia/netstack/cpp/fidl.h>
#include <fuchsia/scheduler/cpp/fidl.h>
#include <fuchsia/sysinfo/cpp/fidl.h>
#include <fuchsia/sysmem/cpp/fidl.h>
#include <fuchsia/tracing/provider/cpp/fidl.h>
#include <fuchsia/ui/scenic/cpp/fidl.h>
#include <lib/fdio/directory.h>
#include <lib/fitx/result.h>
#include <lib/fpromise/single_threaded_executor.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/clock.h>
#include <sys/mount.h>
#include <zircon/errors.h>
#include <zircon/status.h>
#include <algorithm>
#include <memory>
#include <optional>
#include <string>
#include "fuchsia/logger/cpp/fidl.h"
#include "fuchsia/virtualization/cpp/fidl.h"
#include "src/lib/files/file.h"
#include "src/lib/files/glob.h"
#include "src/lib/fxl/strings/ascii.h"
#include "src/lib/fxl/strings/string_printf.h"
#include "src/lib/testing/loop_fixture/real_loop_fixture.h"
#include "src/virtualization/lib/grpc/grpc_vsock_stub.h"
#include "src/virtualization/lib/guest_config/guest_config.h"
#include "src/virtualization/tests/backtrace_watchdog.h"
#include "src/virtualization/tests/guest_constants.h"
#include "src/virtualization/tests/logger.h"
#include "src/virtualization/tests/periodic_logger.h"
namespace {
constexpr char kZirconGuestUrl[] =
"fuchsia-pkg://fuchsia.com/zircon_guest_manager#meta/zircon_guest_manager.cm";
constexpr char kDebianGuestUrl[] =
"fuchsia-pkg://fuchsia.com/debian_guest_manager#meta/debian_guest_manager.cm";
constexpr char kTerminaGuestUrl[] =
"fuchsia-pkg://fuchsia.com/termina_guest_manager#meta/termina_guest_manager.cm";
// TODO(fxbug.dev/12589): Use consistent naming for the test utils here.
constexpr char kDebianTestUtilDir[] = "/test_utils";
constexpr zx::duration kLoopConditionStep = zx::msec(10);
constexpr zx::duration kRetryStep = zx::msec(200);
constexpr uint32_t kTerminaStartupListenerPort = 7777;
constexpr uint32_t kTerminaMaitredPort = 8888;
bool RunLoopUntil(async::Loop* loop, fit::function<bool()> condition, zx::time deadline) {
while (zx::clock::get_monotonic() < deadline) {
// Check our condition.
if (condition()) {
return true;
}
// Wait until next polling interval.
loop->Run(zx::deadline_after(kLoopConditionStep));
loop->ResetQuit();
}
return condition();
}
std::string JoinArgVector(const std::vector<std::string>& argv) {
std::string result;
for (const auto& arg : argv) {
result += arg;
result += " ";
}
return result;
}
} // namespace
// Execute |command| on the guest serial and wait for the |result|.
zx_status_t EnclosedGuest::Execute(const std::vector<std::string>& argv,
const std::unordered_map<std::string, std::string>& env,
zx::time deadline, std::string* result, int32_t* return_code) {
if (!env.empty()) {
FX_LOGS(ERROR) << "Only TerminaEnclosedGuest::Execute accepts environment variables.";
return ZX_ERR_NOT_SUPPORTED;
}
auto command = JoinArgVector(argv);
return console_->ExecuteBlocking(command, ShellPrompt(), deadline, result);
}
zx_status_t EnclosedGuest::Start(zx::time deadline) {
using component_testing::RealmBuilder;
using component_testing::RealmRoot;
GuestLaunchInfo guest_launch_info;
auto realm_builder = RealmBuilder::Create();
if (auto status = InstallInRealm(realm_builder, guest_launch_info); status != ZX_OK) {
return status;
}
realm_root_ = std::make_unique<RealmRoot>(realm_builder.Build(loop_.dispatcher()));
return LaunchInRealm(*realm_root_, guest_launch_info, deadline);
}
zx_status_t EnclosedGuest::InstallInRealm(component_testing::RealmBuilder& realm_builder,
GuestLaunchInfo& guest_launch_info) {
using component_testing::ChildRef;
using component_testing::Directory;
using component_testing::ParentRef;
using component_testing::Protocol;
using component_testing::Route;
constexpr auto kFakeNetstackComponentName = "fake_netstack";
constexpr auto kFakeScenicComponentName = "fake_scenic";
constexpr auto kDevGpuDirectory = "dev-gpu";
constexpr auto kGuestManagerName = "guest_manager";
zx_status_t status = LaunchInfo(&guest_launch_info);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failure launching guest image: ";
return status;
}
realm_builder.AddChild(kGuestManagerName, guest_launch_info.url);
realm_builder.AddLocalChild(kFakeNetstackComponentName, &fake_netstack_);
realm_builder.AddLocalChild(kFakeScenicComponentName, &fake_scenic_);
realm_builder
.AddRoute(Route{.capabilities =
{
Protocol{fuchsia::logger::LogSink::Name_},
Protocol{fuchsia::kernel::HypervisorResource::Name_},
Protocol{fuchsia::kernel::IrqResource::Name_},
Protocol{fuchsia::kernel::MmioResource::Name_},
Protocol{fuchsia::kernel::VmexResource::Name_},
Protocol{fuchsia::sysinfo::SysInfo::Name_},
Protocol{fuchsia::sysmem::Allocator::Name_},
Protocol{fuchsia::tracing::provider::Registry::Name_},
Protocol{fuchsia::scheduler::ProfileProvider::Name_},
Directory{.name = kDevGpuDirectory,
.rights = fuchsia::io::R_STAR_DIR,
.path = "/dev/class/gpu"},
},
.source = {ParentRef()},
.targets = {ChildRef{kGuestManagerName}}})
.AddRoute(Route{.capabilities =
{
Protocol{fuchsia::net::virtualization::Control::Name_},
},
.source = {ChildRef{kFakeNetstackComponentName}},
.targets = {ChildRef{kGuestManagerName}}})
.AddRoute(Route{.capabilities =
{
Protocol{fuchsia::ui::scenic::Scenic::Name_},
},
.source = {ChildRef{kFakeScenicComponentName}},
.targets = {ChildRef{kGuestManagerName}}})
.AddRoute(Route{.capabilities =
{
Protocol{guest_launch_info.interface_name},
},
.source = ChildRef{kGuestManagerName},
.targets = {ParentRef()}});
return ZX_OK;
}
zx_status_t EnclosedGuest::LaunchInRealm(const component_testing::RealmRoot& realm_root,
GuestLaunchInfo& guest_launch_info, zx::time deadline) {
Logger::Get().Reset();
PeriodicLogger logger;
fuchsia::virtualization::GuestManager_LaunchGuest_Result res;
guest_manager_ = realm_root.ConnectSync<fuchsia::virtualization::GuestManager>(
guest_launch_info.interface_name);
auto status =
guest_manager_->LaunchGuest(std::move(guest_launch_info.config), guest_.NewRequest(), &res);
if (status != ZX_OK) {
FX_PLOGS(ERROR, status) << "Failure launching guest " << guest_launch_info.url;
return status;
}
guest_cid_ = fuchsia::virtualization::DEFAULT_GUEST_CID;
status = SetupVsockServices(deadline);
if (status != ZX_OK) {
return status;
}
// Launch the guest.
logger.Start("Launching guest", zx::sec(5));
std::optional<zx_status_t> guest_error;
guest_.set_error_handler([&guest_error](zx_status_t status) { guest_error = status; });
// Connect to guest serial, and log it to the logger.
logger.Start("Connecting to guest serial", zx::sec(10));
std::optional<fuchsia::virtualization::Guest_GetSerial_Result> get_serial_result;
guest_->GetSerial([&get_serial_result](fuchsia::virtualization::Guest_GetSerial_Result result) {
get_serial_result = std::move(result);
});
bool success = RunLoopUntil(
GetLoop(),
[&guest_error, &get_serial_result] {
return guest_error.has_value() || get_serial_result.has_value();
},
deadline);
if (!success) {
FX_LOGS(ERROR) << "Timed out waiting to connect to guest's serial";
return ZX_ERR_TIMED_OUT;
}
if (guest_error.has_value()) {
FX_LOGS(ERROR) << "Error connecting to guest's serial: "
<< zx_status_get_string(guest_error.value());
return guest_error.value();
}
if (get_serial_result->is_err()) {
FX_PLOGS(ERROR, get_serial_result->err()) << "Failed to connect to guest's serial";
return get_serial_result->err();
}
serial_logger_.emplace(&Logger::Get(), std::move(get_serial_result->response().socket));
// Connect to guest console.
logger.Start("Connecting to guest console", zx::sec(10));
std::optional<fuchsia::virtualization::Guest_GetConsole_Result> get_console_result;
guest_->GetConsole(
[&get_console_result](fuchsia::virtualization::Guest_GetConsole_Result result) {
get_console_result = std::move(result);
});
success = RunLoopUntil(
GetLoop(),
[&guest_error, &get_console_result] {
return guest_error.has_value() || get_console_result.has_value();
},
deadline);
if (!success) {
FX_LOGS(ERROR) << "Timed out waiting to connect to guest's console";
return ZX_ERR_TIMED_OUT;
}
if (guest_error.has_value()) {
FX_LOGS(ERROR) << "Error connecting to guest's console: "
<< zx_status_get_string(guest_error.value());
return guest_error.value();
}
if (get_console_result->is_err()) {
FX_PLOGS(ERROR, get_console_result->err()) << "Failed to open guest console";
return get_console_result->err();
}
console_.emplace(std::make_unique<ZxSocket>(std::move(get_console_result->response().socket)));
// Wait for output to appear on the console.
logger.Start("Waiting for output to appear on guest console", zx::sec(10));
status = console_->Start(deadline);
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Error waiting for output on guest console: " << zx_status_get_string(status);
return status;
}
// Poll the system for all services to come up.
logger.Start("Waiting for system to become ready", zx::sec(10));
status = WaitForSystemReady(deadline);
if (status != ZX_OK) {
FX_LOGS(ERROR) << "Failure while waiting for guest system to become ready: "
<< zx_status_get_string(status);
return status;
}
return ZX_OK;
}
void EnclosedGuest::ConnectToBalloon(
::fidl::InterfaceRequest<::fuchsia::virtualization::BalloonController> controller) {
guest_manager_->ConnectToBalloon(std::move(controller));
}
void EnclosedGuest::GetHostVsockEndpoint(
::fidl::InterfaceRequest<::fuchsia::virtualization::HostVsockEndpoint> endpoint) {
guest_manager_->GetHostVsockEndpoint(std::move(endpoint));
}
zx_status_t EnclosedGuest::Stop(zx::time deadline) {
zx_status_t status = ShutdownAndWait(deadline);
if (status != ZX_OK) {
return status;
}
return ZX_OK;
}
zx_status_t EnclosedGuest::RunUtil(const std::string& util, const std::vector<std::string>& argv,
zx::time deadline, std::string* result) {
return Execute(GetTestUtilCommand(util, argv), {}, deadline, result);
}
zx_status_t ZirconEnclosedGuest::LaunchInfo(GuestLaunchInfo* launch_info) {
launch_info->url = kZirconGuestUrl;
launch_info->interface_name = fuchsia::virtualization::ZirconGuestManager::Name_;
// Disable netsvc to avoid spamming the net device with logs.
launch_info->config.mutable_cmdline_add()->emplace_back("netsvc.disable=true");
return ZX_OK;
}
namespace {
fitx::result<std::string> EnsureValidZirconPsOutput(std::string_view ps_output) {
if (ps_output.find("appmgr") == std::string::npos) {
return fitx::error("'appmgr' cannot be found in 'ps' output");
}
if (ps_output.find("virtual-console") == std::string::npos) {
return fitx::error("'virtual-console' cannot be found in 'ps' output");
}
return fitx::ok();
}
} // namespace
zx_status_t ZirconEnclosedGuest::WaitForSystemReady(zx::time deadline) {
std::string ps;
// Keep running `ps` until we get a reasonable result or run out of time.
do {
// Execute `ps`.
zx_status_t status = Execute({"ps"}, {}, deadline, &ps);
if (status != ZX_OK) {
return status;
}
if (EnsureValidZirconPsOutput(ps).is_ok()) {
return ZX_OK;
}
// Keep trying until we run out of time.
zx::nanosleep(std::min(zx::deadline_after(kRetryStep), deadline));
} while (zx::clock::get_monotonic() < deadline);
FX_LOGS(ERROR) << "Failed to wait for appmgr and virtual-console: "
<< EnsureValidZirconPsOutput(ps).error_value();
return ZX_ERR_TIMED_OUT;
}
zx_status_t ZirconEnclosedGuest::ShutdownAndWait(zx::time deadline) {
std::optional<GuestConsole>& console_opt = GetConsole();
if (console_opt.has_value()) {
GuestConsole& console = console_opt.value();
zx_status_t status = console.SendBlocking("dm shutdown\n", deadline);
if (status != ZX_OK) {
return status;
}
return console.WaitForSocketClosed(deadline);
}
return ZX_OK;
}
std::vector<std::string> ZirconEnclosedGuest::GetTestUtilCommand(
const std::string& util, const std::vector<std::string>& argv) {
std::vector<std::string> exec_argv = {util};
exec_argv.insert(exec_argv.end(), argv.begin(), argv.end());
return exec_argv;
}
zx_status_t DebianEnclosedGuest::LaunchInfo(GuestLaunchInfo* launch_info) {
launch_info->url = kDebianGuestUrl;
launch_info->interface_name = fuchsia::virtualization::DebianGuestManager::Name_;
// Enable kernel debugging serial output.
for (std::string_view cmd : kLinuxKernelSerialDebugCmdline) {
launch_info->config.mutable_cmdline_add()->emplace_back(cmd);
}
return ZX_OK;
}
zx_status_t DebianEnclosedGuest::WaitForSystemReady(zx::time deadline) {
std::optional<GuestConsole>& console_opt = GetConsole();
if (console_opt.has_value()) {
GuestConsole& console = console_opt.value();
constexpr zx::duration kEchoWaitTime = zx::sec(1);
return console.RepeatCommandTillSuccess("echo guest ready", ShellPrompt(), "guest ready",
deadline, kEchoWaitTime);
} else {
return ZX_ERR_BAD_STATE;
}
}
zx_status_t DebianEnclosedGuest::ShutdownAndWait(zx::time deadline) {
PeriodicLogger logger("Attempting to shut down guest", zx::sec(10));
std::optional<GuestConsole>& console_opt = GetConsole();
if (console_opt.has_value()) {
GuestConsole& console = console_opt.value();
zx_status_t status = console.SendBlocking("shutdown now\n", deadline);
if (status != ZX_OK) {
return status;
}
return console.WaitForSocketClosed(deadline);
}
return ZX_OK;
}
std::vector<std::string> DebianEnclosedGuest::GetTestUtilCommand(
const std::string& util, const std::vector<std::string>& argv) {
std::string bin_path = fxl::StringPrintf("%s/%s", kDebianTestUtilDir, util.c_str());
std::vector<std::string> exec_argv = {bin_path};
exec_argv.insert(exec_argv.end(), argv.begin(), argv.end());
return exec_argv;
}
zx_status_t TerminaEnclosedGuest::LaunchInfo(GuestLaunchInfo* launch_info) {
launch_info->url = kTerminaGuestUrl;
launch_info->interface_name = fuchsia::virtualization::TerminaGuestManager::Name_;
launch_info->config.set_virtio_gpu(false);
launch_info->config.set_magma_device(fuchsia::virtualization::MagmaDevice());
// Add the block device that contains the VM extras
{
fbl::unique_fd fd(open("/pkg/data/vm_extras.img", O_RDONLY));
if (!fd.is_valid()) {
return ZX_ERR_BAD_STATE;
}
zx::channel client;
zx_status_t status = fdio_get_service_handle(fd.get(), client.reset_and_get_address());
if (status != ZX_OK) {
return status;
}
launch_info->config.mutable_block_devices()->push_back({
"vm_extras",
fuchsia::virtualization::BlockMode::READ_ONLY,
fuchsia::virtualization::BlockFormat::FILE,
std::move(client),
});
}
// Add the block device that contains the test binaries.
{
fbl::unique_fd fd(open("/pkg/data/linux_tests.img", O_RDONLY));
if (!fd.is_valid()) {
return ZX_ERR_BAD_STATE;
}
zx::channel client;
zx_status_t status = fdio_get_service_handle(fd.get(), client.reset_and_get_address());
if (status != ZX_OK) {
return status;
}
launch_info->config.mutable_block_devices()->push_back({
"linux_tests",
fuchsia::virtualization::BlockMode::READ_ONLY,
fuchsia::virtualization::BlockFormat::FILE,
std::move(client),
});
}
{
// Add non-prebuilt test extras.
fbl::unique_fd fd(open("/pkg/data/extras.img", O_RDONLY));
if (!fd.is_valid()) {
return ZX_ERR_BAD_STATE;
}
zx::channel client;
zx_status_t status = fdio_get_service_handle(fd.get(), client.reset_and_get_address());
if (status != ZX_OK) {
return status;
}
launch_info->config.mutable_block_devices()->push_back({
"extras",
fuchsia::virtualization::BlockMode::READ_ONLY,
fuchsia::virtualization::BlockFormat::FILE,
std::move(client),
});
}
// Enable kernel debugging serial output.
for (std::string_view cmd : kLinuxKernelSerialDebugCmdline) {
launch_info->config.mutable_cmdline_add()->emplace_back(cmd);
}
return ZX_OK;
}
zx_status_t TerminaEnclosedGuest::SetupVsockServices(zx::time deadline) {
fuchsia::virtualization::HostVsockEndpointPtr grpc_endpoint;
GetHostVsockEndpoint(vsock_.NewRequest());
GetHostVsockEndpoint(grpc_endpoint.NewRequest());
GrpcVsockServerBuilder builder(std::move(grpc_endpoint));
builder.AddListenPort(kTerminaStartupListenerPort);
builder.RegisterService(this);
executor_.schedule_task(
builder.Build().and_then([this](std::unique_ptr<GrpcVsockServer>& result) mutable {
server_ = std::move(result);
return fpromise::ok();
}));
if (!RunLoopUntil(
GetLoop(), [this] { return server_ != nullptr; }, deadline)) {
return ZX_ERR_TIMED_OUT;
}
return ZX_OK;
}
grpc::Status TerminaEnclosedGuest::VmReady(grpc::ServerContext* context,
const vm_tools::EmptyMessage* request,
vm_tools::EmptyMessage* response) {
auto p = NewGrpcVsockStub<vm_tools::Maitred>(vsock_, kTerminaMaitredPort);
auto result = fpromise::run_single_threaded(std::move(p));
if (result.is_ok()) {
maitred_ = std::move(result.value());
} else {
FX_PLOGS(ERROR, result.error()) << "Failed to connect to maitred";
}
return grpc::Status::OK;
}
// Use Maitred to mount the given block device at the given location.
//
// The destination directory will be created if required.
zx_status_t MountDeviceInGuest(vm_tools::Maitred::Stub& maitred, std::string_view block_device,
std::string_view mount_point, std::string_view fs_type,
uint64_t mount_flags) {
grpc::ClientContext context;
vm_tools::MountRequest request;
vm_tools::MountResponse response;
request.mutable_source()->assign(block_device);
request.mutable_target()->assign(mount_point);
request.mutable_fstype()->assign(fs_type);
request.set_mountflags(mount_flags);
request.set_create_target(true);
auto grpc_status = maitred.Mount(&context, request, &response);
if (!grpc_status.ok()) {
FX_LOGS(ERROR) << "Request to mount block device '" << block_device
<< "' failed: " << grpc_status.error_message();
return ZX_ERR_IO;
}
if (response.error() != 0) {
FX_LOGS(ERROR) << "Mounting block device '" << block_device << "' failed: " << response.error();
return ZX_ERR_IO;
}
return ZX_OK;
}
zx_status_t TerminaEnclosedGuest::WaitForSystemReady(zx::time deadline) {
// The VM will connect to the StartupListener port when it's ready and we'll
// create the maitred stub in |VmReady|.
{
PeriodicLogger logger("Wait for maitred", zx::sec(1));
if (!RunLoopUntil(
GetLoop(), [this] { return maitred_ != nullptr; }, deadline)) {
return ZX_ERR_TIMED_OUT;
}
}
FX_CHECK(maitred_) << "No maitred connection";
// Connect to vshd.
fuchsia::virtualization::HostVsockEndpointPtr endpoint;
GetHostVsockEndpoint(endpoint.NewRequest());
command_runner_ = std::make_unique<vsh::BlockingCommandRunner>(std::move(endpoint));
// Create mountpoints for test utils and extras. The root filesystem is read only so we
// put these under /tmp.
zx_status_t status;
status = MountDeviceInGuest(*maitred_, "/dev/vdc", "/tmp/vm_extras", "ext2", MS_RDONLY);
if (status != ZX_OK) {
return status;
}
status = MountDeviceInGuest(*maitred_, "/dev/vdd", "/tmp/test_utils", "romfs", MS_RDONLY);
if (status != ZX_OK) {
return status;
}
status = MountDeviceInGuest(*maitred_, "/dev/vde", "/tmp/extras", "romfs", MS_RDONLY);
if (status != ZX_OK) {
return status;
}
return ZX_OK;
}
zx_status_t TerminaEnclosedGuest::ShutdownAndWait(zx::time deadline) {
if (server_) {
server_->inner()->Shutdown();
server_->inner()->Wait();
}
return ZX_OK;
}
zx_status_t TerminaEnclosedGuest::Execute(const std::vector<std::string>& command,
const std::unordered_map<std::string, std::string>& env,
zx::time deadline, std::string* result,
int32_t* return_code) {
std::vector<std::string> argv = {"sh", "-c", JoinArgVector(command)};
auto command_result = command_runner_->Execute({argv, env});
if (command_result.is_error()) {
return command_result.error();
}
if (result) {
*result = std::move(command_result.value().out);
if (!command_result.value().err.empty()) {
*result += "\n";
*result += command_result.value().err;
}
}
if (return_code) {
*return_code = command_result.value().return_code;
}
return ZX_OK;
}
std::vector<std::string> TerminaEnclosedGuest::GetTestUtilCommand(
const std::string& util, const std::vector<std::string>& argv) {
std::vector<std::string> final_argv;
final_argv.emplace_back("/tmp/test_utils/" + util);
final_argv.insert(final_argv.end(), argv.begin(), argv.end());
return final_argv;
}