diff --git a/dev-tools/omdb/tests/successes.out b/dev-tools/omdb/tests/successes.out index 2738d6ff1fb..9723d0a0fe3 100644 --- a/dev-tools/omdb/tests/successes.out +++ b/dev-tools/omdb/tests/successes.out @@ -1565,6 +1565,12 @@ parent: B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ----------------------------------------------------------------- vendor model serial disposition @@ -1607,6 +1613,12 @@ parent: B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ---------------------------------------------------------------- vendor model serial disposition @@ -1695,6 +1707,12 @@ parent: B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ----------------------------------------------------------------- vendor model serial disposition @@ -1737,6 +1755,12 @@ parent: B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ---------------------------------------------------------------- vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-add-sled-no-disks-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-add-sled-no-disks-stdout index 0e7584d3ea5..b218190c32b 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-add-sled-no-disks-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-add-sled-no-disks-stdout @@ -70,6 +70,12 @@ parent: dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------- vendor model serial disposition @@ -102,6 +108,12 @@ parent: dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -170,6 +182,12 @@ parent: dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -235,6 +253,12 @@ parent: dbcbd3d6-41ff-48ae-ac0b-1becc9b2fd21 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout index 83aa46e3d5c..82eb244e0d5 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-example-stdout @@ -95,6 +95,12 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -212,6 +218,12 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -326,6 +338,12 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -519,6 +537,12 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1174,6 +1198,12 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1229,6 +1259,12 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1284,6 +1320,12 @@ parent: 02697f74-b14a-4418-90f0-c28b2a3a6aa9 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1564,6 +1606,9 @@ LEDGERED SLED CONFIG b61b7c3c-d665-44b3-9312-794aa81c59de crucible install-dataset b957d6cf-f7b2-4bee-9928-c5fde8c59e04 crucible install-dataset e246f5e3-0650-4afc-860f-ee7114d309c5 crucible install-dataset + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -1571,6 +1616,12 @@ LEDGERED SLED CONFIG manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) no artifacts in install dataset (this should only be seen in simulated systems) no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -1590,6 +1641,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled 32d8d836-4d8a-4e54-8fa9-f31d79c42646 (role = Gimlet, serial serial2) @@ -1691,6 +1744,9 @@ LEDGERED SLED CONFIG 6c2a57b0-2de0-4409-a6b9-c9aa5614eefa crucible install-dataset 99a750b2-724d-4828-ae5f-0df1aad90166 crucible install-dataset e668d83e-a28c-42dc-b574-467e57403cc1 crucible install-dataset + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -1698,6 +1754,12 @@ LEDGERED SLED CONFIG manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) no artifacts in install dataset (this should only be seen in simulated systems) no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -1717,6 +1779,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled 89d02b1b-478c-401a-8e28-7a26f74fa41b (role = Gimlet, serial serial0) @@ -1911,6 +1975,9 @@ LEDGERED SLED CONFIG dc2666e6-4c3e-4b8e-99bc-bcdb5f8986e1 crucible_pantry install-dataset f4dc5b5d-6eb6-40a9-a079-971eca862285 crucible install-dataset ffbf02f0-261d-4723-b613-eb861245acbd internal_dns install-dataset + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -1918,6 +1985,12 @@ LEDGERED SLED CONFIG manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) no artifacts in install dataset (this should only be seen in simulated systems) no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -1937,6 +2010,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) KEEPER MEMBERSHIP diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-external-dns-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-external-dns-stdout index 567030c7ba4..9bc80b79c48 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-external-dns-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-external-dns-stdout @@ -24,6 +24,12 @@ parent: 06c88262-f435-410e-ba98-101bed41ec27 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -141,6 +147,12 @@ parent: 06c88262-f435-410e-ba98-101bed41ec27 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -255,6 +267,12 @@ parent: 06c88262-f435-410e-ba98-101bed41ec27 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -571,6 +589,12 @@ parent: 3f00b694-1b16-4aaa-8f78-e6b3a527b434 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -688,6 +712,12 @@ parent: 3f00b694-1b16-4aaa-8f78-e6b3a527b434 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -802,6 +832,12 @@ parent: 3f00b694-1b16-4aaa-8f78-e6b3a527b434 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1143,6 +1179,12 @@ parent: 366b0b68-d80e-4bc1-abd3-dc69837847e0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1263,6 +1305,12 @@ parent: 366b0b68-d80e-4bc1-abd3-dc69837847e0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1377,6 +1425,12 @@ parent: 366b0b68-d80e-4bc1-abd3-dc69837847e0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-internal-dns-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-internal-dns-stdout index 8d8f96928c4..3a1e9369392 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-internal-dns-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-newly-added-internal-dns-stdout @@ -22,6 +22,12 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -139,6 +145,12 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -253,6 +265,12 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-zones-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-zones-stdout index 213417a4d10..9fbabb5602d 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-zones-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-expunge-zones-stdout @@ -27,6 +27,12 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -95,6 +101,12 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -160,6 +172,12 @@ parent: 184f10b3-61cb-41ef-9b93-3489b2bac559 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-host-phase-2-source-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-host-phase-2-source-stdout index 45933052308..c87207e081a 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-host-phase-2-source-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-host-phase-2-source-stdout @@ -366,6 +366,12 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 B artifact: version 2.0.0 + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -872,6 +878,12 @@ parent: af934083-59b5-4bf6-8966-6fb5292c29e1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-mupdate-update-flow-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-mupdate-update-flow-stdout index 25242992fcd..f8a957f82fc 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-mupdate-update-flow-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-mupdate-update-flow-stdout @@ -286,6 +286,9 @@ LEDGERED SLED CONFIG 99e2f30b-3174-40bf-a78a-90da8abba8ca internal_dns install-dataset ad6a3a03-8d0f-4504-99a4-cbf73d69b973 crucible_pantry install-dataset bd354eef-d8a6-4165-9124-283fb5e46d77 crucible install-dataset + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -293,6 +296,12 @@ LEDGERED SLED CONFIG manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) no artifacts in install dataset (this should only be seen in simulated systems) no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json error obtaining override on boot disk: reconfigurator-cli simulated mupdate-override error @@ -312,6 +321,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (role = Gimlet, serial serial0) @@ -393,6 +404,9 @@ LEDGERED SLED CONFIG 6444f8a5-6465-4f0b-a549-1993c113569c internal_ntp install-dataset 803bfb63-c246-41db-b0da-d3b87ddfc63d external_dns install-dataset ba4994a8-23f9-4b1a-a84f-a08d74591389 crucible_pantry install-dataset + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -411,6 +425,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1681 bytes with hash 67593d686ed04a1709f93972b71f4ebc148a9362120f65d239943e814a9a7439): ok - oximeter.tar.gz (expected 1682 bytes with hash 048d8fe8cdef5b175aad714d0f148aa80ce36c9114ac15ce9d02ed3d37877a77): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json override on boot disk: 6123eac1-ec5b-42ba-b73f-9845105a9971 @@ -430,6 +450,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled d81c6a84-79b8-4958-ae41-ea46c9b19763 (role = Gimlet, serial serial2) @@ -511,6 +533,9 @@ LEDGERED SLED CONFIG f10a4fb9-759f-4a65-b25e-5794ad2d07d8 internal_ntp install-dataset f55647d4-5500-4ad3-893a-df45bd50d622 crucible install-dataset f6ec9c67-946a-4da3-98d5-581f72ce8bf0 external_dns install-dataset + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -518,6 +543,12 @@ LEDGERED SLED CONFIG manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) no artifacts in install dataset (this should only be seen in simulated systems) no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json override on boot disk: 203fa72c-85c1-466a-8ed3-338ee029530d @@ -537,6 +568,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) KEEPER MEMBERSHIP @@ -1234,6 +1267,12 @@ parent: c1a0d242-9160-40f4-96ae-61f8f40a0b1b B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1288,6 +1327,12 @@ parent: c1a0d242-9160-40f4-96ae-61f8f40a0b1b B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1339,6 +1384,12 @@ parent: c1a0d242-9160-40f4-96ae-61f8f40a0b1b B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1574,6 +1625,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1628,6 +1685,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1679,6 +1742,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1949,6 +2018,12 @@ parent: 8f2d1f39-7c88-4701-aa43-56bf281b28c1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -2003,6 +2078,12 @@ parent: 8f2d1f39-7c88-4701-aa43-56bf281b28c1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -2054,6 +2135,12 @@ parent: 8f2d1f39-7c88-4701-aa43-56bf281b28c1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-nexus-generation-autobump-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-nexus-generation-autobump-stdout index 28743676866..85996c8079f 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-nexus-generation-autobump-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-nexus-generation-autobump-stdout @@ -663,6 +663,9 @@ LEDGERED SLED CONFIG ad6a3a03-8d0f-4504-99a4-cbf73d69b973 crucible_pantry artifact: 21f0ada306859c23917361f2e0b9235806c32607ec689c7e8cf16bb898bc5a02 bd354eef-d8a6-4165-9124-283fb5e46d77 crucible artifact: 6f17cf65fb5a5bec5542dd07c03cd0acc01e59130f02c532c8d848ecae810047 e2fdefe7-95b2-4fd2-ae37-56929a06d58c crucible artifact: 6f17cf65fb5a5bec5542dd07c03cd0acc01e59130f02c532c8d848ecae810047 + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -681,6 +684,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1681 bytes with hash 67593d686ed04a1709f93972b71f4ebc148a9362120f65d239943e814a9a7439): ok - oximeter.tar.gz (expected 1682 bytes with hash 048d8fe8cdef5b175aad714d0f148aa80ce36c9114ac15ce9d02ed3d37877a77): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -700,6 +709,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (role = Gimlet, serial serial0) @@ -829,6 +840,9 @@ LEDGERED SLED CONFIG 803bfb63-c246-41db-b0da-d3b87ddfc63d external_dns artifact: ccca13ed19b8731f9adaf0d6203b02ea3b9ede4fa426b9fac0a07ce95440046d ba4994a8-23f9-4b1a-a84f-a08d74591389 crucible_pantry artifact: 21f0ada306859c23917361f2e0b9235806c32607ec689c7e8cf16bb898bc5a02 dfac80b4-a887-430a-ae87-a4e065dba787 crucible artifact: 6f17cf65fb5a5bec5542dd07c03cd0acc01e59130f02c532c8d848ecae810047 + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -847,6 +861,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1681 bytes with hash 67593d686ed04a1709f93972b71f4ebc148a9362120f65d239943e814a9a7439): ok - oximeter.tar.gz (expected 1682 bytes with hash 048d8fe8cdef5b175aad714d0f148aa80ce36c9114ac15ce9d02ed3d37877a77): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -866,6 +886,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled d81c6a84-79b8-4958-ae41-ea46c9b19763 (role = Gimlet, serial serial2) @@ -995,6 +1017,9 @@ LEDGERED SLED CONFIG f10a4fb9-759f-4a65-b25e-5794ad2d07d8 internal_ntp artifact: 67593d686ed04a1709f93972b71f4ebc148a9362120f65d239943e814a9a7439 f55647d4-5500-4ad3-893a-df45bd50d622 crucible artifact: 6f17cf65fb5a5bec5542dd07c03cd0acc01e59130f02c532c8d848ecae810047 f6ec9c67-946a-4da3-98d5-581f72ce8bf0 external_dns artifact: ccca13ed19b8731f9adaf0d6203b02ea3b9ede4fa426b9fac0a07ce95440046d + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -1013,6 +1038,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1681 bytes with hash 67593d686ed04a1709f93972b71f4ebc148a9362120f65d239943e814a9a7439): ok - oximeter.tar.gz (expected 1682 bytes with hash 048d8fe8cdef5b175aad714d0f148aa80ce36c9114ac15ce9d02ed3d37877a77): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -1032,6 +1063,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) KEEPER MEMBERSHIP @@ -1086,6 +1119,12 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1154,6 +1193,12 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1219,6 +1264,12 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-set-mgs-updates-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-set-mgs-updates-stdout index dce677037c7..d43140c861e 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-set-mgs-updates-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-set-mgs-updates-stdout @@ -24,6 +24,12 @@ parent: 6ccc786b-17f1-4562-958f-5a7d9a5a15fd B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -92,6 +98,12 @@ parent: 6ccc786b-17f1-4562-958f-5a7d9a5a15fd B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -157,6 +169,12 @@ parent: 6ccc786b-17f1-4562-958f-5a7d9a5a15fd B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -254,6 +272,12 @@ parent: ad97e762-7bf1-45a6-a98f-60afb7e491c0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -322,6 +346,12 @@ parent: ad97e762-7bf1-45a6-a98f-60afb7e491c0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -387,6 +417,12 @@ parent: ad97e762-7bf1-45a6-a98f-60afb7e491c0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -566,6 +602,12 @@ parent: cca24b71-09b5-4042-9185-b33e9f2ebba0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -634,6 +676,12 @@ parent: cca24b71-09b5-4042-9185-b33e9f2ebba0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -699,6 +747,12 @@ parent: cca24b71-09b5-4042-9185-b33e9f2ebba0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -881,6 +935,12 @@ parent: 5bf974f3-81f9-455b-b24e-3099f765664c B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -949,6 +1009,12 @@ parent: 5bf974f3-81f9-455b-b24e-3099f765664c B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1014,6 +1080,12 @@ parent: 5bf974f3-81f9-455b-b24e-3099f765664c B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1155,6 +1227,12 @@ parent: 1b837a27-3be1-4fcb-8499-a921c839e1d0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1223,6 +1301,12 @@ parent: 1b837a27-3be1-4fcb-8499-a921c839e1d0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1288,6 +1372,12 @@ parent: 1b837a27-3be1-4fcb-8499-a921c839e1d0 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-set-remove-mupdate-override-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-set-remove-mupdate-override-stdout index 38e000249d0..0c1a5791704 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-set-remove-mupdate-override-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-set-remove-mupdate-override-stdout @@ -62,6 +62,12 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -116,6 +122,12 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -168,6 +180,12 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -220,6 +238,12 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -262,6 +286,12 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -304,6 +334,12 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -346,6 +382,12 @@ parent: df06bb57-ad42-4431-9206-abff322896c7 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -730,6 +772,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------- vendor model serial disposition @@ -763,6 +811,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -817,6 +871,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -869,6 +929,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -921,6 +987,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -963,6 +1035,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1004,6 +1082,12 @@ parent: afb09faf-a586-4483-9289-04d4f1d8ba23 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-set-zone-images-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-set-zone-images-stdout index 86d9ef94924..fa8ae0ba1ff 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-set-zone-images-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-set-zone-images-stdout @@ -24,6 +24,12 @@ parent: 1b013011-2062-4b48-b544-a32b23bce83a B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -149,6 +155,12 @@ parent: 9766ca20-38d4-4380-b005-e7c43c797e7c B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -390,6 +402,12 @@ parent: bb128f06-a2e1-44c1-8874-4f789d0ff896 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-target-release-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-target-release-stdout index 952056fb802..dd506061520 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-target-release-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-target-release-stdout @@ -650,6 +650,9 @@ LEDGERED SLED CONFIG ad6a3a03-8d0f-4504-99a4-cbf73d69b973 crucible_pantry artifact: 6055871bfa626d582162302bf027102d90a03a42866867df2582f8eba231fc6d bd354eef-d8a6-4165-9124-283fb5e46d77 crucible artifact: f3694b20fa1de79fb1f7c3a9f89f9f9eb5ebaaefc3caba7e1991e7e2b3191ed4 e2fdefe7-95b2-4fd2-ae37-56929a06d58c crucible artifact: f3694b20fa1de79fb1f7c3a9f89f9f9eb5ebaaefc3caba7e1991e7e2b3191ed4 + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -668,6 +671,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1682 bytes with hash b661b5d1370f5ac593b4c15b5fcd22c904991cf33b6db32f886374bc022a3531): ok - oximeter.tar.gz (expected 1683 bytes with hash 7ea25be50cd4e98e2ba20916cb98fe8ea457372f5973eb6ac691b5bc90dbddc0): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -687,6 +696,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (role = Gimlet, serial serial0) @@ -816,6 +827,9 @@ LEDGERED SLED CONFIG 803bfb63-c246-41db-b0da-d3b87ddfc63d external_dns artifact: 584217eae459e4c2bd00621cf1910d06edb8258948a4832ab0329cf42067c0c7 ba4994a8-23f9-4b1a-a84f-a08d74591389 crucible_pantry artifact: 6055871bfa626d582162302bf027102d90a03a42866867df2582f8eba231fc6d dfac80b4-a887-430a-ae87-a4e065dba787 crucible artifact: f3694b20fa1de79fb1f7c3a9f89f9f9eb5ebaaefc3caba7e1991e7e2b3191ed4 + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -834,6 +848,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1682 bytes with hash b661b5d1370f5ac593b4c15b5fcd22c904991cf33b6db32f886374bc022a3531): ok - oximeter.tar.gz (expected 1683 bytes with hash 7ea25be50cd4e98e2ba20916cb98fe8ea457372f5973eb6ac691b5bc90dbddc0): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -853,6 +873,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled d81c6a84-79b8-4958-ae41-ea46c9b19763 (role = Gimlet, serial serial2) @@ -982,6 +1004,9 @@ LEDGERED SLED CONFIG f10a4fb9-759f-4a65-b25e-5794ad2d07d8 internal_ntp artifact: b661b5d1370f5ac593b4c15b5fcd22c904991cf33b6db32f886374bc022a3531 f55647d4-5500-4ad3-893a-df45bd50d622 crucible artifact: f3694b20fa1de79fb1f7c3a9f89f9f9eb5ebaaefc3caba7e1991e7e2b3191ed4 f6ec9c67-946a-4da3-98d5-581f72ce8bf0 external_dns artifact: 584217eae459e4c2bd00621cf1910d06edb8258948a4832ab0329cf42067c0c7 + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -1000,6 +1025,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1682 bytes with hash b661b5d1370f5ac593b4c15b5fcd22c904991cf33b6db32f886374bc022a3531): ok - oximeter.tar.gz (expected 1683 bytes with hash 7ea25be50cd4e98e2ba20916cb98fe8ea457372f5973eb6ac691b5bc90dbddc0): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -1019,6 +1050,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) KEEPER MEMBERSHIP @@ -9068,6 +9101,12 @@ parent: 05685571-d61f-4754-a2b2-604ea8c45dff B artifact: version 1.0.0 + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -9148,6 +9187,12 @@ parent: 05685571-d61f-4754-a2b2-604ea8c45dff B artifact: version 1.0.0 + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -9227,6 +9272,12 @@ parent: 05685571-d61f-4754-a2b2-604ea8c45dff B artifact: version 1.0.0 + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/dev-tools/reconfigurator-cli/tests/output/cmds-unsafe-zone-mgs-stdout b/dev-tools/reconfigurator-cli/tests/output/cmds-unsafe-zone-mgs-stdout index 82562dc16a7..5c3561f8527 100644 --- a/dev-tools/reconfigurator-cli/tests/output/cmds-unsafe-zone-mgs-stdout +++ b/dev-tools/reconfigurator-cli/tests/output/cmds-unsafe-zone-mgs-stdout @@ -634,6 +634,9 @@ LEDGERED SLED CONFIG ad6a3a03-8d0f-4504-99a4-cbf73d69b973 crucible_pantry artifact: 6055871bfa626d582162302bf027102d90a03a42866867df2582f8eba231fc6d bd354eef-d8a6-4165-9124-283fb5e46d77 crucible artifact: f3694b20fa1de79fb1f7c3a9f89f9f9eb5ebaaefc3caba7e1991e7e2b3191ed4 e2fdefe7-95b2-4fd2-ae37-56929a06d58c crucible artifact: f3694b20fa1de79fb1f7c3a9f89f9f9eb5ebaaefc3caba7e1991e7e2b3191ed4 + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -652,6 +655,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1682 bytes with hash b661b5d1370f5ac593b4c15b5fcd22c904991cf33b6db32f886374bc022a3531): ok - oximeter.tar.gz (expected 1683 bytes with hash 7ea25be50cd4e98e2ba20916cb98fe8ea457372f5973eb6ac691b5bc90dbddc0): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -671,6 +680,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled 98e6b7c2-2efa-41ca-b20a-0a4d61102fe6 (role = Gimlet, serial serial0) @@ -800,6 +811,9 @@ LEDGERED SLED CONFIG 803bfb63-c246-41db-b0da-d3b87ddfc63d external_dns artifact: 584217eae459e4c2bd00621cf1910d06edb8258948a4832ab0329cf42067c0c7 ba4994a8-23f9-4b1a-a84f-a08d74591389 crucible_pantry artifact: 6055871bfa626d582162302bf027102d90a03a42866867df2582f8eba231fc6d dfac80b4-a887-430a-ae87-a4e065dba787 crucible artifact: f3694b20fa1de79fb1f7c3a9f89f9f9eb5ebaaefc3caba7e1991e7e2b3191ed4 + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -818,6 +832,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1682 bytes with hash b661b5d1370f5ac593b4c15b5fcd22c904991cf33b6db32f886374bc022a3531): ok - oximeter.tar.gz (expected 1683 bytes with hash 7ea25be50cd4e98e2ba20916cb98fe8ea457372f5973eb6ac691b5bc90dbddc0): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -837,6 +857,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) sled d81c6a84-79b8-4958-ae41-ea46c9b19763 (role = Gimlet, serial serial2) @@ -966,6 +988,9 @@ LEDGERED SLED CONFIG f10a4fb9-759f-4a65-b25e-5794ad2d07d8 internal_ntp artifact: b661b5d1370f5ac593b4c15b5fcd22c904991cf33b6db32f886374bc022a3531 f55647d4-5500-4ad3-893a-df45bd50d622 crucible artifact: f3694b20fa1de79fb1f7c3a9f89f9f9eb5ebaaefc3caba7e1991e7e2b3191ed4 f6ec9c67-946a-4da3-98d5-581f72ce8bf0 external_dns artifact: 584217eae459e4c2bd00621cf1910d06edb8258948a4832ab0329cf42067c0c7 + measurements: + install dataset + zone image resolver status: zone manifest: path on boot disk: /fake/path/install/zones.json @@ -984,6 +1009,12 @@ LEDGERED SLED CONFIG - ntp.tar.gz (expected 1682 bytes with hash b661b5d1370f5ac593b4c15b5fcd22c904991cf33b6db32f886374bc022a3531): ok - oximeter.tar.gz (expected 1683 bytes with hash 7ea25be50cd4e98e2ba20916cb98fe8ea457372f5973eb6ac691b5bc90dbddc0): ok no non-boot disks + measurement manifest: + path on boot disk: /fake/path/install/zones.json + boot disk inventory: + manifest generated by installinator (mupdate ID: 00000000-0000-0000-0000-000000000000) + no artifacts in install dataset (this should only be seen in simulated systems) + no non-boot disks mupdate override: path on boot disk: /fake/path/install/mupdate_override.json no override on boot disk @@ -1003,6 +1034,8 @@ LEDGERED SLED CONFIG all disks reconciled successfully all datasets reconciled successfully all zones reconciled successfully + reference measurements: + (measurement set is empty) reconciler task status: idle (finished at after running for s) KEEPER MEMBERSHIP @@ -1095,6 +1128,12 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1163,6 +1202,12 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -1228,6 +1273,12 @@ parent: 8da82a8e-bf97-4fbd-8ddd-9f6462732cf1 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/nexus/db-model/src/deployment.rs b/nexus/db-model/src/deployment.rs index 7e8d1b84346..b5d6396b235 100644 --- a/nexus/db-model/src/deployment.rs +++ b/nexus/db-model/src/deployment.rs @@ -24,10 +24,11 @@ use nexus_db_schema::schema::{ bp_omicron_physical_disk, bp_omicron_zone, bp_omicron_zone_nic, bp_oximeter_read_policy, bp_pending_mgs_update_host_phase_1, bp_pending_mgs_update_rot, bp_pending_mgs_update_rot_bootloader, - bp_pending_mgs_update_sp, bp_sled_metadata, bp_target, - debug_log_blueprint_planning, + bp_pending_mgs_update_sp, bp_single_measurements, bp_sled_metadata, + bp_target, debug_log_blueprint_planning, }; use nexus_types::deployment::BlueprintPhysicalDiskDisposition; +use nexus_types::deployment::BlueprintSingleMeasurement; use nexus_types::deployment::BlueprintTarget; use nexus_types::deployment::BlueprintZoneConfig; use nexus_types::deployment::BlueprintZoneDisposition; @@ -64,8 +65,9 @@ use omicron_common::disk::DiskIdentity; use omicron_common::zpool_name::ZpoolName; use omicron_uuid_kinds::{ BlueprintKind, BlueprintUuid, DatasetKind, ExternalIpKind, ExternalIpUuid, - GenericUuid, MupdateOverrideKind, OmicronZoneKind, OmicronZoneUuid, - PhysicalDiskKind, SledKind, SledUuid, ZpoolKind, ZpoolUuid, + GenericUuid, MeasurementKind, MupdateOverrideKind, OmicronZoneKind, + OmicronZoneUuid, PhysicalDiskKind, SledKind, SledUuid, ZpoolKind, + ZpoolUuid, }; use sled_agent_types::inventory::OmicronZoneDataset; use std::net::{IpAddr, SocketAddrV6}; @@ -1217,6 +1219,51 @@ impl TryFrom for BlueprintZoneImageSource { } } +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = bp_single_measurements)] +pub struct BpSingleMeasurement { + pub blueprint_id: DbTypedUuid, + pub sled_id: DbTypedUuid, + pub id: DbTypedUuid, + + pub image_artifact_sha256: Option, + pub prune: bool, +} + +impl BpSingleMeasurement { + pub fn new( + blueprint_id: BlueprintUuid, + sled_id: SledUuid, + measurement: &BlueprintSingleMeasurement, + ) -> Self { + Self { + blueprint_id: blueprint_id.into(), + sled_id: sled_id.into(), + id: omicron_uuid_kinds::MeasurementUuid::new_v4().into(), + image_artifact_sha256: Some(measurement.hash.into()), + prune: measurement.prune, + } + } + + pub fn to_measurement( + self, + artifact: Option, + ) -> BlueprintSingleMeasurement { + BlueprintSingleMeasurement { + version: match artifact { + Some(a) => { + BlueprintArtifactVersion::Available { version: a.version.0 } + } + None => BlueprintArtifactVersion::Unknown, + }, + hash: *self + .image_artifact_sha256 + .expect("this should always be set"), + prune: self.prune, + } + } +} + #[derive(Queryable, Clone, Debug, Selectable, Insertable)] #[diesel(table_name = bp_omicron_zone_nic)] pub struct BpOmicronZoneNic { diff --git a/nexus/db-model/src/inventory.rs b/nexus/db-model/src/inventory.rs index fba0df6b6a5..5d018ffeb71 100644 --- a/nexus/db-model/src/inventory.rs +++ b/nexus/db-model/src/inventory.rs @@ -33,15 +33,17 @@ use nexus_db_schema::schema::{ inv_cockroachdb_status, inv_collection, inv_collection_error, inv_dataset, inv_host_phase_1_active_slot, inv_host_phase_1_flash_hash, inv_internal_dns, inv_last_reconciliation_dataset_result, - inv_last_reconciliation_disk_result, + inv_last_reconciliation_disk_result, inv_last_reconciliation_measurements, inv_last_reconciliation_orphaned_dataset, - inv_last_reconciliation_zone_result, inv_mupdate_override_non_boot, - inv_ntp_timesync, inv_nvme_disk_firmware, inv_omicron_sled_config, - inv_omicron_sled_config_dataset, inv_omicron_sled_config_disk, - inv_omicron_sled_config_zone, inv_omicron_sled_config_zone_nic, - inv_physical_disk, inv_root_of_trust, inv_root_of_trust_page, - inv_service_processor, inv_sled_agent, inv_sled_boot_partition, - inv_sled_config_reconciler, inv_zpool, sw_caboose, sw_root_of_trust_page, + inv_last_reconciliation_zone_result, inv_measurement_manifest_non_boot, + inv_mupdate_override_non_boot, inv_ntp_timesync, inv_nvme_disk_firmware, + inv_omicron_sled_config, inv_omicron_sled_config_dataset, + inv_omicron_sled_config_disk, inv_omicron_sled_config_zone, + inv_omicron_sled_config_zone_nic, inv_physical_disk, inv_root_of_trust, + inv_root_of_trust_page, inv_service_processor, inv_sled_agent, + inv_sled_boot_partition, inv_sled_config_reconciler, + inv_zone_manifest_measurement, inv_zpool, sw_caboose, + sw_root_of_trust_page, }; use nexus_types::inventory::HostPhase1ActiveSlot; use nexus_types::inventory::{ @@ -83,7 +85,10 @@ use sled_agent_types::inventory::ManifestNonBootInventory; use sled_agent_types::inventory::MupdateOverrideBootInventory; use sled_agent_types::inventory::MupdateOverrideInventory; use sled_agent_types::inventory::MupdateOverrideNonBootInventory; +use sled_agent_types::inventory::OmicronMeasurementSetDesiredContents; +use sled_agent_types::inventory::OmicronMeasurements; use sled_agent_types::inventory::OrphanedDataset; +use sled_agent_types::inventory::ReconciledSingleMeasurement; use sled_agent_types::inventory::RemoveMupdateOverrideBootSuccessInventory; use sled_agent_types::inventory::RemoveMupdateOverrideInventory; use sled_agent_types::inventory::ZoneArtifactInventory; @@ -1369,6 +1374,66 @@ impl From for ConfigReconcilerInventoryResult { } } +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = inv_last_reconciliation_measurements)] +pub struct InvLastReconciliationMeasurements { + pub inv_collection_id: DbTypedUuid, + pub sled_id: DbTypedUuid, + + pub file_name: String, + pub path: String, + pub error_message: Option, +} + +impl InvLastReconciliationMeasurements { + pub fn new( + inv_collection_id: CollectionUuid, + sled_id: SledUuid, + file_name: String, + path: String, + result: ConfigReconcilerInventoryResult, + ) -> Self { + let error_message = match result { + ConfigReconcilerInventoryResult::Ok => None, + ConfigReconcilerInventoryResult::Err { message } => Some(message), + }; + Self { + inv_collection_id: inv_collection_id.into(), + sled_id: sled_id.into(), + + path, + file_name, + error_message, + } + } +} + +impl From for ReconciledSingleMeasurement { + fn from(row: InvLastReconciliationMeasurements) -> Self { + Self { + file_name: row.file_name, + path: row.path.into(), + result: match row.error_message { + None => ConfigReconcilerInventoryResult::Ok, + Some(message) => { + ConfigReconcilerInventoryResult::Err { message } + } + }, + } + } +} + +impl From + for ConfigReconcilerInventoryResult +{ + fn from(result: InvLastReconciliationMeasurements) -> Self { + match result.error_message { + None => Self::Ok, + Some(message) => Self::Err { message }, + } + } +} + #[derive(Queryable, Clone, Debug, Selectable, Insertable)] #[diesel(table_name = inv_last_reconciliation_dataset_result)] pub struct InvLastReconciliationDatasetResult { @@ -1533,6 +1598,11 @@ pub struct InvZoneImageResolver { pub zone_manifest_mupdate_id: Option>, pub zone_manifest_boot_disk_error: Option, + pub measurement_manifest_boot_disk_path: String, + pub measurement_manifest_source: Option, + pub measurement_manifest_mupdate_id: Option>, + pub measurement_manifest_boot_disk_error: Option, + pub mupdate_override_boot_disk_path: String, pub mupdate_override_id: Option>, pub mupdate_override_boot_disk_error: Option, @@ -1561,6 +1631,26 @@ impl InvZoneImageResolver { Err(error) => (None, None, Some(error.to_string())), }; + let measurement_manifest_boot_disk_path = + inv.measurement_manifest.boot_disk_path.clone().into(); + let ( + measurement_manifest_source, + measurement_manifest_mupdate_id, + measurement_manifest_boot_disk_error, + ) = match &inv.measurement_manifest.boot_inventory { + Ok(manifest) => match manifest.source { + OmicronInstallManifestSource::Installinator { mupdate_id } => ( + Some(InvZoneManifestSourceEnum::Installinator), + Some(mupdate_id.into()), + None, + ), + OmicronInstallManifestSource::SledAgent => { + (Some(InvZoneManifestSourceEnum::SledAgent), None, None) + } + }, + Err(error) => (None, None, Some(error.to_string())), + }; + let mupdate_override_boot_disk_path = inv.mupdate_override.boot_disk_path.clone().into(); let mupdate_override_id = inv @@ -1579,6 +1669,10 @@ impl InvZoneImageResolver { zone_manifest_source, zone_manifest_mupdate_id, zone_manifest_boot_disk_error, + measurement_manifest_boot_disk_path, + measurement_manifest_source, + measurement_manifest_mupdate_id, + measurement_manifest_boot_disk_error, mupdate_override_boot_disk_path, mupdate_override_id, mupdate_override_boot_disk_error, @@ -1589,7 +1683,11 @@ impl InvZoneImageResolver { pub fn into_inventory( self, artifacts: Option>, + measurement_artifacts: Option>, zone_manifest_non_boot: Option>, + measurement_manifest_non_boot: Option< + IdOrdMap, + >, mupdate_override_non_boot: Option< IdOrdMap, >, @@ -1643,6 +1741,55 @@ impl InvZoneImageResolver { } }; + let measurement_manifest = { + let boot_inventory = if let Some(error) = + self.measurement_manifest_boot_disk_error + { + Err(error) + } else { + let source = match self.measurement_manifest_source { + Some(InvZoneManifestSourceEnum::Installinator) => { + OmicronInstallManifestSource::Installinator { + mupdate_id: self + .measurement_manifest_mupdate_id + .context( + "illegal database state (CHECK constraint broken?!): \ + if the source is Installinator, then the \ + db schema guarantees that mupdate_id is Some", + )? + .into(), + } + } + Some(InvZoneManifestSourceEnum::SledAgent) => { + OmicronInstallManifestSource::SledAgent + } + None => { + bail!( + "illegal database state (CHECK constraint broken?!): \ + if the source is None, then the db schema guarantees \ + that there was an error", + ) + } + }; + + Ok(ManifestBootInventory { + source, + // Artifacts might really be None in case no zones were found. + // (This is unusual but permitted by the data model, so any + // checks around this should happen at a higher level.) + artifacts: measurement_artifacts.unwrap_or_default(), + }) + }; + + ManifestInventory { + boot_disk_path: self.measurement_manifest_boot_disk_path.into(), + boot_inventory, + // This might be None if no non-boot disks were found. + non_boot_status: measurement_manifest_non_boot + .unwrap_or_default(), + } + }; + // Build up the mupdate override struct. let boot_override = if let Some(error) = self.mupdate_override_boot_disk_error @@ -1662,7 +1809,57 @@ impl InvZoneImageResolver { non_boot_status: mupdate_override_non_boot.unwrap_or_default(), }; - Ok(ZoneImageResolverInventory { zone_manifest, mupdate_override }) + Ok(ZoneImageResolverInventory { + zone_manifest, + measurement_manifest, + mupdate_override, + }) + } +} + +/// Represents a zone file entry from the zone manifest on a sled. +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = inv_zone_manifest_measurement)] +pub struct InvZoneManifestMeasurement { + pub inv_collection_id: DbTypedUuid, + pub sled_id: DbTypedUuid, + pub zone_file_name: String, + pub path: String, + pub expected_size: i64, + pub expected_sha256: ArtifactHash, + pub error: Option, +} + +impl InvZoneManifestMeasurement { + pub fn new( + collection_id: CollectionUuid, + sled_id: SledUuid, + artifact: &ZoneArtifactInventory, + ) -> Self { + Self { + inv_collection_id: collection_id.into(), + sled_id: sled_id.into(), + zone_file_name: artifact.file_name.clone(), + path: artifact.path.clone().into(), + expected_size: artifact.expected_size as i64, + expected_sha256: artifact.expected_hash.into(), + error: artifact.status.as_ref().err().cloned(), + } + } +} + +impl From for ZoneArtifactInventory { + fn from(row: InvZoneManifestMeasurement) -> Self { + Self { + file_name: row.zone_file_name, + path: row.path.into(), + expected_size: row.expected_size as u64, + expected_hash: row.expected_sha256.into(), + status: match row.error { + None => Ok(()), + Some(error) => Err(error), + }, + } } } @@ -1712,6 +1909,46 @@ impl From for ZoneArtifactInventory { } } +/// Represents a non-boot zpool entry from the zone manifest on a sled. +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = inv_measurement_manifest_non_boot)] +pub struct InvMeasurementManifestNonBoot { + pub inv_collection_id: DbTypedUuid, + pub sled_id: DbTypedUuid, + pub non_boot_zpool_id: DbTypedUuid, + pub path: String, + pub is_valid: bool, + pub message: String, +} + +impl InvMeasurementManifestNonBoot { + pub fn new( + collection_id: CollectionUuid, + sled_id: SledUuid, + non_boot: &ManifestNonBootInventory, + ) -> Self { + Self { + inv_collection_id: collection_id.into(), + sled_id: sled_id.into(), + non_boot_zpool_id: non_boot.zpool_id.into(), + path: non_boot.path.clone().into(), + is_valid: non_boot.is_valid, + message: non_boot.message.clone(), + } + } +} + +impl From for ManifestNonBootInventory { + fn from(row: InvMeasurementManifestNonBoot) -> Self { + Self { + zpool_id: row.non_boot_zpool_id.into(), + path: row.path.into(), + is_valid: row.is_valid, + message: row.message, + } + } +} + /// Represents a non-boot zpool entry from the zone manifest on a sled. #[derive(Queryable, Clone, Debug, Selectable, Insertable)] #[diesel(table_name = inv_zone_manifest_non_boot)] @@ -2122,6 +2359,8 @@ pub struct InvOmicronSledConfig { #[diesel(embed)] pub host_phase_2: DbHostPhase2DesiredSlots, + #[diesel(embed)] + pub measurements: DbOmicronMeasurements, } impl InvOmicronSledConfig { @@ -2131,6 +2370,7 @@ impl InvOmicronSledConfig { generation: external::Generation, remove_mupdate_override: Option, host_phase_2: HostPhase2DesiredSlots, + measurements: OmicronMeasurements, ) -> Self { Self { inv_collection_id: inv_collection_id.into(), @@ -2138,10 +2378,46 @@ impl InvOmicronSledConfig { generation: Generation(generation), remove_mupdate_override: remove_mupdate_override.map(From::from), host_phase_2: host_phase_2.into(), + measurements: measurements.into(), } } } +#[derive(Queryable, Clone, Debug, Selectable, Insertable)] +#[diesel(table_name = inv_omicron_sled_config)] +pub struct DbOmicronMeasurements { + pub measurements: Option>, +} + +impl From for DbOmicronMeasurements { + fn from(value: OmicronMeasurements) -> Self { + let remap = |desired| match desired { + OmicronMeasurementSetDesiredContents::InstallDataset => None, + // Hashes should always be non-empty + OmicronMeasurementSetDesiredContents::Artifacts { hashes } => { + Some(hashes.into_iter().map(|x| ArtifactHash(x)).collect()) + } + }; + Self { measurements: remap(value.measurements) } + } +} + +impl From for OmicronMeasurements { + fn from(value: DbOmicronMeasurements) -> Self { + let remap = + |maybe_artifact: Option>| match maybe_artifact { + None => OmicronMeasurementSetDesiredContents::InstallDataset, + Some(hashes) => { + let hashes = hashes.into_iter().map(|ArtifactHash(x)| x); + OmicronMeasurementSetDesiredContents::Artifacts { + hashes: hashes.collect(), + } + } + }; + Self { measurements: remap(value.measurements) } + } +} + #[derive(Queryable, Clone, Debug, Selectable, Insertable)] #[diesel(table_name = inv_omicron_sled_config)] pub struct DbHostPhase2DesiredSlots { diff --git a/nexus/db-model/src/schema_versions.rs b/nexus/db-model/src/schema_versions.rs index a9f9ef99b8e..7862d24c8e7 100644 --- a/nexus/db-model/src/schema_versions.rs +++ b/nexus/db-model/src/schema_versions.rs @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock}; /// /// This must be updated when you change the database schema. Refer to /// schema/crdb/README.adoc in the root of this repository for details. -pub const SCHEMA_VERSION: Version = Version::new(213, 0, 0); +pub const SCHEMA_VERSION: Version = Version::new(214, 0, 0); /// List of all past database schema versions, in *reverse* order /// @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock> = LazyLock::new(|| { // | leaving the first copy as an example for the next person. // v // KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"), + KnownVersion::new(214, "measurements"), KnownVersion::new(213, "fm-cases"), KnownVersion::new(212, "local-storage-disk-type"), KnownVersion::new(211, "blueprint-sled-config-subnet"), diff --git a/nexus/db-queries/src/db/datastore/deployment.rs b/nexus/db-queries/src/db/datastore/deployment.rs index 4580c6e4257..b25d4839418 100644 --- a/nexus/db-queries/src/db/datastore/deployment.rs +++ b/nexus/db-queries/src/db/datastore/deployment.rs @@ -57,6 +57,7 @@ use nexus_db_model::BpPendingMgsUpdateHostPhase1; use nexus_db_model::BpPendingMgsUpdateRot; use nexus_db_model::BpPendingMgsUpdateRotBootloader; use nexus_db_model::BpPendingMgsUpdateSp; +use nexus_db_model::BpSingleMeasurement; use nexus_db_model::BpSledMetadata; use nexus_db_model::BpTarget; use nexus_db_model::DbArtifactVersion; @@ -75,6 +76,7 @@ use nexus_db_schema::enums::HwM2SlotEnum; use nexus_db_schema::enums::HwRotSlotEnum; use nexus_db_schema::enums::SpTypeEnum; use nexus_types::deployment::Blueprint; +use nexus_types::deployment::BlueprintMeasurementsDesiredContents; use nexus_types::deployment::BlueprintMetadata; use nexus_types::deployment::BlueprintSledConfig; use nexus_types::deployment::BlueprintSource; @@ -262,6 +264,20 @@ impl DataStore { }) .collect::>(); + let measurements = blueprint + .sleds + .iter() + .flat_map(|(sled_id, sled)| { + sled.measurements.measurements.iter().map(move |measurement| { + BpSingleMeasurement::new( + blueprint_id, + *sled_id, + measurement, + ) + }) + }) + .collect::>(); + let omicron_physical_disks = blueprint .sleds .iter() @@ -391,6 +407,22 @@ impl DataStore { .await?; } + // Insert all measurements for this blueprint. + { + // Skip formatting this line to prevent rustfmt bailing out. + #[rustfmt::skip] + use nexus_db_schema::schema::bp_single_measurements::dsl + as single_measurement; + let _ = diesel::insert_into( + single_measurement::bp_single_measurements, + ) + .values(measurements) + .execute_async(&conn) + .await?; + } + + + // Insert all physical disks for this blueprint. { // Skip formatting this line to prevent rustfmt bailing out. @@ -807,6 +839,8 @@ impl DataStore { .map(|id| id.into()), host_phase_2: s .host_phase_2(slot_a_version, slot_b_version), + // Measurements get loaded from the DB + measurements: BlueprintMeasurementsDesiredContents::default_contents(), }; let old = sled_configs.insert(s.sled_id.into(), config); bail_unless!( @@ -819,6 +853,68 @@ impl DataStore { sled_configs }; + { + use nexus_db_schema::schema::bp_single_measurements::dsl; + use nexus_db_schema::schema::tuf_artifact::dsl as tuf_artifact_dsl; + + let mut paginator = Paginator::new( + SQL_BATCH_SIZE, + dropshot::PaginationOrder::Ascending, + ); + while let Some(p) = paginator.next() { + let batch = paginated( + dsl::bp_single_measurements, + dsl::id, + &p.current_pagparams(), + ) + .filter(dsl::blueprint_id.eq(to_db_typed_uuid(blueprint_id))) + // Left join in case the artifact is missing from the + // tuf_artifact table, which is non-fatal. + .left_join( + tuf_artifact_dsl::tuf_artifact.on(tuf_artifact_dsl::kind + .eq(ArtifactKind::MEASUREMENT_CORPUS.to_string()) + .and( + tuf_artifact_dsl::sha256 + .nullable() + .eq(dsl::image_artifact_sha256), + )), + ) + .select(( + BpSingleMeasurement::as_select(), + Option::::as_select(), + )) + .load_async::<(BpSingleMeasurement, Option)>( + &*conn, + ) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })?; + + paginator = p.found_batch(&batch, &|(z, _)| z.id); + + for (m, artifact) in batch { + let sled_config = sled_configs + .get_mut(&m.sled_id.into()) + .ok_or_else(|| { + // This error means that we found a row in + // bp_omicron_dataset with no associated record in + // bp_sled_omicron_datasets. This should be + // impossible and reflects either a bug or database + // corruption. + Error::internal_error(&format!( + "measurement {}: unknown sled: {}", + m.blueprint_id, m.sled_id + )) + })?; + + sled_config + .measurements + .append_measurement(m.to_measurement(artifact)); + } + } + } + // Assemble a mutable map of all the NICs found, by NIC id. As we // match these up with the corresponding zone below, we'll remove items // from this set. That way we can tell if the same NIC was used twice @@ -4579,6 +4675,7 @@ mod tests { blueprint_id ), query_count!(bp_pending_mgs_update_host_phase_1, blueprint_id), + query_count!(bp_single_measurements, blueprint_id), query_count!(debug_log_blueprint_planning, blueprint_id), ] { let count: i64 = result.unwrap(); @@ -4698,6 +4795,7 @@ mod tests { "bp_clickhouse_keeper_zone_id_to_node_id", "bp_clickhouse_server_zone_id_to_node_id", "debug_log_blueprint_planning", + "bp_single_measurements", ]; // Check that all non-exception tables have at least one row diff --git a/nexus/db-queries/src/db/datastore/inventory.rs b/nexus/db-queries/src/db/datastore/inventory.rs index d71f3ee0938..05541a3fdcc 100644 --- a/nexus/db-queries/src/db/datastore/inventory.rs +++ b/nexus/db-queries/src/db/datastore/inventory.rs @@ -44,8 +44,10 @@ use nexus_db_model::InvHostPhase1FlashHash; use nexus_db_model::InvInternalDns; use nexus_db_model::InvLastReconciliationDatasetResult; use nexus_db_model::InvLastReconciliationDiskResult; +use nexus_db_model::InvLastReconciliationMeasurements; use nexus_db_model::InvLastReconciliationOrphanedDataset; use nexus_db_model::InvLastReconciliationZoneResult; +use nexus_db_model::InvMeasurementManifestNonBoot; use nexus_db_model::InvNtpTimesync; use nexus_db_model::InvNvmeDiskFirmware; use nexus_db_model::InvOmicronSledConfig; @@ -70,7 +72,8 @@ use nexus_db_model::SwCaboose; use nexus_db_model::SwRotPage; use nexus_db_model::to_db_typed_uuid; use nexus_db_model::{ - HwBaseboardId, InvZoneImageResolver, InvZoneManifestZone, + HwBaseboardId, InvZoneImageResolver, InvZoneManifestMeasurement, + InvZoneManifestZone, }; use nexus_db_model::{HwPowerState, InvZoneManifestNonBoot}; use nexus_db_model::{HwRotSlot, InvMupdateOverrideNonBoot}; @@ -114,6 +117,7 @@ use sled_agent_types::inventory::ManifestNonBootInventory; use sled_agent_types::inventory::MupdateOverrideNonBootInventory; use sled_agent_types::inventory::OmicronSledConfig; use sled_agent_types::inventory::OrphanedDataset; +use sled_agent_types::inventory::ReconciledSingleMeasurement; use sled_agent_types::inventory::ZoneArtifactInventory; use slog_error_chain::InlineErrorChain; use std::collections::BTreeMap; @@ -266,6 +270,30 @@ impl DataStore { .flatten() .collect(); + // Pull zone manifest measurements out of all sled agents. + let zone_manifest_measurements: Vec<_> = collection + .sled_agents + .iter() + .filter_map(|sled_agent| { + sled_agent + .zone_image_resolver + .measurement_manifest + .boot_inventory + .as_ref() + .ok() + .map(|artifacts| { + artifacts.artifacts.iter().map(|artifact| { + InvZoneManifestMeasurement::new( + collection_id, + sled_agent.sled_id, + artifact, + ) + }) + }) + }) + .flatten() + .collect(); + // Pull zone manifest non-boot info out of all sled agents. let zone_manifest_non_boot: Vec<_> = collection .sled_agents @@ -286,6 +314,26 @@ impl DataStore { }) .collect(); + // Pull zone manifest non-boot info out of all sled agents. + let measurement_manifest_non_boot: Vec<_> = collection + .sled_agents + .iter() + .flat_map(|sled_agent| { + sled_agent + .zone_image_resolver + .measurement_manifest + .non_boot_status + .iter() + .map(|non_boot| { + InvMeasurementManifestNonBoot::new( + collection_id, + sled_agent.sled_id, + non_boot, + ) + }) + }) + .collect(); + // Pull mupdate override non-boot info out of all sled agents. let mupdate_override_non_boot: Vec<_> = collection .sled_agents @@ -338,6 +386,7 @@ impl DataStore { zone_results: reconciler_zone_results, boot_partitions: reconciler_boot_partitions, mut config_reconciler_fields_by_sled, + measurements: reconciler_measurement_results, } = ConfigReconcilerRows::new(collection_id, collection) .map_err(|e| Error::internal_error(&format!("{e:#}")))?; @@ -1260,6 +1309,27 @@ impl DataStore { } } + // Insert rows for all the sled config reconciler measurements + { + use nexus_db_schema::schema::inv_last_reconciliation_measurements::dsl; + + let batch_size = SQL_BATCH_SIZE.get().try_into().unwrap(); + let mut measurement_results = reconciler_measurement_results.into_iter(); + loop { + let some_measurement_results = + measurement_results.by_ref().take(batch_size).collect::>(); + if some_measurement_results.is_empty() { + break; + } + let _ = diesel::insert_into(dsl::inv_last_reconciliation_measurements) + .values(some_measurement_results) + .execute_async(&conn) + .await?; + } + } + + + // Insert rows for all the sled config reconciler disk results { use nexus_db_schema::schema::inv_last_reconciliation_disk_result::dsl; @@ -1336,6 +1406,28 @@ impl DataStore { } } + // Insert rows for all the zones found in the zone manifest on the + // boot disk. + { + use nexus_db_schema::schema::inv_zone_manifest_measurement::dsl; + + let batch_size = SQL_BATCH_SIZE.get().try_into().unwrap(); + let mut measurements = zone_manifest_measurements.into_iter(); + loop { + let some_measurements = + measurements.by_ref().take(batch_size).collect::>(); + if some_measurements.is_empty() { + break; + } + let _ = diesel::insert_into(dsl::inv_zone_manifest_measurement) + .values(some_measurements) + .execute_async(&conn) + .await?; + } + } + + + // Insert rows for all the zones found in the zone manifest on the // boot disk. { @@ -1375,6 +1467,26 @@ impl DataStore { } } + // Insert rows for non-boot measurement manifests. + { + use nexus_db_schema::schema::inv_measurement_manifest_non_boot::dsl; + + let batch_size = SQL_BATCH_SIZE.get().try_into().unwrap(); + let mut non_boot = measurement_manifest_non_boot.into_iter(); + loop { + let some_non_boot = + non_boot.by_ref().take(batch_size).collect::>(); + if some_non_boot.is_empty() { + break; + } + let _ = diesel::insert_into(dsl::inv_measurement_manifest_non_boot) + .values(some_non_boot) + .execute_async(&conn) + .await?; + } + } + + // Insert rows for non-boot mupdate overrides. { use nexus_db_schema::schema::inv_mupdate_override_non_boot::dsl; @@ -1469,6 +1581,14 @@ impl DataStore { .into_sql::>(), zone_image_resolver.zone_manifest_boot_disk_error .into_sql::>(), + zone_image_resolver.measurement_manifest_boot_disk_path + .into_sql::(), + zone_image_resolver.measurement_manifest_source + .into_sql::>(), + zone_image_resolver.measurement_manifest_mupdate_id + .into_sql::>(), + zone_image_resolver.measurement_manifest_boot_disk_error + .into_sql::>(), zone_image_resolver.mupdate_override_boot_disk_path .into_sql::(), zone_image_resolver.mupdate_override_id @@ -1510,6 +1630,10 @@ impl DataStore { sa_dsl::zone_manifest_source, sa_dsl::zone_manifest_mupdate_id, sa_dsl::zone_manifest_boot_disk_error, + sa_dsl::measurement_manifest_boot_disk_path, + sa_dsl::measurement_manifest_source, + sa_dsl::measurement_manifest_mupdate_id, + sa_dsl::measurement_manifest_boot_disk_error, sa_dsl::mupdate_override_boot_disk_path, sa_dsl::mupdate_override_id, sa_dsl::mupdate_override_boot_disk_error, @@ -1542,6 +1666,10 @@ impl DataStore { _zone_manifest_source, _zone_manifest_mupdate_id, _zone_manifest_boot_disk_error, + _measurement_manifest_boot_disk_path, + _measurement_manifest_source, + _measurement_manifest_mupdate_id, + _measurement_manifest_boot_disk_error, _mupdate_override_boot_disk_path, _mupdate_override_boot_disk_id, _mupdate_override_boot_disk_error, @@ -1873,8 +2001,11 @@ impl DataStore { nlast_reconciliation_dataset_results: usize, nlast_reconciliation_orphaned_datasets: usize, nlast_reconciliation_zone_results: usize, + nlast_reconciliation_measurements: usize, nzone_manifest_zones: usize, + nzone_manifest_measurements: usize, nzone_manifest_non_boot: usize, + nmeasurement_manifest_non_boot: usize, nmupdate_override_non_boot: usize, nconfig_reconcilers: usize, nboot_partitions: usize, @@ -1907,8 +2038,11 @@ impl DataStore { nlast_reconciliation_dataset_results, nlast_reconciliation_orphaned_datasets, nlast_reconciliation_zone_results, + nlast_reconciliation_measurements, nzone_manifest_zones, + nzone_manifest_measurements, nzone_manifest_non_boot, + nmeasurement_manifest_non_boot, nmupdate_override_non_boot, nconfig_reconcilers, nboot_partitions, @@ -2038,7 +2172,7 @@ impl DataStore { }; // Remove rows associated with the last reconciliation - // result (disks, datasets, and zones). + // result (disks, datasets, measurements, and zones). let nlast_reconciliation_disk_results = { use nexus_db_schema::schema::inv_last_reconciliation_disk_result::dsl; diesel::delete(dsl::inv_last_reconciliation_disk_result.filter( @@ -2071,6 +2205,15 @@ impl DataStore { .execute_async(&conn) .await? }; + let nlast_reconciliation_measurements = { + use nexus_db_schema::schema::inv_last_reconciliation_measurements::dsl; + diesel::delete(dsl::inv_last_reconciliation_measurements.filter( + dsl::inv_collection_id.eq(db_collection_id), + )) + .execute_async(&conn) + .await? + }; + // Remove rows associated with zone resolver inventory. let nzone_manifest_zones = { @@ -2081,6 +2224,14 @@ impl DataStore { .execute_async(&conn) .await? }; + let nzone_manifest_measurements = { + use nexus_db_schema::schema::inv_zone_manifest_measurement::dsl; + diesel::delete(dsl::inv_zone_manifest_measurement.filter( + dsl::inv_collection_id.eq(db_collection_id), + )) + .execute_async(&conn) + .await? + }; let nzone_manifest_non_boot = { use nexus_db_schema::schema::inv_zone_manifest_non_boot::dsl; diesel::delete(dsl::inv_zone_manifest_non_boot.filter( @@ -2089,6 +2240,15 @@ impl DataStore { .execute_async(&conn) .await? }; + let nmeasurement_manifest_non_boot = { + use nexus_db_schema::schema::inv_measurement_manifest_non_boot::dsl; + diesel::delete(dsl::inv_measurement_manifest_non_boot.filter( + dsl::inv_collection_id.eq(db_collection_id), + )) + .execute_async(&conn) + .await? + }; + let nmupdate_override_non_boot = { use nexus_db_schema::schema::inv_mupdate_override_non_boot::dsl; diesel::delete(dsl::inv_mupdate_override_non_boot.filter( @@ -2239,8 +2399,11 @@ impl DataStore { nlast_reconciliation_dataset_results, nlast_reconciliation_orphaned_datasets, nlast_reconciliation_zone_results, + nlast_reconciliation_measurements, nzone_manifest_zones, + nzone_manifest_measurements, nzone_manifest_non_boot, + nmeasurement_manifest_non_boot, nmupdate_override_non_boot, nconfig_reconcilers, nboot_partitions, @@ -2283,8 +2446,12 @@ impl DataStore { nlast_reconciliation_orphaned_datasets, "nlast_reconciliation_zone_results" => nlast_reconciliation_zone_results, + "nlast_reconciliation_measurements" => + nlast_reconciliation_measurements, "nzone_manifest_zones" => nzone_manifest_zones, + "nzone_manifest_measurements" => nzone_manifest_measurements, "nzone_manifest_non_boot" => nzone_manifest_non_boot, + "nmeasurement_manifest_non_boot" => nmeasurement_manifest_non_boot, "nmupdate_override_non_boot" => nmupdate_override_non_boot, "nconfig_reconcilers" => nconfig_reconcilers, "nboot_partitions" => nboot_partitions, @@ -3154,6 +3321,7 @@ impl DataStore { datasets: IdOrdMap::default(), zones: IdOrdMap::default(), host_phase_2: sled_config.host_phase_2.into(), + measurements: sled_config.measurements.into(), }, }) .map_err(|e| { @@ -3559,6 +3727,49 @@ impl DataStore { orphaned }; + let mut last_reconciliation_measurements = { + use nexus_db_schema::schema::inv_last_reconciliation_measurements::dsl; + + let mut measurements: BTreeMap< + SledUuid, + IdOrdMap, + > = BTreeMap::new(); + + // TODO-performance This ought to be paginated like the other + // queries in this method, but + // + // (a) this table's primary key is 3 columns, and we don't have + // `paginated` support that wide + // (b) we expect a very small number of reconciled measurements + // + // so we just do the lazy thing and load all the rows at once. + let rows = dsl::inv_last_reconciliation_measurements + .filter(dsl::inv_collection_id.eq(db_id)) + .select(InvLastReconciliationMeasurements::as_select()) + .load_async(&*conn) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })?; + + for row in rows { + measurements + .entry(row.sled_id.into()) + .or_default() + .insert_unique(row.into()) + .map_err(|err| { + // We should never get duplicates: the table's primary + // key is the dataset name (same as the IdOrdMap) + Error::internal_error(&format!( + "unexpected duplicate orphaned dataset: {}", + InlineErrorChain::new(&err) + )) + })?; + } + + measurements + }; + // Load all the config reconciler zone results; build a map of maps // keyed by sled ID. let mut last_reconciliation_zone_results = { @@ -3598,6 +3809,48 @@ impl DataStore { results }; + // Load zone_manifest_measurement rows. + let mut measurement_manifest_artifacts_by_sled_id = { + use nexus_db_schema::schema::inv_zone_manifest_measurement::dsl; + + let mut by_sled_id: BTreeMap< + SledUuid, + IdOrdMap, + > = BTreeMap::new(); + + let mut paginator = Paginator::new( + batch_size, + dropshot::PaginationOrder::Ascending, + ); + while let Some(p) = paginator.next() { + let batch = paginated_multicolumn( + dsl::inv_zone_manifest_measurement, + (dsl::sled_id, dsl::zone_file_name), + &p.current_pagparams(), + ) + .filter(dsl::inv_collection_id.eq(db_id)) + .select(InvZoneManifestMeasurement::as_select()) + .load_async(&*conn) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })?; + paginator = p.found_batch(&batch, &|row| { + (row.sled_id, row.zone_file_name.clone()) + }); + + for row in batch { + by_sled_id + .entry(row.sled_id.into()) + .or_default() + .insert_unique(row.into()) + .expect("database ensures the row is unique"); + } + } + + by_sled_id + }; + // Load zone_manifest_zone rows. let mut zone_manifest_artifacts_by_sled_id = { use nexus_db_schema::schema::inv_zone_manifest_zone::dsl; @@ -3682,6 +3935,47 @@ impl DataStore { by_sled_id }; + let mut measurement_manifest_non_boot_by_sled_id = { + use nexus_db_schema::schema::inv_measurement_manifest_non_boot::dsl; + + let mut by_sled_id: BTreeMap< + SledUuid, + IdOrdMap, + > = BTreeMap::new(); + + let mut paginator = Paginator::new( + batch_size, + dropshot::PaginationOrder::Ascending, + ); + while let Some(p) = paginator.next() { + let batch = paginated_multicolumn( + dsl::inv_measurement_manifest_non_boot, + (dsl::sled_id, dsl::non_boot_zpool_id), + &p.current_pagparams(), + ) + .filter(dsl::inv_collection_id.eq(db_id)) + .select(InvMeasurementManifestNonBoot::as_select()) + .load_async(&*conn) + .await + .map_err(|e| { + public_error_from_diesel(e, ErrorHandler::Server) + })?; + paginator = p.found_batch(&batch, &|row| { + (row.sled_id, row.non_boot_zpool_id) + }); + + for row in batch { + by_sled_id + .entry(row.sled_id.into()) + .or_default() + .insert_unique(row.into()) + .expect("database ensures the row is unique"); + } + } + + by_sled_id + }; + // Load mupdate-override non-boot rows. let mut mupdate_override_non_boot_by_sled_id = { use nexus_db_schema::schema::inv_mupdate_override_non_boot::dsl; @@ -3942,6 +4236,10 @@ impl DataStore { zones: last_reconciliation_zone_results .remove(&sled_id) .unwrap_or_default(), + measurements: last_reconciliation_measurements + .remove(&sled_id) + .unwrap_or_default(), + boot_partitions, remove_mupdate_override, }) @@ -3952,7 +4250,9 @@ impl DataStore { .zone_image_resolver .into_inventory( zone_manifest_artifacts_by_sled_id.remove(&sled_id), + measurement_manifest_artifacts_by_sled_id.remove(&sled_id), zone_manifest_non_boot_by_sled_id.remove(&sled_id), + measurement_manifest_non_boot_by_sled_id.remove(&sled_id), mupdate_override_non_boot_by_sled_id.remove(&sled_id), ) .map_err(|e| { @@ -4147,6 +4447,7 @@ struct ConfigReconcilerRows { boot_partitions: Vec, config_reconciler_fields_by_sled: BTreeMap, + measurements: Vec, } impl ConfigReconcilerRows { @@ -4216,6 +4517,17 @@ impl ConfigReconcilerRows { remove_mupdate_override, )); + self.measurements.extend( + last_reconciliation.measurements.iter().map(|measurement| { + InvLastReconciliationMeasurements::new( + collection_id, + sled_id, + measurement.file_name.clone(), + measurement.path.to_string(), + measurement.result.clone(), + ) + }), + ); // Boot partition _errors_ are kept in `InvSledConfigReconciler` // above, but non-errors get their own rows; handle those here. // @@ -4367,6 +4679,7 @@ impl ConfigReconcilerRows { config.generation, config.remove_mupdate_override, config.host_phase_2.clone(), + config.measurements.clone(), )); self.disks.extend(config.disks.iter().map(|disk| { InvOmicronSledConfigDisk::new( @@ -4456,6 +4769,7 @@ mod test { use async_bb8_diesel::AsyncConnection; use async_bb8_diesel::AsyncRunQueryDsl; use async_bb8_diesel::AsyncSimpleConnection; + use camino::Utf8PathBuf; use diesel::QueryDsl; use nexus_db_schema::schema; use nexus_inventory::examples::Representative; @@ -4479,6 +4793,7 @@ mod test { use sled_agent_types::inventory::BootPartitionContents; use sled_agent_types::inventory::BootPartitionDetails; use sled_agent_types::inventory::OrphanedDataset; + use sled_agent_types::inventory::ReconciledSingleMeasurement; use sled_agent_types::inventory::{ BootImageHeader, RemoveMupdateOverrideBootSuccessInventory, RemoveMupdateOverrideInventory, @@ -5313,6 +5628,15 @@ mod test { (OmicronZoneUuid::new_v4(), make_result("zone", i)) }) .collect(), + measurements: (0..5) + .map(|i| { + ReconciledSingleMeasurement { + file_name: format!("file-{}", i), + path: Utf8PathBuf::from(format!("path/to/{}", i)), + result: make_result("measurement", i), + } + }) + .collect(), boot_partitions: BootPartitionContents { boot_disk: Ok(M2Slot::B), slot_a: Err("some error".to_string()), diff --git a/nexus/db-schema/src/schema.rs b/nexus/db-schema/src/schema.rs index 0a4f2988bce..233b8791c90 100644 --- a/nexus/db-schema/src/schema.rs +++ b/nexus/db-schema/src/schema.rs @@ -1686,6 +1686,11 @@ table! { zone_manifest_mupdate_id -> Nullable, zone_manifest_boot_disk_error -> Nullable, + measurement_manifest_boot_disk_path -> Text, + measurement_manifest_source -> Nullable, + measurement_manifest_mupdate_id -> Nullable, + measurement_manifest_boot_disk_error -> Nullable, + mupdate_override_boot_disk_path -> Text, mupdate_override_id -> Nullable, mupdate_override_boot_disk_error -> Nullable, @@ -1751,6 +1756,19 @@ table! { } } +table! { + inv_last_reconciliation_measurements + (inv_collection_id, sled_id, file_name) + { + inv_collection_id -> Uuid, + sled_id -> Uuid, + + file_name -> Text, + path -> Text, + error_message -> Nullable + } +} + table! { inv_last_reconciliation_orphaned_dataset (inv_collection_id, sled_id, pool_id, kind, zone_name) @@ -1778,6 +1796,18 @@ table! { } } +table! { + inv_zone_manifest_measurement (inv_collection_id, sled_id, zone_file_name) { + inv_collection_id -> Uuid, + sled_id -> Uuid, + zone_file_name -> Text, + path -> Text, + expected_size -> Int8, + expected_sha256 -> Text, + error -> Nullable, + } +} + table! { inv_zone_manifest_zone (inv_collection_id, sled_id, zone_file_name) { inv_collection_id -> Uuid, @@ -1801,6 +1831,17 @@ table! { } } +table! { + inv_measurement_manifest_non_boot (inv_collection_id, sled_id, non_boot_zpool_id) { + inv_collection_id -> Uuid, + sled_id -> Uuid, + non_boot_zpool_id -> Uuid, + path -> Text, + is_valid -> Bool, + message -> Text, + } +} + table! { inv_mupdate_override_non_boot (inv_collection_id, sled_id, non_boot_zpool_id) { inv_collection_id -> Uuid, @@ -1874,6 +1915,7 @@ table! { remove_mupdate_override -> Nullable, host_phase_2_desired_slot_a -> Nullable, host_phase_2_desired_slot_b -> Nullable, + measurements -> Nullable>, } } @@ -2094,6 +2136,19 @@ table! { } } +table! { + bp_single_measurements (blueprint_id, id) { + blueprint_id -> Uuid, + sled_id -> Uuid, + id -> Uuid, + + image_artifact_sha256 -> Nullable, + prune -> Bool, + } +} + +allow_tables_to_appear_in_same_query!(bp_single_measurements, tuf_artifact); + table! { bp_omicron_zone (blueprint_id, id) { blueprint_id -> Uuid, @@ -2486,6 +2541,7 @@ allow_tables_to_appear_in_same_query!( affinity_group_instance_membership, bp_omicron_zone, bp_target, + bp_single_measurements, rendezvous_debug_dataset, crucible_dataset, disk, diff --git a/nexus/inventory/src/collector.rs b/nexus/inventory/src/collector.rs index 560f9021c58..a9782c445c5 100644 --- a/nexus/inventory/src/collector.rs +++ b/nexus/inventory/src/collector.rs @@ -731,6 +731,8 @@ mod test { use omicron_uuid_kinds::ZpoolUuid; use sled_agent_types::inventory::ConfigReconcilerInventoryStatus; use sled_agent_types::inventory::HostPhase2DesiredSlots; + use sled_agent_types::inventory::OmicronMeasurementSetDesiredContents; + use sled_agent_types::inventory::OmicronMeasurements; use sled_agent_types::inventory::OmicronSledConfig; use sled_agent_types::inventory::OmicronZoneConfig; use sled_agent_types::inventory::OmicronZoneImageSource; @@ -753,6 +755,7 @@ mod test { zones, remove_mupdate_override, host_phase_2, + measurements, } = config; swriteln!(s, " generation: {generation}"); @@ -791,6 +794,17 @@ mod test { zone.zone_type.kind().report_str(), ); } + swriteln!(s, " measurements:"); + match &measurements.measurements { + OmicronMeasurementSetDesiredContents::InstallDataset => { + swriteln!(s, " install dataset"); + } + OmicronMeasurementSetDesiredContents::Artifacts { hashes } => { + for h in hashes { + swriteln!(s, " artifact: {h}"); + } + } + } } fn dump_collection(collection: &Collection) -> String { @@ -1004,6 +1018,7 @@ mod test { }, remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), }) .await .expect("failed to write initial zone version to fake sled agent"); diff --git a/nexus/inventory/src/examples.rs b/nexus/inventory/src/examples.rs index 9ad78cfbe13..780ca196245 100644 --- a/nexus/inventory/src/examples.rs +++ b/nexus/inventory/src/examples.rs @@ -7,6 +7,7 @@ use crate::CollectionBuilder; use crate::now_db_precision; use camino::Utf8Path; +use camino::Utf8PathBuf; use clickhouse_admin_types::ClickhouseKeeperClusterMembership; use clickhouse_admin_types::KeeperId; use gateway_client::types::PowerState; @@ -40,15 +41,18 @@ use sled_agent_types::inventory::Baseboard; use sled_agent_types::inventory::BootImageHeader; use sled_agent_types::inventory::BootPartitionDetails; use sled_agent_types::inventory::ConfigReconcilerInventory; +use sled_agent_types::inventory::ConfigReconcilerInventoryResult; use sled_agent_types::inventory::ConfigReconcilerInventoryStatus; use sled_agent_types::inventory::HostPhase2DesiredSlots; use sled_agent_types::inventory::Inventory; use sled_agent_types::inventory::InventoryDataset; use sled_agent_types::inventory::InventoryDisk; use sled_agent_types::inventory::InventoryZpool; +use sled_agent_types::inventory::OmicronMeasurements; use sled_agent_types::inventory::OmicronSledConfig; use sled_agent_types::inventory::OmicronZonesConfig; use sled_agent_types::inventory::OrphanedDataset; +use sled_agent_types::inventory::ReconciledSingleMeasurement; use sled_agent_types::inventory::SledCpuFamily; use sled_agent_types::inventory::SledRole; use sled_agent_types::inventory::ZoneImageResolverInventory; @@ -402,6 +406,7 @@ pub fn representative() -> Representative { zones: sled14.zones.into_iter().collect(), remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), }; let sled16 = OmicronSledConfig { generation: sled16.generation, @@ -410,6 +415,7 @@ pub fn representative() -> Representative { zones: sled16.zones.into_iter().collect(), remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), }; let sled17 = OmicronSledConfig { generation: sled17.generation, @@ -418,6 +424,7 @@ pub fn representative() -> Representative { zones: sled17.zones.into_iter().collect(), remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), }; // Create iterator producing fixed IDs. @@ -795,52 +802,70 @@ pub fn zone_image_resolver( // Determine the zone manifest and mupdate override results for the boot // disk. - let (boot_zm_result, boot_override_result) = match kind { - ZoneImageResolverExampleKind::Success { - deserialized_zone_manifest, - has_mupdate_override, - } => { - if !deserialized_zone_manifest { - cx.write_zone_manifest_to_disk(false); + let (measurement_m_result, boot_zm_result, boot_override_result) = + match kind { + ZoneImageResolverExampleKind::Success { + deserialized_zone_manifest, + has_mupdate_override, + } => { + if !deserialized_zone_manifest { + cx.write_zone_manifest_to_disk(false); + } + let zm_result = Ok(cx.expected_result( + &dir_path.join(&BOOT_PATHS.install_dataset), + )); + let measurement_m_result = Ok(cx.expected_result( + &dir_path.join(&BOOT_PATHS.install_dataset), + )); + let override_result = + Ok(has_mupdate_override.then(|| cx.override_info())); + (measurement_m_result, zm_result, override_result) } - let zm_result = Ok( - cx.expected_result(&dir_path.join(&BOOT_PATHS.install_dataset)) - ); - let override_result = - Ok(has_mupdate_override.then(|| cx.override_info())); - (zm_result, override_result) - } - ZoneImageResolverExampleKind::Mismatch { has_mupdate_override } => { - // In this case, the zone manifest result is generated using the - // invalid (mismatched) context. - let zm_result = Ok(invalid_cx - .expected_result(&dir_path.join(&BOOT_PATHS.install_dataset))); - let override_result = - Ok(has_mupdate_override.then(|| cx.override_info())); - (zm_result, override_result) - } - ZoneImageResolverExampleKind::Error => { - // Use the invalid context to generate an error. - let zm_result = Err(ZoneManifestReadError::InstallMetadata( - dataset_missing_error( + ZoneImageResolverExampleKind::Mismatch { has_mupdate_override } => { + // In this case, the zone manifest result is generated using the + // invalid (mismatched) context. + let zm_result = Ok(invalid_cx.expected_result( &dir_path.join(&BOOT_PATHS.install_dataset), - ), - )); - let override_result = - Err(MupdateOverrideReadError::InstallMetadata( + )); + + let measurement_m_result = Ok(invalid_cx.expected_result( + &dir_path.join(&BOOT_PATHS.install_dataset), + )); + + let override_result = + Ok(has_mupdate_override.then(|| cx.override_info())); + (measurement_m_result, zm_result, override_result) + } + ZoneImageResolverExampleKind::Error => { + // Use the invalid context to generate an error. + let zm_result = Err(ZoneManifestReadError::InstallMetadata( dataset_missing_error( &dir_path.join(&BOOT_PATHS.install_dataset), ), )); - (zm_result, override_result) - } - }; + // Use the invalid context to generate an error. + let measurement_m_result = + Err(ZoneManifestReadError::InstallMetadata( + dataset_missing_error( + &dir_path.join(&BOOT_PATHS.install_dataset), + ), + )); + + let override_result = + Err(MupdateOverrideReadError::InstallMetadata( + dataset_missing_error( + &dir_path.join(&BOOT_PATHS.install_dataset), + ), + )); + (measurement_m_result, zm_result, override_result) + } + }; // Generate a status struct first. let status = ResolverStatus { measurement_manifest: MeasurementManifestStatus { boot_disk_path: dir_path.join(&BOOT_PATHS.measurements_json), - boot_disk_result: boot_zm_result.clone(), + boot_disk_result: measurement_m_result, non_boot_disk_metadata: id_ord_map! { // Non-boot disk metadata that matches. ZoneManifestNonBootInfo { @@ -857,7 +882,7 @@ pub fn zone_image_resolver( ) ), }, - // Non-boot disk mismatch (measurements different + errors). + // Non-boot disk mismatch (zones different + errors). ZoneManifestNonBootInfo { zpool_id: NON_BOOT_2_UUID, dataset_dir: dir_path.join(&NON_BOOT_2_PATHS.install_dataset), @@ -870,7 +895,7 @@ pub fn zone_image_resolver( }, ), }, - // Non-boot disk mismatch (error reading measurement manifest). + // Non-boot disk mismatch (error reading zone manifest). ZoneManifestNonBootInfo { zpool_id: NON_BOOT_3_UUID, dataset_dir: dir_path.join(&NON_BOOT_3_PATHS.install_dataset), @@ -883,6 +908,7 @@ pub fn zone_image_resolver( }, }, }, + zone_manifest: ZoneManifestStatus { boot_disk_path: dir_path.join(&BOOT_PATHS.zones_json), boot_disk_result: boot_zm_result, @@ -1012,6 +1038,17 @@ pub fn sled_agent( artifact_size: 10_000 + 4096, }); + inv.measurements.insert_overwrite(ReconciledSingleMeasurement { + file_name: "file1".to_string(), + path: Utf8PathBuf::from("/this/path"), + result: ConfigReconcilerInventoryResult::Ok, + }); + inv.measurements.insert_overwrite(ReconciledSingleMeasurement { + file_name: "file2".to_string(), + path: Utf8PathBuf::from("/this/path2"), + result: ConfigReconcilerInventoryResult::Ok, + }); + inv }); diff --git a/nexus/inventory/tests/output/collector_basic.txt b/nexus/inventory/tests/output/collector_basic.txt index 1d9f9830ab6..3b91412e684 100644 --- a/nexus/inventory/tests/output/collector_basic.txt +++ b/nexus/inventory/tests/output/collector_basic.txt @@ -91,12 +91,16 @@ sled agents found: host_phase_2.slot_a: CurrentContents host_phase_2.slot_b: CurrentContents zone 8b88a56f-3eb6-4d80-ba42-75d867bc427d type oximeter + measurements: + install dataset last reconciled config: generation: 3 remove_mupdate_override: None host_phase_2.slot_a: CurrentContents host_phase_2.slot_b: CurrentContents zone 8b88a56f-3eb6-4d80-ba42-75d867bc427d type oximeter + measurements: + install dataset result for zone 8b88a56f-3eb6-4d80-ba42-75d867bc427d: Ok reconciler task idle sled 9cb9b78f-5614-440c-b66d-e8e81fab69b0 (Scrimlet) @@ -107,12 +111,16 @@ sled agents found: host_phase_2.slot_a: CurrentContents host_phase_2.slot_b: CurrentContents zone 5125277f-0988-490b-ac01-3bba20cc8f07 type oximeter + measurements: + install dataset last reconciled config: generation: 3 remove_mupdate_override: None host_phase_2.slot_a: CurrentContents host_phase_2.slot_b: CurrentContents zone 5125277f-0988-490b-ac01-3bba20cc8f07 type oximeter + measurements: + install dataset result for zone 5125277f-0988-490b-ac01-3bba20cc8f07: Ok reconciler task idle diff --git a/nexus/inventory/tests/output/collector_sled_agent_errors.txt b/nexus/inventory/tests/output/collector_sled_agent_errors.txt index c3772599c7e..3b2896ba539 100644 --- a/nexus/inventory/tests/output/collector_sled_agent_errors.txt +++ b/nexus/inventory/tests/output/collector_sled_agent_errors.txt @@ -90,12 +90,16 @@ sled agents found: host_phase_2.slot_a: CurrentContents host_phase_2.slot_b: CurrentContents zone 5125277f-0988-490b-ac01-3bba20cc8f07 type oximeter + measurements: + install dataset last reconciled config: generation: 3 remove_mupdate_override: None host_phase_2.slot_a: CurrentContents host_phase_2.slot_b: CurrentContents zone 5125277f-0988-490b-ac01-3bba20cc8f07 type oximeter + measurements: + install dataset result for zone 5125277f-0988-490b-ac01-3bba20cc8f07: Ok reconciler task idle diff --git a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs index 161dc23eeff..ebb4da17b94 100644 --- a/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs +++ b/nexus/mgs-updates/src/test_util/host_phase_2_test_state.rs @@ -177,6 +177,7 @@ impl HostPhase2SledAgentContext { struct HostPhase2SledAgentImpl; mod api_impl { + use super::HostPhase2SledAgentContext; use super::HostPhase2SledAgentImpl; use camino::Utf8PathBuf; @@ -245,6 +246,7 @@ mod api_impl { use sled_agent_types::inventory::Inventory; use sled_agent_types::inventory::ManifestInventory; use sled_agent_types::inventory::MupdateOverrideInventory; + use sled_agent_types::inventory::OmicronMeasurements; use sled_agent_types::inventory::OmicronSledConfig; use sled_agent_types::inventory::SledCpuFamily; use sled_agent_types::inventory::SledRole; @@ -339,6 +341,7 @@ mod api_impl { slot_a: HostPhase2DesiredContents::CurrentContents, slot_b: HostPhase2DesiredContents::CurrentContents, }, + measurements: OmicronMeasurements::measurements_defaults(), }; Ok(HttpResponseOk(Inventory { @@ -364,6 +367,7 @@ mod api_impl { datasets: BTreeMap::new(), orphaned_datasets: IdOrdMap::new(), zones: BTreeMap::new(), + measurements: IdOrdMap::new(), remove_mupdate_override: None, boot_partitions, }), @@ -376,6 +380,14 @@ mod api_impl { ), non_boot_status: IdOrdMap::new(), }, + measurement_manifest: ManifestInventory { + boot_disk_path: Utf8PathBuf::new(), + boot_inventory: Err( + "not implemented by HostPhase2SledAgentImpl" + .to_string(), + ), + non_boot_status: IdOrdMap::new(), + }, mupdate_override: MupdateOverrideInventory { boot_disk_path: Utf8PathBuf::new(), boot_override: Err( diff --git a/nexus/reconfigurator/execution/src/database.rs b/nexus/reconfigurator/execution/src/database.rs index df96d8bab3b..85d99b37c98 100644 --- a/nexus/reconfigurator/execution/src/database.rs +++ b/nexus/reconfigurator/execution/src/database.rs @@ -71,6 +71,7 @@ mod test { use nexus_inventory::now_db_precision; use nexus_types::deployment::Blueprint; use nexus_types::deployment::BlueprintHostPhase2DesiredSlots; + use nexus_types::deployment::BlueprintMeasurementsDesiredContents; use nexus_types::deployment::BlueprintSledConfig; use nexus_types::deployment::BlueprintSource; use nexus_types::deployment::BlueprintTarget; @@ -172,6 +173,8 @@ mod test { remove_mupdate_override: None, host_phase_2: BlueprintHostPhase2DesiredSlots::current_contents( ), + measurements: + BlueprintMeasurementsDesiredContents::default_contents(), }, ); diff --git a/nexus/reconfigurator/execution/src/dns.rs b/nexus/reconfigurator/execution/src/dns.rs index 202188c2621..356352909b9 100644 --- a/nexus/reconfigurator/execution/src/dns.rs +++ b/nexus/reconfigurator/execution/src/dns.rs @@ -338,6 +338,7 @@ mod test { use nexus_test_utils_macros::nexus_test; use nexus_types::deployment::Blueprint; use nexus_types::deployment::BlueprintHostPhase2DesiredSlots; + use nexus_types::deployment::BlueprintMeasurementsDesiredContents; use nexus_types::deployment::BlueprintSledConfig; use nexus_types::deployment::BlueprintSource; use nexus_types::deployment::BlueprintTarget; @@ -702,6 +703,7 @@ mod test { remove_mupdate_override: None, host_phase_2: BlueprintHostPhase2DesiredSlots::current_contents(), + measurements: BlueprintMeasurementsDesiredContents::default_contents(), }, ); } diff --git a/nexus/reconfigurator/execution/src/omicron_sled_config.rs b/nexus/reconfigurator/execution/src/omicron_sled_config.rs index 653302dc9d1..5a9450d6957 100644 --- a/nexus/reconfigurator/execution/src/omicron_sled_config.rs +++ b/nexus/reconfigurator/execution/src/omicron_sled_config.rs @@ -85,6 +85,7 @@ mod tests { use nexus_types::deployment::BlueprintDatasetConfig; use nexus_types::deployment::BlueprintDatasetDisposition; use nexus_types::deployment::BlueprintHostPhase2DesiredSlots; + use nexus_types::deployment::BlueprintMeasurementsDesiredContents; use nexus_types::deployment::BlueprintPhysicalDiskConfig; use nexus_types::deployment::BlueprintPhysicalDiskDisposition; use nexus_types::deployment::BlueprintZoneConfig; @@ -268,6 +269,8 @@ mod tests { zones, remove_mupdate_override: None, host_phase_2: BlueprintHostPhase2DesiredSlots::current_contents(), + measurements: + BlueprintMeasurementsDesiredContents::default_contents(), }; let sled_configs = [(sim_sled_agent.id, sled_config.clone())].into_iter().collect(); diff --git a/nexus/reconfigurator/planning/src/blueprint_editor/sled_editor.rs b/nexus/reconfigurator/planning/src/blueprint_editor/sled_editor.rs index 6c11d42d8d3..3628edc9d48 100644 --- a/nexus/reconfigurator/planning/src/blueprint_editor/sled_editor.rs +++ b/nexus/reconfigurator/planning/src/blueprint_editor/sled_editor.rs @@ -24,6 +24,7 @@ use nexus_types::deployment::BlueprintDatasetConfig; use nexus_types::deployment::BlueprintDatasetDisposition; use nexus_types::deployment::BlueprintHostPhase2DesiredContents; use nexus_types::deployment::BlueprintHostPhase2DesiredSlots; +use nexus_types::deployment::BlueprintMeasurementsDesiredContents; use nexus_types::deployment::BlueprintPhysicalDiskConfig; use nexus_types::deployment::BlueprintPhysicalDiskDisposition; use nexus_types::deployment::BlueprintSledConfig; @@ -587,6 +588,9 @@ impl ActiveSledEditor { .remove_mupdate_override .finalize(), host_phase_2: self.host_phase_2.finalize(), + // TODO this will come in a subsequent PR + measurements: + BlueprintMeasurementsDesiredContents::default_contents(), }, edit_counts: SledEditCounts { disks: disks_counts, diff --git a/nexus/reconfigurator/planning/src/mgs_updates/test_helpers.rs b/nexus/reconfigurator/planning/src/mgs_updates/test_helpers.rs index c934e51ab2b..cf2b46db2f4 100644 --- a/nexus/reconfigurator/planning/src/mgs_updates/test_helpers.rs +++ b/nexus/reconfigurator/planning/src/mgs_updates/test_helpers.rs @@ -43,6 +43,7 @@ use sled_agent_types::inventory::ConfigReconcilerInventory; use sled_agent_types::inventory::ConfigReconcilerInventoryStatus; use sled_agent_types::inventory::HostPhase2DesiredSlots; use sled_agent_types::inventory::Inventory; +use sled_agent_types::inventory::OmicronMeasurements; use sled_agent_types::inventory::OmicronSledConfig; use sled_agent_types::inventory::SledCpuFamily; use sled_agent_types::inventory::SledRole; @@ -1301,6 +1302,7 @@ impl<'a> TestBoardCollectionBuilder<'a> { zones: IdOrdMap::new(), remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), }; // The only sled-agent fields that matter for the purposes of @@ -1350,6 +1352,7 @@ impl<'a> TestBoardCollectionBuilder<'a> { datasets: BTreeMap::new(), orphaned_datasets: IdOrdMap::new(), zones: BTreeMap::new(), + measurements: IdOrdMap::new(), boot_partitions, remove_mupdate_override: None, }, diff --git a/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt b/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt index 585009ab08c..9731abd4596 100644 --- a/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt +++ b/nexus/reconfigurator/planning/tests/output/example_builder_zone_counts_blueprint.txt @@ -14,6 +14,12 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -136,6 +142,12 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -253,6 +265,12 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -367,6 +385,12 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -481,6 +505,12 @@ parent: e35b2fdd-354d-48d9-acb5-703b2c269a54 B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt b/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt index aba7d87fb37..67802cb2694 100644 --- a/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt +++ b/nexus/reconfigurator/planning/tests/output/planner_decommissions_sleds_bp2.txt @@ -14,6 +14,12 @@ parent: 516e80a3-b362-4fac-bd3c-4559717120dd B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------- vendor model serial disposition @@ -128,6 +134,12 @@ parent: 516e80a3-b362-4fac-bd3c-4559717120dd B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -243,6 +255,12 @@ parent: 516e80a3-b362-4fac-bd3c-4559717120dd B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt index 109098acfa0..9e1ac880c3c 100644 --- a/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt +++ b/nexus/reconfigurator/planning/tests/output/planner_nonprovisionable_bp2.txt @@ -14,6 +14,12 @@ parent: 2c4d059d-6498-49ca-8052-6e696a675f6e B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -128,6 +134,12 @@ parent: 2c4d059d-6498-49ca-8052-6e696a675f6e B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------- vendor model serial disposition @@ -239,6 +251,12 @@ parent: 2c4d059d-6498-49ca-8052-6e696a675f6e B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------- vendor model serial disposition @@ -350,6 +368,12 @@ parent: 2c4d059d-6498-49ca-8052-6e696a675f6e B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition @@ -462,6 +486,12 @@ parent: 2c4d059d-6498-49ca-8052-6e696a675f6e B current contents + measurements: + ---------------------- + hash version prune + ---------------------- + + physical disks: ------------------------------------------------------------------------------------ vendor model serial disposition diff --git a/nexus/src/app/background/tasks/blueprint_execution.rs b/nexus/src/app/background/tasks/blueprint_execution.rs index be6c31a889c..775e35a589b 100644 --- a/nexus/src/app/background/tasks/blueprint_execution.rs +++ b/nexus/src/app/background/tasks/blueprint_execution.rs @@ -235,6 +235,7 @@ mod test { use nexus_db_queries::context::OpContext; use nexus_db_queries::db::DataStore; use nexus_test_utils_macros::nexus_test; + use nexus_types::deployment::BlueprintMeasurementsDesiredContents; use nexus_types::deployment::execution::{ EventBuffer, EventReport, ExecutionComponent, ExecutionStepId, StepOutcome, StepStatus, @@ -292,6 +293,7 @@ mod test { remove_mupdate_override: None, host_phase_2: BlueprintHostPhase2DesiredSlots::current_contents(), + measurements: BlueprintMeasurementsDesiredContents::default_contents(), }, ) }) diff --git a/nexus/test-utils/src/starter.rs b/nexus/test-utils/src/starter.rs index d045fe79671..d9d1ef24c46 100644 --- a/nexus/test-utils/src/starter.rs +++ b/nexus/test-utils/src/starter.rs @@ -36,6 +36,7 @@ use nexus_types::deployment::Blueprint; use nexus_types::deployment::BlueprintDatasetConfig; use nexus_types::deployment::BlueprintDatasetDisposition; use nexus_types::deployment::BlueprintHostPhase2DesiredSlots; +use nexus_types::deployment::BlueprintMeasurementsDesiredContents; use nexus_types::deployment::BlueprintPhysicalDiskConfig; use nexus_types::deployment::BlueprintPhysicalDiskDisposition; use nexus_types::deployment::BlueprintSledConfig; @@ -94,6 +95,7 @@ use sled_agent_client::types::EarlyNetworkConfig; use sled_agent_client::types::EarlyNetworkConfigBody; use sled_agent_client::types::RackNetworkConfigV2; use sled_agent_types::inventory::HostPhase2DesiredSlots; +use sled_agent_types::inventory::OmicronMeasurements; use sled_agent_types::inventory::OmicronSledConfig; use sled_agent_types::inventory::OmicronZoneDataset; use sled_agent_types::inventory::SledCpuFamily; @@ -977,6 +979,7 @@ impl<'a, N: NexusServer> ControlPlaneStarter<'a, N> { zones, remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), }) .await .expect("Failed to configure sled agent {sled_id} with zones"); @@ -1396,6 +1399,7 @@ impl<'a, N: NexusServer> ControlPlaneStarter<'a, N> { remove_mupdate_override: None, host_phase_2: BlueprintHostPhase2DesiredSlots::current_contents(), + measurements: BlueprintMeasurementsDesiredContents::default_contents(), }, ); } diff --git a/nexus/types/src/deployment.rs b/nexus/types/src/deployment.rs index 080b1f3c883..7a39aca6782 100644 --- a/nexus/types/src/deployment.rs +++ b/nexus/types/src/deployment.rs @@ -20,6 +20,7 @@ pub use crate::inventory::ZpoolName; use blueprint_diff::ClickhouseClusterConfigDiffTablesForSingleBlueprint; use blueprint_display::BpDatasetsTableSchema; use blueprint_display::BpHostPhase2TableSchema; +use blueprint_display::BpMeasurementsTableSchema; use blueprint_display::BpTableColumn; use daft::Diffable; use gateway_types::component::SpType; @@ -53,6 +54,8 @@ use serde::Deserialize; use serde::Serialize; use sled_agent_types_versions::latest::inventory::HostPhase2DesiredContents; use sled_agent_types_versions::latest::inventory::HostPhase2DesiredSlots; +use sled_agent_types_versions::latest::inventory::OmicronMeasurementSetDesiredContents; +use sled_agent_types_versions::latest::inventory::OmicronMeasurements; use sled_agent_types_versions::latest::inventory::OmicronSledConfig; use sled_agent_types_versions::latest::inventory::OmicronZoneConfig; use sled_agent_types_versions::latest::inventory::OmicronZoneImageSource; @@ -85,6 +88,7 @@ use crate::inventory::BaseboardId; use anyhow::anyhow; use anyhow::bail; pub use blueprint_diff::BlueprintDiffSummary; +pub use blueprint_diff::BpDiffMeasurements; use blueprint_display::BpPendingMgsUpdates; pub use clickhouse::ClickhouseClusterConfig; use gateway_types::rot::RotSlot; @@ -695,6 +699,79 @@ impl BpTableData for BlueprintHostPhase2TableData<'_> { } } +/// Wrapper to display a table of a `BlueprintSledConfig`'s measurements. +#[derive(Clone, Debug)] +struct BlueprintMeasurementsTableData<'a> { + measurements: &'a BlueprintMeasurementsDesiredContents, +} + +impl<'a> BlueprintMeasurementsTableData<'a> { + fn new(measurements: &'a BlueprintMeasurementsDesiredContents) -> Self { + Self { measurements } + } + + fn diff_rows<'b>( + diffs: &'b BlueprintMeasurementsDesiredContentsDiff<'_>, + ) -> impl Iterator + 'b { + let mut rows = vec![]; + if !diffs.measurements.removed.is_empty() { + for entry in diffs.measurements.removed.iter() { + rows.push(BpTableRow::from_strings( + BpDiffState::Removed, + vec![ + entry.hash.to_string(), + entry.version.to_string(), + entry.prune.to_string(), + ], + )); + } + } + + if !diffs.measurements.added.is_empty() { + for entry in diffs.measurements.added.iter() { + rows.push(BpTableRow::from_strings( + BpDiffState::Added, + vec![ + entry.hash.to_string(), + entry.version.to_string(), + entry.prune.to_string(), + ], + )); + } + } + + if !diffs.measurements.common.is_empty() { + for entry in diffs.measurements.common.iter() { + rows.push(BpTableRow::from_strings( + BpDiffState::Unchanged, + vec![ + entry.hash.to_string(), + entry.version.to_string(), + entry.prune.to_string(), + ], + )); + } + } + + rows.into_iter() + } +} + +impl BpTableData for BlueprintMeasurementsTableData<'_> { + fn rows(&self, state: BpDiffState) -> impl Iterator { + self.measurements.measurements.iter().map(move |d| { + BpTableRow::from_strings( + state, + vec![ + d.hash.to_string(), + d.version.to_string(), + d.prune.to_string(), + ], + ) + }) + } +} + /// Wrapper to display a table of a `BlueprintSledConfig`'s disks. #[derive(Clone, Debug)] struct BlueprintPhysicalDisksTableData<'a> { @@ -948,6 +1025,7 @@ impl fmt::Display for BlueprintDisplay<'_> { zones, remove_mupdate_override, host_phase_2, + measurements, } = config; // Report toplevel sled info @@ -973,6 +1051,16 @@ impl fmt::Display for BlueprintDisplay<'_> { ); writeln!(f, "{host_phase_2_table}\n")?; + // Construct the desired host phase 2 contents table + let measurements_table = BpTable::new( + BpMeasurementsTableSchema {}, + None, + BlueprintMeasurementsTableData::new(measurements) + .rows(BpDiffState::Unchanged) + .collect(), + ); + writeln!(f, "{measurements_table}\n")?; + // Construct the disks subtable let disks_table = BpTable::new( BpPhysicalDisksTableSchema {}, @@ -1071,6 +1159,7 @@ pub struct BlueprintSledConfig { pub zones: IdOrdMap, pub remove_mupdate_override: Option, pub host_phase_2: BlueprintHostPhase2DesiredSlots, + pub measurements: BlueprintMeasurementsDesiredContents, } impl BlueprintSledConfig { @@ -1117,6 +1206,7 @@ impl BlueprintSledConfig { .collect(), remove_mupdate_override: self.remove_mupdate_override, host_phase_2: self.host_phase_2.into(), + measurements: self.measurements.into(), } } @@ -1480,6 +1570,184 @@ impl fmt::Display for BlueprintArtifactVersion { } } +#[derive( + Clone, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + JsonSchema, + Deserialize, + Serialize, + Diffable, +)] +pub struct BlueprintSingleMeasurement { + pub version: BlueprintArtifactVersion, + pub hash: ArtifactHash, + pub prune: bool, +} + +impl Display for BlueprintSingleMeasurement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} prune: {}", self.hash, self.prune) + } +} +/// Where the measurement source is located +/// +/// This is the blueprint version of [`OmicronMeasurementSetDesiredContents`]. +#[derive( + Clone, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + JsonSchema, + Deserialize, + Serialize, + Diffable, +)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum BlueprintMeasurementSetDesiredContents { + /// This measurement source is whatever happens to be on the sled's + /// "install" dataset. + /// + /// This is whatever was put in place at the factory or by the latest + /// MUPdate. The image used here can vary by sled and even over time (if the + /// sled gets MUPdated again). + /// + /// We expect this to only be used for first install and in emergency + /// MUPdate recovery. + InstallDataset, + + /// This measurement source is the artifact matching this hash from the TUF + /// artifact store (aka "TUF repo depot"). + /// + /// This originates from TUF repos uploaded to Nexus which are then + /// replicated out to all sleds. + #[serde(rename_all = "snake_case")] + Artifacts { artifacts: BTreeSet }, +} + +impl From + for BTreeSet +{ + fn from(value: BlueprintMeasurementSetDesiredContents) -> Self { + match value { + BlueprintMeasurementSetDesiredContents::InstallDataset => { + Self::new() + } + BlueprintMeasurementSetDesiredContents::Artifacts { + artifacts, + .. + } => artifacts, + } + } +} + +impl From> + for BlueprintMeasurementSetDesiredContents +{ + fn from(value: BTreeSet) -> Self { + if value.is_empty() { + Self::InstallDataset + } else { + Self::Artifacts { artifacts: value } + } + } +} + +impl From + for OmicronMeasurementSetDesiredContents +{ + fn from(value: BlueprintMeasurementSetDesiredContents) -> Self { + match value { + BlueprintMeasurementSetDesiredContents::InstallDataset => { + Self::InstallDataset + } + BlueprintMeasurementSetDesiredContents::Artifacts { + artifacts, + .. + } => Self::Artifacts { + hashes: artifacts.into_iter().map(|x| x.hash).collect(), + }, + } + } +} + +impl fmt::Display for BlueprintMeasurementSetDesiredContents { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InstallDataset => { + write!(f, "install dataset") + } + Self::Artifacts { artifacts } => { + for a in artifacts { + write!(f, "artifact: {}", a.version)?; + } + Ok(()) + } + } + } +} +#[derive( + Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq, Diffable, +)] +#[serde(rename_all = "snake_case")] +pub struct BlueprintMeasurementsDesiredContents { + pub measurements: BTreeSet, +} + +impl From for OmicronMeasurements { + fn from(value: BlueprintMeasurementsDesiredContents) -> Self { + Self { + measurements: { + if value.measurements.is_empty() { + OmicronMeasurementSetDesiredContents::InstallDataset + } else { + OmicronMeasurementSetDesiredContents::Artifacts { + hashes: value + .measurements + .into_iter() + .map(|x| x.hash) + .collect(), + } + } + }, + } + } +} + +impl BlueprintMeasurementsDesiredContents { + pub fn default_contents() -> Self { + Self { measurements: BTreeSet::new() } + } + + pub fn append_measurement(&mut self, single: BlueprintSingleMeasurement) { + self.measurements.insert(single); + } +} + +impl From<&BlueprintMeasurementSetDesiredContents> + for BlueprintMeasurementsDesiredContents +{ + fn from(value: &BlueprintMeasurementSetDesiredContents) -> Self { + Self { + measurements: match value { + BlueprintMeasurementSetDesiredContents::InstallDataset => { + BTreeSet::new() + } + BlueprintMeasurementSetDesiredContents::Artifacts { + artifacts, + } => artifacts.clone(), + }, + } + } +} + /// Describes the desired contents for both host phase 2 slots. /// /// This is the blueprint version of [`HostPhase2DesiredSlots`]. diff --git a/nexus/types/src/deployment/blueprint_diff.rs b/nexus/types/src/deployment/blueprint_diff.rs index b2966e18569..12df6fdb2a8 100644 --- a/nexus/types/src/deployment/blueprint_diff.rs +++ b/nexus/types/src/deployment/blueprint_diff.rs @@ -6,17 +6,19 @@ use super::blueprint_display::{ BpClickhouseServersTableSchema, BpDatasetsTableSchema, BpDiffState, - BpGeneration, BpHostPhase2TableSchema, BpOmicronZonesTableSchema, - BpPendingMgsUpdates, BpPhysicalDisksTableSchema, BpTable, BpTableColumn, - BpTableData, BpTableRow, KvList, KvPair, constants::*, - linear_table_modified, linear_table_unchanged, + BpGeneration, BpHostPhase2TableSchema, BpMeasurementsTableSchema, + BpOmicronZonesTableSchema, BpPendingMgsUpdates, BpPhysicalDisksTableSchema, + BpTable, BpTableColumn, BpTableData, BpTableRow, KvList, KvPair, + constants::*, linear_table_modified, linear_table_unchanged, }; use super::{ BlueprintDatasetConfigDiff, BlueprintDatasetDisposition, BlueprintDiff, BlueprintHostPhase2DesiredSlots, BlueprintHostPhase2DesiredSlotsDiff, - BlueprintHostPhase2TableData, BlueprintMetadata, - BlueprintPhysicalDiskConfig, BlueprintPhysicalDiskConfigDiff, - BlueprintZoneConfigDiff, BlueprintZoneImageSource, ClickhouseClusterConfig, + BlueprintHostPhase2TableData, BlueprintMeasurementsDesiredContents, + BlueprintMeasurementsDesiredContentsDiff, BlueprintMeasurementsTableData, + BlueprintMetadata, BlueprintPhysicalDiskConfig, + BlueprintPhysicalDiskConfigDiff, BlueprintZoneConfigDiff, + BlueprintZoneImageSource, ClickhouseClusterConfig, CockroachDbPreserveDowngrade, PendingMgsUpdatesDiff, unwrap_or_none, zone_sort_key, }; @@ -221,6 +223,30 @@ impl<'a> BlueprintDiffSummary<'a> { .fold(0, |acc, c| acc + c.datasets.modified().count()) } + pub fn total_measurements_added(&self) -> usize { + self.diff + .sleds + .added + .values() + .fold(0, |acc, c| acc + c.measurements.measurements.len()) + + self + .diff + .sleds + .modified_values_diff() + .fold(0, |acc, c| acc + c.measurements.measurements.added.len()) + } + + pub fn total_measurements_removed(&self) -> usize { + self.diff + .sleds + .removed + .values() + .fold(0, |acc, c| acc + c.measurements.measurements.len()) + + self.diff.sleds.modified_values_diff().fold(0, |acc, c| { + acc + c.measurements.measurements.removed.len() + }) + } + /// Iterate over all added zones on a sled pub fn added_zones(&self, sled_id: &SledUuid) -> Option { // First check if the sled is added @@ -1584,6 +1610,65 @@ impl ClickhouseClusterConfigDiffTables { } } +/// Differences in measurements +#[derive(Debug)] +pub struct BpDiffMeasurements<'a> { + pub added: BTreeMap, + pub common: + BTreeMap>, + pub removed: BTreeMap, +} + +impl<'a> BpDiffMeasurements<'a> { + /// Convert from our diff summary to our display compatibility layer + pub fn from_diff_summary(summary: &BlueprintDiffSummary<'a>) -> Self { + let sleds = &summary.diff.sleds; + Self { + added: sleds + .added + .iter() + .map(|(sled_id, config)| (**sled_id, &config.measurements)) + .collect(), + common: sleds + .common + .iter() + .map(|(sled_id, config)| { + (**sled_id, config.diff_pair().measurements) + }) + .collect(), + removed: sleds + .removed + .iter() + .map(|(sled_id, config)| (**sled_id, &config.measurements)) + .collect(), + } + } + + pub fn to_bp_sled_subtable(&self, sled_id: &SledUuid) -> Option { + let mut rows = vec![]; + if let Some(diff) = self.common.get(sled_id) { + rows.extend(BlueprintMeasurementsTableData::diff_rows(diff)); + } + if let Some(desired) = self.removed.get(sled_id) { + rows.extend( + BlueprintMeasurementsTableData::new(desired) + .rows(BpDiffState::Removed), + ); + } + if let Some(desired) = self.added.get(sled_id) { + rows.extend( + BlueprintMeasurementsTableData::new(desired) + .rows(BpDiffState::Added), + ); + } + if rows.is_empty() { + None + } else { + Some(BpTable::new(BpMeasurementsTableSchema {}, None, rows)) + } + } +} + /// Differences in host phase 2 contents #[derive(Debug)] pub struct BpDiffHostPhase2<'a> { @@ -1753,6 +1838,7 @@ pub struct BlueprintDiffDisplay<'diff, 'b> { disks: BpDiffPhysicalDisks<'diff>, datasets: BpDiffDatasets, host_phase_2: BpDiffHostPhase2<'diff>, + measurements: BpDiffMeasurements<'diff>, pending_mgs_updates: BpDiffPendingMgsUpdates<'diff, 'b>, } @@ -1765,6 +1851,7 @@ impl<'diff, 'b> BlueprintDiffDisplay<'diff, 'b> { let disks = BpDiffPhysicalDisks::from_diff_summary(summary); let datasets = BpDiffDatasets::from_diff_summary(summary); let host_phase_2 = BpDiffHostPhase2::from_diff_summary(summary); + let measurements = BpDiffMeasurements::from_diff_summary(summary); let pending_mgs_updates = BpDiffPendingMgsUpdates::from_diff_summary(summary); Self { @@ -1775,6 +1862,7 @@ impl<'diff, 'b> BlueprintDiffDisplay<'diff, 'b> { disks, datasets, host_phase_2, + measurements, pending_mgs_updates, } } @@ -1918,6 +2006,11 @@ impl<'diff, 'b> BlueprintDiffDisplay<'diff, 'b> { f: &mut fmt::Formatter<'_>, sled_id: &SledUuid, ) -> fmt::Result { + // Write the measurements if needed + if let Some(table) = self.measurements.to_bp_sled_subtable(sled_id) { + writeln!(f, "{table}\n")?; + } + // Write the host phase 2 table if needed if let Some(table) = self.host_phase_2.to_bp_sled_subtable(sled_id) { writeln!(f, "{table}\n")?; diff --git a/nexus/types/src/deployment/blueprint_display.rs b/nexus/types/src/deployment/blueprint_display.rs index bfa51da9fcb..28710592952 100644 --- a/nexus/types/src/deployment/blueprint_display.rs +++ b/nexus/types/src/deployment/blueprint_display.rs @@ -351,6 +351,18 @@ impl fmt::Display for BpTable { } } +/// The [`BpTable`] schema for desired host phase 2 contents +pub struct BpMeasurementsTableSchema {} +impl BpTableSchema for BpMeasurementsTableSchema { + fn table_name(&self) -> &'static str { + "measurements" + } + + fn column_names(&self) -> &'static [&'static str] { + &["hash", "version", "prune"] + } +} + /// The [`BpTable`] schema for desired host phase 2 contents pub struct BpHostPhase2TableSchema {} impl BpTableSchema for BpHostPhase2TableSchema { diff --git a/nexus/types/src/inventory/display.rs b/nexus/types/src/inventory/display.rs index d220d876fd8..b7b51afe15b 100644 --- a/nexus/types/src/inventory/display.rs +++ b/nexus/types/src/inventory/display.rs @@ -735,6 +735,7 @@ fn display_sleds( zones, boot_partitions, remove_mupdate_override, + measurements, } = last_reconciliation; display_boot_partition_contents(boot_partitions, &mut indented)?; @@ -855,6 +856,16 @@ fn display_sleds( } } } + + writeln!(indented, "reference measurements:")?; + let mut indent2 = IndentWriter::new(" ", &mut indented); + if measurements.is_empty() { + writeln!(indent2, "(measurement set is empty)")?; + } else { + for m in measurements { + writeln!(indent2, "{}", m.display())?; + } + } } write!(indented, "reconciler task status: ")?; @@ -1099,6 +1110,7 @@ fn display_sled_config( zones, remove_mupdate_override, host_phase_2, + measurements, } = config; writeln!(f, "\n{label} SLED CONFIG")?; @@ -1221,6 +1233,8 @@ fn display_sled_config( writeln!(indented, "{table}")?; } + writeln!(indented, "{}", measurements.display())?; + Ok(()) } diff --git a/openapi/nexus-lockstep.json b/openapi/nexus-lockstep.json index 2f9724cdb93..58737c64b4e 100644 --- a/openapi/nexus-lockstep.json +++ b/openapi/nexus-lockstep.json @@ -2132,6 +2132,21 @@ "slot_b" ] }, + "BlueprintMeasurementsDesiredContents": { + "type": "object", + "properties": { + "measurements": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlueprintSingleMeasurement" + }, + "uniqueItems": true + } + }, + "required": [ + "measurements" + ] + }, "BlueprintMetadata": { "description": "Describe high-level metadata about a blueprint", "type": "object", @@ -2327,6 +2342,26 @@ } ] }, + "BlueprintSingleMeasurement": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "prune": { + "type": "boolean" + }, + "version": { + "$ref": "#/components/schemas/BlueprintArtifactVersion" + } + }, + "required": [ + "hash", + "prune", + "version" + ] + }, "BlueprintSledConfig": { "description": "Information about the configuration of a sled as recorded in a blueprint.\n\nPart of [`Blueprint`].", "type": "object", @@ -2370,6 +2405,9 @@ "host_phase_2": { "$ref": "#/components/schemas/BlueprintHostPhase2DesiredSlots" }, + "measurements": { + "$ref": "#/components/schemas/BlueprintMeasurementsDesiredContents" + }, "remove_mupdate_override": { "nullable": true, "allOf": [ @@ -2415,6 +2453,7 @@ "datasets", "disks", "host_phase_2", + "measurements", "sled_agent_generation", "state", "subnet", diff --git a/openapi/sled-agent/sled-agent-12.0.0-7a22ae.json b/openapi/sled-agent/sled-agent-12.0.0-7a22ae.json new file mode 100644 index 00000000000..08249a6af6a --- /dev/null +++ b/openapi/sled-agent/sled-agent-12.0.0-7a22ae.json @@ -0,0 +1,9302 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Oxide Sled Agent API", + "description": "API for interacting with individual sleds", + "contact": { + "url": "https://oxide.computer", + "email": "api@oxide.computer" + }, + "version": "12.0.0" + }, + "paths": { + "/artifacts": { + "get": { + "operationId": "artifact_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactListResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/artifacts/{sha256}": { + "put": { + "operationId": "artifact_put", + "parameters": [ + { + "in": "path", + "name": "sha256", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } + }, + { + "in": "query", + "name": "generation", + "required": true, + "schema": { + "$ref": "#/components/schemas/Generation" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactPutResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/artifacts/{sha256}/copy-from-depot": { + "post": { + "operationId": "artifact_copy_from_depot", + "parameters": [ + { + "in": "path", + "name": "sha256", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } + }, + { + "in": "query", + "name": "generation", + "required": true, + "schema": { + "$ref": "#/components/schemas/Generation" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactCopyFromDepotBody" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "successfully enqueued operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactCopyFromDepotResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/artifacts-config": { + "get": { + "operationId": "artifact_config_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "artifact_config_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ArtifactConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/bootstore/status": { + "get": { + "summary": "Get the internal state of the local bootstore node", + "operationId": "bootstore_status", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BootstoreStatus" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/debug/switch-zone-policy": { + "get": { + "summary": "A debugging endpoint only used by `omdb` that allows us to test", + "description": "restarting the switch zone without restarting sled-agent. See for context.", + "operationId": "debug_operator_switch_zone_policy_get", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperatorSwitchZonePolicy" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "A debugging endpoint only used by `omdb` that allows us to test", + "description": "restarting the switch zone without restarting sled-agent. See for context.\n\nSetting the switch zone policy is asynchronous and inherently racy with the standard process of starting the switch zone. If the switch zone is in the process of being started or stopped when this policy is changed, the new policy may not take effect until that transition completes.", + "operationId": "debug_operator_switch_zone_policy_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperatorSwitchZonePolicy" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/disks/{disk_id}": { + "put": { + "operationId": "disk_put", + "parameters": [ + { + "in": "path", + "name": "disk_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskEnsureBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiskRuntimeState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/eip-gateways": { + "put": { + "summary": "Update per-NIC IP address <-> internet gateway mappings.", + "operationId": "set_eip_gateways", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExternalIpGatewayMap" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/inventory": { + "get": { + "summary": "Fetch basic information about this sled", + "operationId": "inventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Inventory" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/local-storage/{zpool_id}/{dataset_id}": { + "post": { + "summary": "Create a local storage dataset", + "operationId": "local_storage_dataset_ensure", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LocalStorageDatasetEnsureRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a local storage dataset", + "operationId": "local_storage_dataset_delete", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + } + ], + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/network-bootstore-config": { + "get": { + "summary": "This API endpoint is only reading the local sled agent's view of the", + "description": "bootstore. The boostore is a distributed data store that is eventually consistent. Reads from individual nodes may not represent the latest state.", + "operationId": "read_network_bootstore_config_cache", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EarlyNetworkConfig" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "write_network_bootstore_config", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EarlyNetworkConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/omicron-config": { + "put": { + "operationId": "omicron_config_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OmicronSledConfig" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/probes": { + "put": { + "summary": "Update the entire set of probe zones on this sled.", + "description": "Probe zones are used to debug networking configuration. They look similar to instances, in that they have an OPTE port on a VPC subnet and external addresses, but no actual VM.", + "operationId": "probes_put", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProbeSet" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/sled-identifiers": { + "get": { + "summary": "Fetch sled identifiers", + "operationId": "sled_identifiers", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledIdentifiers" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/sleds": { + "put": { + "summary": "Add a sled to a rack that was already initialized via RSS", + "operationId": "sled_add", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddSledRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/dladm-info": { + "get": { + "operationId": "support_dladm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/health-check": { + "get": { + "operationId": "support_health_check", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/ipadm-info": { + "get": { + "operationId": "support_ipadm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/logs/download/{zone}": { + "get": { + "summary": "This endpoint returns a zip file of a zone's logs organized by service.", + "operationId": "support_logs_download", + "parameters": [ + { + "in": "path", + "name": "zone", + "description": "The zone for which one would like to collect logs for", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "max_rotated", + "description": "The max number of rotated logs to include in the final support bundle", + "required": true, + "schema": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support/logs/zones": { + "get": { + "summary": "This endpoint returns a list of known zones on a sled that have service", + "description": "logs that can be collected into a support bundle.", + "operationId": "support_logs", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/nvmeadm-info": { + "get": { + "operationId": "support_nvmeadm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/pargs-info": { + "get": { + "operationId": "support_pargs_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/pfiles-info": { + "get": { + "operationId": "support_pfiles_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/pstack-info": { + "get": { + "operationId": "support_pstack_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SledDiagnosticsQueryOutput", + "type": "array", + "items": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/zfs-info": { + "get": { + "operationId": "support_zfs_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/zoneadm-info": { + "get": { + "operationId": "support_zoneadm_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support/zpool-info": { + "get": { + "operationId": "support_zpool_info", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledDiagnosticsQueryOutput" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}": { + "get": { + "summary": "List all support bundles within a particular dataset", + "operationId": "support_bundle_list", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_SupportBundleMetadata", + "type": "array", + "items": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}": { + "post": { + "summary": "Starts creation of a support bundle within a particular dataset", + "description": "Callers should transfer chunks of the bundle with \"support_bundle_transfer\", and then call \"support_bundle_finalize\" once the bundle has finished transferring.\n\nIf a support bundle was previously created without being finalized successfully, this endpoint will reset the state.\n\nIf a support bundle was previously created and finalized successfully, this endpoint will return metadata indicating that it already exists.", + "operationId": "support_bundle_start_creation", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a support bundle from a particular dataset", + "operationId": "support_bundle_delete", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download": { + "get": { + "summary": "Fetch a support bundle from a particular dataset", + "operationId": "support_bundle_download", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "summary": "Fetch metadata about a support bundle from a particular dataset", + "operationId": "support_bundle_head", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/download/{file}": { + "get": { + "summary": "Fetch a file within a support bundle from a particular dataset", + "operationId": "support_bundle_download_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The path of the file within the support bundle to query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "summary": "Fetch metadata about a file within a support bundle from a particular dataset", + "operationId": "support_bundle_head_file", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "file", + "description": "The path of the file within the support bundle to query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/finalize": { + "post": { + "summary": "Finalizes the creation of a support bundle", + "description": "If the requested hash matched the bundle, the bundle is created. Otherwise, an error is returned.", + "operationId": "support_bundle_finalize", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + }, + { + "in": "query", + "name": "hash", + "required": true, + "schema": { + "type": "string", + "format": "hex string (32 bytes)" + } + } + ], + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/index": { + "get": { + "summary": "Fetch the index (list of files within a support bundle)", + "operationId": "support_bundle_index", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + }, + "head": { + "summary": "Fetch metadata about the list of files within a support bundle", + "operationId": "support_bundle_head_index", + "parameters": [ + { + "in": "header", + "name": "range", + "description": "A request to access a portion of the resource, such as `bytes=0-499`\n\nSee: ", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + } + } + }, + "/support-bundles/{zpool_id}/{dataset_id}/{support_bundle_id}/transfer": { + "put": { + "summary": "Transfers a chunk of a support bundle within a particular dataset", + "operationId": "support_bundle_transfer", + "parameters": [ + { + "in": "path", + "name": "dataset_id", + "description": "The dataset on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/DatasetUuid" + } + }, + { + "in": "path", + "name": "support_bundle_id", + "description": "The ID of the support bundle itself", + "required": true, + "schema": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + { + "in": "path", + "name": "zpool_id", + "description": "The zpool on which this support bundle was provisioned", + "required": true, + "schema": { + "$ref": "#/components/schemas/ZpoolUuid" + } + }, + { + "in": "query", + "name": "offset", + "required": true, + "schema": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "successful creation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SupportBundleMetadata" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/switch-ports": { + "post": { + "operationId": "uplink_ensure", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SwitchPorts" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/v2p": { + "get": { + "summary": "List v2p mappings present on sled", + "operationId": "list_v2p", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_VirtualNetworkInterfaceHost", + "type": "array", + "items": { + "$ref": "#/components/schemas/VirtualNetworkInterfaceHost" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Create a mapping from a virtual NIC to a physical host", + "operationId": "set_v2p", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VirtualNetworkInterfaceHost" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a mapping from a virtual NIC to a physical host", + "operationId": "del_v2p", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VirtualNetworkInterfaceHost" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}": { + "put": { + "operationId": "vmm_register", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceEnsureBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledVmmState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "vmm_unregister", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmUnregisterResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/disks/{disk_id}/snapshot": { + "post": { + "summary": "Take a snapshot of a disk that is attached to an instance", + "operationId": "vmm_issue_disk_snapshot_request", + "parameters": [ + { + "in": "path", + "name": "disk_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmIssueDiskSnapshotRequestBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmIssueDiskSnapshotRequestResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/external-ip": { + "put": { + "operationId": "vmm_put_external_ip", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceExternalIpBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "vmm_delete_external_ip", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceExternalIpBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/multicast-group": { + "put": { + "operationId": "vmm_join_multicast_group", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMulticastBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "operationId": "vmm_leave_multicast_group", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceMulticastBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vmms/{propolis_id}/state": { + "get": { + "operationId": "vmm_get_state", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SledVmmState" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "operationId": "vmm_put_state", + "parameters": [ + { + "in": "path", + "name": "propolis_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/PropolisUuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmPutStateBody" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VmmPutStateResponse" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vpc/{vpc_id}/firewall/rules": { + "put": { + "operationId": "vpc_firewall_rules_put", + "parameters": [ + { + "in": "path", + "name": "vpc_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpcFirewallRulesEnsureBody" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/vpc-routes": { + "get": { + "summary": "Get the current versions of VPC routing rules.", + "operationId": "list_vpc_routes", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ResolvedVpcRouteState", + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcRouteState" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update VPC routing rules.", + "operationId": "set_vpc_routes", + "requestBody": { + "content": { + "application/json": { + "schema": { + "title": "Array_of_ResolvedVpcRouteSet", + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcRouteSet" + } + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones": { + "get": { + "summary": "List the zones that are currently managed by the sled agent.", + "operationId": "zones_list", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundle-cleanup": { + "post": { + "summary": "Trigger a zone bundle cleanup.", + "operationId": "zone_bundle_cleanup", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_CleanupCount", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/CleanupCount" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundle-cleanup/context": { + "get": { + "summary": "Return context used by the zone-bundle cleanup task.", + "operationId": "zone_bundle_cleanup_context", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CleanupContext" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "summary": "Update context used by the zone-bundle cleanup task.", + "operationId": "zone_bundle_cleanup_context_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CleanupContextUpdate" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "resource updated" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundle-cleanup/utilization": { + "get": { + "summary": "Return utilization information about all zone bundles.", + "operationId": "zone_bundle_utilization", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Map_of_BundleUtilization", + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/BundleUtilization" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundles": { + "get": { + "summary": "List all zone bundles that exist, even for now-deleted zones.", + "operationId": "zone_bundle_list_all", + "parameters": [ + { + "in": "query", + "name": "filter", + "description": "An optional substring used to filter zone bundles.", + "schema": { + "nullable": true, + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ZoneBundleMetadata", + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoneBundleMetadata" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundles/{zone_name}": { + "get": { + "summary": "List the zone bundles that are available for a running zone.", + "operationId": "zone_bundle_list", + "parameters": [ + { + "in": "path", + "name": "zone_name", + "description": "The name of the zone.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "title": "Array_of_ZoneBundleMetadata", + "type": "array", + "items": { + "$ref": "#/components/schemas/ZoneBundleMetadata" + } + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, + "/zones/bundles/{zone_name}/{bundle_id}": { + "get": { + "summary": "Fetch the binary content of a single zone bundle.", + "operationId": "zone_bundle_get", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "The ID for this bundle itself.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "zone_name", + "description": "The name of the zone this bundle is derived from.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "delete": { + "summary": "Delete a zone bundle.", + "operationId": "zone_bundle_delete", + "parameters": [ + { + "in": "path", + "name": "bundle_id", + "description": "The ID for this bundle itself.", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "in": "path", + "name": "zone_name", + "description": "The name of the zone this bundle is derived from.", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "successful deletion" + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + } + }, + "components": { + "schemas": { + "AddSledRequest": { + "description": "A request to Add a given sled after rack initialization has occurred", + "type": "object", + "properties": { + "sled_id": { + "$ref": "#/components/schemas/BaseboardId" + }, + "start_request": { + "$ref": "#/components/schemas/StartSledAgentRequest" + } + }, + "required": [ + "sled_id", + "start_request" + ] + }, + "ArtifactConfig": { + "description": "Artifact configuration.\n\nThis type is used in both GET (response) and PUT (request) operations.", + "type": "object", + "properties": { + "artifacts": { + "type": "array", + "items": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "uniqueItems": true + }, + "generation": { + "$ref": "#/components/schemas/Generation" + } + }, + "required": [ + "artifacts", + "generation" + ] + }, + "ArtifactCopyFromDepotBody": { + "description": "Request body for copying artifacts from a depot.", + "type": "object", + "properties": { + "depot_base_url": { + "type": "string" + } + }, + "required": [ + "depot_base_url" + ] + }, + "ArtifactCopyFromDepotResponse": { + "description": "Response for copying artifacts from a depot.", + "type": "object" + }, + "ArtifactListResponse": { + "description": "Response for listing artifacts.", + "type": "object", + "properties": { + "generation": { + "$ref": "#/components/schemas/Generation" + }, + "list": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint", + "minimum": 0 + } + } + }, + "required": [ + "generation", + "list" + ] + }, + "ArtifactPutResponse": { + "description": "Response for putting an artifact.", + "type": "object", + "properties": { + "datasets": { + "description": "The number of valid M.2 artifact datasets we found on the sled. There is typically one of these datasets for each functional M.2.", + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "successful_writes": { + "description": "The number of valid writes to the M.2 artifact datasets. This should be less than or equal to the number of artifact datasets.", + "type": "integer", + "format": "uint", + "minimum": 0 + } + }, + "required": [ + "datasets", + "successful_writes" + ] + }, + "Baseboard": { + "description": "Describes properties that should uniquely identify a Gimlet.", + "oneOf": [ + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "type": { + "type": "string", + "enum": [ + "gimlet" + ] + } + }, + "required": [ + "identifier", + "model", + "revision", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "unknown" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "identifier": { + "type": "string" + }, + "model": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "pc" + ] + } + }, + "required": [ + "identifier", + "model", + "type" + ] + } + ] + }, + "BaseboardId": { + "description": "A representation of a Baseboard ID as used in the inventory subsystem This type is essentially the same as a `Baseboard` except it doesn't have a revision or HW type (Gimlet, PC, Unknown).", + "type": "object", + "properties": { + "part_number": { + "description": "Oxide Part Number", + "type": "string" + }, + "serial_number": { + "description": "Serial number (unique for a given part number)", + "type": "string" + } + }, + "required": [ + "part_number", + "serial_number" + ] + }, + "BfdMode": { + "description": "BFD connection mode.", + "type": "string", + "enum": [ + "single_hop", + "multi_hop" + ] + }, + "BfdPeerConfig": { + "type": "object", + "properties": { + "detection_threshold": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "local": { + "nullable": true, + "type": "string", + "format": "ip" + }, + "mode": { + "$ref": "#/components/schemas/BfdMode" + }, + "remote": { + "type": "string", + "format": "ip" + }, + "required_rx": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "switch": { + "$ref": "#/components/schemas/SwitchLocation" + } + }, + "required": [ + "detection_threshold", + "mode", + "remote", + "required_rx", + "switch" + ] + }, + "BgpConfig": { + "type": "object", + "properties": { + "asn": { + "description": "The autonomous system number for the BGP configuration.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "checker": { + "nullable": true, + "description": "Checker to apply to incoming messages.", + "default": null, + "type": "string" + }, + "originate": { + "description": "The set of prefixes for the BGP router to originate.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + }, + "shaper": { + "nullable": true, + "description": "Shaper to apply to outgoing messages.", + "default": null, + "type": "string" + } + }, + "required": [ + "asn", + "originate" + ] + }, + "BgpPeerConfig": { + "type": "object", + "properties": { + "addr": { + "description": "Address of the peer.", + "type": "string", + "format": "ipv4" + }, + "allowed_export": { + "description": "Define export policy for a peer.", + "default": { + "type": "no_filtering" + }, + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "allowed_import": { + "description": "Define import policy for a peer.", + "default": { + "type": "no_filtering" + }, + "allOf": [ + { + "$ref": "#/components/schemas/ImportExportPolicy" + } + ] + }, + "asn": { + "description": "The autonomous system number of the router the peer belongs to.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "communities": { + "description": "Include the provided communities in updates sent to the peer.", + "default": [], + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "connect_retry": { + "nullable": true, + "description": "The interval in seconds between peer connection retry attempts.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "delay_open": { + "nullable": true, + "description": "How long to delay sending open messages to a peer. In seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "enforce_first_as": { + "description": "Enforce that the first AS in paths received from this peer is the peer's AS.", + "default": false, + "type": "boolean" + }, + "hold_time": { + "nullable": true, + "description": "How long to keep a session alive without a keepalive in seconds. Defaults to 6.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "idle_hold_time": { + "nullable": true, + "description": "How long to keep a peer in idle after a state machine reset in seconds.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "keepalive": { + "nullable": true, + "description": "The interval to send keepalive messages at.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "local_pref": { + "nullable": true, + "description": "Apply a local preference to routes received from this peer.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "md5_auth_key": { + "nullable": true, + "description": "Use the given key for TCP-MD5 authentication with the peer.", + "default": null, + "type": "string" + }, + "min_ttl": { + "nullable": true, + "description": "Require messages from a peer have a minimum IP time to live field.", + "default": null, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "multi_exit_discriminator": { + "nullable": true, + "description": "Apply the provided multi-exit discriminator (MED) updates sent to the peer.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "port": { + "description": "Switch port the peer is reachable on.", + "type": "string" + }, + "remote_asn": { + "nullable": true, + "description": "Require that a peer has a specified ASN.", + "default": null, + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "Associate a VLAN ID with a BGP peer session.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "addr", + "asn", + "port" + ] + }, + "BlobStorageBackend": { + "description": "A storage backend for a disk whose initial contents are given explicitly by the specification.", + "type": "object", + "properties": { + "base64": { + "description": "The disk's initial contents, encoded as a base64 string.", + "type": "string" + }, + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + } + }, + "required": [ + "base64", + "readonly" + ], + "additionalProperties": false + }, + "Board": { + "description": "A VM's mainboard.", + "type": "object", + "properties": { + "chipset": { + "description": "The chipset to expose to guest software.", + "allOf": [ + { + "$ref": "#/components/schemas/Chipset" + } + ] + }, + "cpuid": { + "nullable": true, + "description": "The CPUID values to expose to the guest. If `None`, bhyve will derive default values from the host's CPUID values.", + "allOf": [ + { + "$ref": "#/components/schemas/Cpuid" + } + ] + }, + "cpus": { + "description": "The number of virtual logical processors attached to this VM.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "guest_hv_interface": { + "description": "The hypervisor platform to expose to the guest. The default is a bhyve-compatible interface with no additional features.\n\nFor compatibility with older versions of Propolis, this field is only serialized if it specifies a non-default interface.", + "allOf": [ + { + "$ref": "#/components/schemas/GuestHypervisorInterface" + } + ] + }, + "memory_mb": { + "description": "The amount of guest RAM attached to this VM.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "chipset", + "cpus", + "memory_mb" + ], + "additionalProperties": false + }, + "BootImageHeader": { + "type": "object", + "properties": { + "data_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "flags": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "image_name": { + "type": "string" + }, + "image_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "sha256": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "minItems": 32, + "maxItems": 32 + }, + "target_size": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "data_size", + "flags", + "image_name", + "image_size", + "sha256", + "target_size" + ] + }, + "BootOrderEntry": { + "description": "An entry in the boot order stored in a [`BootSettings`] component.", + "type": "object", + "properties": { + "id": { + "description": "The ID of another component in the spec that Propolis should try to boot from.\n\nCurrently, only disk device components are supported.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + } + }, + "required": [ + "id" + ] + }, + "BootPartitionContents": { + "type": "object", + "properties": { + "boot_disk": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/M2Slot" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/M2Slot" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "slot_a": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/BootPartitionDetails" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/BootPartitionDetails" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "slot_b": { + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/BootPartitionDetails" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/BootPartitionDetails" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + } + }, + "required": [ + "boot_disk", + "slot_a", + "slot_b" + ] + }, + "BootPartitionDetails": { + "type": "object", + "properties": { + "artifact_hash": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "artifact_size": { + "type": "integer", + "format": "uint", + "minimum": 0 + }, + "header": { + "$ref": "#/components/schemas/BootImageHeader" + } + }, + "required": [ + "artifact_hash", + "artifact_size", + "header" + ] + }, + "BootSettings": { + "description": "Settings supplied to the guest's firmware image that specify the order in which it should consider its options when selecting a device to try to boot from.", + "type": "object", + "properties": { + "order": { + "description": "An ordered list of components to attempt to boot from.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BootOrderEntry" + } + } + }, + "required": [ + "order" + ], + "additionalProperties": false + }, + "BootstoreStatus": { + "description": "Status of the local bootstore node.", + "type": "object", + "properties": { + "accepted_connections": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "established_connections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EstablishedConnection" + } + }, + "fsm_ledger_generation": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "fsm_state": { + "type": "string" + }, + "negotiating_connections": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "network_config_ledger_generation": { + "nullable": true, + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "peers": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "required": [ + "accepted_connections", + "established_connections", + "fsm_ledger_generation", + "fsm_state", + "negotiating_connections", + "peers" + ] + }, + "BundleUtilization": { + "description": "The portion of a debug dataset used for zone bundles.", + "type": "object", + "properties": { + "bytes_available": { + "description": "The total number of bytes available for zone bundles.\n\nThis is `dataset_quota` multiplied by the context's storage limit.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "bytes_used": { + "description": "Total bundle usage, in bytes.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "dataset_quota": { + "description": "The total dataset quota, in bytes.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "bytes_available", + "bytes_used", + "dataset_quota" + ] + }, + "ByteCount": { + "description": "Byte count to express memory or storage capacity.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "Chipset": { + "description": "A kind of virtual chipset.", + "oneOf": [ + { + "description": "An Intel 440FX-compatible chipset.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "i440_fx" + ] + }, + "value": { + "$ref": "#/components/schemas/I440Fx" + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, + "CleanupContext": { + "description": "Context provided for the zone bundle cleanup task.", + "type": "object", + "properties": { + "period": { + "description": "The period on which automatic checks and cleanup is performed.", + "allOf": [ + { + "$ref": "#/components/schemas/CleanupPeriod" + } + ] + }, + "priority": { + "description": "The priority ordering for keeping old bundles.", + "allOf": [ + { + "$ref": "#/components/schemas/PriorityOrder" + } + ] + }, + "storage_limit": { + "description": "The limit on the dataset quota available for zone bundles.", + "allOf": [ + { + "$ref": "#/components/schemas/StorageLimit" + } + ] + } + }, + "required": [ + "period", + "priority", + "storage_limit" + ] + }, + "CleanupContextUpdate": { + "description": "Parameters used to update the zone bundle cleanup context.", + "type": "object", + "properties": { + "period": { + "nullable": true, + "description": "The new period on which automatic cleanups are run.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "priority": { + "nullable": true, + "description": "The priority ordering for preserving old zone bundles.", + "allOf": [ + { + "$ref": "#/components/schemas/PriorityOrder" + } + ] + }, + "storage_limit": { + "nullable": true, + "description": "The new limit on the underlying dataset quota allowed for bundles.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + }, + "CleanupCount": { + "description": "The count of bundles / bytes removed during a cleanup operation.", + "type": "object", + "properties": { + "bundles": { + "description": "The number of bundles removed.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "bytes": { + "description": "The number of bytes removed.", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "bundles", + "bytes" + ] + }, + "CleanupPeriod": { + "description": "A period on which bundles are automatically cleaned up.", + "allOf": [ + { + "$ref": "#/components/schemas/Duration" + } + ] + }, + "ComponentV0": { + "oneOf": [ + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioDisk" + }, + "type": { + "type": "string", + "enum": [ + "virtio_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/NvmeDisk" + }, + "type": { + "type": "string", + "enum": [ + "nvme_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNic" + }, + "type": { + "type": "string", + "enum": [ + "virtio_nic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SerialPort" + }, + "type": { + "type": "string", + "enum": [ + "serial_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/PciPciBridge" + }, + "type": { + "type": "string", + "enum": [ + "pci_pci_bridge" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/QemuPvpanic" + }, + "type": { + "type": "string", + "enum": [ + "qemu_pvpanic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BootSettings" + }, + "type": { + "type": "string", + "enum": [ + "boot_settings" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPciPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_pci_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuP9" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_p9" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/P9fs" + }, + "type": { + "type": "string", + "enum": [ + "p9fs" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/MigrationFailureInjector" + }, + "type": { + "type": "string", + "enum": [ + "migration_failure_injector" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/CrucibleStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "crucible_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/FileStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "file_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BlobStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "blob_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "virtio_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/DlpiNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "dlpi_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + } + ] + }, + "CompressionAlgorithm": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "on" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "off" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "gzip" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "level": { + "$ref": "#/components/schemas/GzipLevel" + }, + "type": { + "type": "string", + "enum": [ + "gzip_n" + ] + } + }, + "required": [ + "level", + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "lz4" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "lzjb" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "zle" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "ConfigReconcilerInventory": { + "description": "Describes the last attempt made by the sled-agent-config-reconciler to reconcile the current sled config against the actual state of the sled.", + "type": "object", + "properties": { + "boot_partitions": { + "$ref": "#/components/schemas/BootPartitionContents" + }, + "datasets": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + }, + "external_disks": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + }, + "last_reconciled_config": { + "$ref": "#/components/schemas/OmicronSledConfig" + }, + "measurements": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/ReconciledSingleMeasurement" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/ReconciledSingleMeasurement" + }, + "uniqueItems": true + }, + "orphaned_datasets": { + "title": "IdOrdMap", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/OrphanedDataset" + } + ], + "path": "iddqd::IdOrdMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/OrphanedDataset" + }, + "uniqueItems": true + }, + "remove_mupdate_override": { + "nullable": true, + "description": "The result of removing the mupdate override file on disk.\n\n`None` if `remove_mupdate_override` was not provided in the sled config.", + "allOf": [ + { + "$ref": "#/components/schemas/RemoveMupdateOverrideInventory" + } + ] + }, + "zones": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + } + }, + "required": [ + "boot_partitions", + "datasets", + "external_disks", + "last_reconciled_config", + "measurements", + "orphaned_datasets", + "zones" + ] + }, + "ConfigReconcilerInventoryResult": { + "oneOf": [ + { + "type": "object", + "properties": { + "result": { + "type": "string", + "enum": [ + "ok" + ] + } + }, + "required": [ + "result" + ] + }, + { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "result": { + "type": "string", + "enum": [ + "err" + ] + } + }, + "required": [ + "message", + "result" + ] + } + ] + }, + "ConfigReconcilerInventoryStatus": { + "description": "Status of the sled-agent-config-reconciler task.", + "oneOf": [ + { + "description": "The reconciler task has not yet run for the first time since sled-agent started.", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "not_yet_run" + ] + } + }, + "required": [ + "status" + ] + }, + { + "description": "The reconciler task is actively running.", + "type": "object", + "properties": { + "config": { + "$ref": "#/components/schemas/OmicronSledConfig" + }, + "running_for": { + "$ref": "#/components/schemas/Duration" + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "enum": [ + "running" + ] + } + }, + "required": [ + "config", + "running_for", + "started_at", + "status" + ] + }, + { + "description": "The reconciler task is currently idle, but previously did complete a reconciliation attempt.\n\nThis variant does not include the `OmicronSledConfig` used in the last attempt, because that's always available via [`ConfigReconcilerInventory::last_reconciled_config`].", + "type": "object", + "properties": { + "completed_at": { + "type": "string", + "format": "date-time" + }, + "ran_for": { + "$ref": "#/components/schemas/Duration" + }, + "status": { + "type": "string", + "enum": [ + "idle" + ] + } + }, + "required": [ + "completed_at", + "ran_for", + "status" + ] + } + ] + }, + "Cpuid": { + "description": "A set of CPUID values to expose to a guest.", + "type": "object", + "properties": { + "entries": { + "description": "A list of CPUID leaves/subleaves and their associated values.\n\nPropolis servers require that each entry's `leaf` be unique and that it falls in either the \"standard\" (0 to 0xFFFF) or \"extended\" (0x8000_0000 to 0x8000_FFFF) function ranges, since these are the only valid input ranges currently defined by Intel and AMD. See the Intel 64 and IA-32 Architectures Software Developer's Manual (June 2024) Table 3-17 and the AMD64 Architecture Programmer's Manual (March 2024) Volume 3's documentation of the CPUID instruction.", + "type": "array", + "items": { + "$ref": "#/components/schemas/CpuidEntry" + } + }, + "vendor": { + "description": "The CPU vendor to emulate.\n\nCPUID leaves in the extended range (0x8000_0000 to 0x8000_FFFF) have vendor-defined semantics. Propolis uses this value to determine these semantics when deciding whether it needs to specialize the supplied template values for these leaves.", + "allOf": [ + { + "$ref": "#/components/schemas/CpuidVendor" + } + ] + } + }, + "required": [ + "entries", + "vendor" + ], + "additionalProperties": false + }, + "CpuidEntry": { + "description": "A full description of a CPUID leaf/subleaf and the values it produces.", + "type": "object", + "properties": { + "eax": { + "description": "The value to return in eax.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ebx": { + "description": "The value to return in ebx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ecx": { + "description": "The value to return in ecx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "edx": { + "description": "The value to return in edx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "leaf": { + "description": "The leaf (function) number for this entry.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "subleaf": { + "nullable": true, + "description": "The subleaf (index) number for this entry, if it uses subleaves.", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "eax", + "ebx", + "ecx", + "edx", + "leaf" + ], + "additionalProperties": false + }, + "CpuidVendor": { + "description": "A CPU vendor to use when interpreting the meanings of CPUID leaves in the extended ID range (0x80000000 to 0x8000FFFF).", + "type": "string", + "enum": [ + "amd", + "intel" + ] + }, + "CrucibleStorageBackend": { + "description": "A Crucible storage backend.", + "type": "object", + "properties": { + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + }, + "request_json": { + "description": "A serialized `[crucible_client_types::VolumeConstructionRequest]`. This is stored in serialized form so that breaking changes to the definition of a `VolumeConstructionRequest` do not inadvertently break instance spec deserialization.\n\nWhen using a spec to initialize a new instance, the spec author must ensure this request is well-formed and can be deserialized by the version of `crucible_client_types` used by the target Propolis.", + "type": "string" + } + }, + "required": [ + "readonly", + "request_json" + ], + "additionalProperties": false + }, + "DatasetConfig": { + "description": "Configuration information necessary to request a single dataset.\n\nThese datasets are tracked directly by Nexus.", + "type": "object", + "properties": { + "compression": { + "description": "The compression mode to be used by the dataset", + "allOf": [ + { + "$ref": "#/components/schemas/CompressionAlgorithm" + } + ] + }, + "id": { + "description": "The UUID of the dataset being requested", + "allOf": [ + { + "$ref": "#/components/schemas/DatasetUuid" + } + ] + }, + "name": { + "description": "The dataset's name", + "allOf": [ + { + "$ref": "#/components/schemas/DatasetName" + } + ] + }, + "quota": { + "nullable": true, + "description": "The upper bound on the amount of storage used by this dataset", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + }, + "reservation": { + "nullable": true, + "description": "The lower bound on the amount of storage usable by this dataset", + "allOf": [ + { + "$ref": "#/components/schemas/ByteCount" + } + ] + } + }, + "required": [ + "compression", + "id", + "name" + ] + }, + "DatasetKind": { + "description": "The kind of dataset. See the `DatasetKind` enum in omicron-common for possible values.", + "type": "string" + }, + "DatasetName": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/components/schemas/DatasetKind" + }, + "pool_name": { + "$ref": "#/components/schemas/ZpoolName" + } + }, + "required": [ + "kind", + "pool_name" + ] + }, + "DatasetUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::DatasetUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "DelegatedZvol": { + "description": "Delegate a ZFS volume to a zone", + "oneOf": [ + { + "description": "Delegate a slice of the local storage dataset present on this pool into the zone.", + "type": "object", + "properties": { + "dataset_id": { + "$ref": "#/components/schemas/DatasetUuid" + }, + "type": { + "type": "string", + "enum": [ + "local_storage" + ] + }, + "zpool_id": { + "$ref": "#/components/schemas/ExternalZpoolUuid" + } + }, + "required": [ + "dataset_id", + "type", + "zpool_id" + ] + } + ] + }, + "DhcpConfig": { + "description": "DHCP configuration for a port\n\nNot present here: Hostname (DHCPv4 option 12; used in DHCPv6 option 39); we use `InstanceRuntimeState::hostname` for this value.", + "type": "object", + "properties": { + "dns_servers": { + "description": "DNS servers to send to the instance\n\n(DHCPv4 option 6; DHCPv6 option 23)", + "type": "array", + "items": { + "type": "string", + "format": "ip" + } + }, + "host_domain": { + "nullable": true, + "description": "DNS zone this instance's hostname belongs to (e.g. the `project.example` part of `instance1.project.example`)\n\n(DHCPv4 option 15; used in DHCPv6 option 39)", + "type": "string" + }, + "search_domains": { + "description": "DNS search domains\n\n(DHCPv4 option 119; DHCPv6 option 24)", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "dns_servers", + "search_domains" + ] + }, + "DiskEnsureBody": { + "description": "Sent from to a sled agent to establish the runtime state of a Disk", + "type": "object", + "properties": { + "initial_runtime": { + "description": "Last runtime state of the Disk known to Nexus (used if the agent has never seen this Disk before).", + "allOf": [ + { + "$ref": "#/components/schemas/DiskRuntimeState" + } + ] + }, + "target": { + "description": "requested runtime state of the Disk", + "allOf": [ + { + "$ref": "#/components/schemas/DiskStateRequested" + } + ] + } + }, + "required": [ + "initial_runtime", + "target" + ] + }, + "DiskIdentity": { + "description": "Uniquely identifies a disk.", + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "serial": { + "type": "string" + }, + "vendor": { + "type": "string" + } + }, + "required": [ + "model", + "serial", + "vendor" + ] + }, + "DiskRuntimeState": { + "description": "Runtime state of the Disk, which includes its attach state and some minimal metadata", + "type": "object", + "properties": { + "disk_state": { + "description": "runtime state of the Disk", + "allOf": [ + { + "$ref": "#/components/schemas/DiskState" + } + ] + }, + "gen": { + "description": "generation number for this state", + "allOf": [ + { + "$ref": "#/components/schemas/Generation" + } + ] + }, + "time_updated": { + "description": "timestamp for this information", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "disk_state", + "gen", + "time_updated" + ] + }, + "DiskState": { + "description": "State of a Disk", + "oneOf": [ + { + "description": "Disk is being initialized", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "creating" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready but detached from any Instance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "detached" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is ready to receive blocks from an external source", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "import_ready" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from a URL", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_url" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is importing blocks from bulk writes", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "importing_from_bulk_writes" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being finalized to state Detached", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "finalizing" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is undergoing maintenance", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "maintenance" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is being attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is attached to the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attached" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk is being detached from the given Instance", + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "detaching" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "description": "Disk has been destroyed", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "destroyed" + ] + } + }, + "required": [ + "state" + ] + }, + { + "description": "Disk is unavailable", + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "state" + ] + } + ] + }, + "DiskStateRequested": { + "description": "Used to request a Disk state change", + "oneOf": [ + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "detached" + ] + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "instance": { + "type": "string", + "format": "uuid" + }, + "state": { + "type": "string", + "enum": [ + "attached" + ] + } + }, + "required": [ + "instance", + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "destroyed" + ] + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "faulted" + ] + } + }, + "required": [ + "state" + ] + } + ] + }, + "DiskVariant": { + "type": "string", + "enum": [ + "U2", + "M2" + ] + }, + "DlpiNetworkBackend": { + "description": "A network backend associated with a DLPI VNIC on the host.", + "type": "object", + "properties": { + "vnic_name": { + "description": "The name of the VNIC to use as a backend.", + "type": "string" + } + }, + "required": [ + "vnic_name" + ], + "additionalProperties": false + }, + "Duration": { + "type": "object", + "properties": { + "nanos": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "secs": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "nanos", + "secs" + ] + }, + "EarlyNetworkConfig": { + "description": "Network configuration required to bring up the control plane\n\nThe fields in this structure are those from `RackInitializeRequest` necessary for use beyond RSS. This is just for the initial rack configuration and cold boot purposes. Updates come from Nexus.", + "type": "object", + "properties": { + "body": { + "$ref": "#/components/schemas/EarlyNetworkConfigBody" + }, + "generation": { + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "schema_version": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "body", + "generation", + "schema_version" + ] + }, + "EarlyNetworkConfigBody": { + "description": "This is the actual configuration of EarlyNetworking.\n\nWe nest it below the \"header\" of `generation` and `schema_version` so that we can perform partial deserialization of `EarlyNetworkConfig` to only read the header and defer deserialization of the body once we know the schema version. This is possible via the use of [`serde_json::value::RawValue`] in future (post-v1) deserialization paths.", + "type": "object", + "properties": { + "ntp_servers": { + "description": "The external NTP server addresses.", + "type": "array", + "items": { + "type": "string" + } + }, + "rack_network_config": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RackNetworkConfigV2" + } + ] + } + }, + "required": [ + "ntp_servers" + ] + }, + "Error": { + "description": "Error information from a response.", + "type": "object", + "properties": { + "error_code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "request_id": { + "type": "string" + } + }, + "required": [ + "message", + "request_id" + ] + }, + "EstablishedConnection": { + "description": "An established connection to a bootstore peer.", + "type": "object", + "properties": { + "addr": { + "type": "string" + }, + "baseboard": { + "$ref": "#/components/schemas/Baseboard" + } + }, + "required": [ + "addr", + "baseboard" + ] + }, + "ExternalIp": { + "description": "An external IP address used by a probe.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used by the address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external IP address.", + "type": "string", + "format": "ip" + }, + "kind": { + "description": "The kind of address this is.", + "allOf": [ + { + "$ref": "#/components/schemas/IpKind" + } + ] + }, + "last_port": { + "description": "The last port used by the address.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "kind", + "last_port" + ] + }, + "ExternalIpConfig": { + "description": "A single- or dual-stack external IP configuration.", + "oneOf": [ + { + "description": "Single-stack IPv4 external IP configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v4" + ] + }, + "value": { + "$ref": "#/components/schemas/ExternalIpv4Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Single-stack IPv6 external IP configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v6" + ] + }, + "value": { + "$ref": "#/components/schemas/ExternalIpv6Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Both IPv4 and IPv6 external IP configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "dual_stack" + ] + }, + "value": { + "type": "object", + "properties": { + "v4": { + "$ref": "#/components/schemas/ExternalIpv4Config" + }, + "v6": { + "$ref": "#/components/schemas/ExternalIpv6Config" + } + }, + "required": [ + "v4", + "v6" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "ExternalIpGatewayMap": { + "description": "Per-NIC mappings from external IP addresses to the Internet Gateways which can choose them as a source.", + "type": "object", + "properties": { + "mappings": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + }, + "uniqueItems": true + } + } + } + }, + "required": [ + "mappings" + ] + }, + "ExternalIpv4Config": { + "description": "External IP address configuration.\n\nThis encapsulates all the external addresses of a single IP version, including source NAT, Ephemeral, and Floating IPs. Note that not all of these need to be specified, but this type can only be constructed if _at least one_ of them is.", + "type": "object", + "properties": { + "ephemeral_ip": { + "nullable": true, + "description": "An Ephemeral address for in- and outbound connectivity.", + "type": "string", + "format": "ipv4" + }, + "floating_ips": { + "description": "Additional Floating IPs for in- and outbound connectivity.", + "type": "array", + "items": { + "type": "string", + "format": "ipv4" + } + }, + "source_nat": { + "nullable": true, + "description": "Source NAT configuration, for outbound-only connectivity.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceNatConfigV4" + } + ] + } + }, + "required": [ + "floating_ips" + ] + }, + "ExternalIpv6Config": { + "description": "External IP address configuration.\n\nThis encapsulates all the external addresses of a single IP version, including source NAT, Ephemeral, and Floating IPs. Note that not all of these need to be specified, but this type can only be constructed if _at least one_ of them is.", + "type": "object", + "properties": { + "ephemeral_ip": { + "nullable": true, + "description": "An Ephemeral address for in- and outbound connectivity.", + "type": "string", + "format": "ipv6" + }, + "floating_ips": { + "description": "Additional Floating IPs for in- and outbound connectivity.", + "type": "array", + "items": { + "type": "string", + "format": "ipv6" + } + }, + "source_nat": { + "nullable": true, + "description": "Source NAT configuration, for outbound-only connectivity.", + "allOf": [ + { + "$ref": "#/components/schemas/SourceNatConfigV6" + } + ] + } + }, + "required": [ + "floating_ips" + ] + }, + "ExternalZpoolUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ExternalZpoolUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "FileStorageBackend": { + "description": "A storage backend backed by a file in the host system's file system.", + "type": "object", + "properties": { + "block_size": { + "description": "Block size of the backend", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "path": { + "description": "A path to a file that backs a disk.", + "type": "string" + }, + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + }, + "workers": { + "nullable": true, + "description": "Optional worker threads for the file backend, exposed for testing only.", + "type": "integer", + "format": "uint", + "minimum": 1 + } + }, + "required": [ + "block_size", + "path", + "readonly" + ], + "additionalProperties": false + }, + "Generation": { + "description": "Generation numbers stored in the database, used for optimistic concurrency control", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "GuestHypervisorInterface": { + "description": "A hypervisor interface to expose to the guest.", + "oneOf": [ + { + "description": "Expose a bhyve-like interface (\"bhyve bhyve \" as the hypervisor ID in leaf 0x4000_0000 and no additional leaves or features).", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "bhyve" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "description": "Expose a Hyper-V-compatible hypervisor interface with the supplied features enabled.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "hyper_v" + ] + }, + "value": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HyperVFeatureFlag" + }, + "uniqueItems": true + } + }, + "required": [ + "features" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, + "GzipLevel": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "HostIdentifier": { + "description": "A `HostIdentifier` represents either an IP host or network (v4 or v6), or an entire VPC (identified by its VNI). It is used in firewall rule host filters.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "HostPhase2DesiredContents": { + "description": "Describes the desired contents of a host phase 2 slot (i.e., the boot partition on one of the internal M.2 drives).", + "oneOf": [ + { + "description": "Do not change the current contents.\n\nWe use this value when we've detected a sled has been mupdated (and we don't want to overwrite phase 2 images until we understand how to recover from that mupdate) and as the default value when reading an [`OmicronSledConfig`] that was ledgered before this concept existed.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "current_contents" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Set the phase 2 slot to the given artifact.\n\nThe artifact will come from an unpacked and distributed TUF repo.", + "type": "object", + "properties": { + "hash": { + "type": "string", + "format": "hex string (32 bytes)" + }, + "type": { + "type": "string", + "enum": [ + "artifact" + ] + } + }, + "required": [ + "hash", + "type" + ] + } + ] + }, + "HostPhase2DesiredSlots": { + "description": "Describes the desired contents for both host phase 2 slots.", + "type": "object", + "properties": { + "slot_a": { + "$ref": "#/components/schemas/HostPhase2DesiredContents" + }, + "slot_b": { + "$ref": "#/components/schemas/HostPhase2DesiredContents" + } + }, + "required": [ + "slot_a", + "slot_b" + ] + }, + "HostPortConfig": { + "type": "object", + "properties": { + "addrs": { + "description": "IP Address and prefix (e.g., `192.168.0.1/16`) to apply to switchport (must be in infra_ip pool). May also include an optional VLAN ID.", + "type": "array", + "items": { + "$ref": "#/components/schemas/UplinkAddressConfig" + } + }, + "lldp": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/LldpPortConfig" + } + ] + }, + "port": { + "description": "Switchport to use for external connectivity", + "type": "string" + }, + "tx_eq": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + } + }, + "required": [ + "addrs", + "port" + ] + }, + "Hostname": { + "title": "An RFC-1035-compliant hostname", + "description": "A hostname identifies a host on a network, and is usually a dot-delimited sequence of labels, where each label contains only letters, digits, or the hyphen. See RFCs 1035 and 952 for more details.", + "type": "string", + "pattern": "^([a-zA-Z0-9]+[a-zA-Z0-9\\-]*(? for background.", + "oneOf": [ + { + "description": "Start the switch zone if a switch is present.\n\nThis is the default policy.", + "type": "object", + "properties": { + "policy": { + "type": "string", + "enum": [ + "start_if_switch_present" + ] + } + }, + "required": [ + "policy" + ] + }, + { + "description": "Even if a switch zone is present, stop the switch zone.", + "type": "object", + "properties": { + "policy": { + "type": "string", + "enum": [ + "stop_despite_switch_presence" + ] + } + }, + "required": [ + "policy" + ] + } + ] + }, + "OrphanedDataset": { + "type": "object", + "properties": { + "available": { + "$ref": "#/components/schemas/ByteCount" + }, + "id": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/DatasetUuid" + } + ] + }, + "mounted": { + "type": "boolean" + }, + "name": { + "$ref": "#/components/schemas/DatasetName" + }, + "reason": { + "type": "string" + }, + "used": { + "$ref": "#/components/schemas/ByteCount" + } + }, + "required": [ + "available", + "mounted", + "name", + "reason", + "used" + ] + }, + "P9fs": { + "description": "Describes a filesystem to expose through a P9 device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "chunk_size": { + "description": "The chunk size to use in the 9P protocol. Vanilla Helios images should use 8192. Falcon Helios base images and Linux can use up to 65536.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach the guest to this P9 filesystem.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + }, + "source": { + "description": "The host source path to mount into the guest.", + "type": "string" + }, + "target": { + "description": "The 9P target filesystem tag.", + "type": "string" + } + }, + "required": [ + "chunk_size", + "pci_path", + "source", + "target" + ], + "additionalProperties": false + }, + "PciPath": { + "description": "A PCI bus/device/function tuple.", + "type": "object", + "properties": { + "bus": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "device": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "function": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "bus", + "device", + "function" + ] + }, + "PciPciBridge": { + "description": "A PCI-PCI bridge.", + "type": "object", + "properties": { + "downstream_bus": { + "description": "The logical bus number of this bridge's downstream bus. Other devices may use this bus number in their PCI paths to indicate they should be attached to this bridge's bus.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach this bridge.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "downstream_bus", + "pci_path" + ], + "additionalProperties": false + }, + "PhysicalDiskUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::PhysicalDiskUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "PortConfigV2": { + "type": "object", + "properties": { + "addresses": { + "description": "This port's addresses and optional vlan IDs", + "type": "array", + "items": { + "$ref": "#/components/schemas/UplinkAddressConfig" + } + }, + "autoneg": { + "description": "Whether or not to set autonegotiation", + "default": false, + "type": "boolean" + }, + "bgp_peers": { + "description": "BGP peers on this port", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpPeerConfig" + } + }, + "lldp": { + "nullable": true, + "description": "LLDP configuration for this port", + "allOf": [ + { + "$ref": "#/components/schemas/LldpPortConfig" + } + ] + }, + "port": { + "description": "Nmae of the port this config applies to.", + "type": "string" + }, + "routes": { + "description": "The set of routes associated with this port.", + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteConfig" + } + }, + "switch": { + "description": "Switch the port belongs to.", + "allOf": [ + { + "$ref": "#/components/schemas/SwitchLocation" + } + ] + }, + "tx_eq": { + "nullable": true, + "description": "TX-EQ configuration for this port", + "allOf": [ + { + "$ref": "#/components/schemas/TxEqConfig" + } + ] + }, + "uplink_port_fec": { + "nullable": true, + "description": "Port forward error correction type.", + "allOf": [ + { + "$ref": "#/components/schemas/PortFec" + } + ] + }, + "uplink_port_speed": { + "description": "Port speed.", + "allOf": [ + { + "$ref": "#/components/schemas/PortSpeed" + } + ] + } + }, + "required": [ + "addresses", + "bgp_peers", + "port", + "routes", + "switch", + "uplink_port_speed" + ] + }, + "PortFec": { + "description": "Switchport FEC options", + "type": "string", + "enum": [ + "firecode", + "none", + "rs" + ] + }, + "PortSpeed": { + "description": "Switchport Speed options", + "type": "string", + "enum": [ + "speed0_g", + "speed1_g", + "speed10_g", + "speed25_g", + "speed40_g", + "speed50_g", + "speed100_g", + "speed200_g", + "speed400_g" + ] + }, + "PriorityDimension": { + "description": "A dimension along with bundles can be sorted, to determine priority.", + "oneOf": [ + { + "description": "Sorting by time, with older bundles with lower priority.", + "type": "string", + "enum": [ + "time" + ] + }, + { + "description": "Sorting by the cause for creating the bundle.", + "type": "string", + "enum": [ + "cause" + ] + } + ] + }, + "PriorityOrder": { + "description": "The priority order for bundles during cleanup.\n\nBundles are sorted along the dimensions in [`PriorityDimension`], with each dimension appearing exactly once. During cleanup, lesser-priority bundles are pruned first, to maintain the dataset quota. Note that bundles are sorted by each dimension in the order in which they appear, with each dimension having higher priority than the next.\n\nTODO: The serde deserializer does not currently verify uniqueness of dimensions.", + "type": "array", + "items": { + "$ref": "#/components/schemas/PriorityDimension" + }, + "minItems": 2, + "maxItems": 2 + }, + "PrivateIpConfig": { + "description": "VPC-private IP address configuration for a network interface.", + "oneOf": [ + { + "description": "The interface has only an IPv4 configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v4" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv4Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface has only an IPv6 configuration.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "v6" + ] + }, + "value": { + "$ref": "#/components/schemas/PrivateIpv6Config" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "The interface is dual-stack.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "dual_stack" + ] + }, + "value": { + "type": "object", + "properties": { + "v4": { + "description": "The interface's IPv4 configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/PrivateIpv4Config" + } + ] + }, + "v6": { + "description": "The interface's IPv6 configuration.", + "allOf": [ + { + "$ref": "#/components/schemas/PrivateIpv6Config" + } + ] + } + }, + "required": [ + "v4", + "v6" + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "PrivateIpv4Config": { + "description": "VPC-private IPv4 configuration for a network interface.", + "type": "object", + "properties": { + "ip": { + "description": "VPC-private IP address.", + "type": "string", + "format": "ipv4" + }, + "subnet": { + "description": "The IP subnet.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv4Net" + } + ] + }, + "transit_ips": { + "description": "Additional networks on which the interface can send / receive traffic.", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv4Net" + } + } + }, + "required": [ + "ip", + "subnet" + ] + }, + "PrivateIpv6Config": { + "description": "VPC-private IPv6 configuration for a network interface.", + "type": "object", + "properties": { + "ip": { + "description": "VPC-private IP address.", + "type": "string", + "format": "ipv6" + }, + "subnet": { + "description": "The IP subnet.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Net" + } + ] + }, + "transit_ips": { + "description": "Additional networks on which the interface can send / receive traffic.", + "type": "array", + "items": { + "$ref": "#/components/schemas/Ipv6Net" + } + } + }, + "required": [ + "ip", + "subnet", + "transit_ips" + ] + }, + "ProbeCreate": { + "description": "Parameters used to create a probe.", + "type": "object", + "properties": { + "external_ips": { + "description": "The external IP addresses assigned to the probe.", + "type": "array", + "items": { + "$ref": "#/components/schemas/ExternalIp" + } + }, + "id": { + "description": "The ID for the probe.", + "allOf": [ + { + "$ref": "#/components/schemas/ProbeUuid" + } + ] + }, + "interface": { + "description": "The probe's networking interface.", + "allOf": [ + { + "$ref": "#/components/schemas/NetworkInterface" + } + ] + } + }, + "required": [ + "external_ips", + "id", + "interface" + ] + }, + "ProbeSet": { + "description": "A set of probes that the target sled should run.", + "type": "object", + "properties": { + "probes": { + "title": "IdHashMap", + "description": "The exact set of probes to run.", + "x-rust-type": { + "crate": "iddqd", + "parameters": [ + { + "$ref": "#/components/schemas/ProbeCreate" + } + ], + "path": "iddqd::IdHashMap", + "version": "*" + }, + "type": "array", + "items": { + "$ref": "#/components/schemas/ProbeCreate" + }, + "uniqueItems": true + } + }, + "required": [ + "probes" + ] + }, + "ProbeUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ProbeUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "QemuPvpanic": { + "type": "object", + "properties": { + "enable_isa": { + "description": "Enable the QEMU PVPANIC ISA bus device (I/O port 0x505).", + "type": "boolean" + } + }, + "required": [ + "enable_isa" + ], + "additionalProperties": false + }, + "RackNetworkConfigV2": { + "description": "Initial network configuration", + "type": "object", + "properties": { + "bfd": { + "description": "BFD configuration for connecting the rack to external networks", + "default": [], + "type": "array", + "items": { + "$ref": "#/components/schemas/BfdPeerConfig" + } + }, + "bgp": { + "description": "BGP configurations for connecting the rack to external networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/BgpConfig" + } + }, + "infra_ip_first": { + "description": "First ip address to be used for configuring network infrastructure", + "type": "string", + "format": "ipv4" + }, + "infra_ip_last": { + "description": "Last ip address to be used for configuring network infrastructure", + "type": "string", + "format": "ipv4" + }, + "ports": { + "description": "Uplinks for connecting the rack to external networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/PortConfigV2" + } + }, + "rack_subnet": { + "$ref": "#/components/schemas/Ipv6Net" + } + }, + "required": [ + "bgp", + "infra_ip_first", + "infra_ip_last", + "ports", + "rack_subnet" + ] + }, + "ReconciledSingleMeasurement": { + "description": "An attempt at resolving a single measurement file to a valid path", + "type": "object", + "properties": { + "file_name": { + "type": "string" + }, + "path": { + "type": "string", + "format": "Utf8PathBuf" + }, + "result": { + "$ref": "#/components/schemas/ConfigReconcilerInventoryResult" + } + }, + "required": [ + "file_name", + "path", + "result" + ] + }, + "RemoveMupdateOverrideBootSuccessInventory": { + "description": "Status of removing the mupdate override on the boot disk.", + "oneOf": [ + { + "description": "The mupdate override was successfully removed.", + "type": "string", + "enum": [ + "removed" + ] + }, + { + "description": "No mupdate override was found.\n\nThis is considered a success for idempotency reasons.", + "type": "string", + "enum": [ + "no_override" + ] + } + ] + }, + "RemoveMupdateOverrideInventory": { + "description": "Status of removing the mupdate override in the inventory.", + "type": "object", + "properties": { + "boot_disk_result": { + "description": "The result of removing the mupdate override on the boot disk.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "$ref": "#/components/schemas/RemoveMupdateOverrideBootSuccessInventory" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "$ref": "#/components/schemas/RemoveMupdateOverrideBootSuccessInventory" + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + }, + "non_boot_message": { + "description": "What happened on non-boot disks.\n\nWe aren't modeling this out in more detail, because we plan to not try and keep ledgered data in sync across both disks in the future.", + "type": "string" + } + }, + "required": [ + "boot_disk_result", + "non_boot_message" + ] + }, + "ResolvedVpcFirewallRule": { + "description": "VPC firewall rule after object name resolution has been performed by Nexus", + "type": "object", + "properties": { + "action": { + "$ref": "#/components/schemas/VpcFirewallRuleAction" + }, + "direction": { + "$ref": "#/components/schemas/VpcFirewallRuleDirection" + }, + "filter_hosts": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/HostIdentifier" + }, + "uniqueItems": true + }, + "filter_ports": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/L4PortRange" + } + }, + "filter_protocols": { + "nullable": true, + "type": "array", + "items": { + "$ref": "#/components/schemas/VpcFirewallRuleProtocol" + } + }, + "priority": { + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "status": { + "$ref": "#/components/schemas/VpcFirewallRuleStatus" + }, + "targets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NetworkInterface" + } + } + }, + "required": [ + "action", + "direction", + "priority", + "status", + "targets" + ] + }, + "ResolvedVpcRoute": { + "description": "A VPC route resolved into a concrete target.", + "type": "object", + "properties": { + "dest": { + "$ref": "#/components/schemas/IpNet" + }, + "target": { + "$ref": "#/components/schemas/RouterTarget" + } + }, + "required": [ + "dest", + "target" + ] + }, + "ResolvedVpcRouteSet": { + "description": "An updated set of routes for a given VPC and/or subnet.", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RouterId" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcRoute" + }, + "uniqueItems": true + }, + "version": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RouterVersion" + } + ] + } + }, + "required": [ + "id", + "routes" + ] + }, + "ResolvedVpcRouteState": { + "description": "Version information for routes on a given VPC subnet.", + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/RouterId" + }, + "version": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RouterVersion" + } + ] + } + }, + "required": [ + "id" + ] + }, + "RouteConfig": { + "type": "object", + "properties": { + "destination": { + "description": "The destination of the route.", + "allOf": [ + { + "$ref": "#/components/schemas/IpNet" + } + ] + }, + "nexthop": { + "description": "The nexthop/gateway address.", + "type": "string", + "format": "ip" + }, + "rib_priority": { + "nullable": true, + "description": "The RIB priority (i.e. Admin Distance) associated with this route.", + "default": null, + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id associated with this route.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "destination", + "nexthop" + ] + }, + "RouterId": { + "description": "Identifier for a VPC and/or subnet.", + "type": "object", + "properties": { + "kind": { + "$ref": "#/components/schemas/RouterKind" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "kind", + "vni" + ] + }, + "RouterKind": { + "description": "The scope of a set of VPC router rules.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "system" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "subnet": { + "$ref": "#/components/schemas/IpNet" + }, + "type": { + "type": "string", + "enum": [ + "custom" + ] + } + }, + "required": [ + "subnet", + "type" + ] + } + ] + }, + "RouterTarget": { + "description": "The target for a given router entry.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "drop" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "internet_gateway" + ] + }, + "value": { + "$ref": "#/components/schemas/InternetGatewayRouterTarget" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "type": "string", + "format": "ip" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc_subnet" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "RouterVersion": { + "description": "Information on the current parent router (and version) of a route set according to the control plane.", + "type": "object", + "properties": { + "router_id": { + "type": "string", + "format": "uuid" + }, + "version": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "router_id", + "version" + ] + }, + "SerialPort": { + "description": "A serial port device.", + "type": "object", + "properties": { + "num": { + "description": "The serial port number for this port.", + "allOf": [ + { + "$ref": "#/components/schemas/SerialPortNumber" + } + ] + } + }, + "required": [ + "num" + ], + "additionalProperties": false + }, + "SerialPortNumber": { + "description": "A serial port identifier, which determines what I/O ports a guest can use to access a port.", + "type": "string", + "enum": [ + "com1", + "com2", + "com3", + "com4" + ] + }, + "SledCpuFamily": { + "description": "Identifies the kind of CPU present on a sled, determined by reading CPUID.\n\nThis is intended to broadly support the control plane answering the question \"can I run this instance on that sled?\" given an instance with either no or some CPU platform requirement. It is not enough information for more precise placement questions - for example, is a CPU a high-frequency part or many-core part? We don't include Genoa here, but in that CPU family there are high frequency parts, many-core parts, and large-cache parts. To support those questions (or satisfactorily answer #8730) we would need to collect additional information and send it along.", + "oneOf": [ + { + "description": "The CPU vendor or its family number don't correspond to any of the known family variants.", + "type": "string", + "enum": [ + "unknown" + ] + }, + { + "description": "AMD Milan processors (or very close). Could be an actual Milan in a Gimlet, a close-to-Milan client Zen 3 part, or Zen 4 (for which Milan is the greatest common denominator).", + "type": "string", + "enum": [ + "amd_milan" + ] + }, + { + "description": "AMD Turin processors (or very close). Could be an actual Turin in a Cosmo, or a close-to-Turin client Zen 5 part.", + "type": "string", + "enum": [ + "amd_turin" + ] + }, + { + "description": "AMD Turin Dense processors. There are no \"Turin Dense-like\" CPUs unlike other cases, so this means a bona fide Zen 5c Turin Dense part.", + "type": "string", + "enum": [ + "amd_turin_dense" + ] + } + ] + }, + "SledDiagnosticsQueryOutput": { + "oneOf": [ + { + "type": "object", + "properties": { + "success": { + "type": "object", + "properties": { + "command": { + "description": "The command and its arguments.", + "type": "string" + }, + "exit_code": { + "nullable": true, + "description": "The exit code if one was present when the command exited.", + "type": "integer", + "format": "int32" + }, + "exit_status": { + "description": "The exit status of the command. This will be the exit code (if any) and exit reason such as from a signal.", + "type": "string" + }, + "stdio": { + "description": "Any stdout/stderr produced by the command.", + "type": "string" + } + }, + "required": [ + "command", + "exit_status", + "stdio" + ] + } + }, + "required": [ + "success" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "failure": { + "type": "object", + "properties": { + "error": { + "description": "The reason the command failed to execute.", + "type": "string" + } + }, + "required": [ + "error" + ] + } + }, + "required": [ + "failure" + ], + "additionalProperties": false + } + ] + }, + "SledIdentifiers": { + "description": "Identifiers for a single sled.\n\nThis is intended primarily to be used in timeseries, to identify sled from which metric data originates.", + "type": "object", + "properties": { + "model": { + "description": "Model name of the sled", + "type": "string" + }, + "rack_id": { + "description": "Control plane ID of the rack this sled is a member of", + "type": "string", + "format": "uuid" + }, + "revision": { + "description": "Revision number of the sled", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "serial": { + "description": "Serial number of the sled", + "type": "string" + }, + "sled_id": { + "description": "Control plane ID for the sled itself", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "model", + "rack_id", + "revision", + "serial", + "sled_id" + ] + }, + "SledRole": { + "description": "Describes the role of the sled within the rack.\n\nNote that this may change if the sled is physically moved within the rack.", + "oneOf": [ + { + "description": "The sled is a general compute sled.", + "type": "string", + "enum": [ + "gimlet" + ] + }, + { + "description": "The sled is attached to the network switch, and has additional responsibilities.", + "type": "string", + "enum": [ + "scrimlet" + ] + } + ] + }, + "SledUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::SledUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "SledVmmState": { + "description": "A wrapper type containing a sled's total knowledge of the state of a VMM.", + "type": "object", + "properties": { + "migration_in": { + "nullable": true, + "description": "The current state of any inbound migration to this VMM.", + "allOf": [ + { + "$ref": "#/components/schemas/MigrationRuntimeState" + } + ] + }, + "migration_out": { + "nullable": true, + "description": "The state of any outbound migration from this VMM.", + "allOf": [ + { + "$ref": "#/components/schemas/MigrationRuntimeState" + } + ] + }, + "vmm_state": { + "description": "The most recent state of the sled's VMM process.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmRuntimeState" + } + ] + } + }, + "required": [ + "vmm_state" + ] + }, + "SoftNpuP9": { + "description": "Describes a PCI device that shares host files with the guest using the P9 protocol.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPciPort": { + "description": "Describes a SoftNPU PCI device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPort": { + "description": "Describes a port in a SoftNPU emulated ASIC.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the port's associated DLPI backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "link_name": { + "description": "The data link name for this port.", + "type": "string" + } + }, + "required": [ + "backend_id", + "link_name" + ], + "additionalProperties": false + }, + "SourceNatConfigGeneric": { + "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used for source NAT, inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external address provided to the instance or service.", + "type": "string", + "format": "ip" + }, + "last_port": { + "description": "The last port used for source NAT, also inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "last_port" + ] + }, + "SourceNatConfigV4": { + "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used for source NAT, inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external address provided to the instance or service.", + "type": "string", + "format": "ipv4" + }, + "last_port": { + "description": "The last port used for source NAT, also inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "last_port" + ] + }, + "SourceNatConfigV6": { + "description": "An IP address and port range used for source NAT, i.e., making outbound network connections from guests or services.", + "type": "object", + "properties": { + "first_port": { + "description": "The first port used for source NAT, inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + }, + "ip": { + "description": "The external address provided to the instance or service.", + "type": "string", + "format": "ipv6" + }, + "last_port": { + "description": "The last port used for source NAT, also inclusive.", + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "first_port", + "ip", + "last_port" + ] + }, + "SpecKey": { + "description": "A key identifying a component in an instance spec.", + "oneOf": [ + { + "title": "uuid", + "allOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + { + "title": "name", + "allOf": [ + { + "type": "string" + } + ] + } + ] + }, + "StartSledAgentRequest": { + "description": "Configuration information for launching a Sled Agent.", + "type": "object", + "properties": { + "body": { + "$ref": "#/components/schemas/StartSledAgentRequestBody" + }, + "generation": { + "description": "The current generation number of data as stored in CRDB.\n\nThe initial generation is set during RSS time and then only mutated by Nexus. For now, we don't actually anticipate mutating this data, but we leave open the possiblity.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "schema_version": { + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "body", + "generation", + "schema_version" + ] + }, + "StartSledAgentRequestBody": { + "description": "This is the actual app level data of `StartSledAgentRequest`\n\nWe nest it below the \"header\" of `generation` and `schema_version` so that we can perform partial deserialization of `EarlyNetworkConfig` to only read the header and defer deserialization of the body once we know the schema version. This is possible via the use of [`serde_json::value::RawValue`] in future (post-v1) deserialization paths.", + "type": "object", + "properties": { + "id": { + "description": "Uuid of the Sled Agent to be created.", + "allOf": [ + { + "$ref": "#/components/schemas/SledUuid" + } + ] + }, + "is_lrtq_learner": { + "description": "Is this node an LRTQ learner node?\n\nWe only put the node into learner mode if `use_trust_quorum` is also true.", + "type": "boolean" + }, + "rack_id": { + "description": "Uuid of the rack to which this sled agent belongs.", + "type": "string", + "format": "uuid" + }, + "subnet": { + "description": "Portion of the IP space to be managed by the Sled Agent.", + "allOf": [ + { + "$ref": "#/components/schemas/Ipv6Subnet" + } + ] + }, + "use_trust_quorum": { + "description": "Use trust quorum for key generation", + "type": "boolean" + } + }, + "required": [ + "id", + "is_lrtq_learner", + "rack_id", + "subnet", + "use_trust_quorum" + ] + }, + "StorageLimit": { + "description": "The limit on space allowed for zone bundles, as a percentage of the overall dataset's quota.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "SupportBundleMetadata": { + "description": "Metadata about a support bundle.", + "type": "object", + "properties": { + "state": { + "$ref": "#/components/schemas/SupportBundleState" + }, + "support_bundle_id": { + "$ref": "#/components/schemas/SupportBundleUuid" + } + }, + "required": [ + "state", + "support_bundle_id" + ] + }, + "SupportBundleState": { + "description": "State of a support bundle.", + "type": "string", + "enum": [ + "complete", + "incomplete" + ] + }, + "SupportBundleUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::SupportBundleUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "SwitchLocation": { + "description": "Identifies switch physical location", + "oneOf": [ + { + "description": "Switch in upper slot", + "type": "string", + "enum": [ + "switch0" + ] + }, + { + "description": "Switch in lower slot", + "type": "string", + "enum": [ + "switch1" + ] + } + ] + }, + "SwitchPorts": { + "description": "A set of switch uplinks.", + "type": "object", + "properties": { + "uplinks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HostPortConfig" + } + } + }, + "required": [ + "uplinks" + ] + }, + "TxEqConfig": { + "description": "Per-port tx-eq overrides. This can be used to fine-tune the transceiver equalization settings to improve signal integrity.", + "type": "object", + "properties": { + "main": { + "nullable": true, + "description": "Main tap", + "type": "integer", + "format": "int32" + }, + "post1": { + "nullable": true, + "description": "Post-cursor tap1", + "type": "integer", + "format": "int32" + }, + "post2": { + "nullable": true, + "description": "Post-cursor tap2", + "type": "integer", + "format": "int32" + }, + "pre1": { + "nullable": true, + "description": "Pre-cursor tap1", + "type": "integer", + "format": "int32" + }, + "pre2": { + "nullable": true, + "description": "Pre-cursor tap2", + "type": "integer", + "format": "int32" + } + } + }, + "UplinkAddressConfig": { + "type": "object", + "properties": { + "address": { + "$ref": "#/components/schemas/IpNet" + }, + "vlan_id": { + "nullable": true, + "description": "The VLAN id (if any) associated with this address.", + "default": null, + "type": "integer", + "format": "uint16", + "minimum": 0 + } + }, + "required": [ + "address" + ] + }, + "VirtioDisk": { + "description": "A disk that presents a virtio-block interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the disk's backend component.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "pci_path": { + "description": "The PCI bus/device/function at which this disk should be attached.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "backend_id", + "pci_path" + ], + "additionalProperties": false + }, + "VirtioNetworkBackend": { + "description": "A network backend associated with a virtio-net (viona) VNIC on the host.", + "type": "object", + "properties": { + "vnic_name": { + "description": "The name of the viona VNIC to use as a backend.", + "type": "string" + } + }, + "required": [ + "vnic_name" + ], + "additionalProperties": false + }, + "VirtioNic": { + "description": "A network card that presents a virtio-net interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the device's backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "interface_id": { + "description": "A caller-defined correlation identifier for this interface. If Propolis is configured to collect network interface kstats in its Oximeter metrics, the metric series for this interface will be associated with this identifier.", + "type": "string", + "format": "uuid" + }, + "pci_path": { + "description": "The PCI path at which to attach this device.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "backend_id", + "interface_id", + "pci_path" + ], + "additionalProperties": false + }, + "VirtualNetworkInterfaceHost": { + "description": "A mapping from a virtual NIC to a physical host", + "type": "object", + "properties": { + "physical_host_ip": { + "type": "string", + "format": "ipv6" + }, + "virtual_ip": { + "type": "string", + "format": "ip" + }, + "virtual_mac": { + "$ref": "#/components/schemas/MacAddr" + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "physical_host_ip", + "virtual_ip", + "virtual_mac", + "vni" + ] + }, + "VmmIssueDiskSnapshotRequestBody": { + "description": "Request body for VMM disk snapshot requests.", + "type": "object", + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "snapshot_id" + ] + }, + "VmmIssueDiskSnapshotRequestResponse": { + "description": "Response for VMM disk snapshot requests.", + "type": "object", + "properties": { + "snapshot_id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "snapshot_id" + ] + }, + "VmmPutStateBody": { + "description": "The body of a request to move a previously-ensured instance into a specific runtime state.", + "type": "object", + "properties": { + "state": { + "description": "The state into which the instance should be driven.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmStateRequested" + } + ] + } + }, + "required": [ + "state" + ] + }, + "VmmPutStateResponse": { + "description": "The response sent from a request to move an instance into a specific runtime state.", + "type": "object", + "properties": { + "updated_runtime": { + "nullable": true, + "description": "The current runtime state of the instance after handling the request to change its state. If the instance's state did not change, this field is `None`.", + "allOf": [ + { + "$ref": "#/components/schemas/SledVmmState" + } + ] + } + } + }, + "VmmRuntimeState": { + "description": "The dynamic runtime properties of an individual VMM process.", + "type": "object", + "properties": { + "gen": { + "description": "The generation number for this VMM's state.", + "allOf": [ + { + "$ref": "#/components/schemas/Generation" + } + ] + }, + "state": { + "description": "The last state reported by this VMM.", + "allOf": [ + { + "$ref": "#/components/schemas/VmmState" + } + ] + }, + "time_updated": { + "description": "Timestamp for the VMM's state.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "gen", + "state", + "time_updated" + ] + }, + "VmmSpec": { + "description": "Specifies the virtual hardware configuration of a new Propolis VMM in the form of a Propolis instance specification.", + "allOf": [ + { + "$ref": "#/components/schemas/InstanceSpecV0" + } + ] + }, + "VmmState": { + "description": "One of the states that a VMM can be in.", + "oneOf": [ + { + "description": "The VMM is initializing and has not started running guest CPUs yet.", + "type": "string", + "enum": [ + "starting" + ] + }, + { + "description": "The VMM has finished initializing and may be running guest CPUs.", + "type": "string", + "enum": [ + "running" + ] + }, + { + "description": "The VMM is shutting down.", + "type": "string", + "enum": [ + "stopping" + ] + }, + { + "description": "The VMM's guest has stopped, and the guest will not run again, but the VMM process may not have released all of its resources yet.", + "type": "string", + "enum": [ + "stopped" + ] + }, + { + "description": "The VMM is being restarted or its guest OS is rebooting.", + "type": "string", + "enum": [ + "rebooting" + ] + }, + { + "description": "The VMM is part of a live migration.", + "type": "string", + "enum": [ + "migrating" + ] + }, + { + "description": "The VMM process reported an internal failure.", + "type": "string", + "enum": [ + "failed" + ] + }, + { + "description": "The VMM process has been destroyed and its resources have been released.", + "type": "string", + "enum": [ + "destroyed" + ] + } + ] + }, + "VmmStateRequested": { + "description": "Requestable running state of an Instance.\n\nA subset of [`omicron_common::api::external::InstanceState`].", + "oneOf": [ + { + "description": "Run this instance by migrating in from a previous running incarnation of the instance.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "migration_target" + ] + }, + "value": { + "$ref": "#/components/schemas/InstanceMigrationTargetParams" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "description": "Start the instance if it is not already running.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "running" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Stop the instance.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "stopped" + ] + } + }, + "required": [ + "type" + ] + }, + { + "description": "Immediately reset the instance, as though it had stopped and immediately began to run again.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "reboot" + ] + } + }, + "required": [ + "type" + ] + } + ] + }, + "VmmUnregisterResponse": { + "description": "The response sent from a request to unregister an instance.", + "type": "object", + "properties": { + "updated_runtime": { + "nullable": true, + "description": "The current state of the instance after handling the request to unregister it. If the instance's state did not change, this field is `None`.", + "allOf": [ + { + "$ref": "#/components/schemas/SledVmmState" + } + ] + } + } + }, + "Vni": { + "description": "A Geneve Virtual Network Identifier", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "VpcFirewallIcmpFilter": { + "type": "object", + "properties": { + "code": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/IcmpParamRange" + } + ] + }, + "icmp_type": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "icmp_type" + ] + }, + "VpcFirewallRuleAction": { + "type": "string", + "enum": [ + "allow", + "deny" + ] + }, + "VpcFirewallRuleDirection": { + "type": "string", + "enum": [ + "inbound", + "outbound" + ] + }, + "VpcFirewallRuleProtocol": { + "description": "The protocols that may be specified in a firewall rule's filter", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "tcp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "udp" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "icmp" + ] + }, + "value": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/VpcFirewallIcmpFilter" + } + ] + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "VpcFirewallRuleStatus": { + "type": "string", + "enum": [ + "disabled", + "enabled" + ] + }, + "VpcFirewallRulesEnsureBody": { + "description": "Update firewall rules for a VPC", + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResolvedVpcFirewallRule" + } + }, + "vni": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "rules", + "vni" + ] + }, + "ZoneArtifactInventory": { + "description": "Inventory representation of a single zone artifact on a boot disk.\n\nPart of [`ManifestBootInventory`].", + "type": "object", + "properties": { + "expected_hash": { + "description": "The expected digest of the file's contents.", + "type": "string", + "format": "hex string (32 bytes)" + }, + "expected_size": { + "description": "The expected size of the file, in bytes.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "file_name": { + "description": "The name of the zone file on disk, for example `nexus.tar.gz`. Zone files are always \".tar.gz\".", + "type": "string" + }, + "path": { + "description": "The full path to the zone file.", + "type": "string", + "format": "Utf8PathBuf" + }, + "status": { + "description": "The status of the artifact.\n\nThis is `Ok(())` if the artifact is present and matches the expected size and digest, or an error message if it is missing or does not match.", + "x-rust-type": { + "crate": "std", + "parameters": [ + { + "type": "null" + }, + { + "type": "string" + } + ], + "path": "::std::result::Result", + "version": "*" + }, + "oneOf": [ + { + "type": "object", + "properties": { + "ok": { + "type": "string", + "enum": [ + null + ] + } + }, + "required": [ + "ok" + ] + }, + { + "type": "object", + "properties": { + "err": { + "type": "string" + } + }, + "required": [ + "err" + ] + } + ] + } + }, + "required": [ + "expected_hash", + "expected_size", + "file_name", + "path", + "status" + ] + }, + "ZoneBundleCause": { + "description": "The reason or cause for a zone bundle, i.e., why it was created.", + "oneOf": [ + { + "description": "Some other, unspecified reason.", + "type": "string", + "enum": [ + "other" + ] + }, + { + "description": "A zone bundle taken when a sled agent finds a zone that it does not expect to be running.", + "type": "string", + "enum": [ + "unexpected_zone" + ] + }, + { + "description": "An instance zone was terminated.", + "type": "string", + "enum": [ + "terminated_instance" + ] + } + ] + }, + "ZoneBundleId": { + "description": "An identifier for a zone bundle.", + "type": "object", + "properties": { + "bundle_id": { + "description": "The ID for this bundle itself.", + "type": "string", + "format": "uuid" + }, + "zone_name": { + "description": "The name of the zone this bundle is derived from.", + "type": "string" + } + }, + "required": [ + "bundle_id", + "zone_name" + ] + }, + "ZoneBundleMetadata": { + "description": "Metadata about a zone bundle.", + "type": "object", + "properties": { + "cause": { + "description": "The reason or cause a bundle was created.", + "allOf": [ + { + "$ref": "#/components/schemas/ZoneBundleCause" + } + ] + }, + "id": { + "description": "Identifier for this zone bundle", + "allOf": [ + { + "$ref": "#/components/schemas/ZoneBundleId" + } + ] + }, + "time_created": { + "description": "The time at which this zone bundle was created.", + "type": "string", + "format": "date-time" + }, + "version": { + "description": "A version number for this zone bundle.", + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "required": [ + "cause", + "id", + "time_created", + "version" + ] + }, + "ZoneImageResolverInventory": { + "description": "Inventory representation of zone image resolver status and health.", + "type": "object", + "properties": { + "measurement_manifest": { + "description": "The zone manifest status.", + "allOf": [ + { + "$ref": "#/components/schemas/ManifestInventory" + } + ] + }, + "mupdate_override": { + "$ref": "#/components/schemas/MupdateOverrideInventory" + }, + "zone_manifest": { + "description": "The zone manifest status.", + "allOf": [ + { + "$ref": "#/components/schemas/ManifestInventory" + } + ] + } + }, + "required": [ + "measurement_manifest", + "mupdate_override", + "zone_manifest" + ] + }, + "ZpoolName": { + "title": "The name of a Zpool", + "description": "Zpool names are of the format ox{i,p}_. They are either Internal or External, and should be unique", + "type": "string", + "pattern": "^ox[ip]_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "ZpoolUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::ZpoolUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + }, + "PropolisUuid": { + "x-rust-type": { + "crate": "omicron-uuid-kinds", + "path": "omicron_uuid_kinds::PropolisUuid", + "version": "*" + }, + "type": "string", + "format": "uuid" + } + }, + "responses": { + "Error": { + "description": "Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } +} diff --git a/openapi/sled-agent/sled-agent-latest.json b/openapi/sled-agent/sled-agent-latest.json index 79671bf5429..9e805d0f968 120000 --- a/openapi/sled-agent/sled-agent-latest.json +++ b/openapi/sled-agent/sled-agent-latest.json @@ -1 +1 @@ -sled-agent-11.0.0-5f3d9f.json \ No newline at end of file +sled-agent-12.0.0-7a22ae.json \ No newline at end of file diff --git a/schema/crdb/dbinit.sql b/schema/crdb/dbinit.sql index 879fe5b3309..b1b0d896e2f 100644 --- a/schema/crdb/dbinit.sql +++ b/schema/crdb/dbinit.sql @@ -3914,6 +3914,7 @@ CREATE TABLE IF NOT EXISTS omicron.public.inv_sled_agent ( -- -- The path to the boot disk image file. zone_manifest_boot_disk_path TEXT NOT NULL, + -- The source of the zone manifest on the boot disk: from installinator or -- sled-agent (synthetic). NULL means there is an error reading the zone manifest. zone_manifest_source omicron.public.inv_zone_manifest_source, @@ -3943,6 +3944,22 @@ CREATE TABLE IF NOT EXISTS omicron.public.inv_sled_agent ( -- similar to `usable_hardware_threads` and friends above. cpu_family omicron.public.sled_cpu_family NOT NULL, + -- The path to the boot disk image file. + measurement_manifest_boot_disk_path TEXT NOT NULL, + -- The source of the zone manifest on the boot disk: from installinator or + -- sled-agent (synthetic). NULL means there is an error reading the zone manifest. + measurement_manifest_source omicron.public.inv_zone_manifest_source, + -- The mupdate ID that created the zone manifest if this is from installinator. If + -- this is NULL, then either the zone manifest is synthetic or there was an + -- error reading the zone manifest. + measurement_manifest_mupdate_id UUID, + -- Message describing the status of the zone manifest on the boot disk. If + -- this is NULL, then the zone manifest was successfully read, and the + -- inv_zone_manifest_zone table has entries corresponding to the zone + -- manifest. + measurement_manifest_boot_disk_error TEXT, + + CONSTRAINT reconciler_status_sled_config_present_if_running CHECK ( (reconciler_status_kind = 'running' AND reconciler_status_sled_config IS NOT NULL) @@ -3980,6 +3997,26 @@ CREATE TABLE IF NOT EXISTS omicron.public.inv_sled_agent ( ) ), + -- For the measurement manifest, there are three valid states: + -- 1. Successfully read from installinator (has mupdate_id, no error) + -- 2. Synthetic from sled-agent (no mupdate_id, no error) + -- 3. Error reading (no mupdate_id, has error) + -- + -- This is equivalent to Result. + CONSTRAINT measurement_manifest_consistency CHECK ( + (measurement_manifest_source = 'installinator' + AND measurement_manifest_mupdate_id IS NOT NULL + AND measurement_manifest_boot_disk_error IS NULL) + OR (measurement_manifest_source = 'sled-agent' + AND measurement_manifest_mupdate_id IS NULL + AND measurement_manifest_boot_disk_error IS NULL) + OR ( + measurement_manifest_source IS NULL + AND measurement_manifest_mupdate_id IS NULL + AND measurement_manifest_boot_disk_error IS NOT NULL + ) + ), + -- For the mupdate override, three states are valid: -- 1. No override, no error -- 2. Override, no error @@ -4227,10 +4264,23 @@ CREATE TABLE IF NOT EXISTS omicron.public.inv_omicron_sled_config ( -- NULL is translated to `HostPhase2DesiredContents::CurrentContents` host_phase_2_desired_slot_a STRING(64), host_phase_2_desired_slot_b STRING(64), + + -- measurement contents + measurements STRING(64)[], PRIMARY KEY (inv_collection_id, id) ); +CREATE TABLE IF NOT EXISTS omicron.public.inv_last_reconciliation_measurements ( + inv_collection_id UUID NOT NULL, + sled_id UUID NOT NULL, + file_name TEXT NOT NULL, + path TEXT NOT NULL, + error_message TEXT, + PRIMARY KEY (inv_collection_id, sled_id, file_name) +); + + CREATE TABLE IF NOT EXISTS omicron.public.inv_last_reconciliation_disk_result ( -- where this observation came from -- (foreign key into `inv_collection` table) @@ -4323,6 +4373,18 @@ CREATE TABLE IF NOT EXISTS omicron.public.inv_last_reconciliation_zone_result ( PRIMARY KEY (inv_collection_id, sled_id, zone_id) ); +CREATE TABLE IF NOT EXISTS omicron.public.inv_zone_manifest_measurement ( + inv_collection_id UUID NOT NULL, + sled_id UUID NOT NULL, + zone_file_name TEXT NOT NULL, + path TEXT NOT NULL, + expected_size INT8 NOT NULL, + expected_sha256 STRING(64) NOT NULL, + error TEXT , + PRIMARY KEY (inv_collection_id, sled_id, zone_file_name) +); + + -- A table describing a single zone within a zone manifest collected by inventory. CREATE TABLE IF NOT EXISTS omicron.public.inv_zone_manifest_zone ( -- where this observation came from @@ -4378,6 +4440,32 @@ CREATE TABLE IF NOT EXISTS omicron.public.inv_zone_manifest_non_boot ( PRIMARY KEY (inv_collection_id, sled_id, non_boot_zpool_id) ); +-- A table describing status for a single measurement manifest on a non-boot disk +-- collected by inventory. +CREATE TABLE IF NOT EXISTS omicron.public.inv_measurement_manifest_non_boot ( + -- where this observation came from + -- (foreign key into `inv_collection` table) + inv_collection_id UUID NOT NULL, + + -- unique id for this sled (should be foreign keys into `sled` table, though + -- it's conceivable a sled will report an id that we don't know about) + sled_id UUID NOT NULL, + + -- unique ID for this non-boot disk + non_boot_zpool_id UUID NOT NULL, + + -- The full path to the zone manifest. + path TEXT NOT NULL, + + -- Whether the non-boot disk is in a valid state. + is_valid BOOLEAN NOT NULL, + + -- A message attached to this disk. + message TEXT NOT NULL, + + PRIMARY KEY (inv_collection_id, sled_id, non_boot_zpool_id) +); + -- A table describing status for a single mupdate override on a non-boot disk -- collected by inventory. CREATE TABLE IF NOT EXISTS omicron.public.inv_mupdate_override_non_boot ( @@ -4798,6 +4886,16 @@ CREATE TABLE IF NOT EXISTS omicron.public.bp_sled_metadata ( PRIMARY KEY (blueprint_id, sled_id) ); +CREATE TABLE IF NOT EXISTS omicron.public.bp_single_measurements ( + blueprint_id UUID NOT NULL, + sled_id UUID NOT NULL, + id UUID NOT NULL, + + image_artifact_sha256 STRING(64) NOT NULL, + prune BOOLEAN NOT NULL default false, + PRIMARY KEY (blueprint_id, id) +); + -- description of omicron physical disks specified in a blueprint. CREATE TABLE IF NOT EXISTS omicron.public.bp_omicron_physical_disk ( -- foreign key into the `blueprint` table @@ -7458,7 +7556,7 @@ INSERT INTO omicron.public.db_metadata ( version, target_version ) VALUES - (TRUE, NOW(), NOW(), '213.0.0', NULL) + (TRUE, NOW(), NOW(), '214.0.0', NULL) ON CONFLICT DO NOTHING; COMMIT; diff --git a/schema/crdb/measurements/up01.sql b/schema/crdb/measurements/up01.sql new file mode 100644 index 00000000000..a0352ff309a --- /dev/null +++ b/schema/crdb/measurements/up01.sql @@ -0,0 +1,8 @@ +-- Add measurement image resolver columns to the sled inventory table. +ALTER TABLE omicron.public.inv_sled_agent + ADD COLUMN IF NOT EXISTS measurement_manifest_boot_disk_path TEXT NOT NULL DEFAULT 'old-collection-data-missing', + ADD COLUMN IF NOT EXISTS measurement_manifest_source inv_zone_manifest_source, + ADD COLUMN IF NOT EXISTS measurement_manifest_mupdate_id UUID, + ADD COLUMN IF NOT EXISTS measurement_manifest_boot_disk_error TEXT DEFAULT 'old collection, data missing'; + + diff --git a/schema/crdb/measurements/up02.sql b/schema/crdb/measurements/up02.sql new file mode 100644 index 00000000000..4ea65442658 --- /dev/null +++ b/schema/crdb/measurements/up02.sql @@ -0,0 +1,11 @@ +-- Create table for measurement manifest non-boot disk inventory. +CREATE TABLE IF NOT EXISTS omicron.public.inv_measurement_manifest_non_boot ( + inv_collection_id UUID NOT NULL, + sled_id UUID NOT NULL, + non_boot_zpool_id UUID NOT NULL, + path TEXT NOT NULL, + is_valid BOOLEAN NOT NULL, + message TEXT NOT NULL, + + PRIMARY KEY (inv_collection_id, sled_id, non_boot_zpool_id) +); diff --git a/schema/crdb/measurements/up03.sql b/schema/crdb/measurements/up03.sql new file mode 100644 index 00000000000..d5a69526c29 --- /dev/null +++ b/schema/crdb/measurements/up03.sql @@ -0,0 +1,3 @@ +ALTER TABLE omicron.public.inv_omicron_sled_config + ADD COLUMN IF NOT EXISTS measurements STRING(64)[]; + diff --git a/schema/crdb/measurements/up04.sql b/schema/crdb/measurements/up04.sql new file mode 100644 index 00000000000..a562739e66a --- /dev/null +++ b/schema/crdb/measurements/up04.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS omicron.public.inv_last_reconciliation_measurements ( + inv_collection_id UUID NOT NULL, + sled_id UUID NOT NULL, + file_name TEXT NOT NULL, + path TEXT NOT NULL, + error_message TEXT, + PRIMARY KEY (inv_collection_id, sled_id, file_name) +); + diff --git a/schema/crdb/measurements/up05.sql b/schema/crdb/measurements/up05.sql new file mode 100644 index 00000000000..4ba01d40166 --- /dev/null +++ b/schema/crdb/measurements/up05.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS omicron.public.inv_zone_manifest_measurement ( + inv_collection_id UUID NOT NULL, + sled_id UUID NOT NULL, + zone_file_name TEXT NOT NULL, + path TEXT NOT NULL, + expected_size INT8 NOT NULL, + expected_sha256 STRING(64) NOT NULL, + error TEXT, + PRIMARY KEY (inv_collection_id, sled_id, zone_file_name) +); + diff --git a/schema/crdb/measurements/up06.sql b/schema/crdb/measurements/up06.sql new file mode 100644 index 00000000000..d09f3bf588d --- /dev/null +++ b/schema/crdb/measurements/up06.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS omicron.public.bp_single_measurements ( + blueprint_id UUID NOT NULL, + sled_id UUID NOT NULL, + id UUID NOT NULL, + + image_artifact_sha256 STRING(64) NOT NULL, + prune BOOLEAN NOT NULL default false, + PRIMARY KEY (blueprint_id, id) +); + diff --git a/schema/crdb/measurements/up07.sql b/schema/crdb/measurements/up07.sql new file mode 100644 index 00000000000..73ffc75de4e --- /dev/null +++ b/schema/crdb/measurements/up07.sql @@ -0,0 +1,7 @@ +-- Add zone image resolver columns to the sled inventory table. +ALTER TABLE omicron.public.inv_sled_agent + ALTER COLUMN measurement_manifest_boot_disk_path DROP default, + ALTER COLUMN measurement_manifest_source DROP default, + ALTER COLUMN measurement_manifest_mupdate_id DROP default, + ALTER COLUMN measurement_manifest_boot_disk_error DROP default; + diff --git a/schema/crdb/measurements/up08.sql b/schema/crdb/measurements/up08.sql new file mode 100644 index 00000000000..f3cd2035c1a --- /dev/null +++ b/schema/crdb/measurements/up08.sql @@ -0,0 +1,15 @@ +-- Add constraints for measurement columns. +ALTER TABLE omicron.public.inv_sled_agent + ADD CONSTRAINT IF NOT EXISTS measurement_manifest_consistency CHECK ( + (measurement_manifest_source = 'installinator' + AND measurement_manifest_mupdate_id IS NOT NULL + AND measurement_manifest_boot_disk_error IS NULL) + OR (measurement_manifest_source = 'sled-agent' + AND measurement_manifest_mupdate_id IS NULL + AND measurement_manifest_boot_disk_error IS NULL) + OR ( + measurement_manifest_source IS NULL + AND measurement_manifest_mupdate_id IS NULL + AND measurement_manifest_boot_disk_error IS NOT NULL + ) + ); diff --git a/sled-agent/api/src/lib.rs b/sled-agent/api/src/lib.rs index 615dcb86331..1c821142d5a 100644 --- a/sled-agent/api/src/lib.rs +++ b/sled-agent/api/src/lib.rs @@ -19,7 +19,7 @@ use omicron_common::api::internal::{ SledIdentifiers, SwitchPorts, VirtualNetworkInterfaceHost, }, }; -use sled_agent_types_versions::{latest, v1, v4, v6, v7, v9, v10}; +use sled_agent_types_versions::{latest, v1, v4, v6, v7, v9, v10, v11}; use sled_diagnostics::SledDiagnosticsQueryOutput; api_versions!([ @@ -34,6 +34,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (12, MEASUREMENTS), (11, ADD_DUAL_STACK_EXTERNAL_IP_CONFIG), (10, ADD_DUAL_STACK_SHARED_NETWORK_INTERFACES), (9, DELEGATE_ZVOL_TO_PROPOLIS), @@ -331,7 +332,7 @@ pub trait SledAgentApi { #[endpoint { method = PUT, path = "/omicron-config", - versions = VERSION_ADD_DUAL_STACK_EXTERNAL_IP_CONFIG.. + versions = VERSION_MEASUREMENTS.., }] async fn omicron_config_put( rqctx: RequestContext, @@ -343,17 +344,32 @@ pub trait SledAgentApi { method = PUT, path = "/omicron-config", versions = - VERSION_ADD_DUAL_STACK_SHARED_NETWORK_INTERFACES..VERSION_ADD_DUAL_STACK_EXTERNAL_IP_CONFIG, + VERSION_ADD_DUAL_STACK_EXTERNAL_IP_CONFIG..VERSION_MEASUREMENTS, }] - async fn omicron_config_put_v10( + async fn omicron_config_put_v11( rqctx: RequestContext, - body: TypedBody, + body: TypedBody, ) -> Result { let body = body.try_map(latest::inventory::OmicronSledConfig::try_from)?; Self::omicron_config_put(rqctx, body).await } + #[endpoint { + operation_id = "omicron_config_put", + method = PUT, + path = "/omicron-config", + versions = + VERSION_ADD_DUAL_STACK_SHARED_NETWORK_INTERFACES..VERSION_ADD_DUAL_STACK_EXTERNAL_IP_CONFIG, + }] + async fn omicron_config_put_v10( + rqctx: RequestContext, + body: TypedBody, + ) -> Result { + let body = body.try_map(v11::inventory::OmicronSledConfig::try_from)?; + Self::omicron_config_put_v11(rqctx, body).await + } + #[endpoint { operation_id = "omicron_config_put", method = PUT, @@ -723,12 +739,27 @@ pub trait SledAgentApi { #[endpoint { method = GET, path = "/inventory", - versions = VERSION_ADD_DUAL_STACK_EXTERNAL_IP_CONFIG.., + versions = VERSION_MEASUREMENTS.., }] async fn inventory( rqctx: RequestContext, ) -> Result, HttpError>; + /// Fetch basic information about this sled + #[endpoint { + operation_id = "inventory", + method = GET, + path = "/inventory", + versions = + VERSION_ADD_DUAL_STACK_EXTERNAL_IP_CONFIG..VERSION_MEASUREMENTS, + }] + async fn inventory_v11( + rqctx: RequestContext, + ) -> Result, HttpError> { + let HttpResponseOk(inventory) = Self::inventory(rqctx).await?; + inventory.try_into().map_err(HttpError::from).map(HttpResponseOk) + } + /// Fetch basic information about this sled #[endpoint { operation_id = "inventory", @@ -740,7 +771,7 @@ pub trait SledAgentApi { async fn inventory_v10( rqctx: RequestContext, ) -> Result, HttpError> { - let HttpResponseOk(inventory) = Self::inventory(rqctx).await?; + let HttpResponseOk(inventory) = Self::inventory_v11(rqctx).await?; inventory.try_into().map_err(HttpError::from).map(HttpResponseOk) } diff --git a/sled-agent/config-reconciler/src/ledger.rs b/sled-agent/config-reconciler/src/ledger.rs index f47e7d01081..8cb91b57392 100644 --- a/sled-agent/config-reconciler/src/ledger.rs +++ b/sled-agent/config-reconciler/src/ledger.rs @@ -707,6 +707,7 @@ mod tests { use omicron_uuid_kinds::ZpoolUuid; use sled_agent_types::inventory::HostPhase2DesiredContents; use sled_agent_types::inventory::HostPhase2DesiredSlots; + use sled_agent_types::inventory::OmicronMeasurements; use sled_agent_types::inventory::OmicronZoneConfig; use sled_agent_types::inventory::OmicronZoneImageSource; use sled_agent_types::inventory::OmicronZoneType; @@ -923,6 +924,7 @@ mod tests { zones: IdOrdMap::default(), remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), } } @@ -1125,6 +1127,7 @@ mod tests { .collect(), remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), }; // The ledger task should reject this config due to a missing artifact. diff --git a/sled-agent/config-reconciler/src/ledger/legacy_configs.rs b/sled-agent/config-reconciler/src/ledger/legacy_configs.rs index e6f348e6f02..1e36eee4722 100644 --- a/sled-agent/config-reconciler/src/ledger/legacy_configs.rs +++ b/sled-agent/config-reconciler/src/ledger/legacy_configs.rs @@ -17,6 +17,7 @@ use sled_agent_types::inventory::OmicronSledConfig; use sled_agent_types_versions::v4::inventory::OmicronSledConfig as OmicronSledConfigV4; use sled_agent_types_versions::v4::inventory::OmicronZoneConfig as OmicronZoneConfigV4; use sled_agent_types_versions::v10::inventory::OmicronSledConfig as OmicronSledConfigV10; +use sled_agent_types_versions::v11::inventory::OmicronSledConfig as OmicronSledConfigV11; use slog::Logger; use slog::error; use slog::warn; @@ -51,9 +52,13 @@ pub(super) async fn try_convert_v4_sled_config( "Failed to convert OmicronSledConfigV4 to OmicronSledConfigV10: {e}" ) }); + let new_config: OmicronSledConfigV11 = + new_config.try_into().unwrap_or_else(|e| { + panic!("Failed to convert OmicronSledConfigV10 to V11: {e}") + }); let new_config = new_config.try_into().unwrap_or_else(|e| { panic!( - "Failed to convert OmicronSledConfigV10 to the current version: {e}" + "Failed to convert OmicronSledConfigV11 to the current version: {e}" ) }); write_converted_ledger( @@ -148,9 +153,14 @@ pub(super) async fn convert_legacy_ledgers( .unwrap_or_else(|e| panic!( "Failed to convert OmicronSledConfigV4 to OmicronSledConfigV10: {e}" )); + let sled_config : OmicronSledConfigV11 = OmicronSledConfigV11::try_from(sled_config) + .unwrap_or_else(|e| panic!( + "Failed to convert OmicronSledConfigV10 to OmicronSledConfigV11: {e}" + )); + let sled_config = OmicronSledConfig::try_from(sled_config) .unwrap_or_else(|e| panic!( - "Failed to convert OmicronSledConfigV10 to the current version: {e}" + "Failed to convert OmicronSledConfigV11 to the current version: {e}" )); // Write the newly-merged config to disk. @@ -407,7 +417,9 @@ pub(super) mod tests { .expect("successfully converted config"); let new_as_v10 = OmicronSledConfigV10::try_from(new_as_v4) .expect("successfully converted v4 config to v10"); - let new = OmicronSledConfig::try_from(new_as_v10) + let new_as_v11 = OmicronSledConfigV11::try_from(new_as_v10) + .expect("successfully converted v10 config to v11"); + let new = OmicronSledConfig::try_from(new_as_v11) .expect("successfully converted v10 config to current"); assert_eq!(new, converted); logctx.cleanup_successful(); diff --git a/sled-agent/config-reconciler/src/reconciler_task.rs b/sled-agent/config-reconciler/src/reconciler_task.rs index b0260465f87..3d01a1fc5c2 100644 --- a/sled-agent/config-reconciler/src/reconciler_task.rs +++ b/sled-agent/config-reconciler/src/reconciler_task.rs @@ -236,6 +236,8 @@ impl LatestReconciliationResult { zones: self.zones_inventory.clone(), boot_partitions: self.boot_partitions.clone(), remove_mupdate_override: self.remove_mupdate_override.clone(), + // TODO: this will come in another PR + measurements: IdOrdMap::new(), } } diff --git a/sled-agent/src/rack_setup/plan/service.rs b/sled-agent/src/rack_setup/plan/service.rs index f511f1de056..5717b3c1a8c 100644 --- a/sled-agent/src/rack_setup/plan/service.rs +++ b/sled-agent/src/rack_setup/plan/service.rs @@ -17,13 +17,13 @@ use internal_dns_types::config::{ use internal_dns_types::names::ServiceName; use nexus_types::deployment::{ Blueprint, BlueprintDatasetConfig, BlueprintDatasetDisposition, - BlueprintHostPhase2DesiredSlots, BlueprintPhysicalDiskConfig, - BlueprintPhysicalDiskDisposition, BlueprintSledConfig, BlueprintSource, - BlueprintZoneConfig, BlueprintZoneDisposition, BlueprintZoneImageSource, - BlueprintZoneType, CockroachDbPreserveDowngrade, - OmicronZoneExternalFloatingAddr, OmicronZoneExternalFloatingIp, - OmicronZoneExternalSnatIp, OximeterReadMode, PendingMgsUpdates, - blueprint_zone_type, + BlueprintHostPhase2DesiredSlots, BlueprintMeasurementsDesiredContents, + BlueprintPhysicalDiskConfig, BlueprintPhysicalDiskDisposition, + BlueprintSledConfig, BlueprintSource, BlueprintZoneConfig, + BlueprintZoneDisposition, BlueprintZoneImageSource, BlueprintZoneType, + CockroachDbPreserveDowngrade, OmicronZoneExternalFloatingAddr, + OmicronZoneExternalFloatingIp, OmicronZoneExternalSnatIp, OximeterReadMode, + PendingMgsUpdates, blueprint_zone_type, }; use nexus_types::external_api::views::SledState; use omicron_common::address::{ @@ -921,6 +921,7 @@ impl Plan { host_phase_2: BlueprintHostPhase2DesiredSlots::current_contents(), remove_mupdate_override: None, + measurements: BlueprintMeasurementsDesiredContents::default_contents(), }, ); } diff --git a/sled-agent/src/rack_setup/service.rs b/sled-agent/src/rack_setup/service.rs index aa2b92d9161..53914d046df 100644 --- a/sled-agent/src/rack_setup/service.rs +++ b/sled-agent/src/rack_setup/service.rs @@ -110,8 +110,9 @@ use sled_agent_types::early_networking::{ EarlyNetworkConfig, EarlyNetworkConfigBody, }; use sled_agent_types::inventory::{ - ConfigReconcilerInventoryResult, HostPhase2DesiredSlots, OmicronSledConfig, - OmicronZoneConfig, OmicronZoneType, OmicronZonesConfig, + ConfigReconcilerInventoryResult, HostPhase2DesiredSlots, + OmicronMeasurements, OmicronSledConfig, OmicronZoneConfig, OmicronZoneType, + OmicronZonesConfig, }; use sled_agent_types::rack_init::{ BootstrapAddressDiscovery, RackInitializeRequest as Config, @@ -600,6 +601,7 @@ impl ServiceInner { zones: zones_config.zones.into_iter().collect(), remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), }; self.set_config_on_sled(*sled_address, sled_config).await?; diff --git a/sled-agent/src/sim/sled_agent.rs b/sled-agent/src/sim/sled_agent.rs index 2bf479e3f5e..8553cc5fed2 100644 --- a/sled-agent/src/sim/sled_agent.rs +++ b/sled-agent/src/sim/sled_agent.rs @@ -62,8 +62,8 @@ use sled_agent_types::instance::{ use sled_agent_types::inventory::{ ConfigReconcilerInventory, ConfigReconcilerInventoryStatus, HostPhase2DesiredSlots, Inventory, InventoryDataset, InventoryDisk, - InventoryZpool, OmicronSledConfig, OmicronZonesConfig, SledRole, - ZoneImageResolverInventory, + InventoryZpool, OmicronMeasurements, OmicronSledConfig, OmicronZonesConfig, + SledRole, ZoneImageResolverInventory, }; use sled_agent_types::support_bundle::SupportBundleMetadata; @@ -814,6 +814,7 @@ impl SledAgent { zones: zones_config.zones.into_iter().collect(), remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), }; Ok(Inventory { diff --git a/sled-agent/types/src/zone_images.rs b/sled-agent/types/src/zone_images.rs index 896bcbf5a46..c969e7c8a0b 100644 --- a/sled-agent/types/src/zone_images.rs +++ b/sled-agent/types/src/zone_images.rs @@ -63,8 +63,8 @@ impl ResolverStatus { pub fn to_inventory(&self) -> ZoneImageResolverInventory { ZoneImageResolverInventory { zone_manifest: self.zone_manifest.to_inventory(), + measurement_manifest: self.measurement_manifest.to_inventory(), mupdate_override: self.mupdate_override.to_inventory(), - // Adding the measurement to inventory will come later } } } diff --git a/sled-agent/types/versions/src/impls/inventory.rs b/sled-agent/types/versions/src/impls/inventory.rs index b5a767e09b4..f86656a5e27 100644 --- a/sled-agent/types/versions/src/impls/inventory.rs +++ b/sled-agent/types/versions/src/impls/inventory.rs @@ -23,8 +23,8 @@ use crate::latest::inventory::{ HostPhase2DesiredContents, HostPhase2DesiredSlots, ManifestBootInventory, ManifestInventory, ManifestNonBootInventory, MupdateOverrideBootInventory, MupdateOverrideInventory, MupdateOverrideNonBootInventory, - OmicronSledConfig, OmicronZoneConfig, OmicronZoneImageSource, - OmicronZoneType, OmicronZonesConfig, + OmicronMeasurements, OmicronSledConfig, OmicronZoneConfig, + OmicronZoneImageSource, OmicronZoneType, OmicronZonesConfig, RemoveMupdateOverrideBootSuccessInventory, RemoveMupdateOverrideInventory, ZoneArtifactInventory, ZoneImageResolverInventory, ZoneKind, }; @@ -418,6 +418,7 @@ impl ConfigReconcilerInventory { zones: BTreeMap::new(), remove_mupdate_override: None, boot_partitions: BootPartitionContents::debug_assume_success(), + measurements: IdOrdMap::new(), }; ret.debug_update_assume_success(config); ret @@ -537,6 +538,7 @@ impl ZoneImageResolverInventory { pub fn new_fake() -> ZoneImageResolverInventory { ZoneImageResolverInventory { zone_manifest: ManifestInventory::new_fake(), + measurement_manifest: ManifestInventory::new_fake(), mupdate_override: MupdateOverrideInventory::new_fake(), } } @@ -588,12 +590,19 @@ pub struct ZoneImageResolverInventoryDisplay<'a> { impl fmt::Display for ZoneImageResolverInventoryDisplay<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let ZoneImageResolverInventory { zone_manifest, mupdate_override } = - self.inner; + let ZoneImageResolverInventory { + zone_manifest, + measurement_manifest, + mupdate_override, + } = self.inner; writeln!(f, "zone manifest:")?; let mut indented = IndentWriter::new(" ", f); write!(indented, "{}", zone_manifest.display())?; let f = indented.into_inner(); + writeln!(f, "measurement manifest:")?; + let mut indented = IndentWriter::new(" ", f); + write!(indented, "{}", measurement_manifest.display())?; + let f = indented.into_inner(); writeln!(f, "mupdate override:")?; let mut indented = IndentWriter::new(" ", f); write!(indented, "{}", mupdate_override.display())?; @@ -865,6 +874,7 @@ impl Default for OmicronSledConfig { zones: IdOrdMap::default(), remove_mupdate_override: None, host_phase_2: HostPhase2DesiredSlots::current_contents(), + measurements: OmicronMeasurements::measurements_defaults(), } } } diff --git a/sled-agent/types/versions/src/latest.rs b/sled-agent/types/versions/src/latest.rs index e2cb3ebc880..2b2a1c251d7 100644 --- a/sled-agent/types/versions/src/latest.rs +++ b/sled-agent/types/versions/src/latest.rs @@ -102,17 +102,21 @@ pub mod inventory { pub use crate::v1::inventory::SledCpuFamily; pub use crate::v1::inventory::SledRole; pub use crate::v1::inventory::ZoneArtifactInventory; - pub use crate::v1::inventory::ZoneImageResolverInventory; pub use crate::v1::inventory::ZoneKind; - pub use crate::v11::inventory::ConfigReconcilerInventory; - pub use crate::v11::inventory::ConfigReconcilerInventoryStatus; - pub use crate::v11::inventory::Inventory; - pub use crate::v11::inventory::OmicronSledConfig; pub use crate::v11::inventory::OmicronZoneConfig; pub use crate::v11::inventory::OmicronZoneType; pub use crate::v11::inventory::OmicronZonesConfig; + pub use crate::v12::inventory::ConfigReconcilerInventory; + pub use crate::v12::inventory::ConfigReconcilerInventoryStatus; + pub use crate::v12::inventory::Inventory; + pub use crate::v12::inventory::OmicronMeasurementSetDesiredContents; + pub use crate::v12::inventory::OmicronMeasurements; + pub use crate::v12::inventory::OmicronSledConfig; + pub use crate::v12::inventory::ReconciledSingleMeasurement; + pub use crate::v12::inventory::ZoneImageResolverInventory; + pub use crate::impls::inventory::ManifestBootInventoryDisplay; pub use crate::impls::inventory::ManifestInventoryDisplay; pub use crate::impls::inventory::ManifestNonBootInventoryDisplay; diff --git a/sled-agent/types/versions/src/lib.rs b/sled-agent/types/versions/src/lib.rs index da02546faf7..d6b4f631fa0 100644 --- a/sled-agent/types/versions/src/lib.rs +++ b/sled-agent/types/versions/src/lib.rs @@ -39,6 +39,8 @@ pub mod v1; pub mod v10; #[path = "add_dual_stack_external_ip_config/mod.rs"] pub mod v11; +#[path = "measurements/mod.rs"] +pub mod v12; #[path = "add_switch_zone_operator_policy/mod.rs"] pub mod v3; #[path = "add_nexus_lockstep_port_to_inventory/mod.rs"] diff --git a/sled-agent/types/versions/src/measurements/inventory.rs b/sled-agent/types/versions/src/measurements/inventory.rs new file mode 100644 index 00000000000..d38717b212b --- /dev/null +++ b/sled-agent/types/versions/src/measurements/inventory.rs @@ -0,0 +1,396 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::BTreeMap; +use std::net::SocketAddrV6; + +use chrono::{DateTime, Utc}; +use iddqd::IdOrdItem; +use iddqd::IdOrdMap; +use iddqd::id_upcast; +use omicron_common::api::external; +use omicron_common::{ + api::external::{ByteCount, Generation}, + disk::{DatasetConfig, OmicronPhysicalDiskConfig}, +}; +use omicron_uuid_kinds::SledUuid; +use omicron_uuid_kinds::{DatasetUuid, OmicronZoneUuid}; +use omicron_uuid_kinds::{MupdateOverrideUuid, PhysicalDiskUuid}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use sled_hardware_types::{Baseboard, SledCpuFamily}; +use std::time::Duration; + +use crate::v1; +use crate::v1::inventory::{ + BootPartitionContents, ConfigReconcilerInventoryResult, + HostPhase2DesiredSlots, InventoryDataset, InventoryDisk, InventoryZpool, + ManifestInventory, MupdateOverrideInventory, OrphanedDataset, + RemoveMupdateOverrideInventory, SledRole, +}; +use crate::v11; +use crate::v11::inventory::OmicronZoneConfig; +use camino::Utf8PathBuf; +use daft::Diffable; +use schemars::SchemaGenerator; +use schemars::schema::{Schema, SchemaObject}; +use std::fmt; +use tufaceous_artifact::ArtifactHash; + +/// Identity and basic status information about this sled agent +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct Inventory { + pub sled_id: SledUuid, + pub sled_agent_address: SocketAddrV6, + pub sled_role: SledRole, + pub baseboard: Baseboard, + pub usable_hardware_threads: u32, + pub usable_physical_ram: ByteCount, + pub cpu_family: SledCpuFamily, + pub reservoir_size: ByteCount, + pub disks: Vec, + pub zpools: Vec, + pub datasets: Vec, + pub ledgered_sled_config: Option, + pub reconciler_status: ConfigReconcilerInventoryStatus, + pub last_reconciliation: Option, + pub zone_image_resolver: ZoneImageResolverInventory, +} + +impl TryFrom for v11::inventory::Inventory { + type Error = external::Error; + + fn try_from(value: Inventory) -> Result { + let ledgered_sled_config = + value.ledgered_sled_config.map(TryInto::try_into).transpose()?; + let last_reconciliation = + value.last_reconciliation.map(TryInto::try_into).transpose()?; + let zone_image_resolver = value.zone_image_resolver.try_into()?; + let reconciler_status = value.reconciler_status.try_into()?; + Ok(Self { + sled_id: value.sled_id, + sled_agent_address: value.sled_agent_address, + sled_role: value.sled_role, + baseboard: value.baseboard, + usable_hardware_threads: value.usable_hardware_threads, + usable_physical_ram: value.usable_physical_ram, + cpu_family: value.cpu_family, + reservoir_size: value.reservoir_size, + disks: value.disks, + zpools: value.zpools, + datasets: value.datasets, + ledgered_sled_config, + reconciler_status, + last_reconciliation, + zone_image_resolver, + }) + } +} + +/// Inventory representation of zone image resolver status and health. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema, Serialize)] +pub struct ZoneImageResolverInventory { + /// The zone manifest status. + pub zone_manifest: ManifestInventory, + + /// The zone manifest status. + pub measurement_manifest: ManifestInventory, + + pub mupdate_override: MupdateOverrideInventory, +} + +impl TryFrom + for v1::inventory::ZoneImageResolverInventory +{ + type Error = external::Error; + + fn try_from( + value: ZoneImageResolverInventory, + ) -> Result { + Ok(Self { + zone_manifest: value.zone_manifest, + mupdate_override: value.mupdate_override, + }) + } +} + +/// Describes the last attempt made by the sled-agent-config-reconciler to +/// reconcile the current sled config against the actual state of the sled. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct ConfigReconcilerInventory { + pub last_reconciled_config: OmicronSledConfig, + pub external_disks: + BTreeMap, + pub datasets: BTreeMap, + pub orphaned_datasets: IdOrdMap, + pub zones: BTreeMap, + pub boot_partitions: BootPartitionContents, + pub measurements: IdOrdMap, + /// The result of removing the mupdate override file on disk. + /// + /// `None` if `remove_mupdate_override` was not provided in the sled config. + pub remove_mupdate_override: Option, +} + +impl TryFrom + for v11::inventory::ConfigReconcilerInventory +{ + type Error = external::Error; + + fn try_from(value: ConfigReconcilerInventory) -> Result { + let last_reconciled_config = value.last_reconciled_config.try_into()?; + Ok(Self { + last_reconciled_config, + external_disks: value.external_disks, + datasets: value.datasets, + orphaned_datasets: value.orphaned_datasets, + zones: value.zones, + boot_partitions: value.boot_partitions, + remove_mupdate_override: value.remove_mupdate_override, + }) + } +} + +/// Status of the sled-agent-config-reconciler task. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema, Serialize)] +#[serde(tag = "status", rename_all = "snake_case")] +pub enum ConfigReconcilerInventoryStatus { + /// The reconciler task has not yet run for the first time since sled-agent + /// started. + NotYetRun, + /// The reconciler task is actively running. + Running { + config: Box, + started_at: DateTime, + running_for: Duration, + }, + /// The reconciler task is currently idle, but previously did complete a + /// reconciliation attempt. + /// + /// This variant does not include the `OmicronSledConfig` used in the last + /// attempt, because that's always available via + /// [`ConfigReconcilerInventory::last_reconciled_config`]. + Idle { completed_at: DateTime, ran_for: Duration }, +} + +impl TryFrom + for v11::inventory::ConfigReconcilerInventoryStatus +{ + type Error = external::Error; + + fn try_from( + value: ConfigReconcilerInventoryStatus, + ) -> Result { + match value { + ConfigReconcilerInventoryStatus::NotYetRun => { + Ok(v11::inventory::ConfigReconcilerInventoryStatus::NotYetRun) + } + ConfigReconcilerInventoryStatus::Running { + config, + started_at, + running_for, + } => Ok(v11::inventory::ConfigReconcilerInventoryStatus::Running { + config: Box::new((*config).try_into()?), + started_at, + running_for, + }), + ConfigReconcilerInventoryStatus::Idle { completed_at, ran_for } => { + Ok(v11::inventory::ConfigReconcilerInventoryStatus::Idle { + completed_at, + ran_for, + }) + } + } + } +} + +/// Describes the set of Reconfigurator-managed configuration elements of a sled +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] +pub struct OmicronSledConfig { + pub generation: Generation, + // Serialize and deserialize disks, datasets, and zones as maps for + // backwards compatibility. Newer IdOrdMaps should not use IdOrdMapAsMap. + #[serde( + with = "iddqd::id_ord_map::IdOrdMapAsMap::" + )] + pub disks: IdOrdMap, + #[serde(with = "iddqd::id_ord_map::IdOrdMapAsMap::")] + pub datasets: IdOrdMap, + #[serde(with = "iddqd::id_ord_map::IdOrdMapAsMap::")] + pub zones: IdOrdMap, + pub remove_mupdate_override: Option, + #[serde(default = "HostPhase2DesiredSlots::current_contents")] + pub host_phase_2: HostPhase2DesiredSlots, + #[serde(default = "OmicronMeasurements::measurements_defaults")] + pub measurements: OmicronMeasurements, +} + +impl TryFrom for v11::inventory::OmicronSledConfig { + type Error = external::Error; + + fn try_from(value: OmicronSledConfig) -> Result { + Ok(Self { + generation: value.generation, + disks: value.disks, + datasets: value.datasets, + zones: value.zones, + remove_mupdate_override: value.remove_mupdate_override, + host_phase_2: value.host_phase_2, + }) + } +} + +impl TryFrom for OmicronSledConfig { + type Error = external::Error; + + fn try_from( + value: v11::inventory::OmicronSledConfig, + ) -> Result { + Ok(Self { + generation: value.generation, + disks: value.disks, + datasets: value.datasets, + zones: value.zones, + remove_mupdate_override: value.remove_mupdate_override, + host_phase_2: value.host_phase_2, + measurements: OmicronMeasurements::measurements_defaults(), + }) + } +} + +/// Where the measurement source is located +/// +#[derive( + Clone, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + JsonSchema, + Deserialize, + Serialize, + Diffable, +)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum OmicronMeasurementSetDesiredContents { + /// This measurement source is whatever happens to be on the sled's + /// "install" dataset. + /// + /// This is whatever was put in place at the factory or by the latest + /// MUPdate. The image used here can vary by sled and even over time (if the + /// sled gets MUPdated again). We expect this to be only used for + /// emergencies + InstallDataset, + + /// This measurement source source are the artifacts matching this hash from the TUF + /// artifact store (aka "TUF repo depot"). + /// + /// This originates from TUF repos uploaded to Nexus which are then + /// replicated out to all sleds. + #[serde(rename_all = "snake_case")] + Artifacts { hashes: Vec }, +} + +fn measurement_set_default() -> OmicronMeasurementSetDesiredContents { + OmicronMeasurementSetDesiredContents::InstallDataset +} + +/// Describes the set of reference measurements for a sled +#[derive( + Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq, Hash, +)] +pub struct OmicronMeasurements { + #[serde(default = "measurement_set_default")] + pub measurements: OmicronMeasurementSetDesiredContents, +} + +impl OmicronMeasurements { + pub fn measurements_defaults() -> OmicronMeasurements { + OmicronMeasurements { measurements: measurement_set_default() } + } + pub fn display(&self) -> OmicronMeasurementsDisplay<'_> { + OmicronMeasurementsDisplay { inner: self } + } +} + +/// a displayer for [`OmicronMeasurements`] +pub struct OmicronMeasurementsDisplay<'a> { + inner: &'a OmicronMeasurements, +} + +impl fmt::Display for OmicronMeasurementsDisplay<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let OmicronMeasurements { measurements } = self.inner; + + writeln!(f, "measurements: ")?; + match measurements { + OmicronMeasurementSetDesiredContents::InstallDataset => { + writeln!(f, "install dataset")? + } + OmicronMeasurementSetDesiredContents::Artifacts { hashes } => { + for h in hashes { + writeln!(f, "artifact: {h}")?; + } + } + } + Ok(()) + } +} + +/// An attempt at resolving a single measurement file to a valid path +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema, Serialize)] +pub struct ReconciledSingleMeasurement { + pub file_name: String, + + #[schemars(schema_with = "path_schema")] + pub path: Utf8PathBuf, + pub result: ConfigReconcilerInventoryResult, +} + +impl IdOrdItem for ReconciledSingleMeasurement { + type Key<'a> = String; + fn key(&self) -> Self::Key<'_> { + // XXX uuugghg + self.file_name.clone() + } + id_upcast!(); +} + +impl ReconciledSingleMeasurement { + pub fn display(&self) -> ReconciledSingleMeasurementDisplay<'_> { + ReconciledSingleMeasurementDisplay { inner: self } + } +} + +/// a displayer for [`ReconciledSingleMeasurement`] +pub struct ReconciledSingleMeasurementDisplay<'a> { + inner: &'a ReconciledSingleMeasurement, +} + +impl fmt::Display for ReconciledSingleMeasurementDisplay<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let ReconciledSingleMeasurement { file_name, path, result } = + self.inner; + + write!(f, "{file_name} with path {path}: ")?; + match result { + ConfigReconcilerInventoryResult::Ok => writeln!(f, "ok")?, + ConfigReconcilerInventoryResult::Err { message } => { + writeln!(f, "error : {message}")? + } + } + Ok(()) + } +} + +// Used for schemars to be able to be used with camino: +// See https://github.com/camino-rs/camino/issues/91#issuecomment-2027908513 +fn path_schema(generator: &mut SchemaGenerator) -> Schema { + let mut schema: SchemaObject = ::json_schema(generator).into(); + schema.format = Some("Utf8PathBuf".to_owned()); + schema.into() +} diff --git a/sled-agent/types/versions/src/measurements/mod.rs b/sled-agent/types/versions/src/measurements/mod.rs new file mode 100644 index 00000000000..bab1c776806 --- /dev/null +++ b/sled-agent/types/versions/src/measurements/mod.rs @@ -0,0 +1,10 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Version `MEASUREMENTS` of the Sled Agent API. +//! +//! This version adds support for reference measurements for use with +//! sprockets/TrustQuorum + +pub mod inventory; diff --git a/uuid-kinds/src/lib.rs b/uuid-kinds/src/lib.rs index 1b190e0f0ce..17edc992216 100644 --- a/uuid-kinds/src/lib.rs +++ b/uuid-kinds/src/lib.rs @@ -60,6 +60,7 @@ impl_typed_uuid_kinds! { InternalZpool = {}, LoopbackAddress = {}, MulticastGroup = {}, + Measurement = {}, Mupdate = {}, MupdateOverride = {}, // `OmicronSledConfig`s do not themselves contain IDs, but we generate IDs