From 16681ba66d06505380db5c8ded941e72d3a04d9f Mon Sep 17 00:00:00 2001 From: Grant Orndorff Date: Thu, 27 Jul 2023 11:21:26 -0400 Subject: [PATCH] landscape: support enabling and disabling landscape-client --- .github/workflows/ci-integration.yaml | 3 + features/attached_commands.feature | 37 ++- features/attached_enable.feature | 16 +- features/environment.py | 12 + features/landscape.feature | 257 +++++++++++++++++ features/steps/landscape.py | 69 +++++ features/steps/shell.py | 26 +- features/unattached_commands.feature | 48 ++-- features/unattached_status.feature | 24 +- help_data.yaml | 10 + tools/ua.bash | 4 +- uaclient/actions.py | 4 +- uaclient/cli.py | 1 + uaclient/entitlements/__init__.py | 2 + uaclient/entitlements/base.py | 5 + uaclient/entitlements/landscape.py | 141 +++++++++ uaclient/entitlements/tests/test_landscape.py | 272 ++++++++++++++++++ uaclient/messages.py | 33 +++ uaclient/tests/test_cli_disable.py | 4 +- uaclient/tests/test_cli_enable.py | 8 +- uaclient/tests/test_status.py | 12 + uaclient/tests/test_util.py | 36 +++ uaclient/util.py | 9 + 23 files changed, 972 insertions(+), 61 deletions(-) create mode 100644 features/landscape.feature create mode 100644 features/steps/landscape.py create mode 100644 uaclient/entitlements/landscape.py create mode 100644 uaclient/entitlements/tests/test_landscape.py diff --git a/.github/workflows/ci-integration.yaml b/.github/workflows/ci-integration.yaml index 0f40c0cc87..be05d93dc4 100644 --- a/.github/workflows/ci-integration.yaml +++ b/.github/workflows/ci-integration.yaml @@ -123,6 +123,9 @@ jobs: UACLIENT_BEHAVE_CONTRACT_TOKEN: '${{ secrets.UACLIENT_BEHAVE_CONTRACT_TOKEN }}' UACLIENT_BEHAVE_CONTRACT_TOKEN_STAGING: '${{ secrets.UACLIENT_BEHAVE_CONTRACT_TOKEN_STAGING }}' UACLIENT_BEHAVE_CONTRACT_TOKEN_STAGING_EXPIRED: '${{ secrets.UACLIENT_BEHAVE_CONTRACT_TOKEN_STAGING_EXPIRED }}' + UACLIENT_BEHAVE_LANDSCAPE_REGISTRATION_KEY: '${{ secrets.UACLIENT_BEHAVE_LANDSCAPE_REGISTRATION_KEY }}' + UACLIENT_BEHAVE_LANDSCAPE_API_ACCESS_KEY: '${{ secrets.UACLIENT_BEHAVE_LANDSCAPE_API_ACCESS_KEY }}' + UACLIENT_BEHAVE_LANDSCAPE_API_SECRET_KEY: '${{ secrets.UACLIENT_BEHAVE_LANDSCAPE_API_SECRET_KEY }}' run: | PYCLOUDLIB_CONFIG="$(mktemp --tmpdir="${{ runner.temp }}" pycloudlib.toml.XXXXXXXXXX)" GCE_CREDENTIALS_PATH="$(mktemp --tmpdir="${{ runner.temp }}" gcloud.json.XXXXXXXXXX)" diff --git a/features/attached_commands.feature b/features/attached_commands.feature index b7b5b1f392..932ffce291 100644 --- a/features/attached_commands.feature +++ b/features/attached_commands.feature @@ -136,10 +136,10 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription Examples: ubuntu release | release | valid_services | - | xenial | anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | bionic | anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | focal | anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, livepatch,\nrealtime-kernel, ros, ros-updates, usg. | - | jammy | anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, livepatch,\nrealtime-kernel, ros, ros-updates, usg. | + | xenial | anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | bionic | anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | focal | anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, landscape,\nlivepatch, realtime-kernel, ros, ros-updates, usg. | + | jammy | anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, landscape,\nlivepatch, realtime-kernel, ros, ros-updates, usg. | @series.lts @uses.config.machine_type.lxd-container @@ -176,10 +176,10 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription Examples: ubuntu release | release | msg | - | xenial | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | bionic | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | focal | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, livepatch,\nrealtime-kernel, ros, ros-updates, usg. | - | jammy | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, livepatch,\nrealtime-kernel, ros, ros-updates, usg. | + | xenial | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | bionic | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | focal | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, landscape,\nlivepatch, realtime-kernel, ros, ros-updates, usg. | + | jammy | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, landscape,\nlivepatch, realtime-kernel, ros, ros-updates, usg. | @series.lts @uses.config.machine_type.lxd-container @@ -219,6 +219,7 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription esm-infra +yes +Expanded Security Maintenance for Infrastructure fips + +NIST-certified core packages fips-updates + +NIST-certified core packages with priority security updates + landscape +(yes|no) +Management and administration tool for Ubuntu livepatch +(yes|no) +(Canonical Livepatch service|Current kernel is not supported) realtime-kernel + +Ubuntu kernel with PREEMPT_RT patches integrated ros + +Security Updates for the Robot Operating System @@ -428,10 +429,10 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription Examples: ubuntu release | release | msg | - | xenial | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | bionic | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | focal | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, livepatch,\nrealtime-kernel, ros, ros-updates, usg. | - | jammy | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, livepatch,\nrealtime-kernel, ros, ros-updates, usg. | + | xenial | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | bionic | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | focal | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, landscape,\nlivepatch, realtime-kernel, ros, ros-updates, usg. | + | jammy | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, landscape,\nlivepatch, realtime-kernel, ros, ros-updates, usg. | @series.xenial @series.bionic @@ -488,6 +489,8 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription \(https://ubuntu.com/security/certifications#fips\) - fips: NIST-certified core packages \(https://ubuntu.com/security/certifications#fips\) + - landscape: Management and administration tool for Ubuntu + \(https://ubuntu.com/landscape\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) """ @@ -508,6 +511,8 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription \(https://ubuntu.com/security/certifications#fips\) - fips: NIST-certified core packages \(https://ubuntu.com/security/certifications#fips\) + - landscape: Management and administration tool for Ubuntu + \(https://ubuntu.com/landscape\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) """ @@ -528,6 +533,8 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription \(https://ubuntu.com/security/certifications#fips\) - fips: NIST-certified core packages \(https://ubuntu.com/security/certifications#fips\) + - landscape: Management and administration tool for Ubuntu + \(https://ubuntu.com/landscape\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) - realtime-kernel: Ubuntu kernel with PREEMPT_RT patches integrated @@ -596,6 +603,8 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription \(https://ubuntu.com/security/certifications#fips\) - fips: NIST-certified core packages \(https://ubuntu.com/security/certifications#fips\) + - landscape: Management and administration tool for Ubuntu + \(https://ubuntu.com/landscape\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) - realtime-kernel: Ubuntu kernel with PREEMPT_RT patches integrated @@ -622,6 +631,8 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription \(https://ubuntu.com/security/certifications#fips\) - fips: NIST-certified core packages \(https://ubuntu.com/security/certifications#fips\) + - landscape: Management and administration tool for Ubuntu + \(https://ubuntu.com/landscape\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) - realtime-kernel: Ubuntu kernel with PREEMPT_RT patches integrated @@ -648,6 +659,8 @@ Feature: Command behaviour when attached to an Ubuntu Pro subscription \(https://ubuntu.com/security/certifications#fips\) - fips: NIST-certified core packages \(https://ubuntu.com/security/certifications#fips\) + - landscape: Management and administration tool for Ubuntu + \(https://ubuntu.com/landscape\) - livepatch: Canonical Livepatch service \(https://ubuntu.com/security/livepatch\) - realtime-kernel: Ubuntu kernel with PREEMPT_RT patches integrated diff --git a/features/attached_enable.feature b/features/attached_enable.feature index 93d4f80b19..b101e34f34 100644 --- a/features/attached_enable.feature +++ b/features/attached_enable.feature @@ -190,10 +190,10 @@ Feature: Enable command behaviour when attached to an Ubuntu Pro subscription Examples: ubuntu release | release | valid_services | - | xenial | anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | bionic | anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | focal | anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, livepatch,\nrealtime-kernel, ros, ros-updates, usg. | - | jammy | anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, livepatch,\nrealtime-kernel, ros, ros-updates, usg. | + | xenial | anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | bionic | anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | focal | anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, landscape,\nlivepatch, realtime-kernel, ros, ros-updates, usg. | + | jammy | anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, landscape,\nlivepatch, realtime-kernel, ros, ros-updates, usg. | @series.lts @uses.config.machine_type.lxd-container @@ -247,9 +247,9 @@ Feature: Enable command behaviour when attached to an Ubuntu Pro subscription Examples: ubuntu release | release | infra-pkg | esm-infra-url | msg | - | xenial | libkrad0 | https://esm.ubuntu.com/infra/ubuntu | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | bionic | libkrad0 | https://esm.ubuntu.com/infra/ubuntu | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel, ros, ros-updates. | - | focal | hello | https://esm.ubuntu.com/infra/ubuntu | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, livepatch,\nrealtime-kernel, ros, ros-updates, usg. | + | xenial | libkrad0 | https://esm.ubuntu.com/infra/ubuntu | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | bionic | libkrad0 | https://esm.ubuntu.com/infra/ubuntu | Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates. | + | focal | hello | https://esm.ubuntu.com/infra/ubuntu | Try anbox-cloud, cc-eal, esm-apps, esm-infra, fips, fips-updates, landscape,\nlivepatch, realtime-kernel, ros, ros-updates, usg. | @series.all @uses.config.machine_type.lxd-container @@ -488,7 +488,7 @@ Feature: Enable command behaviour when attached to an Ubuntu Pro subscription And stderr matches regexp: """ Cannot enable unknown service 'usg'. - Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlivepatch, realtime-kernel. + Try anbox-cloud, cc-eal, cis, esm-apps, esm-infra, fips, fips-updates,\nlandscape, livepatch, realtime-kernel, ros, ros-updates\. """ Examples: cis service diff --git a/features/environment.py b/features/environment.py index 6bb8e5cf61..dda001e54b 100644 --- a/features/environment.py +++ b/features/environment.py @@ -86,6 +86,9 @@ class UAClientBehaveConfig: "contract_token", "contract_token_staging", "contract_token_staging_expired", + "landscape_registration_key", + "landscape_api_access_key", + "landscape_api_secret_key", "machine_type", "private_key_file", "private_key_name", @@ -102,6 +105,9 @@ class UAClientBehaveConfig: "contract_token", "contract_token_staging", "contract_token_staging_expired", + "landscape_registration_key", + "landscape_api_access_key", + "landscape_api_secret_key", ] # This variable is used in .from_environ() but also to emit the "Config @@ -124,6 +130,9 @@ def __init__( contract_token: Optional[str] = None, contract_token_staging: Optional[str] = None, contract_token_staging_expired: Optional[str] = None, + landscape_registration_key: Optional[str] = None, + landscape_api_access_key: Optional[str] = None, + landscape_api_secret_key: Optional[str] = None, artifact_dir: str = "artifacts", install_from: InstallationSource = InstallationSource.LOCAL, custom_ppa: Optional[str] = None, @@ -141,6 +150,9 @@ def __init__( self.contract_token = contract_token self.contract_token_staging = contract_token_staging self.contract_token_staging_expired = contract_token_staging_expired + self.landscape_registration_key = landscape_registration_key + self.landscape_api_access_key = landscape_api_access_key + self.landscape_api_secret_key = landscape_api_secret_key self.image_clean = image_clean self.destroy_instances = destroy_instances self.machine_type = machine_type diff --git a/features/landscape.feature b/features/landscape.feature new file mode 100644 index 0000000000..e88e7a07ed --- /dev/null +++ b/features/landscape.feature @@ -0,0 +1,257 @@ +@uses.config.contract_token +@uses.config.landscape_registration_key +@uses.config.landscape_api_access_key +@uses.config.landscape_api_secret_key +Feature: Enable landscape on Ubuntu + + @series.mantic + @uses.config.machine_type.any + @uses.config.machine_type.lxd-container + Scenario Outline: Enable Landscape non-interactively + Given a `` `` machine with ubuntu-advantage-tools installed + When I attach `contract_token_staging` with sudo and options `--no-auto-enable` + + Then I verify that running `pro enable landscape` `as non-root` exits `1` + And I will see the following on stderr: + """ + This command must be run as root (try using sudo). + """ + + When I run `pro enable landscape -- --computer-title $behave_var{machine-name system-under-test} --account-name pro-client-qa --registration-key $behave_var{config landscape_registration_key} --silent` with sudo + Then stdout contains substring: + """ + One moment, checking your subscription first + Updating package lists + Installing landscape-client + Executing `landscape-config --computer-title $behave_var{machine-name system-under-test} --account-name pro-client-qa --registration-key --silent` + """ + Then stdout contains substring + """ + System successfully registered. + """ + When I run `pro status` as non-root + Then stdout matches regexp: + """ + landscape +yes +enabled + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +enabled + """ + + When I run `systemctl stop landscape-client` with sudo + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +warning + """ + Then stdout contains substring: + """ + Landscape is installed and configured and registered but not running. + Run `sudo landscape-config` to start it, or run `sudo pro disable landscape` + """ + + When I run `rm /etc/landscape/client.conf` with sudo + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +warning + """ + Then stdout contains substring: + """ + Landscape is installed but not configured. + Run `sudo landscape-config` to set it up, or run `sudo pro disable landscape` + """ + + When I run `sudo pro disable landscape` with sudo + Then I will see the following on stdout: + """ + Executing `landscape-config --disable` + Failed running command 'landscape-config --disable' [exit(1)]. Message: error: config file /etc/landscape/client.conf can't be read + Backing up /etc/landscape/client.conf as /etc/landscape/client.conf.pro-disable-backup + [Errno 2] No such file or directory: '/etc/landscape/client.conf' -> '/etc/landscape/client.conf.pro-disable-backup' + Uninstalling landscape-client + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +disabled + """ + + # Enable with assume-yes + When I run `pro enable landscape --assume-yes -- --computer-title $behave_var{machine-name system-under-test} --account-name pro-client-qa --registration-key $behave_var{config landscape_registration_key}` with sudo + Then I will see the following on stdout: + """ + One moment, checking your subscription first + Updating package lists + Installing landscape-client + Executing `landscape-config --computer-title $behave_var{machine-name system-under-test} --account-name pro-client-qa --registration-key --silent` + Landscape enabled + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +enabled + """ + When I run `sudo pro disable landscape` with sudo + + # Fail to enable with assume-yes + When I verify that running `pro enable landscape --assume-yes -- --computer-title $behave_var{machine-name system-under-test} --account-name pro-client-qa` `with sudo` exits `1` + Then I will see the following on stdout: + """ + One moment, checking your subscription first + Updating package lists + Installing landscape-client + Executing `landscape-config --computer-title $behave_var{machine-name system-under-test} --account-name pro-client-qa --silent` + Created symlink /etc/systemd/system/multi-user.target.wants/landscape-client.service → /lib/systemd/system/landscape-client.service. + Invalid account name or registration key. + Could not enable Landscape. + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +warning + """ + Then stdout contains substring: + """ + Landscape is installed and configured but not registered. + Run `sudo landscape-config` to register, or run `sudo pro disable landscape` + """ + When I run `sudo pro disable landscape` with sudo + + # Enable with assume-yes and format json + When I run `pro enable landscape --assume-yes --format=json -- --computer-title $behave_var{machine-name system-under-test} --account-name pro-client-qa --registration-key $behave_var{config landscape_registration_key}` with sudo + Then I will see the following on stdout: + """ + {"_schema_version": "0.1", "errors": [], "failed_services": [], "needs_reboot": false, "processed_services": ["landscape"], "result": "success", "warnings": []} + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +enabled + """ + When I run `sudo pro disable landscape` with sudo + + # Fail to enable with assume-yes and format json + When I verify that running `pro enable landscape --assume-yes --format=json -- --computer-title $behave_var{machine-name system-under-test} --account-name pro-client-qa` `with sudo` exits `1` + Then I will see the following on stdout: + """ + {"_schema_version": "0.1", "errors": [{"additional_info": {"stderr": "Created symlink /etc/systemd/system/multi-user.target.wants/landscape-client.service \u2192 /lib/systemd/system/landscape-client.service.\nInvalid account name or registration key.", "stdout": "Please wait..."}, "message": "landscape-config command failed", "message_code": "landscape-config-failed", "service": "landscape", "type": "service"}], "failed_services": ["landscape"], "needs_reboot": false, "processed_services": [], "result": "failure", "warnings": []} + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +warning + """ + Then stdout contains substring: + """ + Landscape is installed and configured but not registered. + Run `sudo landscape-config` to register, or run `sudo pro disable landscape` + """ + When I run `sudo pro disable landscape` with sudo + + # cleanup + Then I reject all pending computers on Landscape + Examples: ubuntu release + | release | machine_type | + | mantic | lxd-container | + + @series.mantic + @uses.config.machine_type.any + @uses.config.machine_type.lxd-container + Scenario Outline: Enable Landscape interactively + Given a `` `` machine with ubuntu-advantage-tools installed + When I attach `contract_token_staging` with sudo and options `--no-auto-enable` + + Then I verify that running `pro enable landscape` `as non-root` exits `1` + And I will see the following on stderr: + """ + This command must be run as root (try using sudo). + """ + + When I run `pro enable landscape` `with sudo` and the following stdin + # This will change in the future, but right now the lines are: + # allow starting on boot + # computer title + # account name + # registration key + # confirm registration key + # http proxy + # https proxy + # enable script execution + # access group + # tags + # request registration + """ + y + $behave_var{machine-name system-under-test} + pro-client-qa + $behave_var{config landscape_registration_key} + $behave_var{config landscape_registration_key} + + + n + + + y + """ + Then stdout contains substring: + """ + One moment, checking your subscription first + Updating package lists + Installing landscape-client + Executing `landscape-config` + """ + Then stdout contains substring: + """ + System successfully registered. + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +enabled + """ + When I run `pro disable landscape` with sudo + + When I verify that running `pro enable landscape` `with sudo` and the following stdin exits `1` + """ + y + $behave_var{machine-name system-under-test} + pro-client-qa + wrong + wrong + + + n + + + y + """ + Then stdout contains substring: + """ + One moment, checking your subscription first + Updating package lists + Installing landscape-client + Executing `landscape-config` + """ + Then stderr contains substring: + """ + Invalid account name or registration key. + """ + When I run `pro status` with sudo + Then stdout matches regexp: + """ + landscape +yes +warning + """ + Then stdout contains substring: + """ + Landscape is installed and configured but not registered. + Run `sudo landscape-config` to register, or run `sudo pro disable landscape` + """ + + # cleanup + Then I reject all pending computers on Landscape + Examples: ubuntu release + | release | machine_type | + | mantic | lxd-container | diff --git a/features/steps/landscape.py b/features/steps/landscape.py new file mode 100644 index 0000000000..9151756f85 --- /dev/null +++ b/features/steps/landscape.py @@ -0,0 +1,69 @@ +import hmac +import json +import time +from base64 import b64encode +from hashlib import sha256 +from urllib.parse import quote +from urllib.request import Request, urlopen + +from behave import step + + +def _landscape_api_request(access_key, secret_key, action, action_params): + timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + params = { + "action": action, + "access_key_id": access_key, + "signature_method": "HmacSHA256", + "signature_version": "2", + "timestamp": timestamp, + "version": "2011-08-01", + **action_params, + } + method = "POST" + uri = "https://landscape.canonical.com/api/" + host = "landscape.canonical.com" + path = "/api/" + + formatted_params = "&".join( + quote(k, safe="~") + "=" + quote(v, safe="~") + for k, v in sorted(params.items()) + ) + + to_sign = "{method}\n{host}\n{path}\n{formatted_params}".format( + method=method, + host=host, + path=path, + formatted_params=formatted_params, + ) + digest = hmac.new(secret_key.encode(), to_sign.encode(), sha256).digest() + signature = b64encode(digest) + formatted_params += "&signature=" + quote(signature) + + request = Request( + uri, + headers={"Host": host}, + method=method, + data=formatted_params.encode(), + ) + response = urlopen(request) + + return response.code, json.load(response) + + +@step("I reject all pending computers on Landscape") +def reject_all_pending_computers(context): + access_key = context.pro_config.landscape_api_access_key + secret_key = context.pro_config.landscape_api_secret_key + code, pending_computers = _landscape_api_request( + access_key, secret_key, "GetPendingComputers", {} + ) + assert code == 200 + reject_params = { + "computer_ids.{}".format(i + 1): str(computer["id"]) + for i, computer in enumerate(pending_computers) + } + code, _resp = _landscape_api_request( + access_key, secret_key, "RejectPendingComputers", reject_params + ) + assert code == 200 diff --git a/features/steps/shell.py b/features/steps/shell.py index 59a55583bf..1f74413bc4 100644 --- a/features/steps/shell.py +++ b/features/steps/shell.py @@ -96,17 +96,19 @@ def then_i_should_see_that_the_command_is_not_found(context, cmd_name): assert_that(expected_return, equal_to(actual_return)) -@step("I verify that running `{cmd_name}` `{spec}` exits `{exit_codes}`") -def then_i_verify_that_running_cmd_with_spec_exits_with_codes( +@step( + "I verify that running `{cmd_name}` `{spec}` and the following stdin exits `{exit_codes}`" # noqa +) +def i_verify_that_running_cmd_with_spec_and_stdin_exits_with_codes( context, cmd_name, spec, exit_codes ): - when_i_run_command(context, cmd_name, spec, verify_return=False) - logging.debug("got return code: %d", context.process.returncode) - expected_codes = exit_codes.split(",") - assert str(context.process.returncode) in expected_codes + text = process_template_vars(context, context.text) + then_i_verify_that_running_cmd_with_spec_and_stdin_exits_with_codes( + context, cmd_name, spec, text, exit_codes + ) -@when( +@step( "I verify that running `{cmd_name}` `{spec}` and stdin `{stdin}` exits `{exit_codes}`" # noqa ) def then_i_verify_that_running_cmd_with_spec_and_stdin_exits_with_codes( @@ -120,6 +122,16 @@ def then_i_verify_that_running_cmd_with_spec_and_stdin_exits_with_codes( assert str(context.process.returncode) in expected_codes +@step("I verify that running `{cmd_name}` `{spec}` exits `{exit_codes}`") +def then_i_verify_that_running_cmd_with_spec_exits_with_codes( + context, cmd_name, spec, exit_codes +): + when_i_run_command(context, cmd_name, spec, verify_return=False) + logging.debug("got return code: %d", context.process.returncode) + expected_codes = exit_codes.split(",") + assert str(context.process.returncode) in expected_codes + + def get_command_prefix_for_user_spec(user_spec): prefix = [] if user_spec.lstrip() == "with sudo": diff --git a/features/unattached_commands.feature b/features/unattached_commands.feature index 51c4ddf80b..d22c13719c 100644 --- a/features/unattached_commands.feature +++ b/features/unattached_commands.feature @@ -296,30 +296,30 @@ Feature: Command behaviour when unattached When I press tab twice to autocomplete the `ua enable` command Then stdout matches regexp: """ - anbox-cloud +esm-apps +fips-updates +ros\r - cc-eal +esm-infra +livepatch +ros-updates\r - cis +fips +realtime-kernel + anbox-cloud +esm-apps +fips-updates +realtime-kernel\r + cc-eal +esm-infra +landscape +ros\r + cis +fips +livepatch +ros-updates """ When I press tab twice to autocomplete the `pro enable` command Then stdout matches regexp: """ - anbox-cloud +esm-apps +fips-updates +ros\r - cc-eal +esm-infra +livepatch +ros-updates\r - cis +fips +realtime-kernel + anbox-cloud +esm-apps +fips-updates +realtime-kernel\r + cc-eal +esm-infra +landscape +ros\r + cis +fips +livepatch +ros-updates """ When I press tab twice to autocomplete the `ua disable` command Then stdout matches regexp: """ - anbox-cloud +esm-apps +fips-updates +ros\r - cc-eal +esm-infra +livepatch +ros-updates\r - cis +fips +realtime-kernel + anbox-cloud +esm-apps +fips-updates +realtime-kernel\r + cc-eal +esm-infra +landscape +ros\r + cis +fips +livepatch +ros-updates """ When I press tab twice to autocomplete the `pro disable` command Then stdout matches regexp: """ - anbox-cloud +esm-apps +fips-updates +ros\r - cc-eal +esm-infra +livepatch +ros-updates\r - cis +fips +realtime-kernel + anbox-cloud +esm-apps +fips-updates +realtime-kernel\r + cc-eal +esm-infra +landscape +ros\r + cis +fips +livepatch +ros-updates """ Examples: ubuntu release @@ -357,30 +357,30 @@ Feature: Command behaviour when unattached When I press tab twice to autocomplete the `ua enable` command Then stdout matches regexp: """ - anbox-cloud +esm-infra +livepatch +ros-updates\r - cc-eal +fips +realtime-kernel +usg\r - esm-apps +fips-updates +ros + anbox-cloud +esm-infra +landscape +ros\r + cc-eal +fips +livepatch +ros-updates\r + esm-apps +fips-updates +realtime-kernel +usg """ When I press tab twice to autocomplete the `pro enable` command Then stdout matches regexp: """ - anbox-cloud +esm-infra +livepatch +ros-updates\r - cc-eal +fips +realtime-kernel +usg\r - esm-apps +fips-updates +ros + anbox-cloud +esm-infra +landscape +ros\r + cc-eal +fips +livepatch +ros-updates\r + esm-apps +fips-updates +realtime-kernel +usg """ When I press tab twice to autocomplete the `ua disable` command Then stdout matches regexp: """ - anbox-cloud +esm-infra +livepatch +ros-updates\r - cc-eal +fips +realtime-kernel +usg\r - esm-apps +fips-updates +ros + anbox-cloud +esm-infra +landscape +ros\r + cc-eal +fips +livepatch +ros-updates\r + esm-apps +fips-updates +realtime-kernel +usg """ When I press tab twice to autocomplete the `pro disable` command Then stdout matches regexp: """ - anbox-cloud +esm-infra +livepatch +ros-updates\r - cc-eal +fips +realtime-kernel +usg\r - esm-apps +fips-updates +ros + anbox-cloud +esm-infra +landscape +ros\r + cc-eal +fips +livepatch +ros-updates\r + esm-apps +fips-updates +realtime-kernel +usg """ Examples: ubuntu release diff --git a/features/unattached_status.feature b/features/unattached_status.feature index 5cfa7b0743..6eea7548e0 100644 --- a/features/unattached_status.feature +++ b/features/unattached_status.feature @@ -58,6 +58,7 @@ Feature: Unattached status esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates + landscape +yes +Management and administration tool for Ubuntu livepatch +yes +(Canonical Livepatch service|Current kernel is not supported) ros +yes +Security Updates for the Robot Operating System ros-updates +yes +All Updates for the Robot Operating System @@ -79,6 +80,7 @@ Feature: Unattached status esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates + landscape +yes +Management and administration tool for Ubuntu livepatch +yes +(Canonical Livepatch service|Current kernel is not supported) realtime-kernel +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +yes +Security Updates for the Robot Operating System @@ -104,6 +106,7 @@ Feature: Unattached status esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates + landscape +yes +Management and administration tool for Ubuntu livepatch +yes +(Canonical Livepatch service|Current kernel is not supported) ros +yes +Security Updates for the Robot Operating System ros-updates +yes +All Updates for the Robot Operating System @@ -136,6 +139,7 @@ Feature: Unattached status esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates + landscape +yes +Management and administration tool for Ubuntu livepatch +yes +Canonical Livepatch service usg +yes +Security compliance and audit tools @@ -155,6 +159,7 @@ Feature: Unattached status esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates + landscape +yes +Management and administration tool for Ubuntu livepatch +yes +Canonical Livepatch service realtime-kernel +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +no +Security Updates for the Robot Operating System @@ -179,6 +184,7 @@ Feature: Unattached status esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +yes +NIST-certified core packages fips-updates +yes +NIST-certified core packages with priority security updates + landscape +yes +Management and administration tool for Ubuntu livepatch +yes +Canonical Livepatch service usg +yes +Security compliance and audit tools @@ -207,6 +213,7 @@ Feature: Unattached status anbox-cloud +yes +.* esm-apps +yes +Expanded Security Maintenance for Applications esm-infra +yes +Expanded Security Maintenance for Infrastructure + landscape +yes +Management and administration tool for Ubuntu livepatch +yes +Canonical Livepatch service realtime-kernel +yes +Ubuntu kernel with PREEMPT_RT patches integrated usg +yes +Security compliance and audit tools @@ -227,6 +234,7 @@ Feature: Unattached status esm-infra +yes +Expanded Security Maintenance for Infrastructure fips +no +NIST-certified core packages fips-updates +no +NIST-certified core packages with priority security updates + landscape +yes +Management and administration tool for Ubuntu livepatch +yes +Canonical Livepatch service realtime-kernel +yes +Ubuntu kernel with PREEMPT_RT patches integrated ros +no +Security Updates for the Robot Operating System @@ -249,6 +257,7 @@ Feature: Unattached status anbox-cloud +yes +.* esm-apps +yes +Expanded Security Maintenance for Applications esm-infra +yes +Expanded Security Maintenance for Infrastructure + landscape +yes +Management and administration tool for Ubuntu livepatch +yes +Canonical Livepatch service realtime-kernel +yes +Ubuntu kernel with PREEMPT_RT patches integrated usg +yes +Security compliance and audit tools @@ -283,6 +292,7 @@ Feature: Unattached status esm-infra +yes +yes +yes +Expanded Security Maintenance for Infrastructure fips +yes +yes +no +NIST-certified core packages fips-updates +yes +yes +no +NIST-certified core packages with priority security updates + landscape +yes +yes +no +Management and administration tool for Ubuntu livepatch +yes +yes +yes +Canonical Livepatch service """ When I do a preflight check for `contract_token` with the all flag @@ -296,6 +306,7 @@ Feature: Unattached status esm-infra +yes +yes +yes +Expanded Security Maintenance for Infrastructure fips +yes +yes +no +NIST-certified core packages fips-updates +yes +yes +no +NIST-certified core packages with priority security updates + landscape +yes +yes +no +Management and administration tool for Ubuntu livepatch +yes +yes +yes +Canonical Livepatch service realtime-kernel +no +yes +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +yes +yes +no +Security Updates for the Robot Operating System @@ -344,6 +355,7 @@ Feature: Unattached status esm-infra +yes +yes +yes +Expanded Security Maintenance for Infrastructure fips +yes +yes +no +NIST-certified core packages fips-updates +yes +yes +no +NIST-certified core packages with priority security updates + landscape +yes +yes +no +Management and administration tool for Ubuntu livepatch +yes +yes +yes +Canonical Livepatch service usg +yes +yes +no +Security compliance and audit tools """ @@ -357,6 +369,7 @@ Feature: Unattached status esm-infra +yes +yes +yes +Expanded Security Maintenance for Infrastructure fips +yes +yes +no +NIST-certified core packages fips-updates +yes +yes +no +NIST-certified core packages with priority security updates + landscape +yes +yes +no +Management and administration tool for Ubuntu livepatch +yes +yes +yes +Canonical Livepatch service realtime-kernel +no +yes +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +no +yes +no +Security Updates for the Robot Operating System @@ -404,6 +417,7 @@ Feature: Unattached status anbox-cloud +yes +.* esm-apps +yes +yes +yes +Expanded Security Maintenance for Applications esm-infra +yes +yes +yes +Expanded Security Maintenance for Infrastructure + landscape +yes +yes +no +Management and administration tool for Ubuntu livepatch +yes +yes +yes +Canonical Livepatch service realtime-kernel +yes +yes +no +Ubuntu kernel with PREEMPT_RT patches integrated usg +yes +yes +no +Security compliance and audit tools @@ -418,6 +432,7 @@ Feature: Unattached status esm-infra +yes +yes +yes +Expanded Security Maintenance for Infrastructure fips +no +yes +no +NIST-certified core packages fips-updates +no +yes +no +NIST-certified core packages with priority security updates + landscape +yes +yes +no +Management and administration tool for Ubuntu livepatch +yes +yes +yes +Canonical Livepatch service realtime-kernel +yes +yes +no +Ubuntu kernel with PREEMPT_RT patches integrated ros +no +yes +no +Security Updates for the Robot Operating System @@ -492,7 +507,8 @@ Feature: Unattached status esm-infra +yes +yes +yes +Expanded Security Maintenance for Infrastructure fips +yes +yes +no +NIST-certified core packages fips-updates +yes +yes +no +NIST-certified core packages with priority security updates - livepatch +yes +yes +yes +Canonical Livepatch service + (landscape +(yes|no) +yes +no +Management and administration tool for Ubuntu)? + ?livepatch +yes +yes +yes +Canonical Livepatch service ros +yes +no +no +Security Updates for the Robot Operating System ros-updates +yes +no +no +All Updates for the Robot Operating System """ @@ -537,7 +553,8 @@ Feature: Unattached status esm-infra +yes +yes +yes +Expanded Security Maintenance for Infrastructure fips +yes +yes +no +NIST-certified core packages fips-updates +yes +yes +no +NIST-certified core packages with priority security updates - livepatch +yes +yes +yes +Canonical Livepatch service + (landscape +(yes|no) +yes +no +Management and administration tool for Ubuntu)? + ?livepatch +yes +yes +yes +Canonical Livepatch service usg +yes +yes +no +Security compliance and audit tools """ @@ -579,7 +596,8 @@ Feature: Unattached status esm-apps +yes +no +no +Expanded Security Maintenance for Applications esm-infra +yes +yes +yes +Expanded Security Maintenance for Infrastructure fips +yes +yes +no +NIST-certified core packages - livepatch +yes +yes +yes +Canonical Livepatch service + (landscape +(yes|no) +yes +no +Management and administration tool for Ubuntu)? + ?livepatch +yes +yes +yes +Canonical Livepatch service """ Examples: ubuntu release diff --git a/help_data.yaml b/help_data.yaml index ae8559e5e2..61b81df466 100644 --- a/help_data.yaml +++ b/help_data.yaml @@ -62,6 +62,16 @@ fips-updates: for those modules that have been provided since their certification date. You can find out more at https://ubuntu.com/security/certifications#fips. +landscape: + help: | + Landscape Client can be installed on this machine and enrolled in + Canonical's Landscape SaaS: https://landscape.canonical.com + or a self-hosted Landscape: https://ubuntu.com/landscape/install + Landscape allows you to manage many machines as easily as one, + with an intuitive dashboard and API interface for automation, + hardening, auditing, and more. Find out more about Landscape at + https://ubuntu.com/landscape + livepatch: help: | Livepatch provides selected high and critical kernel CVE fixes and other diff --git a/tools/ua.bash b/tools/ua.bash index 4ac02bc1c2..8263f48f90 100644 --- a/tools/ua.bash +++ b/tools/ua.bash @@ -9,9 +9,9 @@ _ua_complete() prev_word="${COMP_WORDS[COMP_CWORD-1]}" if [ "$VERSION_ID" = "16.04" ] || [ "$VERSION_ID" == "18.04" ]; then - services="anbox-cloud cc-eal cis esm-apps esm-infra fips fips-updates livepatch realtime-kernel ros ros-updates" + services="anbox-cloud cc-eal cis esm-apps esm-infra fips fips-updates landscape livepatch realtime-kernel ros ros-updates" else - services="anbox-cloud cc-eal esm-apps esm-infra fips fips-updates livepatch realtime-kernel ros ros-updates usg" + services="anbox-cloud cc-eal esm-apps esm-infra fips fips-updates landscape livepatch realtime-kernel ros ros-updates usg" fi subcmds="--debug --help --version api attach auto-attach collect-logs config detach disable enable fix help refresh security-status status system version" diff --git a/uaclient/actions.py b/uaclient/actions.py index e4c139da9a..4270542551 100644 --- a/uaclient/actions.py +++ b/uaclient/actions.py @@ -133,7 +133,8 @@ def enable_entitlement_by_name( assume_yes: bool = False, allow_beta: bool = False, access_only: bool = False, - variant: str = "" + variant: str = "", + extra_args: Optional[List[str]] = None ): """ Constructs an entitlement based on the name provided. Passes kwargs onto @@ -150,6 +151,7 @@ def enable_entitlement_by_name( allow_beta=allow_beta, called_name=name, access_only=access_only, + extra_args=extra_args, ) return entitlement.enable() diff --git a/uaclient/cli.py b/uaclient/cli.py index 413e6eaa2d..1b9ec0da44 100644 --- a/uaclient/cli.py +++ b/uaclient/cli.py @@ -1310,6 +1310,7 @@ def action_enable(args, *, cfg, **kwargs): allow_beta=args.beta, access_only=access_only, variant=variant, + extra_args=kwargs.get("extra_args"), ) ua_status.status(cfg=cfg) # Update the status cache diff --git a/uaclient/entitlements/__init__.py b/uaclient/entitlements/__init__.py index e38181c4c4..eb89df8b54 100644 --- a/uaclient/entitlements/__init__.py +++ b/uaclient/entitlements/__init__.py @@ -10,6 +10,7 @@ from uaclient.entitlements.cc import CommonCriteriaEntitlement from uaclient.entitlements.cis import CISEntitlement from uaclient.entitlements.esm import ESMAppsEntitlement, ESMInfraEntitlement +from uaclient.entitlements.landscape import LandscapeEntitlement from uaclient.entitlements.livepatch import LivepatchEntitlement from uaclient.entitlements.realtime import RealtimeKernelEntitlement from uaclient.entitlements.ros import ROSEntitlement, ROSUpdatesEntitlement @@ -24,6 +25,7 @@ ESMInfraEntitlement, fips.FIPSEntitlement, fips.FIPSUpdatesEntitlement, + LandscapeEntitlement, LivepatchEntitlement, RealtimeKernelEntitlement, ROSEntitlement, diff --git a/uaclient/entitlements/base.py b/uaclient/entitlements/base.py index e553744a72..32eb629b00 100644 --- a/uaclient/entitlements/base.py +++ b/uaclient/entitlements/base.py @@ -288,6 +288,7 @@ def __init__( allow_beta: bool = False, called_name: str = "", access_only: bool = False, + extra_args: Optional[List[str]] = None, ) -> None: """Setup UAEntitlement instance @@ -299,6 +300,10 @@ def __init__( self.assume_yes = assume_yes self.allow_beta = allow_beta self.access_only = access_only + if extra_args is not None: + self.extra_args = extra_args + else: + self.extra_args = [] self._called_name = called_name self._valid_service = None # type: Optional[bool] diff --git a/uaclient/entitlements/landscape.py b/uaclient/entitlements/landscape.py new file mode 100644 index 0000000000..e09c9f52c5 --- /dev/null +++ b/uaclient/entitlements/landscape.py @@ -0,0 +1,141 @@ +import logging +import os +from typing import Any, Dict, Optional, Tuple + +from uaclient import apt, event_logger, exceptions, messages, system, util +from uaclient.entitlements.base import UAEntitlement +from uaclient.entitlements.entitlement_status import ApplicationStatus + +event = event_logger.get_event_logger() + +LANDSCAPE_CLIENT_PACKAGE_NAME = "landscape-client" +LANDSCAPE_CLIENT_CONFIG_PATH = "/etc/landscape/client.conf" +LANDSCAPE_CLIENT_CONFIG_PATH_DISABLE_BACKUP = ( + "/etc/landscape/client.conf.pro-disable-backup" +) + + +class LandscapeEntitlement(UAEntitlement): + name = "landscape" + title = "Landscape" + description = "Management and administration tool for Ubuntu" + help_doc_url = "https://ubuntu.com/landscape" + + def _perform_enable(self, silent: bool = False) -> bool: + cmd = ["landscape-config"] + self.extra_args + if self.assume_yes and "--silent" not in cmd: + cmd += ["--silent"] + + logging.debug(messages.EXECUTING_COMMAND.format(" ".join(cmd))) + event.info( + util.redact_sensitive_logs( + messages.EXECUTING_COMMAND.format(" ".join(cmd)) + ) + ) + try: + system.subp(cmd, pipe_stdouterr=self.assume_yes) + except exceptions.ProcessExecutionError as e: + if self.assume_yes: + err_msg = messages.LANDSCAPE_CONFIG_FAILED + event.error( + err_msg.msg, + err_msg.name, + service=self.name, + additional_info={ + "stdout": e.stdout.strip(), + "stderr": e.stderr.strip(), + }, + ) + event.info(e.stderr.strip()) + event.info( + messages.ENABLED_FAILED.format(title=self.title).msg + ) + return False + + if self.assume_yes: + # when silencing landscape-config, include a success message + # otherwise, let landscape-config say what happened + event.info(messages.ENABLED_TMPL.format(title=self.title)) + return True + + def _perform_disable(self, silent: bool = False) -> bool: + cmd = ["landscape-config", "--disable"] + event.info(messages.EXECUTING_COMMAND.format(" ".join(cmd))) + try: + system.subp(cmd) + except exceptions.ProcessExecutionError as e: + with util.disable_log_to_console(): + logging.error(e) + event.info(str(e).strip()) + event.warning(str(e), self.name) + + msg = messages.BACKING_UP_FILE.format( + original=LANDSCAPE_CLIENT_CONFIG_PATH, + backup=LANDSCAPE_CLIENT_CONFIG_PATH_DISABLE_BACKUP, + ) + logging.debug(msg) + event.info(msg) + try: + os.rename( + LANDSCAPE_CLIENT_CONFIG_PATH, + LANDSCAPE_CLIENT_CONFIG_PATH_DISABLE_BACKUP, + ) + except FileNotFoundError as e: + with util.disable_log_to_console(): + logging.error(e) + event.info(str(e)) + event.warning(str(e), self.name) + + return True + + def application_status( + self, + ) -> Tuple[ApplicationStatus, Optional[messages.NamedMessage]]: + if apt.is_installed(LANDSCAPE_CLIENT_PACKAGE_NAME): + return (ApplicationStatus.ENABLED, None) + else: + return ( + ApplicationStatus.DISABLED, + messages.LANDSCAPE_CLIENT_NOT_INSTALLED, + ) + + def enabled_warning_status( + self, + ) -> Tuple[bool, Optional[messages.NamedMessage]]: + if not os.path.exists(LANDSCAPE_CLIENT_CONFIG_PATH): + return ( + True, + messages.LANDSCAPE_NOT_CONFIGURED, + ) + + # This check wrongly gives warning when non-root + if util.we_are_currently_root(): + try: + system.subp( + ["landscape-config", "--is-registered", "--silent"] + ) + except exceptions.ProcessExecutionError: + return ( + True, + messages.LANDSCAPE_NOT_REGISTERED, + ) + + if not system.is_systemd_unit_active("landscape-client"): + return ( + True, + messages.LANDSCAPE_SERVICE_NOT_ACTIVE, + ) + + return False, None + + def process_contract_deltas( + self, + orig_access: Dict[str, Any], + deltas: Dict[str, Any], + allow_enable: bool = False, + ) -> bool: + # overriding allow_enable to always be False for this entitlement + # effectively prevents enableByDefault from ever happening + return super().process_contract_deltas( + orig_access, deltas, allow_enable=False + ) diff --git a/uaclient/entitlements/tests/test_landscape.py b/uaclient/entitlements/tests/test_landscape.py new file mode 100644 index 0000000000..2040d226fd --- /dev/null +++ b/uaclient/entitlements/tests/test_landscape.py @@ -0,0 +1,272 @@ +import mock +import pytest + +from uaclient import exceptions, messages +from uaclient.entitlements.entitlement_status import ApplicationStatus +from uaclient.entitlements.landscape import LandscapeEntitlement + + +class TestLandscapeEntitlement: + @pytest.mark.parametrize( + [ + "assume_yes", + "extra_args", + "subp_sideeffect", + "expected_subp_calls", + "expected_result", + ], + [ + ( + False, + None, + None, + [mock.call(["landscape-config"], pipe_stdouterr=False)], + True, + ), + ( + False, + ["extra"], + None, + [ + mock.call( + ["landscape-config", "extra"], pipe_stdouterr=False + ) + ], + True, + ), + ( + True, + None, + None, + [ + mock.call( + ["landscape-config", "--silent"], pipe_stdouterr=True + ) + ], + True, + ), + ( + True, + ["extra"], + None, + [ + mock.call( + ["landscape-config", "extra", "--silent"], + pipe_stdouterr=True, + ) + ], + True, + ), + ( + True, + ["--silent", "extra"], + None, + [ + mock.call( + ["landscape-config", "--silent", "extra"], + pipe_stdouterr=True, + ) + ], + True, + ), + ( + False, + None, + exceptions.ProcessExecutionError("test"), + [mock.call(["landscape-config"], pipe_stdouterr=False)], + False, + ), + ], + ) + @mock.patch("uaclient.system.subp") + def test_perform_enable( + self, + m_subp, + assume_yes, + extra_args, + subp_sideeffect, + expected_subp_calls, + expected_result, + FakeConfig, + ): + m_subp.side_effect = subp_sideeffect + landscape = LandscapeEntitlement( + FakeConfig(), assume_yes=assume_yes, extra_args=extra_args + ) + assert expected_result == landscape._perform_enable() + assert expected_subp_calls == m_subp.call_args_list + + @pytest.mark.parametrize( + [ + "subp_sideeffect", + "rename_sideeffect", + "expected_subp_calls", + "expected_rename_calls", + "expected_result", + ], + [ + ( + None, + None, + [mock.call(["landscape-config", "--disable"])], + [ + mock.call( + "/etc/landscape/client.conf", + "/etc/landscape/client.conf.pro-disable-backup", + ) + ], + True, + ), + ( + exceptions.ProcessExecutionError("test"), + None, + [mock.call(["landscape-config", "--disable"])], + [ + mock.call( + "/etc/landscape/client.conf", + "/etc/landscape/client.conf.pro-disable-backup", + ) + ], + True, + ), + ( + None, + FileNotFoundError(), + [mock.call(["landscape-config", "--disable"])], + [ + mock.call( + "/etc/landscape/client.conf", + "/etc/landscape/client.conf.pro-disable-backup", + ) + ], + True, + ), + ], + ) + @mock.patch("os.rename") + @mock.patch("uaclient.system.subp") + def test_perform_disable( + self, + m_subp, + m_rename, + subp_sideeffect, + rename_sideeffect, + expected_subp_calls, + expected_rename_calls, + expected_result, + FakeConfig, + ): + m_subp.side_effect = subp_sideeffect + m_rename.side_effect = rename_sideeffect + landscape = LandscapeEntitlement(FakeConfig()) + assert expected_result == landscape._perform_disable() + assert expected_subp_calls == m_subp.call_args_list + assert expected_rename_calls == m_rename.call_args_list + + @pytest.mark.parametrize( + [ + "is_installed", + "expected_is_installed_calls", + "expected_result", + ], + [ + ( + False, + [mock.call("landscape-client")], + ( + ApplicationStatus.DISABLED, + messages.LANDSCAPE_CLIENT_NOT_INSTALLED, + ), + ), + ( + True, + [mock.call("landscape-client")], + (ApplicationStatus.ENABLED, None), + ), + ], + ) + @mock.patch("uaclient.apt.is_installed") + def test_application_status( + self, + m_is_installed, + is_installed, + expected_is_installed_calls, + expected_result, + FakeConfig, + ): + m_is_installed.return_value = is_installed + landscape = LandscapeEntitlement(FakeConfig()) + assert expected_result == landscape.application_status() + assert expected_is_installed_calls == m_is_installed.call_args_list + + @pytest.mark.parametrize( + [ + "exists", + "we_are_currently_root", + "subp_sideeffect", + "unit_active", + "expected_subp_calls", + "expected_result", + ], + [ + ( + False, + None, + None, + None, + [], + (True, messages.LANDSCAPE_NOT_CONFIGURED), + ), + ( + True, + True, + exceptions.ProcessExecutionError("test"), + None, + [mock.call(mock.ANY)], + (True, messages.LANDSCAPE_NOT_REGISTERED), + ), + ( + True, + True, + None, + False, + [mock.call(mock.ANY)], + (True, messages.LANDSCAPE_SERVICE_NOT_ACTIVE), + ), + (True, True, None, True, [mock.call(mock.ANY)], (False, None)), + ( + True, + False, + None, + False, + [], + (True, messages.LANDSCAPE_SERVICE_NOT_ACTIVE), + ), + (True, False, None, True, [], (False, None)), + ], + ) + @mock.patch("uaclient.system.is_systemd_unit_active") + @mock.patch("uaclient.system.subp") + @mock.patch("uaclient.util.we_are_currently_root") + @mock.patch("os.path.exists") + def test_enabled_warning_status( + self, + m_exists, + m_we_are_currently_root, + m_subp, + m_is_systemd_unit_active, + exists, + we_are_currently_root, + subp_sideeffect, + unit_active, + expected_subp_calls, + expected_result, + FakeConfig, + ): + m_exists.return_value = exists + m_we_are_currently_root.return_value = we_are_currently_root + m_subp.side_effect = subp_sideeffect + m_is_systemd_unit_active.return_value = unit_active + landscape = LandscapeEntitlement(FakeConfig()) + assert expected_result == landscape.enabled_warning_status() + assert expected_subp_calls == m_subp.call_args_list diff --git a/uaclient/messages.py b/uaclient/messages.py index 7cb854b65a..1438c522f9 100644 --- a/uaclient/messages.py +++ b/uaclient/messages.py @@ -189,6 +189,7 @@ class TxtColor: DISABLE_FAILED_TMPL = "Could not disable {title}." ACCESS_ENABLED_TMPL = "{title} access enabled" ENABLED_TMPL = "{title} enabled" +DISABLED_TMPL = "{title} disabled" UNABLE_TO_DETERMINE_CLOUD_TYPE = ( """\ Unable to determine auto-attach platform support @@ -1408,3 +1409,35 @@ class TxtColor: PROXY_AUTH_FAIL = NamedMessage( "proxy-auth-fail", "Proxy authentication failed" ) + +EXECUTING_COMMAND = "Executing `{}`" +BACKING_UP_FILE = "Backing up {original} as {backup}" + +LANDSCAPE_CLIENT_NOT_INSTALLED = NamedMessage( + "landscape-client-not-installed", "lanscape-client is not installed" +) +LANDSCAPE_NOT_CONFIGURED = NamedMessage( + "landscape-not-configured", + """\ +Landscape is installed but not configured. +Run `sudo landscape-config` to set it up, or run `sudo pro disable landscape`\ +""", +) +LANDSCAPE_NOT_REGISTERED = NamedMessage( + "landscape-not-registered", + """\ +Landscape is installed and configured but not registered. +Run `sudo landscape-config` to register, or run `sudo pro disable landscape`\ +""", +) +LANDSCAPE_SERVICE_NOT_ACTIVE = NamedMessage( + "landscape-service-not-active", + """\ +Landscape is installed and configured and registered but not running. +Run `sudo landscape-config` to start it, or run `sudo pro disable landscape`\ +""", +) +LANDSCAPE_CONFIG_FAILED = NamedMessage( + "landscape-config-failed", + """landscape-config command failed""", +) diff --git a/uaclient/tests/test_cli_disable.py b/uaclient/tests/test_cli_disable.py index dc6a025c2a..0f8df1f497 100644 --- a/uaclient/tests/test_cli_disable.py +++ b/uaclient/tests/test_cli_disable.py @@ -40,8 +40,8 @@ def all_service_msg(FakeConfig): Arguments: service the name(s) of the Ubuntu Pro services to disable. One of: anbox-cloud, cc-eal, cis, esm-apps, esm-infra, - fips, fips-updates, livepatch, realtime-kernel, ros, - ros-updates + fips, fips-updates, landscape, livepatch, realtime- + kernel, ros, ros-updates Flags: -h, --help show this help message and exit diff --git a/uaclient/tests/test_cli_enable.py b/uaclient/tests/test_cli_enable.py index 808cec490a..e13df984de 100644 --- a/uaclient/tests/test_cli_enable.py +++ b/uaclient/tests/test_cli_enable.py @@ -21,8 +21,8 @@ Arguments: service the name(s) of the Ubuntu Pro services to enable. One of: anbox-cloud, cc-eal, cis, esm-apps, esm-infra, - fips, fips-updates, livepatch, realtime-kernel, ros, - ros-updates + fips, fips-updates, landscape, livepatch, realtime- + kernel, ros, ros-updates Flags: -h, --help show this help message and exit @@ -443,6 +443,7 @@ def test_assume_yes_passed_to_service_init( allow_beta=False, called_name="testitlement", access_only=False, + extra_args=None, ) ] == m_entitlement_cls.call_args_list @@ -527,6 +528,7 @@ def factory_side_effect(cfg, name, variant): allow_beta=False, called_name=m_ent_cls.name, access_only=False, + extra_args=None, ) ] == m_ent_cls.call_args_list @@ -668,6 +670,7 @@ def valid_services_side_effect(cfg, allow_beta, all_names=False): allow_beta=beta_flag, called_name=m_ent_cls.name, access_only=False, + extra_args=None, ) ] == m_ent_cls.call_args_list @@ -901,6 +904,7 @@ def test_entitlement_instantiated_and_enabled( allow_beta=allow_beta, called_name="testitlement", access_only=False, + extra_args=None, ) ] == m_entitlement_cls.call_args_list diff --git a/uaclient/tests/test_status.py b/uaclient/tests/test_status.py index ca093dfc72..b8518570ed 100644 --- a/uaclient/tests/test_status.py +++ b/uaclient/tests/test_status.py @@ -651,6 +651,8 @@ def test_cache_file_is_written_world_readable( ) @mock.patch(M_PATH + "livepatch.LivepatchEntitlement.user_facing_status") @mock.patch(M_PATH + "livepatch.LivepatchEntitlement.contract_status") + @mock.patch(M_PATH + "landscape.LandscapeEntitlement.user_facing_status") + @mock.patch(M_PATH + "landscape.LandscapeEntitlement.contract_status") @mock.patch(M_PATH + "esm.ESMAppsEntitlement.user_facing_status") @mock.patch(M_PATH + "esm.ESMAppsEntitlement.contract_status") @mock.patch(M_PATH + "repo.RepoEntitlement.user_facing_status") @@ -663,6 +665,8 @@ def test_attached_reports_contract_and_service_status( m_repo_uf_status, m_esm_contract_status, m_esm_uf_status, + m_landscape_contract_status, + m_landscape_uf_status, m_livepatch_contract_status, m_livepatch_uf_status, _m_livepatch_status, @@ -693,6 +697,11 @@ def test_attached_reports_contract_and_service_status( UserFacingStatus.INAPPLICABLE, messages.NamedMessage("test-code", "esm-apps details"), ) + m_landscape_contract_status.return_value = ContractStatus.ENTITLED + m_landscape_uf_status.return_value = ( + UserFacingStatus.INAPPLICABLE, + messages.NamedMessage("test-code", "landscape details"), + ) token = { "availableResources": all_resources_available, "machineTokenInfo": { @@ -758,6 +767,7 @@ def test_attached_reports_contract_and_service_status( ent_details = { "livepatch": "livepatch details", "esm-apps": "esm-apps details", + "landscape": "landscape details", } rt_variants = { @@ -842,6 +852,8 @@ def test_attached_reports_contract_and_service_status( "variants": variants, } ) + print("expected") + print(repr(expected)) with mock.patch( "uaclient.status._get_config_status" ) as m_get_cfg_status: diff --git a/uaclient/tests/test_util.py b/uaclient/tests/test_util.py index 3235e15540..08de7aa066 100644 --- a/uaclient/tests/test_util.py +++ b/uaclient/tests/test_util.py @@ -341,6 +341,42 @@ class TestRedactSensitiveLogs: "'magic_token=SEKRET'", "'magic_token='", ), + ( + "--account-name name --registration-key=reg-key --silent", + "--account-name name --registration-key= --silent", + ), + ( + '--account-name name --registration-key="reg key" --silent', + "--account-name name --registration-key= --silent", + ), + ( + "--account-name name --registration-key='reg key' --silent", + "--account-name name --registration-key= --silent", + ), + ( + "--account-name name --registration-key reg-key --silent", + "--account-name name --registration-key --silent", + ), + ( + '--account-name name --registration-key "reg key" --silent', + "--account-name name --registration-key --silent", + ), + ( + "--account-name name --registration-key 'reg key' --silent", + "--account-name name --registration-key --silent", + ), + ( + "--account-name name -p reg-key --silent", + "--account-name name -p --silent", + ), + ( + '--account-name name -p "reg key" --silent', + "--account-name name -p --silent", + ), + ( + "--account-name name -p 'reg key' --silent", + "--account-name name -p --silent", + ), ), ) def test_redact_all_matching_regexs(self, raw_log, expected): diff --git a/uaclient/util.py b/uaclient/util.py index c60f223789..822876cda9 100644 --- a/uaclient/util.py +++ b/uaclient/util.py @@ -300,6 +300,15 @@ def is_config_value_true(config: Dict[str, Any], path_to_value: str) -> bool: r"(\'token\': \')[^\']+", r"(\'userCode\': \')[^\']+", r"(\'magic_token=)[^\']+", + r"(--registration-key=\")[^\"]+", + r"(--registration-key=\')[^\']+", + r"(--registration-key=)[^ ]+", + r"(--registration-key \")[^\"]+", + r"(--registration-key \')[^\']+", + r"(--registration-key )[^\s]+", + r"(-p \")[^\"]+", + r"(-p \')[^\']+", + r"(-p )[^\s]+", ]