Repository navigation
wire shutdown policy into vmm_stop_for_update bg task #11703
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ use diesel::prelude::*; | |
| use diesel::sql_types::{Bool, Nullable}; | ||
| use nexus_db_schema::schema::{disk, external_ip, external_subnet, instance}; | ||
| use nexus_types::external_api::instance as instance_types; | ||
| use omicron_common::api::external::Error; | ||
| use omicron_generation_kinds::{ | ||
| InstanceStateGeneration, InstanceStateGenerationKind, | ||
| InstanceUpdaterGeneration, InstanceUpdaterGenerationKind, | ||
|
|
@@ -152,14 +153,17 @@ pub struct Instance { | |
| /// Changes to this field only take effect on the next instance restart. | ||
| pub enable_jumbo_frames: bool, | ||
|
|
||
| // The `shutdown_policy_*` fields are private to avoid leaking the db | ||
| // representation of a stronger type; callers should use `shutdown_policy()` | ||
| // to get the reconstructed value from these db columns. | ||
| /// When `shutdown_policy_action` is `PowerButton`, this represents the time | ||
| /// propolis will wait for the guest to power itself down after sending it | ||
| /// the power button event, before forcefully terminating it. | ||
| /// Must be None if the action is `HardOff`. | ||
| pub shutdown_policy_timeout: Option<TimeDelta>, | ||
| shutdown_policy_timeout: Option<TimeDelta>, | ||
|
|
||
| /// The means by which to stop the instance. | ||
| pub shutdown_policy_action: InstanceShutdownAction, | ||
| shutdown_policy_action: InstanceShutdownAction, | ||
| } | ||
|
|
||
| impl Instance { | ||
|
|
@@ -322,24 +326,23 @@ impl Instance { | |
| } | ||
| } | ||
|
|
||
| /// Reconstruct the [`instance_types::InstanceShutdownPolicy`] of this | ||
| /// instance from its database representation. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Fails only if reconstruction fails, which should be impossible as long | ||
| /// as this conversion stays consistent with the constraints on the table in | ||
| /// CRDB. | ||
| pub fn shutdown_policy( | ||
| &self, | ||
| ) -> Option<instance_types::InstanceShutdownPolicy> { | ||
| use instance_types::InstanceShutdownPolicy; | ||
| match self.shutdown_policy_action { | ||
| InstanceShutdownAction::HardOff => { | ||
| Some(InstanceShutdownPolicy::HardOff) | ||
| } | ||
| InstanceShutdownAction::PowerButton => { | ||
| // return None if timeout NULL or negative (enforced by sql-side check) | ||
| let timeout_secs: u64 = self | ||
| .shutdown_policy_timeout? | ||
| .num_seconds() | ||
| .try_into() | ||
| .ok()?; | ||
| Some(InstanceShutdownPolicy::PowerButton { timeout_secs }) | ||
| } | ||
| ) -> Result<instance_types::InstanceShutdownPolicy, Error> { | ||
| InstanceShutdownPolicyColumns { | ||
| id: self.identity.id, | ||
| shutdown_policy_timeout: self.shutdown_policy_timeout, | ||
| shutdown_policy_action: self.shutdown_policy_action, | ||
| } | ||
| .shutdown_policy() | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -378,6 +381,62 @@ impl DatastoreAttachTargetConfig<ExternalSubnet> for Instance { | |
| type ResourceTimeDeletedColumn = external_subnet::dsl::time_deleted; | ||
| } | ||
|
|
||
| /// Just the columns needed to decode an instance's shutdown policy. | ||
| /// | ||
| /// Used by queries that join against `instance` but only care about the | ||
| /// shutdown policy. | ||
|
Comment on lines
+384
to
+387
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cute! |
||
| #[derive(Queryable, Selectable, Clone, Debug)] | ||
| #[diesel(table_name = instance)] | ||
| pub struct InstanceShutdownPolicyColumns { | ||
| id: Uuid, | ||
| shutdown_policy_timeout: Option<TimeDelta>, | ||
| shutdown_policy_action: InstanceShutdownAction, | ||
| } | ||
|
|
||
| impl InstanceShutdownPolicyColumns { | ||
| /// Reconstruct the [`instance_types::InstanceShutdownPolicy`] of this | ||
| /// instance from its database representation. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Fails only if reconstruction fails, which should be impossible as long | ||
| /// as this conversion stays consistent with the constraints on the table in | ||
| /// CRDB. | ||
| pub fn shutdown_policy( | ||
| &self, | ||
| ) -> Result<instance_types::InstanceShutdownPolicy, Error> { | ||
| use instance_types::InstanceShutdownPolicy; | ||
| match self.shutdown_policy_action { | ||
| InstanceShutdownAction::HardOff => { | ||
| Ok(InstanceShutdownPolicy::HardOff) | ||
| } | ||
| InstanceShutdownAction::PowerButton => { | ||
| // DB constraints require `shutdown_policy_timeout` to be | ||
| // present and in a valid range if the action is `PowerButton`. | ||
| let timeout = | ||
| self.shutdown_policy_timeout.ok_or_else(|| { | ||
| Error::internal_error(format!( | ||
| "db constraint violated: shutdown_policy_timeout \ | ||
| is NULL when action is PowerButton \ | ||
| (instance {})", | ||
| self.id | ||
| )) | ||
| })?; | ||
| let timeout_secs: u64 = | ||
| timeout.num_seconds().try_into().map_err(|_| { | ||
| Error::internal_error(format!( | ||
| "db constraint violated: shutdown_policy_timeout \ | ||
| has invalid value {timeout} \ | ||
| (instance {})", | ||
| self.id | ||
| )) | ||
| })?; | ||
| Ok(InstanceShutdownPolicy::PowerButton { timeout_secs }) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Runtime state of the Instance, including the actual running state and minimal | ||
| /// metadata | ||
| /// | ||
|
|
@@ -688,3 +747,91 @@ pub struct InstanceUpdate { | |
| pub shutdown_policy_action: Option<InstanceShutdownAction>, | ||
| pub shutdown_policy_timeout: Option<TimeDelta>, | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use omicron_common::api::external; | ||
|
|
||
| #[test] | ||
| fn test_instance_shutdown_policy() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. quite right, makes more sense here 😅 |
||
| let mut params = instance_types::InstanceCreate { | ||
| identity: external::IdentityMetadataCreateParams { | ||
| name: external::Name::try_from("acpi".to_owned()).unwrap(), | ||
| description: "instance under acpi shutdown policy test" | ||
| .to_owned(), | ||
| }, | ||
| ncpus: instance_types::InstanceCpuCount(1), | ||
| memory: external::ByteCount::from_gibibytes_u32(1), | ||
| hostname: external::Hostname::try_from("jumbo").unwrap(), | ||
| user_data: vec![], | ||
| network_interfaces: | ||
| instance_types::InstanceNetworkInterfaceAttachment::None, | ||
| external_ips: vec![], | ||
| disks: vec![], | ||
| boot_disk: None, | ||
| cpu_platform: None, | ||
| ssh_public_keys: None, | ||
| start: false, | ||
| auto_restart_policy: Default::default(), | ||
| anti_affinity_groups: Vec::new(), | ||
| multicast_groups: Vec::new(), | ||
| enable_jumbo_frames: false, | ||
| shutdown_policy: None, | ||
| }; | ||
| let instance_id = InstanceUuid::from_untyped_uuid(Uuid::new_v4()); | ||
| let project_id = ProjectUuid::from_untyped_uuid(Uuid::new_v4()); | ||
|
|
||
| // when shutdown policy omitted at creation, default to 600 seconds | ||
| let db_instance = Instance::new(instance_id, project_id, ¶ms); | ||
| assert_eq!( | ||
| db_instance.shutdown_policy_action, | ||
| InstanceShutdownAction::PowerButton | ||
| ); | ||
| assert_eq!( | ||
| db_instance.shutdown_policy_timeout.unwrap().num_seconds(), | ||
| 600 | ||
| ); | ||
|
|
||
| // explicitly set hard-off | ||
| params.shutdown_policy = | ||
| Some(instance_types::InstanceShutdownPolicy::HardOff); | ||
| let db_instance = Instance::new(instance_id, project_id, ¶ms); | ||
| assert_eq!( | ||
| db_instance.shutdown_policy_action, | ||
| InstanceShutdownAction::HardOff | ||
| ); | ||
| assert_eq!(db_instance.shutdown_policy_timeout, None); | ||
|
|
||
| // explicitly set power button with timeout | ||
| params.shutdown_policy = | ||
| Some(instance_types::InstanceShutdownPolicy::PowerButton { | ||
| timeout_secs: 42, | ||
| }); | ||
| let db_instance = Instance::new(instance_id, project_id, ¶ms); | ||
| assert_eq!( | ||
| db_instance.shutdown_policy_action, | ||
| InstanceShutdownAction::PowerButton | ||
| ); | ||
| assert_eq!( | ||
| db_instance.shutdown_policy_timeout.unwrap().num_seconds(), | ||
| 42 | ||
| ); | ||
|
|
||
| // Instance::new doesn't clamp an invalid timeout | ||
| // (enforced instead by Nexus and crdb constraint) | ||
| params.shutdown_policy = | ||
| Some(instance_types::InstanceShutdownPolicy::PowerButton { | ||
| timeout_secs: 999000, | ||
| }); | ||
| let db_instance = Instance::new(instance_id, project_id, ¶ms); | ||
| assert_eq!( | ||
| db_instance.shutdown_policy_action, | ||
| InstanceShutdownAction::PowerButton | ||
| ); | ||
| assert_eq!( | ||
| db_instance.shutdown_policy_timeout.unwrap().num_seconds(), | ||
| 999000 | ||
| ); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could this be achieved by
#[diesel(embed)]dingInstanceShutdownPolicyColumnsdirectly in theInstancestruct, so that we don't have to do this part? I guess we can't currently, because as written,InstanceShutdownPolicyColumnsalso has the UUID, but maybe the queries could.select((Uuid::as_select(), InstanceShutdownPolicyColumns::as_select())or whatever?Not a big deal to me if we don't do it like that, though, just a thought. I know we've had some problems with
#[diesel(embed)]and column ordering in the past, but I don't think that would happen here, because they're being added at the end of the struct in the same order as they appear on the schema?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, after reading the code, I don't think we even need to select the instance ID from the instance record at all, since we're already selecting it from the
vmmrecord? I think diesel shouldn't require us to select the key we join on from both tables, right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to select it for the use there, but I am using it in the
shutdown_policy()method (only to include in the error message in the event of a bogus row - maybe it's fine to omit it there too?).I'm pretty skittish about
diesel(embed), I think partly from the problems you referenced and partly because I've been burned so badly byserde(flatten). I think I'd prefer to keep it separate, unless you feel strongly about it? And if we're keeping it separate, it seems okay to select the ID too for that error message usage?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm fine with that! Just felt it was worth mentioning.