Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d3c57b4
Add enable-zones and disable-zones commands
levkropp May 12, 2025
1e46870
Add tests for enable-zones and disable-zones commands
levkropp May 14, 2025
5419e19
Implement --all option for enabling zones in CLI
levkropp Jun 2, 2025
b604cb3
Implement --all option for disabling zones in CLI
levkropp Jun 2, 2025
4b1683a
ReturnCode::CommandFail if no zones exist
levkropp Jun 2, 2025
59e33e7
Clarify message for disable-zones --force
levkropp Jun 2, 2025
d1678ab
Enhance description for disable-zones command
levkropp Jun 2, 2025
c7e2314
Enhance description for enable-zones command
levkropp Jun 2, 2025
be0a243
Refine descriptions in disable_zones
levkropp Jul 10, 2025
13b4e3d
Refine description for enable_zones
levkropp Jul 10, 2025
73c4cf5
disable_zones accepts an empty answer to mean Yes
levkropp Jul 10, 2025
df16559
remove redundant no arguments check
levkropp Jul 11, 2025
e1b367e
empty ZonesStateRequest = all zones
levkropp Jul 11, 2025
a0f8c93
add tests for DisableZones::confirm() & on_failure
levkropp Jul 11, 2025
3ecc556
add tests for launch.cpp
levkropp Jul 14, 2025
76e053e
cover enable zones on_failure
levkropp Jul 14, 2025
c319729
cover !is_live() in disable_zones
levkropp Jul 15, 2025
c2a338e
add daemon coverage (requires mocked az headers)
levkropp Jul 15, 2025
5a32132
cover DisableZones::confirm() with multiple zones
levkropp Jul 15, 2025
406cd34
use az_manager var for both if branches in daemon
levkropp Jul 31, 2025
f83e49b
make DisableZones::confirm() const
levkropp Jul 31, 2025
141fe74
add missing space to DisableZones::description string
levkropp Jul 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/client/cli/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
#include "cmd/authenticate.h"
#include "cmd/clone.h"
#include "cmd/delete.h"
#include "cmd/disable_zones.h"
#include "cmd/enable_zones.h"
#include "cmd/exec.h"
#include "cmd/find.h"
#include "cmd/get.h"
Expand Down Expand Up @@ -87,6 +89,8 @@ mp::Client::Client(ClientConfig& config)
add_command<cmd::Authenticate>();
add_command<cmd::Launch>(aliases);
add_command<cmd::Purge>(aliases);
add_command<cmd::DisableZones>();
add_command<cmd::EnableZones>();
add_command<cmd::Exec>(aliases);
add_command<cmd::Find>();
add_command<cmd::Get>();
Expand Down
2 changes: 2 additions & 0 deletions src/client/cli/cmd/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ add_library(commands STATIC
common_cli.cpp
create_alias.cpp
delete.cpp
disable_zones.cpp
enable_zones.cpp
exec.cpp
find.cpp
get.cpp
Expand Down
141 changes: 141 additions & 0 deletions src/client/cli/cmd/disable_zones.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
* 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 "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 <QCommandLineOption>

namespace multipass::cmd
{
ReturnCode 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&) {
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) {
spinner.stop();
return standard_failure_handler_for(name(), cerr, status);
};

const auto streaming_callback = make_logging_spinner_callback<ZonesStateRequest, ZonesStateReply>(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", "<zone> [<zone> ...]");

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
41 changes: 41 additions & 0 deletions src/client/cli/cmd/disable_zones.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

#ifndef MULTIPASS_DISABLE_ZONES_H
#define MULTIPASS_DISABLE_ZONES_H

#include <multipass/cli/command.h>

namespace multipass::cmd
{
class DisableZones : 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:
bool ask_for_confirmation = true;
ZonesStateRequest request{};
ParseCode parse_args(ArgParser* parser);
bool confirm() const;
};
} // namespace multipass::cmd
#endif // MULTIPASS_DISABLE_ZONES_H
101 changes: 101 additions & 0 deletions src/client/cli/cmd/enable_zones.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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 "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 <QCommandLineOption>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comes with argparser.h above.


namespace multipass::cmd
{
ReturnCode 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&) {
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) {
spinner.stop();
return standard_failure_handler_for(name(), cerr, status);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I use a bad AZ, I get this error message:

$ multipass enable-zones asdf
enable-zones failed: no AZ with name "asdf" found

Could we have another PR to avoid "AZ" in messages to the user? Let's stick to "zone" as a single nomenclature. "Availability zone" in the help text and docs is fine though IMO.

};

const auto streaming_callback = make_logging_spinner_callback<ZonesStateRequest, ZonesStateReply>(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", "<zone> [<zone> ...]");

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
39 changes: 39 additions & 0 deletions src/client/cli/cmd/enable_zones.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

#ifndef MULTIPASS_ENABLE_ZONES_H
#define MULTIPASS_ENABLE_ZONES_H

#include <multipass/cli/command.h>

namespace multipass::cmd
{
class EnableZones : 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:
ZonesStateRequest request{};
ParseCode parse_args(ArgParser* parser);
};
} // namespace multipass::cmd
#endif // MULTIPASS_ENABLE_ZONES_H
Loading
Loading