Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions redfish/src/account/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,21 @@ impl<B: Bmc> AccountCollection<B> {
// Slot is already explicitly enabled. Find another slot.
continue;
}

// Expanded collection members are a snapshot. Re-fetch the
// candidate immediately before updating it so concurrent
// account creation cannot reuse stale slot state.
let fresh_nav = NavProperty::new_reference(account.raw().odata_id().clone());
let Ok(account) = Account::new(&self.bmc, &fresh_nav, &self.config.account).await
else {
continue;
};
if account.is_enabled() || account.raw().etag().is_none() {
// Without a fresh ETag, the HTTP BMC would fall back to an
// unconditional `If-Match: *` update.
continue;
}
Comment thread
nvlitagaki marked this conversation as resolved.
Outdated

// Build an update based on the create request:
let update = ManagerAccountUpdate {
base: None,
Expand Down
80 changes: 79 additions & 1 deletion tests/tests/tests-account-service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,19 @@ fn slot_member(accounts_id: &str, id: u32, enabled: bool, user_name: &str) -> Js
})
}

fn slot_member_with_etag(
Comment thread
nvlitagaki marked this conversation as resolved.
Outdated
accounts_id: &str,
id: u32,
enabled: bool,
user_name: &str,
etag: &str,
) -> JsonValue {
json_merge([
&slot_member(accounts_id, id, enabled, user_name),
&json!({ "@odata.etag": etag }),
])
}

async fn account_fixture(
vendor: &str,
slots: &[(u32, bool, &str)],
Expand Down Expand Up @@ -419,9 +432,16 @@ async fn create_account_dell_slot_defined_first_available() -> TestResult<()> {

let update_req = slot_update();
let update_json = serde_json::to_value(&update_req).unwrap();
let account_id = format!("{accounts_id}/3");
let etag = "slot-3-v1";

bmc.expect(Expect::get(
&account_id,
slot_member_with_etag(&accounts_id, 3, false, "", etag),
));

bmc.expect(Expect::update(
format!("{accounts_id}/3"),
&account_id,
update_json,
json_merge([
&slot_member(&accounts_id, 3, true, "user"),
Expand All @@ -439,6 +459,59 @@ async fn create_account_dell_slot_defined_first_available() -> TestResult<()> {
Ok(())
}

#[test]
async fn create_account_slot_defined_rechecks_stale_candidate() -> TestResult<()> {
let (bmc, accounts_id, accounts) =
account_fixture("Dell", &[(3, false, ""), (4, false, "")]).await?;
let update_req = slot_update();
let update_json = serde_json::to_value(&update_req).unwrap();
let stale_account_id = format!("{accounts_id}/3");
let available_account_id = format!("{accounts_id}/4");
let etag = "slot-4-v1";

bmc.expect(Expect::get(
&stale_account_id,
slot_member_with_etag(&accounts_id, 3, true, "another-user", "slot-3-v2"),
));
bmc.expect(Expect::get(
&available_account_id,
slot_member_with_etag(&accounts_id, 4, false, "", etag),
));
bmc.expect(Expect::update(
&available_account_id,
update_json,
json_merge([
&slot_member(&accounts_id, 4, true, "user"),
&json! {{ "RoleId": "Operator" }},
]),
));

let account = into_entity(accounts.create_account(create_request("user")).await?).raw();

assert_eq!(account.base.id, "4");
assert_eq!(account.user_name.as_deref(), Some("user"));

Ok(())
}

#[test]
async fn create_account_slot_defined_requires_etag() -> TestResult<()> {
let (bmc, accounts_id, accounts) = account_fixture("Dell", &[(3, false, "")]).await?;
let account_id = format!("{accounts_id}/3");

bmc.expect(Expect::get(
&account_id,
slot_member(&accounts_id, 3, false, ""),
));

assert!(matches!(
accounts.create_account(create_request("user")).await,
Err(nv_redfish::Error::AccountSlotNotAvailable)
));

Ok(())
}

#[test]
async fn create_account_slot_defined_preserves_async_task() -> TestResult<()> {
let (bmc, accounts_id, accounts) =
Expand All @@ -449,6 +522,11 @@ async fn create_account_slot_defined_preserves_async_task() -> TestResult<()> {
let account_id = format!("{accounts_id}/3");
let task_id = "/redfish/v1/TaskService/Tasks/43";

bmc.expect(Expect::get(
&account_id,
slot_member_with_etag(&accounts_id, 3, false, "", "slot-3-v1"),
));

bmc.expect(Expect::update_task(
&account_id,
update_json,
Expand Down
Loading