| // Copyright 2022 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/guest_manager/guest_manager.h" |
| |
| #include <fuchsia/net/interfaces/cpp/fidl.h> |
| #include <fuchsia/virtualization/cpp/fidl.h> |
| #include <lib/fdio/directory.h> |
| #include <lib/fpromise/result.h> |
| #include <lib/syslog/cpp/macros.h> |
| |
| #include <memory> |
| #include <unordered_set> |
| |
| #include <src/lib/files/file.h> |
| |
| #include "src/virtualization/bin/guest_manager/memory_pressure_handler.h" |
| |
| namespace { |
| |
| // This is a locally administered MAC address (first byte 0x02) mixed with the |
| // Google Organizationally Unique Identifier (00:1a:11). The host gets ff:ff:ff |
| // and the guest gets 00:00:00 for the last three octets. |
| constexpr fuchsia::net::MacAddress kGuestMacAddress = { |
| .octets = {0x02, 0x1a, 0x11, 0x00, 0x01, 0x00}, |
| }; |
| |
| uint64_t GetDefaultGuestMemory() { |
| const uint64_t host_memory = zx_system_get_physmem(); |
| const uint64_t max_reserved_host_memory = 3 * (1ul << 30); // 3 GiB. |
| |
| // Reserve half the host memory up to 2 GiB, and allow the rest to be used by the guest. |
| return host_memory - std::min(host_memory / 2, max_reserved_host_memory); |
| } |
| |
| uint8_t GetDefaultNumCpus() { |
| return static_cast<uint8_t>(std::min(zx_system_get_num_cpus(), UINT8_MAX)); |
| } |
| |
| } // namespace |
| |
| using ::fuchsia::virtualization::GuestConfig; |
| using ::fuchsia::virtualization::GuestDescriptor; |
| using ::fuchsia::virtualization::GuestError; |
| using ::fuchsia::virtualization::GuestLifecycle_Create_Result; |
| using ::fuchsia::virtualization::GuestLifecycle_Run_Result; |
| using ::fuchsia::virtualization::GuestManagerError; |
| using ::fuchsia::virtualization::GuestStatus; |
| |
| GuestManager::GuestManager(async_dispatcher_t* dispatcher, sys::ComponentContext* context, |
| std::string config_pkg_dir_path, std::string config_path) |
| : dispatcher_(dispatcher), |
| context_(context), |
| config_pkg_dir_path_(std::move(config_pkg_dir_path)), |
| config_path_(std::move(config_path)) { |
| context_->outgoing()->AddPublicService(manager_bindings_.GetHandler(this)); |
| } |
| |
| fit::result<GuestManagerError, GuestConfig> GuestManager::GetDefaultGuestConfig() { |
| // Reads guest config from [zircon|termina|debian]_guest package provided as child in |
| // [zircon|termina|debian]_guest_manager component hierarchy. |
| const std::string config_path = config_pkg_dir_path_ + config_path_; |
| auto open_at = [&](const std::string& path, fidl::InterfaceRequest<fuchsia::io::File> file) { |
| return fdio_open((config_pkg_dir_path_ + path).c_str(), |
| static_cast<uint32_t>(fuchsia::io::OpenFlags::RIGHT_READABLE), |
| file.TakeChannel().release()); |
| }; |
| |
| std::string content; |
| bool readFileSuccess = files::ReadFileToString(config_path, &content); |
| if (!readFileSuccess) { |
| FX_LOGS(ERROR) << "Failed to read guest configuration " << config_path; |
| return fit::error(GuestManagerError::BAD_CONFIG); |
| } |
| auto config = guest_config::ParseConfig(content, std::move(open_at)); |
| if (config.is_error()) { |
| FX_PLOGS(ERROR, config.error_value()) << "Failed to parse guest configuration " << config_path; |
| return fit::error(GuestManagerError::BAD_CONFIG); |
| } |
| |
| return fit::ok(std::move(*config)); |
| } |
| |
| // |fuchsia::virtualization::GuestManager| |
| void GuestManager::Launch(GuestConfig user_config, |
| fidl::InterfaceRequest<fuchsia::virtualization::Guest> controller, |
| LaunchCallback callback) { |
| if (is_guest_started()) { |
| callback(fpromise::error(GuestManagerError::ALREADY_RUNNING)); |
| return; |
| } |
| |
| if (!lifecycle_.is_bound()) { |
| // Opening the lifecycle channel will start the VMM component, and closing the channel will |
| // destroy the VMM component. |
| // |
| // This error handler is only invoked if the server side of the channel is closed, not if the |
| // guest manager closes the channel to destroy the VMM component. As the VMM component will |
| // never intentionally close the channel, this channel closing means that the component has |
| // terminated unexpectedly. |
| context_->svc()->Connect(lifecycle_.NewRequest()); |
| lifecycle_.set_error_handler([this](zx_status_t) { |
| state_ = GuestStatus::VMM_UNEXPECTED_TERMINATION; |
| OnGuestStopped(); |
| }); |
| } |
| |
| auto default_config = GetDefaultGuestConfig(); |
| if (default_config.is_error()) { |
| callback(fpromise::error(default_config.error_value())); |
| return; |
| } |
| |
| // Use the static config as a base, but apply the user config as an override. |
| GuestConfig merged_cfg; |
| merged_cfg = guest_config::MergeConfigs(std::move(*default_config), std::move(user_config)); |
| if (!merged_cfg.has_guest_memory()) { |
| merged_cfg.set_guest_memory(GetDefaultGuestMemory()); |
| } |
| if (!merged_cfg.has_cpus()) { |
| merged_cfg.set_cpus(GetDefaultNumCpus()); |
| } |
| |
| if (merged_cfg.has_default_net() && merged_cfg.default_net()) { |
| merged_cfg.mutable_net_devices()->push_back({ |
| .mac_address = kGuestMacAddress, |
| .enable_bridge = true, |
| }); |
| } |
| |
| // Merge the command-line additions into the main kernel command-line field. |
| for (auto& cmdline : *merged_cfg.mutable_cmdline_add()) { |
| merged_cfg.mutable_cmdline()->append(" " + cmdline); |
| } |
| merged_cfg.clear_cmdline_add(); |
| |
| // If there are any initial vsock listeners, they must be bound to unique host ports. |
| if (merged_cfg.has_vsock_listeners() && merged_cfg.vsock_listeners().size() > 1) { |
| std::unordered_set<uint32_t> ports; |
| for (auto& listener : merged_cfg.vsock_listeners()) { |
| if (!ports.insert(listener.port).second) { |
| callback(fpromise::error(GuestManagerError::BAD_CONFIG)); |
| return; |
| } |
| } |
| } |
| |
| start_time_ = zx::clock::get_monotonic(); |
| state_ = GuestStatus::STARTING; |
| last_error_ = std::nullopt; |
| SnapshotConfig(merged_cfg); |
| bool balloon_enabled = merged_cfg.has_virtio_balloon(); |
| lifecycle_->Create(std::move(merged_cfg), [this, callback = std::move(callback), balloon_enabled]( |
| GuestLifecycle_Create_Result result) mutable { |
| this->HandleCreateResult(std::move(result), balloon_enabled, std::move(callback)); |
| }); |
| lifecycle_->Bind(std::move(controller)); |
| } |
| |
| void GuestManager::HandleCreateResult(GuestLifecycle_Create_Result result, bool balloon_enabled, |
| LaunchCallback callback) { |
| if (result.is_err()) { |
| HandleGuestStopped(fit::error(result.err())); |
| callback(fpromise::error(GuestManagerError::START_FAILURE)); |
| } else { |
| state_ = GuestStatus::RUNNING; |
| lifecycle_->Run( |
| [this](GuestLifecycle_Run_Result result) { this->HandleRunResult(std::move(result)); }); |
| if (balloon_enabled) { |
| memory_pressure_handler_ = std::make_unique<MemoryPressureHandler>(dispatcher_); |
| zx_status_t status = memory_pressure_handler_->Start(context_); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Failed to start memory pressure handler"; |
| } |
| } |
| OnGuestLaunched(); |
| callback(fpromise::ok()); |
| } |
| } |
| |
| void GuestManager::HandleRunResult(GuestLifecycle_Run_Result result) { |
| if (result.is_response()) { |
| HandleGuestStopped(fit::ok()); |
| } else { |
| HandleGuestStopped(fit::error(result.err())); |
| } |
| } |
| |
| void GuestManager::HandleGuestStopped(fit::result<GuestError> err) { |
| if (err.is_ok()) { |
| last_error_ = std::nullopt; |
| } else { |
| last_error_ = err.error_value(); |
| } |
| |
| stop_time_ = zx::clock::get_monotonic(); |
| state_ = GuestStatus::STOPPED; |
| |
| for (auto& pending_force_shutdown : pending_force_shutdowns_) { |
| auto callback = std::move(pending_force_shutdown); |
| callback(); |
| } |
| pending_force_shutdowns_.clear(); |
| |
| OnGuestStopped(); |
| } |
| |
| void GuestManager::ForceShutdown(ForceShutdownCallback callback) { |
| if (!lifecycle_.is_bound() || !is_guest_started()) { |
| // VMM component isn't running. |
| callback(); |
| return; |
| } |
| |
| state_ = GuestStatus::STOPPING; |
| pending_force_shutdowns_.push_back(std::move(callback)); |
| lifecycle_->Stop([]() {}); |
| } |
| |
| GuestNetworkState GuestManager::QueryGuestNetworkState() { |
| if (!guest_descriptor_.has_networks() || guest_descriptor_.networks().empty()) { |
| return GuestNetworkState::NO_NETWORK_DEVICE; |
| } |
| |
| ::fuchsia::net::interfaces::StateSyncPtr state; |
| zx_status_t status = context_->svc()->Connect(state.NewRequest()); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Failed to connect to network interface service"; |
| return GuestNetworkState::FAILED_TO_QUERY; |
| } |
| |
| ::fuchsia::net::interfaces::WatcherSyncPtr watcher; |
| status = state->GetWatcher(::fuchsia::net::interfaces::WatcherOptions(), watcher.NewRequest()); |
| if (status != ZX_OK || !watcher.is_bound()) { |
| FX_PLOGS(ERROR, status) << "Failed to bind to network watcher service"; |
| return GuestNetworkState::FAILED_TO_QUERY; |
| } |
| |
| bool has_bridge = false, has_ethernet = false, has_wlan = false; |
| uint32_t num_virtual = 0; |
| ::fuchsia::net::interfaces::Event event; |
| do { |
| status = watcher->Watch(&event); |
| if (status != ZX_OK) { |
| FX_PLOGS(ERROR, status) << "Failed to watch for interface event"; |
| return GuestNetworkState::FAILED_TO_QUERY; |
| } |
| |
| if (!event.is_existing()) { |
| // Only care about existing interfaces at the moment of this query. |
| continue; |
| } |
| |
| if (!event.existing().has_device_class() || !event.existing().device_class().is_device()) { |
| // Ignore loopback interfaces. |
| continue; |
| } |
| |
| if (!event.existing().has_online() || !event.existing().online()) { |
| // Only consider enabled interfaces. |
| continue; |
| } |
| |
| switch (event.existing().device_class().device()) { |
| case ::fuchsia::hardware::network::DeviceClass::VIRTUAL: |
| num_virtual++; |
| break; |
| case ::fuchsia::hardware::network::DeviceClass::ETHERNET: |
| has_ethernet = true; |
| break; |
| case ::fuchsia::hardware::network::DeviceClass::WLAN: |
| has_wlan = true; |
| break; |
| case ::fuchsia::hardware::network::DeviceClass::BRIDGE: |
| has_bridge = true; |
| break; |
| case ::fuchsia::hardware::network::DeviceClass::PPP: |
| case ::fuchsia::hardware::network::DeviceClass::WLAN_AP: |
| // Ignore. |
| break; |
| } |
| } while (!event.is_idle()); |
| |
| if (!has_ethernet && !has_wlan) { |
| // No usable host networking, so there won't be any functional guest networking. |
| return GuestNetworkState::NO_HOST_NETWORKING; |
| } |
| |
| if (num_virtual < guest_descriptor_.networks().size()) { |
| // Something went wrong during virtio-net device initialization, and there are fewer virtual |
| // interfaces than there should be. This is an unlikely state as virtual interfaces may be |
| // non-functional, but they should at least be present. |
| return GuestNetworkState::MISSING_VIRTUAL_INTERFACES; |
| } |
| |
| // See if a bridge is expected from the guest network configurations. |
| bool expected_bridge = |
| std::any_of(guest_descriptor_.networks().begin(), guest_descriptor_.networks().end(), |
| [](auto& spec) { return spec.enable_bridge; }); |
| |
| if (expected_bridge && !has_bridge) { |
| // A bridge was expected from the guest network configurations, but none are present. |
| if (has_wlan && !has_ethernet) { |
| // There's no ethernet interface to bridge against, but there is a WLAN interface. Bridging |
| // against WLAN isn't supported, so the user needs to disconnect from WiFi and connect |
| // ethernet. |
| return GuestNetworkState::ATTEMPTED_TO_BRIDGE_WITH_WLAN; |
| } |
| |
| // Possibly a transient state where a bridge is still being created. |
| return GuestNetworkState::NO_BRIDGE_CREATED; |
| } |
| |
| // The host and guest are likely correctly configured for guest networking. |
| return GuestNetworkState::OK; |
| } |
| |
| // Static. |
| std::string GuestManager::GuestNetworkStateToStringExplanation(GuestNetworkState state) { |
| switch (state) { |
| case GuestNetworkState::OK: |
| return "Guest network likely configured correctly; " |
| "check host connectivity if suspected network failure"; |
| case GuestNetworkState::NO_NETWORK_DEVICE: |
| return "Guest not configured with a network device; " |
| "check guest configuration if networking is required"; |
| case GuestNetworkState::FAILED_TO_QUERY: |
| return "Failed to query guest network status"; |
| case GuestNetworkState::NO_HOST_NETWORKING: |
| return "Host has no network interfaces; guest networking will not work"; |
| case GuestNetworkState::MISSING_VIRTUAL_INTERFACES: |
| return "Fewer than expected virtual interfaces; guest failed network device startup"; |
| case GuestNetworkState::NO_BRIDGE_CREATED: |
| return "No bridge between guest and host network interaces; " |
| "this may be transient so retrying is recommended"; |
| case GuestNetworkState::ATTEMPTED_TO_BRIDGE_WITH_WLAN: |
| return "Cannot create bridged guest network when host is using WiFi; " |
| "disconnect WiFi and connect via ethernet"; |
| } |
| } |
| |
| void GuestManager::Connect(fidl::InterfaceRequest<fuchsia::virtualization::Guest> controller, |
| fuchsia::virtualization::GuestManager::ConnectCallback callback) { |
| if (is_guest_started()) { |
| lifecycle_->Bind(std::move(controller)); |
| callback(fpromise::ok()); |
| } else { |
| FX_LOGS(ERROR) << "Failed to connect to guest. Guest is not running"; |
| callback(fpromise::error(GuestManagerError::NOT_RUNNING)); |
| } |
| } |
| |
| std::vector<std::string> GuestManager::CheckForProblems() { |
| std::vector<std::string> problems; |
| |
| GuestNetworkState network_state = QueryGuestNetworkState(); |
| switch (network_state) { |
| case GuestNetworkState::OK: |
| case GuestNetworkState::NO_NETWORK_DEVICE: |
| // Do nothing. |
| break; |
| case GuestNetworkState::FAILED_TO_QUERY: |
| case GuestNetworkState::NO_HOST_NETWORKING: |
| case GuestNetworkState::MISSING_VIRTUAL_INTERFACES: |
| case GuestNetworkState::NO_BRIDGE_CREATED: |
| case GuestNetworkState::ATTEMPTED_TO_BRIDGE_WITH_WLAN: |
| problems.push_back(GuestNetworkStateToStringExplanation(network_state)); |
| break; |
| } |
| |
| if (memory_pressure_handler_ != nullptr) { |
| switch (memory_pressure_handler_->get_latest_memory_pressure_event()) { |
| case fuchsia_memorypressure::Level::kNormal: |
| // Do nothing. |
| break; |
| case fuchsia_memorypressure::Level::kWarning: |
| problems.push_back("Host is experiencing moderate memory pressure"); |
| break; |
| case fuchsia_memorypressure::Level::kCritical: |
| problems.push_back("Host is experiencing severe memory pressure"); |
| break; |
| } |
| } |
| |
| return problems; |
| } |
| |
| void GuestManager::GetInfo(GetInfoCallback callback) { |
| fuchsia::virtualization::GuestInfo info; |
| info.set_guest_status(state_); |
| |
| switch (state_) { |
| case GuestStatus::STARTING: |
| case GuestStatus::RUNNING: |
| case GuestStatus::STOPPING: { |
| GuestDescriptor descriptor; |
| FX_CHECK(guest_descriptor_.Clone(&descriptor) == ZX_OK); |
| info.set_guest_descriptor(std::move(descriptor)); |
| info.set_uptime((zx::clock::get_monotonic() - start_time_).to_nsecs()); |
| break; |
| } |
| case GuestStatus::STOPPED: { |
| info.set_uptime((stop_time_ - start_time_).to_nsecs()); |
| if (last_error_.has_value()) { |
| info.set_stop_error(last_error_.value()); |
| } |
| break; |
| } |
| case GuestStatus::VMM_UNEXPECTED_TERMINATION: |
| case GuestStatus::NOT_STARTED: |
| // Do nothing. |
| break; |
| } |
| |
| *info.mutable_detected_problems() = CheckForProblems(); |
| |
| callback(std::move(info)); |
| } |
| |
| void GuestManager::SnapshotConfig(const fuchsia::virtualization::GuestConfig& config) { |
| guest_descriptor_.set_num_cpus(config.cpus()); |
| guest_descriptor_.set_guest_memory(config.guest_memory()); |
| |
| guest_descriptor_.set_wayland(config.has_wayland_device()); |
| guest_descriptor_.set_magma(config.has_magma_device()); |
| |
| guest_descriptor_.set_balloon(config.has_virtio_balloon() && config.virtio_balloon()); |
| guest_descriptor_.set_console(config.has_virtio_console() && config.virtio_console()); |
| guest_descriptor_.set_gpu(config.has_virtio_gpu() && config.virtio_gpu()); |
| guest_descriptor_.set_rng(config.has_virtio_rng() && config.virtio_rng()); |
| guest_descriptor_.set_vsock(config.has_virtio_vsock() && config.virtio_vsock()); |
| guest_descriptor_.set_sound(config.has_virtio_sound() && config.virtio_sound()); |
| |
| if (config.has_net_devices()) { |
| *guest_descriptor_.mutable_networks() = config.net_devices(); |
| } |
| // TODO(https://fxbug.dev/42051237): After updating FIDL, this should also return the full MemoryDevice |
| // structure. |
| guest_descriptor_.set_mem(config.has_virtio_mem() && config.virtio_mem()); |
| } |
| |
| bool GuestManager::is_guest_started() const { |
| switch (state_) { |
| case GuestStatus::STARTING: |
| case GuestStatus::RUNNING: |
| case GuestStatus::STOPPING: |
| return true; |
| default: |
| return false; |
| } |
| } |