Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion include/multipass/virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,20 @@ class VirtualMachine : private DisabledCopyMove
unknown
};

enum class ShutdownPolicy
{
Powerdown, // gracefully shut down the vm
Poweroff, // forcefully shut down the vm
Halt // halt the vm to non-running state. More specifically. suspended and stopped state will remain the same
// and running state will be shut down to stopped state
};

using UPtr = std::unique_ptr<VirtualMachine>;
using ShPtr = std::shared_ptr<VirtualMachine>;

virtual ~VirtualMachine() = default;
virtual void start() = 0;
virtual void shutdown(bool force = false) = 0;
virtual void shutdown(ShutdownPolicy shutdown_policy = ShutdownPolicy::Powerdown) = 0;
virtual void suspend() = 0;
virtual State current_state() = 0;
virtual int ssh_port() = 0;
Expand Down
7 changes: 6 additions & 1 deletion src/client/cli/cmd/delete.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ mp::ReturnCode cmd::Delete::run(mp::ArgParser* parser)
return mp::ReturnCode::Ok;
};

auto on_failure = [this](grpc::Status& status) { return standard_failure_handler_for(name(), cerr, status); };
auto on_failure = [this](grpc::Status& status) {
// grpc::StatusCode::FAILED_PRECONDITION matches mp::VMStateInvalidException
return status.error_code() == grpc::StatusCode::FAILED_PRECONDITION
? standard_failure_handler_for(name(), cerr, status, "Use --purge to forcefully delete it.")
: standard_failure_handler_for(name(), cerr, status);
};

using Client = grpc::ClientReaderWriterInterface<DeleteRequest, DeleteReply>;
auto streaming_callback = [this](const mp::DeleteReply& reply, Client* client) {
Expand Down
6 changes: 5 additions & 1 deletion src/client/cli/cmd/stop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ mp::ReturnCode cmd::Stop::run(mp::ArgParser* parser)
AnimatedSpinner spinner{cout};
auto on_failure = [this, &spinner](grpc::Status& status) {
spinner.stop();
return standard_failure_handler_for(name(), cerr, status);

// grpc::StatusCode::FAILED_PRECONDITION matches mp::VMStateInvalidException
return status.error_code() == grpc::StatusCode::FAILED_PRECONDITION
? standard_failure_handler_for(name(), cerr, status, "Use --force to power it off.")
: standard_failure_handler_for(name(), cerr, status);
};

spinner.start(instance_action_message_for(request.instance_names(), "Stopping "));
Expand Down
57 changes: 22 additions & 35 deletions src/daemon/daemon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <multipass/exceptions/snapshot_exceptions.h>
#include <multipass/exceptions/sshfs_missing_error.h>
#include <multipass/exceptions/start_exception.h>
#include <multipass/exceptions/virtual_machine_state_exceptions.h>
#include <multipass/ip_address.h>
#include <multipass/json_utils.h>
#include <multipass/logging/client_logger.h>
Expand Down Expand Up @@ -2146,9 +2147,13 @@

status_promise->set_value(status);
}
catch (const mp::VMStateInvalidException& e)

Check warning on line 2150 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L2150

Added line #L2150 was not covered by tests
{
status_promise->set_value(grpc::Status{grpc::StatusCode::FAILED_PRECONDITION, e.what()});

Check warning on line 2152 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L2152

Added line #L2152 was not covered by tests
}
catch (const std::exception& e)
{
status_promise->set_value(grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, e.what(), ""));
status_promise->set_value(grpc::Status(grpc::StatusCode::INTERNAL, e.what()));

Check warning on line 2156 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L2156

Added line #L2156 was not covered by tests
}

void mp::Daemon::suspend(const SuspendRequest* request,
Expand Down Expand Up @@ -2295,9 +2300,13 @@
server->Write(response);
status_promise->set_value(status);
}
catch (const mp::VMStateInvalidException& e)

Check warning on line 2303 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L2303

Added line #L2303 was not covered by tests
{
status_promise->set_value(grpc::Status{grpc::StatusCode::FAILED_PRECONDITION, e.what()});

Check warning on line 2305 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L2305

Added line #L2305 was not covered by tests
}
catch (const std::exception& e)
{
status_promise->set_value(grpc::Status(grpc::StatusCode::FAILED_PRECONDITION, e.what(), ""));
status_promise->set_value(grpc::Status(grpc::StatusCode::INTERNAL, e.what()));

Check warning on line 2309 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L2309

Added line #L2309 was not covered by tests
}

void mp::Daemon::umount(const UmountRequest* request,
Expand Down Expand Up @@ -3075,8 +3084,9 @@
delayed_shutdown_instances.erase(name);

mounts[name].clear();
instance->shutdown(purge);

instance->shutdown(purge == true ? VirtualMachine::ShutdownPolicy::Poweroff
: VirtualMachine::ShutdownPolicy::Halt);
if (!purge)
{
vm_instance_specs[name].deleted = true;
Expand Down Expand Up @@ -3120,50 +3130,27 @@
grpc::Status mp::Daemon::shutdown_vm(VirtualMachine& vm, const std::chrono::milliseconds delay)
{
const auto& name = vm.vm_name;
const auto& current_state = vm.current_state();
delayed_shutdown_instances.erase(name);

Check warning on line 3133 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L3133

Added line #L3133 was not covered by tests

using St = VirtualMachine::State;
const auto skip_states = {St::off, St::stopped, St::suspended};
auto stop_all_mounts = [this](const std::string& name) { stop_mounts(name); };
auto& shutdown_timer = delayed_shutdown_instances[name] =
std::make_unique<DelayedShutdownTimer>(&vm, stop_all_mounts);

Check warning on line 3137 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L3135-L3137

Added lines #L3135 - L3137 were not covered by tests

if (std::none_of(cbegin(skip_states), cend(skip_states), [&current_state](const auto& skip_state) {
return current_state == skip_state;
}))
{
QObject::connect(shutdown_timer.get(), &DelayedShutdownTimer::finished, [this, name]() {

Check warning on line 3139 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L3139

Added line #L3139 was not covered by tests
delayed_shutdown_instances.erase(name);
});

Check warning on line 3141 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L3141

Added line #L3141 was not covered by tests

auto stop_all_mounts = [this](const std::string& name) { stop_mounts(name); };
auto& shutdown_timer = delayed_shutdown_instances[name] =
std::make_unique<DelayedShutdownTimer>(&vm, stop_all_mounts);

QObject::connect(shutdown_timer.get(), &DelayedShutdownTimer::finished,
[this, name]() { delayed_shutdown_instances.erase(name); });

shutdown_timer->start(delay);
}
else
mpl::log(mpl::Level::debug, category, fmt::format("instance \"{}\" does not need stopping", name));
shutdown_timer->start(delay);

Check warning on line 3143 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L3143

Added line #L3143 was not covered by tests

return grpc::Status::OK;
}

grpc::Status mp::Daemon::switch_off_vm(VirtualMachine& vm)
{
const auto& name = vm.vm_name;
const auto& current_state = vm.current_state();
delayed_shutdown_instances.erase(name);

Check warning on line 3151 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L3151

Added line #L3151 was not covered by tests

using St = VirtualMachine::State;
const auto skip_states = {St::off, St::stopped};

if (std::none_of(cbegin(skip_states), cend(skip_states), [&current_state](const auto& skip_state) {
return current_state == skip_state;
}))
{
delayed_shutdown_instances.erase(name);

vm.shutdown(true);
}
else
mpl::log(mpl::Level::debug, category, fmt::format("instance \"{}\" does not need stopping", name));
vm.shutdown(VirtualMachine::ShutdownPolicy::Poweroff);

Check warning on line 3153 in src/daemon/daemon.cpp

View check run for this annotation

Codecov / codecov/patch

src/daemon/daemon.cpp#L3153

Added line #L3153 was not covered by tests

return grpc::Status::OK;
}
Expand Down
6 changes: 3 additions & 3 deletions src/platform/backends/libvirt/libvirt_virtual_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ void mp::LibVirtVirtualMachine::start()
monitor->on_resume();
}

void mp::LibVirtVirtualMachine::shutdown(bool force)
void mp::LibVirtVirtualMachine::shutdown(ShutdownPolicy shutdown_policy)
{
std::unique_lock<std::mutex> lock{state_mutex};
auto domain = checked_vm_domain();
Expand All @@ -355,15 +355,15 @@ void mp::LibVirtVirtualMachine::shutdown(bool force)

try
{
check_state_for_shutdown(force);
check_state_for_shutdown(shutdown_policy);
}
catch (const VMStateIdempotentException& e)
{
mpl::log(mpl::Level::info, vm_name, e.what());
return;
}

if (force) // TODO delete suspension state if it exists
if (shutdown_policy == ShutdownPolicy::Poweroff) // TODO delete suspension state if it exists
{
mpl::log(mpl::Level::info, vm_name, "Forcing shutdown");

Expand Down
2 changes: 1 addition & 1 deletion src/platform/backends/libvirt/libvirt_virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class LibVirtVirtualMachine final : public BaseVirtualMachine
~LibVirtVirtualMachine();

void start() override;
void shutdown(bool force = false) override;
void shutdown(ShutdownPolicy shutdown_policy = ShutdownPolicy::Powerdown) override;
void suspend() override;
State current_state() override;
int ssh_port() override;
Expand Down
7 changes: 4 additions & 3 deletions src/platform/backends/lxd/lxd_virtual_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -262,23 +262,24 @@ void mp::LXDVirtualMachine::start()
update_state();
}

void mp::LXDVirtualMachine::shutdown(bool force)
void mp::LXDVirtualMachine::shutdown(ShutdownPolicy shutdown_policy)
{
std::unique_lock<std::mutex> lock{state_mutex};

const auto present_state = current_state();

try
{
check_state_for_shutdown(force);
check_state_for_shutdown(shutdown_policy);
}
catch (const VMStateIdempotentException& e)
{
mpl::log(mpl::Level::info, vm_name, e.what());
return;
}

request_state("stop", {{"force", force}});
// ShutdownPolicy::Poweroff is force and the other two values are non-force
request_state("stop", {{"force", shutdown_policy == ShutdownPolicy::Poweroff}});

state = State::off;

Expand Down
2 changes: 1 addition & 1 deletion src/platform/backends/lxd/lxd_virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class LXDVirtualMachine : public BaseVirtualMachine
~LXDVirtualMachine() override;

void start() override;
void shutdown(bool force = false) override;
void shutdown(ShutdownPolicy shutdown_policy = ShutdownPolicy::Powerdown) override;
void suspend() override;
State current_state() override;
int ssh_port() override;
Expand Down
9 changes: 4 additions & 5 deletions src/platform/backends/qemu/qemu_virtual_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -345,29 +345,28 @@ void mp::QemuVirtualMachine::start()
vm_process->write(qmp_execute_json("qmp_capabilities"));
}

void mp::QemuVirtualMachine::shutdown(bool force)
void mp::QemuVirtualMachine::shutdown(ShutdownPolicy shutdown_policy)
{
std::unique_lock<std::mutex> lock{state_mutex};

force_shutdown = force;

try
{
check_state_for_shutdown(force);
check_state_for_shutdown(shutdown_policy);
}
catch (const VMStateIdempotentException& e)
{
mpl::log(mpl::Level::info, vm_name, e.what());
return;
}

if (force)
if (shutdown_policy == ShutdownPolicy::Poweroff)
{
mpl::log(mpl::Level::info, vm_name, "Forcing shutdown");

if (vm_process)
{
mpl::log(mpl::Level::info, vm_name, "Killing process");
force_shutdown = true;
lock.unlock();
vm_process->kill();
if (vm_process != nullptr && !vm_process->wait_for_finished(kill_process_timeout))
Expand Down
2 changes: 1 addition & 1 deletion src/platform/backends/qemu/qemu_virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class QemuVirtualMachine : public QObject, public BaseVirtualMachine
~QemuVirtualMachine();

void start() override;
void shutdown(bool force = false) override;
void shutdown(ShutdownPolicy shutdown_policy = ShutdownPolicy::Powerdown) override;
void suspend() override;
State current_state() override;
int ssh_port() override;
Expand Down
23 changes: 14 additions & 9 deletions src/platform/backends/shared/base_virtual_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,34 +188,39 @@
return MP_CLOUD_INIT_FILE_OPS.get_instance_id_from_cloud_init(cloud_init_config_iso_file_path);
}

void mp::BaseVirtualMachine::check_state_for_shutdown(bool force)
void mp::BaseVirtualMachine::check_state_for_shutdown(ShutdownPolicy shutdown_policy)
{
const std::string force_statement{"Use --force to override."};

// A mutex should already be locked by the caller here
if (state == State::off || state == State::stopped)
{
throw VMStateIdempotentException{"Ignoring shutdown since instance is already stopped."};
}

if (force)
if (shutdown_policy == ShutdownPolicy::Poweroff)
{
return;
}

if (state == State::suspending)
if (state == State::suspended)
{
throw VMStateInvalidException{fmt::format("Cannot stop instance while suspending. {}", force_statement)};
if (shutdown_policy == ShutdownPolicy::Halt)
{
throw VMStateIdempotentException{"Ignoring shutdown since instance is already suspended."};

Check warning on line 208 in src/platform/backends/shared/base_virtual_machine.cpp

View check run for this annotation

Codecov / codecov/patch

src/platform/backends/shared/base_virtual_machine.cpp#L208

Added line #L208 was not covered by tests
}
else // else only can be ShutdownPolicy::Powerdown since ShutdownPolicy::Poweroff check was preemptively done.
{
throw VMStateInvalidException{fmt::format("Cannot shut down suspended instance {}.", vm_name)};
}
}

if (state == State::suspended)
if (state == State::suspending)
{
throw VMStateInvalidException{fmt::format("Cannot stop suspended instance. {}", force_statement)};
throw VMStateInvalidException{fmt::format("Cannot shut down instance {} while suspending.", vm_name)};
}

if (state == State::starting || state == State::restarting)
{
throw VMStateInvalidException{fmt::format("Cannot stop instance while starting. {}", force_statement)};
throw VMStateInvalidException{fmt::format("Cannot shut down instance {} while starting.", vm_name)};
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/platform/backends/shared/base_virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class BaseVirtualMachine : public VirtualMachine
const std::string& new_instance_id) const;
virtual std::string get_instance_id_from_the_cloud_init() const;

virtual void check_state_for_shutdown(bool force);
virtual void check_state_for_shutdown(ShutdownPolicy shutdown_policy);

private:
using SnapshotMap = std::unordered_map<std::string, std::shared_ptr<Snapshot>>;
Expand Down
2 changes: 1 addition & 1 deletion tests/libvirt/test_libvirt_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ TEST_F(LibVirtBackend, shutdown_while_starting_throws_and_sets_correct_state)

ASSERT_EQ(machine->state, mp::VirtualMachine::State::starting);

mp::AutoJoinThread thread = [&machine] { machine->shutdown(true); };
mp::AutoJoinThread thread = [&machine] { machine->shutdown(mp::VirtualMachine::ShutdownPolicy::Poweroff); };

using namespace std::chrono_literals;
while (!destroy_called)
Expand Down
8 changes: 5 additions & 3 deletions tests/lxd/test_lxd_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1702,7 +1702,7 @@ TEST_F(LXDBackend, shutdown_while_stopped_does_nothing_and_logs_debug)

TEST_F(LXDBackend, shutdown_while_frozen_throws_and_logs_info)
{
const std::string error_msg{"Cannot stop suspended instance. Use --force to override."};
const std::string sub_error_msg{"Cannot shut down suspended instance"};
mpt::MockVMStatusMonitor mock_monitor;

EXPECT_CALL(*mock_network_access_manager, createRequest(_, _, _)).WillRepeatedly([](auto, auto request, auto) {
Expand Down Expand Up @@ -1730,7 +1730,7 @@ TEST_F(LXDBackend, shutdown_while_frozen_throws_and_logs_info)

EXPECT_CALL(mock_monitor, persist_state_for(_, _));

MP_EXPECT_THROW_THAT(machine.shutdown(), mp::VMStateInvalidException, mpt::match_what(StrEq(error_msg)));
MP_EXPECT_THROW_THAT(machine.shutdown(), mp::VMStateInvalidException, mpt::match_what(HasSubstr(sub_error_msg)));

EXPECT_EQ(machine.current_state(), mp::VirtualMachine::State::suspended);
}
Expand Down Expand Up @@ -1843,7 +1843,9 @@ TEST_F(LXDBackend, shutdown_while_starting_throws_and_sets_correct_state)

ASSERT_EQ(machine.state, mp::VirtualMachine::State::starting);

mp::AutoJoinThread thread = [&machine] { machine.shutdown(true); }; // force shutdown
mp::AutoJoinThread thread = [&machine] {
machine.shutdown(mp::VirtualMachine::ShutdownPolicy::Poweroff);
}; // force shutdown

while (machine.state != mp::VirtualMachine::State::off)
std::this_thread::sleep_for(1ms);
Expand Down
2 changes: 1 addition & 1 deletion tests/mock_virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct MockVirtualMachineT : public T
}

MOCK_METHOD(void, start, (), (override));
MOCK_METHOD(void, shutdown, (bool), (override));
MOCK_METHOD(void, shutdown, (VirtualMachine::ShutdownPolicy), (override));
MOCK_METHOD(void, suspend, (), (override));
MOCK_METHOD(multipass::VirtualMachine::State, current_state, (), (override));
MOCK_METHOD(int, ssh_port, (), (override));
Expand Down
Loading
Loading