Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a Stopping state #3483

Draft
wants to merge 49 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4e8fd7d
[rpc] Add option to force stopping an instance.
luis4a0 Jan 29, 2021
862802b
[vm] Add bool to force shutdown.
luis4a0 Jan 29, 2021
992cfc8
[cli] Handle command.
luis4a0 Jan 29, 2021
f79ed51
[tests] Adapt stub and mock to new vm interface.
luis4a0 Jan 29, 2021
2d69854
[daemon] Handle forced stop command
luis4a0 May 28, 2021
ea86fea
[tests] Use the new virtual_machine.h interface.
luis4a0 May 28, 2021
04edcc3
[qemu] Implement forced stop.
luis4a0 May 28, 2021
c9579e7
[lxd] Implement forced stop.
luis4a0 May 28, 2021
48846e6
[libvirt] Implement forced stop.
luis4a0 May 28, 2021
dce9c9f
refactor with main
sharder996 Aug 19, 2022
99f9368
[cli/stop] Make force a long option only
Mar 21, 2024
3cca6db
[virtual_machine] Make force shutdown bool const
Mar 26, 2024
4450fec
[tests] Fix QEMU test for setting bool for shutdown()
Apr 4, 2024
b73e5a0
[tests/cli] Test the --force option for stop command
Apr 5, 2024
9e6737b
[tests/qemu] Add tests for the force shutdown handling
Apr 5, 2024
c39895e
Update src/platform/backends/qemu/qemu_virtual_machine.cpp
Apr 11, 2024
8ac8d93
[tests] Fix tests due to change in log string
Apr 12, 2024
96894a8
[vm] Add a stopping state
ricab Apr 12, 2024
a189588
[daemon] Handle start while stopping
ricab Apr 12, 2024
86490ed
[daemon] Handle reboot while stopping
ricab Apr 12, 2024
5461553
[daemon] Refuse SSH info while stopping
ricab Apr 12, 2024
291592d
[libvirt] Recognize stopping VMs
ricab Apr 12, 2024
a8225da
[libvirt] Prevent start while stopping
ricab Apr 12, 2024
d52683b
[libvirt] Ignore shutdown while stopping
ricab Apr 12, 2024
fbda51f
[libvirt] Ignore suspend while stopping
ricab Apr 12, 2024
352cc58
[lxd] Recognize stopping VMs
ricab Apr 12, 2024
88d259f
[lxd] Refuse to start a stopping VM
ricab Apr 12, 2024
696ab5c
[lxd] Ignore stop if off
ricab Apr 12, 2024
fb36326
[lxd] Ignore shutdown while stopping
ricab Apr 12, 2024
6fa15bc
[lxd] Drop SSH session when stopping
ricab Apr 12, 2024
c6d8d52
[lxd] Handle further states when ensuring running
ricab Apr 12, 2024
f8b1955
[qemu] Recognize stopping VMs
ricab Apr 12, 2024
046ac69
[qemu] Handle multiple QMP entries in single read
ricab Apr 12, 2024
10ab157
[qemu] Warn if we read outside newline boundaries
ricab Apr 12, 2024
c58570c
[qemu] Drop SSH session when stop is detected
ricab Apr 15, 2024
77f9b9f
[qemu] Refuse to start while stopping
ricab Apr 12, 2024
ff76b49
[qemu] Ignore suspend while stopping
ricab Apr 12, 2024
769c4ab
[daemon] Skip (ACPI) shutdown on stopping VMs
ricab Apr 15, 2024
69f7862
[tests] Cover Stopping state in existing test
ricab Apr 15, 2024
9c5d246
[test] Verify is_running on stopping state
ricab Apr 15, 2024
212be77
[cli] Convert stopping state to string
ricab Apr 23, 2024
edc1195
[tests] Check str representation of STOPPING state
ricab Apr 23, 2024
fcd1fb5
[tests] Add stopping instance to formatter tests
ricab Apr 23, 2024
a3631ab
[shutdown] Lock when cancelling delayed_shutdown
ricab Apr 23, 2024
d759e58
[qemu] Lock and log stopping state
ricab Apr 23, 2024
4faa19a
[lxd] Drive LXD instances into Stopping state
ricab Apr 26, 2024
2115048
[libvirt] Drive libvirt VMs into stopping state
ricab Apr 26, 2024
5a768d0
[qemu] Drive QEMU VMs into the stopping state
ricab Apr 30, 2024
89b4ab1
[qemu] Ignore shutdown in more inapplicable states
ricab Apr 30, 2024
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
5 changes: 3 additions & 2 deletions include/multipass/virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,16 @@ class VirtualMachine : private DisabledCopyMove
delayed_shutdown,
suspending,
suspended,
unknown
unknown,
stopping // weird order preserves backward compatibility with persisted states (this was added later)
};

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

virtual ~VirtualMachine() = default;
virtual void start() = 0;
virtual void shutdown() = 0;
virtual void shutdown(const bool force = false) = 0;
virtual void suspend() = 0;
virtual State current_state() = 0;
virtual int ssh_port() = 0;
Expand Down
11 changes: 10 additions & 1 deletion src/client/cli/cmd/stop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ mp::ParseCode cmd::Stop::parse_args(mp::ArgParser* parser)
QCommandLineOption time_option({"t", "time"}, "Time from now, in minutes, to delay shutdown of the instance",
"time", "0");
QCommandLineOption cancel_option({"c", "cancel"}, "Cancel a pending delayed shutdown");
parser->addOptions({all_option, time_option, cancel_option});
QCommandLineOption force_option("force", "Force switching the instance off");
parser->addOptions({all_option, time_option, cancel_option, force_option});

auto status = parser->commandParse(this);
if (status != ParseCode::Ok)
Expand All @@ -105,6 +106,14 @@ mp::ParseCode cmd::Stop::parse_args(mp::ArgParser* parser)
return ParseCode::CommandLineError;
}

if (parser->isSet(force_option) && (parser->isSet(time_option) || parser->isSet(cancel_option)))
{
cerr << "Cannot set \'force\' along with \'time\' or \'cancel\' options at the same time\n";
return ParseCode::CommandLineError;
}

request.set_force_stop(parser->isSet(force_option));

auto time = parser->value(time_option);

if (time.startsWith("+"))
Expand Down
3 changes: 3 additions & 0 deletions src/client/cli/formatter/format_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ std::string mp::format::status_string_for(const mp::InstanceStatus& status)
case mp::InstanceStatus::RESTARTING:
status_val = "Restarting";
break;
case mp::InstanceStatus::STOPPING:
status_val = "Stopping";
break;
case mp::InstanceStatus::DELAYED_SHUTDOWN:
status_val = "Delayed Shutdown";
break;
Expand Down
59 changes: 52 additions & 7 deletions src/daemon/daemon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,8 @@ mp::InstanceStatus::Status grpc_instance_status_for(const mp::VirtualMachine::St
case mp::VirtualMachine::State::off:
case mp::VirtualMachine::State::stopped:
return mp::InstanceStatus::STOPPED;
case mp::VirtualMachine::State::stopping:
return mp::InstanceStatus::STOPPING;
case mp::VirtualMachine::State::starting:
return mp::InstanceStatus::STARTING;
case mp::VirtualMachine::State::restarting:
Expand Down Expand Up @@ -2109,7 +2111,8 @@ try // clang-format on
std::lock_guard lock{start_mutex};
const auto& name = vm_it->first;
auto& vm = *vm_it->second;
switch (vm.current_state())
auto state = vm.current_state();
switch (state)
{
case VirtualMachine::State::unknown:
{
Expand All @@ -2118,9 +2121,16 @@ try // clang-format on
fmt::format_to(std::back_inserter(start_errors), error_string);
continue;
}
case VirtualMachine::State::stopping:
case VirtualMachine::State::suspending:
fmt::format_to(std::back_inserter(start_errors), "Cannot start the instance \'{}\' while suspending", name);
{
auto state_str = state == VirtualMachine::State::stopping ? "stopping" : "suspending";
fmt::format_to(std::back_inserter(start_errors),
"Cannot start the instance \'{}\' while {}",
name,
state_str);
continue;
}
case VirtualMachine::State::delayed_shutdown:
delayed_shutdown_instances.erase(name);
continue;
Expand Down Expand Up @@ -2176,6 +2186,8 @@ try // clang-format on
std::function<grpc::Status(VirtualMachine&)> operation;
if (request->cancel_shutdown())
operation = std::bind(&Daemon::cancel_vm_shutdown, this, std::placeholders::_1);
else if (request->force_stop())
operation = std::bind(&Daemon::switch_off_vm, this, std::placeholders::_1);
else
operation = std::bind(&Daemon::shutdown_vm, this, std::placeholders::_1,
std::chrono::minutes(request->time_minutes()));
Expand Down Expand Up @@ -3114,7 +3126,7 @@ bool mp::Daemon::delete_vm(InstanceTable::iterator vm_it, bool purge, DeleteRepl
delayed_shutdown_instances.erase(name);

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

if (!purge)
{
Expand Down Expand Up @@ -3148,7 +3160,14 @@ grpc::Status mp::Daemon::reboot_vm(VirtualMachine& vm)
if (vm.state == VirtualMachine::State::delayed_shutdown)
delayed_shutdown_instances.erase(vm.vm_name);

if (!MP_UTILS.is_running(vm.current_state()))
// TODO@no-merge streamline this stuff
auto state = vm.current_state();
if (state == VirtualMachine::State::stopping)
return grpc::Status{grpc::StatusCode::INVALID_ARGUMENT,
fmt::format("instance \"{}\" is currently stopping", vm.vm_name),
""};

if (!MP_UTILS.is_running(state))
return grpc::Status{grpc::StatusCode::INVALID_ARGUMENT,
fmt::format("instance \"{}\" is not running", vm.vm_name), ""};

Expand All @@ -3162,7 +3181,7 @@ grpc::Status mp::Daemon::shutdown_vm(VirtualMachine& vm, const std::chrono::mill
const auto& state = vm.current_state();

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

if (std::none_of(cbegin(skip_states), cend(skip_states), [&state](const auto& st) { return state == st; }))
{
Expand All @@ -3183,6 +3202,26 @@ grpc::Status mp::Daemon::shutdown_vm(VirtualMachine& vm, const std::chrono::mill
return grpc::Status::OK;
}

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

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

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

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

return grpc::Status::OK;
}

grpc::Status mp::Daemon::cancel_vm_shutdown(const VirtualMachine& vm)
{
auto it = delayed_shutdown_instances.find(vm.vm_name);
Expand All @@ -3198,10 +3237,16 @@ grpc::Status mp::Daemon::cancel_vm_shutdown(const VirtualMachine& vm)
grpc::Status mp::Daemon::get_ssh_info_for_vm(VirtualMachine& vm, SSHInfoReply& response)
{
const auto& name = vm.vm_name;
if (vm.current_state() == VirtualMachine::State::unknown)
auto state = vm.current_state();

// TODO@no-merge streamline this stuff
if (state == VirtualMachine::State::unknown)
throw std::runtime_error("Cannot retrieve credentials in unknown state");

if (!MP_UTILS.is_running(vm.current_state()))
if (state == VirtualMachine::State::stopping)
return grpc::Status{grpc::StatusCode::ABORTED, fmt::format("instance \"{}\" is stopping", name)};

if (!MP_UTILS.is_running(state))
return grpc::Status{grpc::StatusCode::ABORTED, fmt::format("instance \"{}\" is not running", name)};

if (vm.state == VirtualMachine::State::delayed_shutdown &&
Expand Down
1 change: 1 addition & 0 deletions src/daemon/daemon.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ public slots:
bool delete_vm(InstanceTable::iterator vm_it, bool purge, DeleteReply& response);
grpc::Status reboot_vm(VirtualMachine& vm);
grpc::Status shutdown_vm(VirtualMachine& vm, const std::chrono::milliseconds delay);
grpc::Status switch_off_vm(VirtualMachine& vm);
grpc::Status cancel_vm_shutdown(const VirtualMachine& vm);
grpc::Status get_ssh_info_for_vm(VirtualMachine& vm, SSHInfoReply& response);

Expand Down
8 changes: 7 additions & 1 deletion src/daemon/delayed_shutdown_timer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ mp::DelayedShutdownTimer::~DelayedShutdownTimer()
{
shutdown_timer.stop();
mpl::log(mpl::Level::info, virtual_machine->vm_name, "Cancelling delayed shutdown");
virtual_machine->state = VirtualMachine::State::running;

{
std::lock_guard lock{virtual_machine->state_mutex};
if (virtual_machine->state == VirtualMachine::State::delayed_shutdown)
virtual_machine->state = VirtualMachine::State::running;
}

attempt_ssh_exec(*virtual_machine, "wall The system shutdown has been cancelled");
}
});
Expand Down
56 changes: 39 additions & 17 deletions src/platform/backends/libvirt/libvirt_virtual_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,26 @@ auto refresh_instance_state_for_domain(virDomainPtr domain, const mp::VirtualMac
domain_state == VIR_DOMAIN_NOSTATE)
return mp::VirtualMachine::State::unknown;

if (domain_state == VIR_DOMAIN_SHUTDOWN)
return mp::VirtualMachine::State::stopping;

if (libvirt_wrapper->virDomainHasManagedSaveImage(domain, 0) == 1)
return mp::VirtualMachine::State::suspended;

// Most of these libvirt domain states don't have a Multipass instance state
// analogue, so we'll treat them as "off".
const auto domain_off_states = {VIR_DOMAIN_BLOCKED, VIR_DOMAIN_PAUSED, VIR_DOMAIN_SHUTDOWN,
VIR_DOMAIN_SHUTOFF, VIR_DOMAIN_CRASHED, VIR_DOMAIN_PMSUSPENDED};
const auto domain_off_states = {
VIR_DOMAIN_BLOCKED,
VIR_DOMAIN_PAUSED,
VIR_DOMAIN_SHUTOFF,
VIR_DOMAIN_CRASHED,
VIR_DOMAIN_PMSUSPENDED}; // TODO shouldn't we treat PMSUSPENDED as suspended? and maybe PAUSED as well?

if (std::find(domain_off_states.begin(), domain_off_states.end(), domain_state) != domain_off_states.end())
return mp::VirtualMachine::State::off;

if (domain_state == VIR_DOMAIN_RUNNING && current_instance_state == mp::VirtualMachine::State::off)
return mp::VirtualMachine::State::running;
if (domain_state == VIR_DOMAIN_RUNNING && current_instance_state == mp::VirtualMachine::State::stopping)
return mp::VirtualMachine::State::stopping;

return current_instance_state;
}
Expand Down Expand Up @@ -299,7 +306,7 @@ mp::LibVirtVirtualMachine::~LibVirtVirtualMachine()
update_suspend_status = false;

if (state == State::running)
suspend();
suspend(); // TODO this can throw!
}

void mp::LibVirtVirtualMachine::start()
Expand All @@ -313,9 +320,10 @@ void mp::LibVirtVirtualMachine::start()
domain = domain_by_name_for(vm_name, connection.get(), libvirt_wrapper);

state = refresh_instance_state_for_domain(domain.get(), state, libvirt_wrapper);

if (state == State::suspended)
mpl::log(mpl::Level::info, vm_name, fmt::format("Resuming from a suspended state"));
else if (state == State::stopping)
throw std::runtime_error{fmt::format("Cannot start {} while it is stopping", vm_name)};

state = State::starting;
update_state();
Expand Down Expand Up @@ -345,38 +353,51 @@ void mp::LibVirtVirtualMachine::start()
monitor->on_resume();
}

void mp::LibVirtVirtualMachine::shutdown()
void mp::LibVirtVirtualMachine::shutdown(const bool force)
{
std::unique_lock<decltype(state_mutex)> lock{state_mutex};
auto domain = domain_by_name_for(vm_name, open_libvirt_connection(libvirt_wrapper).get(), libvirt_wrapper);
state = refresh_instance_state_for_domain(domain.get(), state, libvirt_wrapper);
if (state == State::suspended)

if (force)
{
mpl::log(mpl::Level::info, vm_name, fmt::format("Ignoring shutdown issued while suspended"));
mpl::log(mpl::Level::info, vm_name, "Forced shutdown");

auto domain = domain_by_name_for(vm_name, open_libvirt_connection(libvirt_wrapper).get(), libvirt_wrapper);

libvirt_wrapper->virDomainDestroy(domain.get());

state = State::off;
update_state();
}
else
{
drop_ssh_session();

state = refresh_instance_state_for_domain(domain.get(), state, libvirt_wrapper);
if (state == State::running || state == State::delayed_shutdown || state == State::unknown)
{
state = State::stopping;
update_state();

drop_ssh_session();

if (!domain || libvirt_wrapper->virDomainShutdown(domain.get()) == -1)
{
auto warning_string{
fmt::format("Cannot shutdown '{}': {}", vm_name, libvirt_wrapper->virGetLastErrorMessage())};
mpl::log(mpl::Level::warning, vm_name, warning_string);
throw std::runtime_error(warning_string);
}

state = State::off;
update_state();
}
else if (state == State::starting)
{
libvirt_wrapper->virDomainDestroy(domain.get());
state_wait.wait(lock, [this] { return shutdown_while_starting; });
update_state();
}
else if (state == State::suspended || state == State::stopping)
{
auto state_str = state == State::suspended ? "suspended" : "stopping";
mpl::log(mpl::Level::info, vm_name, fmt::format("Ignoring shutdown issued while {}", state_str));
}
}

lock.unlock();
Expand Down Expand Up @@ -404,9 +425,10 @@ void mp::LibVirtVirtualMachine::suspend()
update_state();
}
}
else if (state == State::off)
else if (state == State::off || state == State::stopped || state == State::stopping)
{
mpl::log(mpl::Level::info, vm_name, fmt::format("Ignoring suspend issued while stopped"));
auto state_str = state == State::stopping ? "stopping" : "stopped";
mpl::log(mpl::Level::info, vm_name, fmt::format("Ignoring suspend issued while {}", state_str));
}

monitor->on_suspend();
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() override;
void shutdown(const bool force = false) override;
void suspend() override;
State current_state() override;
int ssh_port() override;
Expand Down
Loading