Skip to content

Commit

Permalink
Merge pull request #3264 from canonical/multipass_clone
Browse files Browse the repository at this point in the history
Multipass clone
  • Loading branch information
ricab authored Oct 19, 2024
2 parents 2f84668 + cf96ba7 commit ea024df
Show file tree
Hide file tree
Showing 60 changed files with 1,309 additions and 98 deletions.
8 changes: 7 additions & 1 deletion completions/bash/multipass
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ _multipass_complete()
prev_opts=false
multipass_cmds="authenticate transfer delete exec find help info launch list mount networks \
purge recover restart restore shell snapshot start stop suspend umount version get set \
alias aliases unalias"
clone alias aliases unalias"

if [[ "${multipass_cmds}" =~ " ${cmd} " || "${multipass_cmds}" =~ ^${cmd} || "${multipass_cmds}" =~ \ ${cmd}$ ]];
then
Expand Down Expand Up @@ -283,6 +283,9 @@ _multipass_complete()
"restore")
_add_nonrepeating_args "--destructive"
;;
"clone")
_add_nonrepeating_args "--name"
;;
"get")
_add_nonrepeating_args "--raw --keys"
;;
Expand Down Expand Up @@ -369,6 +372,9 @@ _multipass_complete()
"snapshot")
_multipass_instances "Stopped"
;;
"clone")
_multipass_instances "Stopped"
;;
"restore")
_multipass_restorable_snapshots
;;
Expand Down
5 changes: 5 additions & 0 deletions include/multipass/cloud_init_iso.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ class CloudInitFileOps : public Singleton<CloudInitFileOps>
const std::vector<NetworkInterface>& extra_interfaces,
const std::string& new_instance_id,
const std::filesystem::path& cloud_init_path) const;

virtual void update_cloned_cloud_init_unique_identifiers(const std::string& default_mac_addr,
const std::vector<NetworkInterface>& extra_interfaces,
const std::string& new_hostname,
const std::filesystem::path& cloud_init_path) const;
virtual void add_extra_interface_to_cloud_init(const std::string& default_mac_addr,
const NetworkInterface& extra_interfaces,
const std::filesystem::path& cloud_init_path) const;
Expand Down
2 changes: 2 additions & 0 deletions include/multipass/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ constexpr auto mounts_key = "local.privileged-mounts"; // idem
constexpr auto winterm_key = "client.apps.windows-terminal.profiles"; // idem
constexpr auto mirror_key = "local.image.mirror"; // idem; this defines the mirror of simple streams

constexpr auto cloud_init_file_name = "cloud-init-config.iso";

[[maybe_unused]] // hands off clang-format
constexpr auto key_examples = {petenv_key, driver_key, mounts_key};

Expand Down
32 changes: 32 additions & 0 deletions include/multipass/exceptions/clone_exceptions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
*/

#ifndef MULTIPASS_CLONE_EXCEPTIONS_H
#define MULTIPASS_CLONE_EXCEPTIONS_H

#include <stdexcept>

namespace multipass
{
class CloneInvalidNameException : public std::runtime_error
{
public:
using std::runtime_error::runtime_error;
};
} // namespace multipass

#endif // MULTIPASS_CLONE_EXCEPTIONS_H
1 change: 1 addition & 0 deletions include/multipass/file_ops.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class FileOps : public Singleton<FileOps>
std::ios_base::openmode mode = std::ios_base::out) const;
virtual std::unique_ptr<std::istream> open_read(const fs::path& path,
std::ios_base::openmode mode = std::ios_base::in) const;
virtual void copy(const fs::path& src, const fs::path& dist, fs::copy_options copy_options) const;
virtual bool exists(const fs::path& path, std::error_code& err) const;
virtual bool is_directory(const fs::path& path, std::error_code& err) const;
virtual bool create_directory(const fs::path& path, std::error_code& err) const;
Expand Down
9 changes: 9 additions & 0 deletions include/multipass/json_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,22 @@

namespace multipass
{
struct VMSpecs;
class JsonUtils : public Singleton<JsonUtils>
{
public:
explicit JsonUtils(const Singleton<JsonUtils>::PrivatePass&) noexcept;

virtual void write_json(const QJsonObject& root, QString file_name) const; // transactional; creates parent dirs
virtual std::string json_to_string(const QJsonObject& root) const;
virtual QJsonValue update_cloud_init_instance_id(const QJsonValue& cloud_init_instance_id_value,
const std::string& src_vm_name,
const std::string& dest_vm_name) const;
virtual QJsonValue update_unique_identifiers_of_metadata(const QJsonValue& metadata_value,
const multipass::VMSpecs& src_specs,
const multipass::VMSpecs& dest_specs,
const std::string& src_vm_name,
const std::string& dest_vm_name) const;
virtual QJsonArray extra_interfaces_to_json_array(const std::vector<NetworkInterface>& extra_interfaces) const;
virtual std::optional<std::vector<NetworkInterface>> read_extra_interfaces(const QJsonObject& record) const;
};
Expand Down
1 change: 1 addition & 0 deletions include/multipass/virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class VirtualMachine : private DisabledCopyMove
virtual void load_snapshots() = 0;
virtual std::vector<std::string> get_childrens_names(const Snapshot* parent) const = 0;
virtual int get_snapshot_count() const = 0;
virtual void remove_snapshots_from_image() const = 0;

QDir instance_directory() const;

Expand Down
8 changes: 8 additions & 0 deletions include/multipass/virtual_machine_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ class VirtualMachineFactory : private DisabledCopyMove
virtual VirtualMachine::UPtr create_virtual_machine(const VirtualMachineDescription& desc,
const SSHKeyProvider& key_provider,
VMStatusMonitor& monitor) = 0;
virtual VirtualMachine::UPtr create_vm_and_clone_instance_dir_data(const VMSpecs& src_vm_spec,
const VMSpecs& dest_vm_spec,
const std::string& source_name,
const std::string& destination_name,
const VMImage& dest_vm_image,
const SSHKeyProvider& key_provider,
VMStatusMonitor& monitor) = 0;

/** Removes any resources associated with a VM of the given name.
*
Expand All @@ -73,6 +80,7 @@ class VirtualMachineFactory : private DisabledCopyMove
virtual std::vector<NetworkInterfaceInfo> networks() const = 0;
virtual void require_snapshots_support() const = 0;
virtual void require_suspend_support() const = 0;
virtual void require_clone_support() const = 0;

protected:
VirtualMachineFactory() = default;
Expand Down
1 change: 1 addition & 0 deletions include/multipass/vm_image_vault.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class VMImageVault : private DisabledCopyMove
virtual void update_images(const FetchType& fetch_type, const PrepareAction& prepare,
const ProgressMonitor& monitor) = 0;
virtual MemorySize minimum_image_size_for(const std::string& id) = 0;
virtual void clone(const std::string& source_instance_name, const std::string& destination_instance_name) = 0;
virtual VMImageHost* image_host_for(const std::string& remote_name) const = 0;
virtual std::vector<std::pair<std::string, VMImageInfo>> all_info_for(const Query& query) const = 0;

Expand Down
23 changes: 13 additions & 10 deletions include/multipass/vm_specs.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct VMSpecs
std::unordered_map<std::string, VMMount> mounts;
bool deleted;
QJsonObject metadata;
int clone_count = 0; // tracks the number of cloned vm from this source vm (regardless of deletes)
};

inline bool operator==(const VMSpecs& a, const VMSpecs& b)
Expand All @@ -57,16 +58,18 @@ inline bool operator==(const VMSpecs& a, const VMSpecs& b)
a.state,
a.mounts,
a.deleted,
a.metadata) == std::tie(b.num_cores,
b.mem_size,
b.disk_space,
b.default_mac_address,
b.extra_interfaces,
b.ssh_username,
b.state,
b.mounts,
b.deleted,
b.metadata);
a.metadata,
a.clone_count) == std::tie(b.num_cores,
b.mem_size,
b.disk_space,
b.default_mac_address,
b.extra_interfaces,
b.ssh_username,
b.state,
b.mounts,
b.deleted,
b.metadata,
a.clone_count);
}

inline bool operator!=(const VMSpecs& a, const VMSpecs& b) // TODO drop in C++20
Expand Down
4 changes: 4 additions & 0 deletions include/multipass/yaml_node_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ YAML::Node make_cloud_init_network_config(const std::string& default_mac_addr,
YAML::Node add_extra_interface_to_network_config(const std::string& default_mac_addr,
const NetworkInterface& extra_interface,
const std::string& network_config_file_content);
// the make_cloud_init_network_config and add_extra_interface_to_network_config functions are adapted to generate the
// new format network-config file (having default interface present and having dhcp-identifier: mac on every network
// interface). At the same time, it also needs to take care of the pre-existed file, meaning that the generated file
// from the old file should have the new format.
} // namespace utils
} // namespace multipass
#endif // MULTIPASS_YAML_NODE_UTILS_H
2 changes: 2 additions & 0 deletions src/client/cli/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "cmd/alias.h"
#include "cmd/aliases.h"
#include "cmd/authenticate.h"
#include "cmd/clone.h"
#include "cmd/delete.h"
#include "cmd/exec.h"
#include "cmd/find.h"
Expand Down Expand Up @@ -108,6 +109,7 @@ mp::Client::Client(ClientConfig& config)
add_command<cmd::Delete>(aliases);
add_command<cmd::Umount>();
add_command<cmd::Version>();
add_command<cmd::Clone>();

sort_commands();

Expand Down
1 change: 1 addition & 0 deletions src/client/cli/cmd/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ add_library(commands STATIC
aliases.cpp
animated_spinner.cpp
authenticate.cpp
clone.cpp
common_cli.cpp
create_alias.cpp
delete.cpp
Expand Down
109 changes: 109 additions & 0 deletions src/client/cli/cmd/clone.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
*/

#include "clone.h"

#include "animated_spinner.h"
#include "common_cli.h"

#include <multipass/cli/argparser.h>

namespace mp = multipass;
namespace cmd = multipass::cmd;

mp::ReturnCode cmd::Clone::run(ArgParser* parser)
{
const auto parscode = parse_args(parser);
if (parscode != ParseCode::Ok)
{
return parser->returnCodeFrom(parscode);
}

AnimatedSpinner spinner{cout};
auto action_on_success = [this, &spinner](CloneReply& reply) -> ReturnCode {
spinner.stop();
cout << reply.reply_message();

return ReturnCode::Ok;
};

auto action_on_failure = [this, &spinner](grpc::Status& status, CloneReply& reply) -> ReturnCode {
spinner.stop();
return standard_failure_handler_for(name(), cerr, status, reply.reply_message());
};

spinner.start("Cloning " + rpc_request.source_name());
return dispatch(&RpcMethod::clone, rpc_request, action_on_success, action_on_failure);
}

std::string cmd::Clone::name() const
{
return "clone";
}

QString cmd::Clone::short_help() const
{
return QStringLiteral("Clone an instance");
}

QString cmd::Clone::description() const
{
return QStringLiteral(
"Create an independent copy of an existing instance. The instance must be stopped before you proceed.");
}

mp::ParseCode cmd::Clone::parse_args(ArgParser* parser)
{
parser->addPositionalArgument("source_name", "The name of the source instance to be cloned", "<source_name>");

const QCommandLineOption destination_name_option{
{"n", "name"},
"Specify a custom name for the cloned instance. The name must follow the same validity rules as instance names "
"(see \"help launch\"). Default: \"<source_name>-cloneN\", where N is the Nth cloned instance.",
"destination-name"};

parser->addOption(destination_name_option);

const auto status = parser->commandParse(this);
if (status != ParseCode::Ok)
{
return status;
}

const auto number_of_positional_arguments = parser->positionalArguments().count();
if (number_of_positional_arguments < 1)
{
cerr << "Please provide the name of the source instance.\n";
return ParseCode::CommandLineError;
}

if (number_of_positional_arguments > 1)
{
cerr << "Too many arguments.\n";
return ParseCode::CommandLineError;
}

const auto source_name = parser->positionalArguments()[0];
rpc_request.set_source_name(source_name.toStdString());
rpc_request.set_verbosity_level(parser->verbosityLevel());
if (parser->isSet(destination_name_option))
{
rpc_request.set_destination_name(parser->value(destination_name_option).toStdString());
}

return ParseCode::Ok;
}
44 changes: 44 additions & 0 deletions src/client/cli/cmd/clone.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*
*/

#ifndef MULTIPASS_CLONE_H
#define MULTIPASS_CLONE_H

#include <multipass/cli/command.h>

namespace multipass
{
namespace cmd
{
class Clone final : public Command
{
public:
using Command::Command;
ReturnCode run(ArgParser* parser) override;

std::string name() const override;
QString short_help() const override;
QString description() const override;

private:
ParseCode parse_args(ArgParser* parser);

CloneRequest rpc_request;
};
} // namespace cmd
} // namespace multipass
#endif // MULTIPASS_CLONE_H
Loading

0 comments on commit ea024df

Please sign in to comment.