| // Copyright 2017 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "vm_tools/concierge/termina_vm.h" |
| |
| #include <arpa/inet.h> |
| #include <linux/capability.h> |
| #include <signal.h> |
| #include <sys/mount.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| |
| #include <memory> |
| #include <optional> |
| #include <utility> |
| #include <vector> |
| |
| #include <base/check.h> |
| #include <base/containers/span.h> |
| #include <base/files/file.h> |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/functional/bind.h> |
| #include <base/functional/callback_forward.h> |
| #include <base/functional/callback_helpers.h> |
| #include <base/logging.h> |
| #include <base/memory/ptr_util.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| #include <base/system/sys_info.h> |
| #include <base/task/sequenced_task_runner.h> |
| #include <base/time/time.h> |
| #include <google/protobuf/repeated_field.h> |
| #include <chromeos/constants/vm_tools.h> |
| #include <grpcpp/grpcpp.h> |
| #include <net-base/ipv4_address.h> |
| #include <sys/epoll.h> |
| #include <vm_concierge/concierge_service.pb.h> |
| #include <vm_protos/proto_bindings/vm_guest.grpc.pb.h> |
| |
| #include "vm_tools/common/vm_id.h" |
| #include "vm_tools/concierge/network/guest_os_network.h" |
| #include "vm_tools/concierge/tap_device_builder.h" |
| #include "vm_tools/concierge/tracing.h" |
| #include "vm_tools/concierge/vm_base_impl.h" |
| #include "vm_tools/concierge/vm_builder.h" |
| #include "vm_tools/concierge/vm_permission_interface.h" |
| #include "vm_tools/concierge/vm_util.h" |
| #include "vm_tools/concierge/vm_wl_interface.h" |
| |
| namespace vm_tools::concierge { |
| namespace { |
| |
| // Features to enable. |
| constexpr StartTerminaRequest_Feature kEnabledTerminaFeatures[] = {}; |
| |
| // How long to wait before timing out on shutdown RPCs. |
| constexpr int64_t kShutdownTimeoutSeconds = 30; |
| |
| // How long to wait before timing out on StartTermina RPCs. |
| constexpr int64_t kStartTerminaTimeoutSeconds = 150; |
| |
| // How long to wait before timing out on regular RPCs. |
| constexpr int64_t kDefaultTimeoutSeconds = 10; |
| |
| // How long to wait before timing out on child process exits. |
| constexpr base::TimeDelta kChildExitTimeout = base::Seconds(10); |
| |
| // The maximum GPU shader cache disk usage, interpreted by Mesa. For details |
| // see MESA_GLSL_CACHE_MAX_SIZE at https://docs.mesa3d.org/envvars.html. |
| constexpr char kGpuCacheSizeString[] = "50M"; |
| constexpr char kRenderServerCacheSizeString[] = "50M"; |
| |
| // The maximum render server shader cache disk usage for borealis. |
| // TODO(b/169802596): Set cache size in a smarter way. |
| // See b/209849605#comment5 for borealis cache size reasoning. |
| constexpr char kGpuCacheSizeStringBorealis[] = "1000M"; |
| constexpr char kRenderServerCacheSizeStringBorealis[] = "1000M"; |
| |
| // Special value to represent an invalid disk index for `crosvm disk` |
| // operations. |
| constexpr int kInvalidDiskIndex = -1; |
| |
| // Helper function to convert spaced enum to vm_tools equivalent. |
| vm_tools::StatefulDiskSpaceState MapSpacedStateToGuestState( |
| spaced::StatefulDiskSpaceState state) { |
| switch (state) { |
| case spaced::StatefulDiskSpaceState::NORMAL: |
| return vm_tools::StatefulDiskSpaceState::DISK_NORMAL; |
| break; |
| case spaced::StatefulDiskSpaceState::LOW: |
| return vm_tools::StatefulDiskSpaceState::DISK_LOW; |
| break; |
| case spaced::StatefulDiskSpaceState::CRITICAL: |
| return vm_tools::StatefulDiskSpaceState::DISK_CRITICAL; |
| break; |
| case spaced::StatefulDiskSpaceState::NONE: |
| default: |
| return vm_tools::StatefulDiskSpaceState::DISK_NONE; |
| } |
| } |
| |
| } // namespace |
| |
| void TerminaVm::MaitredDeleter::operator()( |
| brillo::AsyncGrpcClient<vm_tools::Maitred>* maitred) const { |
| // It is unsafe to delete the handle until shutdown has completed, so |
| // instead of blocking the destructor, we move ownership to a callback |
| // which deletes the handle. |
| maitred->ShutDown(base::BindOnce( |
| [](brillo::AsyncGrpcClient<vm_tools::Maitred>* maitred) { |
| // Deleting in a callback like this looks dangerous, but it's |
| // (currently) safe for the same reason that *not* deleting in a |
| // callback (currently) deadlocks, i.e. the callback is posted to |
| // the same sequence that called ShutDown(). |
| delete maitred; |
| }, |
| maitred)); |
| } |
| |
| TerminaVm::TerminaVm(Config config) |
| : VmBaseImpl(VmBaseImpl::Config{ |
| .vsock_cid = config.vsock_cid, |
| .network = std::move(config.network), |
| .seneschal_server_proxy = std::move(config.seneschal_server_proxy), |
| .cros_vm_socket = config.cros_vm_socket, |
| .runtime_dir = std::move(config.runtime_dir), |
| }), |
| features_(config.features), |
| stateful_device_(config.stateful_device), |
| stateful_size_(config.stateful_size), |
| log_path_(std::move(config.log_path)), |
| id_(config.id), |
| bus_(config.bus), |
| vm_permission_service_proxy_(config.vm_permission_service_proxy), |
| classification_(config.classification), |
| storage_ballooning_(config.storage_ballooning), |
| socket_(std::move(config.socket)) {} |
| |
| TerminaVm::~TerminaVm() { |
| Shutdown(); |
| } |
| |
| std::unique_ptr<TerminaVm> TerminaVm::Create(Config config) { |
| auto vm_builder = std::move(config.vm_builder); |
| |
| auto vm = base::WrapUnique(new TerminaVm(std::move(config))); |
| |
| if (!vm->Start(std::move(vm_builder))) { |
| return {}; |
| } |
| |
| return vm; |
| } |
| |
| std::string TerminaVm::GetCrosVmSerial(std::string hardware, |
| std::string console_type) const { |
| std::string common_params = "hardware=" + hardware; |
| if (console_type != "") { |
| common_params += "," + console_type + "=true"; |
| } |
| if (hardware != "debugcon") { |
| common_params += ",num=1"; |
| } |
| if (log_path_.empty()) { |
| return common_params + ",type=syslog"; |
| } |
| return common_params + ",type=unix,path=" + log_path_.value(); |
| } |
| |
| bool TerminaVm::Start(VmBuilder vm_builder) { |
| // Sommelier relies on implicit modifier, which does not pass host modifier to |
| // zwp_linux_buffer_params_v1_add. Graphics will be broken if modifiers are |
| // enabled. Sommelier shall be fixed to mirror what arc wayland_service does, |
| // and then we can re-enable UBWC here. |
| // |
| // See b/229147702 |
| setenv("MINIGBM_DEBUG", "nocompression", 0); |
| |
| // TODO(b/193370101) Remove borealis specific code once crostini uses |
| // permission service. |
| if (classification_ == apps::VmType::BOREALIS) { |
| // Register the VM with permission service and obtain permission |
| // token. |
| if (!vm_permission::RegisterVm(bus_, vm_permission_service_proxy_, id_, |
| vm_permission::VmType::BOREALIS, |
| &permission_token_)) { |
| LOG(ERROR) << "Failed to register with permission service"; |
| // TODO(lqu): Add "return false;" after chrome uprevs. |
| } |
| } |
| |
| if (classification_ == apps::BOREALIS) { |
| vm_builder.EnableWorkingSetReporting(true); |
| |
| // Disable split lock detection in the guest kernel. |
| // |
| // Split lock detection has the potential to negatively impact performance. |
| // Typically, this setting only makes sense on the host kernel. |
| // However, some x86 architectures have a way to send |
| // this notification to user space applications (vCPU's). To ensure we |
| // don't see any issues on these architectures, we disable split lock |
| // detection completely in Borealis. |
| // |
| // Other guests in the system may want to preserve this behavior as it |
| // can be useful for application development/debugging. |
| vm_builder.AppendKernelParam("split_lock_detect=off"); |
| } |
| |
| // Open the tap device. |
| base::ScopedFD tap_fd = OpenTapDevice( |
| Network()->TapDevice(), true /*vnet_hdr*/, nullptr /*ifname_out*/); |
| if (!tap_fd.is_valid()) { |
| LOG(ERROR) << "Unable to open and configure TAP device " |
| << Network()->TapDevice(); |
| return false; |
| } |
| |
| vm_builder.AppendTapFd(std::move(tap_fd)) |
| .SetVsockCid(vsock_cid_) |
| .SetSocketPath(GetVmSocketPath()) |
| .SetMemory(std::to_string(GetVmMemoryMiB())) |
| .AppendSerialDevice(GetCrosVmSerial("serial", "earlycon")) |
| .AppendSerialDevice(GetCrosVmSerial("virtio-console", "console")) |
| .AppendSerialDevice(GetCrosVmSerial("debugcon", "")) |
| .EnableBattery(true) |
| .SetSyslogTag(base::StringPrintf("VM(%u)", vsock_cid_)); |
| |
| if (features_.gpu) { |
| vm_builder.EnableGpu(true) |
| .EnableVulkan(USE_CROSVM_VULKAN) |
| .EnableBigGl(features_.big_gl) |
| .EnableVirtgpuNativeContext(features_.virtgpu_native_context); |
| |
| if (classification_ == apps::VmType::BOREALIS) { |
| vm_builder.SetGpuCacheSize(kGpuCacheSizeStringBorealis); |
| // For Borealis, place the render server process in |
| // the GPU server cpuset cgroup. |
| vm_builder.AppendCustomParam("--gpu-server-cgroup-path", |
| kBorealisGpuServerCpusetCgroup); |
| } else { |
| vm_builder.SetGpuCacheSize(kGpuCacheSizeString); |
| } |
| |
| if (features_.render_server) { |
| vm_builder.EnableRenderServer(true); |
| if (classification_ == apps::VmType::BOREALIS) { |
| vm_builder.SetRenderServerCacheSize( |
| kRenderServerCacheSizeStringBorealis); |
| } else { |
| vm_builder.SetRenderServerCacheSize(kRenderServerCacheSizeString); |
| } |
| } |
| } |
| |
| // Enable dGPU passthrough argument is only supported on Borealis VM. |
| if (features_.dgpu_passthrough) { |
| if (classification_ == apps::VmType::BOREALIS) { |
| vm_builder.EnableDGpuPassthrough(true); |
| } else { |
| LOG(ERROR) << "--enable-dgpu-passthrough is only supported on Borealis."; |
| return false; |
| } |
| } |
| |
| if (features_.vtpm_proxy) |
| vm_builder.EnableVtpmProxy(true /* enable */); |
| |
| // TODO(b/193370101) Remove borealis specific code once crostini uses |
| // permission service. |
| if (classification_ == apps::VmType::BOREALIS) { |
| if (vm_permission::IsMicrophoneEnabled(bus_, vm_permission_service_proxy_, |
| permission_token_)) { |
| vm_builder.AppendAudioDevice( |
| "capture=true,backend=cras,client_type=borealis," |
| "socket_type=unified,num_output_devices=3,num_input_devices=3," |
| "num_output_streams=10,num_input_streams=5"); |
| } else { |
| vm_builder.AppendAudioDevice( |
| "backend=cras,client_type=borealis,socket_type=unified," |
| "num_output_devices=3,num_input_devices=3," |
| "num_output_streams=10,num_input_streams=5"); |
| } |
| } else { |
| if (features_.audio_capture) { |
| vm_builder.AppendAudioDevice( |
| "capture=true,backend=cras,socket_type=unified"); |
| } else { |
| vm_builder.AppendAudioDevice("backend=cras,socket_type=unified"); |
| } |
| } |
| |
| for (const std::string& p : features_.kernel_params) |
| vm_builder.AppendKernelParam(p); |
| |
| for (const std::string& s : features_.oem_strings) |
| vm_builder.AppendOemString(s); |
| |
| // Switch off kmsg throttling so we can log all relevant startup messages |
| vm_builder.AppendKernelParam("printk.devkmsg=on"); |
| |
| // Change the process group before exec so that crosvm sending SIGKILL to the |
| // whole process group doesn't kill us as well. The function also changes the |
| // cpu cgroup for Termina crosvm processes. |
| process_.SetPreExecCallback(base::BindOnce( |
| &SetUpCrosvmProcess, base::FilePath(kTerminaCpuCgroup).Append("tasks"))); |
| |
| std::unique_ptr<CustomParametersForDev> custom_parameters = |
| MaybeLoadCustomParametersForDev(classification_); |
| |
| std::optional<base::StringPairs> args = |
| std::move(vm_builder).BuildVmArgs(custom_parameters.get()); |
| if (!args) { |
| LOG(ERROR) << "Failed to build VM arguments"; |
| return false; |
| } |
| |
| if (!StartProcess(std::move(args).value())) { |
| LOG(ERROR) << "Failed to start VM process"; |
| return false; |
| } |
| |
| // Create a stub for talking to the maitre'd instance inside the VM. |
| InitializeMaitredService( |
| std::make_unique<vm_tools::Maitred::Stub>(grpc::CreateChannel( |
| base::StringPrintf("vsock:%u:%u", vsock_cid_, vm_tools::kMaitredPort), |
| grpc::InsecureChannelCredentials()))); |
| |
| return true; |
| } |
| |
| bool TerminaVm::SetTimezone(const std::string& timezone, |
| std::string* out_error) { |
| if (!stub_) { |
| *out_error = "maitred stub not initialized"; |
| return false; |
| } |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| ::vm_tools::SetTimezoneRequest request; |
| request.set_timezone_name(timezone); |
| // Borealis needs timezone info to be bind-mounted due to Steam bug, see |
| // TODO(b/237960004): Clean up this exception once Steam bug is fixed. |
| request.set_use_bind_mount(classification_ == apps::VmType::BOREALIS); |
| ::vm_tools::EmptyMessage response; |
| |
| auto result = stub_->SetTimezone(&ctx, request, &response); |
| if (result.ok()) { |
| *out_error = ""; |
| return true; |
| } |
| |
| *out_error = result.error_message(); |
| return false; |
| } |
| |
| grpc::Status TerminaVm::SendVMShutdownMessage() { |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kShutdownTimeoutSeconds, GPR_TIMESPAN))); |
| |
| vm_tools::EmptyMessage empty; |
| return stub_->Shutdown(&ctx, empty, &empty); |
| } |
| |
| bool TerminaVm::Shutdown() { |
| // Notify permission service of VM destruction. |
| if (!permission_token_.empty()) { |
| vm_permission::UnregisterVm(bus_, vm_permission_service_proxy_, id_); |
| } |
| |
| // Do a check here to make sure the process is still around. It may have |
| // crashed and we don't want to be waiting around for an RPC response that's |
| // never going to come. kill with a signal value of 0 is explicitly |
| // documented as a way to check for the existence of a process. |
| if (!CheckProcessExists(process_.pid())) { |
| // The process is already gone. |
| process_.Release(); |
| return true; |
| } |
| |
| grpc::Status status = SendVMShutdownMessage(); |
| // brillo::ProcessImpl doesn't provide a timed wait function and while the |
| // Shutdown RPC may have been successful we can't really trust crosvm to |
| // actually exit. This may result in an untimed wait() blocking indefinitely. |
| // Instead, do a timed wait here and only return success if the process |
| // _actually_ exited as reported by the kernel, which is really the only |
| // thing we can trust here. |
| if (status.ok() && WaitForChild(process_.pid(), kChildExitTimeout)) { |
| process_.Release(); |
| return true; |
| } |
| |
| LOG(WARNING) << "Shutdown RPC failed for VM " << vsock_cid_ << " with error " |
| << "code " << status.error_code() << ": " |
| << status.error_message(); |
| |
| // Try to shut it down via the crosvm socket. |
| Stop(); |
| |
| // We can't actually trust the exit codes that crosvm gives us so just see if |
| // it exited. |
| if (WaitForChild(process_.pid(), kChildExitTimeout)) { |
| process_.Release(); |
| return true; |
| } |
| |
| LOG(WARNING) << "Failed to stop VM " << vsock_cid_ << " via crosvm socket"; |
| |
| // Kill the process with SIGTERM. |
| if (process_.Kill(SIGTERM, kChildExitTimeout.InSeconds())) { |
| return true; |
| } |
| |
| LOG(WARNING) << "Failed to kill VM " << vsock_cid_ << " with SIGTERM"; |
| |
| // Kill it with fire. |
| if (process_.Kill(SIGKILL, kChildExitTimeout.InSeconds())) { |
| return true; |
| } |
| |
| LOG(ERROR) << "Failed to kill VM " << vsock_cid_ << " with SIGKILL"; |
| return false; |
| } |
| |
| bool TerminaVm::ConfigureNetwork( |
| const std::vector<std::string>& nameservers, |
| const std::vector<std::string>& search_domains) { |
| LOG(INFO) << "Configuring network for VM " << vsock_cid_; |
| |
| vm_tools::NetworkConfigRequest request; |
| vm_tools::EmptyMessage response; |
| |
| vm_tools::IPv4Config* config = request.mutable_ipv4_config(); |
| config->set_address(IPv4Address().ToInAddr().s_addr); |
| config->set_gateway(GatewayAddress().ToInAddr().s_addr); |
| config->set_netmask(Netmask().ToInAddr().s_addr); |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = stub_->ConfigureNetwork(&ctx, request, &response); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to configure network for VM " << vsock_cid_ << ": " |
| << status.error_message(); |
| return false; |
| } |
| |
| return SetResolvConfig(nameservers, search_domains); |
| } |
| |
| bool TerminaVm::ConfigureContainerGuest(const std::string& vm_token, |
| const std::string& vm_username, |
| std::string* out_error) { |
| LOG(INFO) << "Configuring container guest for for VM " << vsock_cid_; |
| |
| vm_tools::ConfigureContainerGuestRequest request; |
| vm_tools::EmptyMessage response; |
| |
| request.set_container_token(vm_token); |
| request.set_vm_username(vm_username); |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = |
| stub_->ConfigureContainerGuest(&ctx, request, &response); |
| if (!status.ok()) { |
| *out_error = status.error_message(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool TerminaVm::Mount(std::string source, |
| std::string target, |
| std::string fstype, |
| uint64_t mountflags, |
| std::string options) { |
| LOG(INFO) << "Mounting " << source << " on " << target << " inside VM " |
| << vsock_cid_; |
| |
| vm_tools::MountRequest request; |
| vm_tools::MountResponse response; |
| |
| request.mutable_source()->swap(source); |
| request.mutable_target()->swap(target); |
| request.mutable_fstype()->swap(fstype); |
| request.set_mountflags(mountflags); |
| request.mutable_options()->swap(options); |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = stub_->Mount(&ctx, request, &response); |
| if (!status.ok() || response.error() != 0) { |
| LOG(ERROR) << "Failed to mount " << request.source() << " on " |
| << request.target() << " inside VM " << vsock_cid_ << ": " |
| << (status.ok() ? strerror(response.error()) |
| : status.error_message()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool TerminaVm::StartTermina( |
| std::string lxd_subnet, |
| const google::protobuf::RepeatedField<int>& features, |
| std::string* out_error, |
| vm_tools::StartTerminaResponse* response) { |
| DCHECK(out_error); |
| DCHECK(response); |
| |
| // We record the kernel version early to ensure that no container has |
| // been started and the VM can still be trusted. |
| RecordKernelVersionForEnterpriseReporting(); |
| |
| vm_tools::StartTerminaRequest request; |
| |
| request.set_tremplin_ipv4_address(GatewayAddress().ToInAddr().s_addr); |
| request.mutable_lxd_ipv4_subnet()->swap(lxd_subnet); |
| request.set_stateful_device(StatefulDevice()); |
| request.set_allow_privileged_containers(true); |
| for (const auto feature : kEnabledTerminaFeatures) { |
| request.add_feature(feature); |
| } |
| request.mutable_feature()->MergeFrom(features); |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kStartTerminaTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = stub_->StartTermina(&ctx, request, response); |
| |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to start Termina: " << status.error_message(); |
| out_error->assign(status.error_message()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void TerminaVm::RecordKernelVersionForEnterpriseReporting() { |
| grpc::ClientContext ctx_get_kernel_version; |
| ctx_get_kernel_version.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kStartTerminaTimeoutSeconds, GPR_TIMESPAN))); |
| vm_tools::EmptyMessage empty; |
| vm_tools::GetKernelVersionResponse grpc_response; |
| grpc::Status get_kernel_version_status = |
| stub_->GetKernelVersion(&ctx_get_kernel_version, empty, &grpc_response); |
| if (!get_kernel_version_status.ok()) { |
| LOG(WARNING) << "Failed to retrieve kernel version for VM " << vsock_cid_ |
| << ": " << get_kernel_version_status.error_message(); |
| } else { |
| kernel_version_ = |
| grpc_response.kernel_release() + " " + grpc_response.kernel_version(); |
| } |
| } |
| |
| void TerminaVm::HandleSuspendImminent() { |
| LOG(INFO) << "Preparing to suspend"; |
| |
| vm_tools::EmptyMessage request; |
| vm_tools::EmptyMessage response; |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = stub_->PrepareToSuspend(&ctx, request, &response); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to prepare for suspending" << status.error_message(); |
| } |
| |
| SuspendCrosvm(); |
| } |
| |
| void TerminaVm::HandleSuspendDone() { |
| ResumeCrosvm(); |
| } |
| |
| bool TerminaVm::Mount9P(uint32_t port, std::string target) { |
| LOG(INFO) << "Mounting 9P file system from port " << port << " on " << target; |
| |
| vm_tools::Mount9PRequest request; |
| vm_tools::MountResponse response; |
| |
| request.set_port(port); |
| request.set_target(std::move(target)); |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = stub_->Mount9P(&ctx, request, &response); |
| if (!status.ok() || response.error() != 0) { |
| LOG(ERROR) << "Failed to mount 9P server on " << request.target() |
| << " inside VM " << vsock_cid_ << ": " |
| << (status.ok() ? strerror(response.error()) |
| : status.error_message()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool TerminaVm::MountExternalDisk(std::string source, std::string target_dir) { |
| const std::string target = "/mnt/external/" + target_dir; |
| |
| LOG(INFO) << "Mounting an external disk on " << target; |
| |
| vm_tools::MountRequest request; |
| vm_tools::MountResponse response; |
| |
| request.set_source(std::move(source)); |
| request.set_target(std::move(target)); |
| request.set_fstype("btrfs"); |
| request.set_options(""); |
| request.set_create_target(true); |
| request.set_permissions(0777); |
| request.set_mkfs_if_needed(true); |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = stub_->Mount(&ctx, request, &response); |
| if (!status.ok() || response.error() != 0) { |
| LOG(ERROR) << "Failed to mount an external disk " << request.source() |
| << " on " << request.target() << " inside VM " << vsock_cid_ |
| << ": " |
| << (status.ok() ? strerror(response.error()) |
| : status.error_message()); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool TerminaVm::SetResolvConfig( |
| const std::vector<std::string>& nameservers, |
| const std::vector<std::string>& search_domains) { |
| VMT_TRACE(kCategory, "TerminaVm::SetResolvConfig"); |
| LOG(INFO) << "Setting resolv config for VM " << vsock_cid_; |
| |
| vm_tools::SetResolvConfigRequest request; |
| vm_tools::EmptyMessage response; |
| |
| vm_tools::ResolvConfig* resolv_config = request.mutable_resolv_config(); |
| |
| google::protobuf::RepeatedPtrField<std::string> request_nameservers( |
| nameservers.begin(), nameservers.end()); |
| resolv_config->mutable_nameservers()->Swap(&request_nameservers); |
| |
| google::protobuf::RepeatedPtrField<std::string> request_search_domains( |
| search_domains.begin(), search_domains.end()); |
| resolv_config->mutable_search_domains()->Swap(&request_search_domains); |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = stub_->SetResolvConfig(&ctx, request, &response); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to set resolv config for VM " << vsock_cid_ << ": " |
| << status.error_message(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void TerminaVm::HostNetworkChanged() { |
| LOG(INFO) << "Sending OnHostNetworkChanged for VM " << vsock_cid_; |
| |
| vm_tools::EmptyMessage request; |
| vm_tools::EmptyMessage response; |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = stub_->OnHostNetworkChanged(&ctx, request, &response); |
| if (!status.ok()) { |
| LOG(WARNING) << "Failed to send OnHostNetworkChanged for VM " << vsock_cid_ |
| << ": " << status.error_message(); |
| } |
| } |
| |
| bool TerminaVm::SetTime(std::string* failure_reason) { |
| VMT_TRACE(kCategory, "TerminaVm::SetTime"); |
| DCHECK(failure_reason); |
| |
| base::Time now = base::Time::Now(); |
| struct timeval current = now.ToTimeVal(); |
| |
| vm_tools::SetTimeRequest request; |
| vm_tools::EmptyMessage response; |
| |
| google::protobuf::Timestamp* timestamp = request.mutable_time(); |
| timestamp->set_seconds(current.tv_sec); |
| timestamp->set_nanos(current.tv_usec * 1000); |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| grpc::Status status = stub_->SetTime(&ctx, request, &response); |
| if (!status.ok()) { |
| LOG(ERROR) << "Failed to set guest time on VM " << vsock_cid_ << ":" |
| << status.error_message(); |
| |
| *failure_reason = status.error_message(); |
| return false; |
| } |
| return true; |
| } |
| |
| bool TerminaVm::GetVmEnterpriseReportingInfo( |
| GetVmEnterpriseReportingInfoResponse* response) { |
| LOG(INFO) << "Get enterprise reporting info"; |
| if (kernel_version_.empty()) { |
| response->set_success(false); |
| response->set_failure_reason( |
| "Kernel version could not be recorded at startup."); |
| return false; |
| } |
| |
| response->set_success(true); |
| response->set_vm_kernel_version(kernel_version_); |
| return true; |
| } |
| |
| // static |
| bool TerminaVm::SetVmCpuRestriction(CpuRestrictionState cpu_restriction_state) { |
| return VmBaseImpl::SetVmCpuRestriction(cpu_restriction_state, |
| kTerminaCpuCgroup) && |
| VmBaseImpl::SetVmCpuRestriction(cpu_restriction_state, |
| kTerminaVcpuCpuCgroup); |
| } |
| |
| // Extract the disk index of a virtio-blk device name. |
| // |name| should match "/dev/vdX", where X is in the range 'a' to 'z'. |
| // Returns the zero-based index of the disk (e.g. 'a' = 0, 'b' = 1, etc.). |
| static int DiskIndexFromName(const std::string& name) { |
| // TODO(dverkamp): handle more than 26 disks? (e.g. /dev/vdaa) |
| if (name.length() != 8) { |
| return kInvalidDiskIndex; |
| } |
| |
| int disk_letter = name[7]; |
| if (disk_letter < 'a' || disk_letter > 'z') { |
| return kInvalidDiskIndex; |
| } |
| |
| return disk_letter - 'a'; |
| } |
| |
| bool TerminaVm::ResizeDiskImage(uint64_t new_size) { |
| auto disk_index = DiskIndexFromName(stateful_device_); |
| if (disk_index == kInvalidDiskIndex) { |
| LOG(ERROR) << "Could not determine disk index from stateful device name " |
| << stateful_device_; |
| return false; |
| } |
| return CrosvmDiskResize(GetVmSocketPath(), disk_index, new_size); |
| } |
| |
| bool TerminaVm::ResizeFilesystem(uint64_t new_size) { |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| vm_tools::ResizeFilesystemRequest request; |
| vm_tools::ResizeFilesystemResponse response; |
| request.set_size(new_size); |
| grpc::Status status = stub_->ResizeFilesystem(&ctx, request, &response); |
| if (status.ok()) |
| return true; |
| LOG(ERROR) << "Resize filesystem failed (" << status.error_code() |
| << "): " << status.error_message(); |
| return false; |
| } |
| |
| vm_tools::concierge::DiskImageStatus TerminaVm::ResizeDisk( |
| uint64_t new_size, std::string* failure_reason) { |
| if (stateful_resize_type_ != DiskResizeType::NONE) { |
| LOG(ERROR) << "Attempted resize while resize is already in progress"; |
| *failure_reason = "Resize already in progress"; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_FAILED; |
| return last_stateful_resize_status_; |
| } |
| |
| LOG(INFO) << "TerminaVm resize request: current size = " << stateful_size_ |
| << " new size = " << new_size; |
| |
| if (new_size == stateful_size_) { |
| LOG(INFO) << "Disk is already requested size"; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_RESIZED; |
| return last_stateful_resize_status_; |
| } |
| |
| stateful_target_size_ = new_size; |
| |
| if (new_size > stateful_size_) { |
| LOG(INFO) << "Expanding disk"; |
| // Expand disk image first, then expand filesystem. |
| if (!ResizeDiskImage(new_size)) { |
| LOG(ERROR) << "ResizeDiskImage failed"; |
| *failure_reason = "ResizeDiskImage failed"; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_FAILED; |
| return last_stateful_resize_status_; |
| } |
| |
| if (!ResizeFilesystem(new_size)) { |
| LOG(ERROR) << "ResizeFilesystem failed"; |
| *failure_reason = "ResizeFilesystem failed"; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_FAILED; |
| return last_stateful_resize_status_; |
| } |
| |
| LOG(INFO) << "ResizeFilesystem in progress"; |
| stateful_resize_type_ = DiskResizeType::EXPAND; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_IN_PROGRESS; |
| return last_stateful_resize_status_; |
| } else { |
| DCHECK(new_size < stateful_size_); |
| |
| LOG(INFO) << "Shrinking disk"; |
| |
| // Shrink filesystem first, then shrink disk image. |
| if (!ResizeFilesystem(new_size)) { |
| LOG(ERROR) << "ResizeFilesystem failed"; |
| *failure_reason = "ResizeFilesystem failed"; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_FAILED; |
| return last_stateful_resize_status_; |
| } |
| |
| LOG(INFO) << "ResizeFilesystem in progress"; |
| stateful_resize_type_ = DiskResizeType::SHRINK; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_IN_PROGRESS; |
| return last_stateful_resize_status_; |
| } |
| } |
| |
| vm_tools::concierge::DiskImageStatus TerminaVm::GetDiskResizeStatus( |
| std::string* failure_reason) { |
| if (stateful_resize_type_ == DiskResizeType::NONE) { |
| return last_stateful_resize_status_; |
| } |
| |
| // If a resize is in progress, then we must be waiting on filesystem resize to |
| // complete. Check its status and update our state to match. |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| vm_tools::EmptyMessage request; |
| vm_tools::GetResizeStatusResponse response; |
| |
| grpc::Status status = stub_->GetResizeStatus(&ctx, request, &response); |
| |
| if (!status.ok()) { |
| stateful_resize_type_ = DiskResizeType::NONE; |
| LOG(ERROR) << "GetResizeStatus RPC failed"; |
| *failure_reason = "GetResizeStatus RPC failed"; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_FAILED; |
| return last_stateful_resize_status_; |
| } |
| |
| if (response.resize_in_progress()) { |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_IN_PROGRESS; |
| return last_stateful_resize_status_; |
| } |
| |
| if (response.current_size() != stateful_target_size_) { |
| stateful_resize_type_ = DiskResizeType::NONE; |
| LOG(ERROR) << "Unexpected size after filesystem resize: got " |
| << response.current_size() << ", expected " |
| << stateful_target_size_; |
| *failure_reason = "Unexpected size after filesystem resize"; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_FAILED; |
| return last_stateful_resize_status_; |
| } |
| |
| stateful_size_ = response.current_size(); |
| |
| if (stateful_resize_type_ == DiskResizeType::SHRINK) { |
| LOG(INFO) << "Filesystem shrink complete; shrinking disk image"; |
| if (!ResizeDiskImage(response.current_size())) { |
| LOG(ERROR) << "ResizeDiskImage failed"; |
| *failure_reason = "ResizeDiskImage failed"; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_FAILED; |
| return last_stateful_resize_status_; |
| } |
| } else { |
| LOG(INFO) << "Filesystem expansion complete"; |
| } |
| |
| LOG(INFO) << "Disk resize successful"; |
| stateful_resize_type_ = DiskResizeType::NONE; |
| last_stateful_resize_status_ = DiskImageStatus::DISK_STATUS_RESIZED; |
| return last_stateful_resize_status_; |
| } |
| |
| uint64_t TerminaVm::GetMinDiskSize() { |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| vm_tools::EmptyMessage request; |
| vm_tools::GetResizeBoundsResponse response; |
| |
| grpc::Status status = stub_->GetResizeBounds(&ctx, request, &response); |
| |
| if (!status.ok()) { |
| LOG(ERROR) << "GetResizeBounds RPC failed"; |
| return 0; |
| } |
| |
| return response.minimum_size(); |
| } |
| |
| uint64_t TerminaVm::GetAvailableDiskSpace() { |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| vm_tools::EmptyMessage request; |
| vm_tools::GetAvailableSpaceResponse response; |
| |
| grpc::Status status = stub_->GetAvailableSpace(&ctx, request, &response); |
| |
| if (!status.ok()) { |
| LOG(ERROR) << "GetAvailableSpace RPC failed"; |
| return 0; |
| } |
| |
| return response.available_space(); |
| } |
| |
| void TerminaVm::HandleStatefulUpdate( |
| const spaced::StatefulDiskSpaceUpdate update) { |
| if (IsSuspended() || !storage_ballooning_) { |
| return; |
| } |
| |
| grpc::ClientContext ctx; |
| ctx.set_deadline(gpr_time_add( |
| gpr_now(GPR_CLOCK_MONOTONIC), |
| gpr_time_from_seconds(kDefaultTimeoutSeconds, GPR_TIMESPAN))); |
| |
| vm_tools::UpdateStorageBalloonRequest request; |
| vm_tools::UpdateStorageBalloonRequest out; |
| request.set_state(MapSpacedStateToGuestState(update.state())); |
| request.set_free_space_bytes(update.free_space_bytes()); |
| |
| maitred_handle_->CallRpc( |
| &vm_tools::Maitred::Stub::AsyncUpdateStorageBalloon, |
| base::Seconds(kDefaultTimeoutSeconds), std::move(request), |
| base::BindOnce( |
| [](grpc::Status status, |
| std::unique_ptr<vm_tools::UpdateStorageBalloonResponse> response) { |
| if (!status.ok()) { |
| LOG(ERROR) << "HandleStatefulUpdate RPC failed"; |
| } |
| })); |
| |
| return; |
| } |
| |
| net_base::IPv4Address TerminaVm::GatewayAddress() const { |
| return Network()->GatewayV4(); |
| } |
| |
| net_base::IPv4Address TerminaVm::IPv4Address() const { |
| return Network()->AddressV4(); |
| } |
| |
| net_base::IPv4Address TerminaVm::Netmask() const { |
| return Network()->SubnetV4().ToNetmask(); |
| } |
| |
| net_base::IPv4CIDR TerminaVm::ContainerCIDRAddress() const { |
| CHECK(classification_ == apps::TERMINA); |
| return *net_base::IPv4CIDR::CreateFromAddressAndPrefix( |
| Network()->ContainerAddressV4(), |
| Network()->ContainerSubnetV4().prefix_length()); |
| } |
| |
| std::string TerminaVm::PermissionToken() const { |
| return permission_token_; |
| } |
| |
| VmBaseImpl::Info TerminaVm::GetInfo() const { |
| VmBaseImpl::Info info = { |
| .ipv4_address = IPv4Address().ToInAddr().s_addr, |
| .pid = pid(), |
| .cid = cid(), |
| .seneschal_server_handle = seneschal_server_handle(), |
| .permission_token = permission_token_, |
| .status = classification_ == apps::TERMINA && !IsTremplinStarted() |
| ? VmBaseImpl::Status::STARTING |
| : VmBaseImpl::Status::RUNNING, |
| .type = classification_, |
| .storage_ballooning = storage_ballooning_, |
| }; |
| |
| return info; |
| } |
| |
| GuestOsNetwork* TerminaVm::Network() const { |
| return static_cast<GuestOsNetwork*>(GetNetwork()); |
| } |
| |
| void TerminaVm::set_kernel_version_for_testing(std::string kernel_version) { |
| kernel_version_ = kernel_version; |
| } |
| |
| void TerminaVm::InitializeMaitredService( |
| std::unique_ptr<vm_tools::Maitred::Stub> stub) { |
| // It is not safe to delete the maitred handle without shutting it down first. |
| CHECK(!maitred_handle_); |
| stub_ = stub.get(); |
| // The TaskRunner supplied here is the one on which *responses* will be |
| // posted, so we use the current sequence. |
| maitred_handle_.reset(new brillo::AsyncGrpcClient<vm_tools::Maitred>( |
| base::SequencedTaskRunner::GetCurrentDefault(), std::move(stub))); |
| } |
| |
| std::unique_ptr<TerminaVm> TerminaVm::CreateForTesting( |
| std::unique_ptr<GuestOsNetwork> network, |
| uint32_t vsock_cid, |
| base::FilePath runtime_dir, |
| base::FilePath log_path, |
| std::string stateful_device, |
| uint64_t stateful_size, |
| std::string kernel_version, |
| std::unique_ptr<vm_tools::Maitred::Stub> stub, |
| VmBuilder vm_builder) { |
| VmFeatures features{ |
| .gpu = false, |
| .vtpm_proxy = false, |
| .audio_capture = false, |
| }; |
| auto vm = base::WrapUnique(new TerminaVm( |
| TerminaVm::Config{.vsock_cid = vsock_cid, |
| .network = std::move(network), |
| .seneschal_server_proxy = nullptr, |
| .cros_vm_socket = "", |
| .runtime_dir = std::move(runtime_dir), |
| .log_path = std::move(log_path), |
| .stateful_device = std::move(stateful_device), |
| .stateful_size = std::move(stateful_size), |
| .features = features, |
| .id = VmId("foo", "bar"), |
| .classification = apps::VmType::UNKNOWN})); |
| vm->set_kernel_version_for_testing(kernel_version); |
| vm->InitializeMaitredService(std::move(stub)); |
| return vm; |
| } |
| |
| void TerminaVm::StopMaitredForTesting(base::OnceClosure stop_callback) { |
| auto maitred = maitred_handle_.release(); |
| maitred->ShutDown(base::BindOnce( |
| [](brillo::AsyncGrpcClient<vm_tools::Maitred>* maitred, |
| base::OnceClosure stop_callback) { |
| delete maitred; |
| std::move(stop_callback).Run(); |
| }, |
| maitred, std::move(stop_callback))); |
| } |
| |
| } // namespace vm_tools::concierge |