diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..a22d580327 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,213 @@ +--- +name: stratisd CI + +# yamllint disable-line rule:truthy +on: + push: + branches: + - master + - develop-2.2.1 + pull_request: + branches: + - master + - develop-2.2.1 + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + checks: + strategy: + matrix: + include: + # MANDATORY CHECKS USING CURRENT DEVELOPMENT COMPILER + - task: make -f Makefile fmt-travis + toolchain: 1.48.0 + components: rustfmt + - task: make -f Makefile clippy + toolchain: 1.48.0 + components: clippy + # MANDATORY TESTING ON STABLE + - task: make -f Makefile build + toolchain: stable + components: cargo + - task: make -f Makefile build-no-default + toolchain: stable + components: cargo + - task: make -f Makefile build-extras + toolchain: stable + components: cargo + - task: make -f Makefile docs-travis + toolchain: stable + components: cargo + - task: make -f Makefile test + toolchain: stable + components: cargo + - task: > + sudo + PATH=$GITHUB_WORKSPACE/.cargo/bin:$PATH + RUST_LOG=libstratis=info + make -f Makefile test-travis + toolchain: stable + components: cargo + - task: make -f Makefile release + toolchain: stable + components: cargo + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + components: ${{ matrix.components }} + toolchain: ${{ matrix.toolchain }} + override: true + - name: Install dependencies + run: | + sudo apt-get -q update + sudo apt-get -y install libdbus-1-dev libudev-dev libdbus-glib-1-dev + - name: Install dependencies + run: > + sudo + add-apt-repository -y + "deb http://us.archive.ubuntu.com/ubuntu/ eoan main" + - name: Install dependencies + run: | + sudo apt-get -q update + # Update to a more recent version of blkid + sudo apt-get -y install util-linux libblkid-dev + # cryptsetup-bin conflicts with custom built cryptsetup + sudo apt-get remove cryptsetup-bin + sudo apt-get -y install libargon2-0 libjson-c3 + - name: Install dependencies + run: > + wget + "https://github.com/jbaublitz/stratisd/raw/deb/cryptsetup_2.3.0-1_amd64.deb" + - name: Install dependencies + run: | + sudo dpkg -i ./cryptsetup_2.3.0-1_amd64.deb + # Linking fails if libcryptsetup 2.2 is present - must force + # remove due to system dependencies + sudo dpkg --purge --force-all libcryptsetup12 + - name: Test ${{ matrix.task }} on ${{ matrix.toolchain }} toolchain + run: ${{ matrix.task }} + + # ALLOWED FAILURES + allowed_failures: + continue-on-error: true + strategy: + matrix: + include: + # Allowed because a failure may occur after a new Rust stable + # version is released. + - task: make -f Makefile clippy + toolchain: stable + components: clippy + # Run audit on Rust stable. Make it an allowed failure, because: + # * It takes 9 minutes, the longest of any task. + # * It should be an advisory, and should not gate our development. + - task: make -f Makefile audit + toolchain: stable + components: cargo + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + components: ${{ matrix.components }} + toolchain: ${{ matrix.toolchain }} + override: true + - name: Install dependencies + run: | + sudo apt-get -q update + sudo apt-get -y install libdbus-1-dev libudev-dev libdbus-glib-1-dev + - name: Install dependencies + run: > + sudo add-apt-repository -y + "deb http://us.archive.ubuntu.com/ubuntu/ eoan main" + - name: Install dependencies + run: | + sudo apt-get -q update + # Update to a more recent version of blkid + sudo apt-get -y install util-linux libblkid-dev + # cryptsetup-bin conflicts with custom built cryptsetup + sudo apt-get remove cryptsetup-bin + sudo apt-get -y install libargon2-0 libjson-c3 + - name: Install dependencies + run: > + wget + "https://github.com/jbaublitz/stratisd/raw/deb/cryptsetup_2.3.0-1_amd64.deb" + - name: Install dependencies + run: | + sudo dpkg -i ./cryptsetup_2.3.0-1_amd64.deb + # Linking fails if libcryptsetup 2.2 is present - must force + # remove due to system dependencies + sudo dpkg --purge --force-all libcryptsetup12 + - name: Test ${{ matrix.task }} on ${{ matrix.toolchain }} toolchain + run: ${{ matrix.task }} + + python-checks: + strategy: + matrix: + include: + # MANDATORY PYTHON CHECKS ON RECOMMENDED DEVELOPMENT INTERPRETER + - python-version: 3.7.9 + dependencies: > + pylint==2.4.4 + dbus-client-gen==0.4 + dbus-python-client-gen==0.7 + psutil==5.6.7 + pyudev==0.22.0 + networkx==2.3 + requests==2.22.0 + semantic_version==2.6.0 + task: > + (cd developer_tools; make -f Makefile lint) && + (cd tests/client-dbus; PYTHONPATH=./src make -f Makefile lint) + - python-version: 3.7.9 + dependencies: black==19.10b0 isort==4.3.21 + task: > + (cd developer_tools; make fmt-travis) && + (cd tests/client-dbus; make fmt-travis) + # MANDATORY PYTHON CHECKS ON LOWEST SUPPORTED INTERPRETER + - python-version: 3.6.8 + dependencies: > + pylint==2.4.4 + dbus-client-gen==0.4 + dbus-python-client-gen==0.7 + psutil==5.4.3 + pyudev==0.22.0 + networkx==2.3 + requests==2.20.0 + semantic_version==2.6.0 + task: > + (cd developer_tools; make -f Makefile lint) && + (cd tests/client-dbus; PYTHONPATH=./src make -f Makefile lint) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + sudo apt-get -q update + sudo apt-get -y install libdbus-1-dev libudev-dev libdbus-glib-1-dev + pip3 install ${{ matrix.dependencies }} + - name: Run test + run: ${{ matrix.task }} + + # VERIFICATION OF TEST INFRASTRUCTURE + yamllint: + env: + TASK: yamllint + PYTHON: 3.7.6 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ env.PYTHON }} + - name: Install dependencies + run: pip3 install yamllint==1.23.0 + - name: Run yamllint + run: make -f Makefile $TASK diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 48109eb327..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,169 +0,0 @@ ---- -os: linux -# use most recent available distro to support installation of crypsetup 2.2.0 -# or above with as little fuss as possible. -dist: bionic - -addons: - apt: - packages: - - libdbus-1-dev - - libudev-dev - - libdbus-glib-1-dev - -language: rust - -# Use a package which supplies cryptsetup 2.3.0 which is required -# by features added in libcryptsetup-rs that are used in stratisd. -before_install: - - > - sudo - add-apt-repository - -y - "deb http://us.archive.ubuntu.com/ubuntu/ eoan main" - - sudo apt-get -q update - # Update to a more recent version of blkid - - sudo apt-get -y install util-linux libblkid-dev - # cryptsetup-bin conflicts with custom built cryptsetup - - sudo apt-get remove cryptsetup-bin - - sudo apt-get install -y libargon2-0 libjson-c3 - - > - wget - "https://github.com/jbaublitz/stratisd/raw/deb/cryptsetup_2.3.0-1_amd64.deb" - - sudo dpkg -i ./cryptsetup_2.3.0-1_amd64.deb - # Linking fails if libcryptsetup 2.2 is present - must force - # remove due to system dependencies - - sudo dpkg --purge --force-all libcryptsetup12 - -jobs: - fast_finish: true - allow_failures: - # Allow audit task to fail - - env: TASK=audit - # Allow stable clippy task to fail - - rust: stable - env: TASK=clippy - include: - - # MANDATORY CHECKS USING CURRENT DEVELOPMENT COMPILER - - name: "format Rust source using current development toolchain" - rust: 1.47.0 - before_script: - - rustup component add rustfmt - env: TASK=fmt-travis - - name: "lint Rust source using current development toolchain" - rust: 1.47.0 - before_script: - - rustup component add clippy - env: TASK=clippy - - # MANDATORY TESTING ON STABLE - - name: "build using stable toolchain" - rust: stable - env: TASK=build - - name: "build without defaults using stable toolchain" - rust: stable - env: TASK=build-no-default - - name: "build extras using stable toolchain" - rust: stable - env: TASK=build-extras - - name: "build Rust docs using stable toolchain" - rust: stable - env: TASK=docs-travis - - name: "run Rust unit tests using stable toolchain" - rust: stable - env: TASK=test - - name: "run Rust destructive unit tests using stable toolchain" - rust: stable - script: - - > - sudo - PATH=${TRAVIS_HOME}/.cargo/bin:$PATH RUST_LOG=libstratis=info - make - -f - Makefile - test-travis - - name: "build release using stable toolchain" - rust: stable - env: TASK=release - - - # MANDATORY PYTHON CHECKS ON RECOMMENDED DEVELOPMENT INTERPRETER - - name: "lint Python code on recommended development interpreter" - language: python - python: "3.7.9" - install: - - > - pip - install - pylint==2.4.4 - dbus-client-gen==0.4 - dbus-python-client-gen==0.7 - psutil==5.6.7 - pyudev==0.22.0 - networkx==2.3 - requests==2.22.0 - semantic_version==2.6.0 - script: - - > - (cd developer_tools; make -f Makefile lint) && - (cd tests/client-dbus; PYTHONPATH=./src make -f Makefile lint) - - name: "format Python code on recommended development interpreter" - language: python - python: "3.7.9" - install: pip install black==19.10b0 isort==4.3.21 - script: - - > - (cd developer_tools; make fmt-travis) && - (cd tests/client-dbus; make fmt-travis) - - # MANDATORY PYTHON CHECKS ON LOWEST SUPPORTED INTERPRETER - - name: "lint Python code on lowest supported interpreter" - language: python - python: "3.6.8" - install: - - > - pip - install - pylint==2.4.4 - dbus-client-gen==0.4 - dbus-python-client-gen==0.7 - psutil==5.4.3 - pyudev==0.22.0 - networkx==2.3 - requests==2.20.0 - semantic_version==2.6.0 - script: - - > - (cd developer_tools; make -f Makefile lint) && - (cd tests/client-dbus; PYTHONPATH=./src make -f Makefile lint) - - # VERIFICATION OF TEST INFRASTRUCTURE - - name: "run yamllint 1.23.0 on .travis.yml" - language: python - python: "3.7.6" - install: pip install yamllint==1.23.0 - env: TASK=yamllint - - # ALLOWED FAILURES - # Run audit on Rust stable. Make it an allowed failure, because: - # * It takes 9 minutes, the longest of any task. - # * It should be an advisory, and should not gate our development. - - name: "run Rust audit task using stable toolchain" - rust: stable - env: TASK=audit - - # Allowed because a failure may occur after a new Rust stable - # version is released. - - name: "lint Rust source using stable toolchain" - rust: stable - before_script: - - rustup component add clippy - env: TASK=clippy - -branches: - only: - - master - - develop-2.2.0 - -script: make -f Makefile $TASK diff --git a/CHANGES.txt b/CHANGES.txt index 6b8c63dcd0..3035d8956a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,40 @@ +stratisd 2.3.0 +============== +Recommended Rust toolchain version: 1.48.0 +Lowest supported Rust toolchain version: 1.45 + +Recommended Python interpreter: 3.7.9 +Lowest supported Python interpreter: 3.6.8 +Python linter: pylint (2.4.4) +Python auto-formatter: black (19.10b0) +Python import sorter: isort (4.3.21) + +YAML linter: yamllint (1.23.0) + +New Rust crate version requirements: + - error-chain: 0.12.2 + +New external dependencies: + - clevis: 15 + - clevis-luks: 15 + + +- Introduce support for Clevis encryption policies: + https://github.com/stratis-storage/stratisd/pull/2315 + https://github.com/stratis-storage/stratisd/pull/2314 + +- Tidies and Maintenance: + https://github.com/stratis-storage/stratisd/pull/2332 + https://github.com/stratis-storage/stratisd/pull/2330 + https://github.com/stratis-storage/stratisd/pull/2329 + https://github.com/stratis-storage/stratisd/pull/2319 + https://github.com/stratis-storage/stratisd/pull/2317 + https://github.com/stratis-storage/stratisd/pull/2311 + https://github.com/stratis-storage/stratisd/pull/2309 + https://github.com/stratis-storage/stratisd/pull/2304 + https://github.com/stratis-storage/stratisd/pull/2299 + + stratisd 2.2.1 ============== Recommended Rust toolchain version: 1.47.0 @@ -44,6 +81,8 @@ New Rust crate version requirements: https://github.com/stratis-storage/stratisd/pull/2251 - Tidies and Maintenance: + https://github.com/stratis-storage/stratisd/pull/2298 + https://github.com/stratis-storage/stratisd/pull/2297 https://github.com/stratis-storage/stratisd/pull/2296 https://github.com/stratis-storage/stratisd/pull/2295 https://github.com/stratis-storage/stratisd/pull/2293 diff --git a/Cargo.lock b/Cargo.lock index ef838964e2..bd9b37f84d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bindgen" version = "0.54.0" @@ -115,6 +121,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "build_const" version = "0.2.1" @@ -129,9 +144,9 @@ checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "cc" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1770ced377336a88a67c473594ccc14eca6f4559217c34f64aac8f83d641b40" +checksum = "ad9c6140b5a2c7db40ea56eb1821245e5362b44385c05b76288b1a599934ac87" [[package]] name = "cexpr" @@ -202,6 +217,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "cpuid-bool" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" + [[package]] name = "crc" version = "1.8.1" @@ -234,6 +255,15 @@ dependencies = [ "serde", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "either" version = "1.6.1" @@ -255,9 +285,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54532e3223c5af90a6a757c90b5c5521564b07e5e7a958681bcd2afad421cdcd" +checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e" dependencies = [ "atty", "humantime 2.0.1", @@ -268,9 +298,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eab5ee3df98a279d9b316b1af6ac95422127b1290317e6d18c1743c99418b01" +checksum = "fa68f2fb9cae9d37c9b2b3584aba698a2e97f72d7aef7b9f7aa71d8b54ce46fe" dependencies = [ "errno-dragonfly", "libc", @@ -315,6 +345,16 @@ version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.1.15" @@ -454,8 +494,9 @@ dependencies = [ [[package]] name = "libstratis" -version = "2.2.1" +version = "2.3.0" dependencies = [ + "base64", "byteorder", "chrono", "clap", @@ -463,7 +504,7 @@ dependencies = [ "dbus", "devicemapper", "either", - "env_logger 0.8.1", + "env_logger 0.8.2", "error-chain", "itertools", "lazy_static", @@ -484,6 +525,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "sha-1", "tempfile", "termios", "timerfd", @@ -537,9 +579,9 @@ checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" [[package]] name = "memchr" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "miniz_oxide" @@ -611,6 +653,12 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -945,18 +993,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5" +checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8" +checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" dependencies = [ "proc-macro2", "quote", @@ -974,6 +1022,19 @@ dependencies = [ "serde", ] +[[package]] +name = "sha-1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool", + "digest", + "opaque-debug", +] + [[package]] name = "shlex" version = "0.1.1" @@ -1013,9 +1074,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +checksum = "bf11676eb135389f21fcda654382c4859bbfc1d2f36e4425a2f829bb41b1e20e" dependencies = [ "winapi-util", ] @@ -1067,6 +1128,12 @@ dependencies = [ "libc", ] +[[package]] +name = "typenum" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" + [[package]] name = "ucd-trie" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 70e8c2a259..c16ce1d876 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libstratis" -version = "2.2.1" +version = "2.3.0" authors = ["Stratis Developers "] edition = "2018" build = "build.rs" @@ -45,6 +45,8 @@ libcryptsetup-rs = "0.4" semver = "0.11" termios = "0.3" regex = "1" +base64 = "0.13" +sha-1 = "0.9" [dependencies.dbus] version = "0.8" @@ -52,7 +54,7 @@ optional = true [dependencies.libdbus-sys] version = "0.2" -optional=true +optional = true [dependencies.uuid] version = "0.8" @@ -62,7 +64,7 @@ features = ["serde", "v4"] pkg-config = "0.3" [dev-dependencies] -error-chain = "0.12" +error-chain = "0.12.2" loopdev = "0.2" either = "1" proptest = "0.10" diff --git a/Makefile b/Makefile index 37fb872ea7..e5afc7189e 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,9 @@ DENY = -D warnings -D future-incompatible -D unused ${RUST_2018_IDIOMS} # Clippy deny variable, including allows for troublesome lints. # Notable allows: +# map_err_ignore: we generally drop the errors for a reason # option_if_let_else: causing problems with if-else chains +# similar_names: judges "yes" and "res" to be too similar CLIPPY_DENY = -D clippy::pedantic \ -A clippy::cast_possible_wrap \ -A clippy::cast_sign_loss \ @@ -22,6 +24,7 @@ CLIPPY_DENY = -D clippy::pedantic \ -A clippy::find_map \ -A clippy::if_not_else \ -A clippy::items_after_statements \ + -A clippy::map_err_ignore \ -A clippy::map_unwrap_or \ -A clippy::match_same_arms \ -A clippy::match_wildcard_for_single_variants \ @@ -33,6 +36,7 @@ CLIPPY_DENY = -D clippy::pedantic \ -A clippy::option_if_let_else \ -A clippy::redundant-closure-for-method-calls \ -A clippy::shadow_unrelated \ + -A clippy::similar_names \ -A clippy::single_match_else \ -A clippy::too_many_lines \ -A clippy::unseparated_literal_suffix \ @@ -115,7 +119,7 @@ test: RUSTFLAGS="${DENY}" RUST_BACKTRACE=1 cargo test -- --skip real_ --skip loop_ --skip travis_ yamllint: - yamllint --strict .travis.yml + yamllint --strict .github/workflows/main.yml docs: stratisd.8 docs-rust diff --git a/README.md b/README.md index 85e5b98678..337d172c96 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ mailing list, if preferred. ### Setting up for development #### Development Compiler -The version of the compiler recommended for development is 1.47. Other +The version of the compiler recommended for development is 1.48. Other versions of the compiler may disagree with the CI tasks on some points, so should be avoided. diff --git a/src/dbus_api/api/fetch_properties_2_2/methods.rs b/src/dbus_api/api/fetch_properties_2_2/methods.rs index 5601aa7ddb..b346ec0b6a 100644 --- a/src/dbus_api/api/fetch_properties_2_2/methods.rs +++ b/src/dbus_api/api/fetch_properties_2_2/methods.rs @@ -33,10 +33,10 @@ pub fn locked_pools( Ok(engine .locked_pools() .into_iter() - .map(|(u, kd)| { + .map(|(u, info)| { ( u.to_simple_ref().to_string(), - kd.as_application_str().to_string(), + info.key_description.as_application_str().to_string(), ) }) .collect()) diff --git a/src/dbus_api/api/manager_2_1/methods.rs b/src/dbus_api/api/manager_2_1/methods.rs index e983f9e962..625a0bccd7 100644 --- a/src/dbus_api/api/manager_2_1/methods.rs +++ b/src/dbus_api/api/manager_2_1/methods.rs @@ -11,12 +11,11 @@ use dbus::{ use crate::{ dbus_api::{ - api::shared::{create_pool_shared, set_key_shared}, + api::shared::{create_pool_shared, set_key_shared, unlock_pool_shared}, types::TData, util::{engine_to_dbus_err_tuple, get_next_arg, msg_code_ok, msg_string_ok}, }, - engine::{DeleteAction, EngineAction, KeyDescription, PoolUuid}, - stratis::{ErrorEnum, StratisError}, + engine::{DeleteAction, KeyDescription}, }; pub fn create_pool(m: &MethodInfo, TData>) -> MethodResult { @@ -61,45 +60,5 @@ pub fn unset_key(m: &MethodInfo, TData>) -> MethodResult { } pub fn unlock_pool(m: &MethodInfo, TData>) -> MethodResult { - let message: &Message = m.msg; - let mut iter = message.iter_init(); - - let dbus_context = m.tree.get_data(); - let default_return: (_, Vec) = (false, Vec::new()); - let return_message = message.method_return(); - - let pool_uuid_str: &str = get_next_arg(&mut iter, 0)?; - let pool_uuid_result = PoolUuid::parse_str(pool_uuid_str); - let pool_uuid = match pool_uuid_result { - Ok(uuid) => uuid, - Err(e) => { - let e = StratisError::Engine( - ErrorEnum::Invalid, - format!("Malformed UUID passed to UnlockPool: {}", e), - ); - let (rc, rs) = engine_to_dbus_err_tuple(&e); - return Ok(vec![return_message.append3(default_return, rc, rs)]); - } - }; - - let msg = match dbus_context - .engine - .borrow_mut() - .unlock_pool(pool_uuid) - .map(|v| v.changed()) - { - Ok(Some(vec)) => { - let str_uuids: Vec<_> = vec - .into_iter() - .map(|u| u.to_simple_ref().to_string()) - .collect(); - return_message.append3((true, str_uuids), msg_code_ok(), msg_string_ok()) - } - Ok(_) => return_message.append3(default_return, msg_code_ok(), msg_string_ok()), - Err(e) => { - let (rc, rs) = engine_to_dbus_err_tuple(&e); - return_message.append3(default_return, rc, rs) - } - }; - Ok(vec![msg]) + unlock_pool_shared(m, false) } diff --git a/src/dbus_api/api/manager_2_3/api.rs b/src/dbus_api/api/manager_2_3/api.rs new file mode 100644 index 0000000000..9bf8604fd3 --- /dev/null +++ b/src/dbus_api/api/manager_2_3/api.rs @@ -0,0 +1,21 @@ +// 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 http://mozilla.org/MPL/2.0/. + +use dbus::tree::{Factory, MTFn, Method}; + +use crate::dbus_api::{api::manager_2_3::methods::unlock_pool, types::TData}; + +pub fn unlock_pool_method(f: &Factory, TData>) -> Method, TData> { + f.method("UnlockPool", (), unlock_pool) + .in_arg(("pool_uuid", "s")) + .in_arg(("unlock_method", "s")) + // b: true if some encrypted devices were newly opened. + // as: array of device UUIDs converted to Strings of all of the newly opened + // devices. + // + // Rust representation: (bool, Vec) + .out_arg(("result", "(bas)")) + .out_arg(("return_code", "q")) + .out_arg(("return_string", "s")) +} diff --git a/src/dbus_api/api/manager_2_3/methods.rs b/src/dbus_api/api/manager_2_3/methods.rs new file mode 100644 index 0000000000..1e65acef56 --- /dev/null +++ b/src/dbus_api/api/manager_2_3/methods.rs @@ -0,0 +1,11 @@ +// 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 http://mozilla.org/MPL/2.0/. + +use dbus::tree::{MTFn, MethodInfo, MethodResult}; + +use crate::dbus_api::{api::shared::unlock_pool_shared, types::TData}; + +pub fn unlock_pool(m: &MethodInfo, TData>) -> MethodResult { + unlock_pool_shared(m, true) +} diff --git a/src/dbus_api/api/manager_2_3/mod.rs b/src/dbus_api/api/manager_2_3/mod.rs new file mode 100644 index 0000000000..e5e0b87e36 --- /dev/null +++ b/src/dbus_api/api/manager_2_3/mod.rs @@ -0,0 +1,4 @@ +mod api; +mod methods; + +pub use api::unlock_pool_method; diff --git a/src/dbus_api/api/mod.rs b/src/dbus_api/api/mod.rs index 5188c52fa2..186ae72e23 100644 --- a/src/dbus_api/api/mod.rs +++ b/src/dbus_api/api/mod.rs @@ -14,6 +14,7 @@ mod fetch_properties_2_2; mod manager_2_0; mod manager_2_1; mod manager_2_2; +mod manager_2_3; mod report_2_1; mod shared; @@ -53,6 +54,16 @@ pub fn get_base_tree<'a>(dbus_context: DbusContext) -> (Tree, TData> .add_m(manager_2_0::configure_simulator_method(&f)) .add_p(manager_2_0::version_property(&f)), ) + .add( + f.interface(consts::MANAGER_INTERFACE_NAME_2_3, ()) + .add_m(manager_2_1::create_pool_method(&f)) + .add_m(manager_2_2::set_key_method(&f)) + .add_m(manager_2_1::unset_key_method(&f)) + .add_m(manager_2_3::unlock_pool_method(&f)) + .add_m(manager_2_0::destroy_pool_method(&f)) + .add_m(manager_2_0::configure_simulator_method(&f)) + .add_p(manager_2_0::version_property(&f)), + ) .add( f.interface(consts::PROPERTY_FETCH_INTERFACE_NAME_2_1, ()) .add_m(fetch_properties_2_1::get_all_properties_method(&f)) @@ -63,6 +74,11 @@ pub fn get_base_tree<'a>(dbus_context: DbusContext) -> (Tree, TData> .add_m(fetch_properties_2_2::get_all_properties_method(&f)) .add_m(fetch_properties_2_2::get_properties_method(&f)), ) + .add( + f.interface(consts::PROPERTY_FETCH_INTERFACE_NAME_2_3, ()) + .add_m(fetch_properties_2_2::get_all_properties_method(&f)) + .add_m(fetch_properties_2_2::get_properties_method(&f)), + ) .add( f.interface(consts::REPORT_INTERFACE_NAME_2_1, ()) .add_m(report_2_1::get_report_method(&f)), diff --git a/src/dbus_api/api/shared.rs b/src/dbus_api/api/shared.rs index 09f3656d28..b920b6d6b2 100644 --- a/src/dbus_api/api/shared.rs +++ b/src/dbus_api/api/shared.rs @@ -19,7 +19,11 @@ use crate::{ engine_to_dbus_err_tuple, get_next_arg, msg_code_ok, msg_string_ok, tuple_to_option, }, }, - engine::{CreateAction, KeyDescription, MappingCreateAction, Name}, + engine::{ + CreateAction, EngineAction, KeyDescription, MappingCreateAction, Name, PoolUuid, + UnlockMethod, + }, + stratis::{ErrorEnum, StratisError}, }; /// Shared code for the creation of pools using the D-Bus API without the option @@ -167,3 +171,62 @@ pub fn locked_pool_uuids(info: &MethodInfo, TData>) -> Result, TData>, + take_unlock_arg: bool, +) -> MethodResult { + let message: &Message = m.msg; + let mut iter = message.iter_init(); + + let dbus_context = m.tree.get_data(); + let default_return: (_, Vec) = (false, Vec::new()); + let return_message = message.method_return(); + + let pool_uuid_str: &str = get_next_arg(&mut iter, 0)?; + let pool_uuid_result = PoolUuid::parse_str(pool_uuid_str); + let pool_uuid = match pool_uuid_result { + Ok(uuid) => uuid, + Err(e) => { + let e = StratisError::Engine( + ErrorEnum::Invalid, + format!("Malformed UUID passed to UnlockPool: {}", e), + ); + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + }; + let unlock_method = if take_unlock_arg { + let unlock_method_str: &str = get_next_arg(&mut iter, 1)?; + match UnlockMethod::try_from(unlock_method_str) { + Ok(um) => um, + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + } + } else { + UnlockMethod::Keyring + }; + + let msg = match dbus_context + .engine + .borrow_mut() + .unlock_pool(pool_uuid, unlock_method) + .map(|v| v.changed()) + { + Ok(Some(vec)) => { + let str_uuids: Vec<_> = vec + .into_iter() + .map(|u| u.to_simple_ref().to_string()) + .collect(); + return_message.append3((true, str_uuids), msg_code_ok(), msg_string_ok()) + } + Ok(_) => return_message.append3(default_return, msg_code_ok(), msg_string_ok()), + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return_message.append3(default_return, rc, rs) + } + }; + Ok(vec![msg]) +} diff --git a/src/dbus_api/blockdev/mod.rs b/src/dbus_api/blockdev/mod.rs index 6f0664b4dd..482d69f526 100644 --- a/src/dbus_api/blockdev/mod.rs +++ b/src/dbus_api/blockdev/mod.rs @@ -77,6 +77,11 @@ pub fn create_dbus_blockdev<'a>( f.interface(consts::PROPERTY_FETCH_INTERFACE_NAME_2_2, ()) .add_m(fetch_properties_2_0::get_all_properties_method(&f)) .add_m(fetch_properties_2_0::get_properties_method(&f)), + ) + .add( + f.interface(consts::PROPERTY_FETCH_INTERFACE_NAME_2_3, ()) + .add_m(fetch_properties_2_0::get_all_properties_method(&f)) + .add_m(fetch_properties_2_0::get_properties_method(&f)), ); let path = object_path.get_name().to_owned(); diff --git a/src/dbus_api/consts.rs b/src/dbus_api/consts.rs index f62e7c310c..c272faac11 100644 --- a/src/dbus_api/consts.rs +++ b/src/dbus_api/consts.rs @@ -10,11 +10,13 @@ pub const STRATIS_BASE_SERVICE: &str = "org.storage.stratis2"; pub const MANAGER_INTERFACE_NAME: &str = "org.storage.stratis2.Manager"; pub const MANAGER_INTERFACE_NAME_2_1: &str = "org.storage.stratis2.Manager.r1"; pub const MANAGER_INTERFACE_NAME_2_2: &str = "org.storage.stratis2.Manager.r2"; +pub const MANAGER_INTERFACE_NAME_2_3: &str = "org.storage.stratis2.Manager.r3"; pub const REPORT_INTERFACE_NAME_2_1: &str = "org.storage.stratis2.Report.r1"; pub const PROPERTY_FETCH_INTERFACE_NAME: &str = "org.storage.stratis2.FetchProperties"; pub const PROPERTY_FETCH_INTERFACE_NAME_2_1: &str = "org.storage.stratis2.FetchProperties.r1"; pub const PROPERTY_FETCH_INTERFACE_NAME_2_2: &str = "org.storage.stratis2.FetchProperties.r2"; +pub const PROPERTY_FETCH_INTERFACE_NAME_2_3: &str = "org.storage.stratis2.FetchProperties.r3"; pub const KEY_LIST_PROP: &str = "KeyList"; @@ -23,6 +25,7 @@ pub const LOCKED_POOLS: &str = "LockedPools"; pub const POOL_INTERFACE_NAME: &str = "org.storage.stratis2.pool"; pub const POOL_INTERFACE_NAME_2_1: &str = "org.storage.stratis2.pool.r1"; +pub const POOL_INTERFACE_NAME_2_3: &str = "org.storage.stratis2.pool.r3"; pub const POOL_NAME_PROP: &str = "Name"; pub const POOL_UUID_PROP: &str = "Uuid"; pub const POOL_HAS_CACHE_PROP: &str = "HasCache"; @@ -30,6 +33,7 @@ pub const POOL_ENCRYPTED_PROP: &str = "Encrypted"; pub const POOL_ENCRYPTION_KEY_DESC: &str = "KeyDescription"; pub const POOL_TOTAL_SIZE_PROP: &str = "TotalPhysicalSize"; pub const POOL_TOTAL_USED_PROP: &str = "TotalPhysicalUsed"; +pub const POOL_CLEVIS_INFO: &str = "ClevisInfo"; pub const FILESYSTEM_INTERFACE_NAME: &str = "org.storage.stratis2.filesystem"; pub const FILESYSTEM_NAME_PROP: &str = "Name"; @@ -67,10 +71,14 @@ pub fn fetch_properties_interfaces() -> Vec { /// Get a list of all the standard pool interfaces; i.e., all the revisions of /// org.storage.stratis2.pool. pub fn standard_pool_interfaces() -> Vec { - [POOL_INTERFACE_NAME, POOL_INTERFACE_NAME_2_1] - .iter() - .map(|s| (*s).to_string()) - .collect() + [ + POOL_INTERFACE_NAME, + POOL_INTERFACE_NAME_2_1, + POOL_INTERFACE_NAME_2_3, + ] + .iter() + .map(|s| (*s).to_string()) + .collect() } /// Get a list of all the standard filesystem interfaces; i.e., all the diff --git a/src/dbus_api/filesystem/mod.rs b/src/dbus_api/filesystem/mod.rs index 38f76eac1a..1ac2e2fbf0 100644 --- a/src/dbus_api/filesystem/mod.rs +++ b/src/dbus_api/filesystem/mod.rs @@ -62,6 +62,11 @@ pub fn create_dbus_filesystem<'a>( f.interface(consts::PROPERTY_FETCH_INTERFACE_NAME_2_2, ()) .add_m(fetch_properties_2_0::get_all_properties_method(&f)) .add_m(fetch_properties_2_0::get_properties_method(&f)), + ) + .add( + f.interface(consts::PROPERTY_FETCH_INTERFACE_NAME_2_3, ()) + .add_m(fetch_properties_2_0::get_all_properties_method(&f)) + .add_m(fetch_properties_2_0::get_properties_method(&f)), ); let path = object_path.get_name().to_owned(); diff --git a/src/dbus_api/pool/fetch_properties_2_3/api.rs b/src/dbus_api/pool/fetch_properties_2_3/api.rs new file mode 100644 index 0000000000..56ecdb611f --- /dev/null +++ b/src/dbus_api/pool/fetch_properties_2_3/api.rs @@ -0,0 +1,31 @@ +// 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 http://mozilla.org/MPL/2.0/. + +use dbus::tree::{Factory, MTFn, Method}; + +use crate::dbus_api::{ + pool::fetch_properties_2_3::methods::{get_all_properties, get_properties}, + types::TData, +}; + +pub fn get_all_properties_method(f: &Factory, TData>) -> Method, TData> { + f.method("GetAllProperties", (), get_all_properties) + // a{s(bv)}: Dictionary of property names to tuples + // In the tuple: + // b: Indicates whether the property value fetched was successful + // v: If b is true, represents the value for the given property + // If b is false, represents the error returned when fetching the property + .out_arg(("results", "a{s(bv)}")) +} + +pub fn get_properties_method(f: &Factory, TData>) -> Method, TData> { + f.method("GetProperties", (), get_properties) + .in_arg(("properties", "as")) + // a{s(bv)}: Dictionary of property names to tuples + // In the tuple: + // b: Indicates whether the property value fetched was successful + // v: If b is true, represents the value for the given property + // If b is false, represents the error returned when fetching the property + .out_arg(("results", "a{s(bv)}")) +} diff --git a/src/dbus_api/pool/fetch_properties_2_3/methods.rs b/src/dbus_api/pool/fetch_properties_2_3/methods.rs new file mode 100644 index 0000000000..9eadb0ff9b --- /dev/null +++ b/src/dbus_api/pool/fetch_properties_2_3/methods.rs @@ -0,0 +1,57 @@ +// 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 http://mozilla.org/MPL/2.0/. + +use std::collections::HashMap; + +use dbus::{ + arg::{RefArg, Variant}, + tree::{MTFn, MethodInfo, MethodResult}, + Message, +}; +use itertools::Itertools; + +use crate::dbus_api::{ + consts, + pool::shared::{ + get_pool_clevis_info, get_pool_encryption_key_desc, get_pool_has_cache, + get_pool_total_size, get_pool_total_used, + }, + types::TData, + util::result_to_tuple, +}; + +const ALL_PROPERTIES: [&str; 5] = [ + consts::POOL_ENCRYPTION_KEY_DESC, + consts::POOL_HAS_CACHE_PROP, + consts::POOL_TOTAL_SIZE_PROP, + consts::POOL_TOTAL_USED_PROP, + consts::POOL_CLEVIS_INFO, +]; + +fn get_properties_shared( + m: &MethodInfo, TData>, + properties: &mut dyn Iterator, +) -> MethodResult { + let message: &Message = m.msg; + + let return_message = message.method_return(); + + let return_value: HashMap>)> = properties + .unique() + .filter_map(|prop| match prop.as_str() { + consts::POOL_ENCRYPTION_KEY_DESC => { + Some((prop, result_to_tuple(get_pool_encryption_key_desc(m)))) + } + consts::POOL_HAS_CACHE_PROP => Some((prop, result_to_tuple(get_pool_has_cache(m)))), + consts::POOL_TOTAL_SIZE_PROP => Some((prop, result_to_tuple(get_pool_total_size(m)))), + consts::POOL_TOTAL_USED_PROP => Some((prop, result_to_tuple(get_pool_total_used(m)))), + consts::POOL_CLEVIS_INFO => Some((prop, result_to_tuple(get_pool_clevis_info(m)))), + _ => None, + }) + .collect(); + + Ok(vec![return_message.append1(return_value)]) +} + +properties_footer!(); diff --git a/src/dbus_api/pool/fetch_properties_2_3/mod.rs b/src/dbus_api/pool/fetch_properties_2_3/mod.rs new file mode 100644 index 0000000000..4409247246 --- /dev/null +++ b/src/dbus_api/pool/fetch_properties_2_3/mod.rs @@ -0,0 +1,4 @@ +mod api; +mod methods; + +pub use api::{get_all_properties_method, get_properties_method}; diff --git a/src/dbus_api/pool/mod.rs b/src/dbus_api/pool/mod.rs index a11789120e..d74494559f 100644 --- a/src/dbus_api/pool/mod.rs +++ b/src/dbus_api/pool/mod.rs @@ -15,8 +15,10 @@ use crate::{ mod fetch_properties_2_0; mod fetch_properties_2_1; +mod fetch_properties_2_3; mod pool_2_0; mod pool_2_1; +mod pool_2_3; mod shared; pub fn create_dbus_pool<'a>( @@ -60,6 +62,21 @@ pub fn create_dbus_pool<'a>( .add_p(pool_2_0::uuid_property(&f)) .add_p(pool_2_1::encrypted_property(&f)), ) + .add( + f.interface(consts::POOL_INTERFACE_NAME_2_3, ()) + .add_m(pool_2_0::create_filesystems_method(&f)) + .add_m(pool_2_0::destroy_filesystems_method(&f)) + .add_m(pool_2_0::snapshot_filesystem_method(&f)) + .add_m(pool_2_0::add_blockdevs_method(&f)) + .add_m(pool_2_3::bind_clevis_method(&f)) + .add_m(pool_2_3::unbind_clevis_method(&f)) + .add_m(pool_2_1::init_cache_method(&f)) + .add_m(pool_2_1::add_cachedevs_method(&f)) + .add_m(pool_2_0::rename_method(&f)) + .add_p(pool_2_0::name_property(&f)) + .add_p(pool_2_0::uuid_property(&f)) + .add_p(pool_2_1::encrypted_property(&f)), + ) .add( f.interface(consts::PROPERTY_FETCH_INTERFACE_NAME, ()) .add_m(fetch_properties_2_0::get_all_properties_method(&f)) @@ -74,6 +91,11 @@ pub fn create_dbus_pool<'a>( f.interface(consts::PROPERTY_FETCH_INTERFACE_NAME_2_2, ()) .add_m(fetch_properties_2_1::get_all_properties_method(&f)) .add_m(fetch_properties_2_1::get_properties_method(&f)), + ) + .add( + f.interface(consts::PROPERTY_FETCH_INTERFACE_NAME_2_3, ()) + .add_m(fetch_properties_2_3::get_all_properties_method(&f)) + .add_m(fetch_properties_2_3::get_properties_method(&f)), ); let path = object_path.get_name().to_owned(); diff --git a/src/dbus_api/pool/pool_2_3/api.rs b/src/dbus_api/pool/pool_2_3/api.rs new file mode 100644 index 0000000000..3a592a3ee6 --- /dev/null +++ b/src/dbus_api/pool/pool_2_3/api.rs @@ -0,0 +1,32 @@ +// 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 http://mozilla.org/MPL/2.0/. + +use dbus::tree::{Factory, MTFn, Method}; + +use crate::dbus_api::{ + pool::pool_2_3::methods::{bind_clevis, unbind_clevis}, + types::TData, +}; + +pub fn bind_clevis_method(f: &Factory, TData>) -> Method, TData> { + f.method("Bind", (), bind_clevis) + .in_arg(("pin", "s")) + .in_arg(("json", "s")) + // b: Indicates if new clevis bindings were added + // + // Rust representation: bool + .out_arg(("results", "b")) + .out_arg(("return_code", "q")) + .out_arg(("return_string", "s")) +} + +pub fn unbind_clevis_method(f: &Factory, TData>) -> Method, TData> { + f.method("Unbind", (), unbind_clevis) + // b: Indicates if clevis bindings were removed + // + // Rust representation: bool + .out_arg(("results", "b")) + .out_arg(("return_code", "q")) + .out_arg(("return_string", "s")) +} diff --git a/src/dbus_api/pool/pool_2_3/methods.rs b/src/dbus_api/pool/pool_2_3/methods.rs new file mode 100644 index 0000000000..a389f34179 --- /dev/null +++ b/src/dbus_api/pool/pool_2_3/methods.rs @@ -0,0 +1,88 @@ +// 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 http://mozilla.org/MPL/2.0/. + +use dbus::{ + tree::{MTFn, MethodInfo, MethodResult}, + Message, +}; +use serde_json::Value; + +use crate::{ + dbus_api::{ + types::TData, + util::{engine_to_dbus_err_tuple, get_next_arg, msg_code_ok, msg_string_ok}, + }, + engine::{CreateAction, DeleteAction}, + stratis::StratisError, +}; + +pub fn bind_clevis(m: &MethodInfo, TData>) -> MethodResult { + let message: &Message = m.msg; + let mut iter = message.iter_init(); + let pin: String = get_next_arg(&mut iter, 0)?; + let json_string: String = get_next_arg(&mut iter, 1)?; + + let dbus_context = m.tree.get_data(); + let object_path = m.path.get_name(); + let return_message = message.method_return(); + let default_return = false; + + let pool_path = m + .tree + .get(object_path) + .expect("implicit argument must be in tree"); + let pool_uuid = get_data!(pool_path; default_return; return_message).uuid; + + let mut engine = dbus_context.engine.borrow_mut(); + let (_, pool) = get_mut_pool!(engine; pool_uuid; default_return; return_message); + + let json: Value = match serde_json::from_str(&json_string) { + Ok(j) => j, + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&StratisError::Serde(e)); + return Ok(vec![return_message.append3(default_return, rc, rs)]); + } + }; + let msg = match pool.bind_clevis(pin, json) { + Ok(CreateAction::Identity) => return_message.append3(false, msg_code_ok(), msg_string_ok()), + Ok(CreateAction::Created(_)) => { + return_message.append3(true, msg_code_ok(), msg_string_ok()) + } + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return_message.append3(default_return, rc, rs) + } + }; + Ok(vec![msg]) +} + +pub fn unbind_clevis(m: &MethodInfo, TData>) -> MethodResult { + let message: &Message = m.msg; + + let dbus_context = m.tree.get_data(); + let object_path = m.path.get_name(); + let return_message = message.method_return(); + let default_return = false; + + let pool_path = m + .tree + .get(object_path) + .expect("implicit argument must be in tree"); + let pool_uuid = get_data!(pool_path; default_return; return_message).uuid; + + let mut engine = dbus_context.engine.borrow_mut(); + let (_, pool) = get_mut_pool!(engine; pool_uuid; default_return; return_message); + + let msg = match pool.unbind_clevis() { + Ok(DeleteAction::Identity) => return_message.append3(false, msg_code_ok(), msg_string_ok()), + Ok(DeleteAction::Deleted(_)) => { + return_message.append3(true, msg_code_ok(), msg_string_ok()) + } + Err(e) => { + let (rc, rs) = engine_to_dbus_err_tuple(&e); + return_message.append3(default_return, rc, rs) + } + }; + Ok(vec![msg]) +} diff --git a/src/dbus_api/pool/pool_2_3/mod.rs b/src/dbus_api/pool/pool_2_3/mod.rs new file mode 100644 index 0000000000..d16ad05c61 --- /dev/null +++ b/src/dbus_api/pool/pool_2_3/mod.rs @@ -0,0 +1,4 @@ +mod api; +mod methods; + +pub use api::{bind_clevis_method, unbind_clevis_method}; diff --git a/src/dbus_api/pool/shared.rs b/src/dbus_api/pool/shared.rs index d29e246d8a..bcffb8130b 100644 --- a/src/dbus_api/pool/shared.rs +++ b/src/dbus_api/pool/shared.rs @@ -61,7 +61,8 @@ pub fn get_pool_encryption_key_desc( ) -> Result<(bool, String), String> { pool_operation(m.tree, m.path.get_name(), |(_, _, pool)| { Ok(option_to_tuple( - pool.key_desc().map(|k| k.as_application_str().to_string()), + pool.encryption_info() + .map(|i| i.key_description.as_application_str().to_string()), String::new(), )) }) @@ -90,6 +91,19 @@ pub fn get_pool_total_used(m: &MethodInfo, TData>) -> Result, TData>, +) -> Result<(bool, (String, String)), String> { + pool_operation(m.tree, m.path.get_name(), |(_, _, pool)| { + Ok(option_to_tuple( + pool.encryption_info() + .and_then(|i| i.clevis_info.as_ref()) + .map(|(pin, config)| (pin.to_owned(), config.to_string())), + (String::new(), String::new()), + )) + }) +} + /// A method shared by all pool interfaces and by all blockdev-adding /// operations, including cache initialization, which is considered a /// blockdev-adding operation because when a cache is initialized, the diff --git a/src/engine/engine.rs b/src/engine/engine.rs index dae1fbc418..1b2b3b2fbf 100644 --- a/src/engine/engine.rs +++ b/src/engine/engine.rs @@ -17,9 +17,9 @@ use devicemapper::{Bytes, Sectors}; use crate::{ engine::types::{ - BlockDevPath, BlockDevTier, CreateAction, DeleteAction, DevUuid, FilesystemUuid, - KeyDescription, MappingCreateAction, MaybeDbusPath, Name, PoolUuid, RenameAction, - ReportType, SetCreateAction, SetDeleteAction, SetUnlockAction, + BlockDevPath, BlockDevTier, CreateAction, DeleteAction, DevUuid, EncryptionInfo, + FilesystemUuid, KeyDescription, MappingCreateAction, MaybeDbusPath, Name, PoolUuid, + RenameAction, ReportType, SetCreateAction, SetDeleteAction, SetUnlockAction, UnlockMethod, }, stratis::StratisResult, }; @@ -164,6 +164,13 @@ pub trait Pool: Debug { tier: BlockDevTier, ) -> StratisResult>; + /// Bind all devices in the given pool for automated unlocking + /// using clevis. + fn bind_clevis(&mut self, pin: String, clevis_info: Value) -> StratisResult>; + + /// Unbind all devices in the given pool from using clevis. + fn unbind_clevis(&mut self) -> StratisResult>; + /// Ensures that all designated filesystems are gone from pool. /// Returns a list of the filesystems found, and actually destroyed. /// This list will be a subset of the uuids passed in fs_uuids. @@ -255,9 +262,8 @@ pub trait Pool: Debug { /// Determine if the pool's data is encrypted fn is_encrypted(&self) -> bool; - /// Get key description for the key in the kernel keyring used for encryption - /// if it is encrypted - fn key_desc(&self) -> Option<&KeyDescription>; + /// Get all encryption information for this pool. + fn encryption_info(&self) -> Option<&EncryptionInfo>; } pub trait Engine: Debug + Report { @@ -301,7 +307,11 @@ pub trait Engine: Debug + Report { /// in the unlocked state. If some devices are able to be unlocked /// and some fail, an error is returned as all devices should be able to /// be unlocked if the necessary key is in the keyring. - fn unlock_pool(&mut self, uuid: PoolUuid) -> StratisResult>; + fn unlock_pool( + &mut self, + uuid: PoolUuid, + unlock_method: UnlockMethod, + ) -> StratisResult>; /// Find the pool designated by uuid. fn get_pool(&self, uuid: PoolUuid) -> Option<(Name, &dyn Pool)>; @@ -309,9 +319,9 @@ pub trait Engine: Debug + Report { /// Get a mutable referent to the pool designated by uuid. fn get_mut_pool(&mut self, uuid: PoolUuid) -> Option<(Name, &mut dyn Pool)>; - /// Get a mapping of encrypted pool UUIDs for pools that have not yet been set up - /// and need to be unlocked to their key descriptions. - fn locked_pools(&self) -> HashMap; + /// Get a mapping of encrypted pool UUIDs for pools that have not yet + /// been set up and need to be unlocked to their encryption infos. + fn locked_pools(&self) -> HashMap; /// Configure the simulator, for the real engine, this is a null op. /// denominator: the probably of failure is 1/denominator. diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 61abf2cc96..e34e30d288 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -10,7 +10,7 @@ pub use self::{ types::{ BlockDevState, BlockDevTier, CreateAction, DeleteAction, DevUuid, EngineAction, FilesystemUuid, KeyDescription, MappingCreateAction, MaybeDbusPath, Name, PoolUuid, - Redundancy, RenameAction, ReportType, SetCreateAction, SetDeleteAction, + Redundancy, RenameAction, ReportType, SetCreateAction, SetDeleteAction, UnlockMethod, }, }; diff --git a/src/engine/sim_engine/blockdev.rs b/src/engine/sim_engine/blockdev.rs index 648b8ce439..df7055c905 100644 --- a/src/engine/sim_engine/blockdev.rs +++ b/src/engine/sim_engine/blockdev.rs @@ -13,7 +13,7 @@ use devicemapper::{Bytes, Sectors, IEC}; use crate::engine::{ engine::BlockDev, sim_engine::randomization::Randomizer, - types::{BlockDevPath, KeyDescription, MaybeDbusPath}, + types::{BlockDevPath, EncryptionInfo, MaybeDbusPath}, }; #[derive(Debug)] @@ -25,7 +25,7 @@ pub struct SimDev { hardware_info: Option, initialization_time: u64, dbus_path: MaybeDbusPath, - key_description: Option, + encryption_info: Option, } impl SimDev { @@ -65,7 +65,7 @@ impl BlockDev for SimDev { } fn is_encrypted(&self) -> bool { - self.key_description.is_some() + self.encryption_info.is_some() } } @@ -74,7 +74,7 @@ impl SimDev { pub fn new( rdm: Rc>, devnode: &Path, - key_description: Option<&KeyDescription>, + encryption_info: Option<&EncryptionInfo>, ) -> (Uuid, SimDev) { ( Uuid::new_v4(), @@ -85,7 +85,7 @@ impl SimDev { hardware_info: None, initialization_time: Utc::now().timestamp() as u64, dbus_path: MaybeDbusPath(None), - key_description: key_description.cloned(), + encryption_info: encryption_info.cloned(), }, ) } @@ -96,6 +96,25 @@ impl SimDev { pub fn set_user_info(&mut self, user_info: Option<&str>) -> bool { set_blockdev_user_info!(self; user_info) } + + /// Set the clevis info for a block device. + pub fn set_clevis_info(&mut self, pin: String, config: Value) { + if let Some(ref mut info) = self.encryption_info { + info.clevis_info = Some((pin, config)); + } + } + + /// Unset the clevis info for a block device. + pub fn unset_clevis_info(&mut self) { + if let Some(ref mut info) = self.encryption_info { + info.clevis_info = None; + } + } + + /// Get encryption information for this block device. + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { + self.encryption_info.as_ref() + } } impl<'a> Into for &'a SimDev { @@ -105,11 +124,19 @@ impl<'a> Into for &'a SimDev { "path".to_string(), Value::from(self.devnode.physical_path().display().to_string()), ); - if let Some(ref key_desc) = self.key_description { + if let Some(EncryptionInfo { + ref key_description, + ref clevis_info, + }) = self.encryption_info + { json.insert( "key_description".to_string(), - Value::from(key_desc.as_application_str()), + Value::from(key_description.as_application_str()), ); + if let Some((ref pin, ref config)) = clevis_info { + json.insert("clevis_pin".to_string(), Value::from(pin.to_owned())); + json.insert("clevis_config".to_string(), config.to_owned()); + } } Value::from(json) } diff --git a/src/engine/sim_engine/engine.rs b/src/engine/sim_engine/engine.rs index 1ed6e1eda2..236822b2f3 100644 --- a/src/engine/sim_engine/engine.rs +++ b/src/engine/sim_engine/engine.rs @@ -20,8 +20,8 @@ use crate::{ sim_engine::{keys::SimKeyActions, pool::SimPool, randomization::Randomizer}, structures::Table, types::{ - CreateAction, DeleteAction, DevUuid, KeyDescription, Name, PoolUuid, RenameAction, - ReportType, SetUnlockAction, + CreateAction, DeleteAction, DevUuid, EncryptionInfo, KeyDescription, Name, PoolUuid, + RenameAction, ReportType, SetUnlockAction, UnlockMethod, }, EngineEvent, }, @@ -116,7 +116,10 @@ impl Engine for SimEngine { &Rc::clone(&self.rdm), &devices, redundancy, - key_desc.as_ref(), + key_desc.map(|kd| EncryptionInfo { + key_description: kd, + clevis_info: None, + }), ); if self.rdm.borrow_mut().throw_die() { @@ -178,7 +181,11 @@ impl Engine for SimEngine { Ok(RenameAction::Renamed(uuid)) } - fn unlock_pool(&mut self, _pool_uuid: PoolUuid) -> StratisResult> { + fn unlock_pool( + &mut self, + _pool_uuid: PoolUuid, + _unlock_method: UnlockMethod, + ) -> StratisResult> { Ok(SetUnlockAction::empty()) } @@ -190,7 +197,7 @@ impl Engine for SimEngine { get_mut_pool!(self; uuid) } - fn locked_pools(&self) -> HashMap { + fn locked_pools(&self) -> HashMap { HashMap::new() } diff --git a/src/engine/sim_engine/pool.rs b/src/engine/sim_engine/pool.rs index aa86882fd5..864b6717ba 100644 --- a/src/engine/sim_engine/pool.rs +++ b/src/engine/sim_engine/pool.rs @@ -24,24 +24,19 @@ use crate::{ sim_engine::{blockdev::SimDev, filesystem::SimFilesystem, randomization::Randomizer}, structures::Table, types::{ - BlockDevTier, CreateAction, DevUuid, FilesystemUuid, KeyDescription, MaybeDbusPath, - Name, PoolUuid, Redundancy, RenameAction, SetCreateAction, SetDeleteAction, + BlockDevTier, CreateAction, DeleteAction, DevUuid, EncryptionInfo, FilesystemUuid, + MaybeDbusPath, Name, PoolUuid, Redundancy, RenameAction, SetCreateAction, + SetDeleteAction, }, EngineEvent, }, stratis::{ErrorEnum, StratisError, StratisResult}, }; -// NOTE: There are currently two separate key descriptions which currently must be the -// same and as a result are functionally equivalent at the moment. The reason for this -// separation is to allow ease in future development if separate key descriptions are ever -// needed for data devices and cache devices. #[derive(Debug)] pub struct SimPool { block_devs: HashMap, - block_devs_key_desc: Option, cache_devs: HashMap, - cache_devs_key_desc: Option, filesystems: Table, redundancy: Redundancy, rdm: Rc>, @@ -53,19 +48,17 @@ impl SimPool { rdm: &Rc>, paths: &[&Path], redundancy: Redundancy, - key_desc: Option<&KeyDescription>, + enc_info: Option, ) -> (PoolUuid, SimPool) { let devices: HashSet<_, RandomState> = HashSet::from_iter(paths); let device_pairs = devices .iter() - .map(|p| SimDev::new(Rc::clone(rdm), p, key_desc)); + .map(|p| SimDev::new(Rc::clone(rdm), p, enc_info.as_ref())); ( Uuid::new_v4(), SimPool { block_devs: HashMap::from_iter(device_pairs), - block_devs_key_desc: key_desc.cloned(), cache_devs: HashMap::new(), - cache_devs_key_desc: key_desc.cloned(), filesystems: Table::default(), redundancy, rdm: Rc::clone(rdm), @@ -91,12 +84,31 @@ impl SimPool { } fn datadevs_encrypted(&self) -> bool { - self.block_devs_key_desc.is_some() + self.encryption_info().is_some() } pub fn destroy(&mut self) -> StratisResult<()> { Ok(()) } + + fn encryption_info_impl(&self) -> Option<&EncryptionInfo> { + self.block_devs + .iter() + .next() + .and_then(|(_, bd)| bd.encryption_info()) + } + + fn add_clevis_info(&mut self, pin: String, config: Value) { + self.block_devs + .iter_mut() + .for_each(|(_, bd)| bd.set_clevis_info(pin.clone(), config.clone())) + } + + fn clear_clevis_info(&mut self) { + self.block_devs + .iter_mut() + .for_each(|(_, bd)| bd.unset_clevis_info()) + } } // Precondition: SimDev::into() always returns a value that matches Value::Object(_). @@ -239,8 +251,8 @@ impl Pool for SimPool { Rc::clone(&self.rdm), p, match tier { - BlockDevTier::Data => self.block_devs_key_desc.as_ref(), - BlockDevTier::Cache => self.cache_devs_key_desc.as_ref(), + BlockDevTier::Data => self.encryption_info(), + BlockDevTier::Cache => None, }, ) }) @@ -268,6 +280,49 @@ impl Pool for SimPool { Ok(SetCreateAction::new(ret_uuids)) } + fn bind_clevis(&mut self, pin: String, clevis_info: Value) -> StratisResult> { + let encryption_info = self.encryption_info(); + let clevis_info_current = encryption_info.and_then(|info| info.clevis_info.as_ref()); + if encryption_info.is_some() { + if let Some(info) = clevis_info_current { + let clevis_tuple = (pin, clevis_info); + if info == &clevis_tuple { + Ok(CreateAction::Identity) + } else { + Err(StratisError::Error(format!( + "This pool is already bound with clevis pin and config {:?}; + this differs from the requested pin and config {:?}", + info, clevis_tuple, + ))) + } + } else { + self.add_clevis_info(pin, clevis_info); + Ok(CreateAction::Created(())) + } + } else { + Err(StratisError::Error( + "Requested pool does not appear to be encrypted".to_string(), + )) + } + } + + fn unbind_clevis(&mut self) -> StratisResult> { + let encryption_info = self.encryption_info(); + let clevis_info = encryption_info.and_then(|info| info.clevis_info.as_ref()); + if encryption_info.is_some() { + Ok(if clevis_info.is_some() { + self.clear_clevis_info(); + DeleteAction::Deleted(()) + } else { + DeleteAction::Identity + }) + } else { + Err(StratisError::Error( + "Requested pool does not appear to be encrypted".to_string(), + )) + } + } + fn destroy_filesystems<'a>( &'a mut self, _pool_name: &str, @@ -454,8 +509,8 @@ impl Pool for SimPool { self.datadevs_encrypted() } - fn key_desc(&self) -> Option<&KeyDescription> { - self.block_devs_key_desc.as_ref() + fn encryption_info(&self) -> Option<&EncryptionInfo> { + self.encryption_info_impl() } } diff --git a/src/engine/strat_engine/backstore/backstore.rs b/src/engine/strat_engine/backstore/backstore.rs index fb0ce444cc..6be1363284 100644 --- a/src/engine/strat_engine/backstore/backstore.rs +++ b/src/engine/strat_engine/backstore/backstore.rs @@ -22,11 +22,11 @@ use crate::{ }, dm::get_dm, metadata::MDADataSize, - names::{format_backstore_ids, CacheRole, KeyDescription}, + names::{format_backstore_ids, CacheRole}, serde_structs::{BackstoreSave, CapSave, Recordable}, writing::wipe_sectors, }, - BlockDevTier, DevUuid, PoolUuid, + types::{BlockDevTier, DevUuid, EncryptionInfo, KeyDescription, PoolUuid}, }, stratis::{ErrorEnum, StratisError, StratisResult}, }; @@ -127,9 +127,8 @@ impl Backstore { datadevs: Vec, cachedevs: Vec, last_update_time: DateTime, - key_description: Option<&KeyDescription>, ) -> StratisResult { - let block_mgr = BlockDevMgr::new(datadevs, Some(last_update_time), key_description); + let block_mgr = BlockDevMgr::new(datadevs, Some(last_update_time)); let data_tier = DataTier::setup(block_mgr, &backstore_save.data_tier)?; let (dm_name, dm_uuid) = format_backstore_ids(pool_uuid, CacheRole::OriginSub); let origin = LinearDev::setup( @@ -140,7 +139,7 @@ impl Backstore { )?; let (cache_tier, cache, origin) = if !cachedevs.is_empty() { - let block_mgr = BlockDevMgr::new(cachedevs, Some(last_update_time), None); + let block_mgr = BlockDevMgr::new(cachedevs, Some(last_update_time)); match backstore_save.cache_tier { Some(ref cache_tier_save) => { let cache_tier = CacheTier::setup(block_mgr, cache_tier_save)?; @@ -171,6 +170,9 @@ impl Backstore { /// Immediately after initialization a backstore has no cap device, since /// no segments are allocated in the data tier. /// + /// When the backstore is initialized it may be unencrypted, or it may + /// be encrypted only with a kernel keyring and without Clevis information. + /// /// WARNING: metadata changing event pub fn initialize( pool_uuid: PoolUuid, @@ -642,17 +644,25 @@ impl Backstore { ) } - pub fn data_key_desc(&self) -> Option<&KeyDescription> { - self.data_tier.key_desc() + pub fn data_tier_is_encrypted(&self) -> bool { + self.data_tier.block_mgr.is_encrypted() } - pub fn data_tier_is_encrypted(&self) -> bool { - self.data_tier.is_encrypted() + pub fn data_tier_encryption_info(&self) -> Option<&EncryptionInfo> { + self.data_tier.block_mgr.encryption_info() } pub fn has_cache(&self) -> bool { self.cache_tier.is_some() } + + pub fn bind_clevis(&mut self, pin: String, clevis_info: Value) -> StratisResult { + self.data_tier.block_mgr.bind_clevis(pin, clevis_info) + } + + pub fn unbind_clevis(&mut self) -> StratisResult { + self.data_tier.block_mgr.unbind_clevis() + } } impl<'a> Into for &'a Backstore { diff --git a/src/engine/strat_engine/backstore/blockdev.rs b/src/engine/strat_engine/backstore/blockdev.rs index 10354df3a6..ba46381496 100644 --- a/src/engine/strat_engine/backstore/blockdev.rs +++ b/src/engine/strat_engine/backstore/blockdev.rs @@ -17,10 +17,9 @@ use crate::{ strat_engine::{ backstore::{crypt::CryptHandle, range_alloc::RangeAllocator}, metadata::{disown_device, BDAExtendedSize, BlockdevSize, MDADataSize, BDA}, - names::KeyDescription, serde_structs::{BaseBlockDevSave, Recordable}, }, - types::{BlockDevPath, DevUuid, MaybeDbusPath}, + types::{BlockDevPath, DevUuid, EncryptionInfo, MaybeDbusPath, PoolUuid}, }, stratis::{StratisError, StratisResult}, }; @@ -34,7 +33,7 @@ pub struct StratBlockDev { user_info: Option, hardware_info: Option, dbus_path: MaybeDbusPath, - key_description: Option, + encryption_info: Option, } impl StratBlockDev { @@ -66,7 +65,7 @@ impl StratBlockDev { other_segments: &[(Sectors, Sectors)], user_info: Option, hardware_info: Option, - key_description: Option<&KeyDescription>, + encryption_info: Option, ) -> StratisResult { let mut segments = vec![(Sectors(0), bda.extended_size().sectors())]; segments.extend(other_segments); @@ -81,7 +80,7 @@ impl StratBlockDev { user_info, hardware_info, dbus_path: MaybeDbusPath(None), - key_description: key_description.cloned(), + encryption_info, }) } @@ -133,6 +132,11 @@ impl StratBlockDev { self.bda.save_state(time, metadata, &mut f) } + /// The pool's UUID. + pub fn pool_uuid(&self) -> PoolUuid { + self.bda.pool_uuid() + } + /// The device's UUID. pub fn uuid(&self) -> DevUuid { self.bda.dev_uuid() @@ -183,8 +187,41 @@ impl StratBlockDev { &self.devnode } - pub fn key_description(&self) -> Option<&KeyDescription> { - self.key_description.as_ref() + /// Get the encryption_info stored on the given encrypted blockdev. + /// + /// Returns Some(_) if it is encrypted. + /// Returns None if it is not encrypted. + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { + self.encryption_info.as_ref() + } + + /// Set the clevis config cached in the blockdev data structure to the given + /// values. + pub fn set_clevis_info(&mut self, pin: String, config: Value) -> StratisResult<()> { + match self.encryption_info { + Some(ref mut info) => { + info.clevis_info = Some((pin, config)); + Ok(()) + } + None => Err(StratisError::Error(format!( + "Block device {} is not encrypted", + self.devnode.physical_path().display(), + ))), + } + } + + /// Unset the clevis config cached in the blockdev data structure. + pub fn unset_clevis_info(&mut self) -> StratisResult<()> { + match self.encryption_info { + Some(ref mut info) => { + info.clevis_info = None; + Ok(()) + } + None => Err(StratisError::Error(format!( + "Block device {} is not encrypted", + self.devnode.physical_path().display(), + ))), + } } } @@ -194,13 +231,14 @@ impl<'a> Into for &'a StratBlockDev { "path": self.devnode.physical_path(), "uuid": self.bda.dev_uuid().to_simple_ref().to_string(), }); - if let Some(ref key_desc) = self.key_description { - json.as_object_mut() - .expect("Created a JSON object above") - .insert( - "key_description".to_string(), - Value::from(key_desc.as_application_str().to_string()), - ); + let map = json.as_object_mut().expect("just created above"); + if let Some(encryption_info) = &self.encryption_info { + if let Value::Object(enc_map) = <&EncryptionInfo as Into>::into(encryption_info) + { + map.extend(enc_map); + } else { + unreachable!("EncryptionInfo conversion returns a JSON object"); + }; } json } @@ -238,7 +276,7 @@ impl BlockDev for StratBlockDev { } fn is_encrypted(&self) -> bool { - self.key_description.is_some() + self.encryption_info().is_some() } } diff --git a/src/engine/strat_engine/backstore/blockdevmgr.rs b/src/engine/strat_engine/backstore/blockdevmgr.rs index e618fd6ab9..a9d3f699bd 100644 --- a/src/engine/strat_engine/backstore/blockdevmgr.rs +++ b/src/engine/strat_engine/backstore/blockdevmgr.rs @@ -11,6 +11,7 @@ use std::{ use chrono::{DateTime, Duration, Utc}; use rand::{seq::IteratorRandom, thread_rng}; +use serde_json::Value; use devicemapper::{Bytes, Device, LinearDevTargetParams, LinearTargetParams, Sectors, TargetLine}; @@ -19,14 +20,15 @@ use crate::{ strat_engine::{ backstore::{ blockdev::StratBlockDev, - crypt::CryptHandle, + crypt::{interpret_clevis_config, CryptHandle}, devices::{initialize_devices, process_and_verify_devices, wipe_blockdevs}, }, + keys::MemoryPrivateFilesystem, metadata::MDADataSize, names::KeyDescription, serde_structs::{BaseBlockDevSave, BaseDevSave, Recordable}, }, - types::{DevUuid, PoolUuid}, + types::{DevUuid, EncryptionInfo, PoolUuid}, }, stratis::{ErrorEnum, StratisError, StratisResult}, }; @@ -108,11 +110,29 @@ pub fn map_to_dm(bsegs: &[BlkDevSegment]) -> Vec StratisResult> { + let mut handles = Vec::new(); + for bd in blockdevs.iter() { + let path = bd.devnode().physical_path(); + let crypt_handle_opt = CryptHandle::setup(path)?; + let crypt_handle = crypt_handle_opt.ok_or_else(|| { + StratisError::Error(format!( + "Device {} is not an encrypted device", + path.display(), + )) + })?; + handles.push(crypt_handle); + } + Ok(handles) +} + #[derive(Debug)] pub struct BlockDevMgr { block_devs: Vec, last_update_time: Option>, - key_desc: Option, } impl BlockDevMgr { @@ -120,12 +140,10 @@ impl BlockDevMgr { pub fn new( block_devs: Vec, last_update_time: Option>, - key_desc: Option<&KeyDescription>, ) -> BlockDevMgr { BlockDevMgr { block_devs, last_update_time, - key_desc: key_desc.cloned(), } } @@ -139,9 +157,16 @@ impl BlockDevMgr { let devices = process_and_verify_devices(pool_uuid, &HashSet::new(), paths)?; Ok(BlockDevMgr::new( - initialize_devices(devices, pool_uuid, mda_data_size, key_desc)?, + initialize_devices( + devices, + pool_uuid, + mda_data_size, + key_desc.map(|k| EncryptionInfo { + key_description: k.clone(), + clevis_info: None, + }), + )?, None, - key_desc, )) } @@ -170,6 +195,16 @@ impl BlockDevMgr { /// Return the uuids of all blockdevs corresponding to paths that were /// added. pub fn add(&mut self, pool_uuid: PoolUuid, paths: &[&Path]) -> StratisResult> { + let this_pool_uuid = self.block_devs.get(0).map(|bd| bd.pool_uuid()); + if this_pool_uuid.is_some() && this_pool_uuid != Some(pool_uuid) { + return Err(StratisError::Engine( + ErrorEnum::Invalid, + format!("block devices being managed have pool UUID {} but new devices are to be added with pool UUID {}", + this_pool_uuid.expect("guarded by if-expression").to_simple_ref(), + pool_uuid) + )); + } + let current_uuids = self .block_devs .iter() @@ -177,13 +212,14 @@ impl BlockDevMgr { .collect::>(); let devices = process_and_verify_devices(pool_uuid, ¤t_uuids, paths)?; - if self.is_encrypted() && !self.has_valid_passphrase() { + let encryption_info = self.encryption_info(); + if encryption_info.is_some() && !self.has_valid_passphrase() { return Err(StratisError::Engine( ErrorEnum::Invalid, "The key associated with the current registered key description \ - was not able to unlock an existing encrypted device. Check that \ + was not able to unlock an existing encrypted device; check that \ the same key is in the keyring that was used to create the encrypted \ - pool." + pool" .to_string(), )); } @@ -196,7 +232,7 @@ impl BlockDevMgr { devices, pool_uuid, MDADataSize::default(), - self.key_desc.as_ref(), + encryption_info.cloned(), )?; let bdev_uuids = bds.iter().map(|bd| bd.uuid()).collect(); self.block_devs.extend(bds); @@ -376,12 +412,170 @@ impl BlockDevMgr { .sum() } - pub fn key_desc(&self) -> Option<&KeyDescription> { - self.key_desc.as_ref() + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { + let mut iter = self.block_devs.iter().map(|bd| bd.encryption_info()); + let info = iter.next().and_then(|opt| opt); + + // Liminal device code will not set up a pool with devices with + // different encryption information. + assert!(iter.all(|elem| info == elem)); + + info } pub fn is_encrypted(&self) -> bool { - self.key_desc.is_some() + self.encryption_info().is_some() + } + + #[cfg(test)] + fn invariant(&self) { + let pool_uuids = self + .block_devs + .iter() + .map(|bd| bd.pool_uuid()) + .collect::>(); + assert!(pool_uuids.len() == 1); + + let encryption_infos = self + .block_devs + .iter() + .filter_map(|bd| bd.encryption_info()) + .collect::>(); + if encryption_infos.is_empty() { + assert_eq!(self.encryption_info(), None); + } else { + assert_eq!(encryption_infos.len(), self.block_devs.len()); + + let info_set = encryption_infos.iter().collect::>(); + assert!(info_set.len() == 1); + } + } + + /// Bind all devices in the given blockdev manager using the given clevis + /// configuration. + /// + /// * Returns Ok(true) if the binding was performed. + /// * Returns Ok(false) if the binding had already been previously performed and + /// nothing was changed. + /// * Returns Err(_) if an inconsistency was found in the metadata across pools + /// or binding failed. + pub fn bind_clevis(&mut self, pin: String, mut clevis_info: Value) -> StratisResult { + fn bind_clevis_loop<'a>( + key_fs: &MemoryPrivateFilesystem, + rollback_record: &'a mut Vec, + handles: &'a mut Vec, + key_desc: &KeyDescription, + pin: &str, + clevis_info: &Value, + yes: bool, + ) -> StratisResult<()> { + for mut crypt_handle in handles.drain(..) { + let res = key_fs.key_op(key_desc, |keyfile_path| { + crypt_handle + .clevis_bind(keyfile_path, pin, clevis_info, yes) + .map_err(StratisError::Crypt) + }); + if res.is_ok() { + rollback_record.push(crypt_handle); + } + res?; + } + Ok(()) + } + + fn rollback_loop(rollback_record: Vec) { + rollback_record.into_iter().for_each(|mut crypt_dev| { + if let Err(e) = crypt_dev.clevis_unbind() { + warn!( + "Failed to unbind device {} from clevis during \ + rollback: {}", + crypt_dev.physical_device_path().display(), + e, + ); + } + }); + } + + let encryption_info = match self.encryption_info() { + Some(info) => info, + None => { + return Err(StratisError::Error( + "Requested pool does not appear to be encrypted".to_string(), + )) + } + }; + + let yes = interpret_clevis_config(&pin, &mut clevis_info)?; + + if let Some(info) = &encryption_info.clevis_info { + let clevis_tuple = (pin, clevis_info); + if info == &clevis_tuple { + return Ok(false); + } else { + return Err(StratisError::Error(format!( + "Block devices have already been bound with pin {} and config {}; \ + requested pin {} and config {} can't be applied", + info.0, info.1, clevis_tuple.0, clevis_tuple.1, + ))); + } + } + + let key_fs = MemoryPrivateFilesystem::new()?; + + let mut crypt_handles = get_crypt_handles(&self.block_devs)?; + let mut rollback_record = Vec::new(); + let result = bind_clevis_loop( + &key_fs, + &mut rollback_record, + &mut crypt_handles, + &encryption_info.key_description, + pin.as_str(), + &clevis_info, + yes, + ); + if let Err(e) = result { + rollback_loop(rollback_record); + return Err(e); + } + + for (_, bd) in self.blockdevs_mut() { + bd.set_clevis_info(pin.clone(), clevis_info.clone()) + .expect("devices are certainly encrypted, so all must have encryption info struct"); + } + Ok(true) + } + + pub fn unbind_clevis(&mut self) -> StratisResult { + match self.encryption_info() { + None => { + return Err(StratisError::Error( + "Requested pool does not appear to be encrypted".to_string(), + )); + } + Some(info) => { + if info.clevis_info.is_none() { + return Ok(false); + } + } + } + + let crypt_handles = get_crypt_handles(&self.block_devs)?; + for mut handle in crypt_handles { + let res = handle.clevis_unbind().map_err(StratisError::Crypt); + if let Err(ref e) = res { + warn!( + "Failed to unbind from the tang server using clevis: {}. \ + This operation cannot be rolled back automatically.", + e, + ); + } + res? + } + for (_, bd) in self.blockdevs_mut() { + bd.unset_clevis_info() + .expect("blockdevs are definitely encrypted, must have encryption_info set"); + } + Ok(true) } } @@ -581,6 +775,7 @@ mod tests { ); let original_length = bd_mgr.block_devs.len(); + assert_matches!(bd_mgr.add(uuid2, paths1), Err(_)); assert_matches!(bd_mgr.add(uuid, paths1), Ok(_)); assert_eq!(bd_mgr.block_devs.len(), original_length); @@ -588,6 +783,8 @@ mod tests { cmd::udev_settle().unwrap(); assert_matches!(bd_mgr.add(uuid, paths2), Err(_)); + + bd_mgr.invariant() } #[test] diff --git a/src/engine/strat_engine/backstore/crypt.rs b/src/engine/strat_engine/backstore/crypt.rs index 97f9624a2e..48f137ec31 100644 --- a/src/engine/strat_engine/backstore/crypt.rs +++ b/src/engine/strat_engine/backstore/crypt.rs @@ -8,7 +8,9 @@ use std::{ path::{Path, PathBuf}, }; -use serde_json::Value; +use base64::{decode, encode_config, CharacterSet, Config}; +use serde_json::{Map, Value}; +use sha1::{Digest, Sha1}; use devicemapper::Sectors; use libcryptsetup_rs::{ @@ -17,8 +19,13 @@ use libcryptsetup_rs::{ }; use crate::engine::{ - strat_engine::{keys, metadata::StratisIdentifiers, names::format_crypt_name}, - types::{KeyDescription, SizedKeyMemory}, + strat_engine::{ + cmd::{clevis_luks_bind, clevis_luks_unbind, clevis_luks_unlock}, + keys, + metadata::StratisIdentifiers, + names::format_crypt_name, + }, + types::{KeyDescription, SizedKeyMemory, UnlockMethod}, DevUuid, PoolUuid, }; @@ -33,6 +40,13 @@ const STRATIS_TOKEN_DEV_UUID_KEY: &str = "device_uuid"; const STRATIS_TOKEN_ID: c_uint = 0; const LUKS2_TOKEN_ID: c_uint = 1; +/// NOTE: Only token IDs 0 and 1 will be used at the time of clevis bindings +/// so until support for more clevis operations are added, this can be reliably +/// depended upon as cryptsetup token import will use the next available token ID +/// which, in this case, is 2. +/// FIXME: Specify token ID when clevis adds support so that we can rely on this +/// deterministically as we add other tokens. +const CLEVIS_LUKS_TOKEN_ID: c_uint = 2; const LUKS2_TOKEN_TYPE: &str = "luks2-keyring"; const STRATIS_TOKEN_TYPE: &str = "stratis"; @@ -47,6 +61,10 @@ const SECTOR_SIZE: u64 = 512; /// Path to logical devices for encrypted devices const DEVICEMAPPER_PATH: &str = "/dev/mapper"; +/// Key in clevis configuration for tang indicating that the URL of the +/// tang server does not need to be verified. +const CLEVIS_TANG_TRUST_URL: &str = "stratis:tang:trust_url"; + macro_rules! log_on_failure { ($op:expr, $fmt:tt $(, $arg:expr)*) => {{ let result = $op; @@ -271,7 +289,7 @@ impl CryptInitializer { "Failed to create the Stratis token" ); - activate_and_check_device_path(device, key_description, &activation_name) + activate_and_check_device_path(device, key_description, &activation_name).map(|_| ()) } /// Lay down properly configured LUKS2 metadata on a new physical device @@ -461,14 +479,82 @@ impl CryptHandle { &self.key_description } + /// Get the keyslot associated with the given token ID. + pub fn keyslots(&mut self, token_id: c_uint) -> Result>> { + get_keyslot_number(&mut self.device, token_id) + } + + /// Get info for the clevis binding. + pub fn clevis_info(&mut self) -> Result> { + let json = match self + .device + .token_handle() + .json_get(CLEVIS_LUKS_TOKEN_ID) + .ok() + { + Some(j) => j, + None => return Ok(None), + }; + let json_b64 = match json + .get("jwe") + .and_then(|map| map.get("protected")) + .and_then(|string| string.as_str()) + { + Some(s) => s.to_owned(), + None => return Ok(None), + }; + let json_bytes = decode(json_b64).map_err(|e| LibcryptErr::Other(e.to_string()))?; + + let subjson: Value = serde_json::from_slice(json_bytes.as_slice()) + .map_err(|e| LibcryptErr::Other(e.to_string()))?; + + pin_dispatch(&subjson).map(Some) + } + /// Activate encrypted Stratis device using the name stored in the /// Stratis token - pub fn activate(&mut self) -> Result<()> { - activate_and_check_device_path( - &mut self.device, - &self.key_description, - &self.name.to_owned(), - ) + pub fn activate(&mut self, unlock_method: UnlockMethod) -> Result<()> { + match unlock_method { + UnlockMethod::Keyring => activate_and_check_device_path( + &mut self.device, + &self.key_description, + &self.name.to_owned(), + ), + UnlockMethod::Clevis => clevis_luks_unlock(&self.physical_path, &self.name) + .map_err(|e| LibcryptErr::Other(e.to_string())), + } + } + + /// Bind the given device using clevis. + pub fn clevis_bind( + &mut self, + keyfile_path: &Path, + pin: &str, + json: &Value, + yes: bool, + ) -> Result<()> { + clevis_luks_bind(&self.physical_path, keyfile_path, pin, &json, yes) + .map_err(|e| LibcryptErr::Other(e.to_string())) + } + + /// Unbind the given device using clevis. + pub fn clevis_unbind(&mut self) -> Result<()> { + let keyslots = self.keyslots(CLEVIS_LUKS_TOKEN_ID)?.ok_or_else(|| { + LibcryptErr::Other(format!( + "Token slot {} appears to be empty; could not determine keyslots", + CLEVIS_LUKS_TOKEN_ID, + )) + })?; + for keyslot in keyslots { + if let Err(e) = clevis_luks_unbind(&self.physical_path, keyslot) { + warn!( + "Failed to unbind device {} from Clevis: {}", + self.physical_path.display(), + e, + ); + } + } + Ok(()) } /// Deactivate the device referenced by the current device handle. @@ -497,6 +583,162 @@ impl CryptHandle { } } +/// Interpret non-Clevis keys that may contain additional information about +/// how to configure Clevis when binding. Remove any expected non-Clevis keys +/// from the configuration. +/// The only value to be returned is whether or not the bind command should be +/// passed the argument yes. +pub fn interpret_clevis_config(pin: &str, clevis_config: &mut Value) -> Result { + let yes = if pin == "tang" { + if let Some(map) = clevis_config.as_object_mut() { + map.remove(CLEVIS_TANG_TRUST_URL) + .and_then(|v| v.as_bool()) + .unwrap_or(false) + } else { + return Err(LibcryptErr::Other(format!( + "configuration for Clevis is is not in JSON object format: {}", + clevis_config + ))); + } + } else { + false + }; + + Ok(yes) +} + +/// Generate tang JSON +fn tang_dispatch(json: &Value) -> Result { + let object = json + .get("clevis") + .and_then(|map| map.get("tang")) + .and_then(|val| val.as_object()) + .ok_or_else(|| { + LibcryptErr::Other("Expected an object for value of clevis.tang".to_string()) + })?; + let url = object.get("url").and_then(|s| s.as_str()).ok_or_else(|| { + LibcryptErr::Other("Expected a string for value of clevis.tang.url".to_string()) + })?; + + let keys = object + .get("adv") + .and_then(|adv| adv.get("keys")) + .and_then(|keys| keys.as_array()) + .ok_or_else(|| { + LibcryptErr::Other("Expected an array for value of clevis.tang.adv.keys".to_string()) + })?; + let mut key = keys + .iter() + .cloned() + .find(|obj| obj.get("key_ops") == Some(&Value::Array(vec![Value::from("verify")]))) + .ok_or_else(|| { + LibcryptErr::Other("Verification key not found in clevis metadata".to_string()) + })?; + + let map = if let Some(m) = key.as_object_mut() { + m + } else { + return Err(LibcryptErr::Other( + "Key value is not in JSON object format".to_string(), + )); + }; + map.remove("key_ops"); + map.remove("alg"); + + let thp = key.to_string(); + let mut hasher = Sha1::new(); + hasher.update(thp.as_bytes()); + let array = hasher.finalize(); + let thp = encode_config(array, Config::new(CharacterSet::UrlSafe, false)); + + Ok(json!({"url": url.to_owned(), "thp": thp})) +} + +/// Generate Shamir secret sharing JSON +fn sss_dispatch(json: &Value) -> Result { + let object = json + .get("clevis") + .and_then(|map| map.get("sss")) + .and_then(|val| val.as_object()) + .ok_or_else(|| { + LibcryptErr::Other("Expected an object for value of clevis.sss".to_string()) + })?; + + let threshold = object + .get("t") + .and_then(|val| val.as_u64()) + .ok_or_else(|| { + LibcryptErr::Other("Expected an int for value of clevis.sss.t".to_string()) + })?; + let jwes = object + .get("jwe") + .and_then(|val| val.as_array()) + .ok_or_else(|| { + LibcryptErr::Other("Expected an array for value of clevis.sss.jwe".to_string()) + })?; + + let mut sss_map = Map::new(); + sss_map.insert("t".to_string(), Value::from(threshold)); + + let mut pin_map = Map::new(); + for jwe in jwes { + if let Value::String(ref s) = jwe { + // NOTE: Workaround for the on-disk format for Shamir secret sharing + // as written by clevis. The base64 encoded string delimits the end + // of the JSON blob with a period. + let json_s = s.splitn(2, '.').next().ok_or_else(|| { + LibcryptErr::Other(format!( + "Splitting string {} on character '.' did not result in \ + at least one string segment.", + s, + )) + })?; + + let json_bytes = decode(json_s).map_err(|e| LibcryptErr::Other(e.to_string()))?; + let value: Value = serde_json::from_slice(&json_bytes) + .map_err(|e| LibcryptErr::Other(e.to_string()))?; + let (pin, value) = pin_dispatch(&value)?; + match pin_map.get_mut(&pin) { + Some(Value::Array(ref mut vec)) => vec.push(value), + None => { + pin_map.insert(pin, Value::from(vec![value])); + } + _ => { + return Err(LibcryptErr::Other(format!( + "There appears to be a data type that is not an array in \ + the data structure being used to construct the sss JSON config + under pin name {}", + pin, + ))) + } + }; + } else { + return Err(LibcryptErr::Other( + "Expected a string for each value in the array at clevis.sss.jwe".to_string(), + )); + } + } + sss_map.insert("pins".to_string(), Value::from(pin_map)); + + Ok(Value::from(sss_map)) +} + +/// Match pin for existing JWE +fn pin_dispatch(decoded_jwe: &Value) -> Result<(String, Value)> { + let pin_value = decoded_jwe + .get("clevis") + .and_then(|map| map.get("pin")) + .ok_or_else(|| { + LibcryptErr::Other("Key .clevis.pin not found in clevis JSON token".to_string()) + })?; + match pin_value.as_str() { + Some("tang") => tang_dispatch(decoded_jwe).map(|val| ("tang".to_owned(), val)), + Some("sss") => sss_dispatch(decoded_jwe).map(|val| ("sss".to_owned(), val)), + Some("tpm2") => Ok(("tpm2".to_owned(), json!({}))), + _ => Err(LibcryptErr::Other("Unsupported clevis pin".to_string())), + } +} + /// Check whether the physical device path corresponds to an encrypted /// Stratis device. /// @@ -606,40 +848,41 @@ fn activate_and_check_device_path( /// Get a list of all keyslots associated with the LUKS2 token. /// This is necessary because attempting to destroy an uninitialized /// keyslot will result in an error. -fn get_keyslot_number(device: &mut CryptDevice) -> Result> { - let json = log_on_failure!( - device.token_handle().json_get(LUKS2_TOKEN_ID), - "Failed to get the JSON LUKS2 keyring token from the assigned keyslot" - ); +fn get_keyslot_number(device: &mut CryptDevice, token_id: c_uint) -> Result>> { + let json = match device.token_handle().json_get(token_id) { + Ok(j) => j, + Err(_) => return Ok(None), + }; let vec = json .get(TOKEN_KEYSLOTS_KEY) .and_then(|k| k.as_array()) .ok_or_else(|| LibcryptErr::Other("keyslots value was malformed".to_string()))?; - Ok(vec - .iter() - .filter_map(|int_val| { - let as_str = int_val.as_str(); - if as_str.is_none() { - warn!( - "Discarding invalid value in LUKS2 token keyslot array: {}", - int_val - ); - } - let s = match as_str { - Some(s) => s, - None => return None, - }; - let as_c_uint = s.parse::(); - if let Err(ref e) = as_c_uint { - warn!( - "Discarding invalid value in LUKS2 token keyslot array: {}; \ + Ok(Some( + vec.iter() + .filter_map(|int_val| { + let as_str = int_val.as_str(); + if as_str.is_none() { + warn!( + "Discarding invalid value in LUKS2 token keyslot array: {}", + int_val + ); + } + let s = match as_str { + Some(s) => s, + None => return None, + }; + let as_c_uint = s.parse::(); + if let Err(ref e) = as_c_uint { + warn!( + "Discarding invalid value in LUKS2 token keyslot array: {}; \ failed to convert it to an integer: {}", - s, e, - ); - } - as_c_uint.ok() - }) - .collect::>()) + s, e, + ); + } + as_c_uint.ok() + }) + .collect::>(), + )) } /// Deactivate an encrypted Stratis device but do not wipe it. This is not @@ -678,9 +921,9 @@ fn ceiling_sector_size_alignment(bytes: u64) -> u64 { /// This method is idempotent and leaves the disk as wiped. fn ensure_wiped(device: &mut CryptDevice, physical_path: &Path, name: &str) -> Result<()> { ensure_inactive(device, name)?; - let keyslot_number = get_keyslot_number(device); + let keyslot_number = get_keyslot_number(device, LUKS2_TOKEN_ID); match keyslot_number { - Ok(nums) => { + Ok(Some(nums)) => { for i in nums.iter() { log_on_failure!( device.keyslot_handle().destroy(*i), @@ -689,6 +932,12 @@ fn ensure_wiped(device: &mut CryptDevice, physical_path: &Path, name: &str) -> R ); } } + Ok(None) => { + info!( + "Token ID for keyslots to be wiped appears to be empty; the keyslot \ + area will still be wiped in the next step." + ); + } Err(e) => { info!( "Keyslot numbers were not found; skipping explicit \ @@ -1126,7 +1375,7 @@ mod tests { handle.deactivate()?; - handle.activate()?; + handle.activate(UnlockMethod::Keyring)?; handle.wipe()?; Ok(()) diff --git a/src/engine/strat_engine/backstore/data_tier.rs b/src/engine/strat_engine/backstore/data_tier.rs index f27e6481ea..ca4c95e082 100644 --- a/src/engine/strat_engine/backstore/data_tier.rs +++ b/src/engine/strat_engine/backstore/data_tier.rs @@ -16,7 +16,6 @@ use crate::{ blockdevmgr::{BlkDevSegment, BlockDevMgr}, shared::{coalesce_blkdevsegs, metadata_to_segment}, }, - names::KeyDescription, serde_structs::{BaseDevSave, BlockDevSave, DataTierSave, Recordable}, }, types::{BlockDevTier, DevUuid, PoolUuid}, @@ -152,16 +151,6 @@ impl DataTier { pub fn blockdevs_mut(&mut self) -> Vec<(DevUuid, &mut StratBlockDev)> { self.block_mgr.blockdevs_mut() } - - /// Data tier is encrypted - pub fn is_encrypted(&self) -> bool { - self.block_mgr.is_encrypted() - } - - /// Data tier key description - pub fn key_desc(&self) -> Option<&KeyDescription> { - self.block_mgr.key_desc() - } } impl Recordable for DataTier { diff --git a/src/engine/strat_engine/backstore/devices.rs b/src/engine/strat_engine/backstore/devices.rs index ba7a1fa8ec..60ccf1eb13 100644 --- a/src/engine/strat_engine/backstore/devices.rs +++ b/src/engine/strat_engine/backstore/devices.rs @@ -12,6 +12,7 @@ use std::{ use chrono::Utc; use itertools::Itertools; +use serde_json::Value; use uuid::Uuid; use devicemapper::{Bytes, Device, Sectors, IEC}; @@ -24,6 +25,7 @@ use crate::{ crypt::{CryptHandle, CryptInitializer}, }, device::blkdev_size, + keys::MemoryPrivateFilesystem, metadata::{ device_identifiers, disown_device, BlockdevSize, MDADataSize, StratisIdentifiers, BDA, @@ -31,7 +33,7 @@ use crate::{ names::KeyDescription, udev::{block_device_apply, decide_ownership, get_udev_property, UdevOwnership}, }, - types::{BlockDevPath, DevUuid, PoolUuid}, + types::{BlockDevPath, DevUuid, EncryptionInfo, PoolUuid}, }, stratis::{ErrorEnum, StratisError, StratisResult}, }; @@ -259,7 +261,7 @@ impl MaybeEncrypted { /// /// The returned error is a `StratisError` to limit usage of /// libcryptsetup-rs outside of the crypt module. - fn to_blockdev_path(&self) -> StratisResult { + fn as_blockdev_path(&self) -> StratisResult { match *self { MaybeEncrypted::Unencrypted(ref path, _) => { Ok(BlockDevPath::physical_device_path(path)) @@ -435,7 +437,7 @@ pub fn initialize_devices( devices: Vec, pool_uuid: PoolUuid, mda_data_size: MDADataSize, - key_description: Option<&KeyDescription>, + encryption_info: Option, ) -> StratisResult> { /// Map a major/minor device number of a physical device /// to the corresponding major/minor number of the encrypted @@ -457,17 +459,49 @@ pub fn initialize_devices( pool_uuid: PoolUuid, dev_uuid: DevUuid, key_description: &KeyDescription, + enable_clevis: Option<(&str, &Value)>, ) -> StratisResult<(CryptHandle, Device, Sectors)> { + fn initialize_encrypted_with_err( + handle: &mut CryptHandle, + key_description: &KeyDescription, + enable_clevis: Option<(&str, &Value)>, + ) -> StratisResult<(Device, Sectors)> { + let device_size = handle.logical_device_size()?; + + if let Some((pin, json)) = enable_clevis { + let mem_fs = MemoryPrivateFilesystem::new()?; + mem_fs.key_op(key_description, |key_path| { + handle + .clevis_bind(key_path, pin, &json, false) + .map_err(|e| StratisError::Error(e.to_string())) + })?; + }; + + map_device_nums( + &handle + .logical_device_path() + .expect("Initialization completed successfully"), + ) + .map(|dn| (dn, device_size)) + } + let mut handle = CryptInitializer::new(physical_path.to_owned(), pool_uuid, dev_uuid) .initialize(key_description)?; - let device_size = handle.logical_device_size()?; - - map_device_nums( - &handle - .logical_device_path() - .expect("Initialization completed successfully"), - ) - .map(|dn| (handle, dn, device_size)) + match initialize_encrypted_with_err(&mut handle, key_description, enable_clevis) { + Ok((devno, devsize)) => Ok((handle, devno, devsize)), + Err(error) => { + if let Err(e) = handle.wipe() { + warn!( + "Failed to clean up encrypted device {}; cleanup \ + was attempted because initialization of the device \ + failed: {}", + handle.physical_device_path().display(), + e + ); + } + Err(error) + } + } } fn initialize_stratis_metadata( @@ -477,7 +511,7 @@ pub fn initialize_devices( dev_uuid: DevUuid, sizes: (MDADataSize, BlockdevSize), id_wwn: &Option>, - key_description: Option<&KeyDescription>, + encryption_info: Option, ) -> StratisResult { let (mda_data_size, data_size) = sizes; let mut f = OpenOptions::new().write(true).open(path.metadata_path())?; @@ -485,7 +519,7 @@ pub fn initialize_devices( // NOTE: Encrypted devices will discard the hardware ID as encrypted devices // are always represented as logical, software-based devicemapper devices // which will never have a hardware ID. - let hw_id = match (key_description.is_some(), id_wwn) { + let hw_id = match (encryption_info.is_some(), id_wwn) { (true, _) => None, (_, Some(Ok(ref hw_id))) => Some(hw_id.to_owned()), (_, Some(Err(_))) => { @@ -513,7 +547,7 @@ pub fn initialize_devices( &[], None, hw_id, - key_description, + encryption_info, ) }) } @@ -568,32 +602,39 @@ pub fn initialize_devices( dev_info: &DeviceInfo, pool_uuid: PoolUuid, mda_data_size: MDADataSize, - key_description: Option<&KeyDescription>, + encryption_info: Option, ) -> StratisResult { let dev_uuid = Uuid::new_v4(); - let (maybe_encrypted, devno, blockdev_size) = match key_description { - Some(desc) => initialize_encrypted(&dev_info.devnode, pool_uuid, dev_uuid, desc).map( - |(handle, devno, devsize)| { - debug!( - "Info on physical device {}, logical device {}", - &dev_info.devnode.display(), - handle - .logical_device_path() - .expect("Initialization must have succeeded") - .display(), - ); - debug!( - "Physical device size: {}, logical device size: {}", - dev_info.size, - devsize.bytes(), - ); - debug!( - "Physical device numbers: {}, logical device numbers: {}", - dev_info.devno, devno, - ); - (MaybeEncrypted::Encrypted(handle), devno, devsize) - }, - )?, + let (maybe_encrypted, devno, blockdev_size) = match encryption_info { + Some(ref info) => initialize_encrypted( + &dev_info.devnode, + pool_uuid, + dev_uuid, + &info.key_description, + info.clevis_info + .as_ref() + .map(|(pin, json)| (pin.as_str(), json)), + ) + .map(|(handle, devno, devsize)| { + debug!( + "Info on physical device {}, logical device {}", + &dev_info.devnode.display(), + handle + .logical_device_path() + .expect("Initialization must have succeeded") + .display(), + ); + debug!( + "Physical device size: {}, logical device size: {}", + dev_info.size, + devsize.bytes(), + ); + debug!( + "Physical device numbers: {}, logical device numbers: {}", + dev_info.devno, devno, + ); + (MaybeEncrypted::Encrypted(handle), devno, devsize) + })?, None => ( MaybeEncrypted::Unencrypted(dev_info.devnode.clone(), pool_uuid), dev_info.devno, @@ -601,7 +642,7 @@ pub fn initialize_devices( ), }; - let path = match maybe_encrypted.to_blockdev_path() { + let path = match maybe_encrypted.as_blockdev_path() { Ok(p) => p, Err(e) => { clean_up(maybe_encrypted); @@ -615,7 +656,7 @@ pub fn initialize_devices( dev_uuid, (mda_data_size, BlockdevSize::new(blockdev_size)), &dev_info.id_wwn, - key_description, + encryption_info, ); if blockdev.is_err() { clean_up(maybe_encrypted); @@ -625,12 +666,7 @@ pub fn initialize_devices( let mut initialized_blockdevs: Vec = Vec::new(); for dev_info in devices { - match initialize_one( - &dev_info, - pool_uuid, - mda_data_size, - key_description.as_deref(), - ) { + match initialize_one(&dev_info, pool_uuid, mda_data_size, encryption_info.clone()) { Ok(blockdev) => initialized_blockdevs.push(blockdev), Err(err) => { if let Err(err) = wipe_blockdevs(&initialized_blockdevs) { @@ -711,7 +747,10 @@ mod tests { dev_infos, pool_uuid, MDADataSize::default(), - key_description, + key_description.map(|kd| EncryptionInfo { + key_description: kd.clone(), + clevis_info: None, + }), )?; if blockdevs.len() != paths.len() { @@ -982,7 +1021,17 @@ mod tests { dev_infos.push(new_info); } - if initialize_devices(dev_infos, pool_uuid, MDADataSize::default(), key_desc).is_ok() { + if initialize_devices( + dev_infos, + pool_uuid, + MDADataSize::default(), + key_desc.map(|kd| EncryptionInfo { + key_description: kd.clone(), + clevis_info: None, + }), + ) + .is_ok() + { return Err(Box::new(StratisError::Error( "Initialization should not have succeeded".to_string(), ))); diff --git a/src/engine/strat_engine/cmd.rs b/src/engine/strat_engine/cmd.rs index 7c08d93c2b..ec095590ed 100644 --- a/src/engine/strat_engine/cmd.rs +++ b/src/engine/strat_engine/cmd.rs @@ -20,6 +20,7 @@ use std::{ process::Command, }; +use serde_json::Value; use uuid::Uuid; use crate::stratis::{StratisError, StratisResult}; @@ -47,6 +48,10 @@ const THIN_REPAIR: &str = "thin_repair"; const UDEVADM: &str = "udevadm"; const XFS_DB: &str = "xfs_db"; const XFS_GROWFS: &str = "xfs_growfs"; +const CLEVIS: &str = "clevis"; +const CLEVIS_BIND: &str = "clevis-luks-bind"; +const CLEVIS_UNBIND: &str = "clevis-luks-unbind"; +const CLEVIS_UNLOCK: &str = "clevis-luks-unlock"; lazy_static! { static ref BINARIES: HashMap> = [ @@ -56,6 +61,10 @@ lazy_static! { (UDEVADM.to_string(), find_binary(UDEVADM)), (XFS_DB.to_string(), find_binary(XFS_DB)), (XFS_GROWFS.to_string(), find_binary(XFS_GROWFS)), + (CLEVIS.to_string(), find_binary(CLEVIS)), + (CLEVIS_BIND.to_string(), find_binary(CLEVIS_BIND)), + (CLEVIS_UNBIND.to_string(), find_binary(CLEVIS_UNBIND)), + (CLEVIS_UNLOCK.to_string(), find_binary(CLEVIS_UNLOCK)), ] .iter() .cloned() @@ -179,3 +188,56 @@ pub fn thin_repair(meta_dev: &Path, new_meta_dev: &Path) -> StratisResult<()> { pub fn udev_settle() -> StratisResult<()> { execute_cmd(Command::new(get_executable(UDEVADM).as_os_str()).arg("settle")) } + +/// Bind a LUKS device using clevis. +pub fn clevis_luks_bind( + dev_path: &Path, + keyfile_path: &Path, + pin: &str, + json: &Value, + yes: bool, +) -> StratisResult<()> { + let mut cmd = Command::new(CLEVIS); + + cmd.arg("luks").arg("bind"); + + if yes { + cmd.arg("-y"); + }; + + cmd.arg("-d") + .arg(dev_path.display().to_string()) + .arg("-k") + .arg(keyfile_path) + .arg(pin) + .arg(json.to_string()); + + execute_cmd(&mut cmd) +} + +/// Unbind a LUKS device using clevis. +pub fn clevis_luks_unbind(dev_path: &Path, keyslot: libc::c_uint) -> StratisResult<()> { + execute_cmd( + Command::new(CLEVIS) + .arg("luks") + .arg("unbind") + .arg("-d") + .arg(dev_path.display().to_string()) + .arg("-s") + .arg(keyslot.to_string()) + .arg("-f"), + ) +} + +/// Unlock a device using the clevis CLI. +pub fn clevis_luks_unlock(dev_path: &Path, dm_name: &str) -> StratisResult<()> { + execute_cmd( + Command::new(CLEVIS) + .arg("luks") + .arg("unlock") + .arg("-d") + .arg(dev_path.display().to_string()) + .arg("-n") + .arg(dm_name), + ) +} diff --git a/src/engine/strat_engine/dm.rs b/src/engine/strat_engine/dm.rs index 42b2ffc315..1b513f9866 100644 --- a/src/engine/strat_engine/dm.rs +++ b/src/engine/strat_engine/dm.rs @@ -24,11 +24,9 @@ pub fn get_dm_init() -> StratisResult<&'static DM> { INIT.call_once(|| DM_CONTEXT = Some(DM::new())); match DM_CONTEXT { Some(Ok(ref context)) => Ok(context), - // Can not move the error out of DM_CONTEXT, so synthesize a new - // error. - Some(Err(_)) => Err(StratisError::Engine( + Some(Err(ref e)) => Err(StratisError::Engine( ErrorEnum::Error, - "Failed to initialize DM context".into(), + format!("Failed to initialize DM context: {}", e), )), _ => panic!("DM_CONTEXT.is_some()"), } diff --git a/src/engine/strat_engine/engine.rs b/src/engine/strat_engine/engine.rs index 32761ef8de..a314482b8e 100644 --- a/src/engine/strat_engine/engine.rs +++ b/src/engine/strat_engine/engine.rs @@ -17,13 +17,15 @@ use crate::{ cmd::verify_binaries, devlinks, dm::{get_dm, get_dm_init}, - keys::StratKeyActions, + keys::{MemoryFilesystem, StratKeyActions}, liminal::{find_all, LiminalDevices}, - names::KeyDescription, pool::StratPool, }, structures::Table, - types::{CreateAction, DeleteAction, DevUuid, RenameAction, ReportType, SetUnlockAction}, + types::{ + CreateAction, DeleteAction, DevUuid, EncryptionInfo, KeyDescription, RenameAction, + ReportType, SetUnlockAction, UnlockMethod, + }, Engine, EngineEvent, Name, Pool, PoolUuid, Report, }, stratis::{ErrorEnum, StratisError, StratisResult}, @@ -45,6 +47,12 @@ pub struct StratEngine { // Handler for key operations key_handler: StratKeyActions, + + // TODO: Remove this code when Clevis supports reading keys from the + // kernel keyring. + // In memory filesystem for passing keys to Clevis. + // See GitHub issue: https://github.com/stratis-storage/project/issues/212. + key_fs: MemoryFilesystem, } impl StratEngine { @@ -80,6 +88,7 @@ impl StratEngine { liminal_devices, watched_dev_last_event_nrs: HashMap::new(), key_handler: StratKeyActions, + key_fs: MemoryFilesystem::new()?, }) } @@ -262,8 +271,14 @@ impl Engine for StratEngine { } } - fn unlock_pool(&mut self, pool_uuid: PoolUuid) -> StratisResult> { - let unlocked = self.liminal_devices.unlock_pool(&self.pools, pool_uuid)?; + fn unlock_pool( + &mut self, + pool_uuid: PoolUuid, + unlock_method: UnlockMethod, + ) -> StratisResult> { + let unlocked = self + .liminal_devices + .unlock_pool(&self.pools, pool_uuid, unlock_method)?; Ok(SetUnlockAction::new(unlocked)) } @@ -275,7 +290,7 @@ impl Engine for StratEngine { get_mut_pool!(self; uuid) } - fn locked_pools(&self) -> HashMap { + fn locked_pools(&self) -> HashMap { self.liminal_devices.locked_pools() } diff --git a/src/engine/strat_engine/keys.rs b/src/engine/strat_engine/keys.rs index 5a1537c4aa..c0f76489b1 100644 --- a/src/engine/strat_engine/keys.rs +++ b/src/engine/strat_engine/keys.rs @@ -2,11 +2,28 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use std::{ffi::CString, io, mem::size_of, os::unix::io::RawFd, str}; +use std::{ + ffi::CString, + fs::{create_dir_all, remove_file, OpenOptions}, + io::{self, Write}, + mem::size_of, + os::unix::io::{AsRawFd, RawFd}, + path::{Path, PathBuf}, + ptr, slice, str, +}; use libc::{syscall, SYS_add_key, SYS_keyctl}; +use nix::{ + mount::{mount, umount, MsFlags}, + sched::{unshare, CloneFlags}, + sys::{ + mman::{mmap, munmap, MapFlags, ProtFlags}, + stat::stat, + }, +}; +use rand::{distributions::Standard, Rng}; -use libcryptsetup_rs::SafeMemHandle; +use libcryptsetup_rs::{SafeBorrowedMemZero, SafeMemHandle}; use crate::{ engine::{ @@ -397,3 +414,232 @@ impl KeyActions for StratKeyActions { } } } + +/// A top-level tmpfs that can be made a private recursive mount so that any tmpfs +/// mounts inside of it will not be visible to any process but stratisd. +#[derive(Debug)] +pub struct MemoryFilesystem; + +impl MemoryFilesystem { + pub const TMPFS_LOCATION: &'static str = "/run/stratisd/keyfiles"; + + pub fn new() -> StratisResult { + let tmpfs_path = &Path::new(Self::TMPFS_LOCATION); + if tmpfs_path.exists() { + if !tmpfs_path.is_dir() { + return Err(StratisError::Io(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("{} exists and is not a directory", tmpfs_path.display()), + ))); + } else { + let stat_info = stat(Self::TMPFS_LOCATION)?; + let parent_path: PathBuf = vec![Self::TMPFS_LOCATION, ".."].iter().collect(); + let parent_stat_info = stat(&parent_path)?; + if stat_info.st_dev != parent_stat_info.st_dev { + info!("Mount found at {}; unmounting", Self::TMPFS_LOCATION); + if let Err(e) = umount(Self::TMPFS_LOCATION) { + warn!( + "Failed to unmount filesystem at {}: {}", + Self::TMPFS_LOCATION, + e + ); + } + } + } + } else { + create_dir_all(Self::TMPFS_LOCATION)?; + }; + mount( + Some("tmpfs"), + Self::TMPFS_LOCATION, + Some("tmpfs"), + MsFlags::empty(), + Some("size=1M"), + )?; + + unshare(CloneFlags::CLONE_NEWNS)?; + mount::( + None, + MemoryFilesystem::TMPFS_LOCATION, + None, + MsFlags::MS_SLAVE | MsFlags::MS_REC, + None, + )?; + Ok(MemoryFilesystem) + } +} + +impl Drop for MemoryFilesystem { + fn drop(&mut self) { + if let Err(e) = umount(Self::TMPFS_LOCATION) { + warn!( + "Could not unmount temporary in memory storage for Clevis keyfiles: {}", + e + ); + } + } +} + +/// An in-memory filesystem that mounts a tmpfs that can house keyfiles so that they +/// are never writen to disk. The interface aims to keep the keys in memory for as +/// short of a period of time as possible (only for the duration of the operation +/// that the keyfile is needed for). +pub struct MemoryPrivateFilesystem(PathBuf); + +impl MemoryPrivateFilesystem { + pub fn new() -> StratisResult { + let tmpfs_path = &Path::new(MemoryFilesystem::TMPFS_LOCATION); + if tmpfs_path.exists() { + if !tmpfs_path.is_dir() { + return Err(StratisError::Io(io::Error::new( + io::ErrorKind::AlreadyExists, + format!("{} exists and is not a directory", tmpfs_path.display()), + ))); + } else { + let stat_info = stat(MemoryFilesystem::TMPFS_LOCATION)?; + let parent_path: PathBuf = vec![MemoryFilesystem::TMPFS_LOCATION, ".."] + .iter() + .collect(); + let parent_stat_info = stat(&parent_path)?; + if stat_info.st_dev == parent_stat_info.st_dev { + return Err(StratisError::Io(io::Error::new( + io::ErrorKind::NotFound, + format!( + "No mount found at {} which is required to proceed", + tmpfs_path.display(), + ), + ))); + } + } + } else { + return Err(StratisError::Io(io::Error::new( + io::ErrorKind::NotFound, + format!("Path {} does not exist", MemoryFilesystem::TMPFS_LOCATION,), + ))); + }; + let mut random_string = String::new(); + while random_string.len() < 16 { + let ch: char = rand::thread_rng().sample(Standard); + if ch.is_ascii_alphanumeric() { + random_string.push(ch); + } + } + let private_fs_path = vec![MemoryFilesystem::TMPFS_LOCATION, &random_string] + .iter() + .collect(); + create_dir_all(&private_fs_path)?; + + // Ensure that the original tmpfs mount point is private. This will work + // even if someone mounts their own volume at this mount point as the + // mount only needs to be private, it does not need to be tmpfs. + // The mount directly after this one will also be a tmpfs meaning that + // no keys will be written to disk even if this mount turns out to be + // a physical device. + mount::( + None, + MemoryFilesystem::TMPFS_LOCATION, + None, + MsFlags::MS_SLAVE | MsFlags::MS_REC, + None, + )?; + mount( + Some("tmpfs"), + &private_fs_path, + Some("tmpfs"), + MsFlags::empty(), + Some("size=1M"), + )?; + Ok(MemoryPrivateFilesystem(private_fs_path)) + } + + pub fn key_op(&self, key_desc: &KeyDescription, mut f: F) -> StratisResult<()> + where + F: FnMut(&Path) -> StratisResult<()>, + { + let persistent_id = get_persistent_keyring()?; + let key_data = if let Some((_, mem)) = read_key(persistent_id, key_desc)? { + mem + } else { + return Err(StratisError::Io(io::Error::new( + io::ErrorKind::NotFound, + format!( + "Key with given key description {} was not found", + key_desc.as_application_str() + ), + ))); + }; + let mut mem_file_path = PathBuf::from(&self.0); + mem_file_path.push(key_desc.as_application_str()); + let mem_file = MemoryMappedKeyfile::new(&mem_file_path, key_data)?; + f(mem_file.keyfile_path()) + } +} + +impl Drop for MemoryPrivateFilesystem { + fn drop(&mut self) { + if let Err(e) = umount(&self.0) { + warn!( + "Could not unmount temporary in memory storage for Clevis keyfiles: {}", + e + ); + } + } +} + +/// Keyfile integration with Clevis for keys so that they are never written to disk. +/// This struct will handle memory mapping and locking internally to avoid disk usage. +pub struct MemoryMappedKeyfile(*mut libc::c_void, usize, PathBuf); + +impl MemoryMappedKeyfile { + pub fn new(file_path: &Path, key_data: SizedKeyMemory) -> StratisResult { + if file_path.exists() { + return Err(StratisError::Io(io::Error::new( + io::ErrorKind::AlreadyExists, + "Keyfile is already present", + ))); + } + + let keyfile = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .open(file_path)?; + let needed_keyfile_length = key_data.as_ref().len(); + keyfile.set_len(convert_int!(needed_keyfile_length, usize, u64)?)?; + let mem = unsafe { + mmap( + ptr::null_mut(), + needed_keyfile_length, + ProtFlags::PROT_WRITE, + MapFlags::MAP_SHARED | MapFlags::MAP_LOCKED, + keyfile.as_raw_fd(), + 0, + ) + }?; + let mut slice = unsafe { slice::from_raw_parts_mut(mem as *mut u8, needed_keyfile_length) }; + slice.write_all(key_data.as_ref())?; + Ok(MemoryMappedKeyfile( + mem, + needed_keyfile_length, + file_path.to_owned(), + )) + } + + pub fn keyfile_path(&self) -> &Path { + &self.2 + } +} + +impl Drop for MemoryMappedKeyfile { + fn drop(&mut self) { + { + unsafe { SafeBorrowedMemZero::from_ptr(self.0, self.1) }; + } + if let Err(e) = unsafe { munmap(self.0, self.1) } { + warn!("Could not unmap temporary keyfile: {}", e); + } + if let Err(e) = remove_file(self.keyfile_path()) { + warn!("Failed to clean up temporary key file: {}", e); + } + } +} diff --git a/src/engine/strat_engine/liminal/device_info.rs b/src/engine/strat_engine/liminal/device_info.rs index 2de7fa7a9d..21912b7182 100644 --- a/src/engine/strat_engine/liminal/device_info.rs +++ b/src/engine/strat_engine/liminal/device_info.rs @@ -16,7 +16,7 @@ use crate::engine::{ liminal::identify::{DeviceInfo, LuksInfo, StratisInfo}, metadata::StratisIdentifiers, }, - types::{DevUuid, KeyDescription}, + types::{DevUuid, EncryptionInfo}, }; /// Info for a discovered Luks Device belonging to Stratis. @@ -24,17 +24,12 @@ use crate::engine::{ pub struct LLuksInfo { /// Generic information + Stratis identifiers pub ids: StratisInfo, - pub key_description: KeyDescription, + pub encryption_info: EncryptionInfo, } impl fmt::Display for LLuksInfo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}, key description: \"{}\"", - self.ids, - self.key_description.as_application_str() - ) + write!(f, "{}, {}", self.ids, self.encryption_info) } } @@ -42,28 +37,28 @@ impl From for LLuksInfo { fn from(info: LuksInfo) -> LLuksInfo { LLuksInfo { ids: info.info, - key_description: info.key_description, + encryption_info: info.encryption_info, } } } impl<'a> Into for &'a LLuksInfo { // Precondition: (&StratisInfo).into() pattern matches Value::Object() + // Precondition: (&EncryptionInfo).into() pattern matches Value::Object() fn into(self) -> Value { - let mut json = json!({ - "key_description": Value::from(self.key_description.as_application_str()) - }); - if let Value::Object(ref mut map) = json { - map.extend( - if let Value::Object(map) = <&StratisInfo as Into>::into(&self.ids) { - map.into_iter() - } else { - unreachable!("StratisInfo conversion returns a JSON object"); - }, - ); - } else { - unreachable!("json!() always creates a JSON object"); - }; + let mut json = <&StratisInfo as Into>::into(&self.ids); + let map = json + .as_object_mut() + .expect("StratisInfo conversion returns a JSON object"); + map.extend( + if let Value::Object(enc_map) = + <&EncryptionInfo as Into>::into(&self.encryption_info) + { + enc_map.into_iter() + } else { + unreachable!("EncryptionInfo conversion returns a JSON object"); + }, + ); json } } @@ -186,17 +181,17 @@ impl LInfo { } } - pub fn key_desc(&self) -> Option<&KeyDescription> { + fn encryption_info(&self) -> Option<&EncryptionInfo> { match self { - LInfo::Luks(info) => Some(&info.key_description), - LInfo::Stratis(info) => info.luks.as_ref().map(|i| &i.key_description), + LInfo::Luks(info) => Some(&info.encryption_info), + LInfo::Stratis(info) => info.luks.as_ref().map(|i| &i.encryption_info), } } /// Returns true if the data represents a device with encryption managed /// by Stratis, otherwise false. pub fn is_encrypted(&self) -> bool { - self.key_desc().is_some() + self.encryption_info().is_some() } /// Returns true if the data represents a device with encryption managed @@ -267,7 +262,7 @@ impl LInfo { fn luks_luks_compatible(info_1: &LLuksInfo, info_2: &LuksInfo) -> bool { assert_eq!(info_1.ids.identifiers, info_2.info.identifiers); info_1.ids.device_number == info_2.info.device_number - && info_1.key_description == info_2.key_description + && info_1.encryption_info == info_2.encryption_info } // Returns true if the information found via udev for two devices is @@ -346,12 +341,12 @@ impl DeviceSet { #[cfg(test)] #[allow(dead_code)] fn invariant(&self) { - let key_descriptions: HashSet = self + let encryption_infos: HashSet = self .internal .iter() - .filter_map(|(_, info)| info.key_desc().cloned()) + .filter_map(|(_, info)| info.encryption_info().cloned()) .collect(); - assert!(key_descriptions.is_empty() || key_descriptions.len() == 1); + assert!(encryption_infos.is_empty() || encryption_infos.len() == 1); } /// Create a new, empty DeviceSet @@ -404,12 +399,12 @@ impl DeviceSet { } } - /// The unique key description for this set. If none of the infos + /// The unique encryption info for this set. If none of the infos /// correspond to a Stratis managed encrypted device, None. - pub fn key_description(&self) -> Option<&KeyDescription> { + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { self.internal .iter() - .filter_map(|(_, info)| info.key_desc()) + .filter_map(|(_, info)| info.encryption_info()) .next() } @@ -442,10 +437,10 @@ impl DeviceSet { // description, then it is likely that all the devices in the set also // have a key description, so that the search for a device with a key // description will stop at the first one. - if let Some(key_desc) = info.key_description() { + if let Some(encryption_info) = info.encryption_info() { if self - .key_description() - .filter(|&kd| kd != key_desc) + .encryption_info() + .filter(|&ei| ei != encryption_info) .is_some() { let mut hopeless: HashSet = diff --git a/src/engine/strat_engine/liminal/identify.rs b/src/engine/strat_engine/liminal/identify.rs index e7941914cd..2f417bfd39 100644 --- a/src/engine/strat_engine/liminal/identify.rs +++ b/src/engine/strat_engine/liminal/identify.rs @@ -58,7 +58,7 @@ use crate::engine::{ STRATIS_FS_TYPE, }, }, - types::{KeyDescription, PoolUuid}, + types::{EncryptionInfo, PoolUuid}, }; /// A miscellaneous group of identifiers found when identifying a LUKS @@ -67,18 +67,13 @@ use crate::engine::{ pub struct LuksInfo { /// All the usual StratisInfo pub info: StratisInfo, - /// The key description, obtained from the LUKS keyring token - pub key_description: KeyDescription, + /// Encryption information + pub encryption_info: EncryptionInfo, } impl fmt::Display for LuksInfo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{}, key description: \"{}\"", - self.info, - self.key_description.as_application_str() - ) + write!(f, "{}, {}", self.info, self.encryption_info,) } } @@ -145,9 +140,9 @@ impl DeviceInfo { } } - pub fn key_description(&self) -> Option<&KeyDescription> { + pub fn encryption_info(&self) -> Option<&EncryptionInfo> { match self { - DeviceInfo::Luks(info) => Some(&info.key_description), + DeviceInfo::Luks(info) => Some(&info.encryption_info), DeviceInfo::Stratis(_) => None, } } @@ -226,14 +221,27 @@ fn process_luks_device(dev: &libudev::Device) -> Option { ); None } - Ok(Some(handle)) => Some(LuksInfo { - info: StratisInfo { - identifiers: *handle.device_identifiers(), - device_number, - devnode: handle.physical_device_path().to_path_buf(), - }, - key_description: handle.key_description().clone(), - }), + Ok(Some(mut handle)) => match handle.clevis_info() { + Ok(clevis_info) => Some(LuksInfo { + info: StratisInfo { + identifiers: *handle.device_identifiers(), + device_number, + devnode: handle.physical_device_path().to_path_buf(), + }, + encryption_info: EncryptionInfo { + key_description: handle.key_description().clone(), + clevis_info, + }, + }), + Err(err) => { + warn!( + "There was a problem decoding the Clevis info on device {}, disregarding the device: {}", + devnode.display(), + err + ); + None + } + }, }, }, None => { @@ -443,12 +451,15 @@ mod tests { use uuid::Uuid; use crate::{ - engine::strat_engine::{ - backstore::{initialize_devices, process_and_verify_devices}, - cmd::create_fs, - metadata::MDADataSize, - tests::{crypt, loopbacked, real}, - udev::block_device_apply, + engine::{ + strat_engine::{ + backstore::{initialize_devices, process_and_verify_devices}, + cmd::create_fs, + metadata::MDADataSize, + tests::{crypt, loopbacked, real}, + udev::block_device_apply, + }, + types::{EncryptionInfo, KeyDescription}, }, stratis::StratisError, }; @@ -476,7 +487,10 @@ mod tests { process_and_verify_devices(pool_uuid, &HashSet::new(), paths)?, pool_uuid, MDADataSize::default(), - Some(key_description), + Some(EncryptionInfo { + key_description: key_description.clone(), + clevis_info: None, + }), )?; for devnode in devices.iter().map(|sbd| sbd.devnode()) { @@ -509,10 +523,10 @@ mod tests { )))); } - if &info.key_description != key_description { + if &info.encryption_info.key_description != key_description { return Err(Box::new(StratisError::Error(format!( "Discovered key description {} != expected key description {}", - info.key_description.as_application_str(), + info.encryption_info.key_description.as_application_str(), key_description.as_application_str() )))); } diff --git a/src/engine/strat_engine/liminal/liminal.rs b/src/engine/strat_engine/liminal/liminal.rs index 6f92f26854..eecc71232f 100644 --- a/src/engine/strat_engine/liminal/liminal.rs +++ b/src/engine/strat_engine/liminal/liminal.rs @@ -25,7 +25,7 @@ use crate::{ pool::StratPool, }, structures::Table, - types::{DevUuid, KeyDescription, Name, PoolUuid}, + types::{DevUuid, EncryptionInfo, Name, PoolUuid, UnlockMethod}, }, stratis::{ErrorEnum, StratisError, StratisResult}, }; @@ -82,10 +82,11 @@ impl LiminalDevices { &mut self, pools: &Table, pool_uuid: PoolUuid, + unlock_method: UnlockMethod, ) -> StratisResult> { - fn handle_luks(luks_info: &LLuksInfo) -> StratisResult<()> { + fn handle_luks(luks_info: &LLuksInfo, unlock_method: UnlockMethod) -> StratisResult<()> { if let Some(mut handle) = CryptHandle::setup(&luks_info.ids.devnode)? { - handle.activate()?; + handle.activate(unlock_method)?; Ok(()) } else { Err(StratisError::Engine( @@ -115,7 +116,7 @@ impl LiminalDevices { for (dev_uuid, info) in map.iter() { match info { LInfo::Stratis(_) => (), - LInfo::Luks(ref luks_info) => match handle_luks(luks_info) { + LInfo::Luks(ref luks_info) => match handle_luks(luks_info, unlock_method) { Ok(()) => unlocked.push(*dev_uuid), Err(e) => return Err(e), }, @@ -153,13 +154,15 @@ impl LiminalDevices { } /// Get a mapping of pool UUIDs from all of the LUKS2 devices that are currently - /// locked to their key descriptions in the set of pools that are not yet set up. + /// locked to their encryption info in the set of pools that are not yet set up. // Precondition: All devices for a given errored pool have been determined to have - // the same key description. - pub fn locked_pools(&self) -> HashMap { + // the same encryption info. + pub fn locked_pools(&self) -> HashMap { self.errored_pool_devices .iter() - .filter_map(|(pool_uuid, map)| map.key_description().map(|kd| (*pool_uuid, kd.clone()))) + .filter_map(|(pool_uuid, map)| { + map.encryption_info().map(|info| (*pool_uuid, info.clone())) + }) .collect() } @@ -313,9 +316,12 @@ impl LiminalDevices { ))); } + // NOTE: DeviceSet provides infos variable in setup_pool. DeviceSet + // ensures that all encryption infos match so we do not need to + // check again here. let num_with_luks = datadevs .iter() - .filter_map(|sbd| sbd.key_description()) + .filter_map(|sbd| sbd.encryption_info()) .count(); if num_with_luks != 0 && num_with_luks != datadevs.len() { @@ -331,23 +337,7 @@ impl LiminalDevices { &metadata.name))); } - // Either all the devices have a key description or none do; - // so only the first device needs to be accessed. - let key_description = datadevs - .get(0) - .expect("returned with error above if datadevs empty") - .key_description() - .cloned(); - - StratPool::setup( - pool_uuid, - datadevs, - cachedevs, - timestamp, - &metadata, - key_description.as_ref(), - ) - .map_err(|err| { + StratPool::setup(pool_uuid, datadevs, cachedevs, timestamp, &metadata).map_err(|err| { Destination::Errored(format!( "An attempt to set up pool with UUID {} from the assembled devices failed: {}", pool_uuid.to_simple_ref(), diff --git a/src/engine/strat_engine/liminal/setup.rs b/src/engine/strat_engine/liminal/setup.rs index 3e4e7ac461..e3c69cc8bb 100644 --- a/src/engine/strat_engine/liminal/setup.rs +++ b/src/engine/strat_engine/liminal/setup.rs @@ -218,10 +218,10 @@ pub fn get_blockdevs( // conclusion is metadata corruption. let segments = segment_table.get(&dev_uuid); - let (path, key_description) = match &info.luks { + let (path, encryption_info) = match &info.luks { Some(luks) => ( BlockDevPath::mapped_device_path(&luks.ids.devnode, &info.ids.devnode)?, - Some(&luks.key_description), + Some(&luks.encryption_info), ), None => (BlockDevPath::physical_device_path(&info.ids.devnode), None), }; @@ -235,7 +235,7 @@ pub fn get_blockdevs( segments.unwrap_or(&vec![]), bd_save.user_info.clone(), bd_save.hardware_info.clone(), - key_description, + encryption_info.cloned(), )?, )) } diff --git a/src/engine/strat_engine/pool.rs b/src/engine/strat_engine/pool.rs index a400420eff..63d72e6459 100644 --- a/src/engine/strat_engine/pool.rs +++ b/src/engine/strat_engine/pool.rs @@ -22,8 +22,9 @@ use crate::{ thinpool::{ThinPool, ThinPoolSizeParams, DATA_BLOCK_SIZE}, }, types::{ - BlockDevTier, CreateAction, DevUuid, FilesystemUuid, MaybeDbusPath, Name, PoolUuid, - Redundancy, RenameAction, SetCreateAction, SetDeleteAction, + BlockDevTier, CreateAction, DeleteAction, DevUuid, EncryptionInfo, FilesystemUuid, + MaybeDbusPath, Name, PoolUuid, Redundancy, RenameAction, SetCreateAction, + SetDeleteAction, }, }, stratis::{ErrorEnum, StratisError, StratisResult}, @@ -202,18 +203,11 @@ impl StratPool { cachedevs: Vec, timestamp: DateTime, metadata: &PoolSave, - key_description: Option<&KeyDescription>, ) -> StratisResult<(Name, StratPool)> { check_metadata(metadata)?; - let mut backstore = Backstore::setup( - uuid, - &metadata.backstore, - datadevs, - cachedevs, - timestamp, - key_description, - )?; + let mut backstore = + Backstore::setup(uuid, &metadata.backstore, datadevs, cachedevs, timestamp)?; let mut thinpool = ThinPool::setup( uuid, &metadata.thinpool_dev, @@ -379,6 +373,24 @@ impl Pool for StratPool { } } + fn bind_clevis(&mut self, pin: String, clevis_info: Value) -> StratisResult> { + let changed = self.backstore.bind_clevis(pin, clevis_info)?; + if changed { + Ok(CreateAction::Created(())) + } else { + Ok(CreateAction::Identity) + } + } + + fn unbind_clevis(&mut self) -> StratisResult> { + let changed = self.backstore.unbind_clevis()?; + if changed { + Ok(DeleteAction::Deleted(())) + } else { + Ok(DeleteAction::Identity) + } + } + fn create_filesystems<'a, 'b>( &'a mut self, pool_uuid: PoolUuid, @@ -608,8 +620,8 @@ impl Pool for StratPool { self.datadevs_encrypted() } - fn key_desc(&self) -> Option<&KeyDescription> { - self.backstore.data_key_desc() + fn encryption_info(&self) -> Option<&EncryptionInfo> { + self.backstore.data_tier_encryption_info() } } diff --git a/src/engine/strat_engine/tests/util.rs b/src/engine/strat_engine/tests/util.rs index a7e626a0d4..f359daf6a9 100644 --- a/src/engine/strat_engine/tests/util.rs +++ b/src/engine/strat_engine/tests/util.rs @@ -14,10 +14,6 @@ use crate::engine::strat_engine::{ }; mod cleanup_errors { - // FIXME: It should be possible to remove this allow when the next - // version of error_chain is released. - #![allow(deprecated)] - error_chain! { foreign_links { Ioe(std::io::Error); diff --git a/src/engine/types/keys.rs b/src/engine/types/keys.rs index 3ef476866e..c9619e0570 100644 --- a/src/engine/types/keys.rs +++ b/src/engine/types/keys.rs @@ -5,8 +5,11 @@ use std::{ convert::TryFrom, fmt::{self, Debug}, + hash::{Hash, Hasher}, }; +use serde_json::Value; + use libcryptsetup_rs::SafeMemHandle; use crate::stratis::{ErrorEnum, StratisError, StratisResult}; @@ -36,6 +39,56 @@ impl AsRef<[u8]> for SizedKeyMemory { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EncryptionInfo { + pub key_description: KeyDescription, + pub clevis_info: Option<(String, Value)>, +} + +impl fmt::Display for EncryptionInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let key_desc_str = format!( + "key description: \"{}\"", + self.key_description.as_application_str() + ); + if let Some((pin, config)) = &self.clevis_info { + write!( + f, + "{}, clevis pin: \"{}\", clevis configuration: \"{}\"", + key_desc_str, pin, config + ) + } else { + write!(f, "{}, no Clevis information", key_desc_str) + } + } +} + +// Implement Hash explicitly because Value does not implement Hash. +// The reason Value does not implement Hash is that some JSON is really big +// and hashing it would be expensive. This JSON probably won't be, but it +// serves no obvious purpose to stringify it and then hash it. +// Necessary Hash Property: \forall x_1, x_2 in EncryptionInfo, +// if x_1 == x_2, then hash(x_1) == hash(x_2) obviously holds. +#[allow(clippy::derive_hash_xor_eq)] +impl Hash for EncryptionInfo { + fn hash(&self, state: &mut H) { + self.key_description.hash(state); + self.clevis_info.as_ref().map(|(pin, _)| pin).hash(state); + } +} + +impl<'a> Into for &'a EncryptionInfo { + fn into(self) -> Value { + let mut json = json!({"key_description": self.key_description.as_application_str()}); + if let Some(ref info) = self.clevis_info { + let map = json.as_object_mut().expect("Created a JSON object above"); + map.insert("clevis_pin".to_string(), Value::from(info.0.to_owned())); + map.insert("clevis_config".to_string(), info.1.clone()); + } + json + } +} + /// A data type respresenting a key description for the kernel keyring #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct KeyDescription(String); @@ -65,3 +118,11 @@ impl TryFrom for KeyDescription { } } } + +impl<'a> TryFrom<&'a String> for KeyDescription { + type Error = StratisError; + + fn try_from(s: &String) -> StratisResult { + KeyDescription::try_from(s.to_owned()) + } +} diff --git a/src/engine/types/mod.rs b/src/engine/types/mod.rs index f86e75c251..8980a8242e 100644 --- a/src/engine/types/mod.rs +++ b/src/engine/types/mod.rs @@ -19,7 +19,7 @@ pub use crate::engine::types::{ CreateAction, DeleteAction, EngineAction, MappingCreateAction, RenameAction, SetCreateAction, SetDeleteAction, SetUnlockAction, }, - keys::{KeyDescription, SizedKeyMemory}, + keys::{EncryptionInfo, KeyDescription, SizedKeyMemory}, }; use crate::stratis::{ErrorEnum, StratisError, StratisResult}; @@ -29,6 +29,28 @@ pub type DevUuid = Uuid; pub type FilesystemUuid = Uuid; pub type PoolUuid = Uuid; +/// Use Clevis or keyring to unlock LUKS volume. +#[derive(Clone, Copy)] +pub enum UnlockMethod { + Clevis, + Keyring, +} + +impl<'a> TryFrom<&'a str> for UnlockMethod { + type Error = StratisError; + + fn try_from(s: &str) -> StratisResult { + match s { + "keyring" => Ok(UnlockMethod::Keyring), + "clevis" => Ok(UnlockMethod::Clevis), + _ => Err(StratisError::Error(format!( + "{} is an invalid unlock method", + s + ))), + } + } +} + /// See Design Doc section 10.2.1 for more details. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum BlockDevState { @@ -65,6 +87,7 @@ pub enum Redundancy { } #[derive(Debug, PartialEq, Eq, Hash, Clone)] +#[allow(clippy::rc_buffer)] pub struct Name(Rc); impl Name { diff --git a/stratisd.conf b/stratisd.conf index f36722aad6..1ce0b0bf47 100644 --- a/stratisd.conf +++ b/stratisd.conf @@ -25,6 +25,9 @@ + + diff --git a/tests/client-dbus/src/stratisd_client_dbus/_data.py b/tests/client-dbus/src/stratisd_client_dbus/_data.py index 1ee4762280..101eca7f5f 100644 --- a/tests/client-dbus/src/stratisd_client_dbus/_data.py +++ b/tests/client-dbus/src/stratisd_client_dbus/_data.py @@ -6,8 +6,8 @@ """, - "org.storage.stratis2.FetchProperties.r2": """ - + "org.storage.stratis2.FetchProperties.r3": """ + @@ -17,8 +17,8 @@ """, - "org.storage.stratis2.Manager.r2": """ - + "org.storage.stratis2.Manager.r3": """ + @@ -49,6 +49,7 @@ + @@ -131,8 +132,8 @@ """, - "org.storage.stratis2.pool.r1": """ - + "org.storage.stratis2.pool.r3": """ + @@ -145,6 +146,13 @@ + + + + + + + @@ -176,6 +184,11 @@ + + + + + diff --git a/tests/client-dbus/src/stratisd_client_dbus/_implementation.py b/tests/client-dbus/src/stratisd_client_dbus/_implementation.py index 6338c80723..b915ddc60c 100644 --- a/tests/client-dbus/src/stratisd_client_dbus/_implementation.py +++ b/tests/client-dbus/src/stratisd_client_dbus/_implementation.py @@ -24,7 +24,7 @@ from ._data import SPECS -_POOL_SPEC = ET.fromstring(SPECS["org.storage.stratis2.pool.r1"]) +_POOL_SPEC = ET.fromstring(SPECS["org.storage.stratis2.pool.r3"]) _FILESYSTEM_SPEC = ET.fromstring(SPECS["org.storage.stratis2.filesystem"]) _BLOCKDEV_SPEC = ET.fromstring(SPECS["org.storage.stratis2.blockdev.r2"]) @@ -46,11 +46,11 @@ "Report", ET.fromstring(SPECS["org.storage.stratis2.Report.r1"]), TIME_OUT ) Manager = make_class( - "Manager", ET.fromstring(SPECS["org.storage.stratis2.Manager.r2"]), TIME_OUT + "Manager", ET.fromstring(SPECS["org.storage.stratis2.Manager.r3"]), TIME_OUT ) FetchProperties = make_class( "FetchProperties", - ET.fromstring(SPECS["org.storage.stratis2.FetchProperties.r2"]), + ET.fromstring(SPECS["org.storage.stratis2.FetchProperties.r3"]), TIME_OUT, ) Filesystem = make_class("Filesystem", _FILESYSTEM_SPEC, TIME_OUT) diff --git a/tests/client-dbus/src/stratisd_client_dbus/_stratisd_constants.py b/tests/client-dbus/src/stratisd_client_dbus/_stratisd_constants.py index 754e1bb8ae..e8641ae340 100644 --- a/tests/client-dbus/src/stratisd_client_dbus/_stratisd_constants.py +++ b/tests/client-dbus/src/stratisd_client_dbus/_stratisd_constants.py @@ -16,7 +16,7 @@ """ # isort: STDLIB -from enum import IntEnum +from enum import Enum, IntEnum class StratisdErrors(IntEnum): @@ -40,3 +40,15 @@ class BlockDevTiers(IntEnum): Data = 0 Cache = 1 + + +class EncryptionMethod(Enum): + """ + Encryption method, used as argument to unlock. + """ + + KEYRING = "keyring" + CLEVIS = "clevis" + + def __str__(self): + return self.value diff --git a/tests/client-dbus/tests/udev/test_udev.py b/tests/client-dbus/tests/udev/test_udev.py index 3127e1039f..6cc7c18176 100644 --- a/tests/client-dbus/tests/udev/test_udev.py +++ b/tests/client-dbus/tests/udev/test_udev.py @@ -31,6 +31,7 @@ get_object, ) from stratisd_client_dbus._constants import TOP_OBJECT +from stratisd_client_dbus._stratisd_constants import EncryptionMethod from ._loopback import UDEV_ADD_EVENT, LoopBackDevices from ._utils import ( @@ -301,7 +302,11 @@ def _simple_initial_discovery_test( # pylint: disable=bad-continuation with OptionalKeyServiceContextManager(key_spec=key_spec): ((option, unlock_uuids), exit_code, _) = Manager.Methods.UnlockPool( - get_object(TOP_OBJECT), {"pool_uuid": pool_uuid} + get_object(TOP_OBJECT), + { + "pool_uuid": pool_uuid, + "unlock_method": str(EncryptionMethod.KEYRING), + }, ) if key_spec is None: self.assertNotEqual(exit_code, StratisdErrors.OK) @@ -400,7 +405,11 @@ def _simple_event_test(self, *, key_spec=None): # pylint: disable=too-many-loca self.assertEqual(len(get_pools()), 0) ((option, unlock_uuids), exit_code, _) = Manager.Methods.UnlockPool( - get_object(TOP_OBJECT), {"pool_uuid": pool_uuid} + get_object(TOP_OBJECT), + { + "pool_uuid": pool_uuid, + "unlock_method": str(EncryptionMethod.KEYRING), + }, ) if key_spec is None: self.assertNotEqual(exit_code, StratisdErrors.OK) @@ -418,7 +427,11 @@ def _simple_event_test(self, *, key_spec=None): # pylint: disable=too-many-loca wait_for_udev(udev_wait_type, self._lb_mgr.device_files(tokens_up)) ((option, unlock_uuids), exit_code, _) = Manager.Methods.UnlockPool( - get_object(TOP_OBJECT), {"pool_uuid": pool_uuid} + get_object(TOP_OBJECT), + { + "pool_uuid": pool_uuid, + "unlock_method": str(EncryptionMethod.KEYRING), + }, ) if key_spec is None: @@ -537,7 +550,11 @@ def test_duplicate_pool_name(self): # pylint: disable=too-many-locals for pool_uuid in variant_pool_uuids: ((option, _), exit_code, _) = Manager.Methods.UnlockPool( - get_object(TOP_OBJECT), {"pool_uuid": pool_uuid} + get_object(TOP_OBJECT), + { + "pool_uuid": pool_uuid, + "unlock_method": str(EncryptionMethod.KEYRING), + }, ) self.assertEqual(exit_code, StratisdErrors.OK) self.assertEqual(option, True)