diff --git a/include/multipass/base_availability_zone.h b/include/multipass/base_availability_zone.h new file mode 100644 index 0000000000..43343251ac --- /dev/null +++ b/include/multipass/base_availability_zone.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_BASE_AVAILABILITY_ZONE_H +#define MULTIPASS_BASE_AVAILABILITY_ZONE_H + +#include "availability_zone.h" + +#include +#include +#include +#include + +namespace multipass +{ +class BaseAvailabilityZone : public AvailabilityZone +{ +public: + BaseAvailabilityZone(const std::string& name, const std::filesystem::path& az_directory); + + const std::string& get_name() const override; + const std::string& get_subnet() const override; + bool is_available() const override; + void set_available(bool new_available) override; + void add_vm(VirtualMachine& vm) override; + void remove_vm(VirtualMachine& vm) override; + +private: + void serialize() const; + + // we store all the data in one struct so that it can be created from one function call in the initializer list + struct data + { + const std::string name{}; + const std::filesystem::path file_path{}; + const std::string subnet{}; + bool available{}; + std::vector> vms{}; + // we don't have designated initializers, so mutex remains last so it doesn't need to be manually initialized + mutable std::recursive_mutex mutex{}; + } m; + + static data read_from_file(const std::string& name, const std::filesystem::path& file_path); +}; +} // namespace multipass + +#endif // MULTIPASS_BASE_AVAILABILITY_ZONE_H diff --git a/include/multipass/base_availability_zone_manager.h b/include/multipass/base_availability_zone_manager.h new file mode 100644 index 0000000000..1857dabb55 --- /dev/null +++ b/include/multipass/base_availability_zone_manager.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_BASE_AVAILABILITY_ZONE_MANAGER_H +#define MULTIPASS_BASE_AVAILABILITY_ZONE_MANAGER_H + +#include "availability_zone_manager.h" + +#include + +#include +#include +#include +#include + +namespace multipass +{ +class BaseAvailabilityZoneManager final : public AvailabilityZoneManager +{ +public: + explicit BaseAvailabilityZoneManager(const std::filesystem::path& data_dir); + + AvailabilityZone& get_zone(const std::string& name) override; + std::vector> get_zones() override; + std::string get_automatic_zone_name() override; + std::string get_default_zone_name() const override; + +private: + void serialize() const; + + class ZoneCollection + { + public: + static constexpr size_t size = default_zone_names.size(); + using ZoneArray = std::array; + static_assert(size > 0); + + const ZoneArray zones{}; + + ZoneCollection(ZoneArray&& zones, std::string last_used); + [[nodiscard]] std::string next_available(); + [[nodiscard]] std::string last_used() const; + + private: + ZoneArray::const_iterator automatic_zone; + mutable std::shared_mutex mutex{}; + }; + + // we store all the data in one struct so that it can be created from one function call in the initializer list + struct data + { + const std::filesystem::path file_path{}; + ZoneCollection zone_collection; + // we don't have designated initializers, so mutex remains last so it doesn't need to be manually initialized + mutable std::recursive_mutex mutex{}; + } m; + + [[nodiscard]] const ZoneCollection::ZoneArray& zones() const; + + static data read_from_file(const std::filesystem::path& file_path, const std::filesystem::path& zones_directory); +}; +} // namespace multipass + +#endif // MULTIPASS_BASE_AVAILABILITY_ZONE_MANAGER_H diff --git a/include/multipass/constants.h b/include/multipass/constants.h index 00ad8bfe3b..95ed243a06 100644 --- a/include/multipass/constants.h +++ b/include/multipass/constants.h @@ -72,6 +72,8 @@ constexpr auto petenv_default = "primary"; constexpr auto timeout_exit_code = 5; constexpr auto authenticated_certs_dir = "authenticated-certs"; + +constexpr auto default_zone_names = {"zone1", "zone2", "zone3"}; } // namespace multipass #endif // MULTIPASS_CONSTANTS_H diff --git a/include/multipass/json_utils.h b/include/multipass/json_utils.h index 0cf43cea92..53fbdefdcb 100644 --- a/include/multipass/json_utils.h +++ b/include/multipass/json_utils.h @@ -26,6 +26,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,7 @@ class JsonUtils : public Singleton explicit JsonUtils(const Singleton::PrivatePass&) noexcept; virtual void write_json(const QJsonObject& root, QString file_name) const; // transactional; creates parent dirs + virtual QJsonObject read_object_from_file(const std::filesystem::path& file_path) const; virtual std::string json_to_string(const QJsonObject& root) const; virtual QJsonValue update_cloud_init_instance_id(const QJsonValue& id, const std::string& src_vm_name, diff --git a/include/multipass/platform.h b/include/multipass/platform.h index 56dff1665a..93610ca055 100644 --- a/include/multipass/platform.h +++ b/include/multipass/platform.h @@ -19,6 +19,7 @@ #define MULTIPASS_PLATFORM_H #include +#include #include #include #include @@ -86,7 +87,7 @@ void sync_winterm_profiles(); std::string default_server_address(); -VirtualMachineFactory::UPtr vm_backend(const Path& data_dir); +VirtualMachineFactory::UPtr vm_backend(const Path& data_dir, AvailabilityZoneManager& az_manager); logging::Logger::UPtr make_logger(logging::Level level); UpdatePrompt::UPtr make_update_prompt(); std::unique_ptr make_sshfs_server_process(const SSHFSServerConfig& config); diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index 57e5b302b1..1fd11a13bf 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -36,6 +36,7 @@ namespace multipass { +class AvailabilityZone; class MemorySize; class VMMount; struct VMSpecs; @@ -55,7 +56,8 @@ class VirtualMachine : private DisabledCopyMove delayed_shutdown, suspending, suspended, - unknown + unknown, + unavailable, }; enum class ShutdownPolicy @@ -73,6 +75,7 @@ class VirtualMachine : private DisabledCopyMove virtual void start() = 0; virtual void shutdown(ShutdownPolicy shutdown_policy = ShutdownPolicy::Powerdown) = 0; virtual void suspend() = 0; + virtual void set_available(bool available) = 0; virtual State current_state() = 0; virtual int ssh_port() = 0; virtual std::string ssh_hostname() @@ -122,6 +125,7 @@ class VirtualMachine : private DisabledCopyMove virtual int get_snapshot_count() const = 0; QDir instance_directory() const; + virtual const AvailabilityZone& get_zone() const = 0; VirtualMachine::State state; const std::string vm_name; diff --git a/include/multipass/virtual_machine_description.h b/include/multipass/virtual_machine_description.h index 37c3624ab5..c6d9ed449c 100644 --- a/include/multipass/virtual_machine_description.h +++ b/include/multipass/virtual_machine_description.h @@ -40,6 +40,7 @@ class VirtualMachineDescription MemorySize mem_size; MemorySize disk_space; std::string vm_name; + std::string zone; std::string default_mac_address; std::vector extra_interfaces; std::string ssh_username; diff --git a/include/multipass/vm_specs.h b/include/multipass/vm_specs.h index 155715a39a..ba4483390f 100644 --- a/include/multipass/vm_specs.h +++ b/include/multipass/vm_specs.h @@ -45,31 +45,29 @@ struct VMSpecs bool deleted; QJsonObject metadata; int clone_count = 0; // tracks the number of cloned vm from this source vm (regardless of deletes) + std::string zone; }; inline bool operator==(const VMSpecs& a, const VMSpecs& b) { - return std::tie(a.num_cores, - a.mem_size, - a.disk_space, - a.default_mac_address, - a.extra_interfaces, - a.ssh_username, - a.state, - a.mounts, - a.deleted, - a.metadata, - a.clone_count) == std::tie(b.num_cores, - b.mem_size, - b.disk_space, - b.default_mac_address, - b.extra_interfaces, - b.ssh_username, - b.state, - b.mounts, - b.deleted, - b.metadata, - a.clone_count); + const auto properties_of = [](const VMSpecs& spec) { + return std::tuple{ + spec.num_cores, + spec.mem_size, + spec.disk_space, + spec.default_mac_address, + spec.extra_interfaces, + spec.ssh_username, + spec.state, + spec.mounts, + spec.deleted, + spec.metadata, + spec.clone_count, + spec.zone, + }; + }; + + return properties_of(a) == properties_of(b); } inline bool operator!=(const VMSpecs& a, const VMSpecs& b) // TODO drop in C++20 diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp index eaaccdffdc..24ce292764 100644 --- a/src/daemon/daemon.cpp +++ b/src/daemon/daemon.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -217,7 +218,9 @@ auto name_from(const std::string& requested_name, const std::string& blueprint_n } } -std::unordered_map load_db(const mp::Path& data_path, const mp::Path& cache_path) +std::unordered_map load_db(const mp::Path& data_path, + const mp::Path& cache_path, + const mp::AvailabilityZoneManager& az_manager) { QDir data_dir{data_path}; QDir cache_dir{cache_path}; @@ -255,6 +258,8 @@ std::unordered_map load_db(const mp::Path& data_path, auto deleted = record["deleted"].toBool(); auto metadata = record["metadata"].toObject(); auto clone_count = record["clone_count"].toInt(); + auto zone = record["zone"].toString().toStdString(); + zone = zone.empty() ? az_manager.get_default_zone_name() : zone; if (!num_cores && !deleted && ssh_username.empty() && metadata.isEmpty() && !mp::MemorySize{mem_size}.in_bytes() && !mp::MemorySize{disk_space}.in_bytes()) @@ -292,7 +297,9 @@ std::unordered_map load_db(const mp::Path& data_path, mounts, deleted, metadata, - clone_count}; + clone_count, + zone, + }; } return reconstructed_records; } @@ -323,6 +330,7 @@ QJsonObject vm_spec_to_json(const mp::VMSpecs& specs) json.insert("mounts", json_mounts); json.insert("clone_count", specs.clone_count); + json.insert("zone", QString::fromStdString(specs.zone)); return json; } @@ -496,7 +504,8 @@ void validate_image(const mp::LaunchRequest* request, const mp::VMImageVault& va auto validate_create_arguments(const mp::LaunchRequest* request, const mp::DaemonConfig* config) { - assert(config && config->factory && config->blueprint_provider && config->vault && "null ptr somewhere..."); + assert(config && config->factory && config->blueprint_provider && config->vault && config->az_manager && + "null ptr somewhere..."); validate_image(request, *config->vault, *config->blueprint_provider); static const auto min_mem = try_mem_size(mp::min_memory_size); @@ -506,6 +515,7 @@ auto validate_create_arguments(const mp::LaunchRequest* request, const mp::Daemo auto mem_size_str = request->mem_size(); auto disk_space_str = request->disk_space(); auto instance_name = request->instance_name(); + auto zone_name = request->zone(); auto option_errors = mp::LaunchError{}; const auto opt_mem_size = try_mem_size(mem_size_str.empty() ? mp::default_memory_size : mem_size_str); @@ -535,6 +545,16 @@ auto validate_create_arguments(const mp::LaunchRequest* request, const mp::Daemo if (!instance_name.empty() && !mp::utils::valid_hostname(instance_name)) option_errors.add_error_codes(mp::LaunchError::INVALID_HOSTNAME); + try + { + if (!zone_name.empty() && !config->az_manager->get_zone(zone_name).is_available()) + option_errors.add_error_codes(mp::LaunchError::ZONE_UNAVAILABLE); + } + catch (const mp::AvailabilityZoneNotFound& e) + { + option_errors.add_error_codes(mp::LaunchError::INVALID_ZONE); + } + std::vector nets_need_bridging; auto extra_interfaces = validate_extra_interfaces(request, *config->factory, nets_need_bridging, option_errors); @@ -543,11 +563,19 @@ auto validate_create_arguments(const mp::LaunchRequest* request, const mp::Daemo mp::MemorySize mem_size; std::optional disk_space; std::string instance_name; + std::string zone_name; std::vector extra_interfaces; std::vector nets_need_bridging; mp::LaunchError option_errors; - } ret{std::move(mem_size), std::move(disk_space), std::move(instance_name), - std::move(extra_interfaces), std::move(nets_need_bridging), std::move(option_errors)}; + } ret{ + std::move(mem_size), + std::move(disk_space), + std::move(instance_name), + std::move(zone_name), + std::move(extra_interfaces), + std::move(nets_need_bridging), + std::move(option_errors), + }; return ret; } @@ -1293,7 +1321,8 @@ mp::Daemon::Daemon(std::unique_ptr the_config) : config{std::move(the_config)}, vm_instance_specs{load_db( mp::utils::backend_directory_path(config->data_directory, config->factory->get_backend_directory_name()), - mp::utils::backend_directory_path(config->cache_directory, config->factory->get_backend_directory_name()))}, + mp::utils::backend_directory_path(config->cache_directory, config->factory->get_backend_directory_name()), + *config->az_manager)}, daemon_rpc{config->server_address, *config->cert_provider, config->client_cert_store.get()}, instance_mod_handler{register_instance_mod( vm_instance_specs, @@ -1359,6 +1388,7 @@ mp::Daemon::Daemon(std::unique_ptr the_config) spec.mem_size, spec.disk_space, name, + spec.zone, spec.default_mac_address, spec.extra_interfaces, spec.ssh_username, @@ -2863,6 +2893,13 @@ try // clang-format on ZonesReply response{}; + for (const auto& zone : config->az_manager->get_zones()) + { + const auto reply_zone = response.add_zones(); + reply_zone->set_name(zone.get().get_name()); + reply_zone->set_available(zone.get().is_available()); + } + server->Write(response); status_promise->set_value(grpc::Status{}); } @@ -2878,8 +2915,17 @@ try // clang-format on { mpl::ClientLogger logger{mpl::level_from(request->verbosity_level()), *config->logger, server}; + for (const auto& zone_name : request->zones()) + { + config->az_manager->get_zone(zone_name).set_available(request->available()); + } + status_promise->set_value(grpc::Status{}); } +catch (const AvailabilityZoneNotFound& e) +{ + status_promise->set_value(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, e.what(), "")); +} catch (const std::exception& e) { status_promise->set_value(grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, e.what(), "")); @@ -3018,6 +3064,8 @@ void mp::Daemon::create_vm(const CreateRequest* request, } auto name = name_from(checked_args.instance_name, blueprint_name, *config->name_generator, operative_instances); + auto zone_name = + checked_args.zone_name.empty() ? config->az_manager->get_automatic_zone_name() : checked_args.zone_name; auto [instance_trail, status] = find_instance_and_react(operative_instances, deleted_instances, name, require_missing_instances_reaction); @@ -3055,16 +3103,20 @@ void mp::Daemon::create_vm(const CreateRequest* request, auto& vm_aliases = vm_client_data.aliases_to_be_created; auto& vm_workspaces = vm_client_data.workspaces_to_be_created; - vm_instance_specs[name] = {vm_desc.num_cores, - vm_desc.mem_size, - vm_desc.disk_space, - vm_desc.default_mac_address, - vm_desc.extra_interfaces, - config->ssh_username, - VirtualMachine::State::off, - {}, - false, - QJsonObject()}; + vm_instance_specs[name] = { + vm_desc.num_cores, + vm_desc.mem_size, + vm_desc.disk_space, + vm_desc.default_mac_address, + vm_desc.extra_interfaces, + config->ssh_username, + VirtualMachine::State::off, + {}, + false, + QJsonObject(), + 0, + vm_desc.zone, + }; operative_instances[name] = config->factory->create_virtual_machine(vm_desc, *config->ssh_key_provider, *this); preparing_instances.erase(name); @@ -3136,7 +3188,8 @@ void mp::Daemon::create_vm(const CreateRequest* request, delete prepare_future_watcher; }); - auto make_vm_description = [this, server, request, name, checked_args, log_level]() mutable -> VMFullDescription { + auto make_vm_description = + [this, server, request, name, zone_name, checked_args, log_level]() mutable -> VMFullDescription { mpl::ClientLogger logger{log_level, *config->logger, server}; try @@ -3151,6 +3204,7 @@ void mp::Daemon::create_vm(const CreateRequest* request, MemorySize{request->mem_size().empty() ? "0b" : request->mem_size()}, MemorySize{request->disk_space().empty() ? "0b" : request->disk_space()}, name, + zone_name, "", {}, config->ssh_username, @@ -3158,9 +3212,12 @@ void mp::Daemon::create_vm(const CreateRequest* request, "", YAML::Node{}, YAML::Node{}, - make_cloud_init_vendor_config(*config->ssh_key_provider, config->ssh_username, - config->factory->get_backend_version_string().toStdString(), request), - YAML::Node{}}; + make_cloud_init_vendor_config(*config->ssh_key_provider, + config->ssh_username, + config->factory->get_backend_version_string().toStdString(), + request), + YAML::Node{}, + }; ClientLaunchData client_launch_data; diff --git a/src/daemon/daemon_config.cpp b/src/daemon/daemon_config.cpp index 164b74bdbc..9263eedfae 100644 --- a/src/daemon/daemon_config.cpp +++ b/src/daemon/daemon_config.cpp @@ -20,6 +20,7 @@ #include "custom_image_host.h" #include "ubuntu_image_host.h" +#include #include #include #include @@ -152,8 +153,10 @@ std::unique_ptr mp::DaemonConfigBuilder::build() if (url_downloader == nullptr) url_downloader = std::make_unique(cache_directory, std::chrono::seconds{10}); + if (az_manager == nullptr) + az_manager = std::make_unique(data_directory.toStdString()); if (factory == nullptr) - factory = platform::vm_backend(data_directory); + factory = platform::vm_backend(data_directory, *az_manager); if (update_prompt == nullptr) update_prompt = platform::make_update_prompt(); if (image_hosts.empty()) @@ -238,8 +241,23 @@ std::unique_ptr mp::DaemonConfigBuilder::build() server_name_from(server_address)); return std::unique_ptr(new DaemonConfig{ - std::move(url_downloader), std::move(factory), std::move(image_hosts), std::move(vault), - std::move(name_generator), std::move(ssh_key_provider), std::move(cert_provider), std::move(client_cert_store), - std::move(update_prompt), multiplexing_logger, std::move(network_proxy), std::move(blueprint_provider), - cache_directory, data_directory, server_address, ssh_username, image_refresh_timer}); + std::move(url_downloader), + std::move(factory), + std::move(image_hosts), + std::move(vault), + std::move(name_generator), + std::move(ssh_key_provider), + std::move(cert_provider), + std::move(client_cert_store), + std::move(update_prompt), + multiplexing_logger, + std::move(network_proxy), + std::move(blueprint_provider), + std::move(az_manager), + cache_directory, + data_directory, + server_address, + ssh_username, + image_refresh_timer, + }); } diff --git a/src/daemon/daemon_config.h b/src/daemon/daemon_config.h index 6f1b48df51..5b6d2a0c4e 100644 --- a/src/daemon/daemon_config.h +++ b/src/daemon/daemon_config.h @@ -18,6 +18,7 @@ #ifndef MULTIPASS_DAEMON_CONFIG_H #define MULTIPASS_DAEMON_CONFIG_H +#include #include #include #include @@ -56,6 +57,7 @@ struct DaemonConfig const std::shared_ptr logger; const std::unique_ptr network_proxy; const std::unique_ptr blueprint_provider; + const AvailabilityZoneManager::UPtr az_manager; const multipass::Path cache_directory; const multipass::Path data_directory; const std::string server_address; @@ -77,6 +79,7 @@ struct DaemonConfigBuilder std::unique_ptr logger; std::unique_ptr network_proxy; std::unique_ptr blueprint_provider; + AvailabilityZoneManager::UPtr az_manager; multipass::Path cache_directory; multipass::Path data_directory; std::string server_address; diff --git a/src/platform/backends/hyperv/hyperv_virtual_machine.cpp b/src/platform/backends/hyperv/hyperv_virtual_machine.cpp index 0ab44d659c..2216101596 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine.cpp +++ b/src/platform/backends/hyperv/hyperv_virtual_machine.cpp @@ -151,8 +151,9 @@ fs::path locate_vmcx_file(const fs::path& exported_vm_dir_path) mp::HyperVVirtualMachine::HyperVVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const mp::Path& instance_dir) - : HyperVVirtualMachine{desc, monitor, key_provider, instance_dir, true} + : HyperVVirtualMachine{desc, monitor, key_provider, zone, instance_dir, true} { if (!power_shell->run({"Get-VM", "-Name", name})) { @@ -204,8 +205,9 @@ mp::HyperVVirtualMachine::HyperVVirtualMachine(const std::string& source_vm_name const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& dest_instance_dir) - : HyperVVirtualMachine{desc, monitor, key_provider, dest_instance_dir, true} + : HyperVVirtualMachine{desc, monitor, key_provider, zone, dest_instance_dir, true} { // 1. Export-VM -Name vm1 -Path C:\ProgramData\Multipass\data\vault\instances\vm1-clone1 power_shell->easy_run( @@ -257,9 +259,10 @@ mp::HyperVVirtualMachine::HyperVVirtualMachine(const std::string& source_vm_name mp::HyperVVirtualMachine::HyperVVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir, bool /*is_internal*/) - : BaseVirtualMachine{desc.vm_name, key_provider, instance_dir}, + : BaseVirtualMachine{desc.vm_name, key_provider, zone, instance_dir}, desc{desc}, name{QString::fromStdString(desc.vm_name)}, power_shell{std::make_unique(vm_name)}, diff --git a/src/platform/backends/hyperv/hyperv_virtual_machine.h b/src/platform/backends/hyperv/hyperv_virtual_machine.h index 2012983773..a5b81e517e 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine.h +++ b/src/platform/backends/hyperv/hyperv_virtual_machine.h @@ -44,6 +44,7 @@ class HyperVVirtualMachine final : public BaseVirtualMachine HyperVVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir); // Contruct the vm based on the source virtual machine HyperVVirtualMachine(const std::string& source_vm_name, @@ -51,6 +52,7 @@ class HyperVVirtualMachine final : public BaseVirtualMachine const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& dest_instance_dir); ~HyperVVirtualMachine(); void start() override; @@ -85,6 +87,7 @@ class HyperVVirtualMachine final : public BaseVirtualMachine HyperVVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir, bool is_internal); // is_internal is a dummy parameter to differentiate with other constructors diff --git a/src/platform/backends/hyperv/hyperv_virtual_machine_factory.cpp b/src/platform/backends/hyperv/hyperv_virtual_machine_factory.cpp index 29a2b9fdfb..155bffa1fb 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv/hyperv_virtual_machine_factory.cpp @@ -247,8 +247,10 @@ std::string error_msg_helper(const std::string& msg_core, const QString& ps_outp } } // namespace -mp::HyperVVirtualMachineFactory::HyperVVirtualMachineFactory(const mp::Path& data_dir) - : BaseVirtualMachineFactory(MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir)) +mp::HyperVVirtualMachineFactory::HyperVVirtualMachineFactory(const mp::Path& data_dir, + AvailabilityZoneManager& az_manager) + : BaseVirtualMachineFactory(MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir), + az_manager) { } @@ -259,6 +261,7 @@ mp::VirtualMachine::UPtr mp::HyperVVirtualMachineFactory::create_virtual_machine return std::make_unique(desc, monitor, key_provider, + az_manager.get_zone(desc.zone), get_instance_directory(desc.vm_name)); } @@ -433,5 +436,6 @@ mp::VirtualMachine::UPtr mp::HyperVVirtualMachineFactory::clone_vm_impl(const st dest_vm_desc, monitor, key_provider, + az_manager.get_zone(dest_vm_desc.zone), get_instance_directory(dest_vm_desc.vm_name)); } diff --git a/src/platform/backends/hyperv/hyperv_virtual_machine_factory.h b/src/platform/backends/hyperv/hyperv_virtual_machine_factory.h index a23f8ddcba..b1a5a1dfd6 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine_factory.h +++ b/src/platform/backends/hyperv/hyperv_virtual_machine_factory.h @@ -30,7 +30,7 @@ struct HyperVNetworkAccessor; // fwd declaration, to befriend below class HyperVVirtualMachineFactory final : public BaseVirtualMachineFactory { public: - explicit HyperVVirtualMachineFactory(const Path& data_dir); + explicit HyperVVirtualMachineFactory(const Path& data_dir, AvailabilityZoneManager& az_manager); VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, const SSHKeyProvider& key_provider, diff --git a/src/platform/backends/libvirt/libvirt_virtual_machine.cpp b/src/platform/backends/libvirt/libvirt_virtual_machine.cpp index ba36a15773..eb1dfa3aa2 100644 --- a/src/platform/backends/libvirt/libvirt_virtual_machine.cpp +++ b/src/platform/backends/libvirt/libvirt_virtual_machine.cpp @@ -17,6 +17,7 @@ #include "libvirt_virtual_machine.h" +#include #include #include #include @@ -277,8 +278,9 @@ mp::LibVirtVirtualMachine::LibVirtVirtualMachine(const VirtualMachineDescription VMStatusMonitor& monitor, const LibvirtWrapper::UPtr& libvirt_wrapper, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir) - : BaseVirtualMachine{desc.vm_name, key_provider, instance_dir}, + : BaseVirtualMachine{desc.vm_name, key_provider, zone, instance_dir}, username{desc.ssh_username}, desc{desc}, monitor{&monitor}, diff --git a/src/platform/backends/libvirt/libvirt_virtual_machine.h b/src/platform/backends/libvirt/libvirt_virtual_machine.h index c352de9b39..e17d817a41 100644 --- a/src/platform/backends/libvirt/libvirt_virtual_machine.h +++ b/src/platform/backends/libvirt/libvirt_virtual_machine.h @@ -40,6 +40,7 @@ class LibVirtVirtualMachine final : public BaseVirtualMachine VMStatusMonitor& monitor, const LibvirtWrapper::UPtr& libvirt_wrapper, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir); ~LibVirtVirtualMachine(); diff --git a/src/platform/backends/libvirt/libvirt_virtual_machine_factory.cpp b/src/platform/backends/libvirt/libvirt_virtual_machine_factory.cpp index 903eaaf76d..82f289eeac 100644 --- a/src/platform/backends/libvirt/libvirt_virtual_machine_factory.cpp +++ b/src/platform/backends/libvirt/libvirt_virtual_machine_factory.cpp @@ -107,9 +107,10 @@ auto make_libvirt_wrapper(const std::string& libvirt_object_path) } // namespace mp::LibVirtVirtualMachineFactory::LibVirtVirtualMachineFactory(const mp::Path& data_dir, - const std::string& libvirt_object_path) - : BaseVirtualMachineFactory( - MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir)), + const std::string& libvirt_object_path, + AvailabilityZoneManager& az_manager) + : BaseVirtualMachineFactory(MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir), + az_manager), libvirt_wrapper{make_libvirt_wrapper(libvirt_object_path)}, data_dir{data_dir}, bridge_name{enable_libvirt_network(data_dir, libvirt_wrapper)}, @@ -117,8 +118,9 @@ mp::LibVirtVirtualMachineFactory::LibVirtVirtualMachineFactory(const mp::Path& d { } -mp::LibVirtVirtualMachineFactory::LibVirtVirtualMachineFactory(const mp::Path& data_dir) - : LibVirtVirtualMachineFactory(data_dir, "libvirt.so.0") +mp::LibVirtVirtualMachineFactory::LibVirtVirtualMachineFactory(const mp::Path& data_dir, + AvailabilityZoneManager& az_manager) + : LibVirtVirtualMachineFactory(data_dir, "libvirt.so.0", az_manager) { } @@ -134,6 +136,7 @@ mp::VirtualMachine::UPtr mp::LibVirtVirtualMachineFactory::create_virtual_machin monitor, libvirt_wrapper, key_provider, + az_manager.get_zone(desc.zone), get_instance_directory(desc.vm_name)); } diff --git a/src/platform/backends/libvirt/libvirt_virtual_machine_factory.h b/src/platform/backends/libvirt/libvirt_virtual_machine_factory.h index 9538044de3..c57d1295ab 100644 --- a/src/platform/backends/libvirt/libvirt_virtual_machine_factory.h +++ b/src/platform/backends/libvirt/libvirt_virtual_machine_factory.h @@ -31,8 +31,10 @@ class ProcessFactory; class LibVirtVirtualMachineFactory final : public BaseVirtualMachineFactory { public: - explicit LibVirtVirtualMachineFactory(const Path& data_dir, const std::string& libvirt_object_path); // For testing - explicit LibVirtVirtualMachineFactory(const Path& data_dir); + explicit LibVirtVirtualMachineFactory(const Path& data_dir, + const std::string& libvirt_object_path, + AvailabilityZoneManager& az_manager); // For testing + explicit LibVirtVirtualMachineFactory(const Path& data_dir, AvailabilityZoneManager& az_manager); ~LibVirtVirtualMachineFactory(); VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, diff --git a/src/platform/backends/lxd/lxd_virtual_machine.cpp b/src/platform/backends/lxd/lxd_virtual_machine.cpp index 866feb7d6d..62dd598fe0 100644 --- a/src/platform/backends/lxd/lxd_virtual_machine.cpp +++ b/src/platform/backends/lxd/lxd_virtual_machine.cpp @@ -177,8 +177,9 @@ mp::LXDVirtualMachine::LXDVirtualMachine(const VirtualMachineDescription& desc, const QString& bridge_name, const QString& storage_pool, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const mp::Path& instance_dir) - : BaseVirtualMachine{desc.vm_name, key_provider, instance_dir}, + : BaseVirtualMachine{desc.vm_name, key_provider, zone, instance_dir}, name{QString::fromStdString(desc.vm_name)}, username{desc.ssh_username}, monitor{&monitor}, diff --git a/src/platform/backends/lxd/lxd_virtual_machine.h b/src/platform/backends/lxd/lxd_virtual_machine.h index b928d4d94d..f0e936e39d 100644 --- a/src/platform/backends/lxd/lxd_virtual_machine.h +++ b/src/platform/backends/lxd/lxd_virtual_machine.h @@ -39,6 +39,7 @@ class LXDVirtualMachine : public BaseVirtualMachine const QString& bridge_name, const QString& storage_pool, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir); ~LXDVirtualMachine() override; diff --git a/src/platform/backends/lxd/lxd_virtual_machine_factory.cpp b/src/platform/backends/lxd/lxd_virtual_machine_factory.cpp index 5cfe2d8d23..2cc207c17a 100644 --- a/src/platform/backends/lxd/lxd_virtual_machine_factory.cpp +++ b/src/platform/backends/lxd/lxd_virtual_machine_factory.cpp @@ -77,16 +77,19 @@ mp::NetworkInterfaceInfo munch_network(std::map(), data_dir, base_url) +mp::LXDVirtualMachineFactory::LXDVirtualMachineFactory(const mp::Path& data_dir, + AvailabilityZoneManager& az_manager, + const QUrl& base_url) + : LXDVirtualMachineFactory(std::make_unique(), data_dir, az_manager, base_url) { } @@ -101,6 +104,7 @@ mp::VirtualMachine::UPtr mp::LXDVirtualMachineFactory::create_virtual_machine(co multipass_bridge_name, storage_pool, key_provider, + az_manager.get_zone(desc.zone), MP_UTILS.make_dir(get_instance_directory(desc.vm_name))); } diff --git a/src/platform/backends/lxd/lxd_virtual_machine_factory.h b/src/platform/backends/lxd/lxd_virtual_machine_factory.h index 16a2e7c58a..a6c8f2d0cb 100644 --- a/src/platform/backends/lxd/lxd_virtual_machine_factory.h +++ b/src/platform/backends/lxd/lxd_virtual_machine_factory.h @@ -30,8 +30,12 @@ namespace multipass class LXDVirtualMachineFactory : public BaseVirtualMachineFactory { public: - explicit LXDVirtualMachineFactory(const Path& data_dir, const QUrl& base_url = lxd_socket_url); - explicit LXDVirtualMachineFactory(NetworkAccessManager::UPtr manager, const Path& data_dir, + explicit LXDVirtualMachineFactory(const Path& data_dir, + AvailabilityZoneManager& az_manager, + const QUrl& base_url = lxd_socket_url); + explicit LXDVirtualMachineFactory(NetworkAccessManager::UPtr manager, + const Path& data_dir, + AvailabilityZoneManager& az_manager, const QUrl& base_url = lxd_socket_url); VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, diff --git a/src/platform/backends/qemu/qemu_virtual_machine.cpp b/src/platform/backends/qemu/qemu_virtual_machine.cpp index bfe1a70e90..182cf39261 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine.cpp +++ b/src/platform/backends/qemu/qemu_virtual_machine.cpp @@ -244,12 +244,14 @@ mp::QemuVirtualMachine::QemuVirtualMachine(const VirtualMachineDescription& desc QemuPlatform* qemu_platform, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir, bool remove_snapshots) : BaseVirtualMachine{mp::backend::instance_image_has_snapshot(desc.image.image_path, suspend_tag) ? State::suspended : State::off, desc.vm_name, key_provider, + zone, instance_dir}, desc{desc}, qemu_platform{qemu_platform}, diff --git a/src/platform/backends/qemu/qemu_virtual_machine.h b/src/platform/backends/qemu/qemu_virtual_machine.h index 7b0d73d28c..5213000212 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine.h +++ b/src/platform/backends/qemu/qemu_virtual_machine.h @@ -46,6 +46,7 @@ class QemuVirtualMachine : public QObject, public BaseVirtualMachine QemuPlatform* qemu_platform, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir, bool remove_snapshots = false); ~QemuVirtualMachine(); @@ -77,8 +78,11 @@ class QemuVirtualMachine : public QObject, public BaseVirtualMachine protected: // TODO remove this, the onus of composing a VM of stubs should be on the stub VMs - QemuVirtualMachine(const std::string& name, const SSHKeyProvider& key_provider, const Path& instance_dir) - : BaseVirtualMachine{name, key_provider, instance_dir} + QemuVirtualMachine(const std::string& name, + const SSHKeyProvider& key_provider, + AvailabilityZone& zone, + const Path& instance_dir) + : BaseVirtualMachine{name, key_provider, zone, instance_dir} { } diff --git a/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp b/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp index 0817e6a783..e3a211a8e8 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp +++ b/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp @@ -36,14 +36,17 @@ namespace constexpr auto category = "qemu factory"; } // namespace -mp::QemuVirtualMachineFactory::QemuVirtualMachineFactory(const mp::Path& data_dir) - : QemuVirtualMachineFactory{MP_QEMU_PLATFORM_FACTORY.make_qemu_platform(data_dir), data_dir} +mp::QemuVirtualMachineFactory::QemuVirtualMachineFactory(const mp::Path& data_dir, AvailabilityZoneManager& az_manager) + : QemuVirtualMachineFactory{MP_QEMU_PLATFORM_FACTORY.make_qemu_platform(data_dir), data_dir, az_manager} { } -mp::QemuVirtualMachineFactory::QemuVirtualMachineFactory(QemuPlatform::UPtr qemu_platform, const mp::Path& data_dir) +mp::QemuVirtualMachineFactory::QemuVirtualMachineFactory(QemuPlatform::UPtr qemu_platform, + const mp::Path& data_dir, + AvailabilityZoneManager& az_manager) : BaseVirtualMachineFactory( - MP_UTILS.derive_instances_dir(data_dir, qemu_platform->get_directory_name(), instances_subdir)), + MP_UTILS.derive_instances_dir(data_dir, qemu_platform->get_directory_name(), instances_subdir), + az_manager), qemu_platform{std::move(qemu_platform)} { } @@ -56,6 +59,7 @@ mp::VirtualMachine::UPtr mp::QemuVirtualMachineFactory::create_virtual_machine(c qemu_platform.get(), monitor, key_provider, + az_manager.get_zone(desc.zone), get_instance_directory(desc.vm_name)); } @@ -167,6 +171,7 @@ mp::VirtualMachine::UPtr mp::QemuVirtualMachineFactory::clone_vm_impl(const std: qemu_platform.get(), monitor, key_provider, + az_manager.get_zone(desc.zone), get_instance_directory(desc.vm_name), true); } diff --git a/src/platform/backends/qemu/qemu_virtual_machine_factory.h b/src/platform/backends/qemu/qemu_virtual_machine_factory.h index ff779dfd3a..9d07df6feb 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine_factory.h +++ b/src/platform/backends/qemu/qemu_virtual_machine_factory.h @@ -31,7 +31,7 @@ namespace multipass class QemuVirtualMachineFactory final : public BaseVirtualMachineFactory { public: - explicit QemuVirtualMachineFactory(const Path& data_dir); + explicit QemuVirtualMachineFactory(const Path& data_dir, AvailabilityZoneManager& az_manager); VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, const SSHKeyProvider& key_provider, @@ -51,7 +51,9 @@ class QemuVirtualMachineFactory final : public BaseVirtualMachineFactory std::string create_bridge_with(const NetworkInterfaceInfo& interface) override; private: - QemuVirtualMachineFactory(QemuPlatform::UPtr qemu_platform, const Path& data_dir); + QemuVirtualMachineFactory(QemuPlatform::UPtr qemu_platform, + const Path& data_dir, + AvailabilityZoneManager& az_manager); VirtualMachine::UPtr clone_vm_impl(const std::string& source_vm_name, const multipass::VMSpecs& src_vm_specs, const VirtualMachineDescription& desc, diff --git a/src/platform/backends/shared/CMakeLists.txt b/src/platform/backends/shared/CMakeLists.txt index 8981b6c04f..e65d52cbe4 100644 --- a/src/platform/backends/shared/CMakeLists.txt +++ b/src/platform/backends/shared/CMakeLists.txt @@ -13,6 +13,8 @@ # along with this program. If not, see . add_library(shared STATIC + base_availability_zone.cpp + base_availability_zone_manager.cpp base_snapshot.cpp base_virtual_machine.cpp base_virtual_machine_factory.cpp diff --git a/src/platform/backends/shared/base_availability_zone.cpp b/src/platform/backends/shared/base_availability_zone.cpp new file mode 100644 index 0000000000..b7b5816bf2 --- /dev/null +++ b/src/platform/backends/shared/base_availability_zone.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include +#include +#include +#include +#include + +#include + +#include + +namespace mpl = multipass::logging; + +namespace +{ +constexpr auto subnet_key = "subnet"; +constexpr auto available_key = "available"; + +[[nodiscard]] QJsonObject read_json(const multipass::fs::path& file_path, const std::string& name) +try +{ + return MP_JSONUTILS.read_object_from_file(file_path); +} +catch (const std::ios_base::failure& e) +{ + mpl::warn(name, "failed to read AZ file: {}", e.what()); + return QJsonObject{}; +} + +[[nodiscard]] std::string deserialize_subnet(const QJsonObject& json, + const multipass::fs::path& file_path, + const std::string& name) +{ + if (const auto json_subnet = json[subnet_key].toString().toStdString(); !json_subnet.empty()) + return json_subnet; + + mpl::debug(name, "subnet missing from AZ file '{}', using default", file_path); + // TODO GET ACTUAL SUBNET + return {}; +}; + +[[nodiscard]] bool deserialize_available(const QJsonObject& json, + const multipass::fs::path& file_path, + const std::string& name) +{ + if (const auto json_available = json[available_key]; json_available.isBool()) + return json_available.toBool(); + + mpl::debug(name, "availability missing from AZ file '{}', using default", file_path); + return true; +} +} // namespace + +namespace multipass +{ + +BaseAvailabilityZone::data BaseAvailabilityZone::read_from_file(const std::string& name, const fs::path& file_path) +{ + mpl::trace(name, "reading AZ from file '{}'", file_path); + + const auto json = read_json(file_path, name); + return { + // TODO remove these comments in C++20 + /*.name = */ name, + /*.file_path = */ file_path, + /*.subnet = */ deserialize_subnet(json, file_path, name), + /*.available = */ deserialize_available(json, file_path, name), + }; +} + +BaseAvailabilityZone::BaseAvailabilityZone(const std::string& name, const fs::path& az_directory) + : m{read_from_file(name, az_directory / (name + ".json"))} +{ + serialize(); +} + +const std::string& BaseAvailabilityZone::get_name() const +{ + return m.name; +} + +const std::string& BaseAvailabilityZone::get_subnet() const +{ + return m.subnet; +} + +bool BaseAvailabilityZone::is_available() const +{ + const std::unique_lock lock{m.mutex}; + return m.available; +} + +void BaseAvailabilityZone::set_available(const bool new_available) +{ + + mpl::debug(m.name, "making AZ {}available", new_available ? "" : "un"); + const std::unique_lock lock{m.mutex}; + if (m.available == new_available) + return; + + m.available = new_available; + serialize(); + + for (auto& vm : m.vms) + vm.get().set_available(m.available); +} + +void BaseAvailabilityZone::add_vm(VirtualMachine& vm) +{ + mpl::debug(m.name, "adding vm '{}' to AZ", vm.vm_name); + const std::unique_lock lock{m.mutex}; + m.vms.emplace_back(vm); +} + +void BaseAvailabilityZone::remove_vm(VirtualMachine& vm) +{ + mpl::debug(m.name, "removing vm '{}' from AZ", vm.vm_name); + const std::unique_lock lock{m.mutex}; + // as of now, we use vm names to uniquely identify vms, so we can do the same here + const auto to_remove = std::remove_if(m.vms.begin(), m.vms.end(), [&](const auto& some_vm) { + return some_vm.get().vm_name == vm.vm_name; + }); + m.vms.erase(to_remove, m.vms.end()); +} + +void BaseAvailabilityZone::serialize() const +{ + mpl::trace(m.name, "writing AZ to file '{}'", m.file_path); + const std::unique_lock lock{m.mutex}; + + const QJsonObject json{ + {subnet_key, QString::fromStdString(m.subnet)}, + {available_key, m.available}, + }; + + MP_JSONUTILS.write_json(json, QString::fromStdString(m.file_path.u8string())); +} +} // namespace multipass diff --git a/src/platform/backends/shared/base_availability_zone_manager.cpp b/src/platform/backends/shared/base_availability_zone_manager.cpp new file mode 100644 index 0000000000..9cd0df68c7 --- /dev/null +++ b/src/platform/backends/shared/base_availability_zone_manager.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace mpl = multipass::logging; + +namespace +{ +constexpr auto category = "az-manager"; +constexpr auto az_file = "az-manager.json"; +constexpr auto zones_directory_name = "zones"; +constexpr auto automatic_zone_key = "automatic_zone"; + +[[nodiscard]] auto create_default_zones(const multipass::fs::path& zones_directory) +{ + using namespace multipass; + + std::array zones{}; + std::transform(default_zone_names.begin(), default_zone_names.end(), zones.begin(), [&](const auto& zone_name) { + return std::make_unique(zone_name, zones_directory); + }); + return zones; +}; + +[[nodiscard]] QJsonObject read_json(const std::filesystem::path& file_path) +try +{ + return MP_JSONUTILS.read_object_from_file(file_path); +} +catch (const std::ios_base::failure& e) +{ + mpl::warn(category, "failed to read AZ manager file': {}", e.what()); + return QJsonObject{}; +} + +[[nodiscard]] std::string deserialize_automatic_zone(const QJsonObject& json) +{ + using namespace multipass; + + auto json_automatic_zone = json[automatic_zone_key].toString().toStdString(); + if (std::find(default_zone_names.begin(), default_zone_names.end(), json_automatic_zone) != + default_zone_names.end()) + return json_automatic_zone; + + mpl::debug(category, "automatic zone '{}' not known, using default", json_automatic_zone); + return *default_zone_names.begin(); +} +} // namespace + +namespace multipass +{ + +BaseAvailabilityZoneManager::data BaseAvailabilityZoneManager::read_from_file( + const std::filesystem::path& file_path, + const std::filesystem::path& zones_directory) +{ + mpl::debug(category, "reading AZ manager from file '{}'", file_path); + return { + // TODO remove these comments in C++20 + /*.file_path = */ file_path, + /*.zone_collection = */ + {create_default_zones(zones_directory), deserialize_automatic_zone(read_json(file_path))}, + }; +} + +BaseAvailabilityZoneManager::BaseAvailabilityZoneManager(const fs::path& data_dir) + : m{read_from_file(data_dir / az_file, data_dir / zones_directory_name)} +{ + serialize(); +} + +AvailabilityZone& BaseAvailabilityZoneManager::get_zone(const std::string& name) +{ + for (const auto& zone : zones()) + { + if (zone->get_name() == name) + return *zone; + } + throw AvailabilityZoneNotFound{name}; +} + +std::string BaseAvailabilityZoneManager::get_automatic_zone_name() +{ + const auto zone_name = m.zone_collection.next_available(); + serialize(); + return zone_name; +} + +std::vector> BaseAvailabilityZoneManager::get_zones() +{ + std::vector> zone_list; + zone_list.reserve(zones().size()); + for (auto& zone : zones()) + zone_list.emplace_back(*zone); + return zone_list; +} + +std::string BaseAvailabilityZoneManager::get_default_zone_name() const +{ + return (*zones().begin())->get_name(); +} + +void BaseAvailabilityZoneManager::serialize() const +{ + mpl::debug(category, "writing AZ manager to file '{}'", m.file_path); + const std::unique_lock lock{m.mutex}; + + const QJsonObject json{ + {automatic_zone_key, QString::fromStdString(m.zone_collection.last_used())}, + }; + + MP_JSONUTILS.write_json(json, QString::fromStdString(m.file_path.u8string())); +} + +const BaseAvailabilityZoneManager::ZoneCollection::ZoneArray& BaseAvailabilityZoneManager::zones() const +{ + return m.zone_collection.zones; +} + +BaseAvailabilityZoneManager::ZoneCollection::ZoneCollection( + std::array&& _zones, + std::string last_used) + : zones{std::move(_zones)}, automatic_zone{std::find_if(zones.begin(), zones.end(), [&last_used](const auto& zone) { + return zone->get_name() == last_used; + })} +{ +} + +std::string BaseAvailabilityZoneManager::ZoneCollection::next_available() +{ + std::unique_lock lock{mutex}; + const auto start = automatic_zone; + auto current = start; + do + { + const auto& az = *current->get(); + if (++current == zones.end()) + current = zones.begin(); + + if (az.is_available()) + { + automatic_zone = current; + return az.get_name(); + } + } while (current != start); + + throw NoAvailabilityZoneAvailable{}; +} + +std::string BaseAvailabilityZoneManager::ZoneCollection::last_used() const +{ + std::shared_lock lock{mutex}; + return automatic_zone->get()->get_name(); +} + +} // namespace multipass diff --git a/src/platform/backends/shared/base_virtual_machine.cpp b/src/platform/backends/shared/base_virtual_machine.cpp index db5c3f3832..c034bfb786 100644 --- a/src/platform/backends/shared/base_virtual_machine.cpp +++ b/src/platform/backends/shared/base_virtual_machine.cpp @@ -17,6 +17,7 @@ #include "base_virtual_machine.h" +#include #include #include #include @@ -143,16 +144,25 @@ std::optional wait_until_ssh_up_helper(mp::VirtualMachine* virtu mp::BaseVirtualMachine::BaseVirtualMachine(VirtualMachine::State state, const std::string& vm_name, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir) - : VirtualMachine{state, vm_name, instance_dir}, key_provider{key_provider} + : VirtualMachine{state, vm_name, instance_dir}, key_provider{key_provider}, zone{zone} { + zone.add_vm(*this); } mp::BaseVirtualMachine::BaseVirtualMachine(const std::string& vm_name, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir) - : VirtualMachine{vm_name, instance_dir}, key_provider{key_provider} + : VirtualMachine{vm_name, instance_dir}, key_provider{key_provider}, zone{zone} { + zone.add_vm(*this); +} + +mp::BaseVirtualMachine::~BaseVirtualMachine() +{ + mp::top_catch_all(vm_name, [this] { zone.remove_vm(*this); }); } void mp::BaseVirtualMachine::apply_extra_interfaces_and_instance_id_to_cloud_init( diff --git a/src/platform/backends/shared/base_virtual_machine.h b/src/platform/backends/shared/base_virtual_machine.h index 2ee0f9b709..38c86fa03e 100644 --- a/src/platform/backends/shared/base_virtual_machine.h +++ b/src/platform/backends/shared/base_virtual_machine.h @@ -45,11 +45,22 @@ class BaseVirtualMachine : public VirtualMachine BaseVirtualMachine(VirtualMachine::State state, const std::string& vm_name, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir); - BaseVirtualMachine(const std::string& vm_name, const SSHKeyProvider& key_provider, const Path& instance_dir); + BaseVirtualMachine(const std::string& vm_name, + const SSHKeyProvider& key_provider, + AvailabilityZone& zone, + const Path& instance_dir); + ~BaseVirtualMachine(); virtual std::string ssh_exec(const std::string& cmd, bool whisper = false) override; + void set_available(bool available) override + { + // TODO make vm unavailable by force stopping if running or available by starting again if it was running + throw NotImplementedOnThisBackendException("unavailability"); + } + void wait_until_ssh_up(std::chrono::milliseconds timeout) override; void wait_for_cloud_init(std::chrono::milliseconds timeout) override; @@ -85,6 +96,11 @@ class BaseVirtualMachine : public VirtualMachine std::vector get_childrens_names(const Snapshot* parent) const override; int get_snapshot_count() const override; + const AvailabilityZone& get_zone() const override + { + return zone; + } + protected: virtual void require_snapshots_support() const; virtual std::shared_ptr make_specific_snapshot(const QString& filename); @@ -151,6 +167,7 @@ class BaseVirtualMachine : public VirtualMachine protected: const SSHKeyProvider& key_provider; + AvailabilityZone& zone; private: std::optional ssh_session = std::nullopt; diff --git a/src/platform/backends/shared/base_virtual_machine_factory.cpp b/src/platform/backends/shared/base_virtual_machine_factory.cpp index 480fcce202..42a9b09e86 100644 --- a/src/platform/backends/shared/base_virtual_machine_factory.cpp +++ b/src/platform/backends/shared/base_virtual_machine_factory.cpp @@ -31,7 +31,8 @@ namespace mpu = multipass::utils; const mp::Path mp::BaseVirtualMachineFactory::instances_subdir = "vault/instances"; -mp::BaseVirtualMachineFactory::BaseVirtualMachineFactory(const Path& instances_dir) : instances_dir{instances_dir} {}; +mp::BaseVirtualMachineFactory::BaseVirtualMachineFactory(const Path& instances_dir, AvailabilityZoneManager& az_manager) + : az_manager{az_manager}, instances_dir{instances_dir} {}; void mp::BaseVirtualMachineFactory::configure(VirtualMachineDescription& vm_desc) { @@ -109,6 +110,7 @@ mp::VirtualMachine::UPtr mp::BaseVirtualMachineFactory::clone_bare_vm(const VMSp dest_spec.mem_size, dest_spec.disk_space, dest_name, + dest_spec.zone, dest_spec.default_mac_address, dest_spec.extra_interfaces, dest_spec.ssh_username, @@ -119,9 +121,7 @@ mp::VirtualMachine::UPtr mp::BaseVirtualMachineFactory::clone_bare_vm(const VMSp {}, {}}; - mp::VirtualMachine::UPtr cloned_instance = clone_vm_impl(src_name, src_spec, dest_vm_desc, monitor, key_provider); - - return cloned_instance; + return clone_vm_impl(src_name, src_spec, dest_vm_desc, monitor, key_provider); } void mp::BaseVirtualMachineFactory::copy_instance_dir_with_essential_files(const fs::path& source_instance_dir_path, diff --git a/src/platform/backends/shared/base_virtual_machine_factory.h b/src/platform/backends/shared/base_virtual_machine_factory.h index 880c2abf0d..9b16034291 100644 --- a/src/platform/backends/shared/base_virtual_machine_factory.h +++ b/src/platform/backends/shared/base_virtual_machine_factory.h @@ -18,6 +18,7 @@ #ifndef MULTIPASS_BASE_VIRTUAL_MACHINE_FACTORY_H #define MULTIPASS_BASE_VIRTUAL_MACHINE_FACTORY_H +#include #include #include #include @@ -34,7 +35,7 @@ constexpr auto log_category = "base factory"; class BaseVirtualMachineFactory : public VirtualMachineFactory { public: - explicit BaseVirtualMachineFactory(const Path& instances_dir); + explicit BaseVirtualMachineFactory(const Path& instances_dir, AvailabilityZoneManager& az_manager); VirtualMachine::UPtr clone_bare_vm(const VMSpecs& src_spec, const VMSpecs& dest_spec, const std::string& src_name, @@ -83,6 +84,7 @@ class BaseVirtualMachineFactory : public VirtualMachineFactory protected: static const Path instances_subdir; + AvailabilityZoneManager& az_manager; protected: std::string create_bridge_with(const NetworkInterfaceInfo& interface) override diff --git a/src/platform/backends/virtualbox/virtualbox_virtual_machine.cpp b/src/platform/backends/virtualbox/virtualbox_virtual_machine.cpp index 52d6d192a3..7b63cf382a 100644 --- a/src/platform/backends/virtualbox/virtualbox_virtual_machine.cpp +++ b/src/platform/backends/virtualbox/virtualbox_virtual_machine.cpp @@ -179,8 +179,9 @@ void update_mac_addresses_of_network_adapters(const mp::VirtualMachineDescriptio mp::VirtualBoxVirtualMachine::VirtualBoxVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const mp::Path& instance_dir_qstr) - : VirtualBoxVirtualMachine(desc, monitor, key_provider, instance_dir_qstr, true) + : VirtualBoxVirtualMachine(desc, monitor, key_provider, zone, instance_dir_qstr, true) { if (desc.extra_interfaces.size() > 7) { @@ -256,8 +257,9 @@ mp::VirtualBoxVirtualMachine::VirtualBoxVirtualMachine(const std::string& source const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& dest_instance_dir) - : VirtualBoxVirtualMachine(desc, monitor, key_provider, dest_instance_dir, true) + : VirtualBoxVirtualMachine(desc, monitor, key_provider, zone, dest_instance_dir, true) { const fs::path instances_dir = fs::path{dest_instance_dir.toStdString()}.parent_path(); @@ -321,9 +323,10 @@ mp::VirtualBoxVirtualMachine::VirtualBoxVirtualMachine(const std::string& source mp::VirtualBoxVirtualMachine::VirtualBoxVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const mp::Path& instance_dir_qstr, bool /*is_internal*/) - : BaseVirtualMachine{desc.vm_name, key_provider, instance_dir_qstr}, + : BaseVirtualMachine{desc.vm_name, key_provider, zone, instance_dir_qstr}, desc{desc}, name{QString::fromStdString(desc.vm_name)}, monitor{&monitor} diff --git a/src/platform/backends/virtualbox/virtualbox_virtual_machine.h b/src/platform/backends/virtualbox/virtualbox_virtual_machine.h index b8fc12cfff..4eb0aee692 100644 --- a/src/platform/backends/virtualbox/virtualbox_virtual_machine.h +++ b/src/platform/backends/virtualbox/virtualbox_virtual_machine.h @@ -39,12 +39,14 @@ class VirtualBoxVirtualMachine final : public BaseVirtualMachine VirtualBoxVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir); // Contruct the vm based on the source virtual machine VirtualBoxVirtualMachine(const std::string& source_vm_name, const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& dest_instance_dir); ~VirtualBoxVirtualMachine() override; @@ -80,6 +82,7 @@ class VirtualBoxVirtualMachine final : public BaseVirtualMachine VirtualBoxVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir_qstr, bool is_internal); void remove_snapshots_from_backend() const; diff --git a/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.cpp b/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.cpp index f868819a9b..d7aeed16e2 100644 --- a/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.cpp +++ b/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.cpp @@ -108,8 +108,10 @@ mp::NetworkInterfaceInfo list_vbox_network(const QString& vbox_iface_info, } } // namespace -mp::VirtualBoxVirtualMachineFactory::VirtualBoxVirtualMachineFactory(const mp::Path& data_dir) - : BaseVirtualMachineFactory(MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir)) +mp::VirtualBoxVirtualMachineFactory::VirtualBoxVirtualMachineFactory(const mp::Path& data_dir, + AvailabilityZoneManager& az_manager) + : BaseVirtualMachineFactory(MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir), + az_manager) { } @@ -120,6 +122,7 @@ auto mp::VirtualBoxVirtualMachineFactory::create_virtual_machine(const VirtualMa return std::make_unique(desc, monitor, key_provider, + az_manager.get_zone(desc.zone), get_instance_directory(desc.vm_name)); } @@ -257,5 +260,6 @@ mp::VirtualMachine::UPtr mp::VirtualBoxVirtualMachineFactory::clone_vm_impl( dest_vm_desc, monitor, key_provider, + az_manager.get_zone(dest_vm_desc.zone), get_instance_directory(dest_vm_desc.vm_name)); } diff --git a/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.h b/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.h index e54cd87d6d..115001d9e6 100644 --- a/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.h +++ b/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.h @@ -25,7 +25,7 @@ namespace multipass class VirtualBoxVirtualMachineFactory final : public BaseVirtualMachineFactory { public: - explicit VirtualBoxVirtualMachineFactory(const Path& data_dir); + explicit VirtualBoxVirtualMachineFactory(const Path& data_dir, AvailabilityZoneManager& az_manager); VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, const SSHKeyProvider& key_provider, diff --git a/src/platform/platform_linux.cpp b/src/platform/platform_linux.cpp index 7ecd648d18..f6f8cc4614 100644 --- a/src/platform/platform_linux.cpp +++ b/src/platform/platform_linux.cpp @@ -408,19 +408,19 @@ std::string mp::platform::default_server_address() return "unix:" + base_dir + "/multipass_socket"; } -mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_dir) +mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_dir, AvailabilityZoneManager& az_manager) { const auto& driver = MP_SETTINGS.get(mp::driver_key); #ifdef QEMU_ENABLED if (driver == QStringLiteral("qemu")) - return std::make_unique(data_dir); + return std::make_unique(data_dir, az_manager); #endif if (driver == QStringLiteral("libvirt")) - return std::make_unique(data_dir); + return std::make_unique(data_dir, az_manager); if (driver == QStringLiteral("lxd")) - return std::make_unique(data_dir); + return std::make_unique(data_dir, az_manager); #if VIRTUALBOX_ENABLED if (driver == QStringLiteral("virtualbox")) diff --git a/src/platform/platform_osx.cpp b/src/platform/platform_osx.cpp index fc2a90ceda..e0ba97c42d 100644 --- a/src/platform/platform_osx.cpp +++ b/src/platform/platform_osx.cpp @@ -267,7 +267,7 @@ QString mp::platform::Platform::daemon_config_home() const // temporary return ret; } -mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_dir) +mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_dir, AvailabilityZoneManager& az_manager) { auto driver = MP_SETTINGS.get(mp::driver_key); @@ -280,13 +280,13 @@ mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_di there. */ - return std::make_unique(data_dir); + return std::make_unique(data_dir, az_manager); #endif } else if (driver == QStringLiteral("qemu")) { #if QEMU_ENABLED - return std::make_unique(data_dir); + return std::make_unique(data_dir, az_manager); #endif } diff --git a/src/platform/platform_win.cpp b/src/platform/platform_win.cpp index de6d732e3f..3311e9377b 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -599,12 +599,12 @@ QString mp::platform::Platform::daemon_config_home() const // temporary } } -mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_dir) +mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_dir, AvailabilityZoneManager& az_manager) { const auto driver = MP_SETTINGS.get(mp::driver_key); if (driver == QStringLiteral("hyperv")) - return std::make_unique(data_dir); + return std::make_unique(data_dir, az_manager); else if (driver == QStringLiteral("virtualbox")) { qputenv("Path", qgetenv("Path") + ";C:\\Program Files\\Oracle\\VirtualBox"); /* @@ -613,7 +613,7 @@ mp::VirtualMachineFactory::UPtr mp::platform::vm_backend(const mp::Path& data_di there. */ - return std::make_unique(data_dir); + return std::make_unique(data_dir, az_manager); } throw std::runtime_error("Invalid virtualization driver set in the environment"); diff --git a/src/utils/json_utils.cpp b/src/utils/json_utils.cpp index 0603c22eb8..90bbba8529 100644 --- a/src/utils/json_utils.cpp +++ b/src/utils/json_utils.cpp @@ -17,6 +17,7 @@ * */ +#include #include #include #include @@ -55,6 +56,14 @@ void mp::JsonUtils::write_json(const QJsonObject& root, QString file_name) const throw std::runtime_error{fmt::format("Could not commit transactional file; filename: {}", file_name)}; } +QJsonObject mp::JsonUtils::read_object_from_file(const std::filesystem::path& file_path) const +{ + const auto file = MP_FILEOPS.open_read(file_path); + file->exceptions(std::ifstream::failbit | std::ifstream::badbit); + const auto data = QString::fromStdString(std::string{std::istreambuf_iterator{*file}, {}}).toUtf8(); + return QJsonDocument::fromJson(data).object(); +} + std::string mp::JsonUtils::json_to_string(const QJsonObject& root) const { // The function name toJson() is shockingly wrong, for it converts an actual JsonDocument to a QByteArray. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7d047e1d1c..0acb6aed6d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -54,6 +54,8 @@ add_executable(multipass_tests temp_file.cpp test_alias_dict.cpp test_argparser.cpp + test_base_availability_zone.cpp + test_base_availability_zone_manager.cpp test_base_snapshot.cpp test_base_virtual_machine.cpp test_base_virtual_machine_factory.cpp diff --git a/tests/daemon_test_fixture.cpp b/tests/daemon_test_fixture.cpp index bf4193d805..445de6805e 100644 --- a/tests/daemon_test_fixture.cpp +++ b/tests/daemon_test_fixture.cpp @@ -23,6 +23,7 @@ #include "mock_cert_provider.h" #include "mock_server_reader_writer.h" #include "mock_standard_paths.h" +#include "stub_availability_zone_manager.h" #include "stub_cert_store.h" #include "stub_image_host.h" #include "stub_logger.h" @@ -309,11 +310,14 @@ class TestClient : public multipass::Client mpt::DaemonTestFixture::DaemonTestFixture() { + auto az_manager = std::make_unique(); + config_builder.server_address = server_address; config_builder.cache_directory = cache_dir.path(); config_builder.data_directory = data_dir.path(); config_builder.vault = std::make_unique(); - config_builder.factory = std::make_unique(); + config_builder.factory = std::make_unique(*az_manager); + config_builder.az_manager = std::move(az_manager); config_builder.image_hosts.push_back(std::make_unique()); config_builder.ssh_key_provider = std::make_unique(); config_builder.cert_provider = std::make_unique>(); diff --git a/tests/hyperv/test_hyperv_backend.cpp b/tests/hyperv/test_hyperv_backend.cpp index baf42dd618..e194f077da 100644 --- a/tests/hyperv/test_hyperv_backend.cpp +++ b/tests/hyperv/test_hyperv_backend.cpp @@ -19,6 +19,8 @@ #include "tests/mock_logger.h" #include "tests/mock_platform.h" #include "tests/mock_process_factory.h" +#include "tests/stub_availability_zone.h" +#include "tests/stub_availability_zone_manager.h" #include "tests/stub_ssh_key_provider.h" #include "tests/stub_status_monitor.h" #include "tests/temp_dir.h" @@ -124,6 +126,7 @@ struct HyperVBackend : public Test mp::MemorySize{"3M"}, mp::MemorySize{}, // not used, "pied-piper-valley", + "zone1", "ba:ba:ca:ca:ca:ba", {}, "", @@ -131,7 +134,9 @@ struct HyperVBackend : public Test dummy_cloud_init_iso.name()}; mpt::MockLogger::Scope logger_scope = mpt::MockLogger::inject(); mpt::PowerShellTestHelper ps_helper; - mp::HyperVVirtualMachineFactory backend{data_dir.path()}; + mpt::StubAvailabilityZone zone{}; + mpt::StubAvailabilityZoneManager az_manager{}; + mp::HyperVVirtualMachineFactory backend{data_dir.path(), az_manager}; mpt::StubVMStatusMonitor stub_monitor; mpt::StubSSHKeyProvider stub_key_provider; }; @@ -382,7 +387,9 @@ struct HyperVNetworks : public Test mpt::MockPlatform::GuardedMock attr = mpt::MockPlatform::inject(); mpt::MockPlatform* mock_platform = attr.first; mpt::TempDir data_dir; - mp::HyperVVirtualMachineFactory backend{data_dir.path()}; + mpt::StubAvailabilityZone zone{}; + mpt::StubAvailabilityZoneManager az_manager{}; + mp::HyperVVirtualMachineFactory backend{data_dir.path(), az_manager}; }; struct HyperVNetworksPS : public HyperVNetworks diff --git a/tests/libvirt/test_libvirt_backend.cpp b/tests/libvirt/test_libvirt_backend.cpp index 54df51bcef..e1c2b224ea 100644 --- a/tests/libvirt/test_libvirt_backend.cpp +++ b/tests/libvirt/test_libvirt_backend.cpp @@ -21,6 +21,7 @@ #include "tests/mock_logger.h" #include "tests/mock_ssh.h" #include "tests/mock_status_monitor.h" +#include "tests/stub_availability_zone_manager.h" #include "tests/stub_ssh_key_provider.h" #include "tests/stub_status_monitor.h" #include "tests/temp_dir.h" @@ -52,6 +53,7 @@ struct LibVirtBackend : public Test "pied-piper-valley", "", {}, + {}, "ubuntu", {dummy_image.name(), "", "", "", "", {}}, dummy_cloud_init_iso.name(), @@ -61,6 +63,7 @@ struct LibVirtBackend : public Test {}}; mpt::TempDir data_dir; mpt::StubSSHKeyProvider key_provider; + mpt::StubAvailabilityZoneManager az_manager{}; // This indicates that LibvirtWrapper should open the test executable std::string fake_libvirt_path{""}; @@ -81,7 +84,7 @@ TEST_F(LibVirtBackend, health_check_good_does_not_throw) EXPECT_CALL(*mock_backend, check_for_kvm_support()).WillOnce(Return()); EXPECT_CALL(*mock_backend, check_if_kvm_is_in_use()).WillOnce(Return()); - mp::LibVirtVirtualMachineFactory backend(data_dir.path(), fake_libvirt_path); + mp::LibVirtVirtualMachineFactory backend(data_dir.path(), fake_libvirt_path, az_manager); EXPECT_NO_THROW(backend.hypervisor_health_check()); } @@ -96,7 +99,7 @@ TEST_F(LibVirtBackend, health_check_failed_connection_throws) auto virGetLastErrorMessage = [&error_msg] { return error_msg.c_str(); }; static auto static_virGetLastErrorMessage = virGetLastErrorMessage; - mp::LibVirtVirtualMachineFactory backend(data_dir.path(), fake_libvirt_path); + mp::LibVirtVirtualMachineFactory backend(data_dir.path(), fake_libvirt_path, az_manager); backend.libvirt_wrapper->virConnectOpen = [](auto...) -> virConnectPtr { return nullptr; }; backend.libvirt_wrapper->virGetLastErrorMessage = [] { return static_virGetLastErrorMessage(); }; @@ -108,7 +111,7 @@ TEST_F(LibVirtBackend, health_check_failed_connection_throws) TEST_F(LibVirtBackend, creates_in_off_state) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; mpt::StubVMStatusMonitor stub_monitor; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -118,7 +121,7 @@ TEST_F(LibVirtBackend, creates_in_off_state) TEST_F(LibVirtBackend, creates_in_suspended_state_with_managed_save) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virDomainHasManagedSaveImage = [](auto...) { return 1; }; mpt::StubVMStatusMonitor stub_monitor; @@ -132,7 +135,7 @@ TEST_F(LibVirtBackend, machine_sends_monitoring_events) REPLACE(ssh_connect, [](auto...) { return SSH_OK; }); REPLACE(ssh_userauth_publickey, [](auto...) { return SSH_AUTH_SUCCESS; }); - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virNetworkGetDHCPLeases = [](auto, auto, auto leases, auto) { virNetworkDHCPLeasePtr* leases_ret; leases_ret = (virNetworkDHCPLeasePtr*)calloc(1, sizeof(virNetworkDHCPLeasePtr)); @@ -165,7 +168,7 @@ TEST_F(LibVirtBackend, machine_sends_monitoring_events) TEST_F(LibVirtBackend, machine_persists_and_sets_state_on_start) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; NiceMock mock_monitor; auto machine = backend.create_virtual_machine(default_description, key_provider, mock_monitor); @@ -182,7 +185,7 @@ TEST_F(LibVirtBackend, machine_persists_and_sets_state_on_start) TEST_F(LibVirtBackend, machine_persists_and_sets_state_on_shutdown) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virDomainGetState = [](auto, auto state, auto, auto) { *state = VIR_DOMAIN_RUNNING; return 0; @@ -203,7 +206,7 @@ TEST_F(LibVirtBackend, machine_persists_and_sets_state_on_shutdown) TEST_F(LibVirtBackend, machine_persists_and_sets_state_on_suspend) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virDomainGetState = [](auto, auto state, auto, auto) { *state = VIR_DOMAIN_RUNNING; return 0; @@ -226,7 +229,7 @@ TEST_F(LibVirtBackend, machine_persists_and_sets_state_on_suspend) TEST_F(LibVirtBackend, start_with_broken_libvirt_connection_throws) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virConnectOpen = [](auto...) -> virConnectPtr { return nullptr; }; NiceMock mock_monitor; @@ -239,7 +242,7 @@ TEST_F(LibVirtBackend, start_with_broken_libvirt_connection_throws) TEST_F(LibVirtBackend, shutdown_with_broken_libvirt_connection_throws) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virConnectOpen = [](auto...) -> virConnectPtr { return nullptr; }; NiceMock mock_monitor; @@ -252,7 +255,7 @@ TEST_F(LibVirtBackend, shutdown_with_broken_libvirt_connection_throws) TEST_F(LibVirtBackend, suspend_with_broken_libvirt_connection_throws) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virConnectOpen = [](auto...) -> virConnectPtr { return nullptr; }; NiceMock mock_monitor; @@ -265,7 +268,7 @@ TEST_F(LibVirtBackend, suspend_with_broken_libvirt_connection_throws) TEST_F(LibVirtBackend, current_state_with_broken_libvirt_unknown) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virConnectOpen = [](auto...) -> virConnectPtr { return nullptr; }; NiceMock mock_monitor; @@ -276,7 +279,7 @@ TEST_F(LibVirtBackend, current_state_with_broken_libvirt_unknown) TEST_F(LibVirtBackend, current_state_delayed_shutdown_domain_running) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virDomainGetState = [](auto, auto state, auto, auto) { *state = VIR_DOMAIN_RUNNING; return 0; @@ -291,7 +294,7 @@ TEST_F(LibVirtBackend, current_state_delayed_shutdown_domain_running) TEST_F(LibVirtBackend, current_state_delayed_shutdown_domain_off) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; NiceMock mock_monitor; auto machine = backend.create_virtual_machine(default_description, key_provider, mock_monitor); machine->state = mp::VirtualMachine::State::delayed_shutdown; @@ -301,7 +304,7 @@ TEST_F(LibVirtBackend, current_state_delayed_shutdown_domain_off) TEST_F(LibVirtBackend, current_state_off_domain_starts_running) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; NiceMock mock_monitor; auto machine = backend.create_virtual_machine(default_description, key_provider, mock_monitor); @@ -317,7 +320,7 @@ TEST_F(LibVirtBackend, current_state_off_domain_starts_running) TEST_F(LibVirtBackend, returns_version_string) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virConnectGetVersion = [](virConnectPtr, unsigned long* hvVer) { *hvVer = 1002003; return 0; @@ -328,7 +331,7 @@ TEST_F(LibVirtBackend, returns_version_string) TEST_F(LibVirtBackend, returns_version_string_when_error) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virConnectGetVersion = [](auto...) { return -1; }; EXPECT_EQ(backend.get_backend_version_string(), "libvirt-unknown"); @@ -336,7 +339,7 @@ TEST_F(LibVirtBackend, returns_version_string_when_error) TEST_F(LibVirtBackend, returns_version_string_when_lacking_capabilities) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; EXPECT_EQ(backend.get_backend_version_string(), "libvirt-unknown"); } @@ -353,7 +356,7 @@ TEST_F(LibVirtBackend, returns_version_string_when_failed_connecting) // to a pointer to a function. static auto static_virConnectGetVersion = virConnectGetVersion; - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virConnectOpen = [](auto...) -> virConnectPtr { return nullptr; }; backend.libvirt_wrapper->virConnectGetVersion = [](virConnectPtr conn, long unsigned int* hwVer) { return static_virConnectGetVersion(conn, hwVer); @@ -366,7 +369,7 @@ TEST_F(LibVirtBackend, returns_version_string_when_failed_connecting) TEST_F(LibVirtBackend, ssh_hostname_returns_expected_value) { mpt::StubVMStatusMonitor stub_monitor; - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; const std::string expected_ip{"10.10.0.34"}; auto virNetworkGetDHCPLeases = [&expected_ip](auto, auto, auto leases, auto) { @@ -400,7 +403,7 @@ TEST_F(LibVirtBackend, ssh_hostname_returns_expected_value) TEST_F(LibVirtBackend, ssh_hostname_timeout_throws_and_sets_unknown_state) { mpt::StubVMStatusMonitor stub_monitor; - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; backend.libvirt_wrapper->virNetworkGetDHCPLeases = [](auto...) { return 0; }; @@ -419,7 +422,7 @@ TEST_F(LibVirtBackend, ssh_hostname_timeout_throws_and_sets_unknown_state) TEST_F(LibVirtBackend, shutdown_while_starting_throws_and_sets_correct_state) { mpt::StubVMStatusMonitor stub_monitor; - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; auto destroy_called{false}; auto virDomainDestroy = [&backend, &destroy_called](auto...) { @@ -462,7 +465,7 @@ TEST_F(LibVirtBackend, shutdown_while_starting_throws_and_sets_correct_state) TEST_F(LibVirtBackend, machineInOffStateLogsAndIgnoresShutdown) { - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; NiceMock mock_monitor; auto machine = backend.create_virtual_machine(default_description, key_provider, mock_monitor); @@ -485,7 +488,7 @@ TEST_F(LibVirtBackend, machineNoForceCannotShutdownLogsAndThrows) { const std::string error_msg{"Not working"}; - mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path}; + mp::LibVirtVirtualMachineFactory backend{data_dir.path(), fake_libvirt_path, az_manager}; NiceMock mock_monitor; auto machine = backend.create_virtual_machine(default_description, key_provider, mock_monitor); @@ -510,7 +513,7 @@ TEST_F(LibVirtBackend, machineNoForceCannotShutdownLogsAndThrows) TEST_F(LibVirtBackend, lists_no_networks) { - mp::LibVirtVirtualMachineFactory backend(data_dir.path(), fake_libvirt_path); + mp::LibVirtVirtualMachineFactory backend(data_dir.path(), fake_libvirt_path, az_manager); EXPECT_THROW(backend.networks(), mp::NotImplementedOnThisBackendException); } diff --git a/tests/linux/test_platform_linux.cpp b/tests/linux/test_platform_linux.cpp index 6c51db0b7f..c33ddb5865 100644 --- a/tests/linux/test_platform_linux.cpp +++ b/tests/linux/test_platform_linux.cpp @@ -57,6 +57,7 @@ #include #include +#include namespace mp = multipass; namespace mpt = multipass::test; @@ -79,9 +80,10 @@ struct PlatformLinux : public mpt::TestWithMockedBinPath auto factory = mpt::MockProcessFactory::Inject(); setup_driver_settings(driver); + mpt::StubAvailabilityZoneManager az_manager{}; - decltype(mp::platform::vm_backend("")) factory_ptr; - EXPECT_NO_THROW(factory_ptr = mp::platform::vm_backend(backend_path);); + decltype(mp::platform::vm_backend("", az_manager)) factory_ptr; + EXPECT_NO_THROW(factory_ptr = mp::platform::vm_backend(backend_path, az_manager);); EXPECT_TRUE(dynamic_cast(factory_ptr.get())); } @@ -206,7 +208,8 @@ TEST_P(TestUnsupportedDrivers, test_unsupported_driver) ASSERT_FALSE(MP_PLATFORM.is_backend_supported(GetParam())); setup_driver_settings(GetParam()); - EXPECT_THROW(mp::platform::vm_backend(backend_path), std::runtime_error); + mpt::StubAvailabilityZoneManager az_manager{}; + EXPECT_THROW(mp::platform::vm_backend(backend_path, az_manager), std::runtime_error); } INSTANTIATE_TEST_SUITE_P(PlatformLinux, TestUnsupportedDrivers, diff --git a/tests/lxd/test_lxd_backend.cpp b/tests/lxd/test_lxd_backend.cpp index 60e6a69957..23a2b0fc0e 100644 --- a/tests/lxd/test_lxd_backend.cpp +++ b/tests/lxd/test_lxd_backend.cpp @@ -25,6 +25,8 @@ #include "tests/mock_logger.h" #include "tests/mock_platform.h" #include "tests/mock_status_monitor.h" +#include "tests/stub_availability_zone.h" +#include "tests/stub_availability_zone_manager.h" #include "tests/stub_ssh_key_provider.h" #include "tests/stub_status_monitor.h" #include "tests/stub_url_downloader.h" @@ -70,6 +72,7 @@ struct LXDBackend : public Test mp::MemorySize{"3M"}, mp::MemorySize{}, // not used "pied-piper-valley", + "zone1", "00:16:3e:fe:f2:b9", {}, "yoda", @@ -84,6 +87,8 @@ struct LXDBackend : public Test mpt::TempDir data_dir; mpt::TempDir instance_dir; mpt::StubSSHKeyProvider key_provider; + mpt::StubAvailabilityZoneManager az_manager{}; + mpt::StubAvailabilityZone zone{}; std::unique_ptr> mock_network_access_manager; QUrl base_url{"unix:///foo@1.0"}; const QString default_storage_pool{"default"}; @@ -168,7 +173,7 @@ TEST_F(LXDBackend, createsProjectStoragePoolAndNetworkOnHealthcheck) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; backend.hypervisor_health_check(); @@ -227,7 +232,7 @@ TEST_F(LXDBackend, usesDefaultStoragePoolWhenItExistsAndNoMultipassPool) logger_scope.mock_logger->expect_log(mpl::Level::debug, "Using the \'default\' storage pool."); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; backend.hypervisor_health_check(); @@ -284,7 +289,7 @@ TEST_F(LXDBackend, usesMultipassStoragePoolWhenItExists) logger_scope.mock_logger->expect_log(mpl::Level::debug, "Using the \'multipass\' storage pool."); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; backend.hypervisor_health_check(); @@ -352,7 +357,7 @@ TEST_F(LXDBackend, usesMultipassPoolWhenDefaultPoolExists) logger_scope.mock_logger->expect_log(mpl::Level::debug, "Using the \'multipass\' storage pool."); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; backend.hypervisor_health_check(); } @@ -381,7 +386,7 @@ TEST_F(LXDBackend, factory_creates_valid_virtual_machine_ptr) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -396,7 +401,7 @@ TEST_F(LXDBackend, factory_creates_expected_image_vault) mpt::TempDir data_dir; std::vector hosts; - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; auto vault = backend.create_image_vault(hosts, &stub_downloader, cache_dir.path(), data_dir.path(), mp::days{0}); @@ -408,7 +413,7 @@ TEST_F(LXDBackend, factory_does_nothing_on_configure) mpt::TempDir data_dir; mp::VirtualMachineDescription vm_desc{default_description}; - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; backend.configure(vm_desc); @@ -455,6 +460,7 @@ TEST_F(LXDBackend, creates_in_stopped_state) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; EXPECT_TRUE(vm_created); @@ -509,6 +515,7 @@ TEST_F(LXDBackend, machine_persists_and_sets_state_on_start) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; EXPECT_CALL(mock_monitor, persist_state_for(_, _)).Times(2); @@ -565,6 +572,7 @@ TEST_F(LXDBackend, machine_persists_and_sets_state_on_shutdown) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; EXPECT_CALL(mock_monitor, persist_state_for(_, _)).Times(2); @@ -616,6 +624,7 @@ TEST_F(LXDBackend, machine_persists_internal_stopped_state_on_destruction) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; ASSERT_EQ(machine.state, mp::VirtualMachine::State::running); @@ -678,6 +687,7 @@ TEST_F(LXDBackend, machine_does_not_update_state_in_dtor) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; } @@ -740,6 +750,7 @@ TEST_F(LXDBackend, machineLogsNotFoundExceptionInDtor) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; machine.shutdown(); } @@ -795,6 +806,7 @@ TEST_F(LXDBackend, does_not_call_stop_when_snap_refresh_is_detected) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; } @@ -846,6 +858,7 @@ TEST_F(LXDBackend, calls_stop_when_snap_refresh_does_not_exist) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; } @@ -936,12 +949,13 @@ TEST_F(LXDBackend, posts_expected_data_when_creating_instance) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; } TEST_F(LXDBackend, prepare_source_image_does_not_modify) { - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; const mp::VMImage original_image{"/path/to/image", "deadbeef", "bin", "baz", "the past", {"fee", "fi", "fo", "fum"}}; @@ -992,7 +1006,7 @@ TEST_F(LXDBackend, returns_expected_backend_string) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; const QString backend_string{"lxd-4.3"}; @@ -1001,7 +1015,7 @@ TEST_F(LXDBackend, returns_expected_backend_string) TEST_F(LXDBackend, unimplemented_functions_logs_trace_message) { - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; const std::string name{"foo"}; @@ -1022,7 +1036,7 @@ TEST_F(LXDBackend, unimplemented_functions_logs_trace_message) TEST_F(LXDBackend, factoryDoesNotSupportSuspend) { - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; MP_EXPECT_THROW_THAT(backend.require_suspend_support(), mp::NotImplementedOnThisBackendException, mpt::match_what(HasSubstr("suspend"))); @@ -1030,7 +1044,7 @@ TEST_F(LXDBackend, factoryDoesNotSupportSuspend) TEST_F(LXDBackend, image_fetch_type_returns_expected_type) { - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; EXPECT_EQ(backend.fetch_type(), mp::FetchType::ImageOnly); } @@ -1069,7 +1083,7 @@ TEST_F(LXDBackend, healthcheck_throws_when_untrusted) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; MP_EXPECT_THROW_THAT(backend.hypervisor_health_check(), std::runtime_error, mpt::match_what(StrEq("Failed to authenticate to LXD."))); @@ -1084,7 +1098,7 @@ TEST_F(LXDBackend, healthcheck_connection_refused_error_throws_with_expected_mes throw mp::LocalSocketConnectionException(exception_message); }); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; MP_EXPECT_THROW_THAT(backend.hypervisor_health_check(), std::runtime_error, mpt::match_what(StrEq(fmt::format("{}\n\nPlease ensure the LXD snap is installed and enabled.", @@ -1100,7 +1114,7 @@ TEST_F(LXDBackend, healthcheck_unknown_server_error_throws_with_expected_message throw mp::LocalSocketConnectionException(exception_message); }); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; MP_EXPECT_THROW_THAT(backend.hypervisor_health_check(), std::runtime_error, mpt::match_what(StrEq(fmt::format("{}\n\nPlease ensure the LXD snap is installed and enabled.", @@ -1111,7 +1125,7 @@ TEST_F(LXDBackend, healthcheck_error_advises_snap_connections_when_in_snap) { EXPECT_CALL(*mock_network_access_manager, createRequest).WillOnce(Throw(mp::LocalSocketConnectionException(""))); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; mpt::SetEnvScope env{"SNAP_NAME", "multipass"}; MP_EXPECT_THROW_THAT(backend.hypervisor_health_check(), std::runtime_error, @@ -1162,6 +1176,7 @@ TEST_P(LXDNetworkInfoSuite, returns_expected_network_info) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; EXPECT_EQ(machine.management_ipv4(), "10.217.27.168"); @@ -1213,6 +1228,7 @@ TEST_F(LXDBackend, ssh_hostname_timeout_throws_and_sets_unknown_state) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; EXPECT_THROW(machine.ssh_hostname(std::chrono::milliseconds(1)), std::runtime_error); @@ -1257,6 +1273,7 @@ TEST_F(LXDBackend, no_ip_address_returns_unknown) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; EXPECT_EQ(machine.management_ipv4(), "UNKNOWN"); @@ -1592,6 +1609,7 @@ TEST_F(LXDBackend, unsupported_suspend_throws) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; MP_EXPECT_THROW_THAT(machine.suspend(), @@ -1632,6 +1650,7 @@ TEST_F(LXDBackend, start_while_frozen_unfreezes) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; EXPECT_CALL(*logger_scope.mock_logger, @@ -1665,6 +1684,7 @@ TEST_F(LXDBackend, shutdown_while_stopped_does_nothing_and_logs_debug) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; ASSERT_EQ(machine.current_state(), mp::VirtualMachine::State::stopped); @@ -1704,6 +1724,7 @@ TEST_F(LXDBackend, shutdown_while_frozen_throws_and_logs_info) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; ASSERT_EQ(machine.current_state(), mp::VirtualMachine::State::suspended); @@ -1758,6 +1779,7 @@ TEST_F(LXDBackend, ensure_vm_running_does_not_throw_starting) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; machine.start(); @@ -1817,6 +1839,7 @@ TEST_F(LXDBackend, shutdown_while_starting_throws_and_sets_correct_state) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; machine.start(); @@ -1878,6 +1901,7 @@ TEST_F(LXDBackend, start_failure_while_starting_throws_and_sets_correct_state) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; machine.start(); @@ -1938,6 +1962,7 @@ TEST_F(LXDBackend, reboots_while_starting_does_not_throw_and_sets_correct_state) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; machine.start(); @@ -1967,6 +1992,7 @@ TEST_F(LXDBackend, current_state_connection_error_logs_warning_and_sets_unknown_ bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; EXPECT_CALL(*logger_scope.mock_logger, @@ -2026,6 +2052,7 @@ TEST_P(LXDInstanceStatusTestSuite, lxd_state_returns_expected_VirtualMachine_sta bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; EXPECT_EQ(machine.current_state(), expected_state); @@ -2054,7 +2081,7 @@ TEST_F(LXDBackend, requests_networks) createRequest(QNetworkAccessManager::CustomOperation, network_request_matcher, _)) .WillOnce(Return(new mpt::MockLocalSocketReply{mpt::networks_empty_data})); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; EXPECT_THAT(backend.networks(), IsEmpty()); } @@ -2070,7 +2097,7 @@ TEST_P(LXDNetworksBadJson, handles_gibberish_networks_reply) createRequest(QNetworkAccessManager::CustomOperation, network_request_matcher, _)) .WillOnce(Return(new mpt::MockLocalSocketReply{GetParam()})); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; EXPECT_THROW(backend.networks(), std::runtime_error); } @@ -2088,7 +2115,7 @@ TEST_P(LXDNetworksBadFields, ignores_network_without_expected_fields) createRequest(QNetworkAccessManager::CustomOperation, network_request_matcher, _)) .WillOnce(Return(new mpt::MockLocalSocketReply{GetParam()})); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; EXPECT_THAT(backend.networks(), IsEmpty()); } @@ -2111,7 +2138,7 @@ TEST_P(LXDNetworksOnlySupportedTypes, reports_only_bridge_and_ethernet_networks) createRequest(QNetworkAccessManager::CustomOperation, network_request_matcher, _)) .WillOnce(Return(new mpt::MockLocalSocketReply{GetParam()})); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; auto [mock_platform, guard] = mpt::MockPlatform::inject(); EXPECT_CALL(*mock_platform, get_network_interfaces_info) @@ -2145,7 +2172,7 @@ TEST_F(LXDBackend, honors_bridge_description_from_lxd_when_available) EXPECT_CALL(*mock_platform, get_network_interfaces_info) .WillOnce(Return(std::map{{"br0", {"br0", "bridge", "gibberish"}}})); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; EXPECT_THAT(backend.networks(), ElementsAre(Field(&mp::NetworkInterfaceInfo::description, Eq(description)))); } @@ -2163,7 +2190,7 @@ TEST_F(LXDBackend, falls_back_to_bridge_description_from_platform) createRequest(QNetworkAccessManager::CustomOperation, network_request_matcher, _)) .WillOnce(Return(new mpt::MockLocalSocketReply{{data}})); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; EXPECT_THAT(backend.networks(), ElementsAre(Field(&mp::NetworkInterfaceInfo::description, Eq(fallback_desc)))); } @@ -2179,7 +2206,7 @@ TEST_F(LXDBackend, skips_platform_network_inspection_when_lxd_reports_no_network createRequest(QNetworkAccessManager::CustomOperation, network_request_matcher, _)) .WillOnce(Return(new mpt::MockLocalSocketReply{{data}})); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; EXPECT_THAT(backend.networks(), IsEmpty()); } @@ -2263,6 +2290,7 @@ TEST_F(LXDBackend, posts_extra_network_devices) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; } @@ -2287,6 +2315,7 @@ TEST_F(LXDBackend, posts_network_data_config_if_available) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; } @@ -2304,7 +2333,7 @@ class CustomLXDFactory : public mp::LXDVirtualMachineFactory TEST_F(LXDBackend, preparesNetworkingViaBaseFactory) { - CustomLXDFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + CustomLXDFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; std::vector extra_networks{{"netid", "mac", false}}; auto address = [](const auto& v) { return &v; }; // replace with Address matcher once available @@ -2318,7 +2347,7 @@ TEST_F(LXDBackend, createsBridgesViaBackendUtils) const auto bridge = "bli"; auto [mock_backend, guard] = mpt::MockBackend::inject(); - CustomLXDFactory factory{std::move(mock_network_access_manager), data_dir.path(), base_url}; + CustomLXDFactory factory{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; EXPECT_CALL(*mock_backend, create_bridge_with(net.id)).WillOnce(Return(bridge)); EXPECT_EQ(factory.create_bridge_with(net), bridge); @@ -2372,7 +2401,7 @@ TEST_F(LXDBackend, addsNetworkInterface) return new mpt::MockLocalSocketReply(mpt::not_found_data, QNetworkReply::ContentNotFoundError); }); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), base_url}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), az_manager, base_url}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -2392,6 +2421,9 @@ TEST_F(LXDBackend, converts_http_to_https) return new mpt::MockLocalSocketReply(mpt::stop_vm_data); }); - mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), data_dir.path(), QUrl{"http://bar"}}; + mp::LXDVirtualMachineFactory backend{std::move(mock_network_access_manager), + data_dir.path(), + az_manager, + QUrl{"http://bar"}}; backend.create_virtual_machine(default_description, key_provider, stub_monitor); } diff --git a/tests/lxd/test_lxd_mount_handler.cpp b/tests/lxd/test_lxd_mount_handler.cpp index c207fb3f1b..e879ef088b 100644 --- a/tests/lxd/test_lxd_mount_handler.cpp +++ b/tests/lxd/test_lxd_mount_handler.cpp @@ -20,6 +20,7 @@ #include "tests/mock_file_ops.h" #include "tests/mock_logger.h" #include "tests/mock_virtual_machine.h" +#include "tests/stub_availability_zone.h" #include "tests/stub_ssh_key_provider.h" #include "tests/stub_status_monitor.h" #include "tests/temp_dir.h" @@ -46,14 +47,18 @@ class MockLXDVirtualMachine : public mpt::MockVirtualMachineT{desc, - monitor, - manager, - base_url, - bridge_name, - storage_pool, - key_provider} + const mp::SSHKeyProvider& key_provider, + mp::AvailabilityZone& zone) + : mpt::MockVirtualMachineT{ + desc, + monitor, + manager, + base_url, + bridge_name, + storage_pool, + key_provider, + zone, + } { } }; @@ -92,10 +97,12 @@ struct LXDMountHandlerTestFixture : public testing::Test const QString default_storage_pool{"default"}; mpt::StubVMStatusMonitor stub_monitor; const QString bridge_name{"mpbr0"}; + mpt::StubAvailabilityZone zone{}; const mp::VirtualMachineDescription default_description{2, mp::MemorySize{"3M"}, mp::MemorySize{}, // not used "pied-piper-valley", + "zone1", "00:16:3e:fe:f2:b9", {}, "yoda", @@ -120,13 +127,16 @@ struct LXDMountHandlerValidGidUidParameterTests : public LXDMountHandlerTestFixt TEST_F(LXDMountHandlerTestFixture, startDoesNotThrowIfVMIsStopped) { - NiceMock lxd_vm{default_description, - stub_monitor, - &mock_network_access_manager, - base_url, - bridge_name, - default_storage_pool, - key_provider}; + NiceMock lxd_vm{ + default_description, + stub_monitor, + &mock_network_access_manager, + base_url, + bridge_name, + default_storage_pool, + key_provider, + zone, + }; mp::LXDMountHandler lxd_mount_handler(&mock_network_access_manager, &lxd_vm, &key_provider, target_path, vm_mount); @@ -138,13 +148,16 @@ TEST_F(LXDMountHandlerTestFixture, startDoesNotThrowIfVMIsStopped) TEST_F(LXDMountHandlerTestFixture, startThrowsIfVMIsRunning) { - NiceMock lxd_vm{default_description, - stub_monitor, - &mock_network_access_manager, - base_url, - bridge_name, - default_storage_pool, - key_provider}; + NiceMock lxd_vm{ + default_description, + stub_monitor, + &mock_network_access_manager, + base_url, + bridge_name, + default_storage_pool, + key_provider, + zone, + }; mp::LXDMountHandler lxd_mount_handler(&mock_network_access_manager, &lxd_vm, &key_provider, target_path, vm_mount); EXPECT_CALL(lxd_vm, current_state).WillOnce(Return(multipass::VirtualMachine::State::running)); @@ -156,13 +169,16 @@ TEST_F(LXDMountHandlerTestFixture, startThrowsIfVMIsRunning) TEST_F(LXDMountHandlerTestFixture, stopDoesNotThrowIfVMIsStopped) { - NiceMock lxd_vm{default_description, - stub_monitor, - &mock_network_access_manager, - base_url, - bridge_name, - default_storage_pool, - key_provider}; + NiceMock lxd_vm{ + default_description, + stub_monitor, + &mock_network_access_manager, + base_url, + bridge_name, + default_storage_pool, + key_provider, + zone, + }; mp::LXDMountHandler lxd_mount_handler(&mock_network_access_manager, &lxd_vm, &key_provider, target_path, vm_mount); EXPECT_CALL(lxd_vm, current_state) @@ -176,13 +192,16 @@ TEST_F(LXDMountHandlerTestFixture, stopDoesNotThrowIfVMIsStopped) TEST_F(LXDMountHandlerTestFixture, stopThrowsIfVMIsRunning) { - NiceMock lxd_vm{default_description, - stub_monitor, - &mock_network_access_manager, - base_url, - bridge_name, - default_storage_pool, - key_provider}; + NiceMock lxd_vm{ + default_description, + stub_monitor, + &mock_network_access_manager, + base_url, + bridge_name, + default_storage_pool, + key_provider, + zone, + }; mp::LXDMountHandler lxd_mount_handler(&mock_network_access_manager, &lxd_vm, &key_provider, target_path, vm_mount); @@ -206,6 +225,7 @@ TEST_P(LXDMountHandlerInvalidGidUidParameterTests, mountWithGidOrUid) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; const auto& [host_gid, instance_gid, host_uid, instance_uid] = GetParam(); const mp::VMMount vm_mount{ @@ -230,6 +250,7 @@ TEST_P(LXDMountHandlerValidGidUidParameterTests, mountWithGidOrUid) bridge_name, default_storage_pool, key_provider, + zone, instance_dir.path()}; const auto& [host_gid, host_uid] = GetParam(); const int default_instance_id = -1; diff --git a/tests/mock_json_utils.h b/tests/mock_json_utils.h index 7dd2a83869..4f91609b1b 100644 --- a/tests/mock_json_utils.h +++ b/tests/mock_json_utils.h @@ -31,6 +31,7 @@ class MockJsonUtils : public JsonUtils using JsonUtils::JsonUtils; MOCK_METHOD(void, write_json, (const QJsonObject&, QString), (const, override)); + MOCK_METHOD(QJsonObject, read_object_from_file, (const std::filesystem::path& file_path), (const, override)); MOCK_METHOD(std::string, json_to_string, (const QJsonObject& root), (const, override)); MP_MOCK_SINGLETON_BOILERPLATE(MockJsonUtils, JsonUtils); diff --git a/tests/mock_virtual_machine.h b/tests/mock_virtual_machine.h index c9e5ccad64..05600a0085 100644 --- a/tests/mock_virtual_machine.h +++ b/tests/mock_virtual_machine.h @@ -58,6 +58,7 @@ struct MockVirtualMachineT : public T MOCK_METHOD(void, start, (), (override)); MOCK_METHOD(void, shutdown, (VirtualMachine::ShutdownPolicy), (override)); MOCK_METHOD(void, suspend, (), (override)); + MOCK_METHOD(void, set_available, (bool), (override)); MOCK_METHOD(multipass::VirtualMachine::State, current_state, (), (override)); MOCK_METHOD(int, ssh_port, (), (override)); MOCK_METHOD(std::string, ssh_hostname, (), (override)); @@ -101,6 +102,7 @@ struct MockVirtualMachineT : public T MOCK_METHOD(void, load_snapshots, (), (override)); MOCK_METHOD(std::vector, get_childrens_names, (const Snapshot*), (const, override)); MOCK_METHOD(int, get_snapshot_count, (), (const, override)); + MOCK_METHOD(AvailabilityZone&, get_zone, (), (const, override)); std::unique_ptr tmp_dir; }; diff --git a/tests/qemu/test_qemu_backend.cpp b/tests/qemu/test_qemu_backend.cpp index df0f8075fa..19fbcfe859 100644 --- a/tests/qemu/test_qemu_backend.cpp +++ b/tests/qemu/test_qemu_backend.cpp @@ -27,6 +27,8 @@ #include "tests/mock_status_monitor.h" #include "tests/mock_virtual_machine.h" #include "tests/path.h" +#include "tests/stub_availability_zone.h" +#include "tests/stub_availability_zone_manager.h" #include "tests/stub_process_factory.h" #include "tests/stub_ssh_key_provider.h" #include "tests/stub_status_monitor.h" @@ -80,6 +82,7 @@ struct QemuBackend : public mpt::TestWithMockedBinPath mp::MemorySize{"3M"}, mp::MemorySize{}, // not used "pied-piper-valley", + "zone1", "", {}, "", @@ -96,6 +99,8 @@ struct QemuBackend : public mpt::TestWithMockedBinPath const std::string subnet{"192.168.64"}; mpt::StubSSHKeyProvider key_provider{}; mpt::StubVMStatusMonitor stub_monitor{}; + mpt::StubAvailabilityZone zone{}; + mpt::StubAvailabilityZoneManager az_manager{}; mpt::MockProcessFactory::Callback handle_external_process_calls = [](mpt::MockProcess* process) { // Have "qemu-img snapshot" return a string with the suspend tag in it @@ -193,7 +198,7 @@ TEST_F(QemuBackend, creates_in_off_state) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); EXPECT_THAT(machine->current_state(), Eq(mp::VirtualMachine::State::off)); @@ -205,7 +210,7 @@ TEST_F(QemuBackend, machine_in_off_state_handles_shutdown) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); EXPECT_THAT(machine->current_state(), Eq(mp::VirtualMachine::State::off)); @@ -221,7 +226,7 @@ TEST_F(QemuBackend, machine_start_shutdown_sends_monitoring_events) }); NiceMock mock_monitor; - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; process_factory->register_callback(handle_qemu_system); @@ -245,7 +250,7 @@ TEST_F(QemuBackend, machine_start_suspend_sends_monitoring_event) }); NiceMock mock_monitor; - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; process_factory->register_callback(handle_qemu_system); @@ -277,7 +282,7 @@ TEST_F(QemuBackend, throws_when_shutdown_while_starting) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -317,7 +322,7 @@ TEST_F(QemuBackend, throws_on_shutdown_timeout) }); mpt::StubVMStatusMonitor stub_monitor; - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -353,7 +358,7 @@ TEST_F(QemuBackend, includes_error_when_shutdown_while_starting) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -389,7 +394,7 @@ TEST_F(QemuBackend, machine_unknown_state_properly_shuts_down) }); NiceMock mock_monitor; - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; process_factory->register_callback(handle_qemu_system); @@ -416,7 +421,7 @@ TEST_F(QemuBackend, suspendedStateNoForceShutdownThrows) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -436,7 +441,7 @@ TEST_F(QemuBackend, suspendingStateNoForceShutdownThrows) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -458,7 +463,7 @@ TEST_F(QemuBackend, startingStateNoForceShutdownThrows) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -502,7 +507,7 @@ TEST_F(QemuBackend, forceShutdownKillsProcessAndLogs) logger_scope.mock_logger->expect_log(mpl::Level::info, "Killed"); logger_scope.mock_logger->expect_log(mpl::Level::info, "Force stopped"); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -527,7 +532,7 @@ TEST_F(QemuBackend, forceShutdownNoProcessLogs) logger_scope.mock_logger->expect_log(mpl::Level::info, "Forcing shutdown"); logger_scope.mock_logger->expect_log(mpl::Level::debug, "No process to kill"); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); @@ -558,7 +563,7 @@ TEST_F(QemuBackend, forceShutdownSuspendDeletesSuspendImageAndOffState) logger_scope.mock_logger->expect_log(mpl::Level::debug, "No process to kill"); logger_scope.mock_logger->expect_log(mpl::Level::info, "Deleting suspend image"); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; const auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); machine->state = mp::VirtualMachine::State::suspended; @@ -583,7 +588,7 @@ TEST_F(QemuBackend, forceShutdownSuspendedStateButNoSuspensionSnapshotInImage) logger_scope.mock_logger->expect_log(mpl::Level::debug, "No process to kill"); logger_scope.mock_logger->expect_log(mpl::Level::warning, "Image has no suspension snapshot, but the state is 7"); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; const auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); machine->state = mp::VirtualMachine::State::suspended; @@ -613,7 +618,7 @@ TEST_F(QemuBackend, forceShutdownRunningStateButWithSuspensionSnapshotInImage) logger_scope.mock_logger->expect_log(mpl::Level::info, "Deleting suspend image"); logger_scope.mock_logger->expect_log(mpl::Level::warning, "Image has a suspension snapshot, but the state is 4"); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; const auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); machine->state = mp::VirtualMachine::State::running; @@ -634,7 +639,7 @@ TEST_F(QemuBackend, verify_dnsmasq_qemuimg_and_qemu_processes_created) }); auto factory = mpt::StubProcessFactory::Inject(); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); machine->start(); @@ -667,7 +672,7 @@ TEST_F(QemuBackend, verify_some_common_qemu_arguments) } }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); machine->start(); @@ -692,7 +697,7 @@ TEST_F(QemuBackend, verify_qemu_arguments_when_resuming_suspend_image) process_factory->register_callback(handle_external_process_calls); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); machine->start(); @@ -723,7 +728,7 @@ TEST_F(QemuBackend, verify_qemu_arguments_when_resuming_suspend_image_uses_metad EXPECT_CALL(mock_monitor, retrieve_metadata_for(_)) .WillRepeatedly(Return(QJsonObject({{"machine_type", machine_type}}))); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, mock_monitor); machine->start(); @@ -763,7 +768,7 @@ TEST_F(QemuBackend, verify_qemu_arguments_from_metadata_are_used) EXPECT_CALL(mock_monitor, retrieve_metadata_for(_)) .WillRepeatedly(Return(QJsonObject({{"arguments", QJsonArray{"-hi_there", "-hows_it_going"}}}))); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, mock_monitor); machine->start(); @@ -801,7 +806,7 @@ TEST_F(QemuBackend, returns_version_string) process_factory->register_callback(callback); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; EXPECT_EQ(backend.get_backend_version_string(), "qemu-2.11.1"); } @@ -826,7 +831,7 @@ TEST_F(QemuBackend, returns_version_string_when_failed_parsing) process_factory->register_callback(callback); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; EXPECT_EQ(backend.get_backend_version_string(), "qemu-unknown"); } @@ -850,7 +855,7 @@ TEST_F(QemuBackend, returns_version_string_when_errored) process_factory->register_callback(callback); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; EXPECT_EQ(backend.get_backend_version_string(), "qemu-unknown"); } @@ -873,7 +878,7 @@ TEST_F(QemuBackend, returns_version_string_when_exec_failed) process_factory->register_callback(callback); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; EXPECT_EQ(backend.get_backend_version_string(), "qemu-unknown"); } @@ -891,6 +896,7 @@ TEST_F(QemuBackend, ssh_hostname_returns_expected_value) &mock_qemu_platform, stub_monitor, key_provider, + zone, instance_dir.path()}; machine.start(); machine.state = mp::VirtualMachine::State::running; @@ -909,6 +915,7 @@ TEST_F(QemuBackend, gets_management_ip) &mock_qemu_platform, stub_monitor, key_provider, + zone, instance_dir.path()}; machine.start(); machine.state = mp::VirtualMachine::State::running; @@ -926,6 +933,7 @@ TEST_F(QemuBackend, fails_to_get_management_ip_if_dnsmasq_does_not_return_an_ip) &mock_qemu_platform, stub_monitor, key_provider, + zone, instance_dir.path()}; machine.start(); machine.state = mp::VirtualMachine::State::running; @@ -943,6 +951,7 @@ TEST_F(QemuBackend, ssh_hostname_timeout_throws_and_sets_unknown_state) &mock_qemu_platform, stub_monitor, key_provider, + zone, instance_dir.path()}; machine.start(); machine.state = mp::VirtualMachine::State::running; @@ -969,11 +978,14 @@ TEST_F(QemuBackend, logsErrorOnFailureToConvertToQcow2V3UponConstruction) logger_scope.mock_logger->screen_logs(mpl::Level::error); logger_scope.mock_logger->expect_log(mpl::Level::error, "Failed to amend image to QCOW2 v3"); - mp::QemuVirtualMachine machine{default_description, - &mock_qemu_platform, - stub_monitor, - key_provider, - instance_dir.path()}; + mp::QemuVirtualMachine machine{ + default_description, + &mock_qemu_platform, + stub_monitor, + key_provider, + zone, + instance_dir.path(), + }; } struct MockQemuVM : public mpt::MockVirtualMachineT @@ -987,7 +999,7 @@ struct MockQemuVM : public mpt::MockVirtualMachineT TEST_F(QemuBackend, dropsSSHSessionWhenStopping) { - NiceMock machine{"mock-qemu-vm", key_provider}; + NiceMock machine{"mock-qemu-vm", key_provider, zone}; machine.state = multipass::VirtualMachine::State::running; EXPECT_CALL(machine, drop_ssh_session()); @@ -998,28 +1010,32 @@ TEST_F(QemuBackend, dropsSSHSessionWhenStopping) TEST_F(QemuBackend, supportsSnapshots) { - MockQemuVM vm{"asdf", key_provider}; + MockQemuVM vm{"asdf", key_provider, zone}; EXPECT_NO_THROW(vm.require_snapshots_support()); } TEST_F(QemuBackend, createsQemuSnapshotsFromSpecs) { - MockQemuVM machine{"mock-qemu-vm", key_provider}; + MockQemuVM machine{"mock-qemu-vm", key_provider, zone}; auto snapshot_name = "elvis"; auto snapshot_comment = "has left the building"; auto instance_id = "vm1"; - const mp::VMSpecs specs{2, - mp::MemorySize{"3.21G"}, - mp::MemorySize{"4.32M"}, - "00:00:00:00:00:00", - {{"eth18", "18:18:18:18:18:18", true}}, - "asdf", - mp::VirtualMachine::State::stopped, - {}, - false, - {}}; + const mp::VMSpecs specs{ + 2, + mp::MemorySize{"3.21G"}, + mp::MemorySize{"4.32M"}, + "00:00:00:00:00:00", + {{"eth18", "18:18:18:18:18:18", true}}, + "asdf", + mp::VirtualMachine::State::stopped, + {}, + false, + {}, + 0, + "zone1", + }; auto snapshot = machine.make_specific_snapshot(snapshot_name, snapshot_comment, instance_id, specs, nullptr); EXPECT_EQ(snapshot->get_name(), snapshot_name); EXPECT_EQ(snapshot->get_comment(), snapshot_comment); @@ -1033,7 +1049,7 @@ TEST_F(QemuBackend, createsQemuSnapshotsFromSpecs) TEST_F(QemuBackend, createsQemuSnapshotsFromJsonFile) { - MockQemuVM machine{"mock-qemu-vm", key_provider}; + MockQemuVM machine{"mock-qemu-vm", key_provider, zone}; const auto parent = std::make_shared(); EXPECT_CALL(machine, get_snapshot(2)).WillOnce(Return(parent)); @@ -1059,7 +1075,7 @@ TEST_F(QemuBackend, networks_returns_supported_networks) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; const std::map networks{ {"lxdbr0", {"lxdbr0", "bridge", "gobbledygook"}}, @@ -1091,7 +1107,7 @@ TEST_F(QemuBackend, remove_resources_for_calls_qemu_platform) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; backend.remove_resources_for(test_name); @@ -1110,7 +1126,7 @@ TEST_F(QemuBackend, hypervisor_health_check_calls_qemu_platform) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; backend.hypervisor_health_check(); @@ -1134,7 +1150,7 @@ TEST_F(QemuBackend, get_backend_directory_name_calls_qemu_platform) return std::move(mock_qemu_platform); }); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; const auto dir_name = backend.get_backend_directory_name(); @@ -1151,7 +1167,7 @@ TEST_F(QemuBackend, addNetworkInterface) const auto [mock_cloud_init_file_ops, _] = mpt::MockCloudInitFileOps::inject(); EXPECT_CALL(*mock_cloud_init_file_ops, add_extra_interface_to_cloud_init).Times(1); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; auto machine = backend.create_virtual_machine(default_description, key_provider, stub_monitor); EXPECT_NO_THROW(machine->add_network_interface(0, "", {"", "", true})); @@ -1164,7 +1180,7 @@ TEST_F(QemuBackend, createBridgeWithChecksWithQemuPlatform) }); EXPECT_CALL(*mock_qemu_platform, needs_network_prep()).Times(1).WillRepeatedly(Return(true)); - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; std::vector extra_interfaces{{"eth1", "52:54:00:00:00:00", true}}; EXPECT_NO_THROW(backend.prepare_networking(extra_interfaces)); @@ -1194,14 +1210,17 @@ TEST_F(QemuBackend, removeAllSnapshotsFromTheImage) process_factory->register_callback(snapshot_list_callback); mpt::StubVMStatusMonitor stub_monitor; - mp::QemuVirtualMachineFactory backend{data_dir.path()}; - - const mp::QemuVirtualMachine machine{default_description, - mock_qemu_platform.get(), - stub_monitor, - key_provider, - instance_dir.path(), - true}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; + + const mp::QemuVirtualMachine machine{ + default_description, + mock_qemu_platform.get(), + stub_monitor, + key_provider, + zone, + instance_dir.path(), + true, + }; const std::vector processes = process_factory->process_list(); @@ -1222,7 +1241,7 @@ TEST_F(QemuBackend, createVmAndCloneInstanceDirData) }); mpt::StubVMStatusMonitor stub_monitor; - mp::QemuVirtualMachineFactory backend{data_dir.path()}; + mp::QemuVirtualMachineFactory backend{data_dir.path(), az_manager}; const mpt::MockCloudInitFileOps::GuardedMock mock_cloud_init_file_ops_injection = mpt::MockCloudInitFileOps::inject(); EXPECT_CALL(*mock_cloud_init_file_ops_injection.first, update_identifiers(_, _, _, _)).Times(1); diff --git a/tests/qemu/test_qemu_mount_handler.cpp b/tests/qemu/test_qemu_mount_handler.cpp index cf790bde4e..d7d3213a4a 100644 --- a/tests/qemu/test_qemu_mount_handler.cpp +++ b/tests/qemu/test_qemu_mount_handler.cpp @@ -22,6 +22,7 @@ #include "tests/mock_ssh_process_exit_status.h" #include "tests/mock_ssh_test_fixture.h" #include "tests/mock_virtual_machine.h" +#include "tests/stub_availability_zone.h" #include "tests/stub_ssh_key_provider.h" #include "qemu_mount_handler.h" @@ -38,8 +39,12 @@ namespace { struct MockQemuVirtualMachine : mpt::MockVirtualMachineT { - explicit MockQemuVirtualMachine(const std::string& name) - : mpt::MockVirtualMachineT{name, mpt::StubSSHKeyProvider{}} + explicit MockQemuVirtualMachine(const std::string& name, mp::AvailabilityZone& zone) + : mpt::MockVirtualMachineT{ + name, + mpt::StubSSHKeyProvider{}, + zone, + } { } @@ -142,7 +147,8 @@ struct QemuMountHandlerTest : public ::Test mpt::MockServerReaderWriter server; mpt::MockSSHTestFixture mock_ssh_test_fixture; mpt::ExitStatusMock exit_status_mock; - NiceMock vm{"my_instance"}; + mpt::StubAvailabilityZone zone{}; + NiceMock vm{"my_instance", zone}; mp::QemuVirtualMachine::MountArgs mount_args; CommandOutputs command_outputs{ {"echo $PWD/target", {"/home/ubuntu/target"}}, diff --git a/tests/qemu/test_qemu_snapshot.cpp b/tests/qemu/test_qemu_snapshot.cpp index 48d0dc8db8..5d107e67df 100644 --- a/tests/qemu/test_qemu_snapshot.cpp +++ b/tests/qemu/test_qemu_snapshot.cpp @@ -22,6 +22,7 @@ #include "tests/mock_snapshot.h" #include "tests/mock_virtual_machine.h" #include "tests/path.h" +#include "tests/stub_availability_zone.h" #include "tests/stub_ssh_key_provider.h" #include @@ -91,7 +92,8 @@ struct TestQemuSnapshot : public Test }(); mpt::StubSSHKeyProvider key_provider{}; - NiceMock> vm{"qemu-vm", key_provider}; + mpt::StubAvailabilityZone zone{}; + NiceMock> vm{"qemu-vm", key_provider, zone}; ArgsMatcher list_args_matcher = ElementsAre("snapshot", "-l", desc.image.image_path); const mpt::MockCloudInitFileOps::GuardedMock mock_cloud_init_file_ops_injection = mpt::MockCloudInitFileOps::inject(); @@ -111,8 +113,22 @@ struct TestQemuSnapshot : public Test metadata["meta"] = "data"; return metadata; }(); - - return mp::VMSpecs{cpus, mem_size, disk_space, "mac", extra_interfaces, "", state, mounts, false, metadata}; + const auto zone = "zone1"; + + return mp::VMSpecs{ + cpus, + mem_size, + disk_space, + "mac", + extra_interfaces, + "", + state, + mounts, + false, + metadata, + 0, + zone, + }; }(); }; @@ -125,7 +141,7 @@ TEST_F(TestQemuSnapshot, initializesBaseProperties) const auto parent = std::make_shared(); auto desc = mp::VirtualMachineDescription{}; - auto vm = NiceMock>{"qemu-vm", key_provider}; + auto vm = NiceMock>{"qemu-vm", key_provider, zone}; const auto snapshot = mp::QemuSnapshot{name, comment, instance_id, parent, specs, vm, desc}; EXPECT_EQ(snapshot.get_name(), name); diff --git a/tests/qemu/test_qemu_vm_process_spec.cpp b/tests/qemu/test_qemu_vm_process_spec.cpp index 50af4fa9ea..8fec33ee09 100644 --- a/tests/qemu/test_qemu_vm_process_spec.cpp +++ b/tests/qemu/test_qemu_vm_process_spec.cpp @@ -35,6 +35,7 @@ struct TestQemuVMProcessSpec : public Test mp::MemorySize{"3G"} /*mem_size*/, mp::MemorySize{"4G"} /*disk_space*/, "vm_name", + "zone1", "00:11:22:33:44:55", {}, "ssh_username", diff --git a/tests/stub_availability_zone.h b/tests/stub_availability_zone.h new file mode 100644 index 0000000000..d779d1c8f3 --- /dev/null +++ b/tests/stub_availability_zone.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_STUB_AVAILABILITY_ZONE_H +#define MULTIPASS_STUB_AVAILABILITY_ZONE_H + +#include "multipass/availability_zone.h" + +namespace multipass +{ +namespace test +{ +struct StubAvailabilityZone final : public AvailabilityZone +{ + StubAvailabilityZone() + { + } + + const std::string& get_name() const override + { + static std::string name{"zone1"}; + return name; + } + + const std::string& get_subnet() const override + { + static std::string subnet{"192.168.123"}; + return subnet; + } + + bool is_available() const override + { + return true; + } + + void set_available(bool new_available) override + { + } + + void add_vm(VirtualMachine& vm) override + { + } + + void remove_vm(VirtualMachine& vm) override + { + } +}; +} // namespace test +} // namespace multipass +#endif // MULTIPASS_STUB_AVAILABILITY_ZONE_H diff --git a/tests/stub_availability_zone_manager.h b/tests/stub_availability_zone_manager.h new file mode 100644 index 0000000000..88d482679f --- /dev/null +++ b/tests/stub_availability_zone_manager.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef MULTIPASS_STUB_AVAILABILITY_ZONE_MANAGER_H +#define MULTIPASS_STUB_AVAILABILITY_ZONE_MANAGER_H + +#include "multipass/availability_zone_manager.h" +#include "stub_availability_zone.h" + +namespace multipass +{ +namespace test +{ +struct StubAvailabilityZoneManager final : public AvailabilityZoneManager +{ + StubAvailabilityZoneManager() + { + } + + AvailabilityZone& get_zone(const std::string& name) override + { + return zone; + } + std::string get_automatic_zone_name() override + { + return "zone1"; + } + std::vector> get_zones() override + { + return {zone}; + } + + std::string get_default_zone_name() const override + { + return "zone1"; + } + +private: + StubAvailabilityZone zone{}; +}; +} // namespace test +} // namespace multipass +#endif // MULTIPASS_STUB_AVAILABILITY_ZONE_MANAGER_H diff --git a/tests/stub_virtual_machine.h b/tests/stub_virtual_machine.h index f74d716eab..abb2c41cd2 100644 --- a/tests/stub_virtual_machine.h +++ b/tests/stub_virtual_machine.h @@ -18,6 +18,7 @@ #ifndef MULTIPASS_STUB_VIRTUAL_MACHINE_H #define MULTIPASS_STUB_VIRTUAL_MACHINE_H +#include "stub_availability_zone.h" #include "stub_mount_handler.h" #include "stub_snapshot.h" #include "temp_dir.h" @@ -55,6 +56,10 @@ struct StubVirtualMachine final : public multipass::VirtualMachine { } + void set_available(bool) override + { + } + multipass::VirtualMachine::State current_state() override { return multipass::VirtualMachine::State::off; @@ -194,6 +199,12 @@ struct StubVirtualMachine final : public multipass::VirtualMachine return 0; } + const AvailabilityZone& get_zone() const override + { + return zone; + } + + StubAvailabilityZone zone{}; StubSnapshot snapshot; std::unique_ptr tmp_dir; }; diff --git a/tests/stub_virtual_machine_factory.h b/tests/stub_virtual_machine_factory.h index a692fc8fbd..4255905cbb 100644 --- a/tests/stub_virtual_machine_factory.h +++ b/tests/stub_virtual_machine_factory.h @@ -30,12 +30,13 @@ namespace test { struct StubVirtualMachineFactory : public multipass::BaseVirtualMachineFactory { - StubVirtualMachineFactory() : StubVirtualMachineFactory{std::make_unique()} + StubVirtualMachineFactory(AvailabilityZoneManager& az_manager) + : StubVirtualMachineFactory{std::make_unique(), az_manager} { } - StubVirtualMachineFactory(std::unique_ptr tmp_dir) - : multipass::BaseVirtualMachineFactory{tmp_dir->path()}, tmp_dir{std::move(tmp_dir)} + StubVirtualMachineFactory(std::unique_ptr tmp_dir, AvailabilityZoneManager& az_manager) + : multipass::BaseVirtualMachineFactory{tmp_dir->path(), az_manager}, tmp_dir{std::move(tmp_dir)} { } diff --git a/tests/test_base_availability_zone.cpp b/tests/test_base_availability_zone.cpp new file mode 100644 index 0000000000..e31c29328f --- /dev/null +++ b/tests/test_base_availability_zone.cpp @@ -0,0 +1,158 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "common.h" +#include "mock_file_ops.h" +#include "mock_json_utils.h" +#include "mock_logger.h" +#include "mock_virtual_machine.h" + +#include +#include + +#include + +namespace mp = multipass; +namespace mpl = multipass::logging; +namespace mpt = multipass::test; +using namespace testing; + +struct BaseAvailabilityZoneTest : public Test +{ + BaseAvailabilityZoneTest() + { + mock_logger.mock_logger->screen_logs(mpl::Level::error); + } + + const std::string az_name{"zone1"}; + const mp::fs::path az_dir{"/path/to/zones"}; + const mp::fs::path az_file = az_dir / (az_name + ".json"); + const QString az_file_qstr{QString::fromStdString(az_file.u8string())}; + + mpt::MockFileOps::GuardedMock mock_file_ops_guard{mpt::MockFileOps::inject()}; + mpt::MockJsonUtils::GuardedMock mock_json_utils_guard{mpt::MockJsonUtils::inject()}; + mpt::MockLogger::Scope mock_logger{mpt::MockLogger::inject()}; +}; + +TEST_F(BaseAvailabilityZoneTest, CreatesDefaultAvailableZone) +{ + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(az_file)).WillOnce(Return(QJsonObject{})); + + EXPECT_CALL(*mock_logger.mock_logger, log(_, _, _)).Times(AnyNumber()); + + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, QString::fromStdString(az_file.u8string()))); + + mp::BaseAvailabilityZone zone{az_name, az_dir}; + + EXPECT_EQ(zone.get_name(), az_name); + EXPECT_TRUE(zone.is_available()); + // TODO: Subnet generation is not yet implemented + // EXPECT_TRUE(zone.get_subnet().empty()); +} + +// TODO: Re-implement this test when subnet generation is implemented +// TEST_F(TestBaseAvailabilityZone, loads_existing_zone_file) +// { +// const std::string test_subnet = "10.0.0.0/24"; +// const bool test_available = false; +// +// QJsonObject json{{"subnet", QString::fromStdString(test_subnet)}, {"available", test_available}}; +// +// EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(az_file)).WillOnce(Return(json)); +// +// EXPECT_CALL(*mock_logger.mock_logger, log(_, _, _)).Times(AnyNumber()); +// +// EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, QString::fromStdString(az_file.u8string()))); +// +// mp::BaseAvailabilityZone zone{az_name, az_dir}; +// +// EXPECT_EQ(zone.get_name(), az_name); +// EXPECT_EQ(zone.get_subnet(), test_subnet); +// EXPECT_FALSE(zone.is_available()); +// } + +TEST_F(BaseAvailabilityZoneTest, AddsVmAndUpdatesOnAvailabilityChange) +{ + QJsonObject json{{"available", true}}; + + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(az_file)).WillOnce(Return(json)); + + EXPECT_CALL(*mock_logger.mock_logger, log(_, _, _)).Times(AnyNumber()); + + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, QString::fromStdString(az_file.u8string()))) + .Times(2); // Once in constructor, once in set_available + + mp::BaseAvailabilityZone zone{az_name, az_dir}; + + const std::string test_vm_name = "test-vm"; + NiceMock mock_vm{test_vm_name}; + EXPECT_CALL(mock_vm, set_available(false)); + + zone.add_vm(mock_vm); + zone.set_available(false); +} + +TEST_F(BaseAvailabilityZoneTest, RemovesVmCorrectly) +{ + QJsonObject json{{"available", true}}; + + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(az_file)).WillOnce(Return(json)); + + EXPECT_CALL(*mock_logger.mock_logger, log(_, _, _)).Times(AnyNumber()); + + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, QString::fromStdString(az_file.u8string()))); + + mp::BaseAvailabilityZone zone{az_name, az_dir}; + + const std::string test_vm_name = "test-vm"; + NiceMock mock_vm{test_vm_name}; + + zone.add_vm(mock_vm); + zone.remove_vm(mock_vm); +} + +TEST_F(BaseAvailabilityZoneTest, AvailabilityStateManagement) +{ + QJsonObject json{{"available", true}}; + + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(az_file)).WillOnce(Return(json)); + + EXPECT_CALL(*mock_logger.mock_logger, log(_, _, _)).Times(AnyNumber()); + + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, QString::fromStdString(az_file.u8string()))) + .Times(2); // Once in constructor, once in set_available + + mp::BaseAvailabilityZone zone{az_name, az_dir}; + + const std::string vm1_name = "test-vm1"; + const std::string vm2_name = "test-vm2"; + NiceMock mock_vm1{vm1_name}; + NiceMock mock_vm2{vm2_name}; + + // Both VMs should be notified when state changes + EXPECT_CALL(mock_vm1, set_available(false)); + EXPECT_CALL(mock_vm2, set_available(false)); + + zone.add_vm(mock_vm1); + zone.add_vm(mock_vm2); + + // Setting to current state (true) shouldn't trigger VM updates + zone.set_available(true); + + // Setting to new state should notify all VMs + zone.set_available(false); +} diff --git a/tests/test_base_availability_zone_manager.cpp b/tests/test_base_availability_zone_manager.cpp new file mode 100644 index 0000000000..f897da53ba --- /dev/null +++ b/tests/test_base_availability_zone_manager.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (C) Canonical, Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "common.h" +#include "mock_file_ops.h" +#include "mock_json_utils.h" +#include "mock_logger.h" + +#include +#include +#include + +#include + +namespace mp = multipass; +namespace mpl = multipass::logging; +namespace mpt = multipass::test; +using namespace testing; + +struct BaseAvailabilityZoneManagerTest : public Test +{ + BaseAvailabilityZoneManagerTest() + { + mock_logger.mock_logger->screen_logs(mpl::Level::error); + } + + const mp::fs::path data_dir{"/path/to/data"}; + const mp::fs::path manager_file = data_dir / "az-manager.json"; + const mp::fs::path zones_dir = data_dir / "zones"; + const QString manager_file_qstr{QString::fromStdString(manager_file.u8string())}; + + mpt::MockFileOps::GuardedMock mock_file_ops_guard{mpt::MockFileOps::inject()}; + mpt::MockJsonUtils::GuardedMock mock_json_utils_guard{mpt::MockJsonUtils::inject()}; + mpt::MockLogger::Scope mock_logger{mpt::MockLogger::inject()}; +}; + +TEST_F(BaseAvailabilityZoneManagerTest, CreatesDefaultZones) +{ + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(manager_file)).WillOnce(Return(QJsonObject{})); + + EXPECT_CALL(*mock_logger.mock_logger, log(Eq(mpl::Level::trace), _, _)).Times(AnyNumber()); + EXPECT_CALL(*mock_logger.mock_logger, log(Eq(mpl::Level::debug), _, _)).Times(AnyNumber()); + + // Default zones will be created + const int expected_zone_count = static_cast(mp::default_zone_names.size()); + for (const auto& zone_name : mp::default_zone_names) + { + const auto zone_file = zones_dir / (std::string{zone_name} + ".json"); + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(zone_file)).WillOnce(Return(QJsonObject{})); + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, QString::fromStdString(zone_file.u8string()))); + } + + // Manager file gets written with default zone (once in constructor and once in get_automatic_zone_name) + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, manager_file_qstr)).Times(2); + + mp::BaseAvailabilityZoneManager manager{data_dir}; + + const auto zones = manager.get_zones(); + EXPECT_EQ(zones.size(), expected_zone_count); + + // First zone in default_zone_names should be our default + EXPECT_EQ(manager.get_default_zone_name(), *mp::default_zone_names.begin()); + // And also our automatic zone initially + EXPECT_EQ(manager.get_automatic_zone_name(), *mp::default_zone_names.begin()); +} + +TEST_F(BaseAvailabilityZoneManagerTest, AutomaticZoneRotation) +{ + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(manager_file)).WillOnce(Return(QJsonObject{})); + + EXPECT_CALL(*mock_logger.mock_logger, log(Eq(mpl::Level::trace), _, _)).Times(AnyNumber()); + EXPECT_CALL(*mock_logger.mock_logger, log(Eq(mpl::Level::debug), _, _)).Times(AnyNumber()); + + // Set up all zones to be created + for (const auto& zone_name : mp::default_zone_names) + { + const auto zone_file = zones_dir / (std::string{zone_name} + ".json"); + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(zone_file)).WillOnce(Return(QJsonObject{})); + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, QString::fromStdString(zone_file.u8string()))) + .Times(AnyNumber()); + } + + // Manager file will be written multiple times during rotation + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, manager_file_qstr)).Times(AnyNumber()); + + mp::BaseAvailabilityZoneManager manager{data_dir}; + + // First automatic zone should be the first zone + const auto first_zone = manager.get_automatic_zone_name(); + EXPECT_EQ(first_zone, *mp::default_zone_names.begin()); + + // Mark first zone as unavailable + manager.get_zone(first_zone).set_available(false); + + // Next automatic zone should be second zone + const auto second_zone = manager.get_automatic_zone_name(); + EXPECT_NE(second_zone, first_zone); + + // Mark all zones unavailable + for (const auto& zone : manager.get_zones()) + const_cast(zone.get()).set_available(false); + + // Expect exception when no zones available + EXPECT_THROW(manager.get_automatic_zone_name(), mp::NoAvailabilityZoneAvailable); +} + +TEST_F(BaseAvailabilityZoneManagerTest, ThrowsWhenZoneNotFound) +{ + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(manager_file)).WillOnce(Return(QJsonObject{})); + + EXPECT_CALL(*mock_logger.mock_logger, log(_, _, _)).Times(AnyNumber()); + + // Set up default zones to be created + for (const auto& zone_name : mp::default_zone_names) + { + const auto zone_file = zones_dir / (std::string{zone_name} + ".json"); + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(zone_file)).WillOnce(Return(QJsonObject{})); + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, QString::fromStdString(zone_file.u8string()))) + .Times(AnyNumber()); + } + + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, manager_file_qstr)).Times(AnyNumber()); + + mp::BaseAvailabilityZoneManager manager{data_dir}; + + EXPECT_THROW(manager.get_zone("nonexistent-zone"), mp::AvailabilityZoneNotFound); +} + +TEST_F(BaseAvailabilityZoneManagerTest, CyclesThroughAvailableZones) +{ + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(manager_file)).WillOnce(Return(QJsonObject{})); + + EXPECT_CALL(*mock_logger.mock_logger, log(_, _, _)).Times(AnyNumber()); + + // Set up default zones to be created - all initially available + for (const auto& zone_name : mp::default_zone_names) + { + const auto zone_file = zones_dir / (std::string{zone_name} + ".json"); + EXPECT_CALL(*mock_json_utils_guard.first, read_object_from_file(zone_file)).WillOnce(Return(QJsonObject{})); + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, QString::fromStdString(zone_file.u8string()))) + .Times(AnyNumber()); + } + + EXPECT_CALL(*mock_json_utils_guard.first, write_json(_, manager_file_qstr)).Times(AnyNumber()); + + mp::BaseAvailabilityZoneManager manager{data_dir}; + + // First call returns initial zone (zone1) + auto zone = manager.get_automatic_zone_name(); + EXPECT_EQ(zone, "zone1"); + + // Make zone2 unavailable + manager.get_zone("zone2").set_available(false); + + // Second call follows pointer to zone2 (unavailable), so moves to and returns zone3 + zone = manager.get_automatic_zone_name(); + EXPECT_EQ(zone, "zone3"); + + // Third call follows pointer to zone1 and returns it + zone = manager.get_automatic_zone_name(); + EXPECT_EQ(zone, "zone1"); + + // Fourth call follows pointer to zone2 (unavailable), so moves to and returns zone3 + zone = manager.get_automatic_zone_name(); + EXPECT_EQ(zone, "zone3"); +} diff --git a/tests/test_base_virtual_machine.cpp b/tests/test_base_virtual_machine.cpp index 276c1d93e3..c5c76e86b6 100644 --- a/tests/test_base_virtual_machine.cpp +++ b/tests/test_base_virtual_machine.cpp @@ -24,6 +24,7 @@ #include "mock_ssh_test_fixture.h" #include "mock_utils.h" #include "mock_virtual_machine.h" +#include "stub_availability_zone.h" #include "temp_dir.h" #include @@ -119,12 +120,15 @@ struct MockBaseVirtualMachine : public mpt::MockVirtualMachineT()} + + StubBaseVirtualMachine(mp::AvailabilityZone& zone, St s = St::off) + : StubBaseVirtualMachine{s, zone, std::make_unique()} { } - StubBaseVirtualMachine(St s, std::unique_ptr tmp_dir) - : mp::BaseVirtualMachine{s, "stub", mpt::StubSSHKeyProvider{}, tmp_dir->path()}, tmp_dir{std::move(tmp_dir)} + StubBaseVirtualMachine(St s, mp::AvailabilityZone& zone, std::unique_ptr tmp_dir) + : mp::BaseVirtualMachine{s, "stub", mpt::StubSSHKeyProvider{}, zone, tmp_dir->path()}, + tmp_dir{std::move(tmp_dir)} { } @@ -248,9 +252,10 @@ struct BaseVM : public Test return MatchesRegex(fmt::format("{0}*{1}{0}*", space_char_class, idx)); } + mpt::StubAvailabilityZone zone{}; mpt::MockSSHTestFixture mock_ssh_test_fixture; const mpt::DummyKeyProvider key_provider{"keeper of the seven keys"}; - NiceMock vm{"mock-vm", key_provider}; + NiceMock vm{"mock-vm", key_provider, zone}; std::vector> snapshot_album; QString head_path = vm.tmp_dir->filePath(head_filename); QString count_path = vm.tmp_dir->filePath(count_filename); @@ -299,7 +304,7 @@ TEST_F(BaseVM, get_all_ipv4_works_when_instance_is_off) TEST_F(BaseVM, add_network_interface_throws) { - StubBaseVirtualMachine base_vm(St::off); + StubBaseVirtualMachine base_vm(zone, St::off); MP_EXPECT_THROW_THAT(base_vm.add_network_interface(1, "", {"eth1", "52:54:00:00:00:00", true}), mp::NotImplementedOnThisBackendException, @@ -389,7 +394,7 @@ TEST_F(BaseVM, takesSnapshots) TEST_F(BaseVM, takeSnasphotThrowsIfSpecificSnapshotNotOverridden) { - StubBaseVirtualMachine stub{}; + StubBaseVirtualMachine stub{zone}; MP_EXPECT_THROW_THAT(stub.take_snapshot({}, "stub-snap", ""), mp::NotImplementedOnThisBackendException, mpt::match_what(HasSubstr("snapshots"))); @@ -741,16 +746,20 @@ TEST_F(BaseVM, restoresSnapshots) QJsonObject metadata{}; metadata["meta"] = "data"; - const mp::VMSpecs original_specs{2, - mp::MemorySize{"3.5G"}, - mp::MemorySize{"15G"}, - "12:12:12:12:12:12", - {}, - "user", - St::off, - {{"dst", mount}}, - false, - metadata}; + const mp::VMSpecs original_specs{ + 2, + mp::MemorySize{"3.5G"}, + mp::MemorySize{"15G"}, + "12:12:12:12:12:12", + {}, + "user", + St::off, + {{"dst", mount}}, + false, + metadata, + 0, + "zone1", + }; const auto* snapshot_name = "shoot"; vm.take_snapshot(original_specs, snapshot_name, ""); @@ -838,7 +847,7 @@ TEST_F(BaseVM, usesRestoredSnapshotAsParentForNewSnapshots) TEST_F(BaseVM, loadSnasphotThrowsIfSnapshotsNotImplemented) { - StubBaseVirtualMachine stub{}; + StubBaseVirtualMachine stub{zone}; mpt::make_file_with_content(stub.tmp_dir->filePath("0001.snapshot.json"), "whatever-content"); MP_EXPECT_THROW_THAT(stub.load_snapshots(), mp::NotImplementedOnThisBackendException, @@ -1178,16 +1187,20 @@ TEST_F(BaseVM, rollsbackFailedRestore) { mock_snapshotting(); - const mp::VMSpecs original_specs{1, - mp::MemorySize{"1.5G"}, - mp::MemorySize{"4G"}, - "ab:ab:ab:ab:ab:ab", - {}, - "me", - St::off, - {}, - false, - {}}; + const mp::VMSpecs original_specs{ + 1, + mp::MemorySize{"1.5G"}, + mp::MemorySize{"4G"}, + "ab:ab:ab:ab:ab:ab", + {}, + "me", + St::off, + {}, + false, + {}, + 0, + "zone1", + }; vm.take_snapshot(original_specs, "", ""); diff --git a/tests/test_base_virtual_machine_factory.cpp b/tests/test_base_virtual_machine_factory.cpp index 02e773887b..9e9a80d104 100644 --- a/tests/test_base_virtual_machine_factory.cpp +++ b/tests/test_base_virtual_machine_factory.cpp @@ -18,6 +18,7 @@ #include "common.h" #include "mock_logger.h" #include "mock_platform.h" +#include "stub_availability_zone_manager.h" #include "stub_ssh_key_provider.h" #include "stub_url_downloader.h" #include "temp_dir.h" @@ -38,12 +39,13 @@ namespace { struct MockBaseFactory : mp::BaseVirtualMachineFactory { - MockBaseFactory() : MockBaseFactory{std::make_unique()} + MockBaseFactory(mp::AvailabilityZoneManager& az_manager) + : MockBaseFactory{std::make_unique(), az_manager} { } - MockBaseFactory(std::unique_ptr&& tmp_dir) - : mp::BaseVirtualMachineFactory{tmp_dir->path()}, tmp_dir{std::move(tmp_dir)} + MockBaseFactory(std::unique_ptr&& tmp_dir, mp::AvailabilityZoneManager& az_manager) + : mp::BaseVirtualMachineFactory{tmp_dir->path(), az_manager}, tmp_dir{std::move(tmp_dir)} { } @@ -80,18 +82,19 @@ struct MockBaseFactory : mp::BaseVirtualMachineFactory struct BaseFactory : public Test { mpt::MockLogger::Scope logger_scope = mpt::MockLogger::inject(); + mpt::StubAvailabilityZoneManager az_manager{}; }; TEST_F(BaseFactory, returns_image_only_fetch_type) { - MockBaseFactory factory; + MockBaseFactory factory{az_manager}; EXPECT_EQ(factory.fetch_type(), mp::FetchType::ImageOnly); } TEST_F(BaseFactory, dir_name_returns_empty_string) { - MockBaseFactory factory; + MockBaseFactory factory{az_manager}; const auto dir_name = factory.get_backend_directory_name(); @@ -104,7 +107,7 @@ TEST_F(BaseFactory, create_image_vault_returns_default_vault) mpt::TempDir cache_dir; mpt::TempDir data_dir; std::vector hosts; - MockBaseFactory factory; + MockBaseFactory factory{az_manager}; auto vault = factory.create_image_vault(hosts, &stub_downloader, cache_dir.path(), data_dir.path(), mp::days{0}); @@ -113,7 +116,7 @@ TEST_F(BaseFactory, create_image_vault_returns_default_vault) TEST_F(BaseFactory, networks_throws) { - StrictMock factory; + StrictMock factory{az_manager}; ASSERT_THROW(factory.mp::BaseVirtualMachineFactory::networks(), mp::NotImplementedOnThisBackendException); } @@ -123,7 +126,7 @@ TEST_F(BaseFactory, networks_throws) // at this time. Instead, just make sure an ISO image is created and has the expected path. TEST_F(BaseFactory, creates_cloud_init_iso_image) { - MockBaseFactory factory; + MockBaseFactory factory{az_manager}; const std::string name{"foo"}; const YAML::Node metadata{YAML::Load({fmt::format("name: {}", name)})}, vendor_data{metadata}, user_data{metadata}, network_data{metadata}; @@ -135,6 +138,7 @@ TEST_F(BaseFactory, creates_cloud_init_iso_image) mp::MemorySize{"3M"}, mp::MemorySize{}, // not used name, + "zone1", "00:16:3e:fe:f2:b9", {}, "yoda", @@ -153,7 +157,7 @@ TEST_F(BaseFactory, creates_cloud_init_iso_image) TEST_F(BaseFactory, create_bridge_not_implemented) { - StrictMock factory; + StrictMock factory{az_manager}; MP_EXPECT_THROW_THAT(factory.base_create_bridge_with({}), mp::NotImplementedOnThisBackendException, mpt::match_what(HasSubstr("bridge creation"))); @@ -161,7 +165,7 @@ TEST_F(BaseFactory, create_bridge_not_implemented) TEST_F(BaseFactory, prepareNetworkingHasNoObviousEffectByDefault) { - MockBaseFactory factory; + MockBaseFactory factory{az_manager}; EXPECT_CALL(factory, prepare_networking).WillOnce(Invoke([&factory](auto& nets) { factory.mp::BaseVirtualMachineFactory::prepare_networking(nets); @@ -176,7 +180,7 @@ TEST_F(BaseFactory, prepareNetworkingHasNoObviousEffectByDefault) TEST_F(BaseFactory, prepareInterfaceLeavesUnrecognizedNetworkAlone) { - StrictMock factory; + StrictMock factory{az_manager}; auto host_nets = std::vector{{"eth0", "ethernet", "asd"}, {"wlan0", "wifi", "asd"}}; auto extra_net = mp::NetworkInterface{"eth1", "fa:se:ma:c0:12:23", false}; @@ -190,7 +194,7 @@ TEST_F(BaseFactory, prepareInterfaceLeavesUnrecognizedNetworkAlone) TEST_F(BaseFactory, prepareInterfaceLeavesExistingBridgeAlone) { - StrictMock factory; + StrictMock factory{az_manager}; constexpr auto bridge_type = "arbitrary"; auto [mock_platform, platform_guard] = mpt::MockPlatform::inject(); @@ -208,7 +212,7 @@ TEST_F(BaseFactory, prepareInterfaceLeavesExistingBridgeAlone) TEST_F(BaseFactory, prepareInterfaceReplacesBridgedNetworkWithCorrespondingBridge) { - StrictMock factory; + StrictMock factory{az_manager}; constexpr auto bridge_type = "tunnel"; constexpr auto bridge = "br"; @@ -232,7 +236,7 @@ TEST_F(BaseFactory, prepareInterfaceReplacesBridgedNetworkWithCorrespondingBridg TEST_F(BaseFactory, prepareInterfaceCreatesBridgeForUnbridgedNetwork) { - StrictMock factory; + StrictMock factory{az_manager}; constexpr auto bridge_type = "gagah"; constexpr auto bridge = "newbr"; @@ -268,7 +272,7 @@ TEST_F(BaseFactory, prepareInterfaceCreatesBridgeForUnbridgedNetwork) TEST_F(BaseFactory, prepareNetworkingWithNoExtraNetsHasNoObviousEffect) { - MockBaseFactory factory; + MockBaseFactory factory{az_manager}; MP_DELEGATE_MOCK_CALLS_ON_BASE(factory, prepare_networking, mp::BaseVirtualMachineFactory); std::vector empty; @@ -289,7 +293,7 @@ TEST_F(BaseFactory, prepareNetworkingPreparesEachRequestedNetwork) {"aaa", "alpha", true}, {"bbb", "beta", false}, {"br", "bridge", true}, {"brr", "bridge", false}}; const auto num_nets = extra_nets.size(); - MockBaseFactory factory; + MockBaseFactory factory{az_manager}; EXPECT_CALL(factory, networks).WillOnce(Return(host_nets)); MP_DELEGATE_MOCK_CALLS_ON_BASE(factory, prepare_networking, mp::BaseVirtualMachineFactory); @@ -303,7 +307,7 @@ TEST_F(BaseFactory, prepareNetworkingPreparesEachRequestedNetwork) TEST_F(BaseFactory, factoryHasDefaultSuspendSupport) { - MockBaseFactory factory; + MockBaseFactory factory{az_manager}; EXPECT_NO_THROW(factory.mp::BaseVirtualMachineFactory::require_suspend_support()); } } // namespace diff --git a/tests/test_blueprint_provider.cpp b/tests/test_blueprint_provider.cpp index 17ad305b88..9bd587243f 100644 --- a/tests/test_blueprint_provider.cpp +++ b/tests/test_blueprint_provider.cpp @@ -76,7 +76,7 @@ TEST_F(VMBlueprintProvider, fetchBlueprintForUnknownBlueprintThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -96,7 +96,7 @@ TEST_F(VMBlueprintProvider, invalidImageSchemeThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -110,7 +110,7 @@ TEST_F(VMBlueprintProvider, invalidMinCoresThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -124,7 +124,7 @@ TEST_F(VMBlueprintProvider, invalidMinMemorySizeThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -138,7 +138,7 @@ TEST_F(VMBlueprintProvider, invalidMinDiskSpaceThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -152,7 +152,7 @@ TEST_F(VMBlueprintProvider, invalidAliasDefinitionThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -166,7 +166,7 @@ TEST_F(VMBlueprintProvider, fetchTestBlueprint1ReturnsExpectedInfo) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -186,7 +186,7 @@ TEST_F(VMBlueprintProvider, fetchTestBlueprint1ReturnsExpectedAliasesAndWorkspac mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData launch_data; @@ -213,7 +213,7 @@ TEST_F(VMBlueprintProvider, fetchTestBlueprint2ReturnsExpectedInfo) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData launch_data; @@ -278,7 +278,7 @@ TEST_F(VMBlueprintProvider, invalidCloudInitThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; const std::string blueprint{"invalid-cloud-init-blueprint"}; @@ -294,7 +294,7 @@ TEST_F(VMBlueprintProvider, givenCoresLessThanMinimumThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{1, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{1, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -308,7 +308,7 @@ TEST_F(VMBlueprintProvider, givenMemLessThanMinimumThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, mp::MemorySize{"1G"}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, mp::MemorySize{"1G"}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -322,7 +322,7 @@ TEST_F(VMBlueprintProvider, givenDiskSpaceLessThanMinimumThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, mp::MemorySize{"20G"}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, mp::MemorySize{"20G"}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -336,8 +336,8 @@ TEST_F(VMBlueprintProvider, higherOptionsIsNotOverridden) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{ - 4, mp::MemorySize{"4G"}, mp::MemorySize{"50G"}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription + vm_desc{4, mp::MemorySize{"4G"}, mp::MemorySize{"50G"}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -582,7 +582,7 @@ TEST_F(VMBlueprintProvider, noImageDefinedReturnsDefault) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -596,7 +596,7 @@ TEST_F(VMBlueprintProvider, nameMismatchThrows) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -812,7 +812,7 @@ TEST_P(VMBlueprintFileLaunchFromFile, loadsFile) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &mock_url_downloader, cache_dir.path(), default_ttl, "multivacs"}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -842,7 +842,7 @@ TEST_F(VMBlueprintFileLaunch, mergesBlueprintVendorData) vendor_data["system_info"]["default_user"]["name"] = "ubuntu"; vendor_data["growpart"]["devices"].push_back("/dev/vdb1"); - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, vendor_data, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, vendor_data, {}}; mp::ClientLaunchData dummy_data; @@ -874,7 +874,7 @@ TEST_F(VMBlueprintFileLaunch, failsMergeVmBlueprintVendorDataDifferentTypes) YAML::Node vendor_data; vendor_data["runcmd"] = "echo 123"; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, vendor_data, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, vendor_data, {}}; mp::ClientLaunchData dummy_data; @@ -896,7 +896,7 @@ TEST_F(VMBlueprintFileLaunch, failsMergeVmBlueprintVendorDataScalarValues) YAML::Node vendor_data; vendor_data["system_info"]["default_user"]["shell"] = "/bin/fish"; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, vendor_data, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, vendor_data, {}}; mp::ClientLaunchData dummy_data; @@ -913,7 +913,7 @@ TEST_F(VMBlueprintFileLaunch, failsWithNonexistentFile) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -929,7 +929,7 @@ TEST_F(VMBlueprintFileLaunch, fileLoadfailsWithInvalidHostName) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data; @@ -966,7 +966,7 @@ TEST_F(VMBlueprintFileLaunch, fileLoadfailsWithNoUrl) mp::DefaultVMBlueprintProvider blueprint_provider{blueprints_zip_url, &url_downloader, cache_dir.path(), default_ttl, "microvac"}; - mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; + mp::VirtualMachineDescription vm_desc{0, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}; mp::ClientLaunchData dummy_data;