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