diff --git a/pkg/storaged/client.js b/pkg/storaged/client.js
index 9874d3b575e..3122a905a92 100644
--- a/pkg/storaged/client.js
+++ b/pkg/storaged/client.js
@@ -379,6 +379,8 @@ function update_indices() {
}
client.blocks_stratis_stopped_pool = { };
+ client.stratis_stopped_pool_key_description = { };
+ client.stratis_stopped_pool_clevis_info = { };
for (const uuid in client.stratis_manager.StoppedPools) {
const devs = client.stratis_manager.StoppedPools[uuid].devs.v;
for (const d of devs) {
@@ -386,6 +388,22 @@ function update_indices() {
if (block)
client.blocks_stratis_stopped_pool[block.path] = uuid;
}
+ const kinfo = client.stratis_manager.StoppedPools[uuid].key_description;
+ if (kinfo &&
+ kinfo.t == "(bv)" &&
+ kinfo.v[0] &&
+ kinfo.v[1].t == "(bs)" &&
+ kinfo.v[1].v[0]) {
+ client.stratis_stopped_pool_key_description[uuid] = kinfo.v[1].v[1];
+ }
+ const cinfo = client.stratis_manager.StoppedPools[uuid].clevis_info;
+ if (cinfo &&
+ cinfo.t == "(bv)" &&
+ cinfo.v[0] &&
+ cinfo.v[1].t == "(b(ss))" &&
+ cinfo.v[1].v[0]) {
+ client.stratis_stopped_pool_clevis_info[uuid] = cinfo.v[1].v[1];
+ }
}
client.blocks_cleartext = { };
@@ -970,11 +988,11 @@ function stratis3_start() {
return client.stratis_manager.StartPool(uuid, [!!unlock_method, unlock_method || ""]);
};
- client.stratis_create_pool = (name, devs, key_desc) => {
+ client.stratis_create_pool = (name, devs, key_desc, clevis_info) => {
return client.stratis_manager.CreatePool(name, [false, 0],
devs,
key_desc ? [true, key_desc] : [false, ""],
- [false, ["", ""]]);
+ clevis_info ? [true, clevis_info] : [false, ["", ""]]);
};
client.stratis_list_keys = () => {
@@ -986,6 +1004,7 @@ function stratis3_start() {
};
client.features.stratis = true;
+ client.features.stratis_crypto_binding = true;
client.stratis_pools = client.stratis_manager.client.proxies("org.storage.stratis3.pool." +
stratis3_interface_revision,
"/org/storage/stratis3",
diff --git a/pkg/storaged/crypto-keyslots.jsx b/pkg/storaged/crypto-keyslots.jsx
index fbf6afb7f6e..f8e86f5f7d6 100644
--- a/pkg/storaged/crypto-keyslots.jsx
+++ b/pkg/storaged/crypto-keyslots.jsx
@@ -52,7 +52,7 @@ const _ = cockpit.gettext;
/* Tang advertisement utilities
*/
-function get_tang_adv(url) {
+export function get_tang_adv(url) {
return cockpit.spawn(["curl", "-sSf", url + "/adv"], { err: "message" })
.then(JSON.parse)
.catch(error => {
@@ -487,7 +487,7 @@ function parse_url(url) {
}
}
-function validate_url(url) {
+export function validate_url(url) {
if (url.length === 0)
return _("Address cannot be empty");
if (!parse_url(url))
@@ -614,39 +614,41 @@ function add_or_update_tang(dlg, vals, block, url, adv, old_key, passphrase) {
.catch(request_passphrase_on_error_handler(dlg, vals, passphrase, block));
}
-function edit_tang_adv(client, block, key, url, adv, passphrase) {
+export const TangKeyVerification = ({ url, adv }) => {
const parsed = parse_url(url);
const cmd = cockpit.format("ssh $0 tang-show-keys $1", parsed.hostname, parsed.port);
-
const sigkey_thps = compute_sigkey_thps(tang_adv_payload(adv));
+ return (
+
+ {_("Check the key hash with the Tang server.")}
+
+ {_("How to check")}
+
+ {_("In a terminal, run: ")}
+
+ {cmd}
+
+
+
+ {_("Check that the SHA-256 or SHA-1 hash from the command matches this dialog.")}
+
+
+ {_("SHA-256")}
+ { sigkey_thps.map(s => {s.sha256}) }
+
+ {_("SHA-1")}
+ { sigkey_thps.map(s => {s.sha1}) }
+ );
+};
+
+function edit_tang_adv(client, block, key, url, adv, passphrase) {
const dlg = dialog_open({
Title: _("Verify key"),
- Body: (
-
- {_("Check the key hash with the Tang server.")}
-
- {_("How to check")}
-
- {_("In a terminal, run: ")}
-
- {cmd}
-
-
-
- {_("Check that the SHA-256 or SHA-1 hash from the command matches this dialog.")}
-
-
- {_("SHA-256")}
- { sigkey_thps.map(s => {s.sha256}) }
-
- {_("SHA-1")}
- { sigkey_thps.map(s => {s.sha1}) }
-
- ),
+ Body: ,
Fields: existing_passphrase_fields(_("Saving a new passphrase requires unlocking the disk. Please provide a current disk passphrase.")),
Action: {
Title: _("Trust key"),
diff --git a/pkg/storaged/stratis-details.jsx b/pkg/storaged/stratis-details.jsx
index 7ff56069137..834a1620471 100644
--- a/pkg/storaged/stratis-details.jsx
+++ b/pkg/storaged/stratis-details.jsx
@@ -22,6 +22,7 @@ import React from "react";
import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
import { PlusIcon, ExclamationTriangleIcon } from "@patternfly/react-icons";
@@ -29,7 +30,7 @@ import { FilesystemTab, mounting_dialog, is_mounted, is_valid_mount_point, get_f
import { ListingTable } from "cockpit-components-table.jsx";
import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
import { StdDetailsLayout } from "./details.jsx";
-import { StorageButton, StorageBarMenu, StorageMenuItem, StorageUsageBar } from "./storage-controls.jsx";
+import { StorageButton, StorageLink, StorageBarMenu, StorageMenuItem, StorageUsageBar } from "./storage-controls.jsx";
import { SidePanel } from "./side-panel.jsx";
import {
dialog_open,
@@ -48,8 +49,9 @@ import {
} from "./utils.js";
import { fmt_to_fragments } from "utils.jsx";
import { mount_explanation } from "./format-dialog.jsx";
+import { validate_url, get_tang_adv } from "./crypto-keyslots.jsx";
-import { std_reply, with_keydesc, with_stored_passphrase } from "./stratis-utils.js";
+import { std_reply, with_keydesc, with_stored_passphrase, confirm_tang_trust, get_unused_keydesc } from "./stratis-utils.js";
const _ = cockpit.gettext;
@@ -171,6 +173,14 @@ export function validate_pool_name(client, pool, name) {
export const StratisPoolDetails = ({ client, pool }) => {
const filesystems = client.stratis_pool_filesystems[pool.path];
+ const key_desc = (pool.Encrypted &&
+ pool.KeyDescription[0] &&
+ pool.KeyDescription[1][1]);
+ const can_tang = (client.features.stratis_crypto_binding &&
+ pool.Encrypted &&
+ pool.ClevisInfo[0] && // pool has consistent clevis config
+ (!pool.ClevisInfo[1][0] || pool.ClevisInfo[1][1][0] == "tang")); // not bound or bound to "tang"
+ const tang_url = can_tang && pool.ClevisInfo[1][0] ? JSON.parse(pool.ClevisInfo[1][1][1]).url : null;
const forced_options = ["x-systemd.requires=stratis-fstab-setup@" + pool.Uuid + ".service"];
@@ -207,6 +217,140 @@ export const StratisPoolDetails = ({ client, pool }) => {
});
}
+ function add_passphrase() {
+ dialog_open({
+ Title: _("Add passphrase"),
+ Fields: [
+ PassInput("passphrase", _("Passphrase"),
+ { validate: val => !val.length && _("Passphrase cannot be empty") }),
+ PassInput("passphrase2", _("Confirm"),
+ { validate: (val, vals) => vals.passphrase.length && vals.passphrase != val && _("Passphrases do not match") })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: vals => {
+ return get_unused_keydesc(client, pool.Name)
+ .then(keydesc => {
+ return with_stored_passphrase(client, keydesc, vals.passphrase,
+ () => pool.BindKeyring(keydesc))
+ .then(std_reply);
+ });
+ }
+ }
+ });
+ }
+
+ function change_passphrase() {
+ with_keydesc(client, pool, (keydesc, keydesc_set) => {
+ dialog_open({
+ Title: _("Change passphrase"),
+ Fields: [
+ PassInput("old_passphrase", _("Old passphrase"),
+ {
+ visible: vals => !keydesc_set,
+ validate: val => !val.length && _("Passphrase cannot be empty")
+ }),
+ PassInput("new_passphrase", _("New passphrase"),
+ { validate: val => !val.length && _("Passphrase cannot be empty") }),
+ PassInput("new_passphrase2", _("Confirm"),
+ { validate: (val, vals) => vals.new_passphrase.length && vals.new_passphrase != val && _("Passphrases do not match") })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: vals => {
+ function rebind() {
+ return get_unused_keydesc(client, pool.Name)
+ .then(new_keydesc => {
+ return with_stored_passphrase(client, new_keydesc, vals.new_passphrase,
+ () => pool.RebindKeyring(new_keydesc))
+ .then(std_reply);
+ });
+ }
+
+ if (vals.old_passphrase) {
+ return with_stored_passphrase(client, keydesc, vals.old_passphrase, rebind);
+ } else {
+ return rebind();
+ }
+ }
+ }
+ });
+ });
+ }
+
+ function remove_passphrase() {
+ dialog_open({
+ Title: _("Remove passphrase?"),
+ Body:
+
{ fmt_to_fragments(_("Passphrase removal may prevent unlocking $0."), {pool.Name}) }
+
,
+ Action: {
+ DangerButton: true,
+ Title: _("Remove"),
+ action: function (vals) {
+ return pool.UnbindKeyring().then(std_reply);
+ }
+ }
+ });
+ }
+
+ function add_tang() {
+ return with_keydesc(client, pool, (keydesc, keydesc_set) => {
+ dialog_open({
+ Title: _("Add Tang keyserver"),
+ Fields: [
+ TextInput("tang_url", _("Keyserver address"),
+ {
+ validate: validate_url
+ }),
+ PassInput("passphrase", _("Pool passphrase"),
+ {
+ visible: () => !keydesc_set,
+ validate: val => !val.length && _("Passphrase cannot be empty"),
+ explanation: _("Adding a keyserver requires unlocking the pool. Please provide the existing pool passphrase.")
+ })
+ ],
+ Action: {
+ Title: _("Save"),
+ action: function (vals, progress) {
+ return get_tang_adv(vals.tang_url)
+ .then(adv => {
+ function bind() {
+ return pool.BindClevis("tang", JSON.stringify({ url: vals.tang_url, adv }))
+ .then(std_reply);
+ }
+ confirm_tang_trust(vals.tang_url, adv,
+ () => {
+ if (vals.passphrase)
+ return with_stored_passphrase(client, keydesc,
+ vals.passphrase, bind);
+ else
+ return bind();
+ });
+ });
+ }
+ }
+ });
+ });
+ }
+
+ function remove_tang() {
+ dialog_open({
+ Title: _("Remove Tang keyserver?"),
+ Body:
+
{ fmt_to_fragments(_("Remove $0?"), {tang_url}) }
+
{ fmt_to_fragments(_("Keyserver removal may prevent unlocking $0."), {pool.Name}) }
+
,
+ Action: {
+ DangerButton: true,
+ Title: _("Remove"),
+ action: function (vals) {
+ return pool.UnbindClevis().then(std_reply);
+ }
+ }
+ });
+ }
+
function rename() {
dialog_open({
Title: _("Rename Stratis pool"),
@@ -398,6 +542,52 @@ export const StratisPoolDetails = ({ client, pool }) => {
}
+ { pool.Encrypted && client.features.stratis_crypto_binding &&
+
+
+ {_("storage", "Passphrase")}
+
+
+
+ { !key_desc
+ ? {_("Add passphrase")}
+ : <>
+ {_("Change")}
+
+
+ {_("Remove")}
+
+
+ >
+ }
+
+
+
+ }
+ { can_tang &&
+
+
+ {_("storage", "Keyserver")}
+
+
+
+ { tang_url == null
+ ? {_("Add keyserver")}
+ : <>
+ { tang_url }
+
+
+ {_("Remove")}
+
+
+ >
+ }
+
+
+
+ }
@@ -646,24 +836,9 @@ export const StratisPoolDetails = ({ client, pool }) => {
};
export function start_pool(client, uuid, show_devs) {
- const manager = client.stratis_manager;
- const stopped_props = manager.StoppedPools[uuid];
- const devs = stopped_props.devs.v.map(d => d.devnode).sort();
- let key_desc = null;
-
- if (stopped_props.key_description &&
- stopped_props.key_description.t == "(bv)" &&
- stopped_props.key_description.v[0]) {
- if (stopped_props.key_description.v[1].t != "(bs)" ||
- !stopped_props.key_description.v[1].v[0]) {
- dialog_open({
- Title: _("Error"),
- Body: _("This pool can not be unlocked here because its key description is not in the expected format.")
- });
- return;
- }
- key_desc = stopped_props.key_description.v[1].v[1];
- }
+ const devs = client.stratis_manager.StoppedPools[uuid].devs.v.map(d => d.devnode).sort();
+ const key_desc = client.stratis_stopped_pool_key_description[uuid];
+ const clevis_info = client.stratis_stopped_pool_clevis_info[uuid];
function start(unlock_method) {
return client.stratis_start_pool(uuid, unlock_method).then(std_reply);
@@ -691,9 +866,7 @@ export function start_pool(client, uuid, show_devs) {
});
}
- if (!key_desc) {
- return start();
- } else {
+ function unlock_with_keyring() {
return (client.stratis_list_keys()
.catch(() => [{ }])
.then(keys => {
@@ -703,11 +876,21 @@ export function start_pool(client, uuid, show_devs) {
unlock_with_keydesc(key_desc);
}));
}
+
+ if (!key_desc && !clevis_info) {
+ // Not an encrypted pool, just start it
+ return start();
+ } else if (key_desc && clevis_info) {
+ return start("clevis").catch(unlock_with_keyring);
+ } else if (!key_desc && clevis_info) {
+ return start("clevis");
+ } else if (key_desc && !clevis_info) {
+ return unlock_with_keyring();
+ }
}
const StratisStoppedPoolSidebar = ({ client, uuid }) => {
- const stopped_props = client.stratis_manager.StoppedPools[uuid];
- const devs = stopped_props.devs.v.map(d => d.devnode).sort();
+ const devs = client.stratis_manager.StoppedPools[uuid].devs.v.map(d => d.devnode).sort();
function render_dev(dev) {
const block = client.slashdevs_block[dev];
@@ -726,13 +909,21 @@ const StratisStoppedPoolSidebar = ({ client, uuid }) => {
};
export const StratisStoppedPoolDetails = ({ client, uuid }) => {
+ const key_desc = client.stratis_stopped_pool_key_description[uuid];
+ const clevis_info = client.stratis_stopped_pool_clevis_info[uuid];
+
+ const encrypted = key_desc || clevis_info;
+ const can_tang = encrypted && (!clevis_info || clevis_info[0] == "tang");
+ const tang_url = (can_tang && clevis_info) ? JSON.parse(clevis_info[1]).url : null;
+
function start() {
return start_pool(client, uuid);
}
+ const actions = {_("Start")};
const header = (
- {_("Start")}> }}>
+
{_("Stopped Stratis pool")}
@@ -741,6 +932,26 @@ export const StratisStoppedPoolDetails = ({ client, uuid }) => {
{_("storage", "UUID")}
{ uuid }
+ { encrypted && client.features.stratis_crypto_binding &&
+
+
+ {_("storage", "Passphrase")}
+
+
+ { key_desc ? cockpit.format(_("using key description $0"), key_desc) : _("none") }
+
+
+ }
+ { can_tang && client.features.stratis_crypto_binding &&
+
+
+ {_("storage", "Keyserver")}
+
+
+ { tang_url || _("none") }
+
+
+ }
diff --git a/pkg/storaged/stratis-panel.jsx b/pkg/storaged/stratis-panel.jsx
index 18b4b2b46a5..9ffcde69178 100644
--- a/pkg/storaged/stratis-panel.jsx
+++ b/pkg/storaged/stratis-panel.jsx
@@ -29,7 +29,8 @@ import { validate_pool_name, start_pool } from "./stratis-details.jsx";
import { StorageButton } from "./storage-controls.jsx";
import { PlayIcon } from "@patternfly/react-icons";
-import { std_reply, get_unused_keydesc, with_stored_passphrase } from "./stratis-utils.js";
+import { std_reply, get_unused_keydesc, with_stored_passphrase, confirm_tang_trust } from "./stratis-utils.js";
+import { validate_url, get_tang_adv } from "./crypto-keyslots.jsx";
const _ = cockpit.gettext;
@@ -119,10 +120,24 @@ export function create_stratis_pool(client) {
value: name,
validate: name => validate_pool_name(client, null, name)
}),
- CheckBoxes("encrypt", "",
+ SelectSpaces("disks", _("Block devices"),
+ {
+ empty_warning: _("No block devices are available."),
+ validate: function (disks) {
+ if (disks.length === 0)
+ return _("At least one block device is needed.");
+ },
+ spaces: get_available_spaces(client)
+ }),
+ CheckBoxes("encrypt_pass", client.features.stratis_crypto_binding ? _("Encryption") : "",
{
fields: [
- { tag: "on", title: _("Encrypt data") }
+ {
+ tag: "on",
+ title: (client.features.stratis_crypto_binding
+ ? _("Use a passphrase")
+ : _("Encrypt data"))
+ }
],
nested_fields: [
PassInput("passphrase", _("Passphrase"),
@@ -131,7 +146,7 @@ export function create_stratis_pool(client) {
if (phrase === "")
return _("Passphrase cannot be empty");
},
- visible: vals => vals.encrypt.on,
+ visible: vals => vals.encrypt_pass.on,
new_password: true
}),
PassInput("passphrase2", _("Confirm"),
@@ -140,20 +155,25 @@ export function create_stratis_pool(client) {
if (phrase2 != vals.passphrase)
return _("Passphrases do not match");
},
- visible: vals => vals.encrypt.on,
+ visible: vals => vals.encrypt_pass.on,
new_password: true
})
]
}),
- SelectSpaces("disks", _("Block devices"),
- {
- empty_warning: _("No block devices are available."),
- validate: function (disks) {
- if (disks.length === 0)
- return _("At least one block device is needed.");
- },
- spaces: get_available_spaces(client)
- })
+ CheckBoxes("encrypt_tang", "",
+ {
+ visible: () => client.features.stratis_crypto_binding,
+ fields: [
+ { tag: "on", title: _("Use a Tang keyserver") }
+ ],
+ nested_fields: [
+ TextInput("tang_url", _("Keyserver address"),
+ {
+ validate: validate_url,
+ visible: vals => vals.encrypt_tang && vals.encrypt_tang.on
+ }),
+ ]
+ })
],
Action: {
Title: _("Create"),
@@ -161,16 +181,28 @@ export function create_stratis_pool(client) {
return prepare_available_spaces(client, vals.disks).then(function (paths) {
const devs = paths.map(p => decode_filename(client.blocks[p].PreferredDevice));
- function create(key_desc) {
- return client.stratis_create_pool(vals.name, devs, key_desc).then(std_reply);
+ function create(key_desc, adv) {
+ let clevis_info = null;
+ if (adv)
+ clevis_info = ["tang", JSON.stringify({ url: vals.tang_url, adv })];
+ return client.stratis_create_pool(vals.name, devs, key_desc, clevis_info).then(std_reply);
+ }
+
+ function create2(adv) {
+ if (vals.encrypt_pass.on) {
+ return get_unused_keydesc(client, vals.name)
+ .then(keydesc => with_stored_passphrase(client, keydesc, vals.passphrase,
+ () => create(keydesc, adv)));
+ } else {
+ return create(false, adv);
+ }
}
- if (vals.encrypt.on) {
- return get_unused_keydesc(client, vals.name)
- .then(keydesc => with_stored_passphrase(client, keydesc, vals.passphrase,
- () => create(keydesc)));
+ if (vals.encrypt_tang && vals.encrypt_tang.on) {
+ return get_tang_adv(vals.tang_url)
+ .then(adv => confirm_tang_trust(vals.tang_url, adv, () => create2(adv)));
} else {
- return create(false);
+ return create2(false);
}
});
}
diff --git a/pkg/storaged/stratis-utils.js b/pkg/storaged/stratis-utils.js
index 380d7733f5f..98590a47e5d 100644
--- a/pkg/storaged/stratis-utils.js
+++ b/pkg/storaged/stratis-utils.js
@@ -17,6 +17,14 @@
* along with Cockpit; If not, see .
*/
+import cockpit from "cockpit";
+import React from "react";
+
+import { dialog_open } from "./dialog.jsx";
+import { TangKeyVerification } from "./crypto-keyslots.jsx";
+
+const _ = cockpit.gettext;
+
export function std_reply(result, code, message) {
if (code)
return Promise.reject(message);
@@ -60,3 +68,14 @@ export function get_unused_keydesc(client, desc_prefix) {
return desc;
});
}
+
+export function confirm_tang_trust(url, adv, action) {
+ dialog_open({
+ Title: _("Verify key"),
+ Body: ,
+ Action: {
+ Title: _("Trust key"),
+ action
+ }
+ });
+}
diff --git a/test/reference b/test/reference
index 1f6b16a7bcd..c12af9a76be 160000
--- a/test/reference
+++ b/test/reference
@@ -1 +1 @@
-Subproject commit 1f6b16a7bcd1817c84c85a96f7c9ec906f52d678
+Subproject commit c12af9a76bea24c6602e35f7ee77f74e3264cfd8
diff --git a/test/verify/check-storage-stratis b/test/verify/check-storage-stratis
index abd5961a5f9..90c45f6c753 100755
--- a/test/verify/check-storage-stratis
+++ b/test/verify/check-storage-stratis
@@ -32,6 +32,8 @@ class TestStorageStratis(storagelib.StorageCase):
self.machine.execute("systemctl enable --now stratisd")
self.addCleanup(self.machine.execute, "systemctl disable --now stratisd")
+ self.stratis_v2 = self.image.startswith("rhel-8-9") or self.image == "centos-8-stream"
+
def testBasic(self):
m = self.machine
b = self.browser
@@ -81,7 +83,7 @@ class TestStorageStratis(storagelib.StorageCase):
self.dialog_cancel()
self.dialog_wait_close()
- if not m.image.startswith("rhel-8-9") and m.image != "centos-8-stream":
+ if not self.stratis_v2:
# Stop the pool (only works with Stratis 3)
pool_uuid = m.execute("stratis --unhyphenated-uuids pool list --name pool0 | grep ^UUID | cut -d' ' -f2").strip()
m.execute("stratis pool stop pool0")
@@ -275,6 +277,8 @@ class TestStorageStratis(storagelib.StorageCase):
m.add_disk("4G", serial="DISK2")
b.wait_in_text("#drives", dev_2)
+ passphrase = "foodeeboodeebar"
+
# Create an encrypted pool with a filesystem, but don't mount
# it. Cockpit will chose a key description for the pool and
# we occupy its first choice in order to force Cockpit to use
@@ -283,9 +287,9 @@ class TestStorageStratis(storagelib.StorageCase):
self.dialog_open_with_retry(trigger=lambda: self.devices_dropdown("Create Stratis pool"),
expect=lambda: (self.dialog_is_present('disks', dev_1) and
self.dialog_check({"name": "pool0"})))
- self.dialog_set_val("encrypt.on", val=True)
- self.dialog_set_val("passphrase", "foodeeboodeebar")
- self.dialog_set_val("passphrase2", "foodeeboodeebar")
+ self.dialog_set_val("encrypt_pass.on", val=True)
+ self.dialog_set_val("passphrase", passphrase)
+ self.dialog_set_val("passphrase2", passphrase)
self.dialog_set_val("disks", {dev_1: True})
b.assert_pixels("#dialog", "create-encrypted-pool")
self.dialog_apply()
@@ -296,6 +300,7 @@ class TestStorageStratis(storagelib.StorageCase):
b.click('.sidepanel-row:contains("pool0")')
b.wait_visible('#storage-detail')
b.wait_in_text('#detail-header', "Encrypted Stratis pool pool0")
+
b.click("button:contains(Create new filesystem)")
self.dialog({'name': 'fsys1',
'mount_point': '/run/fsys1',
@@ -312,12 +317,26 @@ class TestStorageStratis(storagelib.StorageCase):
self.dialog_set_val('disks', {dev_2: True})
self.dialog_apply()
self.dialog_wait_error("passphrase", "Passphrase cannot be empty")
- self.dialog_set_val('passphrase', "foodeeboodeebar")
+ self.dialog_set_val('passphrase', passphrase)
self.dialog_apply()
self.dialog_wait_close()
b.wait_in_text('#detail-sidebar', dev_2)
b.wait_in_text(f'#detail-sidebar .sidepanel-row:contains({dev_2})', "data")
+ # Change the passphrase (if supported)
+ if not self.stratis_v2:
+ b.wait_visible('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Remove)[aria-disabled=true]')
+ b.click('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Change)')
+ self.dialog({'old_passphrase': passphrase,
+ 'new_passphrase': "boodeefoodeebar",
+ 'new_passphrase2': "boodeefoodeebar"})
+ # do it again, with the old passphrase in the keyring
+ m.execute("echo boodeefoodeebar | stratis key set pool0 --capture-key")
+ b.click('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Change)')
+ self.dialog({'new_passphrase': passphrase,
+ 'new_passphrase2': passphrase})
+ m.execute("stratis key unset pool0")
+
m.reboot()
m.start_cockpit()
b.relogin()
@@ -334,7 +353,7 @@ class TestStorageStratis(storagelib.StorageCase):
self.dialog_set_val('passphrase', "wrong-passphrase")
self.dialog_apply()
b.wait_visible("#dialog .pf-v5-c-alert.pf-m-danger")
- self.dialog_set_val('passphrase', "foodeeboodeebar")
+ self.dialog_set_val('passphrase', passphrase)
self.dialog_apply()
self.dialog_wait_close()
b.wait_not_in_text('#detail-header', "Stopped")
@@ -346,7 +365,7 @@ class TestStorageStratis(storagelib.StorageCase):
self.wait_mounted(1, 1)
# Reboot (this requires the passphrase)
- self.setup_systemd_password_agent("foodeeboodeebar")
+ self.setup_systemd_password_agent(passphrase)
m.reboot()
m.start_cockpit()
b.relogin()
@@ -548,5 +567,171 @@ class TestStoragePackagesStratis(packagelib.PackageCase, storagelib.StorageCase)
b.wait_not_present("#devices .pf-v5-c-dropdown a:contains('Create Stratis pool')")
+@testlib.skipImage("No Stratis", "debian-*", "ubuntu-*")
+@testlib.skipImage("Stratis too old", "rhel-8-*", "centos-8-*")
+class TestStorageStratisNBDE(packagelib.PackageCase, storagelib.StorageCase):
+ provision = {
+ "0": {"address": "10.111.112.1/20", "memory_mb": 2048},
+ "tang": {"address": "10.111.112.5/20"}
+ }
+
+ def setUp(self):
+ super().setUp()
+
+ if self.image == "arch":
+ # Arch Linux does not enable systemd units by default
+ self.machine.execute("systemctl enable --now stratisd")
+ self.addCleanup(self.machine.execute, "systemctl disable --now stratisd")
+
+ def testBasic(self):
+ m = self.machine
+ b = self.browser
+
+ tang_m = self.machines["tang"]
+ tang_m.execute("systemctl start tangd.socket")
+ tang_m.execute("firewall-cmd --add-port 80/tcp")
+
+ self.login_and_go("/storage")
+
+ dev_1 = "/dev/sda"
+ m.add_disk("4G", serial="DISK1")
+ b.wait_in_text("#drives", dev_1)
+
+ dev_2 = "/dev/sdb"
+ m.add_disk("5G", serial="DISK2")
+ b.wait_in_text("#drives", dev_2)
+
+ # Create an encrypted pool with both a passphrase and a keyserver
+ self.dialog_open_with_retry(trigger=lambda: self.devices_dropdown("Create Stratis pool"),
+ expect=lambda: (self.dialog_is_present('disks', dev_1) and
+ self.dialog_check({"name": "pool0"})))
+ self.dialog_set_val("encrypt_pass.on", val=True)
+ self.dialog_set_val("passphrase", "foodeeboodeebar")
+ self.dialog_set_val("passphrase2", "foodeeboodeebar")
+ self.dialog_set_val("encrypt_tang.on", val=True)
+ self.dialog_set_val("tang_url", "10.111.112.5")
+ self.dialog_set_val("disks", {dev_1: True})
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ self.dialog_wait_close()
+
+ b.wait_in_text("#devices", "pool0")
+ b.click('.sidepanel-row:contains("pool0")')
+ b.wait_visible('#storage-detail')
+ b.wait_in_text('#detail-header', "Encrypted Stratis pool pool0")
+ b.wait_in_text('#detail-header', "Passphrase")
+ b.wait_in_text('#detail-header', "Keyserver")
+ b.wait_in_text('#detail-header', "10.111.112.5")
+
+ b.assert_pixels('#detail-header', "header",
+ ignore=['.pf-v5-c-description-list__group:contains(UUID)'])
+
+ # Remove passphrase
+ b.click('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Remove)')
+ self.confirm()
+ b.wait_in_text('#detail-header .pf-v5-c-description-list__group:contains(Passphrase)', "Add passphrase")
+ b.wait_visible('#detail-header .pf-v5-c-description-list__group:contains(Keyserver) button:contains(Remove)[aria-disabled=true]')
+
+ # Stop the pool and start it again. This should not ask
+ # for the passphrase (since there isn't any)
+ m.execute("stratis pool stop pool0")
+ b.wait_in_text('#detail-header', "Stopped Stratis pool")
+ tang_m.execute("systemctl stop tangd.socket")
+ b.click('#detail-header button:contains(Start)')
+ self.dialog_wait_open()
+ b.wait_in_text("#dialog", "Error communicating")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ tang_m.execute("systemctl start tangd.socket")
+ b.click('#detail-header button:contains(Start)')
+ b.wait_not_in_text('#detail-header', "Stopped")
+ b.wait_in_text('#detail-header', "Encrypted Stratis pool pool0")
+
+ # Put passphrase back and do the stopping starting again,
+ # but without tang. This should try clevis but then fall
+ # back to asking for a passphrase.
+
+ b.click('#detail-header .pf-v5-c-description-list__group:contains(Passphrase) button:contains(Add passphrase)')
+ self.dialog({'passphrase': "foodeeboodeebar",
+ 'passphrase2': "foodeeboodeebar"})
+ b.wait_visible('#detail-header .pf-v5-c-description-list__group:contains(Keyserver) button:contains(Remove):not([aria-disabled=true])')
+ m.execute("stratis pool stop pool0")
+ tang_m.execute("systemctl stop tangd.socket")
+ b.click('#detail-header button:contains(Start)')
+ self.dialog_wait_open()
+ self.dialog_set_val("passphrase", "foobar")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ # Finally start tang
+ tang_m.execute("systemctl start tangd.socket")
+ b.click('#detail-header button:contains(Start)')
+ b.wait_not_in_text('#detail-header', "Stopped")
+ b.wait_in_text('#detail-header', "Encrypted Stratis pool pool0")
+
+ # Add a blockdevice. This requires the passphrase.
+
+ b.click('#detail-sidebar .pf-v5-c-card__actions button')
+ self.dialog({'disks': {dev_2: True}, 'passphrase': "foodeeboodeebar"})
+
+ # Remove the keyserver and add it back
+
+ b.click('#detail-header .pf-v5-c-description-list__group:contains(Keyserver) button:contains(Remove)')
+ self.confirm()
+
+ b.click('#detail-header button:contains(Add keyserver)')
+ self.dialog_wait_open()
+ self.dialog_set_val("tang_url", "10.111.112.5")
+ self.dialog_set_val("passphrase", "foodeeboodeebar")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ self.dialog_wait_close()
+ b.wait_in_text('#detail-header', "10.111.112.5")
+
+ # Remove the keyserver and add it back a second time, but try
+ # first with the wrong passphrase already in the keyring
+
+ b.click('#detail-header .pf-v5-c-description-list__group:contains(Keyserver) button:contains(Remove)')
+ self.confirm()
+
+ m.execute("echo foobar | stratis key set pool0 --capture-key")
+ b.click('#detail-header button:contains(Add keyserver)')
+ self.dialog_wait_open()
+ self.dialog_set_val("tang_url", "10.111.112.5")
+ self.dialog_apply()
+ b.wait_in_text("#dialog", "Check the key hash")
+ b.wait_in_text("#dialog", tang_m.execute("tang-show-keys").strip())
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ b.wait_in_text('#dialog', "Command failed")
+ m.execute("stratis key unset pool0")
+ m.execute("echo foodeeboodeebar | stratis key set pool0 --capture-key")
+ self.dialog_apply()
+ with b.wait_timeout(60):
+ self.dialog_wait_close()
+ b.wait_in_text('#detail-header', "10.111.112.5")
+ m.execute("stratis key unset pool0")
+
+ # Create a mounted filesystem and reboot.
+
+ b.click("button:contains(Create new filesystem)")
+ self.dialog({'name': 'fsys1',
+ 'mount_point': '/run/fsys1'})
+ b.wait_in_text("#detail-content", "fsys1")
+ m.reboot()
+ m.start_cockpit()
+ b.relogin()
+ b.enter_page("/storage")
+ b.wait_visible("#storage-detail")
+ self.wait_mounted(1, 1) # should be mounted after boot
+
+
if __name__ == '__main__':
testlib.test_main()