Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5e19650
client: use re-exported crucible type in API definition
luqmana Aug 25, 2022
824ae13
Add 'Provisioned' state to indicate an instance and its resources hav…
luqmana Aug 23, 2022
ce9404f
Add support for provisioning an Instance but not starting it.
luqmana Aug 23, 2022
41d0831
nexus: allow provisioning instance without running it
luqmana Aug 25, 2022
a6d0778
Indicate transitory state of starting a stopped instance.
luqmana Aug 25, 2022
27a197b
nexus: add flag to decide whether to run instance after creating it
luqmana Aug 26, 2022
8e79ebe
Add intermediate 'Provisioning' state.
luqmana Aug 29, 2022
77360bf
Clean up Instance resources if Stop request is sent before it has ful…
luqmana Aug 29, 2022
6539b02
Better represent API types that only need a default and not nullability.
luqmana Aug 30, 2022
390588e
Provisioning/ed states should go through Stopping first.
luqmana Aug 30, 2022
8763a35
Update existing tests.
luqmana Aug 30, 2022
bedca5c
Don't setup propolis zone and other resources only to immediately tea…
luqmana Aug 30, 2022
e0590a4
Add test for creating Instance in stopped state.
luqmana Aug 30, 2022
097ab3c
Add test for provisioning Instance.
luqmana Aug 30, 2022
9ec9b87
Add test for getting serial data of Provisioned Instance.
luqmana Aug 30, 2022
435be25
Clippy clean
luqmana Aug 30, 2022
3ba425b
Use separate endpoints for instance provision and start.
luqmana Aug 31, 2022
5b258ca
Flip start flag in InstanceCreate to read more naturally
luqmana Sep 1, 2022
4329f79
Updating existing tests for parameter and endpoint changes.
luqmana Sep 1, 2022
dd2dd0c
Update endpoint and tags tests.
luqmana Sep 1, 2022
d5d35c3
fmt
luqmana Sep 1, 2022
b2ffc06
Remove provision action and associated states
luqmana Sep 2, 2022
f18f83c
omit comment with link to old issue
luqmana Sep 3, 2022
4301f9a
Fix typo.
luqmana Sep 8, 2022
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
11 changes: 9 additions & 2 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,11 +628,15 @@ pub struct IdentityMetadataUpdateParams {
JsonSchema,
)]
#[serde(rename_all = "snake_case")]
// TODO-polish: RFD 315
pub enum InstanceState {
Creating, // TODO-polish: paper over Creating in the API with Starting?
/// The instance is being created.
Creating,
/// The instance is currently starting up.
Starting,
/// The instance is currently running.
Running,
/// Implied that a transition to "Stopped" is imminent.
/// The instance has been requested to stop and a transition to "Stopped" is imminent.
Stopping,
/// The instance is currently stopped.
Stopped,
Expand All @@ -643,8 +647,11 @@ pub enum InstanceState {
/// in the "migrating" state until the migration process is complete
/// and the destination propolis is ready to continue execution.
Migrating,
/// The instance is attempting to recover from a failure.
Repairing,
/// The instance has encountered a failure.
Failed,
/// The instance has been deleted.
Destroyed,
}

Expand Down
51 changes: 41 additions & 10 deletions nexus/src/app/sagas/instance_create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -940,16 +940,47 @@ async fn sic_instance_ensure(
.await
.map_err(ActionError::action_failed)?;

osagactx
.nexus()
.instance_set_runtime(
&opctx,
&authz_instance,
&db_instance,
runtime_params,
)
.await
.map_err(ActionError::action_failed)?;
if !params.create_params.start {
let instance_id = db_instance.id();
// If we don't need to start the instance, we can skip the ensure
// and just update the instance runtime state to `Stopped`
let runtime_state = db::model::InstanceRuntimeState {
state: db::model::InstanceState::new(InstanceState::Stopped),
// Must update the generation, or the database query will fail.
//
// The runtime state of the instance record is only changed as a result
// of the successful completion of the saga (i.e. after ensure which we're
// skipping in this case) or during saga unwinding. So we're guaranteed
// that the cached generation in the saga log is the most recent in the database.
gen: db::model::Generation::from(
db_instance.runtime_state.gen.next(),
),
..db_instance.runtime_state
};

let updated = datastore
.instance_update_runtime(&instance_id, &runtime_state)
.await
.map_err(ActionError::action_failed)?;

if !updated {
warn!(
osagactx.log(),
"failed to update instance runtime state from creating to stopped",
);
}
} else {
osagactx
.nexus()
.instance_set_runtime(
&opctx,
&authz_instance,
&db_instance,
runtime_params,
)
.await
.map_err(ActionError::action_failed)?;
}

Ok(())
}
1 change: 1 addition & 0 deletions nexus/src/db/queries/network_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1532,6 +1532,7 @@ mod tests {
network_interfaces: InstanceNetworkInterfaceAttachment::None,
external_ips: vec![],
disks: vec![],
start: true,
};
let runtime = InstanceRuntimeState {
run_state: InstanceState::Creating,
Expand Down
3 changes: 2 additions & 1 deletion nexus/test-utils/src/resource_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ pub async fn create_instance(
.await
}

/// Creates an instance with attached resou8rces.
/// Creates an instance with attached resources.
pub async fn create_instance_with(
client: &ClientTestContext,
organization_name: &str,
Expand Down Expand Up @@ -240,6 +240,7 @@ pub async fn create_instance_with(
network_interfaces: nics.clone(),
external_ips: vec![],
disks,
start: true,
},
)
.await
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/integration_tests/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ lazy_static! {
params::ExternalIpCreate::Ephemeral { pool_name: None }
],
disks: vec![],
start: true,
};

// The instance needs a network interface, too.
Expand Down
73 changes: 73 additions & 0 deletions nexus/tests/integration_tests/instances.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use nexus_test_utils::http_testing::NexusRequest;
use nexus_test_utils::http_testing::RequestBuilder;
use nexus_test_utils::resource_helpers::create_disk;
use nexus_test_utils::resource_helpers::create_ip_pool;
use nexus_test_utils::resource_helpers::object_create;
use nexus_test_utils::resource_helpers::objects_list_page_authz;
use nexus_test_utils::resource_helpers::DiskTest;
use omicron_common::api::external::ByteCount;
Expand Down Expand Up @@ -174,6 +175,7 @@ async fn test_instances_create_reboot_halt(
params::InstanceNetworkInterfaceAttachment::Default,
external_ips: vec![],
disks: vec![],
start: true,
}))
.expect_status(Some(StatusCode::BAD_REQUEST)),
)
Expand Down Expand Up @@ -440,6 +442,62 @@ async fn test_instances_create_reboot_halt(
.unwrap();
}

#[nexus_test]
async fn test_instances_create_stopped_start(
cptestctx: &ControlPlaneTestContext,
) {
let client = &cptestctx.external_client;
let apictx = &cptestctx.server.apictx;
let nexus = &apictx.nexus;

// Create an IP pool and project that we'll use for testing.
create_ip_pool(&client, POOL_NAME, None, None).await;
create_organization(&client, ORGANIZATION_NAME).await;
let url_instances = format!(
"/organizations/{}/projects/{}/instances",
ORGANIZATION_NAME, PROJECT_NAME
);
let _ = create_project(&client, ORGANIZATION_NAME, PROJECT_NAME).await;

// Create an instance in a stopped state.
let instance: Instance = object_create(
client,
&url_instances,
&params::InstanceCreate {
identity: IdentityMetadataCreateParams {
name: "just-rainsticks".parse().unwrap(),
description: "instance just-rainsticks".to_string(),
},
ncpus: InstanceCpuCount(4),
memory: ByteCount::from_gibibytes_u32(1),
hostname: String::from("the_host"),
user_data: vec![],
network_interfaces:
params::InstanceNetworkInterfaceAttachment::Default,
external_ips: vec![],
disks: vec![],
start: false,
},
)
.await;
assert_eq!(instance.runtime.run_state, InstanceState::Stopped);

// Start the instance.
let instance_url = format!("{}/just-rainsticks", url_instances);
let instance =
instance_post(&client, &instance_url, InstanceOp::Start).await;

// Now, simulate completion of instance boot and check the state reported.
instance_simulate(nexus, &instance.identity.id).await;
let instance_next = instance_get(&client, &instance_url).await;
identity_eq(&instance.identity, &instance_next.identity);
assert_eq!(instance_next.runtime.run_state, InstanceState::Running);
assert!(
instance_next.runtime.time_run_state_updated
> instance.runtime.time_run_state_updated
);
}

#[nexus_test]
async fn test_instances_delete_fails_when_running_succeeds_when_stopped(
cptestctx: &ControlPlaneTestContext,
Expand Down Expand Up @@ -612,6 +670,7 @@ async fn test_instance_create_saga_removes_instance_database_record(
network_interfaces: interface_params.clone(),
external_ips: vec![],
disks: vec![],
start: true,
};
let response =
NexusRequest::objects_post(client, &url_instances, &instance_params)
Expand All @@ -635,6 +694,7 @@ async fn test_instance_create_saga_removes_instance_database_record(
network_interfaces: interface_params,
external_ips: vec![],
disks: vec![],
start: true,
};
let _ =
NexusRequest::objects_post(client, &url_instances, &instance_params)
Expand Down Expand Up @@ -723,6 +783,7 @@ async fn test_instance_with_single_explicit_ip_address(
network_interfaces: interface_params,
external_ips: vec![],
disks: vec![],
start: true,
};
let response =
NexusRequest::objects_post(client, &url_instances, &instance_params)
Expand Down Expand Up @@ -842,6 +903,7 @@ async fn test_instance_with_new_custom_network_interfaces(
network_interfaces: interface_params,
external_ips: vec![],
disks: vec![],
start: true,
};
let response =
NexusRequest::objects_post(client, &url_instances, &instance_params)
Expand Down Expand Up @@ -958,6 +1020,7 @@ async fn test_instance_create_delete_network_interface(
network_interfaces: params::InstanceNetworkInterfaceAttachment::None,
external_ips: vec![],
disks: vec![],
start: true,
};
let response =
NexusRequest::objects_post(client, &url_instances, &instance_params)
Expand Down Expand Up @@ -1208,6 +1271,7 @@ async fn test_instance_update_network_interfaces(
network_interfaces: params::InstanceNetworkInterfaceAttachment::None,
external_ips: vec![],
disks: vec![],
start: true,
};
let response =
NexusRequest::objects_post(client, &url_instances, &instance_params)
Expand Down Expand Up @@ -1613,6 +1677,7 @@ async fn test_instance_with_multiple_nics_unwinds_completely(
network_interfaces: interface_params,
external_ips: vec![],
disks: vec![],
start: true,
};
let builder =
RequestBuilder::new(client, http::Method::POST, &url_instances)
Expand Down Expand Up @@ -1694,6 +1759,7 @@ async fn test_attach_one_disk_to_instance(cptestctx: &ControlPlaneTestContext) {
name: Name::try_from(String::from("probablydata")).unwrap(),
},
)],
start: true,
};

let url_instances = format!(
Expand Down Expand Up @@ -1800,6 +1866,7 @@ async fn test_attach_eight_disks_to_instance(
)
})
.collect(),
start: true,
};

let url_instances = format!(
Expand Down Expand Up @@ -1907,6 +1974,7 @@ async fn test_cannot_attach_nine_disks_to_instance(
)
})
.collect(),
start: true,
};

let url_instances = format!(
Expand Down Expand Up @@ -2038,6 +2106,7 @@ async fn test_cannot_attach_faulted_disks(cptestctx: &ControlPlaneTestContext) {
)
})
.collect(),
start: true,
};

let url_instances = format!(
Expand Down Expand Up @@ -2153,6 +2222,7 @@ async fn test_disks_detached_when_instance_destroyed(
)
})
.collect(),
start: true,
};

let url_instances = format!(
Expand Down Expand Up @@ -2258,6 +2328,7 @@ async fn test_instances_memory_rejected_less_than_min_memory_size(
network_interfaces: params::InstanceNetworkInterfaceAttachment::Default,
external_ips: vec![],
disks: vec![],
start: true,
};

let error = NexusRequest::new(
Expand Down Expand Up @@ -2307,6 +2378,7 @@ async fn test_instances_memory_not_divisible_by_min_memory_size(
network_interfaces: params::InstanceNetworkInterfaceAttachment::Default,
external_ips: vec![],
disks: vec![],
start: true,
};

let error = NexusRequest::new(
Expand Down Expand Up @@ -2481,6 +2553,7 @@ async fn test_instance_ephemeral_ip_from_correct_project(
pool_name: None,
}],
disks: vec![],
start: true,
};
let response =
NexusRequest::objects_post(client, &url_instances, &instance_params)
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/integration_tests/snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ async fn test_snapshot(cptestctx: &ControlPlaneTestContext) {
params::InstanceDiskAttach { name: base_disk_name.clone() },
)],
external_ips: vec![],
start: true,
},
)
.await;
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/integration_tests/subnet_allocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ async fn create_instance_expect_failure(
network_interfaces,
external_ips: vec![],
disks: vec![],
start: true,
};

NexusRequest::new(
Expand Down
9 changes: 9 additions & 0 deletions nexus/types/src/external_api/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,15 @@ pub struct InstanceCreate {
/// The disks to be created or attached for this instance.
#[serde(default)]
pub disks: Vec<InstanceDiskAttachment>,

/// Should this instance be started upon creation; true by default.
#[serde(default = "bool_true")]
pub start: bool,
}

#[inline]
fn bool_true() -> bool {
true
}

// If you change this, also update the error message in
Expand Down
5 changes: 5 additions & 0 deletions openapi/nexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -8782,6 +8782,11 @@
}
]
},
"start": {
"description": "Should this instance be started upon creation; true by default.",
"default": true,
"type": "boolean"
},
"user_data": {
"description": "User data for instance initialization systems (such as cloud-init). Must be a Base64-encoded string, as specified in RFC 4648 § 4 (+ and / characters with padding). Maximum 32 KiB unencoded data.",
"default": "",
Expand Down
Loading