diff --git a/0001-Update-packit-with-currently-supported-upgrade-paths.patch b/0001-Update-packit-with-currently-supported-upgrade-paths.patch new file mode 100644 index 0000000..1cf8add --- /dev/null +++ b/0001-Update-packit-with-currently-supported-upgrade-paths.patch @@ -0,0 +1,880 @@ +From a504470d697cc24fb4805c4ab8ff120f16d87b77 Mon Sep 17 00:00:00 2001 +From: Inessa Vasilevskaya +Date: Fri, 1 Mar 2024 14:19:50 +0100 +Subject: [PATCH 1/7] Update packit with currently supported upgrade paths + +That' an adoptation of leapp-repository's packit update and +refactoring introduced in PR1176. + +OAMG-10855 +--- + .packit.yaml | 595 ++++++++++++++++----------------------------------- + 1 file changed, 186 insertions(+), 409 deletions(-) + +diff --git a/.packit.yaml b/.packit.yaml +index 76a8777..bf22cd1 100644 +--- a/.packit.yaml ++++ b/.packit.yaml +@@ -45,18 +45,30 @@ jobs: + - bash -c "grep -m1 '^Version:' packaging/leapp.spec | grep -om1 '[0-9].[0-9.]**'" + + +-- &sanity-79to86 ++# NOTE: to see what envars, targets, .. can be set in tests, see ++# the configuration of tests here: ++# https://gitlab.cee.redhat.com/oamg/leapp-tests/-/blob/main/config.yaml ++# Available only to RH Employees. ++ ++# ###################################################################### # ++# ############################### 7 TO 8 ############################### # ++# ###################################################################### # ++ ++# ###################################################################### # ++# ### Abstract job definitions to make individual tests/jobs smaller ### # ++# ###################################################################### # ++- &sanity-abstract-7to8 + job: tests ++ trigger: ignore + fmf_url: "https://gitlab.cee.redhat.com/oamg/leapp-tests" + fmf_ref: "main" + use_internal_tf: True +- trigger: pull_request + labels: + - sanity + targets: + epel-7-x86_64: + distros: [RHEL-7.9-ZStream] +- identifier: sanity-7.9to8.6 ++ identifier: sanity-abstract-7to8 + tmt_plan: "" + tf_extra_params: + test: +@@ -78,20 +90,16 @@ jobs: + provisioning: + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test +- env: +- SOURCE_RELEASE: "7.9" +- TARGET_RELEASE: "8.6" +- LEAPPDATA_BRANCH: "upstream" + +-- &sanity-79to86-aws +- <<: *sanity-79to86 ++- &sanity-abstract-7to8-aws ++ <<: *sanity-abstract-7to8 + labels: + - sanity + - aws + targets: + epel-7-x86_64: + distros: [RHEL-7.9-rhui] +- identifier: sanity-7.9to8.6-aws-e2e ++ identifier: sanity-abstract-7to8-aws + # NOTE(ivasilev) Unfortunately to use yaml templates we need to rewrite the whole tf_extra_params dict + # to use plan_filter (can't just specify one section test.tmt.plan_filter, need to specify environments.* as well) + tf_extra_params: +@@ -115,57 +123,14 @@ jobs: + post_install_script: "#!/bin/sh\nsudo sed -i s/.*ssh-rsa/ssh-rsa/ /root/.ssh/authorized_keys; yum-config-manager --enable rhel-7-server-rhui-optional-rpms" + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test +- env: +- SOURCE_RELEASE: "7.9" +- TARGET_RELEASE: "8.6" +- RHUI: "aws" +- LEAPPDATA_BRANCH: "upstream" +- LEAPP_NO_RHSM: "1" +- USE_CUSTOM_REPOS: rhui +- +-- &sanity-79to88-aws +- <<: *sanity-79to86-aws +- identifier: sanity-7.9to8.8-aws-e2e +- env: +- SOURCE_RELEASE: "7.9" +- TARGET_RELEASE: "8.8" +- RHUI: "aws" +- LEAPPDATA_BRANCH: "upstream" +- LEAPP_NO_RHSM: "1" +- USE_CUSTOM_REPOS: rhui +- +-- &sanity-79to89-aws +- <<: *sanity-79to86-aws +- identifier: sanity-7.9to8.9-aws-e2e +- env: +- SOURCE_RELEASE: "7.9" +- TARGET_RELEASE: "8.9" +- RHUI: "aws" +- LEAPPDATA_BRANCH: "upstream" +- LEAPP_NO_RHSM: "1" +- USE_CUSTOM_REPOS: rhui +- +-# NOTE(mkluson) RHEL 8.10 content is not publicly available (via RHUI) +-#- &sanity-79to810-aws +-# <<: *sanity-79to86-aws +-# identifier: sanity-7.9to8.10-aws +-# env: +-# SOURCE_RELEASE: "7.9" +-# TARGET_RELEASE: "8.10" +-# RHUI: "aws" +-# LEAPPDATA_BRANCH: "upstream" +-# LEAPP_NO_RHSM: "1" +-# USE_CUSTOM_REPOS: rhui + + # On-demand minimal beaker tests +-- &beaker-minimal-79to86 +- <<: *sanity-79to86 ++- &beaker-minimal-7to8-abstract-ondemand ++ <<: *sanity-abstract-7to8 + manual_trigger: True + labels: + - beaker-minimal +- - beaker-minimal-7.9to8.6 +- - 7.9to8.6 +- identifier: sanity-7.9to8.6-beaker-minimal ++ identifier: beaker-minimal-7to8-abstract-ondemand + tf_extra_params: + test: + tmt: +@@ -188,13 +153,11 @@ jobs: + BusinessUnit: sst_upgrades@leapp_upstream_test + + # On-demand kernel-rt tests +-- &kernel-rt-79to86 +- <<: *beaker-minimal-79to86 ++- &kernel-rt-abstract-7to8-ondemand ++ <<: *beaker-minimal-7to8-abstract-ondemand + labels: + - kernel-rt +- - kernel-rt-7.9to8.6 +- - 7.9to8.6 +- identifier: sanity-7.9to8.6-kernel-rt ++ identifier: sanity-7to8-kernel-rt-abstract-ondemand + tf_extra_params: + test: + tmt: +@@ -216,114 +179,133 @@ jobs: + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test + ++ ++# ###################################################################### # ++# ######################### Individual tests ########################### # ++# ###################################################################### # ++ ++# Tests: 7.9 -> 8.8 ++- &sanity-79to88-aws ++ <<: *sanity-abstract-7to8-aws ++ trigger: pull_request ++ identifier: sanity-7.9to8.8-aws ++ env: ++ SOURCE_RELEASE: "7.9" ++ TARGET_RELEASE: "8.8" ++ RHUI: "aws" ++ LEAPPDATA_BRANCH: "upstream" ++ LEAPP_NO_RHSM: "1" ++ USE_CUSTOM_REPOS: rhui ++ + - &sanity-79to88 +- <<: *sanity-79to86 ++ <<: *sanity-abstract-7to8 ++ trigger: pull_request + identifier: sanity-7.9to8.8 + env: + SOURCE_RELEASE: "7.9" + TARGET_RELEASE: "8.8" + LEAPPDATA_BRANCH: "upstream" + +-# On-demand minimal beaker tests + - &beaker-minimal-79to88 +- <<: *beaker-minimal-79to86 ++ <<: *beaker-minimal-7to8-abstract-ondemand ++ trigger: pull_request + labels: + - beaker-minimal + - beaker-minimal-7.9to8.8 + - 7.9to8.8 +- identifier: sanity-7.9to8.8-beaker-minimal ++ identifier: sanity-7.9to8.8-beaker-minimal-ondemand + env: + SOURCE_RELEASE: "7.9" + TARGET_RELEASE: "8.8" + LEAPPDATA_BRANCH: "upstream" + +-# On-demand kernel-rt tests + - &kernel-rt-79to88 +- <<: *kernel-rt-79to86 ++ <<: *kernel-rt-abstract-7to8-ondemand ++ trigger: pull_request + labels: + - kernel-rt + - kernel-rt-7.9to8.8 + - 7.9to8.8 +- identifier: sanity-7.9to8.8-kernel-rt ++ identifier: sanity-7.9to8.8-kernel-rt-ondemand + env: + SOURCE_RELEASE: "7.9" + TARGET_RELEASE: "8.8" + LEAPPDATA_BRANCH: "upstream" + +-- &sanity-79to89 +- <<: *sanity-79to86 +- identifier: sanity-7.9to8.9 +- env: +- SOURCE_RELEASE: "7.9" +- TARGET_RELEASE: "8.9" +- LEAPPDATA_BRANCH: "upstream" +- +-# On-demand minimal beaker tests +-- &beaker-minimal-79to89 +- <<: *beaker-minimal-79to86 +- labels: +- - beaker-minimal +- - beaker-minimal-7.9to8.9 +- - 7.9to8.9 +- identifier: sanity-7.9to8.9-beaker-minimal +- env: +- SOURCE_RELEASE: "7.9" +- TARGET_RELEASE: "8.9" +- LEAPPDATA_BRANCH: "upstream" +- +-# On-demand kernel-rt tests +-- &kernel-rt-79to89 +- <<: *kernel-rt-79to88 +- labels: +- - kernel-rt +- - kernel-rt-7.9to8.9 +- - 7.9to8.9 +- identifier: sanity-7.9to8.9-kernel-rt +- env: +- SOURCE_RELEASE: "7.9" +- TARGET_RELEASE: "8.9" +- LEAPPDATA_BRANCH: "upstream" +- ++# Tests: 7.9 -> 8.10 + - &sanity-79to810 +- <<: *sanity-79to86 ++ <<: *sanity-abstract-7to8 ++ trigger: pull_request + identifier: sanity-7.9to8.10 + env: + SOURCE_RELEASE: "7.9" + TARGET_RELEASE: "8.10" + LEAPPDATA_BRANCH: "upstream" + +-# On-demand minimal beaker tests ++# NOTE(mkluson) RHEL 8.10 content is not publicly available (via RHUI) ++#- &sanity-79to810-aws ++# <<: *sanity-abstract-7to8-aws ++# trigger: pull_request ++# identifier: sanity-7.9to8.10-aws ++# env: ++# SOURCE_RELEASE: "7.9" ++# TARGET_RELEASE: "8.10" ++# RHUI: "aws" ++# LEAPPDATA_BRANCH: "upstream" ++# LEAPP_NO_RHSM: "1" ++# USE_CUSTOM_REPOS: rhui ++ + - &beaker-minimal-79to810 +- <<: *beaker-minimal-79to86 ++ <<: *beaker-minimal-7to8-abstract-ondemand ++ trigger: pull_request + labels: + - beaker-minimal + - beaker-minimal-7.9to8.10 + - 7.9to8.10 +- identifier: sanity-7.9to8.10-beaker-minimal ++ identifier: sanity-7.9to8.10-beaker-minimal-ondemand + env: + SOURCE_RELEASE: "7.9" + TARGET_RELEASE: "8.10" + LEAPPDATA_BRANCH: "upstream" + +-# On-demand kernel-rt tests + - &kernel-rt-79to810 +- <<: *kernel-rt-79to88 ++ <<: *kernel-rt-abstract-7to8-ondemand ++ trigger: pull_request + labels: + - kernel-rt + - kernel-rt-7.9to8.10 + - 7.9to8.10 +- identifier: sanity-7.9to8.10-kernel-rt ++ identifier: sanity-7.9to8.10-kernel-rt-ondemand + env: + SOURCE_RELEASE: "7.9" + TARGET_RELEASE: "8.10" + LEAPPDATA_BRANCH: "upstream" + +-- &sanity-86to90 +- <<: *sanity-79to86 ++ ++# ###################################################################### # ++# ############################## 8 TO 10 ############################### # ++# ###################################################################### # ++ ++# ###################################################################### # ++# ### Abstract job definitions to make individual tests/jobs smaller ### # ++# ###################################################################### # ++ ++#NOTE(pstodulk) putting default values in abstract jobs as from 8.10, as this ++# is the last RHEL 8 release and all new future tests will start from this ++# one release. ++ ++- &sanity-abstract-8to9 ++ job: tests ++ trigger: ignore ++ fmf_url: "https://gitlab.cee.redhat.com/oamg/leapp-tests" ++ fmf_ref: "main" ++ use_internal_tf: True ++ labels: ++ - sanity + targets: + epel-8-x86_64: +- distros: [RHEL-8.6.0-Nightly] +- identifier: sanity-8.6to9.0 ++ distros: [RHEL-8.10.0-Nightly] ++ identifier: sanity-abstract-8to9 + tf_extra_params: + test: + tmt: +@@ -339,32 +321,25 @@ jobs: + order: 40 + tmt: + context: +- distro: "rhel-8.6" ++ distro: "rhel-8.10" + settings: + provisioning: + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test +- env: +- SOURCE_RELEASE: "8.6" +- TARGET_RELEASE: "9.0" +- RHSM_REPOS_EUS: "eus" +- LEAPPDATA_BRANCH: "upstream" + +-# On-demand minimal beaker tests +-- &beaker-minimal-86to90 +- <<: *beaker-minimal-79to86 ++- &sanity-abstract-8to9-aws ++ <<: *sanity-abstract-8to9 + labels: +- - beaker-minimal +- - beaker-minimal-8.6to9.0 +- - 8.6to9.0 ++ - sanity ++ - aws + targets: + epel-8-x86_64: +- distros: [RHEL-8.6.0-Nightly] +- identifier: sanity-8.6to9.0-beaker-minimal ++ distros: [RHEL-8.10-rhui] ++ identifier: sanity-abstract-8to9-aws + tf_extra_params: + test: + tmt: +- plan_filter: 'tag:partitioning & tag:8to9 & enabled:true' ++ plan_filter: 'tag:upgrade_happy_path & enabled:true' + environments: + - artifacts: + - type: "repository" +@@ -376,29 +351,26 @@ jobs: + order: 40 + tmt: + context: +- distro: "rhel-8.6" ++ distro: "rhel-8.10" + settings: + provisioning: ++ post_install_script: "#!/bin/sh\nsudo sed -i s/.*ssh-rsa/ssh-rsa/ /root/.ssh/authorized_keys" + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test +- env: +- SOURCE_RELEASE: "8.6" +- TARGET_RELEASE: "9.0" +- RHSM_REPOS_EUS: "eus" +- LEAPPDATA_BRANCH: "upstream" + +-# On-demand kernel-rt tests +-- &kernel-rt-86to90 +- <<: *beaker-minimal-86to90 ++- &beaker-minimal-8to9-abstract-ondemand ++ <<: *sanity-abstract-8to9 ++ manual_trigger: True + labels: +- - kernel-rt +- - kernel-rt-8.6to9.0 +- - 8.6to9.0 +- identifier: sanity-8.6to9.0-kernel-rt ++ - beaker-minimal ++ targets: ++ epel-8-x86_64: ++ distros: [RHEL-8.10.0-Nightly] ++ identifier: beaker-minimal-8to9-abstract-ondemand + tf_extra_params: + test: + tmt: +- plan_filter: 'tag:kernel-rt & tag:8to9 & enabled:true' ++ plan_filter: 'tag:partitioning & tag:8to9 & enabled:true' + environments: + - artifacts: + - type: "repository" +@@ -410,60 +382,43 @@ jobs: + order: 40 + tmt: + context: +- distro: "rhel-8.6" ++ distro: "rhel-8.10" + settings: + provisioning: + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test + +-- &sanity-88to92 +- <<: *sanity-86to90 +- targets: +- epel-8-x86_64: +- distros: [RHEL-8.8.0-Nightly] +- identifier: sanity-8.8to9.2 ++ ++- &kernel-rt-abstract-8to9-ondemand ++ <<: *beaker-minimal-8to9-abstract-ondemand ++ labels: ++ - kernel-rt ++ identifier: sanity-8to9-kernel-rt-abstract-ondemand + tf_extra_params: + test: + tmt: +- plan_filter: 'tag:sanity & tag:8to9 & enabled:true' ++ plan_filter: 'tag:kernel-rt & tag:8to9 & enabled:true' + environments: +- - artifacts: +- - type: "repository" +- id: "https://download.copr.fedorainfracloud.org/results/@oamg/leapp/epel-8-x86_64/" +- packages: +- - leapp-repository +- - python3-leapp +- - leapp-upgrade-el8toel9-deps +- order: 40 +- tmt: ++ - tmt: + context: +- distro: "rhel-8.8" ++ distro: "rhel-8.10" + settings: + provisioning: + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test +- env: +- SOURCE_RELEASE: "8.8" +- TARGET_RELEASE: "9.2" +- RHSM_REPOS_EUS: "eus" +- LEAPPDATA_BRANCH: "upstream" +- LEAPP_DEVEL_TARGET_RELEASE: "9.2" + +-# On-demand minimal beaker tests +-- &beaker-minimal-88to92 +- <<: *beaker-minimal-86to90 +- labels: +- - beaker-minimal +- - beaker-minimal-8.8to9.2 +- - 8.6to9.2 ++# Tests: 8.8 -> 9.2 ++- &sanity-88to92 ++ <<: *sanity-abstract-8to9 ++ trigger: pull_request + targets: + epel-8-x86_64: + distros: [RHEL-8.8.0-Nightly] +- identifier: sanity-8.8to9.2-beaker-minimal ++ identifier: sanity-8.8to9.2 + tf_extra_params: + test: + tmt: +- plan_filter: 'tag:partitioning & tag:8to9 & enabled:true' ++ plan_filter: 'tag:sanity & tag:8to9 & enabled:true' + environments: + - artifacts: + - type: "repository" +@@ -478,54 +433,27 @@ jobs: + distro: "rhel-8.8" + settings: + provisioning: +- post_install_script: "#!/bin/sh\nsudo sed -i s/.*ssh-rsa/ssh-rsa/ /root/.ssh/authorized_keys" + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test + env: + SOURCE_RELEASE: "8.8" + TARGET_RELEASE: "9.2" ++ RHSM_REPOS_EUS: "eus" + LEAPPDATA_BRANCH: "upstream" + LEAPP_DEVEL_TARGET_RELEASE: "9.2" + +-# On-demand kernel-rt tests +-- &kernel-rt-88to92 +- <<: *beaker-minimal-88to92 +- labels: +- - kernel-rt +- - kernel-rt-8.8to9.2 +- - 8.8to9.2 +- identifier: sanity-8.8to9.2-kernel-rt +- tf_extra_params: +- test: +- tmt: +- plan_filter: 'tag:kernel-rt & tag:8to9 & enabled:true' +- environments: +- - artifacts: +- - type: "repository" +- id: "https://download.copr.fedorainfracloud.org/results/@oamg/leapp/epel-8-x86_64/" +- packages: +- - leapp-repository +- - python3-leapp +- - leapp-upgrade-el8toel9-deps +- order: 40 +- tmt: +- context: +- distro: "rhel-8.8" +- settings: +- provisioning: +- tags: +- BusinessUnit: sst_upgrades@leapp_upstream_test +- +-- &sanity-89to93 +- <<: *sanity-88to92 ++- &sanity-88to92-aws ++ <<: *sanity-abstract-8to9-aws ++ trigger: pull_request + targets: + epel-8-x86_64: +- distros: [RHEL-8.9.0-Nightly] +- identifier: sanity-8.9to9.3 ++ distros: [RHEL-8.8-rhui] ++ identifier: sanity-8.8to9.2-aws ++ # NOTE(mkluson) Unfortunately to use yaml templates we need to rewrite the whole tf_extra_params dict + tf_extra_params: + test: + tmt: +- plan_filter: 'tag:sanity & tag:8to9 & enabled:true' ++ plan_filter: 'tag:upgrade_happy_path & enabled:true' + environments: + - artifacts: + - type: "repository" +@@ -537,28 +465,32 @@ jobs: + order: 40 + tmt: + context: +- distro: "rhel-8.9" ++ distro: "rhel-8.8" + settings: + provisioning: ++ post_install_script: "#!/bin/sh\nsudo sed -i s/.*ssh-rsa/ssh-rsa/ /root/.ssh/authorized_keys" + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test + env: +- SOURCE_RELEASE: "8.9" +- TARGET_RELEASE: "9.3" ++ SOURCE_RELEASE: "8.8" ++ TARGET_RELEASE: "9.2" ++ RHSM_REPOS: "rhel-8-for-x86_64-appstream-eus-rpms,rhel-8-for-x86_64-baseos-eus-rpms" ++ RHUI: "aws" + LEAPPDATA_BRANCH: "upstream" +- LEAPP_DEVEL_TARGET_RELEASE: "9.3" ++ LEAPP_NO_RHSM: "1" ++ USE_CUSTOM_REPOS: rhui + +-# On-demand minimal beaker tests +-- &beaker-minimal-89to93 +- <<: *beaker-minimal-88to92 ++- &beaker-minimal-88to92 ++ <<: *beaker-minimal-8to9-abstract-ondemand ++ trigger: pull_request + labels: + - beaker-minimal +- - beaker-minimal-8.9to9.3 +- - 8.9to9.3 ++ - beaker-minimal-8.8to9.2 ++ - 8.8to9.2 + targets: + epel-8-x86_64: +- distros: [RHEL-8.9.0-Nightly] +- identifier: sanity-8.9to9.3-beaker-minimal ++ distros: [RHEL-8.8.0-Nightly] ++ identifier: sanity-8.8to9.2-beaker-minimal-ondemand + tf_extra_params: + test: + tmt: +@@ -574,25 +506,29 @@ jobs: + order: 40 + tmt: + context: +- distro: "rhel-8.9" ++ distro: "rhel-8.8" + settings: + provisioning: ++ post_install_script: "#!/bin/sh\nsudo sed -i s/.*ssh-rsa/ssh-rsa/ /root/.ssh/authorized_keys" + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test + env: +- SOURCE_RELEASE: "8.9" +- TARGET_RELEASE: "9.3" ++ SOURCE_RELEASE: "8.8" ++ TARGET_RELEASE: "9.2" + LEAPPDATA_BRANCH: "upstream" +- LEAPP_DEVEL_TARGET_RELEASE: "9.3" ++ LEAPP_DEVEL_TARGET_RELEASE: "9.2" + +-# On-demand kernel-rt tests +-- &kernel-rt-89to93 +- <<: *beaker-minimal-89to93 ++- &kernel-rt-88to92 ++ <<: *kernel-rt-abstract-8to9-ondemand ++ trigger: pull_request + labels: + - kernel-rt +- - kernel-rt-8.9to9.3 +- - 8.9to9.3 +- identifier: sanity-8.9to9.3-kernel-rt ++ - kernel-rt-8.8to9.2 ++ - 8.8to9.2 ++ identifier: sanity-8.8to9.2-kernel-rt-ondemand ++ targets: ++ epel-8-x86_64: ++ distros: [RHEL-8.8.0-Nightly] + tf_extra_params: + test: + tmt: +@@ -608,38 +544,23 @@ jobs: + order: 40 + tmt: + context: +- distro: "rhel-8.9" ++ distro: "rhel-8.8" + settings: + provisioning: + tags: + BusinessUnit: sst_upgrades@leapp_upstream_test ++ env: ++ SOURCE_RELEASE: "8.8" ++ TARGET_RELEASE: "9.2" ++ LEAPPDATA_BRANCH: "upstream" ++ LEAPP_DEVEL_TARGET_RELEASE: "9.2" ++ + ++# Tests: 8.10 -> 9.4 + - &sanity-810to94 +- <<: *sanity-88to92 +- targets: +- epel-8-x86_64: +- distros: [RHEL-8.10.0-Nightly] ++ <<: *sanity-abstract-8to9 ++ trigger: pull_request + identifier: sanity-8.10to9.4 +- tf_extra_params: +- test: +- tmt: +- plan_filter: 'tag:sanity & tag:8to9 & enabled:true' +- environments: +- - artifacts: +- - type: "repository" +- id: "https://download.copr.fedorainfracloud.org/results/@oamg/leapp/epel-8-x86_64/" +- packages: +- - leapp-repository +- - python3-leapp +- - leapp-upgrade-el8toel9-deps +- order: 40 +- tmt: +- context: +- distro: "rhel-8.10" +- settings: +- provisioning: +- tags: +- BusinessUnit: sst_upgrades@leapp_upstream_test + env: + SOURCE_RELEASE: "8.10" + TARGET_RELEASE: "9.4" +@@ -648,35 +569,13 @@ jobs: + + # On-demand minimal beaker tests + - &beaker-minimal-810to94 +- <<: *beaker-minimal-88to92 ++ <<: *beaker-minimal-8to9-abstract-ondemand ++ trigger: pull_request + labels: + - beaker-minimal + - beaker-minimal-8.10to9.4 + - 8.10to9.4 +- targets: +- epel-8-x86_64: +- distros: [RHEL-8.10.0-Nightly] +- identifier: sanity-8.10to9.4-beaker-minimal +- tf_extra_params: +- test: +- tmt: +- plan_filter: 'tag:partitioning & tag:8to9 & enabled:true' +- environments: +- - artifacts: +- - type: "repository" +- id: "https://download.copr.fedorainfracloud.org/results/@oamg/leapp/epel-8-x86_64/" +- packages: +- - leapp-repository +- - python3-leapp +- - leapp-upgrade-el8toel9-deps +- order: 40 +- tmt: +- context: +- distro: "rhel-8.10" +- settings: +- provisioning: +- tags: +- BusinessUnit: sst_upgrades@leapp_upstream_test ++ identifier: sanity-8.10to9.4-beaker-minimal-ondemand + env: + SOURCE_RELEASE: "8.10" + TARGET_RELEASE: "9.4" +@@ -684,137 +583,15 @@ jobs: + + # On-demand kernel-rt tests + - &kernel-rt-810to94 +- <<: *beaker-minimal-810to94 ++ <<: *kernel-rt-abstract-8to9-ondemand ++ trigger: pull_request + labels: + - kernel-rt + - kernel-rt-8.10to9.4 + - 8.10to9.4 +- identifier: sanity-8.10to9.4-kernel-rt +- tf_extra_params: +- test: +- tmt: +- plan_filter: 'tag:kernel-rt & tag:8to9 & enabled:true' +- environments: +- - artifacts: +- - type: "repository" +- id: "https://download.copr.fedorainfracloud.org/results/@oamg/leapp/epel-8-x86_64/" +- packages: +- - leapp-repository +- - python3-leapp +- - leapp-upgrade-el8toel9-deps +- order: 40 +- tmt: +- context: +- distro: "rhel-8.10" +- settings: +- provisioning: +- tags: +- BusinessUnit: sst_upgrades@leapp_upstream_test +- +-- &sanity-86to90-aws +- <<: *sanity-79to86-aws +- targets: +- epel-8-x86_64: +- distros: [RHEL-8.6-rhui] +- identifier: sanity-8.6to9.0-aws-e2e +- tf_extra_params: +- test: +- tmt: +- plan_filter: 'tag:upgrade_happy_path & enabled:true' +- environments: +- - artifacts: +- - type: "repository" +- id: "https://download.copr.fedorainfracloud.org/results/@oamg/leapp/epel-8-x86_64/" +- packages: +- - leapp-repository +- - python3-leapp +- - leapp-upgrade-el8toel9-deps +- order: 40 +- tmt: +- context: +- distro: "rhel-8.6" +- settings: +- provisioning: +- post_install_script: "#!/bin/sh\nsudo sed -i s/.*ssh-rsa/ssh-rsa/ /root/.ssh/authorized_keys" +- tags: +- BusinessUnit: sst_upgrades@leapp_upstream_test +- env: +- SOURCE_RELEASE: "8.6" +- TARGET_RELEASE: "9.0" +- RHSM_REPOS: "rhel-8-for-x86_64-appstream-eus-rpms,rhel-8-for-x86_64-baseos-eus-rpms" +- RHUI: "aws" +- LEAPPDATA_BRANCH: "upstream" +- LEAPP_NO_RHSM: "1" +- USE_CUSTOM_REPOS: rhui +- +-- &sanity-88to92-aws +- <<: *sanity-86to90-aws +- targets: +- epel-8-x86_64: +- distros: [RHEL-8.8-rhui] +- identifier: sanity-8.8to9.2-aws +- tf_extra_params: +- test: +- tmt: +- plan_filter: 'tag:upgrade_happy_path & enabled:true' +- environments: +- - artifacts: +- - type: "repository" +- id: "https://download.copr.fedorainfracloud.org/results/@oamg/leapp/epel-8-x86_64/" +- packages: +- - leapp-repository +- - python3-leapp +- - leapp-upgrade-el8toel9-deps +- order: 40 +- tmt: +- context: +- distro: "rhel-8.8" +- settings: +- provisioning: +- post_install_script: "#!/bin/sh\nsudo sed -i s/.*ssh-rsa/ssh-rsa/ /root/.ssh/authorized_keys" +- tags: +- BusinessUnit: sst_upgrades@leapp_upstream_test ++ identifier: sanity-8.10to9.4-kernel-rt-ondemand + env: +- SOURCE_RELEASE: "8.8" +- TARGET_RELEASE: "9.2" +- RHSM_REPOS: "rhel-8-for-x86_64-appstream-eus-rpms,rhel-8-for-x86_64-baseos-eus-rpms" +- RHUI: "aws" +- LEAPPDATA_BRANCH: "upstream" +- LEAPP_NO_RHSM: "1" +- USE_CUSTOM_REPOS: rhui +- +-- &sanity-89to93-aws +- <<: *sanity-86to90-aws +- targets: +- epel-8-x86_64: +- distros: [RHEL-8.9-rhui] +- identifier: sanity-8.9to9.3-aws +- tf_extra_params: +- test: +- tmt: +- plan_filter: 'tag:upgrade_happy_path & enabled:true' +- environments: +- - artifacts: +- - type: "repository" +- id: "https://download.copr.fedorainfracloud.org/results/@oamg/leapp/epel-8-x86_64/" +- packages: +- - leapp-repository +- - python3-leapp +- - leapp-upgrade-el8toel9-deps +- order: 40 +- tmt: +- context: +- distro: "rhel-8.9" +- settings: +- provisioning: +- post_install_script: "#!/bin/sh\nsudo sed -i s/.*ssh-rsa/ssh-rsa/ /root/.ssh/authorized_keys" +- tags: +- BusinessUnit: sst_upgrades@leapp_upstream_test +- env: +- SOURCE_RELEASE: "8.9" +- TARGET_RELEASE: "9.3" +- RHSM_REPOS: "rhel-8-for-x86_64-appstream-rpms,rhel-8-for-x86_64-baseos-rpms" +- RHUI: "aws" ++ SOURCE_RELEASE: "8.10" ++ TARGET_RELEASE: "9.4" ++ RHSM_REPOS: "rhel-8-for-x86_64-appstream-beta-rpms,rhel-8-for-x86_64-baseos-beta-rpms" + LEAPPDATA_BRANCH: "upstream" +- LEAPP_NO_RHSM: "1" +- USE_CUSTOM_REPOS: rhui +-- +2.42.0 + diff --git a/0002-Update-packit-config-after-tier-redefinition.patch b/0002-Update-packit-config-after-tier-redefinition.patch new file mode 100644 index 0000000..7116269 --- /dev/null +++ b/0002-Update-packit-config-after-tier-redefinition.patch @@ -0,0 +1,47 @@ +From 9050ff0c540f0ceebfdc156b7ca8e28db70d4133 Mon Sep 17 00:00:00 2001 +From: Inessa Vasilevskaya +Date: Thu, 25 Apr 2024 13:32:47 +0200 +Subject: [PATCH 2/7] Update packit config after tier redefinition + +Now for basic sanity test verification in upstream tests +tagged by 'tier0' will be used instead of 'sanity'. + +RHELMISC-3211 +--- + .packit.yaml | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/.packit.yaml b/.packit.yaml +index bf22cd1..4e9c239 100644 +--- a/.packit.yaml ++++ b/.packit.yaml +@@ -73,7 +73,7 @@ jobs: + tf_extra_params: + test: + tmt: +- plan_filter: 'tag:sanity & enabled:true' ++ plan_filter: 'tag:tier0 & enabled:true' + environments: + - artifacts: + - type: "repository" +@@ -309,7 +309,7 @@ jobs: + tf_extra_params: + test: + tmt: +- plan_filter: 'tag:sanity & tag:8to9 & enabled:true' ++ plan_filter: 'tag:tier0 & tag:8to9 & enabled:true' + environments: + - artifacts: + - type: "repository" +@@ -418,7 +418,7 @@ jobs: + tf_extra_params: + test: + tmt: +- plan_filter: 'tag:sanity & tag:8to9 & enabled:true' ++ plan_filter: 'tag:tier0 & tag:8to9 & enabled:true' + environments: + - artifacts: + - type: "repository" +-- +2.42.0 + diff --git a/0003-Reword-the-report-msg-in-the-console-output-to-note-.patch b/0003-Reword-the-report-msg-in-the-console-output-to-note-.patch new file mode 100644 index 0000000..e3265a3 --- /dev/null +++ b/0003-Reword-the-report-msg-in-the-console-output-to-note-.patch @@ -0,0 +1,44 @@ +From b21450ac397ad9aca7aa13a247f889e9dc354344 Mon Sep 17 00:00:00 2001 +From: Petr Stodulka +Date: Fri, 26 Apr 2024 13:46:00 +0200 +Subject: [PATCH 3/7] Reword the report msg in the console output to note + remediations + +Users who do not understand why they should read the generated leapp +report. They are missing that the console output is just a summary +overview of the report itself. + +Rewording the msg little bit to make it explicitely clear that report +contains more details about discovered problems and possible remediation +instructions. + +Also switch order of printed reports paths: txt first. + +Jira: https://issues.redhat.com/browse/RHEL-25406 + https://issues.redhat.com/browse/RHEL-25407 +--- + leapp/utils/output.py | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/leapp/utils/output.py b/leapp/utils/output.py +index 4bca481..4e91718 100644 +--- a/leapp/utils/output.py ++++ b/leapp/utils/output.py +@@ -202,10 +202,12 @@ def report_info(context_id, report_paths, log_paths, answerfile=None, fail=False + _print_reports_summary(reports) + + print( +- '\n{bold}Before continuing consult the full report:{reset}' ++ '\n{bold}Before continuing, review the full report below for details' ++ ' about discovered problems and possible remediation instructions:{reset}' + .format(bold=Color.bold, reset=Color.reset) + ) +- for report_path in report_paths: ++ for report_path in sorted(report_paths, reverse=True): ++ # NOTE: sort hack -> print .txt first + sys.stdout.write(" A report has been generated at {path}\n".format(path=report_path)) + + if answerfile: +-- +2.42.0 + diff --git a/0004-Add-renovate.json.patch b/0004-Add-renovate.json.patch new file mode 100644 index 0000000..d5efc99 --- /dev/null +++ b/0004-Add-renovate.json.patch @@ -0,0 +1,25 @@ +From 210b02021fc03f6ea86cc08fd129ecd3a62c6ee9 Mon Sep 17 00:00:00 2001 +From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> +Date: Wed, 8 May 2024 12:45:08 +0000 +Subject: [PATCH 4/7] Add renovate.json + +--- + renovate.json | 6 ++++++ + 1 file changed, 6 insertions(+) + create mode 100644 renovate.json + +diff --git a/renovate.json b/renovate.json +new file mode 100644 +index 0000000..5db72dd +--- /dev/null ++++ b/renovate.json +@@ -0,0 +1,6 @@ ++{ ++ "$schema": "https://docs.renovatebot.com/renovate-schema.json", ++ "extends": [ ++ "config:recommended" ++ ] ++} +-- +2.42.0 + diff --git a/0005-Update-renovate.json.patch b/0005-Update-renovate.json.patch new file mode 100644 index 0000000..b8961fd --- /dev/null +++ b/0005-Update-renovate.json.patch @@ -0,0 +1,29 @@ +From 6bb9098f2e47c3d52f9129d8fc355f362b9ceae6 Mon Sep 17 00:00:00 2001 +From: Rodolfo Olivieri +Date: Wed, 8 May 2024 10:12:48 -0300 +Subject: [PATCH 5/7] Update renovate.json + +--- + renovate.json | 10 ++++++---- + 1 file changed, 6 insertions(+), 4 deletions(-) + +diff --git a/renovate.json b/renovate.json +index 5db72dd..771ce84 100644 +--- a/renovate.json ++++ b/renovate.json +@@ -1,6 +1,8 @@ + { +- "$schema": "https://docs.renovatebot.com/renovate-schema.json", +- "extends": [ +- "config:recommended" +- ] ++ "extends": [ ++ "config:base" ++ ], ++ "enabledManagers": [ ++ "github-actions" ++ ] + } +-- +2.42.0 + diff --git a/0006-Add-process-lock.patch b/0006-Add-process-lock.patch new file mode 100644 index 0000000..1c95684 --- /dev/null +++ b/0006-Add-process-lock.patch @@ -0,0 +1,175 @@ +From b64c44bfb741e17650c7c0d65f25fc4ef67fdf19 Mon Sep 17 00:00:00 2001 +From: David Kubek +Date: Thu, 22 Feb 2024 12:51:04 +0100 +Subject: [PATCH 6/7] Add process lock + +This commit addresses the potential risk of running multiple instances +of Leapp simultaneously on a single system. It implements a simple lock +mechanism to prevent concurrent executions on a single system using a +simple BSD lock (`flock(2)`). + +Lock is acquired at the start of the execution and a PID number is +stored in lockfile. The PID in lockfile currently has purely +informational character. +--- + leapp/cli/__init__.py | 10 ++++-- + leapp/config.py | 3 ++ + leapp/exceptions.py | 4 +++ + leapp/utils/lock.py | 83 +++++++++++++++++++++++++++++++++++++++++++ + 4 files changed, 97 insertions(+), 3 deletions(-) + create mode 100644 leapp/utils/lock.py + +diff --git a/leapp/cli/__init__.py b/leapp/cli/__init__.py +index fe997d4..ab16198 100644 +--- a/leapp/cli/__init__.py ++++ b/leapp/cli/__init__.py +@@ -2,12 +2,12 @@ import os + import pkgutil + import socket + import sys +-import textwrap + + from leapp import VERSION + from leapp.cli import commands +-from leapp.exceptions import UnknownCommandError ++from leapp.exceptions import UnknownCommandError, ProcessLockError + from leapp.utils.clicmd import command ++from leapp.utils.lock import leapp_lock + + + @command('') +@@ -42,7 +42,8 @@ def main(): + os.environ['LEAPP_HOSTNAME'] = socket.getfqdn() + _load_commands(cli.command) + try: +- cli.command.execute('leapp version {}'.format(VERSION)) ++ with leapp_lock(): ++ cli.command.execute('leapp version {}'.format(VERSION)) + except UnknownCommandError as e: + bad_cmd = ( + "Command \"{CMD}\" is unknown.\nMost likely there is a typo in the command or particular " +@@ -54,3 +55,6 @@ def main(): + bad_cmd = "No such argument {CMD}" + print(bad_cmd.format(CMD=e.requested)) + sys.exit(1) ++ except ProcessLockError as e: ++ sys.stderr.write('{}\nAborting.\n'.format(e.message)) ++ sys.exit(1) +diff --git a/leapp/config.py b/leapp/config.py +index 18c357d..2487e0f 100644 +--- a/leapp/config.py ++++ b/leapp/config.py +@@ -40,6 +40,9 @@ _CONFIG_DEFAULTS = { + 'dir': '/var/log/leapp/', + 'files': ','.join(_FILES_TO_ARCHIVE), + }, ++ 'lock': { ++ 'path': '/var/run/leapp.pid' ++ }, + 'logs': { + 'dir': '/var/log/leapp/', + 'files': ','.join(_LOGS), +diff --git a/leapp/exceptions.py b/leapp/exceptions.py +index 43c5905..9463a5c 100644 +--- a/leapp/exceptions.py ++++ b/leapp/exceptions.py +@@ -148,3 +148,7 @@ class RequestStopAfterPhase(LeappError): + + def __init__(self): + super(RequestStopAfterPhase, self).__init__('Stop after phase has been requested.') ++ ++ ++class ProcessLockError(LeappError): ++ """ This exception is used to represent an error within the process locking mechanism. """ +diff --git a/leapp/utils/lock.py b/leapp/utils/lock.py +new file mode 100644 +index 0000000..33e825c +--- /dev/null ++++ b/leapp/utils/lock.py +@@ -0,0 +1,83 @@ ++import os ++import fcntl ++import logging ++ ++from leapp.config import get_config ++from leapp.exceptions import ProcessLockError ++ ++ ++def leapp_lock(lockfile=None): ++ return ProcessLock(lockfile=lockfile) ++ ++ ++def _acquire_lock(fd): ++ try: ++ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) ++ return True ++ except OSError: ++ return False ++ ++ ++def _clear_lock(fd): ++ os.lseek(fd, 0, os.SEEK_SET) ++ os.ftruncate(fd, 0) ++ ++ ++def _read_pid(fd): ++ return os.read(fd, 20) ++ ++ ++def _write_pid(fd, pid): ++ _clear_lock(fd) ++ os.write(fd, str(pid).encode('utf-8')) ++ ++ ++class ProcessLock(object): ++ ++ def __init__(self, lockfile=None): ++ self.log = logging.getLogger('leapp.utils.lock') ++ self.lockfile = lockfile if lockfile else get_config().get('lock', 'path') ++ ++ self.fd = None ++ ++ def _get_pid_from_lockfile(self): ++ running_pid = _read_pid(self.fd) ++ self.log.debug("_get_pid_from_lockfile: running_pid=%s", running_pid) ++ running_pid = int(running_pid) ++ ++ return running_pid ++ ++ def _try_lock(self, pid): ++ if not _acquire_lock(self.fd): ++ try: ++ running_pid = self._get_pid_from_lockfile() ++ except ValueError: ++ process_msg = '' ++ else: ++ process_msg = ' by process with PID {}'.format(running_pid) ++ ++ msg = ( ++ 'Leapp is currently locked{} and cannot be started.\n' ++ 'Please ensure no other instance of leapp is running and then delete the lockfile at {} and try again.' ++ ).format(process_msg, self.lockfile) ++ raise ProcessLockError(msg) ++ ++ try: ++ _write_pid(self.fd, pid) ++ except OSError: ++ raise ProcessLockError('Could not write PID to lockfile.') ++ ++ def __enter__(self): ++ my_pid = os.getpid() ++ ++ self.fd = os.open(self.lockfile, os.O_CREAT | os.O_RDWR, 0o600) ++ try: ++ self._try_lock(my_pid) ++ except ProcessLockError: ++ os.close(self.fd) ++ raise ++ ++ def __exit__(self, *exc_args): ++ _clear_lock(self.fd) ++ os.close(self.fd) ++ os.unlink(self.lockfile) +-- +2.42.0 + diff --git a/0007-Extend-information-from-leapp-saved-to-leappdb-847.patch b/0007-Extend-information-from-leapp-saved-to-leappdb-847.patch new file mode 100644 index 0000000..6517603 --- /dev/null +++ b/0007-Extend-information-from-leapp-saved-to-leappdb-847.patch @@ -0,0 +1,1335 @@ +From 91b309ac8c4cf6a709694144cb1e451ff1fa72f7 Mon Sep 17 00:00:00 2001 +From: David Kubek +Date: Mon, 13 May 2024 18:07:26 +0200 +Subject: [PATCH 7/7] Extend information from leapp saved to leappdb (#847) + +This commit combines multiple additions to the leapp database. + +First addition is tracking of entity metadata. The `Metadata` model +stores the metadata of entities such as `Actor` or `Workflow`. This data +is stored in a new table `metadata` of the `leapp.db` file. + + 1. metadata of *discovered* actors. + + For an actor, the metadata stored contain: + + `class_name` - the name of the actor class + `name` - the name given to the actor + `description` - the actor's description + `phase` - phase of execution of the actor + `tags` - names of any tags associated with an actor + `consumes` - list of all messages the actor consumes + `produces` - list of all messages the actor produces + `path` - the path to the actor source file + + 2. workflow metadata. + + For a workflow, the metadata stored contain: + + `name` - name of the workflow + `short_name` - short name of the workflow + `tag` - workflow tag + `description` - workflow description + `phases` - all phases associated with the workflow + +Next addition is tracking of dialog question. Previously leapp was not +able to detect the actual question asked from the user as it could be +generated dynamically when actor is called and depend on the +configuration of the user's system. + +Last addition includes storing the actor exit status. Exit status is now +saved as an audit event `actor-exit-status`. Exit status 0 represents +successful execution or `StopActorExecution`/`StopActorExecutionError`, +while 1 indicates an unexpected and unhandled exception. + +These changes collectively improve the metadata handling capabilities +of, ensuring accurate storage and retrieval of essential information for +various entities. + +Jira: OAMG-8402 +--- + leapp/actors/__init__.py | 13 +- + leapp/dialogs/dialog.py | 1 + + leapp/messaging/answerstore.py | 3 + + leapp/utils/audit/__init__.py | 199 +++++++++++++++- + leapp/utils/audit/contextclone.py | 22 ++ + leapp/workflows/__init__.py | 22 +- + res/schema/audit-layout.sql | 26 +- + .../0002-add-metadata-dialog-tables.sql | 27 +++ + tests/data/leappdb-tests/.leapp/info | 1 + + tests/data/leappdb-tests/.leapp/leapp.conf | 6 + + .../actors/configprovider/actor.py | 17 ++ + .../leappdb-tests/actors/dialogactor/actor.py | 36 +++ + .../actors/exitstatusactor/actor.py | 31 +++ + .../leappdb-tests/libraries/test_helper.py | 7 + + .../leappdb-tests/models/unittestconfig.py | 7 + + tests/data/leappdb-tests/tags/firstphase.py | 5 + + tests/data/leappdb-tests/tags/secondphase.py | 5 + + .../leappdb-tests/tags/unittestworkflow.py | 5 + + tests/data/leappdb-tests/topics/config.py | 5 + + .../data/leappdb-tests/workflows/unit_test.py | 37 +++ + tests/scripts/test_actor_api.py | 6 + + tests/scripts/test_dialog_db.py | 186 +++++++++++++++ + tests/scripts/test_exit_status.py | 68 ++++++ + tests/scripts/test_metadata.py | 223 ++++++++++++++++++ + 24 files changed, 949 insertions(+), 9 deletions(-) + create mode 100644 res/schema/migrations/0002-add-metadata-dialog-tables.sql + create mode 100644 tests/data/leappdb-tests/.leapp/info + create mode 100644 tests/data/leappdb-tests/.leapp/leapp.conf + create mode 100644 tests/data/leappdb-tests/actors/configprovider/actor.py + create mode 100644 tests/data/leappdb-tests/actors/dialogactor/actor.py + create mode 100644 tests/data/leappdb-tests/actors/exitstatusactor/actor.py + create mode 100644 tests/data/leappdb-tests/libraries/test_helper.py + create mode 100644 tests/data/leappdb-tests/models/unittestconfig.py + create mode 100644 tests/data/leappdb-tests/tags/firstphase.py + create mode 100644 tests/data/leappdb-tests/tags/secondphase.py + create mode 100644 tests/data/leappdb-tests/tags/unittestworkflow.py + create mode 100644 tests/data/leappdb-tests/topics/config.py + create mode 100644 tests/data/leappdb-tests/workflows/unit_test.py + create mode 100644 tests/scripts/test_dialog_db.py + create mode 100644 tests/scripts/test_exit_status.py + create mode 100644 tests/scripts/test_metadata.py + +diff --git a/leapp/actors/__init__.py b/leapp/actors/__init__.py +index 7ae18ea..9d83bf1 100644 +--- a/leapp/actors/__init__.py ++++ b/leapp/actors/__init__.py +@@ -10,6 +10,7 @@ from leapp.models import DialogModel, Model + from leapp.models.error_severity import ErrorSeverity + from leapp.tags import Tag + from leapp.utils import get_api_models, path ++from leapp.utils.audit import store_dialog + from leapp.utils.i18n import install_translation_for_actor + from leapp.utils.meta import get_flattened_subclasses + from leapp.workflows.api import WorkflowAPI +@@ -122,12 +123,17 @@ class Actor(object): + :return: dictionary with the requested answers, None if not a defined dialog + """ + self._messaging.register_dialog(dialog, self) ++ answer = None + if dialog in type(self).dialogs: + if self.skip_dialogs: + # non-interactive mode of operation +- return self._messaging.get_answers(dialog) +- return self._messaging.request_answers(dialog) +- return None ++ answer = self._messaging.get_answers(dialog) ++ else: ++ answer = self._messaging.request_answers(dialog) ++ ++ store_dialog(dialog, answer) ++ ++ return answer + + def show_message(self, message): + """ +@@ -285,6 +291,7 @@ class Actor(object): + def run(self, *args): + """ Runs the actor calling the method :py:func:`process`. """ + os.environ['LEAPP_CURRENT_ACTOR'] = self.name ++ + try: + self.process(*args) + except StopActorExecution: +diff --git a/leapp/dialogs/dialog.py b/leapp/dialogs/dialog.py +index 3ead810..320be1b 100644 +--- a/leapp/dialogs/dialog.py ++++ b/leapp/dialogs/dialog.py +@@ -114,4 +114,5 @@ class Dialog(object): + self._store = store + renderer.render(self) + self._store = None ++ + return store.get(self.scope, {}) +diff --git a/leapp/messaging/answerstore.py b/leapp/messaging/answerstore.py +index 3e55e8a..b2c707d 100644 +--- a/leapp/messaging/answerstore.py ++++ b/leapp/messaging/answerstore.py +@@ -117,6 +117,9 @@ class AnswerStore(object): + # NOTE(ivasilev) self.storage.get() will return a DictProxy. To avoid TypeError during later + # JSON serialization a copy() should be invoked to get a shallow copy of data + answer = self._storage.get(scope, fallback).copy() ++ ++ # NOTE(dkubek): It is possible that we do not need to save the 'answer' ++ # here as it is being stored with dialog question right after query + create_audit_entry('dialog-answer', {'scope': scope, 'fallback': fallback, 'answer': answer}) + return answer + +diff --git a/leapp/utils/audit/__init__.py b/leapp/utils/audit/__init__.py +index 6b00413..16db107 100644 +--- a/leapp/utils/audit/__init__.py ++++ b/leapp/utils/audit/__init__.py +@@ -3,6 +3,7 @@ import datetime + import json + import os + import sqlite3 ++import hashlib + + from leapp.config import get_config + from leapp.compat import string_types +@@ -221,6 +222,72 @@ class DataSource(Host): + self._data_source_id = cursor.fetchone()[0] + + ++class Metadata(Storable): ++ """ ++ Metadata of an Entity ++ """ ++ ++ def __init__(self, metadata=None, hash_id=None): ++ """ ++ :param metadata: Entity metadata ++ :type metadata: str ++ :param hash_id: SHA256 hash in hexadecimal representation of data ++ :type hash_id: str ++ """ ++ super(Metadata, self).__init__() ++ self.metadata = metadata ++ self.hash_id = hash_id ++ ++ def do_store(self, connection): ++ super(Metadata, self).do_store(connection) ++ connection.execute('INSERT OR IGNORE INTO metadata (hash, metadata) VALUES(?, ?)', ++ (self.hash_id, self.metadata)) ++ ++ ++class Entity(Host): ++ """ ++ Leapp framework entity (e.g. actor, workflow) ++ """ ++ ++ def __init__(self, context=None, hostname=None, kind=None, metadata=None, name=None): ++ """ ++ :param context: The execution context ++ :type context: str ++ :param hostname: Hostname of the system that produced the entry ++ :type hostname: str ++ :param kind: Kind of the entity for which metadata is stored ++ :type kind: str ++ :param metadata: Entity metadata ++ :type metadata: :py:class:`leapp.utils.audit.Metadata` ++ :param name: Name of the entity ++ :type name: str ++ """ ++ super(Entity, self).__init__(context=context, hostname=hostname) ++ self.kind = kind ++ self.name = name ++ self.metadata = metadata ++ self._entity_id = None ++ ++ @property ++ def entity_id(self): ++ """ ++ Returns the id of the entry, which is only set when already stored. ++ :return: Integer id or None ++ """ ++ return self._entity_id ++ ++ def do_store(self, connection): ++ super(Entity, self).do_store(connection) ++ self.metadata.do_store(connection) ++ connection.execute( ++ 'INSERT OR IGNORE INTO entity (context, kind, name, metadata_hash) VALUES(?, ?, ?, ?)', ++ (self.context, self.kind, self.name, self.metadata.hash_id)) ++ cursor = connection.execute( ++ 'SELECT id FROM entity WHERE context = ? AND kind = ? AND name = ?', ++ (self.context, self.kind, self.name)) ++ self._entity_id = cursor.fetchone()[0] ++ ++ + class Message(DataSource): + def __init__(self, stamp=None, msg_type=None, topic=None, data=None, actor=None, phase=None, + hostname=None, context=None): +@@ -267,6 +334,47 @@ class Message(DataSource): + self._message_id = cursor.lastrowid + + ++class Dialog(DataSource): ++ """ ++ Stores information about dialog questions and their answers ++ """ ++ ++ def __init__(self, scope=None, data=None, actor=None, phase=None, hostname=None, context=None): ++ """ ++ :param scope: Dialog scope ++ :type scope: str ++ :param data: Payload data ++ :type data: dict ++ :param actor: Name of the actor that triggered the entry ++ :type actor: str ++ :param phase: In which phase of the workflow execution the dialog was triggered ++ :type phase: str ++ :param hostname: Hostname of the system that produced the message ++ :type hostname: str ++ :param context: The execution context ++ :type context: str ++ """ ++ super(Dialog, self).__init__(actor=actor, phase=phase, hostname=hostname, context=context) ++ self.scope = scope or '' ++ self.data = data ++ self._dialog_id = None ++ ++ @property ++ def dialog_id(self): ++ """ ++ Returns the id of the entry, which is only set when already stored. ++ :return: Integer id or None ++ """ ++ return self._dialog_id ++ ++ def do_store(self, connection): ++ super(Dialog, self).do_store(connection) ++ cursor = connection.execute( ++ 'INSERT OR IGNORE INTO dialog (context, scope, data, data_source_id) VALUES(?, ?, ?, ?)', ++ (self.context, self.scope, json.dumps(self.data), self.data_source_id)) ++ self._dialog_id = cursor.lastrowid ++ ++ + def create_audit_entry(event, data, message=None): + """ + Create an audit entry +@@ -291,10 +399,10 @@ def get_audit_entry(event, context): + """ + Retrieve audit entries stored in the database for the given context + +- :param context: The execution context +- :type context: str + :param event: Event type identifier + :type event: str ++ :param context: The execution context ++ :type context: str + :return: list of dicts with id, time stamp, actor and phase fields + """ + with get_connection(None) as conn: +@@ -470,3 +578,90 @@ def get_checkpoints(context): + ''', (context, _AUDIT_CHECKPOINT_EVENT)) + cursor.row_factory = dict_factory + return cursor.fetchall() ++ ++ ++def store_dialog(dialog, answer): ++ """ ++ Store ``dialog`` with accompanying ``answer``. ++ ++ :param dialog: instance of a workflow to store. ++ :type dialog: :py:class:`leapp.dialogs.Dialog` ++ :param answer: Answer to for each component of the dialog ++ :type answer: dict ++ """ ++ ++ component_keys = ('key', 'label', 'description', 'default', 'value', 'reason') ++ dialog_keys = ('title', 'reason') # + 'components' ++ ++ tmp = dialog.serialize() ++ data = { ++ 'components': [dict((key, component[key]) for key in component_keys) for component in tmp['components']], ++ ++ # NOTE(dkubek): Storing answer here is redundant as it is already ++ # being stored in audit when we query from the answerstore, however, ++ # this keeps the information coupled with the question more closely ++ 'answer': answer ++ } ++ data.update((key, tmp[key]) for key in dialog_keys) ++ ++ e = Dialog( ++ scope=dialog.scope, ++ data=data, ++ context=os.environ['LEAPP_EXECUTION_ID'], ++ actor=os.environ['LEAPP_CURRENT_ACTOR'], ++ phase=os.environ['LEAPP_CURRENT_PHASE'], ++ hostname=os.environ['LEAPP_HOSTNAME'], ++ ) ++ e.store() ++ ++ return e ++ ++ ++def store_workflow_metadata(workflow): ++ """ ++ Store the metadata of the given ``workflow`` into the database. ++ ++ :param workflow: Workflow to store. ++ :type workflow: :py:class:`leapp.workflows.Workflow` ++ """ ++ ++ metadata = json.dumps(type(workflow).serialize(), sort_keys=True) ++ metadata_hash_id = hashlib.sha256(metadata.encode('utf-8')).hexdigest() ++ ++ md = Metadata(metadata=metadata, hash_id=metadata_hash_id) ++ ent = Entity(kind='workflow', ++ name=workflow.name, ++ context=os.environ['LEAPP_EXECUTION_ID'], ++ hostname=os.environ['LEAPP_HOSTNAME'], ++ metadata=md) ++ ent.store() ++ ++ ++def store_actor_metadata(actor_definition, phase): ++ """ ++ Store the metadata of the given actor given as an ``actor_definition`` ++ object into the database. ++ ++ :param actor_definition: Actor to store ++ :type actor_definition: :py:class:`leapp.repository.actor_definition.ActorDefinition` ++ """ ++ ++ _metadata = dict(actor_definition.discover()) ++ _metadata.update({ ++ 'consumes': sorted(model.__name__ for model in _metadata.get('consumes', ())), ++ 'produces': sorted(model.__name__ for model in _metadata.get('produces', ())), ++ 'tags': sorted(tag.__name__ for tag in _metadata.get('tags', ())), ++ }) ++ _metadata['phase'] = phase ++ ++ actor_metadata_fields = ('class_name', 'name', 'description', 'phase', 'tags', 'consumes', 'produces', 'path') ++ metadata = json.dumps({field: _metadata[field] for field in actor_metadata_fields}, sort_keys=True) ++ metadata_hash_id = hashlib.sha256(metadata.encode('utf-8')).hexdigest() ++ ++ md = Metadata(metadata=metadata, hash_id=metadata_hash_id) ++ ent = Entity(kind='actor', ++ name=actor_definition.name, ++ context=os.environ['LEAPP_EXECUTION_ID'], ++ hostname=os.environ['LEAPP_HOSTNAME'], ++ metadata=md) ++ ent.store() +diff --git a/leapp/utils/audit/contextclone.py b/leapp/utils/audit/contextclone.py +index 2e28b70..1c80f2c 100644 +--- a/leapp/utils/audit/contextclone.py ++++ b/leapp/utils/audit/contextclone.py +@@ -70,6 +70,26 @@ def _dup_audit(db, message, data_source, newcontext, oldcontext): + return lookup + + ++def _dup_metadata(db, newcontext, oldcontext): ++ for row in _fetch_table_for_context(db, 'metadata', oldcontext): ++ # id context kind name metadata ++ row_id, kind, name, metadata = _row_tuple(row, 'id', 'kind', 'name', 'metadata') ++ ++ db.execute( ++ 'INSERT INTO metadata (context, kind, name, metadata) VALUES(?, ?, ?, ?)', ++ (newcontext, kind, name, metadata)) ++ ++ ++def _dup_dialog(db, data_source, newcontext, oldcontext): ++ for row in _fetch_table_for_context(db, 'dialog', oldcontext): ++ # id context scope data data_source_id ++ row_id, scope, data, data_source_id = _row_tuple(row, 'id', 'scope', 'data', 'data_source_id') ++ ++ db.execute( ++ 'INSERT INTO dialog (context, scope, data, data_source_id) VALUES(?, ?, ?, ?)', ++ (newcontext, scope, data, data_source[data_source_id])) ++ ++ + def clone_context(oldcontext, newcontext, use_db=None): + # Enter transaction - In case of any exception automatic rollback is issued + # and it is automatically committed if there was no exception +@@ -82,3 +102,5 @@ def clone_context(oldcontext, newcontext, use_db=None): + message = _dup_message(db=db, data_source=data_source, newcontext=newcontext, oldcontext=oldcontext) + # Last clone message entries and use the lookup table generated by the data_source and message duplications + _dup_audit(db=db, data_source=data_source, message=message, newcontext=newcontext, oldcontext=oldcontext) ++ _dup_metadata(db=db, oldcontext=oldcontext, newcontext=newcontext) ++ _dup_dialog(db=db, data_source=data_source, oldcontext=oldcontext, newcontext=newcontext) +diff --git a/leapp/workflows/__init__.py b/leapp/workflows/__init__.py +index 7f01e0d..1b6fc98 100644 +--- a/leapp/workflows/__init__.py ++++ b/leapp/workflows/__init__.py +@@ -11,7 +11,7 @@ from leapp.messaging.inprocess import InProcessMessaging + from leapp.messaging.commands import SkipPhasesUntilCommand + from leapp.tags import ExperimentalTag + from leapp.utils import reboot_system +-from leapp.utils.audit import checkpoint, get_errors ++from leapp.utils.audit import checkpoint, get_errors, create_audit_entry, store_workflow_metadata, store_actor_metadata + from leapp.utils.meta import with_metaclass, get_flattened_subclasses + from leapp.utils.output import display_status_current_phase, display_status_current_actor + from leapp.workflows.phases import Phase +@@ -165,7 +165,7 @@ class Workflow(with_metaclass(WorkflowMeta)): + self.description = self.description or type(self).__doc__ + + for phase in self.phases: +- phase.filter.tags += (self.tag,) ++ phase.filter.tags += (self.tag,) if self.tag not in phase.filter.tags else () + self._phase_actors.append(( + phase, + # filters all actors with the give tags +@@ -279,6 +279,8 @@ class Workflow(with_metaclass(WorkflowMeta)): + self.log.info('Starting workflow execution: {name} - ID: {id}'.format( + name=self.name, id=os.environ['LEAPP_EXECUTION_ID'])) + ++ store_workflow_metadata(self) ++ + skip_phases_until = (skip_phases_until or '').lower() + needle_phase = until_phase or '' + needle_stage = None +@@ -295,6 +297,12 @@ class Workflow(with_metaclass(WorkflowMeta)): + if phase and not self.is_valid_phase(phase): + raise CommandError('Phase {phase} does not exist in the workflow'.format(phase=phase)) + ++ # Save metadata of all discovered actors ++ for phase in self._phase_actors: ++ for stage in phase[1:]: ++ for actor in stage.actors: ++ store_actor_metadata(actor, phase[0].name) ++ + self._stop_after_phase_requested = False + for phase in self._phase_actors: + os.environ['LEAPP_CURRENT_PHASE'] = phase[0].name +@@ -332,10 +340,12 @@ class Workflow(with_metaclass(WorkflowMeta)): + display_status_current_actor(actor, designation=designation) + current_logger.info("Executing actor {actor} {designation}".format(designation=designation, + actor=actor.name)) ++ + messaging = InProcessMessaging(config_model=config_model, answer_store=self._answer_store) + messaging.load(actor.consumes) + instance = actor(logger=current_logger, messaging=messaging, + config_model=config_model, skip_dialogs=skip_dialogs) ++ + try: + instance.run() + except BaseException as exc: +@@ -346,6 +356,14 @@ class Workflow(with_metaclass(WorkflowMeta)): + current_logger.error('Actor {actor} has crashed: {trace}'.format(actor=actor.name, + trace=exc.exception_info)) + raise ++ finally: ++ # Set and unset the enviromental variable so that audit ++ # associates the entry with the correct data source ++ os.environ['LEAPP_CURRENT_ACTOR'] = actor.name ++ create_audit_entry( ++ event='actor-exit-status', ++ data={'exit_status': 1 if self._unhandled_exception else 0}) ++ os.environ.pop('LEAPP_CURRENT_ACTOR') + + self._stop_after_phase_requested = messaging.stop_after_phase or self._stop_after_phase_requested + +diff --git a/res/schema/audit-layout.sql b/res/schema/audit-layout.sql +index dd88a45..d567ce4 100644 +--- a/res/schema/audit-layout.sql ++++ b/res/schema/audit-layout.sql +@@ -1,6 +1,6 @@ + BEGIN; + +-PRAGMA user_version = 2; ++PRAGMA user_version = 3; + + CREATE TABLE IF NOT EXISTS execution ( + id INTEGER PRIMARY KEY NOT NULL, +@@ -42,6 +42,28 @@ CREATE TABLE IF NOT EXISTS message ( + message_data_hash VARCHAR(64) NOT NULL REFERENCES message_data (hash) + ); + ++CREATE TABLE IF NOT EXISTS metadata ( ++ hash VARCHAR(64) PRIMARY KEY NOT NULL, ++ metadata TEXT ++); ++ ++CREATE TABLE IF NOT EXISTS entity ( ++ id INTEGER PRIMARY KEY NOT NULL, ++ context VARCHAR(36) NOT NULL REFERENCES execution (context), ++ kind VARCHAR(256) NOT NULL DEFAULT '', ++ name VARCHAR(1024) NOT NULL DEFAULT '', ++ metadata_hash VARCHAR(64) NOT NULL REFERENCES metadata (hash), ++ UNIQUE (context, kind, name) ++); ++ ++CREATE TABLE IF NOT EXISTS dialog ( ++ id INTEGER PRIMARY KEY NOT NULL, ++ context VARCHAR(36) NOT NULL REFERENCES execution (context), ++ scope VARCHAR(1024) NOT NULL DEFAULT '', ++ data TEXT DEFAULT NULL, ++ data_source_id INTEGER NOT NULL REFERENCES data_source (id) ++); ++ + + CREATE TABLE IF NOT EXISTS audit ( + id INTEGER PRIMARY KEY NOT NULL, +@@ -74,4 +96,4 @@ CREATE VIEW IF NOT EXISTS messages_data AS + host ON host.id = data_source.host_id + ; + +-COMMIT; +\ No newline at end of file ++COMMIT; +diff --git a/res/schema/migrations/0002-add-metadata-dialog-tables.sql b/res/schema/migrations/0002-add-metadata-dialog-tables.sql +new file mode 100644 +index 0000000..476a0c3 +--- /dev/null ++++ b/res/schema/migrations/0002-add-metadata-dialog-tables.sql +@@ -0,0 +1,27 @@ ++BEGIN; ++ ++CREATE TABLE IF NOT EXISTS metadata ( ++ hash VARCHAR(64) PRIMARY KEY NOT NULL, ++ metadata TEXT ++); ++ ++CREATE TABLE IF NOT EXISTS entity ( ++ id INTEGER PRIMARY KEY NOT NULL, ++ context VARCHAR(36) NOT NULL REFERENCES execution (context), ++ kind VARCHAR(256) NOT NULL DEFAULT '', ++ name VARCHAR(1024) NOT NULL DEFAULT '', ++ metadata_hash VARCHAR(64) NOT NULL REFERENCES metadata (hash), ++ UNIQUE (context, kind, name) ++); ++ ++CREATE TABLE IF NOT EXISTS dialog ( ++ id INTEGER PRIMARY KEY NOT NULL, ++ context VARCHAR(36) NOT NULL REFERENCES execution (context), ++ scope VARCHAR(1024) NOT NULL DEFAULT '', ++ data TEXT DEFAULT NULL, ++ data_source_id INTEGER NOT NULL REFERENCES data_source (id) ++); ++ ++PRAGMA user_version = 3; ++ ++COMMIT; +diff --git a/tests/data/leappdb-tests/.leapp/info b/tests/data/leappdb-tests/.leapp/info +new file mode 100644 +index 0000000..2c42aa6 +--- /dev/null ++++ b/tests/data/leappdb-tests/.leapp/info +@@ -0,0 +1 @@ ++{"name": "workflow-tests", "id": "07005707-67bc-46e5-9732-a10fb13d4e7d"} +\ No newline at end of file +diff --git a/tests/data/leappdb-tests/.leapp/leapp.conf b/tests/data/leappdb-tests/.leapp/leapp.conf +new file mode 100644 +index 0000000..b459134 +--- /dev/null ++++ b/tests/data/leappdb-tests/.leapp/leapp.conf +@@ -0,0 +1,6 @@ ++ ++[repositories] ++repo_path=${repository:root_dir} ++ ++[database] ++path=${repository:state_dir}/leapp.db +diff --git a/tests/data/leappdb-tests/actors/configprovider/actor.py b/tests/data/leappdb-tests/actors/configprovider/actor.py +new file mode 100644 +index 0000000..985de52 +--- /dev/null ++++ b/tests/data/leappdb-tests/actors/configprovider/actor.py +@@ -0,0 +1,17 @@ ++from leapp.actors import Actor ++from leapp.models import UnitTestConfig ++from leapp.tags import UnitTestWorkflowTag ++ ++ ++class ConfigProvider(Actor): ++ """ ++ No documentation has been provided for the config_provider actor. ++ """ ++ ++ name = 'config_provider' ++ consumes = () ++ produces = (UnitTestConfig,) ++ tags = (UnitTestWorkflowTag,) ++ ++ def process(self): ++ self.produce(UnitTestConfig()) +diff --git a/tests/data/leappdb-tests/actors/dialogactor/actor.py b/tests/data/leappdb-tests/actors/dialogactor/actor.py +new file mode 100644 +index 0000000..f9d5f77 +--- /dev/null ++++ b/tests/data/leappdb-tests/actors/dialogactor/actor.py +@@ -0,0 +1,36 @@ ++from leapp.actors import Actor ++from leapp.tags import SecondPhaseTag, UnitTestWorkflowTag ++from leapp.dialogs import Dialog ++from leapp.dialogs.components import BooleanComponent, ChoiceComponent, NumberComponent, TextComponent ++ ++ ++class DialogActor(Actor): ++ name = 'dialog_actor' ++ description = 'No description has been provided for the dialog_actor actor.' ++ consumes = () ++ produces = () ++ tags = (SecondPhaseTag, UnitTestWorkflowTag) ++ dialogs = (Dialog( ++ scope='unique_dialog_scope', ++ reason='Confirmation', ++ components=( ++ TextComponent( ++ key='text', ++ label='text', ++ description='a text value is needed', ++ ), ++ BooleanComponent(key='bool', label='bool', description='a boolean value is needed'), ++ NumberComponent(key='num', label='num', description='a numeric value is needed'), ++ ChoiceComponent( ++ key='choice', ++ label='choice', ++ description='need to choose one of these choices', ++ choices=('One', 'Two', 'Three', 'Four', 'Five'), ++ ), ++ ), ++ ),) ++ ++ def process(self): ++ from leapp.libraries.common.test_helper import log_execution ++ log_execution(self) ++ self.get_answers(self.dialogs[0]).get('confirm', False) +diff --git a/tests/data/leappdb-tests/actors/exitstatusactor/actor.py b/tests/data/leappdb-tests/actors/exitstatusactor/actor.py +new file mode 100644 +index 0000000..ae41aa5 +--- /dev/null ++++ b/tests/data/leappdb-tests/actors/exitstatusactor/actor.py +@@ -0,0 +1,31 @@ ++import os ++ ++from leapp.actors import Actor ++from leapp.tags import FirstPhaseTag, UnitTestWorkflowTag ++from leapp.exceptions import StopActorExecution, StopActorExecutionError ++ ++ ++class ExitStatusActor(Actor): ++ name = 'exit_status_actor' ++ description = 'No description has been provided for the exit_status_actor actor.' ++ consumes = () ++ produces = () ++ tags = (FirstPhaseTag, UnitTestWorkflowTag) ++ ++ def process(self): ++ from leapp.libraries.common.test_helper import log_execution ++ log_execution(self) ++ if not self.configuration or self.configuration.value != 'unit-test': ++ self.report_error('Unit test failed due missing or invalid workflow provided configuration') ++ ++ if os.environ.get('ExitStatusActor-Error') == 'StopActorExecution': ++ self.report_error('Unit test requested StopActorExecution error') ++ raise StopActorExecution ++ ++ if os.environ.get('ExitStatusActor-Error') == 'StopActorExecutionError': ++ self.report_error('Unit test requested StopActorExecutionError error') ++ raise StopActorExecutionError('StopActorExecutionError message') ++ ++ if os.environ.get('ExitStatusActor-Error') == 'UnhandledError': ++ self.report_error('Unit test requested unhandled error') ++ assert 0 == 1, '0 == 1' +diff --git a/tests/data/leappdb-tests/libraries/test_helper.py b/tests/data/leappdb-tests/libraries/test_helper.py +new file mode 100644 +index 0000000..fd5b910 +--- /dev/null ++++ b/tests/data/leappdb-tests/libraries/test_helper.py +@@ -0,0 +1,7 @@ ++import os ++import json ++ ++ ++def log_execution(actor): ++ with open(os.environ['LEAPP_TEST_EXECUTION_LOG'], 'a+') as f: ++ f.write(json.dumps(dict(name=actor.name, class_name=type(actor).__name__)) + '\n') +diff --git a/tests/data/leappdb-tests/models/unittestconfig.py b/tests/data/leappdb-tests/models/unittestconfig.py +new file mode 100644 +index 0000000..10fad83 +--- /dev/null ++++ b/tests/data/leappdb-tests/models/unittestconfig.py +@@ -0,0 +1,7 @@ ++from leapp.models import Model, fields ++from leapp.topics import ConfigTopic ++ ++ ++class UnitTestConfig(Model): ++ topic = ConfigTopic ++ value = fields.String(default='unit-test') +diff --git a/tests/data/leappdb-tests/tags/firstphase.py b/tests/data/leappdb-tests/tags/firstphase.py +new file mode 100644 +index 0000000..e465892 +--- /dev/null ++++ b/tests/data/leappdb-tests/tags/firstphase.py +@@ -0,0 +1,5 @@ ++from leapp.tags import Tag ++ ++ ++class FirstPhaseTag(Tag): ++ name = 'first_phase' +diff --git a/tests/data/leappdb-tests/tags/secondphase.py b/tests/data/leappdb-tests/tags/secondphase.py +new file mode 100644 +index 0000000..ead6c95 +--- /dev/null ++++ b/tests/data/leappdb-tests/tags/secondphase.py +@@ -0,0 +1,5 @@ ++from leapp.tags import Tag ++ ++ ++class SecondPhaseTag(Tag): ++ name = 'second_phase' +diff --git a/tests/data/leappdb-tests/tags/unittestworkflow.py b/tests/data/leappdb-tests/tags/unittestworkflow.py +new file mode 100644 +index 0000000..4a45594 +--- /dev/null ++++ b/tests/data/leappdb-tests/tags/unittestworkflow.py +@@ -0,0 +1,5 @@ ++from leapp.tags import Tag ++ ++ ++class UnitTestWorkflowTag(Tag): ++ name = 'unit_test_workflow' +diff --git a/tests/data/leappdb-tests/topics/config.py b/tests/data/leappdb-tests/topics/config.py +new file mode 100644 +index 0000000..9ed3140 +--- /dev/null ++++ b/tests/data/leappdb-tests/topics/config.py +@@ -0,0 +1,5 @@ ++from leapp.topics import Topic ++ ++ ++class ConfigTopic(Topic): ++ name = 'config_topic' +diff --git a/tests/data/leappdb-tests/workflows/unit_test.py b/tests/data/leappdb-tests/workflows/unit_test.py +new file mode 100644 +index 0000000..856d8e9 +--- /dev/null ++++ b/tests/data/leappdb-tests/workflows/unit_test.py +@@ -0,0 +1,37 @@ ++from leapp.models import UnitTestConfig ++from leapp.workflows import Workflow ++from leapp.workflows.phases import Phase ++from leapp.workflows.flags import Flags ++from leapp.workflows.tagfilters import TagFilter ++from leapp.workflows.policies import Policies ++from leapp.tags import UnitTestWorkflowTag, FirstPhaseTag, SecondPhaseTag ++ ++ ++class UnitTestWorkflow(Workflow): ++ name = 'LeappDBUnitTest' ++ tag = UnitTestWorkflowTag ++ short_name = 'unit_test' ++ description = '''No description has been provided for the UnitTest workflow.''' ++ configuration = UnitTestConfig ++ ++ class FirstPhase(Phase): ++ name = 'first-phase' ++ filter = TagFilter(FirstPhaseTag) ++ policies = Policies(Policies.Errors.FailImmediately, Policies.Retry.Phase) ++ flags = Flags() ++ ++ class SecondPhase(Phase): ++ name = 'second-phase' ++ filter = TagFilter(SecondPhaseTag) ++ policies = Policies(Policies.Errors.FailPhase, Policies.Retry.Phase) ++ flags = Flags() ++ ++ # Template for phase definition - The order in which the phase classes are defined ++ # within the Workflow class represents the execution order ++ # ++ # class PhaseName(Phase): ++ # name = 'phase_name' ++ # filter = TagFilter(PhaseTag) ++ # policies = Policies(Policies.Errors.FailPhase, ++ # Policies.Retry.Phase) ++ # flags = Flags() +diff --git a/tests/scripts/test_actor_api.py b/tests/scripts/test_actor_api.py +index f009e68..2e626da 100644 +--- a/tests/scripts/test_actor_api.py ++++ b/tests/scripts/test_actor_api.py +@@ -188,7 +188,13 @@ def test_actor_get_answers(monkeypatch, leapp_forked, setup_database, repository + def mocked_input(title): + return user_responses[title.split()[0].split(':')[0].lower()][0] + ++ def mocked_store_dialog(dialog, answer): ++ # Silence warnings ++ dialog = answer ++ answer = dialog ++ + monkeypatch.setattr('leapp.dialogs.renderer.input', mocked_input) ++ monkeypatch.setattr('leapp.actors.store_dialog', mocked_store_dialog) + + messaging = _TestableMessaging() + with _with_loaded_actor(repository, actor_name, messaging) as (_unused, actor): +diff --git a/tests/scripts/test_dialog_db.py b/tests/scripts/test_dialog_db.py +new file mode 100644 +index 0000000..e73eac8 +--- /dev/null ++++ b/tests/scripts/test_dialog_db.py +@@ -0,0 +1,186 @@ ++import os ++import json ++import tempfile ++ ++import mock ++import py ++import pytest ++ ++from leapp.repository.scan import scan_repo ++from leapp.dialogs import Dialog ++from leapp.dialogs.components import BooleanComponent, ChoiceComponent, NumberComponent, TextComponent ++from leapp.utils.audit import get_connection, dict_factory, store_dialog ++from leapp.utils.audit import Dialog as DialogDB ++from leapp.config import get_config ++ ++_HOSTNAME = 'test-host.example.com' ++_CONTEXT_NAME = 'test-context-name-dialogdb' ++_ACTOR_NAME = 'test-actor-name' ++_PHASE_NAME = 'test-phase-name' ++_DIALOG_SCOPE = 'test-dialog' ++ ++_TEXT_COMPONENT_METADATA = { ++ 'default': None, ++ 'description': 'a text value is needed', ++ 'key': 'text', ++ 'label': 'text', ++ 'reason': None, ++ 'value': None ++} ++_BOOLEAN_COMPONENT_METADATA = { ++ 'default': None, ++ 'description': 'a boolean value is needed', ++ 'key': 'bool', ++ 'label': 'bool', ++ 'reason': None, ++ 'value': None ++} ++ ++_NUMBER_COMPONENT_METADATA = { ++ 'default': -1, ++ 'description': 'a numeric value is needed', ++ 'key': 'num', ++ 'label': 'num', ++ 'reason': None, ++ 'value': None ++} ++_CHOICE_COMPONENT_METADATA = { ++ 'default': None, ++ 'description': 'need to choose one of these choices', ++ 'key': 'choice', ++ 'label': 'choice', ++ 'reason': None, ++ 'value': None ++} ++_COMPONENT_METADATA = [ ++ _TEXT_COMPONENT_METADATA, _BOOLEAN_COMPONENT_METADATA, _NUMBER_COMPONENT_METADATA, _CHOICE_COMPONENT_METADATA ++] ++_COMPONENT_METADATA_FIELDS = ('default', 'description', 'key', 'label', 'reason', 'value') ++_DIALOG_METADATA_FIELDS = ('answer', 'title', 'reason', 'components') ++ ++_TEST_DIALOG = Dialog( ++ scope=_DIALOG_SCOPE, ++ reason='need to test dialogs', ++ components=( ++ TextComponent( ++ key='text', ++ label='text', ++ description='a text value is needed', ++ ), ++ BooleanComponent(key='bool', label='bool', description='a boolean value is needed'), ++ NumberComponent(key='num', label='num', description='a numeric value is needed'), ++ ChoiceComponent( ++ key='choice', ++ label='choice', ++ description='need to choose one of these choices', ++ choices=('One', 'Two', 'Three', 'Four', 'Five'), ++ ), ++ ), ++) ++ ++ ++@pytest.fixture(scope='module') ++def repository(): ++ repository_path = py.path.local(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'leappdb-tests')) ++ with repository_path.as_cwd(): ++ repo = scan_repo('.') ++ repo.load(resolve=True) ++ yield repo ++ ++ ++def setup_module(): ++ get_config().set('database', 'path', '/tmp/leapp-test.db') ++ ++ ++def setup(): ++ path = get_config().get('database', 'path') ++ if os.path.isfile(path): ++ os.unlink(path) ++ ++ ++def fetch_dialog(dialog_id=None): ++ entry = None ++ with get_connection(None) as conn: ++ ++ if dialog_id is not None: ++ cursor = conn.execute('SELECT * FROM dialog WHERE id = ?;', (dialog_id,)) ++ else: # Fetch last saved dialog ++ cursor = conn.execute('SELECT * FROM dialog ORDER BY id DESC LIMIT 1;',) ++ ++ cursor.row_factory = dict_factory ++ entry = cursor.fetchone() ++ ++ return entry ++ ++ ++def test_save_empty_dialog(): ++ e = DialogDB( ++ scope=_DIALOG_SCOPE, ++ data=None, ++ context=_CONTEXT_NAME, ++ actor=_ACTOR_NAME, ++ phase=_PHASE_NAME, ++ hostname=_HOSTNAME, ++ ) ++ e.store() ++ ++ assert e.dialog_id ++ assert e.data_source_id ++ assert e.host_id ++ ++ entry = fetch_dialog(e.dialog_id) ++ assert entry is not None ++ assert entry['data_source_id'] == e.data_source_id ++ assert entry['context'] == _CONTEXT_NAME ++ assert entry['scope'] == _DIALOG_SCOPE ++ assert entry['data'] == 'null' ++ ++ ++def test_save_dialog(monkeypatch): ++ monkeypatch.setenv('LEAPP_CURRENT_ACTOR', _ACTOR_NAME) ++ monkeypatch.setenv('LEAPP_CURRENT_PHASE', _PHASE_NAME) ++ monkeypatch.setenv('LEAPP_EXECUTION_ID', _CONTEXT_NAME) ++ monkeypatch.setenv('LEAPP_HOSTNAME', _HOSTNAME) ++ e = store_dialog(_TEST_DIALOG, {}) ++ monkeypatch.delenv('LEAPP_CURRENT_ACTOR') ++ monkeypatch.delenv('LEAPP_CURRENT_PHASE') ++ monkeypatch.delenv('LEAPP_EXECUTION_ID') ++ monkeypatch.delenv('LEAPP_HOSTNAME') ++ ++ entry = fetch_dialog(e.dialog_id) ++ assert entry is not None ++ assert entry['data_source_id'] == e.data_source_id ++ assert entry['context'] == _CONTEXT_NAME ++ assert entry['scope'] == _TEST_DIALOG.scope ++ ++ entry_data = json.loads(entry['data']) ++ ++ assert sorted(entry_data.keys()) == sorted(_DIALOG_METADATA_FIELDS) ++ ++ assert entry_data['answer'] == {} ++ assert entry_data['reason'] == 'need to test dialogs' ++ assert entry_data['title'] is None ++ for component_metadata in _COMPONENT_METADATA: ++ assert sorted(component_metadata.keys()) == sorted(_COMPONENT_METADATA_FIELDS) ++ assert component_metadata in entry_data['components'] ++ ++ ++def test_save_dialog_workflow(monkeypatch, repository): ++ workflow = repository.lookup_workflow('LeappDBUnitTest')() ++ with tempfile.NamedTemporaryFile(mode='w') as stdin_dialog: ++ monkeypatch.setenv('LEAPP_TEST_EXECUTION_LOG', '/dev/null') ++ stdin_dialog.write('my answer\n') ++ stdin_dialog.write('yes\n') ++ stdin_dialog.write('42\n') ++ stdin_dialog.write('0\n') ++ stdin_dialog.seek(0) ++ with mock.patch('sys.stdin.fileno', return_value=stdin_dialog.fileno()): ++ workflow.run(skip_dialogs=False) ++ ++ monkeypatch.delenv('LEAPP_TEST_EXECUTION_LOG', '/dev/null') ++ ++ entry = fetch_dialog() ++ assert entry is not None ++ assert entry['scope'] == 'unique_dialog_scope' ++ data = json.loads(entry['data']) ++ assert data['answer'] == {'text': 'my answer', 'num': 42, 'bool': True, 'choice': 'One'} +diff --git a/tests/scripts/test_exit_status.py b/tests/scripts/test_exit_status.py +new file mode 100644 +index 0000000..11e8583 +--- /dev/null ++++ b/tests/scripts/test_exit_status.py +@@ -0,0 +1,68 @@ ++import os ++import json ++import tempfile ++ ++import py ++import pytest ++ ++from leapp.repository.scan import scan_repo ++from leapp.config import get_config ++from leapp.utils.audit import get_audit_entry ++ ++_HOSTNAME = 'test-host.example.com' ++_CONTEXT_NAME = 'test-context-name-exit-status' ++_ACTOR_NAME = 'test-actor-name' ++_PHASE_NAME = 'test-phase-name' ++_DIALOG_SCOPE = 'test-dialog' ++ ++ ++@pytest.fixture(scope='module') ++def repository(): ++ repository_path = py.path.local(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'leappdb-tests')) ++ with repository_path.as_cwd(): ++ repo = scan_repo('.') ++ repo.load(resolve=True) ++ yield repo ++ ++ ++def setup_module(): ++ get_config().set('database', 'path', '/tmp/leapp-test.db') ++ ++ ++def setup(): ++ path = get_config().get('database', 'path') ++ if os.path.isfile(path): ++ os.unlink(path) ++ ++ ++@pytest.mark.parametrize('error, code', [(None, 0), ('StopActorExecution', 0), ('StopActorExecutionError', 0), ++ ('UnhandledError', 1)]) ++def test_exit_status_stopactorexecution(monkeypatch, repository, error, code): ++ ++ workflow = repository.lookup_workflow('LeappDBUnitTest')() ++ ++ if error is not None: ++ os.environ['ExitStatusActor-Error'] = error ++ else: ++ os.environ.pop('ExitStatusActor-Error', None) ++ ++ with tempfile.NamedTemporaryFile() as test_log_file: ++ monkeypatch.setenv('LEAPP_TEST_EXECUTION_LOG', test_log_file.name) ++ monkeypatch.setenv('LEAPP_HOSTNAME', _HOSTNAME) ++ try: ++ workflow.run(skip_dialogs=True, context=_CONTEXT_NAME, until_actor='ExitStatusActor') ++ except BaseException: # pylint: disable=broad-except ++ pass ++ ++ ans = get_audit_entry('actor-exit-status', _CONTEXT_NAME).pop() ++ ++ assert ans is not None ++ assert ans['actor'] == 'exit_status_actor' ++ assert ans['context'] == _CONTEXT_NAME ++ assert ans['hostname'] == _HOSTNAME ++ data = json.loads(ans['data']) ++ assert data['exit_status'] == code ++ ++ ++def teardown(): ++ os.environ.pop('ExitStatusActor-Error', None) +diff --git a/tests/scripts/test_metadata.py b/tests/scripts/test_metadata.py +new file mode 100644 +index 0000000..9b5c07f +--- /dev/null ++++ b/tests/scripts/test_metadata.py +@@ -0,0 +1,223 @@ ++import os ++import json ++import logging ++import hashlib ++ ++import mock ++import py ++import pytest ++ ++from leapp.repository.scan import scan_repo ++from leapp.repository.actor_definition import ActorDefinition ++from leapp.utils.audit import (get_connection, dict_factory, Metadata, Entity, store_actor_metadata, ++ store_workflow_metadata) ++from leapp.config import get_config ++ ++_HOSTNAME = 'test-host.example.com' ++_CONTEXT_NAME = 'test-context-name-metadata' ++_ACTOR_NAME = 'test-actor-name' ++_PHASE_NAME = 'test-phase-name' ++_DIALOG_SCOPE = 'test-dialog' ++ ++_WORKFLOW_METADATA_FIELDS = ('description', 'name', 'phases', 'short_name', 'tag') ++_ACTOR_METADATA_FIELDS = ('class_name', 'name', 'description', 'phase', 'tags', 'consumes', 'produces', 'path') ++ ++_TEST_WORKFLOW_METADATA = { ++ 'description': 'No description has been provided for the UnitTest workflow.', ++ 'name': 'LeappDBUnitTest', ++ 'phases': [{ ++ 'class_name': 'FirstPhase', ++ 'filter': { ++ 'phase': 'FirstPhaseTag', ++ 'tags': ['UnitTestWorkflowTag'] ++ }, ++ 'flags': { ++ 'is_checkpoint': False, ++ 'request_restart_after_phase': False, ++ 'restart_after_phase': False ++ }, ++ 'index': 4, ++ 'name': 'first-phase', ++ 'policies': { ++ 'error': 'FailImmediately', ++ 'retry': 'Phase' ++ } ++ }, { ++ 'class_name': 'SecondPhase', ++ 'filter': { ++ 'phase': 'SecondPhaseTag', ++ 'tags': ['UnitTestWorkflowTag'] ++ }, ++ 'flags': { ++ 'is_checkpoint': False, ++ 'request_restart_after_phase': False, ++ 'restart_after_phase': False ++ }, ++ 'index': 5, ++ 'name': 'second-phase', ++ 'policies': { ++ 'error': 'FailPhase', ++ 'retry': 'Phase' ++ } ++ }], ++ 'short_name': 'unit_test', ++ 'tag': 'UnitTestWorkflowTag' ++} ++_TEST_ACTOR_METADATA = { ++ 'description': 'Test Description', ++ 'class_name': 'TestActor', ++ 'name': 'test-actor', ++ 'path': 'actors/test', ++ 'tags': (), ++ 'consumes': (), ++ 'produces': (), ++ 'dialogs': (), ++ 'apis': () ++} ++ ++ ++@pytest.fixture(scope='module') ++def repository(): ++ repository_path = py.path.local(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'leappdb-tests')) ++ with repository_path.as_cwd(): ++ repo = scan_repo('.') ++ repo.load(resolve=True) ++ yield repo ++ ++ ++def setup_module(): ++ get_config().set('database', 'path', '/tmp/leapp-test.db') ++ ++ ++def setup(): ++ path = get_config().get('database', 'path') ++ if os.path.isfile(path): ++ os.unlink(path) ++ ++ ++def test_save_empty_metadata(): ++ hash_id = hashlib.sha256('test-empty-metadata'.encode('utf-8')).hexdigest() ++ md = Metadata(hash_id=hash_id, metadata='') ++ md.store() ++ ++ entry = None ++ with get_connection(None) as conn: ++ cursor = conn.execute('SELECT * FROM metadata WHERE hash = ?;', (hash_id,)) ++ cursor.row_factory = dict_factory ++ entry = cursor.fetchone() ++ ++ assert entry is not None ++ assert entry['metadata'] == '' ++ ++ ++def test_save_empty_entity(): ++ hash_id = hashlib.sha256('test-empty-entity'.encode('utf-8')).hexdigest() ++ md = Metadata(hash_id=hash_id, metadata='') ++ e = Entity( ++ name='test-name', ++ metadata=md, ++ kind='test-kind', ++ context=_CONTEXT_NAME, ++ hostname=_HOSTNAME, ++ ) ++ e.store() ++ ++ assert e.entity_id ++ assert e.host_id ++ ++ entry = None ++ with get_connection(None) as conn: ++ cursor = conn.execute('SELECT * FROM entity WHERE id = ?;', (e.entity_id,)) ++ cursor.row_factory = dict_factory ++ entry = cursor.fetchone() ++ ++ assert entry is not None ++ assert entry['kind'] == 'test-kind' ++ assert entry['name'] == 'test-name' ++ assert entry['context'] == _CONTEXT_NAME ++ assert entry['metadata_hash'] == hash_id ++ ++ ++def test_store_actor_metadata(monkeypatch, repository_dir): ++ # --- ++ # Test store actor metadata without error ++ # --- ++ with repository_dir.as_cwd(): ++ logger = logging.getLogger('leapp.actor.test') ++ with mock.patch.object(logger, 'log') as log_mock: ++ definition = ActorDefinition('actors/test', '.', log=log_mock) ++ with mock.patch('leapp.repository.actor_definition.get_actor_metadata', return_value=_TEST_ACTOR_METADATA): ++ with mock.patch('leapp.repository.actor_definition.get_actors', return_value=[True]): ++ definition._module = True ++ ++ monkeypatch.setenv('LEAPP_EXECUTION_ID', _CONTEXT_NAME) ++ monkeypatch.setenv('LEAPP_HOSTNAME', _HOSTNAME) ++ store_actor_metadata(definition, 'test-phase') ++ monkeypatch.delenv('LEAPP_EXECUTION_ID') ++ monkeypatch.delenv('LEAPP_HOSTNAME') ++ ++ # --- ++ # Test retrieve correct actor metadata ++ # --- ++ entry = None ++ with get_connection(None) as conn: ++ cursor = conn.execute('SELECT * ' ++ 'FROM entity ' ++ 'JOIN metadata ' ++ 'ON entity.metadata_hash = metadata.hash ' ++ 'WHERE name="test-actor";') ++ cursor.row_factory = dict_factory ++ entry = cursor.fetchone() ++ ++ assert entry is not None ++ assert entry['kind'] == 'actor' ++ assert entry['name'] == _TEST_ACTOR_METADATA['name'] ++ assert entry['context'] == _CONTEXT_NAME ++ ++ metadata = json.loads(entry['metadata']) ++ assert sorted(metadata.keys()) == sorted(_ACTOR_METADATA_FIELDS) ++ assert metadata['class_name'] == _TEST_ACTOR_METADATA['class_name'] ++ assert metadata['name'] == _TEST_ACTOR_METADATA['name'] ++ assert metadata['description'] == _TEST_ACTOR_METADATA['description'] ++ assert metadata['phase'] == 'test-phase' ++ assert sorted(metadata['tags']) == sorted(_TEST_ACTOR_METADATA['tags']) ++ assert sorted(metadata['consumes']) == sorted(_TEST_ACTOR_METADATA['consumes']) ++ assert sorted(metadata['produces']) == sorted(_TEST_ACTOR_METADATA['produces']) ++ ++ ++def test_workflow_metadata(monkeypatch, repository): ++ # --- ++ # Test store workflow metadata without error ++ # --- ++ workflow = repository.lookup_workflow('LeappDBUnitTest')() ++ ++ monkeypatch.setenv('LEAPP_EXECUTION_ID', _CONTEXT_NAME) ++ monkeypatch.setenv('LEAPP_HOSTNAME', _HOSTNAME) ++ store_workflow_metadata(workflow) ++ monkeypatch.delenv('LEAPP_EXECUTION_ID') ++ monkeypatch.delenv('LEAPP_HOSTNAME') ++ ++ # --- ++ # Test retrieve correct workflow metadata ++ # --- ++ entry = None ++ with get_connection(None) as conn: ++ cursor = conn.execute( ++ 'SELECT * ' ++ 'FROM entity ' ++ 'JOIN metadata ' ++ 'ON entity.metadata_hash = metadata.hash ' ++ 'WHERE kind == "workflow" AND context = ? ' ++ 'ORDER BY id DESC ' ++ 'LIMIT 1;', (_CONTEXT_NAME,)) ++ cursor.row_factory = dict_factory ++ entry = cursor.fetchone() ++ ++ assert entry is not None ++ assert entry['kind'] == 'workflow' ++ assert entry['name'] == 'LeappDBUnitTest' ++ assert entry['context'] == _CONTEXT_NAME ++ ++ metadata = json.loads(entry['metadata']) ++ assert sorted(metadata.keys()) == sorted(_WORKFLOW_METADATA_FIELDS) ++ assert metadata == _TEST_WORKFLOW_METADATA +-- +2.42.0 + diff --git a/leapp.spec b/leapp.spec index 033f989..618e260 100644 --- a/leapp.spec +++ b/leapp.spec @@ -37,7 +37,7 @@ Name: leapp Version: 0.17.0 -Release: 1%{?dist} +Release: 2%{?dist} Summary: OS & Application modernization framework License: ASL 2.0 @@ -65,6 +65,13 @@ Requires: leapp-repository # PATCHES HERE # Patch0001: filename.patch +Patch0001: 0001-Update-packit-with-currently-supported-upgrade-paths.patch +Patch0002: 0002-Update-packit-config-after-tier-redefinition.patch +Patch0003: 0003-Reword-the-report-msg-in-the-console-output-to-note-.patch +Patch0004: 0004-Add-renovate.json.patch +Patch0005: 0005-Update-renovate.json.patch +Patch0006: 0006-Add-process-lock.patch +Patch0007: 0007-Extend-information-from-leapp-saved-to-leappdb-847.patch %description @@ -159,7 +166,13 @@ Requires: findutils # APPLY REGISTERED PATCHES HERE -# %%patch0001 -p1 +%patch0001 -p1 +%patch0002 -p1 +%patch0003 -p1 +%patch0004 -p1 +%patch0005 -p1 +%patch0006 -p1 +%patch0007 -p1 ################################################## @@ -243,6 +256,12 @@ install -m 0644 -p man/leapp.1 %{buildroot}%{_mandir}/man1/ # no files here %changelog +* Mon May 13 2024 Toshio Kuratomi - 0.17.0-2 +- Minor improvement of the summary overview in the console output. +- Store metadata about the LEAPP plugins inside leapp audit db. +- Prevent leapp from starting if an instance is already runnning. +- Resolves: RHEL-27848, RHEL-25407 + * Tue Feb 13 2024 Toshio Kuratomi - - 0.17.0-1 - Rebase to upstream version v0.17.0. - Resolves: RHEL-21451