Skip to content
Open
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
163 changes: 122 additions & 41 deletions src/platform/backends/hyperv_api/hcs_virtual_machine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,16 +121,20 @@ HCSVirtualMachine::HCSVirtualMachine(const std::string& network_guid,
monitor(monitor)
{
const auto created_from_scratch = maybe_create_compute_system();
const auto state = fetch_state_from_api();
const auto compute_state = fetch_state_from_api();

mpl::debug(get_name(),
"HCSVirtualMachine() > created_from_scratch: {}, state: {}",
created_from_scratch,
state);
compute_state);

// Reflect compute system's state
set_state(state);
HCSVirtualMachine::handle_state_update();
auto prev_state = state;
set_state(compute_state);

// Persist initial state even if unchanged
if (prev_state == this->state)
HCSVirtualMachine::handle_state_update();
}

HCSVirtualMachine::~HCSVirtualMachine()
Expand All @@ -142,8 +146,7 @@ HCSVirtualMachine::~HCSVirtualMachine()
// Auto-suspend if running
suspend();
// Persist previous VM state
state = VirtualMachine::State::running;
handle_state_update();
set_state(State::running);
});
}

Expand All @@ -162,8 +165,7 @@ void HCSVirtualMachine::compute_system_event_callback(HCS_EVENT* event, void* co
case hcs::HcsEventType::SystemExited:
{
mpl::info(vm->get_name(), "compute_system_event_callback() > SystemExited event received");
vm->state = State::off;
vm->handle_state_update();
vm->set_state(State::off);
vm->termination_signal.signal();
}
break;
Expand Down Expand Up @@ -364,31 +366,36 @@ void HCSVirtualMachine::set_state(hcs::ComputeSystemState compute_system_state)
return;
}

const auto prev_state = state;
switch (compute_system_state)
{
case hcs::ComputeSystemState::created:
state = State::off;
set_state(State::off);
break;
case hcs::ComputeSystemState::paused:
state = State::suspended;
mpl::debug(vm_name, "VM is paused but not completely suspended");
set_state(State::suspended);
break;
case hcs::ComputeSystemState::running:
state = State::running;
set_state(State::running);
break;
case hcs::ComputeSystemState::saved_as_template:
case hcs::ComputeSystemState::stopped:
state = has_saved_state_file() ? State::suspended : State::stopped;
set_state(has_saved_state_file() ? State::suspended : State::stopped);
break;
case hcs::ComputeSystemState::unknown:
state = State::unknown;
set_state(State::unknown);
break;
}
}

if (state == prev_state)
void HCSVirtualMachine::set_state(VirtualMachine::State new_state)
{
if (state == new_state)
return;

mpl::info(get_name(), "set_state() -> State changed from {} to {}", prev_state, state);
mpl::info(get_name(), "set_state() -> State changed from {} to {}", state, new_state);
state = new_state;
handle_state_update();
}

void HCSVirtualMachine::start()
Expand All @@ -400,8 +407,7 @@ void HCSVirtualMachine::start()
mpl::debug(get_name(), "start() -> VM was not present, created from scratch");

const auto prev_state = state;
state = VirtualMachine::State::starting;
handle_state_update();
set_state(VirtualMachine::State::starting);
// Resume and start are the same thing in Multipass terms
// Try to determine whether we need to resume or start here.
const auto result = [&] {
Expand All @@ -426,8 +432,7 @@ void HCSVirtualMachine::start()

if (!result)
{
state = prev_state;
handle_state_update();
set_state(prev_state);
throw StartComputeSystemException{"Could not start the VM: {}", result};
}
else if (has_saved_state_file())
Expand All @@ -444,6 +449,7 @@ void HCSVirtualMachine::start()

mpl::debug(get_name(), "start() -> result `{}`", result);
}

void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy)
{
mpl::debug(get_name(), "shutdown() -> Shutting down, current state {}", state);
Expand All @@ -458,6 +464,10 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy)
return;
}

// Ensure that the ssh session is dropped at the end of this function, even if shutdown
// fails, since it means that the VM is in some sort of error state.
auto drop_ssh_session_sg = sg::make_scope_guard([this]() noexcept { drop_ssh_session(); });

switch (shutdown_policy)
{
case ShutdownPolicy::Powerdown:
Expand All @@ -468,23 +478,36 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy)
{
// Fall back to SSH shutdown.
ssh_exec("sudo shutdown -h now");
drop_ssh_session();
}
break;
case ShutdownPolicy::Halt:
case ShutdownPolicy::Poweroff:
mpl::debug(get_name(),
"shutdown() -> Requested halt/poweroff, initiating forceful shutdown");

// FIXME: There is a rare case where suspend fails, and fails to terminate
// the VM as well. In this case the VM will be "paused". This is not handled
// for now.
if (state == State::suspended)
{
if (const auto ec = remove_saved_state_file_if_exists(); ec)
throw ShutdownComputeSystemException("Could not remove state file '{}': {}",
get_saved_state_file_path(),
ec);
update_current_state();
return;
}
// These are non-graceful variants. Just terminate the system immediately.
const auto r = HCS().terminate_compute_system(hcs_system);
if (!r)
throw ShutdownComputeSystemException("Could not terminate VM `{}`: {}", get_name(), r);
mpl::debug(get_name(), "shutdown -> terminate_compute_system result: {}", r.code);
drop_ssh_session();
break;
}

// We need to wait here.
if (!termination_signal.wait_for(vm_shutdown_timeout))
throw std::runtime_error("timed out waiting for VM shutdown to complete");
throw ShutdownComputeSystemException("timed out waiting for VM shutdown to complete");

switch (auto s = current_state())
{
Expand All @@ -493,7 +516,7 @@ void HCSVirtualMachine::shutdown(ShutdownPolicy shutdown_policy)
case VirtualMachine::State::suspended:
break;
default:
mpl::warn(get_name(), "shutdown -> VM is not in stopped state after termination: {}", s);
throw ShutdownComputeSystemException("VM is not in stopped state after termination: {}", s);
break;
}
}
Expand All @@ -502,34 +525,74 @@ void HCSVirtualMachine::suspend()
{
mpl::debug(get_name(), "suspend() -> Suspending, current state {}", state);

if (const auto pause_result = HCS().pause_compute_system(hcs_system))
if (const auto pause_result = HCS().pause_compute_system(hcs_system); !pause_result)
throw SaveComputeSystemException{"Could not pause VM for suspend: {}", pause_result};

const auto save_result = HCS().save_compute_system(hcs_system, get_saved_state_file_path());
if (save_result)
{
// Pause succeeded. We can suspend to disk now
if (const auto& r = HCS().save_compute_system(hcs_system, get_saved_state_file_path()); r)
{
// Save succeeded. Now, it's safe to terminate the system.
shutdown(ShutdownPolicy::Poweroff);
}
else
throw SaveComputeSystemException{"Could not save the virtual machine state for VM `{}` "
"to the disk for suspend. Error details: {}",
get_name(),
r};
// Save succeeded. Now, it's safe to terminate the system.
return shutdown(ShutdownPolicy::Poweroff);
}
else

// If saved failed, we try to resume
if (const auto ec = remove_saved_state_file_if_exists(); ec)
{
throw SaveComputeSystemException{"Could not pause VM for suspend: {}", pause_result};
// FIXME: This leaves the class in an undefined state.
// Although the suspend file is corrupt, terminating the VM will still
// transition it to the suspended state.
mpl::warn(get_name(),
"Could not remove state file after partial write '{}': {}",
get_saved_state_file_path(),
ec);
}

set_state(fetch_state_from_api());
handle_state_update();
const auto resume_result = HCS().resume_compute_system(hcs_system);
if (resume_result)
{
update_current_state();
throw SaveComputeSystemException{
"Could not save the virtual machine state for VM `{}` to disk for suspend; the VM "
"was resumed. Error details: {}",
get_name(),
save_result};
}

// If resuming failed as well, we try to force shutdown.
try
{
shutdown(ShutdownPolicy::Poweroff);
}
catch (const std::exception& exception)
{
// If we're here, then everything failed.
throw SaveComputeSystemException{
"VM `{}` is not responding correctly: saving its state, resuming it, and "
"terminating it all failed. Save error: {}; resume error: {}.\n{}",
get_name(),
save_result,
resume_result,
exception.what()};
}

// We managed to at least shutdown the VM.
throw SaveComputeSystemException{
"Could not save the virtual machine state for VM `{}` to disk for suspend or "
"resume it; the VM was terminated. Save error: {}; resume error: {}",
get_name(),
save_result,
resume_result};
}

HCSVirtualMachine::State HCSVirtualMachine::current_state()
{
set_state(fetch_state_from_api());
update_current_state();
return state;
}
void HCSVirtualMachine::update_current_state()
{
set_state(fetch_state_from_api());
}
int HCSVirtualMachine::ssh_port()
{
return default_ssh_port;
Expand Down Expand Up @@ -687,4 +750,22 @@ std::shared_ptr<Snapshot> HCSVirtualMachine::make_specific_snapshot(const QStrin
description);
}

std::error_code HCSVirtualMachine::remove_saved_state_file_if_exists()
{
if (has_saved_state_file())
{
mpl::trace(get_name(), "Saved state file exists, attempting to remove");
std::error_code ec{};
if (!MP_FILEOPS.remove(get_saved_state_file_path(), ec))
{
// FIXME: If the VM is stopped or terminated, it will still be reported as
// suspended because the saved-state file exists.
mpl::warn(get_name(), "Could not remove the saved state file, error: {}", ec);
return ec;
}
}

return {};
}

} // namespace multipass::hyperv
4 changes: 4 additions & 0 deletions src/platform/backends/hyperv_api/hcs_virtual_machine.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

#include <memory>
#include <optional>
#include <system_error>

struct HCS_EVENT;

Expand Down Expand Up @@ -105,6 +106,8 @@ struct HCSVirtualMachine : public BaseVirtualMachine

[[nodiscard]] hcs::ComputeSystemState fetch_state_from_api() const;
void set_state(hcs::ComputeSystemState state);
void set_state(State state);
void update_current_state();

/**
* Create the compute system if it's not already present.
Expand All @@ -123,6 +126,7 @@ struct HCSVirtualMachine : public BaseVirtualMachine
[[nodiscard]] std::filesystem::path get_runtime_state_file_path() const;
[[nodiscard]] std::filesystem::path get_saved_state_file_path() const;
[[nodiscard]] bool has_saved_state_file() const;
std::error_code remove_saved_state_file_if_exists();

void grant_access_to_scsi_device(const hcs::HcsScsiDevice& device) const;
void grant_access_to_paths(std::list<std::filesystem::path> paths) const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,9 @@ struct SaveComputeSystemException : public FormattedExceptionBase<>
using FormattedExceptionBase::FormattedExceptionBase;
};

struct ShutdownComputeSystemException : public FormattedExceptionBase<>
{
using FormattedExceptionBase::FormattedExceptionBase;
};

} // namespace multipass::hyperv
Loading
Loading