Skip to content

Commit 5a0d1e0

Browse files
authored
[nexus] Re-implement disk attach/detach (#1106)
1 parent 236f2ac commit 5a0d1e0

File tree

10 files changed

+4344
-401
lines changed

10 files changed

+4344
-401
lines changed

nexus/src/app/disk.rs

+5
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,11 @@ impl super::Nexus {
163163

164164
/// Modifies the runtime state of the Disk as requested. This generally
165165
/// means attaching or detaching the disk.
166+
// TODO(https://github.com/oxidecomputer/omicron/issues/811):
167+
// This will be unused until we implement hot-plug support.
168+
// However, it has been left for reference until then, as it will
169+
// likely be needed once that feature is implemented.
170+
#[allow(dead_code)]
166171
pub(crate) async fn disk_set_runtime(
167172
&self,
168173
opctx: &OpContext,

nexus/src/app/instance.rs

+47-246
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use omicron_common::api::external;
1919
use omicron_common::api::external::CreateResult;
2020
use omicron_common::api::external::DataPageParams;
2121
use omicron_common::api::external::DeleteResult;
22-
use omicron_common::api::external::DiskState;
2322
use omicron_common::api::external::Error;
2423
use omicron_common::api::external::InstanceState;
2524
use omicron_common::api::external::ListResultVec;
@@ -152,17 +151,9 @@ impl super::Nexus {
152151
Ok(db_instance)
153152
}
154153

155-
// TODO-correctness It's not totally clear what the semantics and behavior
156-
// should be here. It might be nice to say that you can only do this
157-
// operation if the Instance is already stopped, in which case we can
158-
// execute this immediately by just removing it from the database, with the
159-
// same race we have with disk delete (i.e., if someone else is requesting
160-
// an instance boot, we may wind up in an inconsistent state). On the other
161-
// hand, we could always allow this operation, issue the request to the SA
162-
// to destroy the instance (not just stop it), and proceed with deletion
163-
// when that finishes. But in that case, although the HTTP DELETE request
164-
// completed, the object will still appear for a little while, which kind of
165-
// sucks.
154+
// This operation may only occur on stopped instances, which implies that
155+
// the attached disks do not have any running "upstairs" process running
156+
// within the sled.
166157
pub async fn project_destroy_instance(
167158
&self,
168159
opctx: &OpContext,
@@ -173,58 +164,14 @@ impl super::Nexus {
173164
// TODO-robustness We need to figure out what to do with Destroyed
174165
// instances? Presumably we need to clean them up at some point, but
175166
// not right away so that callers can see that they've been destroyed.
176-
let (.., authz_instance, db_instance) =
167+
let (.., authz_instance, _) =
177168
LookupPath::new(opctx, &self.db_datastore)
178169
.organization_name(organization_name)
179170
.project_name(project_name)
180171
.instance_name(instance_name)
181172
.fetch()
182173
.await?;
183174

184-
opctx.authorize(authz::Action::Delete, &authz_instance).await?;
185-
186-
match db_instance.runtime_state.state.state() {
187-
InstanceState::Stopped | InstanceState::Failed => {
188-
// ok
189-
}
190-
191-
state => {
192-
return Err(Error::InvalidRequest {
193-
message: format!(
194-
"instance cannot be deleted in state \"{}\"",
195-
state,
196-
),
197-
});
198-
}
199-
}
200-
201-
// Detach all attached disks
202-
let disks = self
203-
.instance_list_disks(
204-
opctx,
205-
organization_name,
206-
project_name,
207-
instance_name,
208-
&DataPageParams {
209-
marker: None,
210-
direction: dropshot::PaginationOrder::Ascending,
211-
limit: std::num::NonZeroU32::new(MAX_DISKS_PER_INSTANCE)
212-
.unwrap(),
213-
},
214-
)
215-
.await?;
216-
217-
for disk in &disks {
218-
self.instance_detach_disk(
219-
opctx,
220-
organization_name,
221-
project_name,
222-
instance_name,
223-
&disk.name(),
224-
)
225-
.await?;
226-
}
227-
228175
self.db_datastore.project_delete_instance(opctx, &authz_instance).await
229176
}
230177

@@ -586,144 +533,43 @@ impl super::Nexus {
586533
instance_name: &Name,
587534
disk_name: &Name,
588535
) -> UpdateResult<db::model::Disk> {
589-
let (.., authz_project, authz_disk, db_disk) =
536+
let (.., authz_project, authz_disk, _) =
590537
LookupPath::new(opctx, &self.db_datastore)
591538
.organization_name(organization_name)
592539
.project_name(project_name)
593540
.disk_name(disk_name)
594541
.fetch()
595542
.await?;
596-
let (.., authz_instance, db_instance) =
543+
let (.., authz_instance, _) =
597544
LookupPath::new(opctx, &self.db_datastore)
598545
.project_id(authz_project.id())
599546
.instance_name(instance_name)
600547
.fetch()
601548
.await?;
602-
let instance_id = &authz_instance.id();
603549

604-
// Enforce attached disks limit
605-
let attached_disks = self
606-
.instance_list_disks(
607-
opctx,
608-
organization_name,
609-
project_name,
610-
instance_name,
611-
&DataPageParams {
612-
marker: None,
613-
direction: dropshot::PaginationOrder::Ascending,
614-
limit: std::num::NonZeroU32::new(MAX_DISKS_PER_INSTANCE)
615-
.unwrap(),
616-
},
550+
// TODO(https://github.com/oxidecomputer/omicron/issues/811):
551+
// Disk attach is only implemented for instances that are not
552+
// currently running. This operation therefore can operate exclusively
553+
// on database state.
554+
//
555+
// To implement hot-plug support, we should do the following in a saga:
556+
// - Update the state to "Attaching", rather than "Attached".
557+
// - If the instance is running...
558+
// - Issue a request to "disk attach" to the associated sled agent,
559+
// using the "state generation" value from the moment we attached.
560+
// - Update the DB if the request succeeded (hopefully to "Attached").
561+
// - If the instance is not running...
562+
// - Update the disk state in the DB to "Attached".
563+
let (_instance, disk) = self
564+
.db_datastore
565+
.instance_attach_disk(
566+
&opctx,
567+
&authz_instance,
568+
&authz_disk,
569+
MAX_DISKS_PER_INSTANCE,
617570
)
618571
.await?;
619-
620-
if attached_disks.len() == MAX_DISKS_PER_INSTANCE as usize {
621-
return Err(Error::invalid_request(&format!(
622-
"cannot attach more than {} disks to instance!",
623-
MAX_DISKS_PER_INSTANCE
624-
)));
625-
}
626-
627-
fn disk_attachment_error(
628-
disk: &db::model::Disk,
629-
) -> CreateResult<db::model::Disk> {
630-
let disk_status = match disk.runtime().state().into() {
631-
DiskState::Destroyed => "disk is destroyed",
632-
DiskState::Faulted => "disk is faulted",
633-
DiskState::Creating => "disk is detached",
634-
DiskState::Detached => "disk is detached",
635-
636-
// It would be nice to provide a more specific message here, but
637-
// the appropriate identifier to provide the user would be the
638-
// other instance's name. Getting that would require another
639-
// database hit, which doesn't seem worth it for this.
640-
DiskState::Attaching(_) => {
641-
"disk is attached to another instance"
642-
}
643-
DiskState::Attached(_) => {
644-
"disk is attached to another instance"
645-
}
646-
DiskState::Detaching(_) => {
647-
"disk is attached to another instance"
648-
}
649-
};
650-
let message = format!(
651-
"cannot attach disk \"{}\": {}",
652-
disk.name().as_str(),
653-
disk_status
654-
);
655-
Err(Error::InvalidRequest { message })
656-
}
657-
658-
match &db_disk.state().into() {
659-
// If we're already attaching or attached to the requested instance,
660-
// there's nothing else to do.
661-
// TODO-security should it be an error if you're not authorized to
662-
// do this and we did not actually have to do anything?
663-
DiskState::Attached(id) if id == instance_id => return Ok(db_disk),
664-
665-
// If the disk is currently attaching or attached to another
666-
// instance, fail this request. Users must explicitly detach first
667-
// if that's what they want. If it's detaching, they have to wait
668-
// for it to become detached.
669-
// TODO-debug: the error message here could be better. We'd have to
670-
// look up the other instance by id (and gracefully handle it not
671-
// existing).
672-
DiskState::Attached(id) => {
673-
assert_ne!(id, instance_id);
674-
return disk_attachment_error(&db_disk);
675-
}
676-
DiskState::Detaching(_) => {
677-
return disk_attachment_error(&db_disk);
678-
}
679-
DiskState::Attaching(id) if id != instance_id => {
680-
return disk_attachment_error(&db_disk);
681-
}
682-
DiskState::Destroyed => {
683-
return disk_attachment_error(&db_disk);
684-
}
685-
DiskState::Faulted => {
686-
return disk_attachment_error(&db_disk);
687-
}
688-
689-
DiskState::Creating => (),
690-
DiskState::Detached => (),
691-
DiskState::Attaching(id) => {
692-
assert_eq!(id, instance_id);
693-
}
694-
}
695-
696-
match &db_instance.runtime_state.state.state() {
697-
// If there's a propolis zone for this instance, ask the Sled Agent
698-
// to hot-plug the disk.
699-
//
700-
// TODO this will probably involve volume construction requests as
701-
// well!
702-
InstanceState::Running | InstanceState::Starting => {
703-
self.disk_set_runtime(
704-
opctx,
705-
&authz_disk,
706-
&db_disk,
707-
self.instance_sled(&db_instance).await?,
708-
sled_agent_client::types::DiskStateRequested::Attached(
709-
*instance_id,
710-
),
711-
)
712-
.await?;
713-
}
714-
715-
_ => {
716-
// If there is not a propolis zone, then disk attach only occurs
717-
// in the DB.
718-
let new_runtime = db_disk.runtime().attach(*instance_id);
719-
720-
self.db_datastore
721-
.disk_update_runtime(opctx, &authz_disk, &new_runtime)
722-
.await?;
723-
}
724-
}
725-
726-
self.db_datastore.disk_refetch(opctx, &authz_disk).await
572+
Ok(disk)
727573
}
728574

729575
/// Detach a disk from an instance.
@@ -735,83 +581,38 @@ impl super::Nexus {
735581
instance_name: &Name,
736582
disk_name: &Name,
737583
) -> UpdateResult<db::model::Disk> {
738-
let (.., authz_project, authz_disk, db_disk) =
584+
let (.., authz_project, authz_disk, _) =
739585
LookupPath::new(opctx, &self.db_datastore)
740586
.organization_name(organization_name)
741587
.project_name(project_name)
742588
.disk_name(disk_name)
743589
.fetch()
744590
.await?;
745-
let (.., authz_instance, db_instance) =
591+
let (.., authz_instance, _) =
746592
LookupPath::new(opctx, &self.db_datastore)
747593
.project_id(authz_project.id())
748594
.instance_name(instance_name)
749595
.fetch()
750596
.await?;
751-
let instance_id = &authz_instance.id();
752-
753-
match &db_disk.state().into() {
754-
// This operation is a noop if the disk is not attached or already
755-
// detaching from the same instance.
756-
// TODO-security should it be an error if you're not authorized to
757-
// do this and we did not actually have to do anything?
758-
DiskState::Creating => return Ok(db_disk),
759-
DiskState::Detached => return Ok(db_disk),
760-
DiskState::Destroyed => return Ok(db_disk),
761-
DiskState::Faulted => return Ok(db_disk),
762-
DiskState::Detaching(id) if id == instance_id => {
763-
return Ok(db_disk)
764-
}
765-
766-
// This operation is not allowed if the disk is attached to some
767-
// other instance.
768-
DiskState::Attaching(id) if id != instance_id => {
769-
return Err(Error::InvalidRequest {
770-
message: String::from("disk is attached elsewhere"),
771-
});
772-
}
773-
DiskState::Attached(id) if id != instance_id => {
774-
return Err(Error::InvalidRequest {
775-
message: String::from("disk is attached elsewhere"),
776-
});
777-
}
778-
DiskState::Detaching(_) => {
779-
return Err(Error::InvalidRequest {
780-
message: String::from("disk is attached elsewhere"),
781-
});
782-
}
783-
784-
// These are the cases where we have to do something.
785-
DiskState::Attaching(_) => (),
786-
DiskState::Attached(_) => (),
787-
}
788-
789-
// If there's a propolis zone for this instance, ask the Sled
790-
// Agent to hot-remove the disk.
791-
match &db_instance.runtime_state.state.state() {
792-
InstanceState::Running | InstanceState::Starting => {
793-
self.disk_set_runtime(
794-
opctx,
795-
&authz_disk,
796-
&db_disk,
797-
self.instance_sled(&db_instance).await?,
798-
sled_agent_client::types::DiskStateRequested::Detached,
799-
)
800-
.await?;
801-
}
802-
803-
_ => {
804-
// If there is not a propolis zone, then disk detach only occurs
805-
// in the DB.
806-
let new_runtime = db_disk.runtime().detach();
807-
808-
self.db_datastore
809-
.disk_update_runtime(opctx, &authz_disk, &new_runtime)
810-
.await?;
811-
}
812-
}
813597

814-
self.db_datastore.disk_refetch(opctx, &authz_disk).await
598+
// TODO(https://github.com/oxidecomputer/omicron/issues/811):
599+
// Disk detach is only implemented for instances that are not
600+
// currently running. This operation therefore can operate exclusively
601+
// on database state.
602+
//
603+
// To implement hot-unplug support, we should do the following in a saga:
604+
// - Update the state to "Detaching", rather than "Detached".
605+
// - If the instance is running...
606+
// - Issue a request to "disk detach" to the associated sled agent,
607+
// using the "state generation" value from the moment we attached.
608+
// - Update the DB if the request succeeded (hopefully to "Detached").
609+
// - If the instance is not running...
610+
// - Update the disk state in the DB to "Detached".
611+
let disk = self
612+
.db_datastore
613+
.instance_detach_disk(&opctx, &authz_instance, &authz_disk)
614+
.await?;
615+
Ok(disk)
815616
}
816617

817618
/// Create a network interface attached to the provided instance.

0 commit comments

Comments
 (0)