diff --git a/feature-flags.cmake b/feature-flags.cmake index 1bde03f2ae4..175921167a6 100644 --- a/feature-flags.cmake +++ b/feature-flags.cmake @@ -16,3 +16,8 @@ include(src/cmake/feature-flag.cmake) # Multipass backend integrating with Apple Virtualization framework feature_flag(APPLEVZ_ENABLED "AppleVZ backend" APPLE) + +feature_flag(AVAILABILITY_ZONES_ENABLED "Availability zone support") +if(AVAILABILITY_ZONES_ENABLED) + add_compile_definitions(AVAILABILITY_ZONES_FEATURE) +endif() diff --git a/include/multipass/availability_zone.h b/include/multipass/availability_zone.h new file mode 100644 index 00000000000..4b6b3251740 --- /dev/null +++ b/include/multipass/availability_zone.h @@ -0,0 +1,46 @@ +/* + * 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_AVAILABILITY_ZONE_H +#define MULTIPASS_AVAILABILITY_ZONE_H + +#include "disabled_copy_move.h" +#include "subnet.h" +#include "virtual_machine.h" + +#include + +namespace multipass +{ +class AvailabilityZone : private DisabledCopyMove +{ +public: + using UPtr = std::unique_ptr; + using ShPtr = std::shared_ptr; + + virtual ~AvailabilityZone() = default; + + [[nodiscard]] virtual const std::string& get_name() const = 0; + [[nodiscard]] virtual const Subnet& get_subnet() const = 0; + [[nodiscard]] virtual bool is_available() const = 0; + virtual void set_available(bool new_available) = 0; + virtual void add_vm(VirtualMachine& vm) = 0; + virtual void remove_vm(VirtualMachine& vm) = 0; +}; +} // namespace multipass + +#endif // MULTIPASS_AVAILABILITY_ZONE_H diff --git a/include/multipass/availability_zone_manager.h b/include/multipass/availability_zone_manager.h new file mode 100644 index 00000000000..c58cf9f578c --- /dev/null +++ b/include/multipass/availability_zone_manager.h @@ -0,0 +1,51 @@ +/* + * 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_AVAILABILITY_ZONE_MANAGER_H +#define MULTIPASS_AVAILABILITY_ZONE_MANAGER_H + +#include "availability_zone.h" + +#include +#include +#include +#include + +namespace multipass +{ +class AvailabilityZoneManager : private DisabledCopyMove +{ +public: + using UPtr = std::unique_ptr; + using ShPtr = std::shared_ptr; + + using Zones = std::vector>; + + virtual ~AvailabilityZoneManager() = default; + + virtual AvailabilityZone& get_zone(const std::string& name) = 0; + virtual Zones get_zones() = 0; + // this returns a computed zone name, using an algorithm e.g. round-robin + // not to be confused with [get_default_zone_name] + virtual std::string get_automatic_zone_name() = 0; + // this always returns the same zone name, to be given to VMs that were not assigned to a zone + // in the past not to be confused with [get_automatic_zone] + virtual std::string get_default_zone_name() const = 0; +}; +} // namespace multipass + +#endif // MULTIPASS_AVAILABILITY_ZONE_MANAGER_H diff --git a/include/multipass/base_availability_zone.h b/include/multipass/base_availability_zone.h new file mode 100644 index 00000000000..7210d2ac61d --- /dev/null +++ b/include/multipass/base_availability_zone.h @@ -0,0 +1,75 @@ +/* + * 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 + +#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 Subnet& 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: + mutable std::recursive_mutex mutex; + const std::filesystem::path file_path; + const std::string name; + std::vector> vms; + + // 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 Subnet subnet; + bool available; + } m; + + static Data load_file(const std::string& name, const std::filesystem::path& file_path); + void save_file() const; + + friend void tag_invoke(const boost::json::value_from_tag&, + boost::json::value& json, + const Data& zone); + friend Data tag_invoke(const boost::json::value_to_tag&, const boost::json::value& json); +}; + +void tag_invoke(const boost::json::value_from_tag&, + boost::json::value& json, + const BaseAvailabilityZone::Data& zone); +BaseAvailabilityZone::Data tag_invoke(const boost::json::value_to_tag&, + const boost::json::value& json); + +} // 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 00000000000..a7ae5cc16e5 --- /dev/null +++ b/include/multipass/base_availability_zone_manager.h @@ -0,0 +1,72 @@ +/* + * 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: + 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{}; + }; + + mutable std::recursive_mutex mutex; + const std::filesystem::path file_path; + ZoneCollection zone_collection; + + [[nodiscard]] const ZoneCollection::ZoneArray& zones() const; + + static std::string load_file(const std::filesystem::path& file_path); + void save_file() const; +}; +} // namespace multipass + +#endif // MULTIPASS_BASE_AVAILABILITY_ZONE_MANAGER_H diff --git a/include/multipass/cli/command.h b/include/multipass/cli/command.h index b3ea2ec323b..b0387ff364d 100644 --- a/include/multipass/cli/command.h +++ b/include/multipass/cli/command.h @@ -77,7 +77,7 @@ class Command : private DisabledCopyMove using Arg0Type = typename multipass::callable_traits::template arg<0>::type; - using ReplyType = typename std::remove_reference::type; + using ReplyType = std::decay_t; ReplyType reply; auto handle_failure = adapt_failure_handler(on_failure, reply); @@ -146,7 +146,7 @@ class Command : private DisabledCopyMove { using Arg0Type = typename multipass::callable_traits::template arg<0>::type; - using ReplyType = typename std::remove_reference::type; + using ReplyType = std::decay_t; return dispatch(rpc_func, request, on_success, @@ -177,9 +177,11 @@ class Command : private DisabledCopyMove std::remove_reference_t::type>; static_assert( - std::is_same::value); + std::is_same_v, + ReturnCodeVariant>); static_assert( - std::is_same::value); + std::is_same_v, + ReturnCodeVariant>); static_assert(SuccessCallableTraits::num_args == 1); static_assert(std::is_base_of_v, @@ -194,7 +196,7 @@ class Command : private DisabledCopyMove static_assert(std::is_same_v, "`on_success` and `on_failure` should handle the same reply types"); } - static_assert(std::is_same::value); + static_assert(std::is_same_v, grpc::Status>); } template diff --git a/include/multipass/cli/csv_formatter.h b/include/multipass/cli/csv_formatter.h index 20c7112826c..470636a6ad0 100644 --- a/include/multipass/cli/csv_formatter.h +++ b/include/multipass/cli/csv_formatter.h @@ -30,5 +30,6 @@ class CSVFormatter final : public Formatter std::string format(const FindReply& list) const override; std::string format(const VersionReply& list, const std::string& client_version) const override; std::string format(const AliasDict& aliases) const override; + std::string format(const ZonesReply& reply) const override; }; } // namespace multipass diff --git a/include/multipass/cli/formatter.h b/include/multipass/cli/formatter.h index 4a35772a1cd..3a833aa87ee 100644 --- a/include/multipass/cli/formatter.h +++ b/include/multipass/cli/formatter.h @@ -40,6 +40,7 @@ class Formatter : private DisabledCopyMove virtual std::string format(const VersionReply& reply, const std::string& client_version) const = 0; virtual std::string format(const AliasDict& aliases) const = 0; + virtual std::string format(const ZonesReply& reply) const = 0; protected: Formatter() = default; diff --git a/include/multipass/cli/json_formatter.h b/include/multipass/cli/json_formatter.h index 25f605d6b2f..5873c9b71c0 100644 --- a/include/multipass/cli/json_formatter.h +++ b/include/multipass/cli/json_formatter.h @@ -30,5 +30,6 @@ class JsonFormatter final : public Formatter std::string format(const FindReply& list) const override; std::string format(const VersionReply& list, const std::string& client_version) const override; std::string format(const AliasDict& aliases) const override; + std::string format(const ZonesReply& reply) const override; }; } // namespace multipass diff --git a/include/multipass/cli/table_formatter.h b/include/multipass/cli/table_formatter.h index a89b1ea94a9..c5e9cb9a2ad 100644 --- a/include/multipass/cli/table_formatter.h +++ b/include/multipass/cli/table_formatter.h @@ -30,5 +30,6 @@ class TableFormatter final : public Formatter std::string format(const FindReply& list) const override; std::string format(const VersionReply& list, const std::string& client_version) const override; std::string format(const AliasDict& aliases) const override; + std::string format(const ZonesReply& reply) const override; }; } // namespace multipass diff --git a/include/multipass/cli/yaml_formatter.h b/include/multipass/cli/yaml_formatter.h index 7f07b6511bd..8c803c6710c 100644 --- a/include/multipass/cli/yaml_formatter.h +++ b/include/multipass/cli/yaml_formatter.h @@ -30,5 +30,6 @@ class YamlFormatter final : public Formatter std::string format(const FindReply& list) const override; std::string format(const VersionReply& list, const std::string& client_version) const override; std::string format(const AliasDict& aliases) const override; + std::string format(const ZonesReply& reply) const override; }; } // namespace multipass diff --git a/include/multipass/constants.h b/include/multipass/constants.h index a0089cf8115..57aeba0cd65 100644 --- a/include/multipass/constants.h +++ b/include/multipass/constants.h @@ -75,4 +75,5 @@ constexpr auto petenv_default = "primary"; constexpr auto timeout_exit_code = 5; constexpr auto authenticated_certs_dir = "authenticated-certs"; constexpr auto home_in_instance = "/home/ubuntu"; +constexpr auto default_zone_names = {"zone1", "zone2", "zone3"}; } // namespace multipass diff --git a/include/multipass/exceptions/availability_zone_exceptions.h b/include/multipass/exceptions/availability_zone_exceptions.h new file mode 100644 index 00000000000..c142fec7886 --- /dev/null +++ b/include/multipass/exceptions/availability_zone_exceptions.h @@ -0,0 +1,41 @@ +/* + * 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_AVAILABILITY_ZONE_EXCEPTIONS_H +#define MULTIPASS_AVAILABILITY_ZONE_EXCEPTIONS_H + +#include "formatted_exception_base.h" + +namespace multipass +{ +struct AvailabilityZoneNotFound final : FormattedExceptionBase<> +{ + explicit AvailabilityZoneNotFound(const std::string& name) + : FormattedExceptionBase{"no AZ with name {:?} found", name} + { + } +}; + +struct NoAvailabilityZoneAvailable final : std::runtime_error +{ + NoAvailabilityZoneAvailable() : std::runtime_error{"no AZ is available"} + { + } +}; +} // namespace multipass + +#endif // MULTIPASS_AVAILABILITY_ZONE_EXCEPTIONS_H diff --git a/include/multipass/ip_address.h b/include/multipass/ip_address.h index 66d081daf4f..b466e270e61 100644 --- a/include/multipass/ip_address.h +++ b/include/multipass/ip_address.h @@ -34,12 +34,8 @@ struct IPAddress [[nodiscard]] std::string as_string() const; [[nodiscard]] uint32_t as_uint32() const; - bool operator==(const IPAddress& other) const; - bool operator!=(const IPAddress& other) const; - bool operator<(const IPAddress& other) const; - bool operator<=(const IPAddress& other) const; - bool operator>(const IPAddress& other) const; - bool operator>=(const IPAddress& other) const; + std::strong_ordering operator<=>(const IPAddress& other) const; + bool operator==(const IPAddress& other) const = default; IPAddress operator+(int value) const; std::array octets; diff --git a/include/multipass/json_utils.h b/include/multipass/json_utils.h index b789ce2bba0..16507ca486d 100644 --- a/include/multipass/json_utils.h +++ b/include/multipass/json_utils.h @@ -29,6 +29,7 @@ #include +#include #include #include #include diff --git a/include/multipass/platform.h b/include/multipass/platform.h index e5ffa236076..db9980dd2cc 100644 --- a/include/multipass/platform.h +++ b/include/multipass/platform.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -75,6 +77,15 @@ class Platform : public Singleton virtual QString default_driver() const; virtual QString default_privileged_mounts() const; [[nodiscard]] virtual std::string bridge_nomenclature() const; + [[nodiscard]] virtual bool subnet_used_locally(Subnet subnet) const; + + // TODO(az): Remove this when AZs are feature-complete. + // When AZs are disabled, we maintain the old networking configuration as closely as possible. + // This means that each platform uses a different subnet for instances. When AZs are enabled, + // all platforms will use the same base subnet, so this function is unnecessary in that + // configuration. + [[nodiscard]] virtual Subnet get_preferred_subnet() const; + virtual int get_cpus() const; virtual long long get_total_ram() const; @@ -90,7 +101,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/single_availability_zone.h b/include/multipass/single_availability_zone.h new file mode 100644 index 00000000000..0d0490e5eb3 --- /dev/null +++ b/include/multipass/single_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 . + * + */ + +#pragma once + +#include "multipass/availability_zone.h" + +namespace multipass +{ +// When removing the AZ feature flag, move this to tests/unit/stub_availability_zone.h. +class SingleAvailabilityZone final : public AvailabilityZone +{ +public: + SingleAvailabilityZone(std::string name, Subnet subnet) + : name(std::move(name)), subnet(std::move(subnet)) + { + } + + const std::string& get_name() const override + { + return name; + } + + const Subnet& get_subnet() const override + { + 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 + { + } + +private: + std::string name; + Subnet subnet; +}; +} // namespace multipass diff --git a/include/multipass/single_availability_zone_manager.h b/include/multipass/single_availability_zone_manager.h new file mode 100644 index 00000000000..748e577f028 --- /dev/null +++ b/include/multipass/single_availability_zone_manager.h @@ -0,0 +1,51 @@ +/* + * 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 . + * + */ + +#pragma once + +#include "multipass/availability_zone_manager.h" +#include + +namespace multipass +{ +class SingleAvailabilityZoneManager final : public AvailabilityZoneManager +{ +public: + SingleAvailabilityZoneManager(const multipass::Path& data_dir); + + AvailabilityZone& get_zone(const std::string& name) override + { + return zone; + } + std::string get_automatic_zone_name() override + { + return zone.get_name(); + } + std::vector> get_zones() override + { + return {zone}; + } + + std::string get_default_zone_name() const override + { + return zone.get_name(); + } + +private: + SingleAvailabilityZone zone; +}; +} // namespace multipass diff --git a/include/multipass/subnet.h b/include/multipass/subnet.h new file mode 100644 index 00000000000..24b6a570968 --- /dev/null +++ b/include/multipass/subnet.h @@ -0,0 +1,155 @@ +/* + * 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 . + * + */ + +#pragma once + +#include +#include +#include + +#include + +#include "ip_address.h" + +namespace multipass +{ +class Subnet +{ +public: + struct PrefixLengthOutOfRange final : FormattedExceptionBase + { + template + explicit PrefixLengthOutOfRange(const T& value) + // Subnet masks of /31 or /32 require some special handling that we don't support. + : FormattedExceptionBase{ + "Subnet prefix length must be non-negative and less than 31: {}", + value} + { + } + }; + + class PrefixLength + { + public: + constexpr PrefixLength(uint8_t value) : value(value) + { + if (value >= 31) + throw PrefixLengthOutOfRange{value}; + } + + constexpr operator uint8_t() const noexcept + { + return value; + } + + private: + uint8_t value; + }; + + Subnet(IPAddress ip, PrefixLength prefix_length); + + Subnet(const std::string& cidr_string); + + // Return the smallest IP address available in this subnet + [[nodiscard]] IPAddress min_address() const; + // Return the largest IP address available in this subnet, excluding the broadcast address + [[nodiscard]] IPAddress max_address() const; + // Return the number of usable IP addresses in this subnet + [[nodiscard]] uint32_t usable_address_count() const; + + // Return the original IP address + [[nodiscard]] IPAddress address() const; + // Return the IP address with the subnet mask applied + [[nodiscard]] IPAddress masked_address() const; + // Return the broadcast address for this subnet + [[nodiscard]] IPAddress broadcast_address() const; + // Return the prefix length, e.g. the 24 in 192.168.1.0/24 + [[nodiscard]] PrefixLength prefix_length() const; + // Return the subnet mask as an IP, e.g. 255.255.255.0 + [[nodiscard]] IPAddress subnet_mask() const; + + // Return this subnet with the subnet mask applied to the IP address + [[nodiscard]] Subnet canonical() const; + + // Return a string representing this subnet in CIDR notation + [[nodiscard]] std::string to_cidr() const; + + // Return the number of subnets of size `prefix_length` that fit in this subnet + [[nodiscard]] size_t size(PrefixLength prefix_length) const; + + // Subnets are either disjoint or the smaller is a subset of the larger + [[nodiscard]] bool contains(Subnet other) const; + [[nodiscard]] bool contains(IPAddress ip) const; + + [[nodiscard]] std::strong_ordering operator<=>(const Subnet& other) const; + [[nodiscard]] bool operator==(const Subnet& other) const = default; + + friend void tag_invoke(const boost::json::value_from_tag&, + boost::json::value& json, + const Subnet& subnet) + { + json = subnet.to_cidr(); + } + + friend Subnet tag_invoke(const boost::json::value_to_tag&, + const boost::json::value& json) + { + return value_to(json); + } + +private: + IPAddress ip_address; + PrefixLength prefix; +}; + +// Allocate child subnets from a base subnet +class SubnetAllocator +{ +public: + SubnetAllocator(Subnet base_subnet, Subnet::PrefixLength prefix); + + [[nodiscard]] Subnet next_available(); + +private: + Subnet base_subnet; + Subnet::PrefixLength prefix; + size_t block_idx = 0; +}; +} // namespace multipass + +namespace fmt +{ +template <> +struct formatter : formatter +{ + template + auto format(const multipass::Subnet& subnet, FormatContext& ctx) const + { + return format_to(ctx.out(), "{}", subnet.to_cidr()); + } +}; + +template <> +struct formatter : formatter +{ + template + auto format(const multipass::Subnet::PrefixLength& prefix, FormatContext& ctx) const + { + return format_to(ctx.out(), "{}", uint8_t(prefix)); + } +}; +} // namespace fmt diff --git a/include/multipass/virtual_machine.h b/include/multipass/virtual_machine.h index 0e48318e300..c315d8c24a9 100644 --- a/include/multipass/virtual_machine.h +++ b/include/multipass/virtual_machine.h @@ -33,6 +33,7 @@ namespace multipass { +class AvailabilityZone; struct IPAddress; class MemorySize; class VMMount; @@ -53,7 +54,8 @@ class VirtualMachine : private DisabledCopyMove delayed_shutdown, suspending, suspended, - unknown + unknown, + unavailable, }; enum class ShutdownPolicy @@ -71,6 +73,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() @@ -121,6 +124,7 @@ class VirtualMachine : private DisabledCopyMove virtual QDir instance_directory() const = 0; virtual const std::string& get_name() const = 0; + virtual const AvailabilityZone& get_zone() const = 0; VirtualMachine::State state; std::condition_variable state_wait; @@ -169,6 +173,9 @@ struct fmt::formatter : fmt::formatter::format(v, ctx); diff --git a/include/multipass/virtual_machine_description.h b/include/multipass/virtual_machine_description.h index 313e35e4b77..9752b22c8b8 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 01544c1e32c..3017ab0c9ab 100644 --- a/include/multipass/vm_specs.h +++ b/include/multipass/vm_specs.h @@ -17,6 +17,7 @@ #pragma once +#include "availability_zone_manager.h" #include "memory_size.h" #include "network_interface.h" #include "virtual_machine.h" @@ -44,11 +45,14 @@ struct VMSpecs boost::json::object metadata; int clone_count = 0; // tracks the number of cloned vm from this source vm (regardless of deletes) + std::string zone; friend inline bool operator==(const VMSpecs& a, const VMSpecs& b) = default; }; void tag_invoke(const boost::json::value_from_tag&, boost::json::value& json, const VMSpecs& mount); -VMSpecs tag_invoke(const boost::json::value_to_tag&, const boost::json::value& json); +VMSpecs tag_invoke(const boost::json::value_to_tag&, + const boost::json::value& json, + const AvailabilityZoneManager& az_manager); } // namespace multipass diff --git a/src/client/cli/client.cpp b/src/client/cli/client.cpp index 313dc8ad618..28b7afdfb21 100644 --- a/src/client/cli/client.cpp +++ b/src/client/cli/client.cpp @@ -48,6 +48,12 @@ #include "cmd/version.h" #include "cmd/wait_ready.h" +#ifdef AVAILABILITY_ZONES_FEATURE +#include "cmd/disable_zones.h" +#include "cmd/enable_zones.h" +#include "cmd/zones.h" +#endif + #include #include #include @@ -114,6 +120,12 @@ mp::Client::Client(ClientConfig& config) add_command(); add_command(); +#ifdef AVAILABILITY_ZONES_FEATURE + add_command(); + add_command(); + add_command(); +#endif + sort_commands(); MP_CLIENT_PLATFORM.enable_ansi_escape_chars(); diff --git a/src/client/cli/cmd/CMakeLists.txt b/src/client/cli/cmd/CMakeLists.txt index 035c93aaa88..a1e4b59d044 100644 --- a/src/client/cli/cmd/CMakeLists.txt +++ b/src/client/cli/cmd/CMakeLists.txt @@ -46,8 +46,14 @@ add_library(commands STATIC umount.cpp unalias.cpp version.cpp - wait_ready.cpp -) + wait_ready.cpp) + +if(AVAILABILITY_ZONES_ENABLED) + target_sources(commands PRIVATE + disable_zones.cpp + enable_zones.cpp + zones.cpp) +endif() target_link_libraries(commands client_common diff --git a/src/client/cli/cmd/disable_zones.cpp b/src/client/cli/cmd/disable_zones.cpp new file mode 100644 index 00000000000..e714a9fdaf1 --- /dev/null +++ b/src/client/cli/cmd/disable_zones.cpp @@ -0,0 +1,150 @@ +/* + * 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 "disable_zones.h" + +#include "animated_spinner.h" +#include "common_callbacks.h" +#include "common_cli.h" +#include "multipass/cli/argparser.h" +#include "multipass/cli/client_common.h" + +#include + +namespace multipass::cmd +{ +ReturnCodeVariant DisableZones::run(ArgParser* parser) +{ + if (const auto ret = parse_args(parser); ret != ParseCode::Ok) + return parser->returnCodeFrom(ret); + + if (ask_for_confirmation) + { + if (!term->is_live()) + throw std::runtime_error{ + "Unable to query client for confirmation. Use '--force' to forcefully make " + "unavailable all instances in the specified zones."}; + + if (!confirm()) + return ReturnCode::CommandFail; + } + + AnimatedSpinner spinner{cout}; + const auto use_all_zones = request.zones().empty(); + const auto message = use_all_zones + ? "Disabling all zones" + : fmt::format("Disabling {}", fmt::join(request.zones(), ", ")); + spinner.start(message); + + const auto on_success = [&](const ZonesStateReply&) -> ReturnCodeVariant { + spinner.stop(); + const auto output_message = use_all_zones + ? "All zones disabled" + : fmt::format("Zone{} disabled: {}", + request.zones_size() == 1 ? "" : "s", + fmt::join(request.zones(), ", ")); + cout << output_message << std::endl; + return Ok; + }; + + const auto on_failure = [this, &spinner](const grpc::Status& status) -> ReturnCodeVariant { + spinner.stop(); + return standard_failure_handler_for(name(), cerr, status); + }; + + const auto streaming_callback = + make_logging_spinner_callback(spinner, cerr); + + return dispatch(&RpcMethod::zones_state, request, on_success, on_failure, streaming_callback); +} + +std::string DisableZones::name() const +{ + return "disable-zones"; +} + +QString DisableZones::short_help() const +{ + return QStringLiteral("Make zones unavailable"); +} + +QString DisableZones::description() const +{ + return QStringLiteral( + "Makes the given availability zones unavailable. Instances therein are " + "forcefully switched off and remain unavailable until their zone is re-enabled " + "(simulating a loss of availability on a cloud provider)."); +} + +ParseCode DisableZones::parse_args(ArgParser* parser) +{ + parser->addPositionalArgument("zone", + "Name of the zones to make unavailable", + " [ ...]"); + + QCommandLineOption all_option(all_option_name, "Disable all zones"); + QCommandLineOption forceOption{"force", "Do not ask for confirmation"}; + parser->addOptions({all_option, forceOption}); + + if (const auto status = parser->commandParse(this); status != ParseCode::Ok) + return status; + + if (const auto status = check_for_name_and_all_option_conflict(parser, cerr); + status != ParseCode::Ok) + return status; + + request.set_available(false); + request.set_verbosity_level(parser->verbosityLevel()); + + if (!parser->isSet(all_option_name)) + { + for (const auto& zone_name : parser->positionalArguments()) + request.add_zones(zone_name.toStdString()); + } + + ask_for_confirmation = !parser->isSet(forceOption); + + return ParseCode::Ok; +} + +bool DisableZones::confirm() const +{ + // joins zones by comma with an 'and' for the last one e.g. 'zone1, zone2 and zone3' + const auto format_zones = [this] { + if (request.zones().empty()) + return std::string("all zones"); + if (request.zones_size() == 1) + return request.zones(0); + + const auto last_zone = request.zones_size() - 1; + return fmt::format( + "{} and {}", + fmt::join(request.zones().begin(), request.zones().begin() + last_zone, ", "), + request.zones(last_zone)); + }; + const auto message = "This operation will forcefully stop the VMs in " + format_zones() + + ". Are you sure you want to continue? (Yes/no)"; + + const PlainPrompter prompter{term}; + auto answer = prompter.prompt(message); + while (!answer.empty() && !std::regex_match(answer, client::yes_answer) && + !std::regex_match(answer, client::no_answer)) + answer = prompter.prompt("Please answer (Yes/no)"); + + return answer.empty() || std::regex_match(answer, client::yes_answer); +} +} // namespace multipass::cmd diff --git a/src/client/cli/cmd/disable_zones.h b/src/client/cli/cmd/disable_zones.h new file mode 100644 index 00000000000..64d00c3f816 --- /dev/null +++ b/src/client/cli/cmd/disable_zones.h @@ -0,0 +1,41 @@ +/* + * 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_DISABLE_ZONES_H +#define MULTIPASS_DISABLE_ZONES_H + +#include + +namespace multipass::cmd +{ +class DisableZones : public Command +{ +public: + using Command::Command; + ReturnCodeVariant run(ArgParser* parser) override; + std::string name() const override; + QString short_help() const override; + QString description() const override; + +private: + bool ask_for_confirmation = true; + ZonesStateRequest request{}; + ParseCode parse_args(ArgParser* parser); + bool confirm() const; +}; +} // namespace multipass::cmd +#endif // MULTIPASS_DISABLE_ZONES_H diff --git a/src/client/cli/cmd/enable_zones.cpp b/src/client/cli/cmd/enable_zones.cpp new file mode 100644 index 00000000000..220af896ece --- /dev/null +++ b/src/client/cli/cmd/enable_zones.cpp @@ -0,0 +1,108 @@ +/* + * 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 "enable_zones.h" + +#include "animated_spinner.h" +#include "common_callbacks.h" +#include "common_cli.h" +#include "multipass/cli/argparser.h" +#include "multipass/cli/client_common.h" + +#include + +namespace multipass::cmd +{ +ReturnCodeVariant EnableZones::run(ArgParser* parser) +{ + if (const auto ret = parse_args(parser); ret != ParseCode::Ok) + return parser->returnCodeFrom(ret); + + AnimatedSpinner spinner{cout}; + const auto use_all_zones = request.zones().empty(); + const auto message = use_all_zones + ? "Enabling all zones" + : fmt::format("Enabling {}", fmt::join(request.zones(), ", ")); + spinner.start(message); + + const auto on_success = [&](const ZonesStateReply&) -> ReturnCodeVariant { + spinner.stop(); + const auto output_message = use_all_zones + ? "All zones enabled" + : fmt::format("Zone{} enabled: {}", + request.zones_size() == 1 ? "" : "s", + fmt::join(request.zones(), ", ")); + cout << output_message << std::endl; + return Ok; + }; + + const auto on_failure = [this, &spinner](const grpc::Status& status) -> ReturnCodeVariant { + spinner.stop(); + return standard_failure_handler_for(name(), cerr, status); + }; + + const auto streaming_callback = + make_logging_spinner_callback(spinner, cerr); + + return dispatch(&RpcMethod::zones_state, request, on_success, on_failure, streaming_callback); +} + +std::string EnableZones::name() const +{ + return "enable-zones"; +} + +QString EnableZones::short_help() const +{ + return QStringLiteral("Make zones available"); +} + +QString EnableZones::description() const +{ + return QStringLiteral( + "Makes the given availability zones available. Instances therein are started if they were " + "running when their zone was last disabled."); +} + +ParseCode EnableZones::parse_args(ArgParser* parser) +{ + parser->addPositionalArgument("zone", + "Name of the zones to make available", + " [ ...]"); + + QCommandLineOption all_option(all_option_name, "Enable all zones"); + parser->addOption(all_option); + + if (const auto status = parser->commandParse(this); status != ParseCode::Ok) + return status; + + if (const auto status = check_for_name_and_all_option_conflict(parser, cerr); + status != ParseCode::Ok) + return status; + + request.set_available(true); + request.set_verbosity_level(parser->verbosityLevel()); + + if (!parser->isSet(all_option_name)) + { + for (const auto& zone_name : parser->positionalArguments()) + request.add_zones(zone_name.toStdString()); + } + + return ParseCode::Ok; +} +} // namespace multipass::cmd diff --git a/src/client/cli/cmd/enable_zones.h b/src/client/cli/cmd/enable_zones.h new file mode 100644 index 00000000000..babe54bff53 --- /dev/null +++ b/src/client/cli/cmd/enable_zones.h @@ -0,0 +1,39 @@ +/* + * 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_ENABLE_ZONES_H +#define MULTIPASS_ENABLE_ZONES_H + +#include + +namespace multipass::cmd +{ +class EnableZones : public Command +{ +public: + using Command::Command; + ReturnCodeVariant run(ArgParser* parser) override; + std::string name() const override; + QString short_help() const override; + QString description() const override; + +private: + ZonesStateRequest request{}; + ParseCode parse_args(ArgParser* parser); +}; +} // namespace multipass::cmd +#endif // MULTIPASS_ENABLE_ZONES_H diff --git a/src/client/cli/cmd/launch.cpp b/src/client/cli/cmd/launch.cpp index 94f486c760c..b6f7fb3acd8 100644 --- a/src/client/cli/cmd/launch.cpp +++ b/src/client/cli/cmd/launch.cpp @@ -247,6 +247,9 @@ mp::ParseCode cmd::Launch::parse_args(mp::ArgParser* parser) "You can also use a shortcut of \"\" to mean \"name=\".", "spec"); QCommandLineOption bridgedOption("bridged", "Adds one `--network bridged` network."); +#ifdef AVAILABILITY_ZONES_FEATURE + QCommandLineOption zoneOption("zone", "The zone in which to launch the instance.", "zone"); +#endif QCommandLineOption mountOption( "mount", QStringLiteral("Mount a local directory inside the instance. If is omitted, the " @@ -255,14 +258,19 @@ mp::ParseCode cmd::Launch::parse_args(mp::ArgParser* parser) .arg(home_in_instance), "source>:addOptions({cpusOption, - diskOption, - memOption, - nameOption, - cloudInitOption, - networkOption, - bridgedOption, - mountOption}); + parser->addOptions({ + cpusOption, + diskOption, + memOption, + nameOption, + cloudInitOption, + networkOption, + bridgedOption, +#ifdef AVAILABILITY_ZONES_FEATURE + zoneOption, +#endif + mountOption, + }); mp::cmd::add_instance_timeout(parser); @@ -447,8 +455,10 @@ mp::ParseCode cmd::Launch::parse_args(mp::ArgParser* parser) try { if (parser->isSet(networkOption)) + { for (const auto& net : parser->values(networkOption)) request.mutable_network_options()->Add(net_digest(net)); + } request.set_timeout(mp::cmd::parse_timeout(parser)); } @@ -470,6 +480,19 @@ mp::ReturnCodeVariant cmd::Launch::request_launch(const ArgParser* parser) spinner = std::make_unique( cout); // Creating just in time to work around canonical/multipass#2075 +#ifdef AVAILABILITY_ZONES_FEATURE + if (parser->isSet("zone")) + { + auto zone = parser->value("zone").trimmed(); + if (zone.isEmpty()) + { + cerr << "Error: Empty zone specified with --zone option\n"; + return ReturnCode::CommandLineError; + } + request.set_zone(zone.toStdString()); + } +#endif + if (timer) timer->resume(); else if (parser->isSet("timeout")) @@ -538,7 +561,11 @@ mp::ReturnCodeVariant cmd::Launch::request_launch(const ArgParser* parser) } } +#ifdef AVAILABILITY_ZONES_FEATURE + cout << "Launched: " << reply.vm_instance_name() << " in " << reply.zone() << "\n"; +#else cout << "Launched: " << reply.vm_instance_name() << "\n"; +#endif if (term->is_live() && update_available(reply.update_info())) { @@ -592,6 +619,14 @@ mp::ReturnCodeVariant cmd::Launch::request_launch(const ArgParser* parser) "To troubleshoot, see " "https://documentation.ubuntu.com/multipass/stable/how-to-guides/troubleshoot/"; } + else if (error == LaunchError::INVALID_ZONE) + { + error_details = fmt::format("Invalid zone name supplied: {}", request.zone()); + } + else if (error == LaunchError::ZONE_UNAVAILABLE) + { + error_details = fmt::format("Unavailable zone name supplied: {}", request.zone()); + } } return standard_failure_handler_for(name(), cerr, status, error_details); diff --git a/src/client/cli/cmd/zones.cpp b/src/client/cli/cmd/zones.cpp new file mode 100644 index 00000000000..0c893cd5106 --- /dev/null +++ b/src/client/cli/cmd/zones.cpp @@ -0,0 +1,87 @@ +/* + * 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 "zones.h" +#include "common_cli.h" + +#include + +namespace multipass::cmd +{ +ReturnCodeVariant Zones::run(ArgParser* parser) +{ + if (const auto ret = parse_args(parser); ret != ParseCode::Ok) + return parser->returnCodeFrom(ret); + + auto on_success = [this](const ZonesReply& reply) -> ReturnCodeVariant { + cout << chosen_formatter->format(reply); + return Ok; + }; + + auto on_failure = [this](const grpc::Status& status) -> ReturnCodeVariant { + return standard_failure_handler_for(name(), cerr, status); + }; + + ZonesRequest request{}; + request.set_verbosity_level(parser->verbosityLevel()); + return dispatch(&RpcMethod::zones, request, on_success, on_failure); +} + +std::string Zones::name() const +{ + return "zones"; +} + +std::vector Zones::aliases() const +{ + return {name()}; +} + +QString Zones::short_help() const +{ + return QStringLiteral("List all availability zones"); +} + +QString Zones::description() const +{ + return QStringLiteral("List all availability zones, along with their availability status."); +} + +ParseCode Zones::parse_args(ArgParser* parser) +{ + QCommandLineOption formatOption{ + "format", + "Output list in the requested format.\nValid formats are: table (default), json, csv and " + "yaml", + "format", + "table", + }; + + parser->addOptions({formatOption}); + + if (const auto status = parser->commandParse(this); status != ParseCode::Ok) + return status; + + if (parser->positionalArguments().count() > 0) + { + cerr << "This command takes no arguments\n"; + return ParseCode::CommandLineError; + } + + return handle_format_option(parser, &chosen_formatter, cerr); +} +} // namespace multipass::cmd diff --git a/src/client/cli/cmd/zones.h b/src/client/cli/cmd/zones.h new file mode 100644 index 00000000000..681c0638bd0 --- /dev/null +++ b/src/client/cli/cmd/zones.h @@ -0,0 +1,42 @@ +/* + * 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_ZONES_H +#define MULTIPASS_ZONES_H + +#include +#include + +namespace multipass::cmd +{ +class Zones : public Command +{ +public: + using Command::Command; + ReturnCodeVariant run(ArgParser* parser) override; + + std::string name() const override; + std::vector aliases() const override; + QString short_help() const override; + QString description() const override; + +private: + ParseCode parse_args(ArgParser* parser); + Formatter* chosen_formatter; +}; +} // namespace multipass::cmd +#endif // MULTIPASS_ZONES_H diff --git a/src/client/cli/formatter/csv_formatter.cpp b/src/client/cli/formatter/csv_formatter.cpp index d629e84626b..d1463c4f951 100644 --- a/src/client/cli/formatter/csv_formatter.cpp +++ b/src/client/cli/formatter/csv_formatter.cpp @@ -88,7 +88,11 @@ std::string generate_instance_details(const mp::InfoReply reply) fmt::memory_buffer buf; fmt::format_to(std::back_inserter(buf), - "Name,State,Ipv4,Release,Image hash,Image release,Load,Disk usage,Disk " + "Name,State," +#ifdef AVAILABILITY_ZONES_FEATURE + "Zone,Zone available," +#endif + "Ipv4,Release,Image hash,Image release,Load,Disk usage,Disk " "total,Memory usage,Memory " "total,Mounts,AllIPv4,CPU(s){}\n", have_num_snapshots ? ",Snapshots" : ""); @@ -98,9 +102,17 @@ std::string generate_instance_details(const mp::InfoReply reply) const auto& instance_details = info.instance_info(); fmt::format_to(std::back_inserter(buf), - "{},{},{},{},{},{},{},{},{},{},{},{},{},{}{}\n", + "{},{}," +#ifdef AVAILABILITY_ZONES_FEATURE + "{},{}," +#endif + "{},{},{},{},{},{},{},{},{},{},{},{}{}\n", info.name(), mp::format::status_string_for(info.instance_status()), +#ifdef AVAILABILITY_ZONES_FEATURE + info.zone().name(), + info.zone().available(), +#endif instance_details.ipv4_size() ? instance_details.ipv4(0) : "", instance_details.current_release(), instance_details.id(), @@ -124,20 +136,35 @@ std::string generate_instances_list(const mp::InstancesList& instance_list) { fmt::memory_buffer buf; - fmt::format_to(std::back_inserter(buf), "Name,State,IPv4,Release,AllIPv4\n"); + fmt::format_to(std::back_inserter(buf), + "Name,State,IPv4,Release,AllIPv4" +#ifdef AVAILABILITY_ZONES_FEATURE + ",Zone,Zone available" +#endif + "\n"); for (const auto& instance : mp::format::sorted(instance_list.instances())) { fmt::format_to( std::back_inserter(buf), - "{},{},{},{},\"{}\"\n", + "{},{},{},{},\"{}\"" +#ifdef AVAILABILITY_ZONES_FEATURE + ",{},{}" +#endif + "\n", instance.name(), mp::format::status_string_for(instance.instance_status()), instance.ipv4_size() ? instance.ipv4(0) : "", instance.current_release().empty() ? "Not Available" : mp::utils::trim(fmt::format("{} {}", instance.os(), instance.current_release())), - fmt::join(instance.ipv4(), ",")); + fmt::join(instance.ipv4(), ",") +#ifdef AVAILABILITY_ZONES_FEATURE + , + instance.zone().name(), + instance.zone().available() +#endif + ); } return fmt::to_string(buf); @@ -269,3 +296,16 @@ std::string mp::CSVFormatter::format(const mp::AliasDict& aliases) const return fmt::to_string(buf); } + +std::string mp::CSVFormatter::format(const ZonesReply& reply) const +{ + fmt::memory_buffer buf; + fmt::format_to(std::back_inserter(buf), "Name,Available\n"); + + for (const auto& zone : reply.zones()) + { + fmt::format_to(std::back_inserter(buf), "{},{}\n", zone.name(), zone.available()); + } + + return fmt::to_string(buf); +} diff --git a/src/client/cli/formatter/json_formatter.cpp b/src/client/cli/formatter/json_formatter.cpp index e02406c6912..0cc3b268011 100644 --- a/src/client/cli/formatter/json_formatter.cpp +++ b/src/client/cli/formatter/json_formatter.cpp @@ -76,6 +76,9 @@ boost::json::object generate_instance_details(const mp::DetailedInfoItem& item) const auto& instance_details = item.instance_info(); boost::json::object instance_info = { +#ifdef AVAILABILITY_ZONES_FEATURE + {"zone", {{"name", item.zone().name()}, {"available", item.zone().available()}}}, +#endif {"state", mp::format::status_string_for(item.instance_status())}, {"image_hash", instance_details.id()}, {"image_release", instance_details.image_release()}, @@ -166,6 +169,10 @@ boost::json::value generate_instances_list(const mp::InstancesList& instance_lis {"state", mp::format::status_string_for(instance.instance_status())}, {"ipv4", boost::json::value_from(instance.ipv4())}, {"release", std::move(release)}, +#ifdef AVAILABILITY_ZONES_FEATURE + {"zone", + {{"name", instance.zone().name()}, {"available", instance.zone().available()}}}, +#endif }); } @@ -306,3 +313,17 @@ std::string mp::JsonFormatter::format(const mp::AliasDict& aliases) const { return pretty_print(boost::json::value_from(aliases)); } + +std::string mp::JsonFormatter::format(const ZonesReply& reply) const +{ + QJsonObject root_object; + + for (const auto& zone : reply.zones()) + { + QJsonObject zone_object; + zone_object["available"] = zone.available(); + root_object[QString::fromStdString(zone.name())] = zone_object; + } + + return MP_JSONUTILS.json_to_string(root_object); +} diff --git a/src/client/cli/formatter/table_formatter.cpp b/src/client/cli/formatter/table_formatter.cpp index 363dc713ed4..9dd7f86e4db 100644 --- a/src/client/cli/formatter/table_formatter.cpp +++ b/src/client/cli/formatter/table_formatter.cpp @@ -144,6 +144,13 @@ void generate_instance_details(Dest&& dest, const mp::DetailedInfoItem& item) "{:<16}{}\n", "State:", mp::format::status_string_for(item.instance_status())); +#ifdef AVAILABILITY_ZONES_FEATURE + fmt::format_to( + dest, + "{:<16}{}\n", + "Zone:", + fmt::format("{}({})", item.zone().name(), item.zone().available() ? "a" : "u/a")); +#endif if (instance_details.has_num_snapshots()) fmt::format_to(dest, "{:<16}{}\n", "Snapshots:", instance_details.num_snapshots()); @@ -258,8 +265,13 @@ std::string generate_instances_list(const mp::InstancesList& instance_list) 24); const std::string::size_type state_column_width = 18; const std::string::size_type ip_column_width = 17; + [[maybe_unused]] const std::string::size_type image_column_width = 20; - constexpr auto row_format = "{:<{}}{:<{}}{:<{}}{:<}\n"; + constexpr auto row_format = "{:<{}}{:<{}}{:<{}}" +#ifdef AVAILABILITY_ZONES_FEATURE + "{:<{}}" +#endif + "{:<}\n"; fmt::format_to(std::back_inserter(buf), row_format, name_col_header, @@ -268,7 +280,13 @@ std::string generate_instances_list(const mp::InstancesList& instance_list) state_column_width, "IPv4", ip_column_width, - "Image"); + "Image" +#ifdef AVAILABILITY_ZONES_FEATURE + , + image_column_width, + "Zone" +#endif + ); for (const auto& instance : mp::format::sorted(instance_list.instances())) { @@ -285,7 +303,13 @@ std::string generate_instances_list(const mp::InstancesList& instance_list) ip_column_width, instance.current_release().empty() ? "Not Available" - : mp::utils::trim(fmt::format("{} {}", instance.os(), instance.current_release()))); + : mp::utils::trim(fmt::format("{} {}", instance.os(), instance.current_release())) +#ifdef AVAILABILITY_ZONES_FEATURE + , + image_column_width, + fmt::format("{}({})", instance.zone().name(), instance.zone().available() ? "a" : "u/a") +#endif + ); for (int i = 1; i < ipv4_size; ++i) { @@ -297,7 +321,13 @@ std::string generate_instances_list(const mp::InstancesList& instance_list) state_column_width, instance.ipv4(i), instance.ipv4(i).size(), - ""); + "" +#ifdef AVAILABILITY_ZONES_FEATURE + , + 0, + "" +#endif + ); } } @@ -586,3 +616,38 @@ std::string mp::TableFormatter::format(const mp::AliasDict& aliases) const return fmt::to_string(buf); } + +std::string mp::TableFormatter::format(const ZonesReply& reply) const +{ + fmt::memory_buffer buf; + + const auto& zones = reply.zones(); + + if (zones.empty()) + return "No availabilty zones found.\n"; + + const std::string name_col_header = "Name"; + const auto name_column_width = mp::format::column_width( + zones.begin(), + zones.end(), + [](const auto& zone) -> int { return zone.name().length(); }, + name_col_header.length()); + + constexpr auto row_format = "{:<{}}{:<}\n"; + fmt::format_to(std::back_inserter(buf), + row_format, + name_col_header, + name_column_width, + "State"); + + for (const auto& zone : zones) + { + fmt::format_to(std::back_inserter(buf), + row_format, + zone.name(), + name_column_width, + zone.available() ? "Available" : "Unavailable"); + } + + return fmt::to_string(buf); +} diff --git a/src/client/cli/formatter/yaml_formatter.cpp b/src/client/cli/formatter/yaml_formatter.cpp index a72fae2e5ad..0b58a36d042 100644 --- a/src/client/cli/formatter/yaml_formatter.cpp +++ b/src/client/cli/formatter/yaml_formatter.cpp @@ -102,6 +102,11 @@ YAML::Node generate_instance_details(const mp::DetailedInfoItem& item) YAML::Node instance_node; instance_node["state"] = mp::format::status_string_for(item.instance_status()); +#ifdef AVAILABILITY_ZONES_FEATURE + instance_node["zone"] = YAML::Node{}; + instance_node["zone"]["name"] = item.zone().name(); + instance_node["zone"]["available"] = item.zone().available(); +#endif if (instance_details.has_num_snapshots()) instance_node["snapshot_count"] = instance_details.num_snapshots(); @@ -190,6 +195,11 @@ std::string generate_instances_list(const mp::InstancesList& instance_list) { YAML::Node instance_node; instance_node["state"] = mp::format::status_string_for(instance.instance_status()); +#ifdef AVAILABILITY_ZONES_FEATURE + instance_node["zone"] = YAML::Node{}; + instance_node["zone"]["name"] = instance.zone().name(); + instance_node["zone"]["available"] = instance.zone().available(); +#endif instance_node["ipv4"] = YAML::Node(YAML::NodeType::Sequence); for (const auto& ip : instance.ipv4()) @@ -351,3 +361,17 @@ std::string mp::YamlFormatter::format(const mp::AliasDict& aliases) const return mpu::emit_yaml(aliases_list); } + +std::string mp::YamlFormatter::format(const mp::ZonesReply& reply) const +{ + YAML::Node root_node; + + for (const auto& zone : reply.zones()) + { + YAML::Node zone_node; + zone_node["available"] = zone.available(); + root_node[zone.name()] = zone_node; + } + + return mpu::emit_yaml(root_node); +} diff --git a/src/client/gui/pubspec.lock b/src/client/gui/pubspec.lock index 81aaee4ce97..b6485b572c8 100644 --- a/src/client/gui/pubspec.lock +++ b/src/client/gui/pubspec.lock @@ -550,10 +550,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -963,26 +963,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.15" tray_menu: dependency: "direct main" description: diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp index f17164b823f..2d5aa9846fc 100644 --- a/src/daemon/daemon.cpp +++ b/src/daemon/daemon.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -225,7 +226,8 @@ auto name_from(const std::string& requested_name, } std::unordered_map load_db(const mp::Path& data_path, - const mp::Path& cache_path) + const mp::Path& cache_path, + const mp::AvailabilityZoneManager& az_manager) { QDir data_dir{data_path}; QDir cache_dir{cache_path}; @@ -256,7 +258,7 @@ std::unordered_map load_db(const mp::Path& data_path, try { - reconstructed_records.emplace(key, value_to(record)); + reconstructed_records.emplace(key, value_to(record, az_manager)); } catch (mp::GhostInstanceException&) { @@ -425,7 +427,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->vault && "null ptr somewhere..."); + assert(config && config->factory && config->vault && config->az_manager && + "null ptr somewhere..."); validate_image(request, *config->vault); static const auto min_mem = try_mem_size(mp::min_memory_size); @@ -435,6 +438,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 = @@ -465,6 +469,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); @@ -474,15 +488,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; } @@ -514,6 +532,8 @@ auto connect_rpc(mp::DaemonRpc& rpc, mp::Daemon& daemon) QObject::connect(&rpc, &mp::DaemonRpc::on_restore, &daemon, &mp::Daemon::restore); QObject::connect(&rpc, &mp::DaemonRpc::on_daemon_info, &daemon, &mp::Daemon::daemon_info); QObject::connect(&rpc, &mp::DaemonRpc::on_wait_ready, &daemon, &mp::Daemon::wait_ready); + QObject::connect(&rpc, &mp::DaemonRpc::on_zones, &daemon, &mp::Daemon::zones); + QObject::connect(&rpc, &mp::DaemonRpc::on_zones_state, &daemon, &mp::Daemon::zones_state); } enum class InstanceGroup @@ -929,6 +949,8 @@ mp::InstanceStatus::Status grpc_instance_status_for(const mp::VirtualMachine::St return mp::InstanceStatus::SUSPENDING; case mp::VirtualMachine::State::suspended: return mp::InstanceStatus::SUSPENDED; + case mp::VirtualMachine::State::unavailable: + return mp::InstanceStatus::UNAVAILABLE; case mp::VirtualMachine::State::unknown: default: return mp::InstanceStatus::UNKNOWN; @@ -1260,11 +1282,12 @@ void populate_snapshot_info(mp::VirtualMachine& vm, 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()))}, + 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()), + *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, @@ -1336,6 +1359,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, @@ -1798,6 +1822,9 @@ try auto present_state = vm.current_state(); auto entry = response.mutable_instance_list()->add_instances(); entry->set_name(name); + const auto zone = entry->mutable_zone(); + zone->set_name(vm.get_zone().get_name()); + zone->set_available(vm.get_zone().is_available()); if (deleted) entry->mutable_instance_status()->set_status(mp::InstanceStatus::DELETED); else @@ -1965,6 +1992,12 @@ try continue; } + if (vm->current_state() == VirtualMachine::State::unavailable) + { + add_fmt_to(errors, "instance '{}' is not available", name); + continue; + } + auto& vm_mounts = mounts[name]; if (vm_mounts.find(target_path) != vm_mounts.end()) { @@ -2141,9 +2174,13 @@ try continue; } case VirtualMachine::State::suspending: + case VirtualMachine::State::unavailable: + // TODO: format State directly fmt::format_to(std::back_inserter(start_errors), - "Cannot start the instance '{}' while suspending.", - name); + "Cannot start the instance '{}' while {}.", + name, + vm.current_state() == VirtualMachine::State::suspending ? "suspending" + : "unavailable"); continue; case VirtualMachine::State::delayed_shutdown: delayed_shutdown_instances.erase(name); @@ -2246,6 +2283,14 @@ try if (status.ok()) { status = cmd_vms(instance_selection.operative_selection, [this](auto& vm) { + if (vm.current_state() == VirtualMachine::State::unavailable) + { + mpl::log(mpl::Level::info, + vm.get_name(), + "Ignoring suspend since instance is unavailable."); + return grpc::Status::OK; + } + stop_mounts(vm.get_name()); vm.suspend(); @@ -2367,6 +2412,14 @@ try { const auto& instance_name = vm_it->first; + if (vm_it->second->current_state() == VirtualMachine::State::unavailable) + { + mpl::log(mpl::Level::info, + instance_name, + "Ignoring delete since instance is unavailable."); + continue; + } + auto snapshot_pick_it = instance_snapshots_map.find(instance_name); const auto& [pick, all] = snapshot_pick_it == instance_snapshots_map.end() ? SnapshotPick{{}, true} @@ -2414,12 +2467,19 @@ try const auto& name = path_entry.instance_name(); const auto target_path = mpu::normalize_path(path_entry.target_path()); - if (operative_instances.find(name) == operative_instances.end()) + auto vm = operative_instances.find(name); + if (vm == operative_instances.end()) { add_fmt_to(errors, "instance '{}' does not exist", name); continue; } + if (vm->second->current_state() == VirtualMachine::State::unavailable) + { + mpl::log(mpl::Level::info, name, "Ignoring umount since instance unavailable."); + continue; + } + auto& vm_spec_mounts = vm_instance_specs[name].mounts; auto& vm_mounts = mounts[name]; @@ -2951,6 +3011,65 @@ catch (const std::exception& e) status_promise->set_value(grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, e.what(), "")); } +void mp::Daemon::zones(const ZonesRequest* request, + grpc::ServerReaderWriterInterface* server, + std::promise* status_promise) // clang-format off +try // clang-format on +{ + mpl::ClientLogger logger{mpl::level_from(request->verbosity_level()), *config->logger, server}; + + 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{}); +} +catch (const std::exception& e) +{ + status_promise->set_value(grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, e.what(), "")); +} + +void mp::Daemon::zones_state( + const ZonesStateRequest* request, + grpc::ServerReaderWriterInterface* server, + std::promise* status_promise) // clang-format off +try // clang-format on +{ + mpl::ClientLogger logger{mpl::level_from(request->verbosity_level()), *config->logger, server}; + + auto& az_manager = *config->az_manager; + if (request->zones().empty()) + { + for (auto&& zone : az_manager.get_zones()) + { + az_manager.get_zone(zone.get().get_name()).set_available(request->available()); + } + } + else + { + for (const auto& zone_name : request->zones()) + { + 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(), "")); +} + void mp::Daemon::on_shutdown() { } @@ -3068,6 +3187,9 @@ void mp::Daemon::create_vm(const CreateRequest* request, auto name = name_from(checked_args.instance_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, @@ -3104,16 +3226,20 @@ void mp::Daemon::create_vm(const CreateRequest* request, { auto vm_desc = prepare_future_watcher->future().result(); - 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, - {}}; + 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, + {}, + 0, + vm_desc.zone, + }; operative_instances[name] = config->factory->create_virtual_machine(vm_desc, *config->ssh_key_provider, @@ -3130,14 +3256,16 @@ void mp::Daemon::create_vm(const CreateRequest* request, operative_instances[name]->start(); - auto future_watcher = create_future_watcher([this, server, name] { - LaunchReply reply; - reply.set_vm_instance_name(name); - config->update_prompt->populate_if_time_to_show( - reply.mutable_update_info()); + auto future_watcher = + create_future_watcher([this, server, name, zone = vm_desc.zone] { + LaunchReply reply; + reply.set_vm_instance_name(name); + config->update_prompt->populate_if_time_to_show( + reply.mutable_update_info()); - server->Write(reply); - }); + reply.set_zone(zone); + server->Write(reply); + }); future_watcher->setFuture(QtConcurrent::run( &Daemon::async_wait_for_ready_all, this, @@ -3169,14 +3297,19 @@ void mp::Daemon::create_vm(const CreateRequest* request, prepare_future_watcher->deleteLater(); }); - auto make_vm_description = [this, server, request, name, checked_args, log_level]() mutable + auto make_vm_description = + [this, server, request, name, zone_name, checked_args, log_level]() mutable -> mp::VirtualMachineDescription { mpl::ClientLogger logger{log_level, *config->logger, server}; try { CreateReply reply; - reply.set_create_message("Creating " + name); +#if AVAILABILITY_ZONES_FEATURE + reply.set_create_message(fmt::format("Creating {} in {}", name, zone_name)); +#else + reply.set_create_message(fmt::format("Creating {}", name)); +#endif server->Write(reply); Query query; @@ -3185,6 +3318,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, @@ -3741,6 +3875,10 @@ void mp::Daemon::populate_instance_info(VirtualMachine& vm, const auto& name = vm.get_name(); info->set_name(name); + const auto zone = info->mutable_zone(); + const auto& az = vm.get_zone(); + zone->set_name(az.get_name()); + zone->set_available(az.is_available()); if (deleted) info->mutable_instance_status()->set_status(mp::InstanceStatus::DELETED); diff --git a/src/daemon/daemon.h b/src/daemon/daemon.h index f586e070e4d..2929cbda33e 100644 --- a/src/daemon/daemon.h +++ b/src/daemon/daemon.h @@ -166,6 +166,13 @@ public slots: const DaemonInfoRequest* request, grpc::ServerReaderWriterInterface* server, std::promise* status_promise); + virtual void zones(const ZonesRequest* request, + grpc::ServerReaderWriterInterface* server, + std::promise* status_promise); + virtual void zones_state( + const ZonesStateRequest* request, + grpc::ServerReaderWriterInterface* server, + std::promise* status_promise); virtual void wait_ready( const WaitReadyRequest* request, diff --git a/src/daemon/daemon_config.cpp b/src/daemon/daemon_config.cpp index 427f3dcdb1c..12d08bbf34c 100644 --- a/src/daemon/daemon_config.cpp +++ b/src/daemon/daemon_config.cpp @@ -32,6 +32,12 @@ #include #include +#ifdef AVAILABILITY_ZONES_FEATURE +#include +#else +#include +#endif + #include #include #include @@ -135,8 +141,14 @@ 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) +#ifdef AVAILABILITY_ZONES_FEATURE + az_manager = std::make_unique(data_directory.toStdString()); +#else + az_manager = std::make_unique(data_directory); +#endif 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()) @@ -230,6 +242,7 @@ std::unique_ptr mp::DaemonConfigBuilder::build() std::move(update_prompt), multiplexing_logger, std::move(network_proxy), + std::move(az_manager), cache_directory, data_directory, server_address, diff --git a/src/daemon/daemon_config.h b/src/daemon/daemon_config.h index c33982c0ad2..cd2f9d61d18 100644 --- a/src/daemon/daemon_config.h +++ b/src/daemon/daemon_config.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -53,6 +54,7 @@ struct DaemonConfig const std::unique_ptr update_prompt; const std::shared_ptr logger; const std::unique_ptr network_proxy; + const AvailabilityZoneManager::UPtr az_manager; const multipass::Path cache_directory; const multipass::Path data_directory; const std::string server_address; @@ -73,6 +75,7 @@ struct DaemonConfigBuilder std::unique_ptr update_prompt; std::unique_ptr logger; std::unique_ptr network_proxy; + AvailabilityZoneManager::UPtr az_manager; multipass::Path cache_directory; multipass::Path data_directory; std::string server_address; diff --git a/src/daemon/daemon_rpc.cpp b/src/daemon/daemon_rpc.cpp index d4a40b986a3..90a6f4fd103 100644 --- a/src/daemon/daemon_rpc.cpp +++ b/src/daemon/daemon_rpc.cpp @@ -470,6 +470,29 @@ grpc::Status mp::DaemonRpc::wait_ready( client_cert_from(context)); } +grpc::Status mp::DaemonRpc::zones(grpc::ServerContext* context, + grpc::ServerReaderWriter* server) +{ + ZonesRequest request; + server->Read(&request); + + return verify_client_and_dispatch_operation( + std::bind(&DaemonRpc::on_zones, this, &request, server, std::placeholders::_1), + client_cert_from(context)); +} + +grpc::Status mp::DaemonRpc::zones_state( + grpc::ServerContext* context, + grpc::ServerReaderWriter* server) +{ + ZonesStateRequest request; + server->Read(&request); + + return verify_client_and_dispatch_operation( + std::bind(&DaemonRpc::on_zones_state, this, &request, server, std::placeholders::_1), + client_cert_from(context)); +} + template grpc::Status mp::DaemonRpc::verify_client_and_dispatch_operation(OperationSignal signal, const std::string& client_cert) diff --git a/src/daemon/daemon_rpc.h b/src/daemon/daemon_rpc.h index 66a3087a84f..4764d45b5e5 100644 --- a/src/daemon/daemon_rpc.h +++ b/src/daemon/daemon_rpc.h @@ -135,6 +135,12 @@ class DaemonRpc : public QObject, public multipass::Rpc::Service, private Disabl void on_wait_ready(const WaitReadyRequest* request, grpc::ServerReaderWriter* server, std::promise* status_promise); + void on_zones(const ZonesRequest* request, + grpc::ServerReaderWriter* server, + std::promise* status_promise); + void on_zones_state(const ZonesStateRequest* request, + grpc::ServerReaderWriter* server, + std::promise* status_promise); private: template @@ -207,5 +213,10 @@ class DaemonRpc : public QObject, public multipass::Rpc::Service, private Disabl grpc::Status wait_ready( grpc::ServerContext* context, grpc::ServerReaderWriter* server) override; + grpc::Status zones(grpc::ServerContext* context, + grpc::ServerReaderWriter* server) override; + grpc::Status zones_state( + grpc::ServerContext* context, + grpc::ServerReaderWriter* server) override; }; } // namespace multipass diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index c0e2654da31..aaa367907db 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -15,6 +15,7 @@ set(CMAKE_AUTOMOC ON) add_library(network STATIC + subnet.cpp url_downloader.cpp) add_library(ip_address STATIC @@ -26,3 +27,6 @@ target_link_libraries(network utils Qt6::Core Qt6::Network) + +target_link_libraries(ip_address + fmt::fmt-header-only) diff --git a/src/network/ip_address.cpp b/src/network/ip_address.cpp index 3966c01ed9c..fd7311f4e8e 100644 --- a/src/network/ip_address.cpp +++ b/src/network/ip_address.cpp @@ -20,6 +20,8 @@ #include #include +#include + namespace mp = multipass; namespace @@ -29,10 +31,9 @@ uint8_t as_octet(uint32_t value) return static_cast(value); } -void check_range(int value) +bool is_valid_octet(int value) { - if (value < 0 || value > 255) - throw std::invalid_argument("invalid IP octet"); + return value >= 0 && value < 256; } std::array parse(const std::string& ip) @@ -45,10 +46,8 @@ std::array parse(const std::string& ip) std::stringstream s(ip); s >> a >> ch >> b >> ch >> c >> ch >> d; - check_range(a); - check_range(b); - check_range(c); - check_range(d); + if (!is_valid_octet(a) || !is_valid_octet(b) || !is_valid_octet(c) || !is_valid_octet(d)) + throw std::invalid_argument(fmt::format("invalid IP address {}", ip)); return {{as_octet(a), as_octet(b), as_octet(c), as_octet(d)}}; } @@ -89,34 +88,10 @@ uint32_t mp::IPAddress::as_uint32() const return value; } -bool mp::IPAddress::operator==(const IPAddress& other) const -{ - return octets == other.octets; -} - -bool mp::IPAddress::operator!=(const IPAddress& other) const -{ - return octets != other.octets; -} - -bool mp::IPAddress::operator<(const IPAddress& other) const -{ - return as_uint32() < other.as_uint32(); -} - -bool mp::IPAddress::operator<=(const IPAddress& other) const -{ - return as_uint32() <= other.as_uint32(); -} - -bool mp::IPAddress::operator>(const IPAddress& other) const -{ - return as_uint32() > other.as_uint32(); -} - -bool mp::IPAddress::operator>=(const IPAddress& other) const +// uint8_t is not required to support <=> by the standard. Appease Apple clang. +std::strong_ordering mp::IPAddress::operator<=>(const IPAddress& other) const { - return as_uint32() >= other.as_uint32(); + return as_uint32() <=> other.as_uint32(); } mp::IPAddress mp::IPAddress::operator+(int value) const diff --git a/src/network/subnet.cpp b/src/network/subnet.cpp new file mode 100644 index 00000000000..6d92ab88070 --- /dev/null +++ b/src/network/subnet.cpp @@ -0,0 +1,199 @@ +/* + * 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 "multipass/platform.h" + +#include +#include +#include +#include + +#include + +#include +#include + +namespace mp = multipass; + +namespace +{ +[[nodiscard]] mp::Subnet parse(const std::string& cidr_string) +try +{ + if (auto i = cidr_string.find('/'); i != std::string::npos) + { + mp::IPAddress addr{cidr_string.substr(0, i)}; + + const auto prefix_length = std::stoul(cidr_string.substr(i + 1)); + // Subnet masks of /31 or /32 require some special handling that we don't support. + if (prefix_length >= 31) + throw mp::Subnet::PrefixLengthOutOfRange(prefix_length); + + return mp::Subnet(addr, prefix_length); + } + throw std::invalid_argument( + fmt::format("CIDR {:?} does not contain '/' seperator", cidr_string)); +} +catch (const mp::Subnet::PrefixLengthOutOfRange&) +{ + throw; +} +catch (const std::out_of_range& e) +{ + throw mp::Subnet::PrefixLengthOutOfRange(e.what()); +} + +[[nodiscard]] mp::IPAddress get_subnet_mask(mp::Subnet::PrefixLength prefix_length) +{ + const uint32_t mask = (prefix_length == 0) ? 0 : ~uint32_t{0} << (32 - prefix_length); + return mp::IPAddress{mask}; +} + +[[nodiscard]] mp::IPAddress apply_mask(mp::IPAddress ip, mp::Subnet::PrefixLength prefix_length) +{ + const auto mask = get_subnet_mask(prefix_length); + return mp::IPAddress{ip.as_uint32() & mask.as_uint32()}; +} +} // namespace + +mp::Subnet::Subnet(IPAddress ip, PrefixLength prefix_length) : ip_address(ip), prefix(prefix_length) +{ +} + +mp::Subnet::Subnet(const std::string& cidr_string) : Subnet(parse(cidr_string)) +{ +} + +mp::IPAddress mp::Subnet::min_address() const +{ + return masked_address() + 1; +} + +mp::IPAddress mp::Subnet::max_address() const +{ + // address + 2^(32 - prefix) - 1 - 1 + // address + 2^(32 - prefix) is the next subnet's network address for this prefix length + // subtracting 1 to stay in this subnet and another 1 to exclude the broadcast address + return masked_address() + ((1ull << (32ull - prefix)) - 2ull); +} + +uint32_t mp::Subnet::usable_address_count() const +{ + return max_address().as_uint32() - min_address().as_uint32() + 1; +} + +mp::IPAddress mp::Subnet::address() const +{ + return ip_address; +} + +mp::IPAddress mp::Subnet::masked_address() const +{ + return apply_mask(ip_address, prefix); +} + +mp::IPAddress mp::Subnet::broadcast_address() const +{ + const auto mask = get_subnet_mask(prefix); + return mp::IPAddress{ip_address.as_uint32() | ~mask.as_uint32()}; +} + +mp::Subnet::PrefixLength mp::Subnet::prefix_length() const +{ + return prefix; +} + +mp::IPAddress mp::Subnet::subnet_mask() const +{ + return ::get_subnet_mask(prefix); +} + +mp::Subnet mp::Subnet::canonical() const +{ + return Subnet{masked_address(), prefix}; +} + +// uses CIDR notation +std::string mp::Subnet::to_cidr() const +{ + return fmt::format("{}/{}", ip_address.as_string(), prefix); +} + +size_t mp::Subnet::size(mp::Subnet::PrefixLength prefix_length) const +{ + if (prefix_length < this->prefix_length()) + return 0; + + // a range with prefix /16 has 65536 prefix /32 networks, + // a range with prefix /24 has 256 prefix /32 networks, + // so a prefix /16 network can hold 65536 / 256 = 256 prefix /24 networks. + // ex. 2^(24 - 16) = 256, [192.168.0.0/24, 192.168.255.0/24] + return std::size_t{1} << (prefix_length - this->prefix_length()); +} + +bool mp::Subnet::contains(Subnet other) const +{ + // can't possibly contain a larger subnet + if (other.prefix_length() < prefix) + return false; + + return contains(other.masked_address()); +} + +bool mp::Subnet::contains(IPAddress ip) const +{ + return masked_address() <= ip && broadcast_address() >= ip; +} + +std::strong_ordering mp::Subnet::operator<=>(const Subnet& other) const +{ + if (const auto ip_res = ip_address <=> other.ip_address; ip_res != 0) + return ip_res; + // note the prefix_length operands are purposely flipped + return other.prefix <=> prefix; +} + +mp::SubnetAllocator::SubnetAllocator(Subnet base_subnet, Subnet::PrefixLength prefix) + : base_subnet(base_subnet), prefix(prefix) +{ + if (base_subnet.size(prefix) == 0) + throw std::logic_error( + fmt::format("A subnet with prefix length {} cannot be contained by {}", + prefix, + base_subnet)); +} + +mp::Subnet mp::SubnetAllocator::next_available() +{ + const size_t possible_subnets = base_subnet.size(prefix); + + while (block_idx < possible_subnets) + { + // ex. 192.168.0.0 + (4 * 2^(32 - 24)) = 192.168.0.0 + 1024 = 192.168.4.0 + mp::IPAddress address = + base_subnet.masked_address() + (block_idx * (std::size_t{1} << (32 - prefix))); + + ++block_idx; + mp::Subnet subnet{address, prefix}; + if (!MP_PLATFORM.subnet_used_locally(subnet)) + return subnet; + } + + throw std::invalid_argument(fmt::format("{} is greater than the largest subnet block index {}", + block_idx, + possible_subnets - 1)); +} diff --git a/src/platform/backends/applevz/applevz_virtual_machine.cpp b/src/platform/backends/applevz/applevz_virtual_machine.cpp index 1843c7a1fea..e3169f836e9 100644 --- a/src/platform/backends/applevz/applevz_virtual_machine.cpp +++ b/src/platform/backends/applevz/applevz_virtual_machine.cpp @@ -37,8 +37,11 @@ namespace multipass::applevz AppleVZVirtualMachine::AppleVZVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir) - : BaseVirtualMachine{desc.vm_name, key_provider, instance_dir}, desc{desc}, monitor{&monitor} + : BaseVirtualMachine{desc.vm_name, key_provider, zone, instance_dir}, + desc{desc}, + monitor{&monitor} { initialize_vm_handle(); } diff --git a/src/platform/backends/applevz/applevz_virtual_machine.h b/src/platform/backends/applevz/applevz_virtual_machine.h index e9d97d0fb2d..d086264d483 100644 --- a/src/platform/backends/applevz/applevz_virtual_machine.h +++ b/src/platform/backends/applevz/applevz_virtual_machine.h @@ -35,6 +35,7 @@ class AppleVZVirtualMachine : public BaseVirtualMachine AppleVZVirtualMachine(const VirtualMachineDescription& desc, VMStatusMonitor& monitor, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir); ~AppleVZVirtualMachine(); diff --git a/src/platform/backends/applevz/applevz_virtual_machine_factory.cpp b/src/platform/backends/applevz/applevz_virtual_machine_factory.cpp index c11b47f2469..5276a5bf406 100644 --- a/src/platform/backends/applevz/applevz_virtual_machine_factory.cpp +++ b/src/platform/backends/applevz/applevz_virtual_machine_factory.cpp @@ -23,9 +23,11 @@ namespace mp = multipass; namespace multipass::applevz { -AppleVZVirtualMachineFactory::AppleVZVirtualMachineFactory(const Path& data_dir) +AppleVZVirtualMachineFactory::AppleVZVirtualMachineFactory(const Path& data_dir, + AvailabilityZoneManager& az_manager) : BaseVirtualMachineFactory( - MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir)) + MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir), + az_manager) { } @@ -38,6 +40,7 @@ VirtualMachine::UPtr AppleVZVirtualMachineFactory::create_virtual_machine( desc, monitor, key_provider, + az_manager.get_zone(desc.zone), get_instance_directory(desc.vm_name)); } @@ -77,6 +80,7 @@ VirtualMachine::UPtr AppleVZVirtualMachineFactory::clone_vm_impl( desc, monitor, key_provider, + az_manager.get_zone(desc.zone), get_instance_directory(desc.vm_name)); } } // namespace multipass::applevz diff --git a/src/platform/backends/applevz/applevz_virtual_machine_factory.h b/src/platform/backends/applevz/applevz_virtual_machine_factory.h index 1a9fd30d83b..79e5fcbc3bc 100644 --- a/src/platform/backends/applevz/applevz_virtual_machine_factory.h +++ b/src/platform/backends/applevz/applevz_virtual_machine_factory.h @@ -26,7 +26,8 @@ namespace multipass::applevz class AppleVZVirtualMachineFactory final : public BaseVirtualMachineFactory { public: - explicit AppleVZVirtualMachineFactory(const Path& data_dir); + explicit AppleVZVirtualMachineFactory(const Path& data_dir, + AvailabilityZoneManager& az_manager); [[nodiscard]] VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc, const SSHKeyProvider& key_provider, diff --git a/src/platform/backends/hyperv/hyperv_virtual_machine.cpp b/src/platform/backends/hyperv/hyperv_virtual_machine.cpp index 47f7dd8a1d1..a3b107e4728 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine.cpp +++ b/src/platform/backends/hyperv/hyperv_virtual_machine.cpp @@ -148,8 +148,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})) { @@ -202,8 +203,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({"Export-VM", @@ -266,9 +268,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)}, @@ -405,9 +408,12 @@ void mp::HyperVVirtualMachine::suspend() handle_state_update(); } } - else if (present_state == State::stopped) + else if (present_state == State::stopped || present_state == State::unavailable) { - mpl::info(vm_name, "Ignoring suspend issued while stopped"); + // TODO: format state directly + mpl::info(vm_name, + "Ignoring suspend since instance is {}", + (present_state == State::unavailable) ? "unavailable" : "stopped"); } monitor->on_suspend(); diff --git a/src/platform/backends/hyperv/hyperv_virtual_machine.h b/src/platform/backends/hyperv/hyperv_virtual_machine.h index 1c58acbbc5b..336e5a3ac73 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine.h +++ b/src/platform/backends/hyperv/hyperv_virtual_machine.h @@ -42,6 +42,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, @@ -49,6 +50,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; @@ -81,6 +83,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 9b0437330c3..2e050e6135c 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine_factory.cpp +++ b/src/platform/backends/hyperv/hyperv_virtual_machine_factory.cpp @@ -270,9 +270,11 @@ std::string error_msg_helper(const std::string& msg_core, const QString& ps_outp } } // namespace -mp::HyperVVirtualMachineFactory::HyperVVirtualMachineFactory(const mp::Path& data_dir) +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)) + MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir), + az_manager) { } @@ -284,6 +286,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)); } @@ -479,5 +482,6 @@ mp::VirtualMachine::UPtr mp::HyperVVirtualMachineFactory::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/hyperv/hyperv_virtual_machine_factory.h b/src/platform/backends/hyperv/hyperv_virtual_machine_factory.h index f50ae1197d6..03f0bf33256 100644 --- a/src/platform/backends/hyperv/hyperv_virtual_machine_factory.h +++ b/src/platform/backends/hyperv/hyperv_virtual_machine_factory.h @@ -29,7 +29,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/qemu/linux/dnsmasq_process_spec.cpp b/src/platform/backends/qemu/linux/dnsmasq_process_spec.cpp index 04fc0f4fbfe..e6c0cd206e2 100644 --- a/src/platform/backends/qemu/linux/dnsmasq_process_spec.cpp +++ b/src/platform/backends/qemu/linux/dnsmasq_process_spec.cpp @@ -24,11 +24,33 @@ namespace mp = multipass; namespace mpu = multipass::utils; +namespace +{ +[[nodiscard]] QStringList make_dnsmasq_subnet_args(const mp::BridgeSubnetList& subnets) +{ + QStringList out{}; + for (const auto& [bridge_name, subnet] : subnets) + { + const auto bridge_addr = subnet.min_address(); + const auto start_ip = bridge_addr + 1; + const auto end_ip = subnet.max_address(); + + out << QString("--interface=%1").arg(bridge_name) + << QString("--listen-address=%1").arg(QString::fromStdString(bridge_addr.as_string())) + << "--dhcp-range" + << QString("%1,%2,infinite") + .arg(QString::fromStdString(start_ip.as_string())) + .arg(QString::fromStdString(end_ip.as_string())); + } + + return out; +} +} // namespace + mp::DNSMasqProcessSpec::DNSMasqProcessSpec(const mp::Path& data_dir, - const QString& bridge_name, - const std::string& subnet, + const BridgeSubnetList& subnets, const QString& conf_file_path) - : data_dir(data_dir), bridge_name(bridge_name), subnet{subnet}, conf_file_path{conf_file_path} + : data_dir(data_dir), subnets(subnets), conf_file_path{conf_file_path} { } @@ -39,26 +61,20 @@ QString mp::DNSMasqProcessSpec::program() const QStringList mp::DNSMasqProcessSpec::arguments() const { - const auto bridge_addr = mp::IPAddress{fmt::format("{}.1", subnet)}; - const auto start_ip = mp::IPAddress{fmt::format("{}.2", subnet)}; - const auto end_ip = mp::IPAddress{fmt::format("{}.254", subnet)}; - - return QStringList() - << "--keep-in-foreground" - << "--strict-order" - << "--bind-interfaces" << QString("--pid-file") << "--domain=multipass" - << "--local=/multipass/" - << "--except-interface=lo" << QString("--interface=%1").arg(bridge_name) - << QString("--listen-address=%1").arg(QString::fromStdString(bridge_addr.as_string())) - << "--dhcp-no-override" - << "--dhcp-ignore-clid" - << "--dhcp-authoritative" << QString("--dhcp-leasefile=%1/dnsmasq.leases").arg(data_dir) - << QString("--dhcp-hostsfile=%1/dnsmasq.hosts").arg(data_dir) << "--dhcp-range" - << QString("%1,%2,infinite") - .arg(QString::fromStdString(start_ip.as_string())) - .arg(QString::fromStdString(end_ip.as_string())) - // This is to prevent it trying to read /etc/dnsmasq.conf - << QString("--conf-file=%1").arg(conf_file_path); + auto out = QStringList() << "--keep-in-foreground" + << "--strict-order" + << "--bind-interfaces" << QString("--pid-file") << "--domain=multipass" + << "--local=/multipass/" + << "--except-interface=lo" + << "--dhcp-no-override" + << "--dhcp-ignore-clid" + << "--dhcp-authoritative" + << QString("--dhcp-leasefile=%1/dnsmasq.leases").arg(data_dir) + << QString("--dhcp-hostsfile=%1/dnsmasq.hosts").arg(data_dir) + // This is to prevent it trying to read /etc/dnsmasq.conf + << QString("--conf-file=%1").arg(conf_file_path); + + return out << make_dnsmasq_subnet_args(subnets); } mp::logging::Level mp::DNSMasqProcessSpec::error_log_level() const diff --git a/src/platform/backends/qemu/linux/dnsmasq_process_spec.h b/src/platform/backends/qemu/linux/dnsmasq_process_spec.h index 36d6be98042..83cc95a7d33 100644 --- a/src/platform/backends/qemu/linux/dnsmasq_process_spec.h +++ b/src/platform/backends/qemu/linux/dnsmasq_process_spec.h @@ -17,7 +17,8 @@ #pragma once -#include +#include "dnsmasq_server.h" + #include #include @@ -30,8 +31,7 @@ class DNSMasqProcessSpec : public ProcessSpec { public: explicit DNSMasqProcessSpec(const Path& data_dir, - const QString& bridge_name, - const std::string& subnet, + const BridgeSubnetList& subnets, const QString& conf_file_path); QString program() const override; @@ -42,8 +42,7 @@ class DNSMasqProcessSpec : public ProcessSpec private: const Path data_dir; - const QString bridge_name; - const std::string subnet; + const BridgeSubnetList subnets; const QString conf_file_path; }; diff --git a/src/platform/backends/qemu/linux/dnsmasq_server.cpp b/src/platform/backends/qemu/linux/dnsmasq_server.cpp index fc181a4ee0c..c588079ef2f 100644 --- a/src/platform/backends/qemu/linux/dnsmasq_server.cpp +++ b/src/platform/backends/qemu/linux/dnsmasq_server.cpp @@ -37,23 +37,16 @@ namespace constexpr auto immediate_wait = 100; // period to wait for immediate dnsmasq failures, in ms auto make_dnsmasq_process(const mp::Path& data_dir, - const QString& bridge_name, - const std::string& subnet, + const mp::BridgeSubnetList& subnets, const QString& conf_file_path) { - auto process_spec = - std::make_unique(data_dir, bridge_name, subnet, conf_file_path); + auto process_spec = std::make_unique(data_dir, subnets, conf_file_path); return MP_PROCFACTORY.create_process(std::move(process_spec)); } } // namespace -mp::DNSMasqServer::DNSMasqServer(const Path& data_dir, - const QString& bridge_name, - const std::string& subnet) - : data_dir{data_dir}, - bridge_name{bridge_name}, - subnet{subnet}, - conf_file{QDir(data_dir).absoluteFilePath("dnsmasq-XXXXXX.conf")} +mp::DNSMasqServer::DNSMasqServer(const Path& data_dir, const BridgeSubnetList& subnets) + : data_dir{data_dir}, conf_file{QDir(data_dir).absoluteFilePath("dnsmasq-XXXXXX.conf")} { if (!conf_file.open()) throw std::runtime_error("unable to create temporary dnsmasq conf file"); @@ -67,7 +60,7 @@ mp::DNSMasqServer::DNSMasqServer(const Path& data_dir, fmt::format("unable to create file {}", dnsmasq_hosts.filesystemFileName())); } - dnsmasq_cmd = make_dnsmasq_process(data_dir, bridge_name, subnet, conf_file.fileName()); + dnsmasq_cmd = make_dnsmasq_process(data_dir, subnets, conf_file.fileName()); start_dnsmasq(); } @@ -117,7 +110,7 @@ std::optional mp::DNSMasqServer::get_ip_for(const std::string& hw return std::nullopt; } -void mp::DNSMasqServer::release_mac(const std::string& hw_addr) +void mp::DNSMasqServer::release_mac(const std::string& hw_addr, const QString& bridge_name) { auto ip = get_ip_for(hw_addr); if (!ip) @@ -215,8 +208,7 @@ void mp::DNSMasqServer::start_dnsmasq() mp::DNSMasqServer::UPtr mp::DNSMasqServerFactory::make_dnsmasq_server( const mp::Path& network_dir, - const QString& bridge_name, - const std::string& subnet) const + const BridgeSubnetList& subnets) const { - return std::make_unique(network_dir, bridge_name, subnet); + return std::make_unique(network_dir, subnets); } diff --git a/src/platform/backends/qemu/linux/dnsmasq_server.h b/src/platform/backends/qemu/linux/dnsmasq_server.h index 32092f85e3e..d59196a749d 100644 --- a/src/platform/backends/qemu/linux/dnsmasq_server.h +++ b/src/platform/backends/qemu/linux/dnsmasq_server.h @@ -21,6 +21,7 @@ #include #include #include +#include #include @@ -32,16 +33,18 @@ namespace multipass { class Process; +using BridgeSubnetList = std::vector>; + class DNSMasqServer : private DisabledCopyMove { public: using UPtr = std::unique_ptr; - DNSMasqServer(const Path& data_dir, const QString& bridge_name, const std::string& subnet); + DNSMasqServer(const Path& data_dir, const BridgeSubnetList& subnets); virtual ~DNSMasqServer(); // inherited by mock for testing virtual std::optional get_ip_for(const std::string& hw_addr); - virtual void release_mac(const std::string& hw_addr); + virtual void release_mac(const std::string& hw_addr, const QString& bridge_name); virtual void check_dnsmasq_running(); protected: @@ -51,8 +54,6 @@ class DNSMasqServer : private DisabledCopyMove void start_dnsmasq(); const QString data_dir; - const QString bridge_name; - const std::string subnet; std::unique_ptr dnsmasq_cmd; QMetaObject::Connection finish_connection; QTemporaryFile conf_file; @@ -67,7 +68,6 @@ class DNSMasqServerFactory : public Singleton : Singleton::Singleton{pass} {}; virtual DNSMasqServer::UPtr make_dnsmasq_server(const Path& network_dir, - const QString& bridge_name, - const std::string& subnet) const; + const BridgeSubnetList& subnets) const; }; } // namespace multipass diff --git a/src/platform/backends/qemu/linux/firewall_config.cpp b/src/platform/backends/qemu/linux/firewall_config.cpp index 400398b9217..a62fef8269d 100644 --- a/src/platform/backends/qemu/linux/firewall_config.cpp +++ b/src/platform/backends/qemu/linux/firewall_config.cpp @@ -174,9 +174,11 @@ auto get_firewall_rules(const QString& firewall, const QString& table) void set_firewall_rules(const QString& firewall, const QString& bridge_name, - const QString& cidr, + const mp::Subnet& cidr, const QString& comment) { + const QString cidr_str = QString::fromStdString(cidr.to_cidr()); + const QStringList comment_option{match, QStringLiteral("comment"), QStringLiteral("--comment"), @@ -231,51 +233,51 @@ void set_firewall_rules(const QString& firewall, nat, POSTROUTING, QStringList() - << source << cidr << destination << QStringLiteral("224.0.0.0/24") << jump - << RETURN << comment_option); + << source << cidr_str << destination << QStringLiteral("224.0.0.0/24") + << jump << RETURN << comment_option); add_firewall_rule(firewall, nat, POSTROUTING, - QStringList() - << source << cidr << destination << QStringLiteral("255.255.255.255/32") - << jump << RETURN << comment_option); + QStringList() << source << cidr_str << destination + << QStringLiteral("255.255.255.255/32") << jump << RETURN + << comment_option); // Masquerade all packets going from VMs to the LAN/Internet add_firewall_rule(firewall, nat, POSTROUTING, QStringList() - << source << cidr << negate << destination << cidr << protocol << tcp - << jump << MASQUERADE << to_ports << port_range << comment_option); + << source << cidr_str << negate << destination << cidr_str << protocol + << tcp << jump << MASQUERADE << to_ports << port_range << comment_option); add_firewall_rule(firewall, nat, POSTROUTING, QStringList() - << source << cidr << negate << destination << cidr << protocol << udp - << jump << MASQUERADE << to_ports << port_range << comment_option); + << source << cidr_str << negate << destination << cidr_str << protocol + << udp << jump << MASQUERADE << to_ports << port_range << comment_option); add_firewall_rule(firewall, nat, POSTROUTING, - QStringList() << source << cidr << negate << destination << cidr << jump - << MASQUERADE << comment_option); + QStringList() << source << cidr_str << negate << destination << cidr_str + << jump << MASQUERADE << comment_option); // Allow established traffic to the private subnet - add_firewall_rule(firewall, - filter, - FORWARD, - QStringList() << destination << cidr << out_interface << bridge_name << match - << QStringLiteral("conntrack") << QStringLiteral("--ctstate") - << QStringLiteral("RELATED,ESTABLISHED") << jump << ACCEPT - << comment_option); + add_firewall_rule( + firewall, + filter, + FORWARD, + QStringList() << destination << cidr_str << out_interface << bridge_name << match + << QStringLiteral("conntrack") << QStringLiteral("--ctstate") + << QStringLiteral("RELATED,ESTABLISHED") << jump << ACCEPT << comment_option); // Allow outbound traffic from the private subnet add_firewall_rule(firewall, filter, FORWARD, - QStringList() << source << cidr << in_interface << bridge_name << jump + QStringList() << source << cidr_str << in_interface << bridge_name << jump << ACCEPT << comment_option); // Allow traffic between virtual machines @@ -304,14 +306,15 @@ void set_firewall_rules(const QString& firewall, void clear_firewall_rules_for(const QString& firewall, const QString& table, const QString& bridge_name, - const QString& cidr, + const mp::Subnet& cidr, const QString& comment) { + const QString cidr_str = QString::fromStdString(cidr.to_cidr()); auto rules = QString::fromUtf8(get_firewall_rules(firewall, table)); for (auto& rule : rules.split('\n')) { - if (rule.contains(comment) || rule.contains(bridge_name) || rule.contains(cidr)) + if (rule.contains(comment) || rule.contains(bridge_name) || rule.contains(cidr_str)) { // Remove the policy type since delete doesn't use that rule.remove(0, 3); @@ -392,10 +395,10 @@ QString detect_firewall() } } // namespace -mp::FirewallConfig::FirewallConfig(const QString& bridge_name, const std::string& subnet) +mp::BasicFirewallConfig::BasicFirewallConfig(const QString& bridge_name, const mp::Subnet& subnet) : firewall{detect_firewall()}, bridge_name{bridge_name}, - cidr{QString("%1.0/24").arg(QString::fromStdString(subnet))}, + cidr{subnet.canonical()}, comment{multipass_firewall_comment(bridge_name)} { try @@ -411,7 +414,7 @@ mp::FirewallConfig::FirewallConfig(const QString& bridge_name, const std::string } } -mp::FirewallConfig::~FirewallConfig() +mp::BasicFirewallConfig::~BasicFirewallConfig() { if (!firewall.isEmpty()) { @@ -419,7 +422,7 @@ mp::FirewallConfig::~FirewallConfig() } } -void mp::FirewallConfig::verify_firewall_rules() +void mp::BasicFirewallConfig::verify_firewall_rules() { if (firewall_error) { @@ -427,7 +430,7 @@ void mp::FirewallConfig::verify_firewall_rules() } } -void mp::FirewallConfig::clear_all_firewall_rules() +void mp::BasicFirewallConfig::clear_all_firewall_rules() { for (const auto& table : firewall_tables) { @@ -437,7 +440,7 @@ void mp::FirewallConfig::clear_all_firewall_rules() mp::FirewallConfig::UPtr mp::FirewallConfigFactory::make_firewall_config( const QString& bridge_name, - const std::string& subnet) const + const mp::Subnet& subnet) const { - return std::make_unique(bridge_name, subnet); + return std::make_unique(bridge_name, subnet); } diff --git a/src/platform/backends/qemu/linux/firewall_config.h b/src/platform/backends/qemu/linux/firewall_config.h index eec65a1d5c2..9cf2664626c 100644 --- a/src/platform/backends/qemu/linux/firewall_config.h +++ b/src/platform/backends/qemu/linux/firewall_config.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include @@ -30,20 +31,25 @@ class FirewallConfig public: using UPtr = std::unique_ptr; - FirewallConfig(const QString& bridge_name, const std::string& subnet); - virtual ~FirewallConfig(); + virtual ~FirewallConfig() = default; - virtual void verify_firewall_rules(); + virtual void verify_firewall_rules() = 0; +}; + +class BasicFirewallConfig final : public FirewallConfig +{ +public: + BasicFirewallConfig(const QString& bridge_name, const Subnet& subnet); + ~BasicFirewallConfig() override; -protected: - FirewallConfig() = default; // for testing + void verify_firewall_rules() override; private: void clear_all_firewall_rules(); const QString firewall; const QString bridge_name; - const QString cidr; + const Subnet cidr; const QString comment; bool firewall_error{false}; @@ -59,6 +65,6 @@ class FirewallConfigFactory : public Singleton : Singleton::Singleton{pass} {}; virtual FirewallConfig::UPtr make_firewall_config(const QString& bridge_name, - const std::string& subnet) const; + const Subnet& subnet) const; }; } // namespace multipass diff --git a/src/platform/backends/qemu/linux/qemu_platform_linux.cpp b/src/platform/backends/qemu/linux/qemu_platform_linux.cpp index cc9ddd0ad3e..d323324eaff 100644 --- a/src/platform/backends/qemu/linux/qemu_platform_linux.cpp +++ b/src/platform/backends/qemu/linux/qemu_platform_linux.cpp @@ -35,7 +35,7 @@ namespace mpu = multipass::utils; namespace { constexpr auto category = "qemu platform"; -const QString multipass_bridge_name{"mpqemubr0"}; +const QString multipass_bridge_name{"mpqemubr%1"}; // An interface name can only be 15 characters, so this generates a hash of the // VM instance name with a "tap-" prefix and then truncates it. @@ -65,13 +65,13 @@ void remove_tap_device(const QString& tap_device_name) } } -void create_virtual_switch(const std::string& subnet, const QString& bridge_name) +void create_virtual_switch(const mp::Subnet& subnet, const QString& bridge_name) { if (!MP_UTILS.run_cmd_for_status("ip", {"addr", "show", bridge_name})) { const auto mac_address = mp::utils::generate_mac_address(); - const auto cidr = fmt::format("{}.1/24", subnet); - const auto broadcast = fmt::format("{}.255", subnet); + const auto cidr = mp::Subnet{subnet.min_address(), subnet.prefix_length()}.to_cidr(); + const auto broadcast = subnet.broadcast_address().as_string(); MP_UTILS.run_cmd_for_status( "ip", @@ -100,13 +100,10 @@ void set_ip_forward() } mp::DNSMasqServer::UPtr init_nat_network(const mp::Path& network_dir, - const QString& bridge_name, - const std::string& subnet) + const mp::BridgeSubnetList& subnets) { - create_virtual_switch(subnet, bridge_name); set_ip_forward(); - - return MP_DNSMASQ_SERVER_FACTORY.make_dnsmasq_server(network_dir, bridge_name, subnet); + return MP_DNSMASQ_SERVER_FACTORY.make_dnsmasq_server(network_dir, subnets); } void delete_virtual_switch(const QString& bridge_name) @@ -118,12 +115,55 @@ void delete_virtual_switch(const QString& bridge_name) } } // namespace -mp::QemuPlatformLinux::QemuPlatformLinux(const mp::Path& data_dir) - : bridge_name{multipass_bridge_name}, - network_dir{MP_UTILS.make_dir(QDir(data_dir), "network")}, - subnet{MP_BACKEND.get_subnet(network_dir, bridge_name)}, - dnsmasq_server{init_nat_network(network_dir, bridge_name, subnet)}, +mp::QemuPlatformLinux::Bridge::Bridge(const Subnet& subnet, const std::string& name) + : bridge_name{multipass_bridge_name.arg(name.c_str())}, + subnet{subnet}, firewall_config{MP_FIREWALL_CONFIG_FACTORY.make_firewall_config(bridge_name, subnet)} +{ + create_virtual_switch(subnet, bridge_name); +} + +mp::QemuPlatformLinux::Bridge::~Bridge() +{ + delete_virtual_switch(bridge_name); +} + +[[nodiscard]] mp::QemuPlatformLinux::Bridges mp::QemuPlatformLinux::get_bridges( + const AvailabilityZoneManager::Zones& zones) +{ + Bridges bridges{}; + bridges.reserve(zones.size()); + + for (const auto& zone_ref : zones) + { + const auto& zone = zone_ref.get(); + const auto& name = zone.get_name(); + bridges.emplace(std::piecewise_construct, + std::forward_as_tuple(name), + std::forward_as_tuple(zone.get_subnet(), name)); + } + + return bridges; +} + +[[nodiscard]] mp::BridgeSubnetList mp::QemuPlatformLinux::get_bridge_list(const Bridges& subnets) +{ + BridgeSubnetList out{}; + out.reserve(subnets.size()); + + for (const auto& [_, subnet] : subnets) + { + out.emplace_back(subnet.bridge_name, subnet.subnet); + } + + return out; +} + +mp::QemuPlatformLinux::QemuPlatformLinux(const mp::Path& data_dir, + const AvailabilityZoneManager::Zones& zones) + : network_dir{MP_UTILS.make_dir(QDir(data_dir), "network")}, + bridges{get_bridges(zones)}, + dnsmasq_server{init_nat_network(network_dir, get_bridge_list(bridges))} { } @@ -131,11 +171,9 @@ mp::QemuPlatformLinux::~QemuPlatformLinux() { for (const auto& it : name_to_net_device_map) { - const auto& [tap_device_name, hw_addr] = it.second; + const auto& [tap_device_name, hw_addr, _] = it.second; remove_tap_device(tap_device_name); } - - delete_virtual_switch(bridge_name); } std::optional mp::QemuPlatformLinux::get_ip_for(const std::string& hw_addr) @@ -148,8 +186,8 @@ void mp::QemuPlatformLinux::remove_resources_for(const std::string& name) auto it = name_to_net_device_map.find(name); if (it != name_to_net_device_map.end()) { - const auto& [tap_device_name, hw_addr] = it->second; - dnsmasq_server->release_mac(hw_addr); + const auto& [tap_device_name, hw_addr, bridge_name] = it->second; + dnsmasq_server->release_mac(hw_addr, bridge_name); remove_tap_device(tap_device_name); name_to_net_device_map.erase(name); @@ -162,17 +200,22 @@ void mp::QemuPlatformLinux::platform_health_check() MP_BACKEND.check_if_kvm_is_in_use(); dnsmasq_server->check_dnsmasq_running(); - firewall_config->verify_firewall_rules(); + for (const auto& [_, bridge] : bridges) + { + bridge.firewall_config->verify_firewall_rules(); + } } QStringList mp::QemuPlatformLinux::vm_platform_args(const VirtualMachineDescription& vm_desc) { // Configure and generate the args for the default network interface auto tap_device_name = generate_tap_device_name(vm_desc.vm_name); + const QString& bridge_name = bridges.at(vm_desc.zone).bridge_name; create_tap_device(tap_device_name, bridge_name); - name_to_net_device_map.emplace(vm_desc.vm_name, - std::make_pair(tap_device_name, vm_desc.default_mac_address)); + name_to_net_device_map.emplace( + vm_desc.vm_name, + std::make_tuple(tap_device_name, vm_desc.default_mac_address, bridge_name)); QStringList opts; @@ -237,9 +280,11 @@ QStringList mp::QemuPlatformLinux::vm_platform_args(const VirtualMachineDescript return opts; } -mp::QemuPlatform::UPtr mp::QemuPlatformFactory::make_qemu_platform(const Path& data_dir) const +mp::QemuPlatform::UPtr mp::QemuPlatformFactory::make_qemu_platform( + const Path& data_dir, + const mp::AvailabilityZoneManager::Zones& zones) const { - return std::make_unique(data_dir); + return std::make_unique(data_dir, zones); } bool mp::QemuPlatformLinux::is_network_supported(const std::string& network_type) const diff --git a/src/platform/backends/qemu/linux/qemu_platform_linux.h b/src/platform/backends/qemu/linux/qemu_platform_linux.h index d0f29d2ee1b..a3825f4b47f 100644 --- a/src/platform/backends/qemu/linux/qemu_platform_linux.h +++ b/src/platform/backends/qemu/linux/qemu_platform_linux.h @@ -22,10 +22,10 @@ #include +#include #include #include -#include namespace multipass { @@ -33,7 +33,7 @@ namespace multipass class QemuPlatformLinux : public QemuPlatform { public: - explicit QemuPlatformLinux(const Path& data_dir); + explicit QemuPlatformLinux(const Path& data_dir, const AvailabilityZoneManager::Zones& zones); ~QemuPlatformLinux() override; std::optional get_ip_for(const std::string& hw_addr) override; @@ -46,11 +46,27 @@ class QemuPlatformLinux : public QemuPlatform void set_authorization(std::vector& networks) override; private: - const QString bridge_name; + // explicitly naming DisabledCopyMove since the private one derived from QemuPlatform takes + // precedence in lookup + struct Bridge : private multipass::DisabledCopyMove + { + const QString bridge_name; + const Subnet subnet; + FirewallConfig::UPtr firewall_config; + + Bridge(const Subnet& subnet, const std::string& name); + ~Bridge(); + }; + using Bridges = std::unordered_map; + + [[nodiscard]] static Bridges get_bridges(const AvailabilityZoneManager::Zones& zones); + + [[nodiscard]] static BridgeSubnetList get_bridge_list(const Bridges&); + const Path network_dir; - const std::string subnet; + const Bridges bridges; DNSMasqServer::UPtr dnsmasq_server; - FirewallConfig::UPtr firewall_config; - std::unordered_map> name_to_net_device_map; + std::unordered_map> + name_to_net_device_map; }; } // namespace multipass diff --git a/src/platform/backends/qemu/macos/qemu_platform_macos.cpp b/src/platform/backends/qemu/macos/qemu_platform_macos.cpp index ccb472ed5f0..0d5ae4fa1ec 100644 --- a/src/platform/backends/qemu/macos/qemu_platform_macos.cpp +++ b/src/platform/backends/qemu/macos/qemu_platform_macos.cpp @@ -28,10 +28,6 @@ namespace mp = multipass; namespace { -constexpr auto VMNET_SUBNET_PREFIX = "192.168.252"; -constexpr auto VMNET_SUBNET_MASK = "255.255.255.0"; -constexpr auto VMNET_START_ADDR = "1"; -constexpr auto VMNET_END_ADDR = "254"; auto get_common_args(const QString& host_arch) { QStringList qemu_args; @@ -48,7 +44,24 @@ auto get_common_args(const QString& host_arch) } } // namespace -mp::QemuPlatformMacOS::QemuPlatformMacOS() : common_args{get_common_args(host_arch)} +[[nodiscard]] mp::QemuPlatformMacOS::Bridges mp::QemuPlatformMacOS::get_bridges( + const AvailabilityZoneManager::Zones& zones) +{ + Bridges bridges{}; + bridges.reserve(zones.size()); + + for (const auto& zone_ref : zones) + { + const auto& zone = zone_ref.get(); + bridges.emplace(zone.get_name(), zone.get_subnet()); + } + + return bridges; +} + +mp::QemuPlatformMacOS::QemuPlatformMacOS(const AvailabilityZoneManager::Zones& zones) + : common_args{get_common_args(host_arch)}, bridges{get_bridges(zones)} + { } @@ -74,8 +87,9 @@ QStringList mp::QemuPlatformMacOS::vmstate_platform_args() QStringList mp::QemuPlatformMacOS::vm_platform_args(const VirtualMachineDescription& vm_desc) { QStringList qemu_args; - // clang-format off + const Subnet& subnet = bridges.at(vm_desc.zone); + // clang-format off qemu_args << common_args << "-accel" << "hvf" @@ -87,18 +101,14 @@ QStringList mp::QemuPlatformMacOS::vm_platform_args(const VirtualMachineDescript << "host" // Set up the network related args << "-nic" - << QString::fromStdString(fmt::format("vmnet-shared,start-address={}.{},end-address={}.{}" + << QString::fromStdString(fmt::format("vmnet-shared,start-address={},end-address={}" ",subnet-mask={},model=virtio-net-pci,mac={}", - VMNET_SUBNET_PREFIX, - VMNET_START_ADDR, - VMNET_SUBNET_PREFIX, - VMNET_END_ADDR, - VMNET_SUBNET_MASK, + subnet.min_address().as_string(), + subnet.max_address().as_string(), + subnet.subnet_mask().as_string(), vm_desc.default_mac_address)); - // The subnet 192.168.252.0/24 is chosen to tackle an issue with subnet collision in macos-26: - // #4383 - // clang-format on + for (const auto& extra_interface : vm_desc.extra_interfaces) { qemu_args << "-nic" @@ -131,9 +141,11 @@ void mp::QemuPlatformMacOS::set_authorization(std::vector& // nothing to do here } -mp::QemuPlatform::UPtr mp::QemuPlatformFactory::make_qemu_platform(const Path& data_dir) const +mp::QemuPlatform::UPtr mp::QemuPlatformFactory::make_qemu_platform( + const Path& data_dir, + const mp::AvailabilityZoneManager::Zones& zones) const { - return std::make_unique(); + return std::make_unique(zones); } std::string mp::QemuPlatformMacOS::create_bridge_with(const NetworkInterfaceInfo& interface) const diff --git a/src/platform/backends/qemu/macos/qemu_platform_macos.h b/src/platform/backends/qemu/macos/qemu_platform_macos.h index a995d05bb8c..3b718995e0a 100644 --- a/src/platform/backends/qemu/macos/qemu_platform_macos.h +++ b/src/platform/backends/qemu/macos/qemu_platform_macos.h @@ -19,15 +19,18 @@ #include +#include #include +#include + namespace multipass { // This class is the platform detail for QEMU on macOS class QemuPlatformMacOS : public QemuPlatform { public: - explicit QemuPlatformMacOS(); + explicit QemuPlatformMacOS(const AvailabilityZoneManager::Zones& zones); std::optional get_ip_for(const std::string& hw_addr) override; void remove_resources_for(const std::string& name) override; @@ -41,7 +44,12 @@ class QemuPlatformMacOS : public QemuPlatform void set_authorization(std::vector& networks) override; private: + using Bridges = std::unordered_map; + + [[nodiscard]] static Bridges get_bridges(const AvailabilityZoneManager::Zones& zones); + const QString host_arch{HOST_ARCH}; const QStringList common_args; + const Bridges bridges; }; } // namespace multipass diff --git a/src/platform/backends/qemu/qemu_platform.h b/src/platform/backends/qemu/qemu_platform.h index 3a747cb2983..bf777085710 100644 --- a/src/platform/backends/qemu/qemu_platform.h +++ b/src/platform/backends/qemu/qemu_platform.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -70,6 +71,7 @@ class QemuPlatformFactory : public Singleton QemuPlatformFactory(const Singleton::PrivatePass& pass) noexcept : Singleton::Singleton{pass} {}; - virtual QemuPlatform::UPtr make_qemu_platform(const Path& data_dir) const; + virtual QemuPlatform::UPtr + make_qemu_platform(const Path& data_dir, const AvailabilityZoneManager::Zones& zones) const; }; } // namespace multipass diff --git a/src/platform/backends/qemu/qemu_virtual_machine.cpp b/src/platform/backends/qemu/qemu_virtual_machine.cpp index 4a1c5ed3284..e303b3e6407 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine.cpp +++ b/src/platform/backends/qemu/qemu_virtual_machine.cpp @@ -223,6 +223,7 @@ 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, @@ -231,6 +232,7 @@ mp::QemuVirtualMachine::QemuVirtualMachine(const VirtualMachineDescription& desc : State::off, desc.vm_name, key_provider, + zone, instance_dir}, desc{desc}, qemu_platform{qemu_platform}, @@ -413,9 +415,10 @@ void mp::QemuVirtualMachine::suspend() vm_process.reset(nullptr); } - else if (state == State::off || state == State::suspended) + else if (state == State::off || state == State::suspended || state == State::unavailable) { - mpl::info(vm_name, "Ignoring suspend issued while stopped/suspended"); + // TODO: format state directly + mpl::info(vm_name, "Ignoring suspend issued while stopped/suspended/unavailable"); monitor->on_suspend(); } } diff --git a/src/platform/backends/qemu/qemu_virtual_machine.h b/src/platform/backends/qemu/qemu_virtual_machine.h index 2f6bb41a475..cb7f3ea810c 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(); @@ -78,8 +79,9 @@ class QemuVirtualMachine : public QObject, public BaseVirtualMachine // 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, + AvailabilityZone& zone, const Path& instance_dir) - : BaseVirtualMachine{name, key_provider, 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 5914ab202fc..c16cda52485 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp +++ b/src/platform/backends/qemu/qemu_virtual_machine_factory.cpp @@ -36,16 +36,22 @@ 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, az_manager.get_zones()), + data_dir, + az_manager} { } mp::QemuVirtualMachineFactory::QemuVirtualMachineFactory(QemuPlatform::UPtr qemu_platform, - const mp::Path& data_dir) + const mp::Path& data_dir, + AvailabilityZoneManager& az_manager) : BaseVirtualMachineFactory(MP_UTILS.derive_instances_dir(data_dir, qemu_platform->get_directory_name(), - instances_subdir)), + instances_subdir), + az_manager), qemu_platform{std::move(qemu_platform)} { } @@ -59,6 +65,7 @@ mp::VirtualMachine::UPtr mp::QemuVirtualMachineFactory::create_virtual_machine( qemu_platform.get(), monitor, key_provider, + az_manager.get_zone(desc.zone), get_instance_directory(desc.vm_name)); } @@ -174,6 +181,7 @@ mp::VirtualMachine::UPtr mp::QemuVirtualMachineFactory::clone_vm_impl( 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 da073b8f240..e75408aa6ed 100644 --- a/src/platform/backends/qemu/qemu_virtual_machine_factory.h +++ b/src/platform/backends/qemu/qemu_virtual_machine_factory.h @@ -30,7 +30,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, @@ -49,7 +49,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 6e6dfa6ee91..ca013128830 100644 --- a/src/platform/backends/shared/CMakeLists.txt +++ b/src/platform/backends/shared/CMakeLists.txt @@ -21,6 +21,15 @@ add_library(shared STATIC ${CMAKE_SOURCE_DIR}/include/multipass/process/process.h ${CMAKE_SOURCE_DIR}/include/multipass/process/basic_process.h) +if(AVAILABILITY_ZONES_ENABLED) + target_sources(shared PRIVATE + base_availability_zone.cpp + base_availability_zone_manager.cpp) +else() + target_sources(shared PRIVATE + single_availability_zone_manager.cpp) +endif() + target_link_libraries(shared iso process 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 00000000000..71e3e086ce1 --- /dev/null +++ b/src/platform/backends/shared/base_availability_zone.cpp @@ -0,0 +1,179 @@ +/* + * 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 subnet_key = "subnet"; +constexpr auto available_key = "available"; + +multipass::SubnetAllocator subnet_allocator(std::string("10.97.0.0/20"), 24); +} // namespace + +namespace multipass +{ + +BaseAvailabilityZone::BaseAvailabilityZone(const std::string& name, const fs::path& az_directory) + : file_path{az_directory / (name + ".json")}, name{name}, m{load_file(name, file_path)} +{ + save_file(); +} + +const std::string& BaseAvailabilityZone::get_name() const +{ + return name; +} + +const Subnet& BaseAvailabilityZone::get_subnet() const +{ + return m.subnet; +} + +bool BaseAvailabilityZone::is_available() const +{ + const std::unique_lock lock{mutex}; + return m.available; +} + +void BaseAvailabilityZone::set_available(const bool new_available) +{ + + mpl::debug(name, "making AZ {}available", new_available ? "" : "un"); + const std::unique_lock lock{mutex}; + if (m.available == new_available) + return; + + m.available = new_available; + auto save_file_guard = sg::make_scope_guard([this]() noexcept { + try + { + save_file(); + } + catch (const std::exception& e) + { + mpl::error(name, "Failed to serialize availability zone: {}", e.what()); + } + }); + + try + { + for (auto& vm : vms) + vm.get().set_available(new_available); + } + catch (...) + { + // if an error occurs fallback to available. + m.available = true; + + // make sure nothing is still unavailable. + for (auto& vm : vms) + { + // setting the state here breaks encapsulation, but it's already broken. + std::unique_lock vm_lock{vm.get().state_mutex}; + if (vm.get().current_state() == VirtualMachine::State::unavailable) + { + vm.get().state = VirtualMachine::State::off; + vm.get().handle_state_update(); + } + } + + // rethrow the error so something else can deal with it. + throw; + } +} + +void BaseAvailabilityZone::add_vm(VirtualMachine& vm) +{ + mpl::debug(name, "adding vm '{}' to AZ", vm.get_name()); + const std::unique_lock lock{mutex}; + vms.emplace_back(vm); +} + +void BaseAvailabilityZone::remove_vm(VirtualMachine& vm) +{ + mpl::debug(name, "removing vm '{}' from AZ", vm.get_name()); + const std::unique_lock lock{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(vms.begin(), vms.end(), [&](const auto& some_vm) { + return some_vm.get().get_name() == vm.get_name(); + }); + vms.erase(to_remove, vms.end()); +} + +BaseAvailabilityZone::Data BaseAvailabilityZone::load_file(const std::string& name, + const fs::path& file_path) +{ + mpl::trace(name, "reading AZ from file '{}'", file_path); + if (auto filedata = MP_FILEOPS.try_read_file(file_path)) + { + try + { + auto json = boost::json::parse(*filedata); + return value_to(json); + } + catch (const boost::system::system_error& e) + { + mpl::error("aliases", "Error parsing file '{}': {}", file_path, e.what()); + } + } + // Return a default value if we couldn't load from `file_path`. + return { + .subnet = subnet_allocator.next_available(), + .available = true, + }; +} + +void BaseAvailabilityZone::save_file() const +{ + mpl::trace(name, "writing AZ to file '{}'", file_path); + const std::unique_lock lock{mutex}; + + auto json = boost::json::value_from(m); + MP_FILEOPS.write_transactionally(QString::fromStdString(file_path.string()), + pretty_print(json)); +} + +void tag_invoke(const boost::json::value_from_tag&, + boost::json::value& json, + const BaseAvailabilityZone::Data& zone) +{ + json = {{subnet_key, boost::json::value_from(zone.subnet)}, {available_key, zone.available}}; +} + +BaseAvailabilityZone::Data tag_invoke(const boost::json::value_to_tag&, + const boost::json::value& json) +{ + return { + .subnet = value_to(json.at(subnet_key)), + .available = value_to(json.at(available_key)), + }; +} + +} // 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 00000000000..db132296fa3 --- /dev/null +++ b/src/platform/backends/shared/base_availability_zone_manager.cpp @@ -0,0 +1,168 @@ +/* + * 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{}; + size_t idx = 0; + for (const auto& zone_name : default_zone_names) + zones[idx++] = std::make_unique(zone_name, zones_directory); + + return zones; +}; +} // namespace + +namespace multipass +{ + +BaseAvailabilityZoneManager::BaseAvailabilityZoneManager(const fs::path& data_dir) + : file_path{data_dir / az_file}, + zone_collection{create_default_zones(data_dir / zones_directory_name), load_file(file_path)} +{ + save_file(); +} + +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 = zone_collection.next_available(); + save_file(); + 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(); +} + +std::string BaseAvailabilityZoneManager::load_file(const std::filesystem::path& file_path) +{ + mpl::debug(category, "reading AZ manager from file '{}'", file_path); + if (auto filedata = MP_FILEOPS.try_read_file(file_path)) + { + try + { + auto json = boost::json::parse(*filedata); + return value_to(json.at(automatic_zone_key)); + } + catch (const boost::system::system_error& e) + { + mpl::error(category, "Error parsing file '{}': {}", file_path, e.what()); + } + } + // Return a default value if we couldn't load from `file_path`. + return ""; +} + +void BaseAvailabilityZoneManager::save_file() const +{ + mpl::debug(category, "writing AZ manager to file '{}'", file_path); + const std::unique_lock lock{mutex}; + + boost::json::value json = {{automatic_zone_key, zone_collection.last_used()}}; + MP_FILEOPS.write_transactionally(QString::fromStdString(file_path.string()), + pretty_print(json)); +} + +const BaseAvailabilityZoneManager::ZoneCollection::ZoneArray& +BaseAvailabilityZoneManager::zones() const +{ + return 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; + })} +{ + if (automatic_zone == zones.end()) + { + mpl::debug(category, "automatic zone '{}' not known, using default", last_used); + automatic_zone = zones.begin(); + } +} + +std::string BaseAvailabilityZoneManager::ZoneCollection::next_available() +{ + std::unique_lock lock{mutex}; + + // Locate the first available zone + auto zone_it = std::find_if(zones.begin(), zones.end(), [](const auto& zone) { + return zone->is_available(); + }); + + // Check if an available zone was found + if (zone_it != zones.end()) + { + automatic_zone = zone_it; + return (*zone_it)->get_name(); + } + + // If none are available, throw an exception + 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 9f66d83869b..c070b9dfb2b 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 @@ -94,20 +95,30 @@ mpu::TimeoutAction log_and_retry(const ExceptionT& e, mp::BaseVirtualMachine::BaseVirtualMachine(const std::string& vm_name, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir) - : vm_name{vm_name}, key_provider{key_provider}, instance_dir{instance_dir} + : vm_name{vm_name}, key_provider{key_provider}, zone{zone}, instance_dir{instance_dir} { + zone.add_vm(*this); } mp::BaseVirtualMachine::BaseVirtualMachine(State state, const std::string& vm_name, const SSHKeyProvider& key_provider, + AvailabilityZone& zone, const Path& instance_dir) : VirtualMachine{state}, vm_name{vm_name}, key_provider{key_provider}, + zone{zone}, instance_dir{instance_dir} { + 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( @@ -147,9 +158,12 @@ std::string mp::BaseVirtualMachine::get_instance_id_from_the_cloud_init() const void mp::BaseVirtualMachine::check_state_for_shutdown(ShutdownPolicy shutdown_policy) { // A mutex should already be locked by the caller here - if (state == State::off || state == State::stopped) + if (state == State::off || state == State::stopped || state == State::unavailable) { - throw VMStateIdempotentException{"Ignoring shutdown since instance is already stopped."}; + // TODO: format state directly + throw VMStateIdempotentException{ + fmt::format("Ignoring shutdown since instance is {}.", + (state == State::unavailable) ? "unavailable" : "already stopped")}; } if (shutdown_policy == ShutdownPolicy::Poweroff) @@ -185,6 +199,33 @@ void mp::BaseVirtualMachine::check_state_for_shutdown(ShutdownPolicy shutdown_po } } +void mp::BaseVirtualMachine::set_available(bool available) +{ + // Ignore idempotent calls + if (available == (state != State::unavailable)) + return; + + if (available) + { + state = State::off; + handle_state_update(); + if (was_running) + { + start(); + + // normally the daemon sets the state to running... + state = State::running; + handle_state_update(); + } + return; + } + + was_running = state == State::running || state == State::starting || state == State::restarting; + shutdown(ShutdownPolicy::Poweroff); + state = State::unavailable; + handle_state_update(); +} + std::string mp::BaseVirtualMachine::ssh_exec(const std::string& cmd, bool whisper) { std::unique_lock lock{state_mutex}; diff --git a/src/platform/backends/shared/base_virtual_machine.h b/src/platform/backends/shared/base_virtual_machine.h index 88277cb774e..723fe931bef 100644 --- a/src/platform/backends/shared/base_virtual_machine.h +++ b/src/platform/backends/shared/base_virtual_machine.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -40,13 +41,18 @@ 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, + 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; + void wait_until_ssh_up(std::chrono::milliseconds timeout) override; void wait_for_cloud_init(std::chrono::milliseconds timeout) override; @@ -85,6 +91,7 @@ class BaseVirtualMachine : public VirtualMachine QDir instance_directory() const override; const std::string& get_name() const override; + const AvailabilityZone& get_zone() const override; protected: virtual std::shared_ptr make_specific_snapshot(const QString& filename); @@ -175,6 +182,7 @@ class BaseVirtualMachine : public VirtualMachine protected: const std::string vm_name; const SSHKeyProvider& key_provider; + AvailabilityZone& zone; const QDir instance_dir; std::optional management_ip; bool shutdown_while_starting = false; @@ -186,6 +194,7 @@ class BaseVirtualMachine : public VirtualMachine std::shared_ptr head_snapshot = nullptr; int snapshot_count = 0; // tracks the number of snapshots ever taken (regardless of deletes) mutable std::recursive_mutex snapshot_mutex; + bool was_running{false}; }; } // namespace multipass @@ -211,6 +220,11 @@ inline const std::string& multipass::BaseVirtualMachine::get_name() const return vm_name; } +inline const multipass::AvailabilityZone& multipass::BaseVirtualMachine::get_zone() const +{ + return zone; +} + inline void multipass::BaseVirtualMachine::save_error_msg(std::string error) noexcept { saved_error_msg = std::move(error); diff --git a/src/platform/backends/shared/base_virtual_machine_factory.cpp b/src/platform/backends/shared/base_virtual_machine_factory.cpp index a681d5ea84e..6202c2f960c 100644 --- a/src/platform/backends/shared/base_virtual_machine_factory.cpp +++ b/src/platform/backends/shared/base_virtual_machine_factory.cpp @@ -31,8 +31,9 @@ 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) { @@ -114,6 +115,7 @@ mp::VirtualMachine::UPtr mp::BaseVirtualMachineFactory::clone_bare_vm( 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, @@ -124,10 +126,7 @@ mp::VirtualMachine::UPtr mp::BaseVirtualMachineFactory::clone_bare_vm( {}, {}}; - 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( diff --git a/src/platform/backends/shared/base_virtual_machine_factory.h b/src/platform/backends/shared/base_virtual_machine_factory.h index 3992f25dcd6..32921b3c2c8 100644 --- a/src/platform/backends/shared/base_virtual_machine_factory.h +++ b/src/platform/backends/shared/base_virtual_machine_factory.h @@ -17,6 +17,7 @@ #pragma once +#include #include #include #include @@ -33,7 +34,8 @@ 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 +85,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/shared/linux/backend_utils.cpp b/src/platform/backends/shared/linux/backend_utils.cpp index 08bfb257c1a..2fdfd3c4207 100644 --- a/src/platform/backends/shared/linux/backend_utils.cpp +++ b/src/platform/backends/shared/linux/backend_utils.cpp @@ -17,6 +17,7 @@ #include "backend_utils.h" #include "dbus_wrappers.h" +#include "multipass/subnet.h" #include #include @@ -54,8 +55,6 @@ Q_DECLARE_METATYPE(VariantMapMap) // for DBus namespace { -std::default_random_engine gen; -std::uniform_int_distribution dist{0, 255}; const auto nm_bus_name = QStringLiteral("org.freedesktop.NetworkManager"); const auto nm_root_obj = QStringLiteral("/org/freedesktop/NetworkManager"); const auto nm_root_ifc = QStringLiteral("org.freedesktop.NetworkManager"); @@ -64,45 +63,6 @@ const auto nm_settings_ifc = QStringLiteral("org.freedesktop.NetworkManager.Sett const auto nm_connection_ifc = QStringLiteral("org.freedesktop.NetworkManager.Settings.Connection"); constexpr auto max_bridge_name_len = 15; // maximum number of characters in a bridge name -bool subnet_used_locally(const std::string& subnet) -{ - // CLI equivalent: ip -4 route show | grep -q ${SUBNET} - const auto output = - QString::fromStdString(MP_UTILS.run_cmd_for_output("ip", {"-4", "route", "show"})); - return output.contains(QString::fromStdString(subnet)); -} - -bool can_reach_gateway(const std::string& ip) -{ - return MP_UTILS.run_cmd_for_status("ping", {"-n", "-q", ip.c_str(), "-c", "1", "-W", "1"}); -} - -auto virtual_switch_subnet(const QString& bridge_name) -{ - // CLI equivalent: ip -4 route show | grep ${BRIDGE_NAME} | cut -d ' ' -f1 | cut -d '.' -f1-3 - QString subnet; - - const auto output = - QString::fromStdString(MP_UTILS.run_cmd_for_output("ip", {"-4", "route", "show"})) - .split('\n'); - for (const auto& line : output) - { - if (line.contains(bridge_name)) - { - subnet = line.section('.', 0, 2); - break; - } - } - - if (subnet.isNull()) - { - mpl::info("daemon", - "Unable to determine subnet for the {} subnet", - qUtf8Printable(bridge_name)); - } - return subnet.toStdString(); -} - const mpdbus::DBusConnection& get_checked_system_bus() { const auto& system_bus = mpdbus::DBusProvider::instance().get_system_bus(); @@ -191,27 +151,6 @@ auto make_bridge_rollback_guard(std::string_view log_category, }); } -std::string generate_random_subnet() -{ - gen.seed(std::chrono::system_clock::now().time_since_epoch().count()); - for (auto i = 0; i < 100; ++i) - { - auto subnet = fmt::format("10.{}.{}", dist(gen), dist(gen)); - if (subnet_used_locally(subnet)) - continue; - - if (can_reach_gateway(fmt::format("{}.1", subnet))) - continue; - - if (can_reach_gateway(fmt::format("{}.254", subnet))) - continue; - - return subnet; - } - - throw std::runtime_error("Could not determine a subnet for networking."); -} - } // namespace // @precondition no bridge exists for this interface @@ -274,22 +213,6 @@ std::string mp::Backend::create_bridge_with(const std::string& interface) return ret; } -std::string mp::Backend::get_subnet(const mp::Path& network_dir, const QString& bridge_name) const -{ - auto subnet = virtual_switch_subnet(bridge_name); - if (!subnet.empty()) - return subnet; - - QFile subnet_file{network_dir + "/multipass_subnet"}; - MP_FILEOPS.open(subnet_file, QIODevice::ReadWrite | QIODevice::Text); - if (MP_FILEOPS.size(subnet_file) > 0) - return MP_FILEOPS.read_all(subnet_file).trimmed().toStdString(); - - auto new_subnet = generate_random_subnet(); - MP_FILEOPS.write(subnet_file, new_subnet.data(), new_subnet.length()); - return new_subnet; -} - void mp::Backend::check_for_kvm_support() { QFile kvm_device{"/dev/kvm"}; diff --git a/src/platform/backends/shared/linux/backend_utils.h b/src/platform/backends/shared/linux/backend_utils.h index 76bc2fdd9a9..6938bda6cd8 100644 --- a/src/platform/backends/shared/linux/backend_utils.h +++ b/src/platform/backends/shared/linux/backend_utils.h @@ -50,7 +50,6 @@ class Backend : public Singleton using Singleton::Singleton; virtual std::string create_bridge_with(const std::string& interface); - virtual std::string get_subnet(const Path& network_dir, const QString& bridge_name) const; // For detecting KVM virtual void check_for_kvm_support(); diff --git a/src/platform/backends/shared/single_availability_zone_manager.cpp b/src/platform/backends/shared/single_availability_zone_manager.cpp new file mode 100644 index 00000000000..5929947319a --- /dev/null +++ b/src/platform/backends/shared/single_availability_zone_manager.cpp @@ -0,0 +1,59 @@ +/* + * 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 + +namespace mp = multipass; + +namespace +{ +constexpr mp::Subnet::PrefixLength subnet_prefix_length = 24; +mp::SubnetAllocator subnet_allocator{MP_PLATFORM.get_preferred_subnet(), subnet_prefix_length}; + +mp::Subnet get_subnet(const mp::Path& data_dir) +{ + auto network_dir = MP_UTILS.make_dir(QDir(data_dir), "network"); + QFile subnet_file{network_dir + "/multipass_subnet"}; + MP_FILEOPS.open(subnet_file, QIODevice::ReadWrite | QIODevice::Text); + if (MP_FILEOPS.size(subnet_file) > 0) + { + auto content = MP_FILEOPS.read_all(subnet_file).trimmed().toStdString(); + return mp::Subnet{mp::IPAddress{content + ".0"}, subnet_prefix_length}; + } + + auto new_subnet = subnet_allocator.next_available(); + auto content = new_subnet.address().as_string(); + content = content.substr(0, content.rfind('.')); + MP_FILEOPS.write(subnet_file, content.data(), content.length()); + return new_subnet; +} +} // namespace + +namespace multipass +{ + +SingleAvailabilityZoneManager::SingleAvailabilityZoneManager(const mp::Path& data_dir) + // We name this zone "0" since that matches the naming of our bridge name from before the + // introduction of AZs; see src/platform/backends/qemu/linux/qemu_platform_detail_linux.cpp. + : zone{"0", get_subnet(data_dir)} +{ +} + +} // namespace multipass diff --git a/src/platform/backends/virtualbox/virtualbox_virtual_machine.cpp b/src/platform/backends/virtualbox/virtualbox_virtual_machine.cpp index 933e3f8efc6..ba28edcdb67 100644 --- a/src/platform/backends/virtualbox/virtualbox_virtual_machine.cpp +++ b/src/platform/backends/virtualbox/virtualbox_virtual_machine.cpp @@ -183,8 +183,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) { @@ -264,8 +265,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(); @@ -330,9 +332,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 8be381d91bb..30a4b78bf08 100644 --- a/src/platform/backends/virtualbox/virtualbox_virtual_machine.h +++ b/src/platform/backends/virtualbox/virtualbox_virtual_machine.h @@ -38,12 +38,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; @@ -76,6 +78,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 4eb59d7a776..b012b5dbe2e 100644 --- a/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.cpp +++ b/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.cpp @@ -119,9 +119,12 @@ mp::NetworkInterfaceInfo list_vbox_network( } } // namespace -mp::VirtualBoxVirtualMachineFactory::VirtualBoxVirtualMachineFactory(const mp::Path& data_dir) +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)) + MP_UTILS.derive_instances_dir(data_dir, get_backend_directory_name(), instances_subdir), + az_manager) { } @@ -133,6 +136,7 @@ auto mp::VirtualBoxVirtualMachineFactory::create_virtual_machine( return std::make_unique(desc, monitor, key_provider, + az_manager.get_zone(desc.zone), get_instance_directory(desc.vm_name)); } @@ -283,5 +287,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 3e8caeb8f55..9b6bb947c5a 100644 --- a/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.h +++ b/src/platform/backends/virtualbox/virtualbox_virtual_machine_factory.h @@ -24,7 +24,8 @@ 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 14157709eb6..14455f44e41 100644 --- a/src/platform/platform_linux.cpp +++ b/src/platform/platform_linux.cpp @@ -354,6 +354,51 @@ std::string mp::platform::Platform::bridge_nomenclature() const return br_nomenclature; } +// validation of the ip and cidr happen later, otherwise this regex would be massive. +const QRegularExpression subnet_regex( + R"(((?:[0-9][0-9]?[0-9]?\.){3}[0-9][0-9]?[0-9]?\/[0-9][0-9]?))"); + +bool mp::platform::Platform::subnet_used_locally(mp::Subnet subnet) const +{ + const auto output = + QString::fromStdString(MP_UTILS.run_cmd_for_output("ip", {"-4", "route", "show"})); + + QRegularExpressionMatchIterator i = subnet_regex.globalMatch(output); + + while (i.hasNext()) + { + QRegularExpressionMatch match = i.next(); + + try + { + mp::Subnet found_net{match.captured(1).toStdString()}; + + // check for overlap + if (found_net.contains(subnet) || subnet.contains(found_net)) + { + return true; + } + } + catch (const std::exception& e) + { + mpl::warn(category, "invalid subnet from ip command: {}", e.what()); + } + } + + auto can_reach_gateway = [](mp::IPAddress ip) { + const auto ipstr = ip.as_string(); + return MP_UTILS.run_cmd_for_status("ping", + {"-n", "-q", ipstr.c_str(), "-c", "1", "-W", "1"}); + }; + + return can_reach_gateway(subnet.min_address()) || can_reach_gateway(subnet.max_address()); +} + +mp::Subnet mp::platform::Platform::get_preferred_subnet() const +{ + return {"10.97.0.0/20"}; +} + auto mp::platform::detail::get_network_interfaces_from(const QDir& sys_dir) -> std::map { @@ -399,15 +444,16 @@ 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); if (driver == QStringLiteral("qemu")) - return std::make_unique(data_dir); + return std::make_unique(data_dir, az_manager); #if VIRTUALBOX_ENABLED if (driver == QStringLiteral("virtualbox")) - return std::make_unique(data_dir); + return std::make_unique(data_dir, az_manager); #endif throw std::runtime_error(fmt::format("Unsupported virtualization driver: {}", driver)); diff --git a/src/platform/platform_osx.cpp b/src/platform/platform_osx.cpp index db54f4af882..a6f8fa55083 100644 --- a/src/platform/platform_osx.cpp +++ b/src/platform/platform_osx.cpp @@ -274,6 +274,23 @@ std::string mp::platform::Platform::bridge_nomenclature() const return br_nomenclature; } +bool mp::platform::Platform::subnet_used_locally(mp::Subnet subnet) const +{ + // ip routes? + auto can_reach_gateway = [](mp::IPAddress ip) { + const auto ipstr = ip.as_string(); + return MP_UTILS.run_cmd_for_status("ping", + {"-n", "-q", ipstr.c_str(), "-c", "1", "-t", "1"}); + }; + + return can_reach_gateway(subnet.min_address()) || can_reach_gateway(subnet.max_address()); +} + +mp::Subnet mp::platform::Platform::get_preferred_subnet() const +{ + return {"192.168.252.0/22"}; +} + QString mp::platform::Platform::daemon_config_home() const // temporary { auto ret = QStringLiteral("/var/root/Library/Preferences/"); @@ -282,7 +299,8 @@ 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); @@ -295,19 +313,19 @@ 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 } else if (driver == QStringLiteral("applevz")) { #if APPLEVZ_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 941c5e69e60..413ac2e0b2c 100644 --- a/src/platform/platform_win.cpp +++ b/src/platform/platform_win.cpp @@ -623,6 +623,19 @@ std::string mp::platform::Platform::bridge_nomenclature() const return "switch"; } +bool mp::platform::Platform::subnet_used_locally(mp::Subnet subnet) const +{ + // ping + // Get-NetAdapter | Get-NetIPAddress | Format-Table IPAddress,PrefixLength + // throw mp::NotImplementedOnThisBackendException{"AZs @TODO"}; + return false; +} + +mp::Subnet mp::platform::Platform::get_preferred_subnet() const +{ + return {"10.97.0.0/20"}; +} + QString mp::platform::Platform::daemon_config_home() const // temporary { auto ret = systemprofile_app_data_path(); @@ -641,12 +654,13 @@ 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"); /* @@ -655,7 +669,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/rpc/multipass.proto b/src/rpc/multipass.proto index 529c4d1a15c..3ea2eb5a77d 100644 --- a/src/rpc/multipass.proto +++ b/src/rpc/multipass.proto @@ -46,6 +46,8 @@ service Rpc { rpc clone (stream CloneRequest) returns (stream CloneReply); rpc daemon_info (stream DaemonInfoRequest) returns (stream DaemonInfoReply); rpc wait_ready (stream WaitReadyRequest) returns (stream WaitReadyReply); + rpc zones (stream ZonesRequest) returns (stream ZonesReply); + rpc zones_state (stream ZonesStateRequest) returns (stream ZonesStateReply); } message LaunchRequest { @@ -74,6 +76,7 @@ message LaunchRequest { bool permission_to_bridge = 13; int32 timeout = 14; string password = 15; + string zone = 16; } message LaunchError { @@ -83,6 +86,8 @@ message LaunchError { INVALID_DISK_SIZE = 2; INVALID_HOSTNAME = 3; INVALID_NETWORK = 4; + INVALID_ZONE = 5; + ZONE_UNAVAILABLE = 6; } repeated ErrorCodes error_codes = 1; } @@ -124,6 +129,7 @@ message LaunchReply { repeated Alias aliases_to_be_created = 10; repeated string workspaces_to_be_created = 11; bool password_requested = 12; + string zone = 13; } message PurgeRequest { @@ -199,6 +205,7 @@ message InstanceStatus { DELAYED_SHUTDOWN = 6; SUSPENDING = 7; SUSPENDED = 8; + UNAVAILABLE = 9; } Status status = 1; } @@ -245,6 +252,8 @@ message DetailedInfoItem { InstanceDetails instance_info = 7; SnapshotDetails snapshot_info = 8; } + + Zone zone = 9; } message InfoReply { @@ -266,6 +275,7 @@ message ListVMInstance { repeated string ipv4 = 3; string current_release = 5; string os = 6; + Zone zone = 7; } message ListVMSnapshot { @@ -553,3 +563,27 @@ message WaitReadyRequest { message WaitReadyReply { string log_line = 1; } + +message Zone { + string name = 1; + bool available = 2; +} + +message ZonesRequest { + int32 verbosity_level = 1; +} + +message ZonesReply { + string log_line = 1; + repeated Zone zones = 2; +} + +message ZonesStateRequest { + int32 verbosity_level = 1; + bool available = 2; + repeated string zones = 3; +} + +message ZonesStateReply { + string log_line = 1; +} diff --git a/src/utils/json_utils.cpp b/src/utils/json_utils.cpp index 358ec169f2c..4d9be247134 100644 --- a/src/utils/json_utils.cpp +++ b/src/utils/json_utils.cpp @@ -17,6 +17,8 @@ * */ +#include +#include #include #include #include diff --git a/src/utils/vm_specs.cpp b/src/utils/vm_specs.cpp index f91834319e3..26ba22290fe 100644 --- a/src/utils/vm_specs.cpp +++ b/src/utils/vm_specs.cpp @@ -28,25 +28,31 @@ void mp::tag_invoke(const boost::json::value_from_tag&, boost::json::value& json, const mp::VMSpecs& specs) { - json = {{"num_cores", specs.num_cores}, - {"mem_size", std::to_string(specs.mem_size.in_bytes())}, - {"disk_space", std::to_string(specs.disk_space.in_bytes())}, - {"ssh_username", specs.ssh_username}, - {"state", static_cast(specs.state)}, - {"deleted", specs.deleted}, - {"metadata", specs.metadata}, + json = { + {"num_cores", specs.num_cores}, + {"mem_size", std::to_string(specs.mem_size.in_bytes())}, + {"disk_space", std::to_string(specs.disk_space.in_bytes())}, + {"ssh_username", specs.ssh_username}, + {"state", static_cast(specs.state)}, + {"deleted", specs.deleted}, + {"metadata", specs.metadata}, - // Write the networking information. Write first a field "mac_addr" containing the MAC - // address of the default network interface. Then, write all the information about the - // rest of the interfaces. - {"mac_addr", specs.default_mac_address}, - {"extra_interfaces", boost::json::value_from(specs.extra_interfaces)}, - {"mounts", boost::json::value_from(specs.mounts, MapAsJsonArray{"target_path"})}, - {"clone_count", specs.clone_count}}; + // Write the networking information. Write first a field "mac_addr" containing the MAC + // address of the default network interface. Then, write all the information about the + // rest of the interfaces. + {"mac_addr", specs.default_mac_address}, + {"extra_interfaces", boost::json::value_from(specs.extra_interfaces)}, + {"mounts", boost::json::value_from(specs.mounts, MapAsJsonArray{"target_path"})}, + {"clone_count", specs.clone_count}, +#ifdef AVAILABILITY_ZONES_FEATURE + {"zone", specs.zone}, +#endif + }; } mp::VMSpecs mp::tag_invoke(const boost::json::value_to_tag&, - const boost::json::value& json) + const boost::json::value& json, + const AvailabilityZoneManager& az_manager) { auto num_cores = value_to(json.at("num_cores")); auto mem_size = value_to(json.at("mem_size")); @@ -66,15 +72,22 @@ mp::VMSpecs mp::tag_invoke(const boost::json::value_to_tag&, ssh_username = "ubuntu"; using mounts_t = std::unordered_map; - return {num_cores, - MemorySize{mem_size.empty() ? default_memory_size : mem_size}, - MemorySize{disk_space.empty() ? default_disk_size : disk_space}, - mac_addr, - lookup_or>(json, "extra_interfaces", {}), - ssh_username, - static_cast(value_to(json.at("state"))), - value_to(json.at("mounts"), MapAsJsonArray{"target_path"}), - deleted, - metadata, - lookup_or(json, "clone_count", 0)}; + return { + num_cores, + MemorySize{mem_size.empty() ? default_memory_size : mem_size}, + MemorySize{disk_space.empty() ? default_disk_size : disk_space}, + mac_addr, + lookup_or>(json, "extra_interfaces", {}), + ssh_username, + static_cast(value_to(json.at("state"))), + value_to(json.at("mounts"), MapAsJsonArray{"target_path"}), + deleted, + metadata, + lookup_or(json, "clone_count", 0), +#ifdef AVAILABILITY_ZONES_FEATURE + lookup_or(json, "zone", az_manager.get_default_zone_name()), +#else + az_manager.get_default_zone_name(), +#endif + }; } diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index cefb4fa8c42..187a330faa9 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -67,6 +67,7 @@ add_executable(multipass_tests test_daemon_suspend.cpp test_daemon_umount.cpp test_daemon_wait_ready.cpp + test_daemon_zones.cpp test_delayed_shutdown.cpp test_disabled_copy_move.cpp test_exception.cpp @@ -112,6 +113,7 @@ add_executable(multipass_tests test_sshfsmount.cpp test_ssl_cert_provider.cpp test_standard_logger.cpp + test_subnet.cpp test_timer.cpp test_top_catch_all.cpp test_ubuntu_image_host.cpp @@ -123,6 +125,12 @@ add_executable(multipass_tests test_yaml_node_utils.cpp ) +if(AVAILABILITY_ZONES_ENABLED) + target_sources(multipass_tests PRIVATE + test_base_availability_zone.cpp + test_base_availability_zone_manager.cpp) +endif() + target_include_directories(multipass_tests PRIVATE ${CMAKE_SOURCE_DIR} PRIVATE ${CMAKE_SOURCE_DIR}/src diff --git a/tests/unit/applevz/test_applevz_virtual_machine.cpp b/tests/unit/applevz/test_applevz_virtual_machine.cpp index 82cb1543af2..5f62016067a 100644 --- a/tests/unit/applevz/test_applevz_virtual_machine.cpp +++ b/tests/unit/applevz/test_applevz_virtual_machine.cpp @@ -19,6 +19,7 @@ #include "tests/unit/common.h" #include "tests/unit/mock_logger.h" #include "tests/unit/mock_status_monitor.h" +#include "tests/unit/stub_availability_zone_manager.h" #include "tests/unit/stub_ssh_key_provider.h" #include "tests/unit/temp_dir.h" #include "tests/unit/temp_file.h" @@ -44,6 +45,7 @@ struct AppleVZVirtualMachine_UnitTests : public testing::Test mp::MemorySize{"3M"}, mp::MemorySize{}, // not used dummy_vm_name, + "zone1", "aa:bb:cc:dd:ee:ff", {}, "", @@ -59,6 +61,8 @@ struct AppleVZVirtualMachine_UnitTests : public testing::Test mpt::StubSSHKeyProvider stub_key_provider{}; NiceMock mock_monitor; + mpt::StubAvailabilityZoneManager az_manager{}; + mpt::MockAppleVZWrapper::GuardedMock mock_applevz_wrapper_injection{ mpt::MockAppleVZWrapper::inject()}; mpt::MockAppleVZWrapper& mock_applevz = *mock_applevz_wrapper_injection.first; @@ -80,6 +84,7 @@ struct AppleVZVirtualMachine_UnitTests : public testing::Test return std::make_shared(desc, mock_monitor, stub_key_provider, + az_manager.get_zone(desc.zone), instance_dir.path()); } }; diff --git a/tests/unit/daemon_test_fixture.cpp b/tests/unit/daemon_test_fixture.cpp index 5e32d95ecf7..f393d63c3d0 100644 --- a/tests/unit/daemon_test_fixture.cpp +++ b/tests/unit/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" @@ -316,11 +317,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>(); @@ -590,6 +594,7 @@ grpc::Status mpt::DaemonTestFixture::call_daemon_slot(Daemon& daemon, template bool mpt::DaemonTestFixture::is_ready(std::future const&); +// @TODO refactor these explicit template instantiations template grpc::Status mpt::DaemonTestFixture::call_daemon_slot( mp::Daemon&, void (mp::Daemon::*)( @@ -766,3 +771,32 @@ template grpc::Status mpt::DaemonTestFixture::call_daemon_slot( std::promise*), const mp::CloneRequest&, NiceMock>&&); + +template +using DaemonSlotPtr = void (mp::Daemon::*)(const Request*, + grpc::ServerReaderWriterInterface*, + std::promise*); + +template