diff -Nru landscape-client-19.12/.github/workflows/ci.yml landscape-client-23.02/.github/workflows/ci.yml --- landscape-client-19.12/.github/workflows/ci.yml 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/.github/workflows/ci.yml 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,34 @@ +name: ci +on: [pull_request, workflow_dispatch] +jobs: + check3: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: ["ubuntu-22.04", "ubuntu-20.04"] + steps: + - uses: actions/checkout@v2 + - run: | + make depends + # -common seems a catch-22, but this is just a shortcut to + # initialize user and dirs, some used through tests. + sudo apt-get -y install landscape-common + - run: make check3 TRIAL=/usr/bin/trial3 + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: make depends + - run: make lint + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + make depends + # -common seems a catch-22, but this is just a shortcut to + # initialize user and dirs, some used through tests. + sudo apt-get -y install landscape-common + - run: make coverage TRIAL=/usr/bin/trial3 + - name: upload + uses: codecov/codecov-action@v1 diff -Nru landscape-client-19.12/.travis.yml landscape-client-23.02/.travis.yml --- landscape-client-19.12/.travis.yml 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/.travis.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,71 +0,0 @@ -dist: trusty -sudo: true -language: python -matrix: - include: - - env: TARGET=check2 IMAGE=ubuntu:xenial - script: - - sg lxd -c 'lxc exec testcontainer -- sh -c "sudo apt-get update && sudo apt-get -y install make python-distutils-extra python-mock python-twisted python-apt python-twisted-core python-pycurl python-netifaces"'; - # creates user and dirs, some used through tests - - sg lxd -c 'lxc exec testcontainer -- sh -c "sudo apt-get -y install landscape-common"'; - - sg lxd -c 'lxc exec testcontainer -- sh -c "sudo chown -R ubuntu:ubuntu /target"'; - - sg lxd -c 'lxc exec testcontainer -- sudo -i -u ubuntu sh -c "cd /target; make ${TARGET}" ubuntu'; - - env: TARGET=check3 IMAGE=ubuntu:bionic - script: - # bionic needs cgroupv2 not on this kernel, so nudge the system a bit - - sg lxd -c 'lxc exec testcontainer -- sh -c "dhclient eth0; cloud-init init"'; - - sg lxd -c 'lxc exec testcontainer -- sh -c "sudo apt-get update && sudo apt-get -y install make python3-distutils-extra python3-mock python3-twisted python3-apt python-twisted-core python3-pycurl python3-netifaces"'; - # creates user and dirs, some used through tests - - sg lxd -c 'lxc exec testcontainer -- sh -c "sudo apt-get -y install landscape-common"'; - - sg lxd -c 'lxc exec testcontainer -- sh -c "sudo chown -R ubuntu:ubuntu /target"'; - - sg lxd -c 'lxc exec testcontainer -- sudo -i -u ubuntu sh -c "cd /target; make ${TARGET}" ubuntu'; - - env: TARGET=check3 IMAGE=ubuntu-daily:focal - script: - # bionic needs cgroupv2 not on this kernel, so nudge the system a bit - - sg lxd -c 'lxc exec testcontainer -- sh -c "dhclient eth0; cloud-init init"'; - - sg lxd -c 'lxc exec testcontainer -- sh -c "sudo apt-get update && sudo apt-get -y install make python3-distutils-extra python3-mock python3-twisted python3-apt python-twisted-core python3-pycurl python3-netifaces net-tools"'; - # creates user and dirs, some used through tests - - sg lxd -c 'lxc exec testcontainer -- sh -c "sudo apt-get -y install landscape-common"'; - - sg lxd -c 'lxc exec testcontainer -- sh -c "sudo chown -R ubuntu:ubuntu /target"'; - - sg lxd -c 'lxc exec testcontainer -- sudo -i -u ubuntu sh -c "cd /target; make ${TARGET}" ubuntu'; - - python: 3.6 - env: TARGET=lint - install: - - pip install flake8 - - python: 3.4 - env: TARGET=coverage - before_script: - - python3 -m pip install -U coverage - - python3 -m pip install -U codecov - install: - - pip install flake8 - # These match "make depends" - - pip install twisted==16.4.0 mock==1.3.0 configobj==5.0.6 pycurl netifaces==0.10.4 - - pip install http://launchpad.net/python-distutils-extra/trunk/2.39/+download/python-distutils-extra-2.39.tar.gz - # build & install python-apt - - make -f Makefile.travis pipinstallpythonapt - script: - - make $TARGET - after_success: - - codecov -env: - global: - - TRIAL_ARGS=-j4 -before_script: - - if [ ! -z ${IMAGE} ]; then - sudo apt-get -y -t trusty-backports install lxd; - sudo lxd init --auto; - sudo usermod -a -G lxd travis; - sudo sed -e 's/LXD_IPV4_ADDR=""/LXD_IPV4_ADDR="10.0.8.1"/' -e 's/LXD_IPV4_NETMASK=""/LXD_IPV4_NETMASK="255.255.255.0"/' -e 's:LXD_IPV4_NETWORK="":LXD_IPV4_NETWORK="10.0.8.0/24":' -e 's/LXD_IPV4_DHCP_RANGE=""/LXD_IPV4_DHCP_RANGE="10.0.8.2,10.0.8.254"/' -e 's/LXD_IPV4_DHCP_MAX=""/LXD_IPV4_DHCP_MAX="250"/' -i /etc/default/lxd-bridge; - sudo /etc/init.d/lxd restart; - fi - - if [ ! -z ${IMAGE} ]; then - sg lxd -c 'lxc launch ${IMAGE} testcontainer -c security.privileged=true'; - echo "waiting for nic"; - for t in 1 2 5 8; do sg lxd -c 'lxc list' | grep 10.0.8 && break; sleep ${t}; done; - sg lxd -c 'lxc config device add testcontainer srcdir disk path=/target source=${PWD}'; - sg lxd -c 'lxc config device add testcontainer srcdir disk path=/target source=${PWD}'; - sg lxd -c 'lxc exec testcontainer -- sh -c "echo 1 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6"'; - fi -script: - - make $TARGET PYTHON=python$TRAVIS_PYTHON_VERSION; diff -Nru landscape-client-19.12/Makefile landscape-client-23.02/Makefile --- landscape-client-19.12/Makefile 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/Makefile 2023-02-07 18:55:50.000000000 +0000 @@ -2,7 +2,7 @@ TXT2MAN ?= txt2man PYTHON2 ?= python2 PYTHON3 ?= python3 -TRIAL ?= $(shell which trial) +TRIAL ?= -m twisted.trial TRIAL_ARGS ?= .PHONY: help @@ -10,16 +10,16 @@ @grep -h -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' .PHONY: depends -depends: depends2 depends3 ## Install py2 and py3 dependencies. - sudo apt -y install python3-flake8 python3-coverage +depends: depends3 ## py2 is deprecated + sudo apt-get -y install python3-flake8 python3-coverage .PHONY: depends2 depends2: - sudo apt -y install python-twisted-core python-distutils-extra python-mock python-configobj python-netifaces + sudo apt-get -y install python-twisted-core python-distutils-extra python-mock python-configobj python-netifaces python-pycurl .PHONY: depends3 depends3: - sudo apt -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces + sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl all: build @@ -52,9 +52,7 @@ .PHONY: coverage coverage: PYTHONPATH=$(PYTHONPATH):$(CURDIR) LC_ALL=C $(PYTHON3) -m coverage run $(TRIAL) --unclean-warnings landscape - -.PHONY: ci-check -ci-check: depends build check ## Install dependencies and run all the tests. + PYTHONPATH=$(PYTHONPATH):$(CURDIR) LC_ALL=C $(PYTHON3) -m coverage xml .PHONY: lint lint: diff -Nru landscape-client-19.12/Makefile.packaging landscape-client-23.02/Makefile.packaging --- landscape-client-19.12/Makefile.packaging 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/Makefile.packaging 2023-02-07 18:55:50.000000000 +0000 @@ -3,7 +3,7 @@ # Use := here, not =, it's really important, otherwise UPSTREAM_VERSION # will be updated behind your back with the current result of that # command everytime it is mentioned/used. -UPSTREAM_VERSION := $(shell python -c "from landscape import UPSTREAM_VERSION; print(UPSTREAM_VERSION)") +UPSTREAM_VERSION := $(shell python3 -c "from landscape import UPSTREAM_VERSION; print(UPSTREAM_VERSION)") CHANGELOG_VERSION := $(shell dpkg-parsechangelog | grep ^Version | cut -f 2 -d " " | cut -f 1 -d '-') GIT_HASH := $(shell git rev-parse --short HEAD) # We simulate a git "revno" for the sake of sortability. diff -Nru landscape-client-19.12/Makefile.travis landscape-client-23.02/Makefile.travis --- landscape-client-19.12/Makefile.travis 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/Makefile.travis 1970-01-01 00:00:00.000000000 +0000 @@ -1,40 +0,0 @@ -UBUNTU_RELEASE := $(shell lsb_release -cs) - -# We'd use the packaged version, but travis runs python in virtualenv, -# thus this pip workaround. -.PHONY: pipinstallpythonapt -pipinstallpythonapt: pipinstallpythonapt_deps - $(MAKE) -f $(lastword $(MAKEFILE_LIST)) pipinstallpythonapt_default || \ - $(MAKE) -f $(lastword $(MAKEFILE_LIST)) pipinstallpythonapt_src_$(UBUNTU_RELEASE) - -.PHONY: pipinstallpythonapt_deps -pipinstallpythonapt_deps: - pip install pyopenssl - pip install service_identity - sudo apt-get update - sudo apt-get -y build-dep python-apt python3-apt - sudo apt-get -y install libapt-pkg-dev - -.PHONY: pipinstallpythonapt_default -pipinstallpythonapt_default: - # See: https://code.launchpad.net/ubuntu/+source/python-apt - bzr branch lp:ubuntu/$(UBUNTU_RELEASE)/python-apt /tmp/python-apt - pip install /tmp/python-apt - -.PHONY: pipinstallpythonapt_src -pipinstallpythonapt_src: - wget -O /tmp/python-apt_$(PYAPT_VER).tar.xz https://launchpad.net/ubuntu/+archive/primary/+files/python-apt_$(PYAPT_VER).tar.xz - # pip2.7 does not support .xz - xz -dfk /tmp/python-apt_$(PYAPT_VER).tar.xz - pip install /tmp/python-apt_$(PYAPT_VER).tar - -.PHONY: pipinstallpythonapt_src_xenial -pipinstallpythonapt_src_xenial: PYAPT_VER = 1.1.0~beta1build1 -pipinstallpythonapt_src_xenial: pipinstallpythonapt_src - -# travis-ci nodes use a backported version of apt incompatible with -# the version of python-apt on trusty. -# This version matches the libapt-pkg on travis nodes so it compiles. -.PHONY: pipinstallpythonapt_src_trusty -pipinstallpythonapt_src_trusty: PYAPT_VER = 1.1.0~beta1build1 -pipinstallpythonapt_src_trusty: pipinstallpythonapt_src diff -Nru landscape-client-19.12/README landscape-client-23.02/README --- landscape-client-19.12/README 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/README 2023-02-07 18:55:50.000000000 +0000 @@ -1,45 +1,88 @@ -[![Build Status](https://travis-ci.org/CanonicalLtd/landscape-client.svg?branch=master)](https://travis-ci.org/CanonicalLtd/landscape-client) +[![Build Status](https://github.com/CanonicalLtd/landscape-client/actions/workflows/ci.yml/badge.svg)](https://github.com/CanonicalLtd/landscape-client/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/CanonicalLtd/landscape-client/branch/master/graph/badge.svg)](https://codecov.io/gh/CanonicalLtd/landscape-client) -== Non-root mode == +## Installation Instructions -The Landscape Client generally runs as a combination of the 'root' and -'landscape' users. It is possible to disable the administrative features of -Landscape and run only the monitoring parts of it without using the 'root' +Add our beta PPA to get the latest updates to the landscape-client package + +#### Add repo to an Ubuntu series +``` +sudo add-apt-repository ppa:landscape/self-hosted-beta +``` + +#### Add repo to a Debian based series that is not Ubuntu (experimental) + +``` +# 1. Install our signing key +gpg --keyserver keyserver.ubuntu.com --recv-keys 6e85a86e4652b4e6 +gpg --export 6e85a86e4652b4e6 | sudo tee -a /usr/share/keyrings/landscape-client-keyring.gpg > /dev/null + +# 2. Add repository +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/landscape-client-keyring.gpg] https://ppa.launchpadcontent.net/landscape/self-hosted-beta/ubuntu focal main" | sudo tee -a /etc/apt/sources.list.d/landscape-client.list +``` + +#### Install the package +``` +sudo apt update && sudo apt install landscape-client +``` + +## Non-root mode + +The Landscape Client generally runs as a combination of the `root` and +`landscape` users. It is possible to disable the administrative features of +Landscape and run only the monitoring parts of it without using the `root` user at all. If you wish to use the Landscape Client in this way, it's recommended that you perform these steps immediately after installing the landscape-client package. -Edit /etc/default/landscape-client and add the following lines: +Edit `/etc/default/landscape-client` and add the following lines: - RUN=1 - DAEMON_USER=landscape +``` +RUN=1 +DAEMON_USER=landscape +``` -Edit /etc/landscape/client.conf and add the following line: +Edit `/etc/landscape/client.conf` and add the following line: +``` +monitor_only = true +``` - monitor_only = true +## Running -Now you can run 'sudo landscape-config' as usual to complete the configuration -of your client and register with the Landscape service. +Now you can complete the configuration of your client and register with the +Landscape service. There are two ways to do this: +1. `sudo landscape-config` and answer interactive prompts to finalize your configuration +2. `sudo landscape-config --account-name standalone --url https:///message-system --ping-url http:///ping` if registering to a self-hosted Landscape instance. Replace `` with the hostname of your self-hosted Landscape instance. -== Developing == +## Developing To run the full test suite, run the following command: +``` make check +``` When you want to test the landscape client manually without management features, you can simply run: +``` $ ./scripts/landscape-client +``` -This defaults to the 'landscape-client.conf' configuration file. +This defaults to the `landscape-client.conf` configuration file. When you want to test management features manually, you'll need to run as root. -There's a configuration file 'root-client.conf' which specifies use of the +There's a configuration file `root-client.conf` which specifies use of the system bus. +``` $ sudo ./scripts/landscape-client -c root-client.conf +``` +Before opening a PR, make sure to run the full testsuite and lint +``` +make check3 +make lint +``` diff -Nru landscape-client-19.12/README.md landscape-client-23.02/README.md --- landscape-client-19.12/README.md 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/README.md 2023-02-07 18:55:50.000000000 +0000 @@ -1,45 +1,88 @@ -[![Build Status](https://travis-ci.org/CanonicalLtd/landscape-client.svg?branch=master)](https://travis-ci.org/CanonicalLtd/landscape-client) +[![Build Status](https://github.com/CanonicalLtd/landscape-client/actions/workflows/ci.yml/badge.svg)](https://github.com/CanonicalLtd/landscape-client/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/CanonicalLtd/landscape-client/branch/master/graph/badge.svg)](https://codecov.io/gh/CanonicalLtd/landscape-client) -== Non-root mode == +## Installation Instructions -The Landscape Client generally runs as a combination of the 'root' and -'landscape' users. It is possible to disable the administrative features of -Landscape and run only the monitoring parts of it without using the 'root' +Add our beta PPA to get the latest updates to the landscape-client package + +#### Add repo to an Ubuntu series +``` +sudo add-apt-repository ppa:landscape/self-hosted-beta +``` + +#### Add repo to a Debian based series that is not Ubuntu (experimental) + +``` +# 1. Install our signing key +gpg --keyserver keyserver.ubuntu.com --recv-keys 6e85a86e4652b4e6 +gpg --export 6e85a86e4652b4e6 | sudo tee -a /usr/share/keyrings/landscape-client-keyring.gpg > /dev/null + +# 2. Add repository +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/landscape-client-keyring.gpg] https://ppa.launchpadcontent.net/landscape/self-hosted-beta/ubuntu focal main" | sudo tee -a /etc/apt/sources.list.d/landscape-client.list +``` + +#### Install the package +``` +sudo apt update && sudo apt install landscape-client +``` + +## Non-root mode + +The Landscape Client generally runs as a combination of the `root` and +`landscape` users. It is possible to disable the administrative features of +Landscape and run only the monitoring parts of it without using the `root` user at all. If you wish to use the Landscape Client in this way, it's recommended that you perform these steps immediately after installing the landscape-client package. -Edit /etc/default/landscape-client and add the following lines: +Edit `/etc/default/landscape-client` and add the following lines: - RUN=1 - DAEMON_USER=landscape +``` +RUN=1 +DAEMON_USER=landscape +``` -Edit /etc/landscape/client.conf and add the following line: +Edit `/etc/landscape/client.conf` and add the following line: +``` +monitor_only = true +``` - monitor_only = true +## Running -Now you can run 'sudo landscape-config' as usual to complete the configuration -of your client and register with the Landscape service. +Now you can complete the configuration of your client and register with the +Landscape service. There are two ways to do this: +1. `sudo landscape-config` and answer interactive prompts to finalize your configuration +2. `sudo landscape-config --account-name standalone --url https:///message-system --ping-url http:///ping` if registering to a self-hosted Landscape instance. Replace `` with the hostname of your self-hosted Landscape instance. -== Developing == +## Developing To run the full test suite, run the following command: +``` make check +``` When you want to test the landscape client manually without management features, you can simply run: +``` $ ./scripts/landscape-client +``` -This defaults to the 'landscape-client.conf' configuration file. +This defaults to the `landscape-client.conf` configuration file. When you want to test management features manually, you'll need to run as root. -There's a configuration file 'root-client.conf' which specifies use of the +There's a configuration file `root-client.conf` which specifies use of the system bus. +``` $ sudo ./scripts/landscape-client -c root-client.conf +``` +Before opening a PR, make sure to run the full testsuite and lint +``` +make check3 +make lint +``` diff -Nru landscape-client-19.12/debian/changelog landscape-client-23.02/debian/changelog --- landscape-client-19.12/debian/changelog 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/changelog 2023-10-04 20:27:05.000000000 +0000 @@ -1,3 +1,39 @@ +landscape-client (23.02-0ubuntu1~22.04.1) jammy; urgency=medium + + * Backporting release 23.02 for SRU (LP: #2006402): + - Service is no longer stopped on upgrade (LP: #2027613) + + -- Mitch Burton Wed, 4 Oct 2023 13:27:05 -0700 + +landscape-client (23.02-0ubuntu1) lunar; urgency=medium + + * New upstream release 23.02: + - Preventing the generation of large messages and logs that can overwhelm + Landscape Server (LP: #1995775) + - Improved MOTD slowdown on machines with many tap network interfaces + (LP: #2006396) + - No longer using deprecated apt-key when storing trusted GPG keys + (LP: #1973202) + - Fixed issue recognising Parallels VMs as Virtual Machine clients + (LP: #1827909) + - Fixes for incorrect logfile rotation config (LP: #1968189) + - Client-side backoff handling to moderate traffic to Landscape Server + during high load (LP: #1947399) + - Avoid sending empty messages when catching up to expected next message + (LP: #1917540) + - --is-registered CLI option to quickly check if client is registered + (LP: #1912516) + - Can now report Ubuntu Pro attachment information if the version of + Landscape Server it is registered to supports this (LP: #2006401) + - Packages installed as dependencies as part of package profiles are now + appropriately autoremovable (LP: #1878957) + - Registration timeouts give an error instead of timing out (LP: #1889464) + - RHEV hypervisor VMs are now recognized as virtual machines (LP: #1884116) + - Doing a Landscape-driven release upgrade from a release running python 2 + to one running python 3 no longer hangs forever (LP: #1943291) + + -- Mitch Burton Wed, 08 Feb 2023 10:23:31 -0800 + landscape-client (19.12-0ubuntu13) jammy; urgency=medium * d/landscape-sysinfo.wrapper, d/landscape-common.postrm: avoid too diff -Nru landscape-client-19.12/debian/compat landscape-client-23.02/debian/compat --- landscape-client-19.12/debian/compat 2022-03-30 10:14:31.000000000 +0000 +++ landscape-client-23.02/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -12 diff -Nru landscape-client-19.12/debian/control landscape-client-23.02/debian/control --- landscape-client-19.12/debian/control 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/control 2023-02-08 18:23:31.000000000 +0000 @@ -3,7 +3,7 @@ Priority: optional Maintainer: Ubuntu Developers XSBC-Original-Maintainer: Landscape Team -Build-Depends: debhelper (>= 12), po-debconf, libdistro-info-perl, +Build-Depends: debhelper-compat (= 12), po-debconf, libdistro-info-perl, dh-python, python3-dev, python3-distutils-extra, lsb-release, gawk, net-tools, python3-apt, python3-twisted, python3-configobj diff -Nru landscape-client-19.12/debian/landscape-client.logrotate landscape-client-23.02/debian/landscape-client.logrotate --- landscape-client-19.12/debian/landscape-client.logrotate 2022-03-30 10:14:31.000000000 +0000 +++ landscape-client-23.02/debian/landscape-client.logrotate 2023-02-08 18:23:31.000000000 +0000 @@ -5,8 +5,9 @@ notifempty compress nocreate + sharedscripts postrotate - [ -f /var/run/landscape/landscape-client.pid ] && kill -s USR1 `cat /var/run/landscape/landscape-client.pid` > /dev/null 2>&1 || : + systemctl kill --signal=USR1 --kill-who=main landscape-client > /dev/null 2>&1 || : endscript } diff -Nru landscape-client-19.12/debian/landscape-client.postrm landscape-client-23.02/debian/landscape-client.postrm --- landscape-client-19.12/debian/landscape-client.postrm 2022-03-30 10:14:31.000000000 +0000 +++ landscape-client-23.02/debian/landscape-client.postrm 2023-02-08 18:23:31.000000000 +0000 @@ -18,6 +18,7 @@ purge) LOG_DIR=/var/log/landscape DATA_DIR=/var/lib/landscape + GPG_DIR=/etc/apt/trusted.gpg.d rm -f "/etc/default/landscape-client" @@ -28,6 +29,8 @@ rm -f "${LOG_DIR}/package-reporter.log"* rm -f "${LOG_DIR}/package-changer.log"* + rm -f "${GPG_DIR}/landscape-server"*.asc + rm -rf "${DATA_DIR}/client" rm -rf "${DATA_DIR}/.gnupg" diff -Nru landscape-client-19.12/debian/landscape-client.service landscape-client-23.02/debian/landscape-client.service --- landscape-client-19.12/debian/landscape-client.service 2022-03-30 10:14:31.000000000 +0000 +++ landscape-client-23.02/debian/landscape-client.service 2023-09-28 23:14:53.000000000 +0000 @@ -11,6 +11,7 @@ [Service] Type=simple Group=landscape +ExecCondition=/usr/bin/landscape-config --is-registered ExecStart=/usr/bin/landscape-client # Don't kill cgroup as child dpkg may restart the service during an upgrade. KillMode=process diff -Nru landscape-client-19.12/debian/patches/0001-Handle-EINVAL-error-of-SIOCETHTOOL-ioctl.patch landscape-client-23.02/debian/patches/0001-Handle-EINVAL-error-of-SIOCETHTOOL-ioctl.patch --- landscape-client-19.12/debian/patches/0001-Handle-EINVAL-error-of-SIOCETHTOOL-ioctl.patch 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/patches/0001-Handle-EINVAL-error-of-SIOCETHTOOL-ioctl.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,28 +0,0 @@ -From 97f74e0354d60b78a75fd7ed96c3f7fad6cd35ef Mon Sep 17 00:00:00 2001 -From: Balint Reczey -Date: Sat, 7 Dec 2019 10:11:09 +0100 -Subject: [PATCH] Handle EINVAL error of SIOCETHTOOL ioctl - -The error occurs on WSL (1) and causes an error showing up in MOTD. - -LP: #1855522 ---- - landscape/lib/network.py | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) - ---- a/landscape/lib/network.py -+++ b/landscape/lib/network.py -@@ -248,11 +248,11 @@ - fcntl.ioctl(sock, SIOCETHTOOL, packed) # Status ioctl() call - res = status_cmd.tostring() - speed, duplex = struct.unpack("12xHB28x", res) -- except IOError as e: -+ except (IOError, OSError) as e: - if e.errno == errno.EPERM: - logging.warn("Could not determine network interface speed, " - "operation not permitted.") -- elif e.errno != errno.EOPNOTSUPP: -+ elif e.errno != errno.EOPNOTSUPP and e.errno != errno.EINVAL: - raise e - speed = -1 - duplex = False diff -Nru landscape-client-19.12/debian/patches/0001-start-service-during-config.patch landscape-client-23.02/debian/patches/0001-start-service-during-config.patch --- landscape-client-19.12/debian/patches/0001-start-service-during-config.patch 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/debian/patches/0001-start-service-during-config.patch 2023-10-03 23:11:58.000000000 +0000 @@ -0,0 +1,80 @@ +Description: Allow landscape-config to start landscape-client systemd service +Author: Mitch Burton +Origin: upstream +Forwarded: https://github.com/canonical/landscape-client/pull/185 +Applied-Upstream: https://github.com/canonical/landscape-client/pull/185 +Reviewed-by: Kevin Nasto +Last-Update: 2023-10-03 +--- +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ +--- a/landscape/client/configuration.py ++++ b/landscape/client/configuration.py +@@ -540,6 +540,7 @@ + + sysvconfig = SysVConfig() + if not config.no_start: ++ set_secure_id(config, "registering") + if config.silent: + setup_init_script_and_start_client() + elif not sysvconfig.is_configured_to_run(): +@@ -710,16 +711,32 @@ + # Results will be things like "success" or "ssl-error". + result = results[0] + +- if isinstance(result, SystemExit): +- raise result +- + # If there was an error and the caller requested that errors be reported + # to the on_error callable, then do so. + if result != "success" and on_error is not None: + on_error(1) ++ ++ if isinstance(result, SystemExit): ++ raise result ++ + return result + + ++def set_secure_id(config, new_id): ++ """Persists a secure id in the identity data file. This is used to indicate ++ whether we are currently in the process of registering. ++ """ ++ persist = Persist( ++ filename=os.path.join( ++ config.data_path, ++ f"{BrokerService.service_name}.bpickle", ++ ), ++ ) ++ identity = Identity(config, persist) ++ identity.secure_id = new_id ++ persist.save() ++ ++ + def report_registration_outcome(what_happened, print=print): + """Report the registration interaction outcome to the user in human-readable + form. +@@ -834,7 +851,11 @@ + # Attempt to register the client. + reactor = LandscapeReactor() + if config.silent: +- result = register(config, reactor) ++ result = register( ++ config, ++ reactor, ++ on_error=lambda _: set_secure_id(config, None), ++ ) + report_registration_outcome(result, print=print) + sys.exit(determine_exit_code(result)) + else: +@@ -843,6 +864,10 @@ + "\nRequest a new registration for this computer now?", + default=default_answer) + if answer: +- result = register(config, reactor) ++ result = register( ++ config, ++ reactor, ++ on_error=lambda _: set_secure_id(config, None), ++ ) + report_registration_outcome(result, print=print) + sys.exit(determine_exit_code(result)) diff -Nru landscape-client-19.12/debian/patches/0002-lp1870087-stale-locks.patch landscape-client-23.02/debian/patches/0002-lp1870087-stale-locks.patch --- landscape-client-19.12/debian/patches/0002-lp1870087-stale-locks.patch 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/patches/0002-lp1870087-stale-locks.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,210 +0,0 @@ -Description: Clean up stale lock files - Rework lockfile to look for process names. -Author: Simon Poirier -Origin: upstream, https://github.com/CanonicalLtd/landscape-client/commit/2450eacd331097431122b9861613b8a03e5f74d9 -Bug-Ubuntu: https://bugs.launchpad.net/bugs/1870087 - -Index: landscape-client-19.12/landscape/client/lockfile.py -=================================================================== ---- /dev/null -+++ landscape-client-19.12/landscape/client/lockfile.py -@@ -0,0 +1,52 @@ -+import os -+ -+from twisted.python import lockfile -+ -+ -+def patch_lockfile(): -+ if lockfile.FilesystemLock is PatchedFilesystemLock: -+ return -+ lockfile.FilesystemLock = PatchedFilesystemLock -+ -+ -+class PatchedFilesystemLock(lockfile.FilesystemLock): -+ """ -+ Patched Twisted's FilesystemLock.lock to handle PermissionError -+ when trying to lock. -+ """ -+ -+ def lock(self): -+ # XXX Twisted assumes PIDs don't get reused, which is incorrect. -+ # As such, we pre-check that any existing lock file isn't -+ # associated to a live process, and that any associated -+ # process is from landscape. Otherwise, clean up the lock file, -+ # considering it to be locked to a recycled PID. -+ # -+ # Although looking for the process name may seem fragile, it's the -+ # most acurate info we have since: -+ # * some process run as root, so the UID is not a reference -+ # * process may not be spawned by systemd, so cgroups are not reliable -+ # * python executable is not a reference -+ clean = True -+ try: -+ pid = os.readlink(self.name) -+ ps_name = get_process_name(int(pid)) -+ if not ps_name.startswith("landscape"): -+ os.remove(self.name) -+ clean = False -+ except Exception: -+ # We can't figure the lock state, let FilesystemLock figure it -+ # out normally. -+ pass -+ -+ result = super(PatchedFilesystemLock, self).lock() -+ self.clean = self.clean and clean -+ return result -+ -+ -+def get_process_name(pid): -+ """Return a process name from a pid.""" -+ stat_path = "/proc/{}/stat".format(pid) -+ with open(stat_path) as stat_file: -+ stat = stat_file.read() -+ return stat.partition("(")[2].rpartition(")")[0] -Index: landscape-client-19.12/landscape/client/reactor.py -=================================================================== ---- landscape-client-19.12.orig/landscape/client/reactor.py -+++ landscape-client-19.12/landscape/client/reactor.py -@@ -2,6 +2,9 @@ - Extend the regular Twisted reactor with event-handling features. - """ - from landscape.lib.reactor import EventHandlingReactor -+from landscape.client.lockfile import patch_lockfile -+ -+patch_lockfile() - - - class LandscapeReactor(EventHandlingReactor): -Index: landscape-client-19.12/landscape/client/tests/test_amp.py -=================================================================== ---- landscape-client-19.12.orig/landscape/client/tests/test_amp.py -+++ landscape-client-19.12/landscape/client/tests/test_amp.py -@@ -1,9 +1,17 @@ --from twisted.internet.error import ConnectError -+import os -+import errno -+import subprocess -+import textwrap -+ -+import mock -+ -+from twisted.internet.error import ConnectError, CannotListenError - from twisted.internet.task import Clock - - from landscape.client.tests.helpers import LandscapeTest - from landscape.client.deployment import Configuration - from landscape.client.amp import ComponentPublisher, ComponentConnector, remote -+from landscape.client.reactor import LandscapeReactor - from landscape.lib.amp import MethodCallError - from landscape.lib.testing import FakeReactor - -@@ -159,3 +167,83 @@ class ComponentConnectorTest(LandscapeTe - effectively a no-op. - """ - self.connector.disconnect() -+ -+ @mock.patch("twisted.python.lockfile.kill") -+ def test_stale_locks_with_dead_pid(self, mock_kill): -+ """Publisher starts with stale lock.""" -+ mock_kill.side_effect = [ -+ OSError(errno.ESRCH, "No such process")] -+ sock_path = os.path.join(self.config.sockets_path, u"test.sock") -+ lock_path = u"{}.lock".format(sock_path) -+ # fake a PID which does not exist -+ os.symlink("-1", lock_path) -+ -+ component = TestComponent() -+ # Test the actual Unix reactor implementation. Fakes won't do. -+ reactor = LandscapeReactor() -+ publisher = ComponentPublisher(component, reactor, self.config) -+ -+ # Shouldn't raise the exception. -+ publisher.start() -+ -+ # ensure stale lock was replaced -+ self.assertNotEqual("-1", os.readlink(lock_path)) -+ mock_kill.assert_called_with(-1, 0) -+ -+ publisher.stop() -+ reactor._cleanup() -+ -+ @mock.patch("twisted.python.lockfile.kill") -+ def test_stale_locks_recycled_pid(self, mock_kill): -+ """Publisher starts with stale lock pointing to recycled process.""" -+ mock_kill.side_effect = [ -+ OSError(errno.EPERM, "Operation not permitted")] -+ sock_path = os.path.join(self.config.sockets_path, u"test.sock") -+ lock_path = u"{}.lock".format(sock_path) -+ # fake a PID recycled by a known process which isn't landscape (init) -+ os.symlink("1", lock_path) -+ -+ component = TestComponent() -+ # Test the actual Unix reactor implementation. Fakes won't do. -+ reactor = LandscapeReactor() -+ publisher = ComponentPublisher(component, reactor, self.config) -+ -+ # Shouldn't raise the exception. -+ publisher.start() -+ -+ # ensure stale lock was replaced -+ self.assertNotEqual("1", os.readlink(lock_path)) -+ mock_kill.assert_not_called() -+ self.assertFalse(publisher._port.lockFile.clean) -+ -+ publisher.stop() -+ reactor._cleanup() -+ -+ @mock.patch("twisted.python.lockfile.kill") -+ def test_with_valid_lock(self, mock_kill): -+ """Publisher raises lock error if a valid lock is held.""" -+ sock_path = os.path.join(self.config.sockets_path, u"test.sock") -+ lock_path = u"{}.lock".format(sock_path) -+ # fake a landscape process -+ app = self.makeFile(textwrap.dedent("""\ -+ #!/usr/bin/python3 -+ import time -+ time.sleep(10) -+ """), basename="landscape-manager") -+ os.chmod(app, 0o755) -+ call = subprocess.Popen([app]) -+ self.addCleanup(call.terminate) -+ os.symlink(str(call.pid), lock_path) -+ -+ component = TestComponent() -+ # Test the actual Unix reactor implementation. Fakes won't do. -+ reactor = LandscapeReactor() -+ publisher = ComponentPublisher(component, reactor, self.config) -+ -+ with self.assertRaises(CannotListenError): -+ publisher.start() -+ -+ # ensure lock was not replaced -+ self.assertEqual(str(call.pid), os.readlink(lock_path)) -+ mock_kill.assert_called_with(call.pid, 0) -+ reactor._cleanup() -Index: landscape-client-19.12/landscape/client/tests/test_lockfile.py -=================================================================== ---- /dev/null -+++ landscape-client-19.12/landscape/client/tests/test_lockfile.py -@@ -0,0 +1,21 @@ -+import os -+import subprocess -+import textwrap -+ -+from landscape.client import lockfile -+from landscape.client.tests.helpers import LandscapeTest -+ -+ -+class LockFileTest(LandscapeTest): -+ -+ def test_read_process_name(self): -+ app = self.makeFile(textwrap.dedent("""\ -+ #!/usr/bin/python3 -+ import time -+ time.sleep(10) -+ """), basename="my_fancy_app") -+ os.chmod(app, 0o755) -+ call = subprocess.Popen([app]) -+ self.addCleanup(call.terminate) -+ proc_name = lockfile.get_process_name(call.pid) -+ self.assertEqual("my_fancy_app", proc_name) diff -Nru landscape-client-19.12/debian/patches/0003-clean-publisher-shutdown.patch landscape-client-23.02/debian/patches/0003-clean-publisher-shutdown.patch --- landscape-client-19.12/debian/patches/0003-clean-publisher-shutdown.patch 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/patches/0003-clean-publisher-shutdown.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,49 +0,0 @@ -Description: Return missing deferred on service shutdown. - This should allow services to let their publisher shut down and clean - after itself, thus avoiding stale locks and sockets. -Author: Simon Poirier -Origin: upstream, https://github.com/CanonicalLtd/landscape-client/commit/8a617a1aafbbe9d972410707563710c3cbedd8e7 -Bug-Ubuntu: https://bugs.launchpad.net/bugs/1870087 ---- a/landscape/client/broker/service.py -+++ b/landscape/client/broker/service.py -@@ -82,10 +82,11 @@ - - def stopService(self): - """Stop the broker.""" -- self.publisher.stop() -+ deferred = self.publisher.stop() - self.exchanger.stop() - self.pinger.stop() - super(BrokerService, self).stopService() -+ return deferred - - - def run(args): ---- a/landscape/client/manager/service.py -+++ b/landscape/client/manager/service.py -@@ -54,8 +54,9 @@ - def stopService(self): - """Stop the manager and close the connection with the broker.""" - self.connector.disconnect() -- self.publisher.stop() -+ deferred = self.publisher.stop() - super(ManagerService, self).stopService() -+ return deferred - - - def run(args): ---- a/landscape/client/monitor/service.py -+++ b/landscape/client/monitor/service.py -@@ -56,10 +56,11 @@ - The monitor is flushed to ensure that things like persist databases - get saved to disk. - """ -- self.publisher.stop() -+ deferred = self.publisher.stop() - self.monitor.flush() - self.connector.disconnect() - super(MonitorService, self).stopService() -+ return deferred - - - def run(args): diff -Nru landscape-client-19.12/debian/patches/1962539_twisted_py3.patch landscape-client-23.02/debian/patches/1962539_twisted_py3.patch --- landscape-client-19.12/debian/patches/1962539_twisted_py3.patch 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/patches/1962539_twisted_py3.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,156 +0,0 @@ -Description: Fix obsolete imports on jammy. - * replace deprecated twisted _PY3 - * replace legacy types which are gone (unicode, long) -Author: Simon Poirier -Origin: backport, https://github.com/CanonicalLtd/landscape-client/commit/80d3da45ca8773d3eee262d4de5dc6e6b8d99121 -Bug-Ubuntu: https://bugs.launchpad.net/bugs/1962539 -Last-Update: 2022-03-03 - ---- a/landscape/client/broker/exchange.py -+++ b/landscape/client/broker/exchange.py -@@ -347,7 +347,7 @@ - from landscape.lib.hashlib import md5 - - from twisted.internet.defer import Deferred, succeed --from twisted.python.compat import _PY3 -+from landscape.lib.compat import _PY3 - - from landscape.lib.fetch import HTTPCodeError, PyCurlError - from landscape.lib.format import format_delta ---- a/landscape/client/broker/tests/test_registration.py -+++ b/landscape/client/broker/tests/test_registration.py -@@ -3,7 +3,7 @@ - import socket - import mock - --from twisted.python.compat import _PY3 -+from landscape.lib.compat import _PY3 - - from landscape.client.broker.registration import RegistrationError, Identity - from landscape.client.tests.helpers import LandscapeTest ---- a/landscape/client/broker/transport.py -+++ b/landscape/client/broker/transport.py -@@ -6,7 +6,7 @@ - - import pycurl - --from twisted.python.compat import unicode, _PY3 -+from landscape.lib.compat import unicode, _PY3 - - from landscape.lib import bpickle - from landscape.lib.fetch import fetch ---- a/landscape/client/manager/keystonetoken.py -+++ b/landscape/client/manager/keystonetoken.py -@@ -1,7 +1,7 @@ - import os - import logging - --from twisted.python.compat import _PY3 -+from landscape.lib.compat import _PY3 - - from landscape.lib.compat import ConfigParser, NoOptionError - from landscape.client.monitor.plugin import DataWatcher ---- a/landscape/client/user/provider.py -+++ b/landscape/client/user/provider.py -@@ -4,7 +4,7 @@ - import logging - import subprocess - --from twisted.python.compat import _PY3 -+from landscape.lib.compat import _PY3 - - - class UserManagementError(Exception): ---- a/landscape/lib/apt/package/skeleton.py -+++ b/landscape/lib/apt/package/skeleton.py -@@ -1,9 +1,8 @@ -+from landscape.lib.compat import unicode, _PY3 - from landscape.lib.hashlib import sha1 - - import apt_pkg - --from twisted.python.compat import unicode, _PY3 -- - - PACKAGE = 1 << 0 - PROVIDES = 1 << 1 ---- a/landscape/lib/bpickle.py -+++ b/landscape/lib/bpickle.py -@@ -32,7 +32,7 @@ - wire compatible and behave the same way (bugs notwithstanding). - """ - --from twisted.python.compat import _PY3 -+from landscape.lib.compat import long, _PY3 - - dumps_table = {} - loads_table = {} ---- a/landscape/lib/compat.py -+++ b/landscape/lib/compat.py -@@ -1,6 +1,6 @@ - # flake8: noqa - --from twisted.python.compat import _PY3 -+_PY3 = str != bytes - - - if _PY3: -@@ -13,6 +13,8 @@ - from io import StringIO - stringio = cstringio = StringIO - from builtins import input -+ unicode = str -+ long = int - - else: - import cPickle -@@ -24,3 +26,5 @@ - stringio = StringIO - from cStringIO import StringIO as cstringio - input = raw_input -+ long = long -+ unicode = unicode ---- a/landscape/lib/disk.py -+++ b/landscape/lib/disk.py -@@ -4,7 +4,7 @@ - import re - import codecs - --from twisted.python.compat import _PY3 -+from landscape.lib.compat import _PY3 - - - # List of filesystem types authorized when generating disk use statistics. ---- a/landscape/lib/network.py -+++ b/landscape/lib/network.py -@@ -11,7 +11,7 @@ - import logging - - import netifaces --from twisted.python.compat import long, _PY3 -+from landscape.lib.compat import long, _PY3 - - __all__ = ["get_active_device_info", "get_network_traffic"] - ---- a/landscape/lib/testing.py -+++ b/landscape/lib/testing.py -@@ -15,7 +15,7 @@ - from logging import Handler, ERROR, Formatter - from twisted.trial.unittest import TestCase - from twisted.python.compat import StringType as basestring --from twisted.python.compat import _PY3 -+from landscape.lib.compat import _PY3 - from twisted.python.failure import Failure - from twisted.internet.defer import Deferred - from twisted.internet.error import ConnectError ---- a/landscape/lib/user.py -+++ b/landscape/lib/user.py -@@ -1,7 +1,7 @@ - import os.path - import pwd - --from twisted.python.compat import _PY3 -+from landscape.lib.compat import _PY3 - - from landscape.lib.encoding import encode_if_needed - diff -Nru landscape-client-19.12/debian/patches/lp1903776-release-upgrade.patch landscape-client-23.02/debian/patches/lp1903776-release-upgrade.patch --- landscape-client-19.12/debian/patches/lp1903776-release-upgrade.patch 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/patches/lp1903776-release-upgrade.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,141 +0,0 @@ -Description: Use /etc/apt/trusted.gpg.d for validating upgrade-tool signature. -Author: Simon Poirier -Origin: upstream, https://github.com/CanonicalLtd/landscape-client/commit/bcbe04db6ca45c7a0afac171e85a9c88c19253ae -Bug-Ubuntu: https://bugs.launchpad.net/bugs/1903776 - -diff --git a/landscape/lib/gpg.py b/landscape/lib/gpg.py -index 66400d18..58218c54 100644 ---- a/landscape/lib/gpg.py -+++ b/landscape/lib/gpg.py -@@ -1,6 +1,9 @@ -+import itertools - import shutil - import tempfile - -+from glob import glob -+ - from twisted.internet.utils import getProcessOutputAndValue - - -@@ -8,14 +11,15 @@ class InvalidGPGSignature(Exception): - """Raised when the gpg signature for a given file is invalid.""" - - --def gpg_verify(filename, signature, gpg="/usr/bin/gpg"): -+def gpg_verify(filename, signature, gpg="/usr/bin/gpg", apt_dir="/etc/apt"): - """Verify the GPG signature of a file. - - @param filename: Path to the file to verify the signature against. - @param signature: Path to signature to use. - @param gpg: Optionally, path to the GPG binary to use. -+ @param apt_dir: Optionally, path to apt trusted keyring. - @return: a C{Deferred} resulting in C{True} if the signature is -- valid, C{False} otherwise. -+ valid, C{False} otherwise. - """ - - def remove_gpg_home(ignored): -@@ -32,9 +36,17 @@ def check_gpg_exit_code(args): - "code='%d')" % (gpg, out, err, code)) - - gpg_home = tempfile.mkdtemp() -- args = ("--no-options", "--homedir", gpg_home, "--no-default-keyring", -- "--ignore-time-conflict", "--keyring", "/etc/apt/trusted.gpg", -- "--verify", signature, filename) -+ keyrings = tuple(itertools.chain(*[ -+ ("--keyring", keyring) -+ for keyring in sorted( -+ glob("{}/trusted.gpg".format(apt_dir)) + -+ glob("{}/trusted.gpg.d/*.gpg".format(apt_dir)) -+ ) -+ ])) -+ args = ( -+ "--no-options", "--homedir", gpg_home, "--no-default-keyring", -+ "--ignore-time-conflict" -+ ) + keyrings + ("--verify", signature, filename) - - result = getProcessOutputAndValue(gpg, args=args) - result.addBoth(remove_gpg_home) -diff --git a/landscape/lib/tests/test_gpg.py b/landscape/lib/tests/test_gpg.py -index e6165a26..4c84e008 100644 ---- a/landscape/lib/tests/test_gpg.py -+++ b/landscape/lib/tests/test_gpg.py -@@ -1,9 +1,10 @@ - import mock - import os -+import textwrap - import unittest - - from twisted.internet import reactor --from twisted.internet.defer import Deferred -+from twisted.internet.defer import Deferred, inlineCallbacks - - from landscape.lib import testing - from landscape.lib.gpg import gpg_verify -@@ -16,6 +17,8 @@ def test_gpg_verify(self): - L{gpg_verify} runs the given gpg binary and returns C{True} if the - provided signature is valid. - """ -+ aptdir = self.makeDir() -+ os.mknod("{}/trusted.gpg".format(aptdir)) - gpg_options = self.makeFile() - gpg = self.makeFile("#!/bin/sh\n" - "touch $3/trustdb.gpg\n" -@@ -27,14 +30,15 @@ def test_gpg_verify(self): - @mock.patch("tempfile.mkdtemp") - def do_test(mkdtemp_mock): - mkdtemp_mock.return_value = gpg_home -- result = gpg_verify("/some/file", "/some/signature", gpg=gpg) -+ result = gpg_verify( -+ "/some/file", "/some/signature", gpg=gpg, apt_dir=aptdir) - - def check_result(ignored): - self.assertEqual( - open(gpg_options).read(), - "--no-options --homedir %s --no-default-keyring " -- "--ignore-time-conflict --keyring /etc/apt/trusted.gpg " -- "--verify /some/signature /some/file" % gpg_home) -+ "--ignore-time-conflict --keyring %s/trusted.gpg " -+ "--verify /some/signature /some/file" % (gpg_home, aptdir)) - self.assertFalse(os.path.exists(gpg_home)) - - result.addCallback(check_result) -@@ -70,3 +74,38 @@ def check_failure(failure): - - reactor.callWhenRunning(do_test) - return deferred -+ -+ @inlineCallbacks -+ def test_gpg_verify_trusted_dir(self): -+ """ -+ gpg_verify uses keys from the trusted.gpg.d if such a folder exists. -+ """ -+ apt_dir = self.makeDir() -+ os.mkdir("{}/trusted.gpg.d".format(apt_dir)) -+ os.mknod("{}/trusted.gpg.d/foo.gpg".format(apt_dir)) -+ os.mknod("{}/trusted.gpg.d/baz.gpg".format(apt_dir)) -+ os.mknod("{}/trusted.gpg.d/bad.gpg~".format(apt_dir)) -+ -+ gpg_call = self.makeFile() -+ fake_gpg = self.makeFile(textwrap.dedent("""\ -+ #!/bin/sh -+ touch $3/trustdb.gpg -+ echo -n $@ > {} -+ """).format(gpg_call)) -+ os.chmod(fake_gpg, 0o755) -+ gpg_home = self.makeDir() -+ -+ with mock.patch("tempfile.mkdtemp", return_value=gpg_home): -+ yield gpg_verify( -+ "/some/file", "/some/signature", gpg=fake_gpg, apt_dir=apt_dir) -+ -+ expected = ( -+ "--no-options --homedir {gpg_home} --no-default-keyring " -+ "--ignore-time-conflict " -+ "--keyring {apt_dir}/trusted.gpg.d/baz.gpg " -+ "--keyring {apt_dir}/trusted.gpg.d/foo.gpg " -+ "--verify /some/signature /some/file" -+ ).format(gpg_home=gpg_home, apt_dir=apt_dir) -+ with open(gpg_call) as call: -+ self.assertEqual(expected, call.read()) -+ self.assertFalse(os.path.exists(gpg_home)) diff -Nru landscape-client-19.12/debian/patches/py3.9.patch landscape-client-23.02/debian/patches/py3.9.patch --- landscape-client-19.12/debian/patches/py3.9.patch 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/patches/py3.9.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,65 +0,0 @@ -Description: Switch from depreated base64.decodestring to - base64.decodebytes to fix FTBFS with python3.9. -Author: Dimitri John Ledkov - - ---- landscape-client-19.12.orig/landscape/client/configuration.py -+++ landscape-client-19.12/landscape/client/configuration.py -@@ -513,7 +513,7 @@ def decode_base64_ssl_public_certificate - # WARNING: ssl_public_certificate is misnamed, it's not the key of the - # certificate, but the actual certificate itself. - if config.ssl_public_key and config.ssl_public_key.startswith("base64:"): -- decoded_cert = base64.decodestring( -+ decoded_cert = base64.decodebytes( - config.ssl_public_key[7:].encode("ascii")) - config.ssl_public_key = store_public_key_data( - config, decoded_cert) ---- landscape-client-19.12.orig/landscape/client/package/changer.py -+++ landscape-client-19.12/landscape/client/package/changer.py -@@ -173,7 +173,7 @@ class PackageChanger(PackageTaskHandler) - hash_ids = {} - for hash, id, deb in binaries: - create_binary_file(os.path.join(binaries_path, "%d.deb" % id), -- base64.decodestring(deb)) -+ base64.decodebytes(deb)) - hash_ids[hash] = id - self._store.set_hash_ids(hash_ids) - self._facade.add_channel_deb_dir(binaries_path) ---- landscape-client-19.12.orig/landscape/client/package/tests/test_changer.py -+++ landscape-client-19.12/landscape/client/package/tests/test_changer.py -@@ -849,9 +849,9 @@ class AptPackageChangerTest(LandscapeTes - - binaries_path = self.config.binaries_path - self.assertFileContent(os.path.join(binaries_path, "111.deb"), -- base64.decodestring(PKGDEB1)) -+ base64.decodebytes(PKGDEB1)) - self.assertFileContent(os.path.join(binaries_path, "222.deb"), -- base64.decodestring(PKGDEB2)) -+ base64.decodebytes(PKGDEB2)) - self.assertEqual( - self.facade.get_channels(), - self.get_binaries_channels(binaries_path)) ---- landscape-client-19.12.orig/landscape/lib/apt/package/testing.py -+++ landscape-client-19.12/landscape/lib/apt/package/testing.py -@@ -322,9 +322,9 @@ PKGDEB_OR_RELATIONS = ( - ).encode("ascii") - - --HASH1 = base64.decodestring(b"/ezv4AefpJJ8DuYFSq4RiEHJYP4=") --HASH2 = base64.decodestring(b"glP4DwWOfMULm0AkRXYsH/exehc=") --HASH3 = base64.decodestring(b"NJM05mj86veaSInYxxqL1wahods=") -+HASH1 = base64.decodebytes(b"/ezv4AefpJJ8DuYFSq4RiEHJYP4=") -+HASH2 = base64.decodebytes(b"glP4DwWOfMULm0AkRXYsH/exehc=") -+HASH3 = base64.decodebytes(b"NJM05mj86veaSInYxxqL1wahods=") - HASH_MINIMAL = b"6\xce\x8f\x1bM\x82MWZ\x1a\xffjAc(\xdb(\xa1\x0eG" - HASH_SIMPLE_RELATIONS = ( - b"'#\xab&k\xe6\xf5E\xcfB\x9b\xceO7\xe6\xec\xa9\xddY\xaa") -@@ -339,7 +339,7 @@ HASH_OR_RELATIONS = ( - def create_deb(target_dir, pkg_name, pkg_data): - """Create a Debian package in the specified C{target_dir}.""" - path = os.path.join(target_dir, pkg_name) -- data = base64.decodestring(pkg_data) -+ data = base64.decodebytes(pkg_data) - create_binary_file(path, data) - - diff -Nru landscape-client-19.12/debian/patches/replace-tostring.patch landscape-client-23.02/debian/patches/replace-tostring.patch --- landscape-client-19.12/debian/patches/replace-tostring.patch 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/patches/replace-tostring.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,25 +0,0 @@ -Index: landscape-client-19.12/landscape/lib/network.py -=================================================================== ---- landscape-client-19.12.orig/landscape/lib/network.py -+++ landscape-client-19.12/landscape/lib/network.py -@@ -11,7 +11,7 @@ import errno - import logging - - import netifaces --from twisted.python.compat import long -+from twisted.python.compat import long, _PY3 - - __all__ = ["get_active_device_info", "get_network_traffic"] - -@@ -246,7 +246,10 @@ def get_network_interface_speed(sock, in - speed = -1 - try: - fcntl.ioctl(sock, SIOCETHTOOL, packed) # Status ioctl() call -- res = status_cmd.tostring() -+ if _PY3: -+ res = status_cmd.tobytes() -+ else: -+ res = status_cmd.tostring() - speed, duplex = struct.unpack("12xHB28x", res) - except (IOError, OSError) as e: - if e.errno == errno.EPERM: diff -Nru landscape-client-19.12/debian/patches/series landscape-client-23.02/debian/patches/series --- landscape-client-19.12/debian/patches/series 2022-03-30 10:32:38.000000000 +0000 +++ landscape-client-23.02/debian/patches/series 2023-10-03 23:05:09.000000000 +0000 @@ -1,7 +1 @@ -0001-Handle-EINVAL-error-of-SIOCETHTOOL-ioctl.patch -0002-lp1870087-stale-locks.patch -py3.9.patch -0003-clean-publisher-shutdown.patch -replace-tostring.patch -1962539_twisted_py3.patch -lp1903776-release-upgrade.patch +0001-start-service-during-config.patch diff -Nru landscape-client-19.12/debian/rules landscape-client-23.02/debian/rules --- landscape-client-19.12/debian/rules 2022-03-30 10:14:31.000000000 +0000 +++ landscape-client-23.02/debian/rules 2023-08-14 19:59:15.000000000 +0000 @@ -23,4 +23,4 @@ install -D -o root -g root -m 755 apt-update/apt-update $(CURDIR)/$(root_dir)$(LIBDIR)/apt-update override_dh_installsystemd: - dh_installsystemd --no-enable --no-start + dh_installsystemd diff -Nru landscape-client-19.12/example.conf landscape-client-23.02/example.conf --- landscape-client-19.12/example.conf 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/example.conf 2023-02-07 18:55:50.000000000 +0000 @@ -174,3 +174,11 @@ # # By default, all usernames are allowed. script_users = ALL + + +# The maximum script output length transmitted to landscape +# Output over this limit is truncated +# +# The default is 512kB +# 2MB is allowed in this example +#script_output_limit=2048 diff -Nru landscape-client-19.12/landscape/__init__.py landscape-client-23.02/landscape/__init__.py --- landscape-client-19.12/landscape/__init__.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/__init__.py 2023-02-08 18:16:34.000000000 +0000 @@ -1,5 +1,5 @@ DEBIAN_REVISION = "" -UPSTREAM_VERSION = "18.01" +UPSTREAM_VERSION = "23.02" VERSION = "%s%s" % (UPSTREAM_VERSION, DEBIAN_REVISION) # The minimum server API version that all Landscape servers are known to speak diff -Nru landscape-client-19.12/landscape/client/broker/exchange.py landscape-client-23.02/landscape/client/broker/exchange.py --- landscape-client-19.12/landscape/client/broker/exchange.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/exchange.py 2023-02-07 18:55:50.000000000 +0000 @@ -347,11 +347,12 @@ from landscape.lib.hashlib import md5 from twisted.internet.defer import Deferred, succeed -from twisted.python.compat import _PY3 +from landscape.lib.compat import _PY3 +from landscape.lib.backoff import ExponentialBackoff from landscape.lib.fetch import HTTPCodeError, PyCurlError from landscape.lib.format import format_delta -from landscape.lib.message import got_next_expected, ANCIENT +from landscape.lib.message import got_next_expected, RESYNC from landscape.lib.versioning import is_version_higher, sort_versions from landscape import DEFAULT_SERVER_API, SERVER_API, CLIENT_API @@ -397,6 +398,7 @@ self._exchange_interval = config.exchange_interval self._urgent_exchange_interval = config.urgent_exchange_interval self._max_messages = max_messages + self._max_log_text_bytes = 100000 # 100KB self._notification_id = None self._exchange_id = None self._exchanging = False @@ -406,6 +408,7 @@ self._message_handlers = {} self._exchange_store = exchange_store self._stopped = False + self._backoff_counter = ExponentialBackoff(300, 7200) # 5 to 120 min self.register_message("accepted-types", self._handle_accepted_types) self.register_message("resynchronize", self._handle_resynchronize) @@ -436,6 +439,20 @@ return result + def truncate_message_field(self, field, message): + """ + Truncates message field value based on length + """ + if field in message: + value = message[field] + if isinstance(value, str): + # Note this is an approximation based on 1 byte chars + max_bytes = self._max_log_text_bytes + if len(value) > max_bytes: + value = value[:max_bytes] + value += '...MESSAGE TRUNCATED DUE TO SIZE' + message[field] = value + def send(self, message, urgent=False): """Include a message to be sent in an exchange. @@ -451,6 +468,10 @@ % message.get('operation-id')) return None + # These fields sometimes have really long output we need to trim + self.truncate_message_field('err', message) + self.truncate_message_field('result-text', message) + if "timestamp" not in message: message["timestamp"] = int(self._reactor.time()) message_id = self._message_store.add(message) @@ -567,6 +588,7 @@ self._urgent_exchange = False self._handle_result(payload, result) self._message_store.record_success(int(self._reactor.time())) + self._backoff_counter.decrease() else: self._reactor.fire("exchange-failed") logging.info("Message exchange failed.") @@ -586,6 +608,20 @@ self.exchange() return + if isinstance(error, HTTPCodeError): + if error.http_code == 429 or (500 <= error.http_code <= 599): + # We add an exponentially increasing delay ("backoff") if + # the server is overloaded to decrease load. We assume that + # any server backend error is deserving backoff, including + # 429 which is sent from the server on purpose to trigger + # the backoff. Whether the server is down or overloaded + # (503), has a server bug crashing the response (500), + # backing off should have no ill effect and help the + # service to recover. Client-configuration related errors + # (501, 505) are probably fine to throttle too as a higher + # rate won't help resolve them + self._backoff_counter.increase() + ssl_error = False if isinstance(error, PyCurlError) and error.error_code == 60: # The error returned is an SSL error, most likely the server @@ -642,6 +678,11 @@ interval = self._config.urgent_exchange_interval else: interval = self._config.exchange_interval + backoff_delay = self._backoff_counter.get_random_delay() + if backoff_delay: + logging.warning("Server is busy. Backing off client for {} " + "seconds".format(backoff_delay)) + interval += backoff_delay if self._notification_id is not None: self._reactor.cancel_call(self._notification_id) @@ -746,9 +787,9 @@ next_expected += len(payload["messages"]) message_store_state = got_next_expected(message_store, next_expected) - if message_store_state == ANCIENT: - # The server has probably lost some data we sent it. The - # slate has been wiped clean (by got_next_expected), now + if message_store_state == RESYNC: + # The server has probably lost some data we sent it. Or next + # expected is too high so the sequences are out of sync. Now # let's fire an event to tell all the plugins that they # ought to generate new messages so the server gets some # up-to-date data. @@ -800,8 +841,7 @@ # actually expect it to be a string. Some unit tests set it to # a regular string (since there is no difference between strings # and bytes in Python 2), so we check the type before converting. - if _PY3 and isinstance(message["type"], bytes): - message["type"] = message["type"].decode("ascii") + message["type"] = maybe_bytes(message["type"]) self.handle_message(message) sequence += 1 message_store.set_server_sequence(sequence) @@ -843,8 +883,9 @@ self._reactor.fire("message", message) # This has plan interference! but whatever. - if message["type"] in self._message_handlers: - for handler in self._message_handlers[message["type"]]: + message_type = maybe_bytes(message["type"]) + if message_type in self._message_handlers: + for handler in self._message_handlers[message_type]: handler(message) def register_client_accepted_message_type(self, type): @@ -866,3 +907,10 @@ diff.extend(["%s" % type for type in stable_types]) diff.extend(["-%s" % type for type in removed_types]) return " ".join(diff) + + +def maybe_bytes(thing): + """Return a py3 ascii string from maybe py2 bytes.""" + if _PY3 and isinstance(thing, bytes): + return thing.decode("ascii") + return thing diff -Nru landscape-client-19.12/landscape/client/broker/registration.py landscape-client-23.02/landscape/client/broker/registration.py --- landscape-client-19.12/landscape/client/broker/registration.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/registration.py 2023-02-07 18:55:50.000000000 +0000 @@ -8,10 +8,13 @@ credentials yet and that the server accepts registration messages, so it will craft an appropriate one and send it out. """ +import json import logging from twisted.internet.defer import Deferred +from landscape.client.broker.exchange import maybe_bytes +from landscape.client.monitor.ubuntuproinfo import get_ubuntu_pro_info from landscape.lib.juju import get_juju_info from landscape.lib.tag import is_valid_tag_list from landscape.lib.network import get_fqdn @@ -106,6 +109,7 @@ self._should_register = None self._fetch_async = fetch_async self._juju_data = None + self._clone_secure_id = None def should_register(self): id = self._identity @@ -191,6 +195,13 @@ "container-info": get_container_info(), "vm-info": get_vm_info()} + if self._clone_secure_id: + # We use the secure id here because the registration is encrypted + # and the insecure id has been already exposed to unencrypted + # http from the ping server. In addition it's more straightforward + # to get the computer from the server through it than the insecure + message["clone_secure_id"] = self._clone_secure_id + if group: message["access_group"] = group @@ -212,6 +223,8 @@ with_tags = "and tags %s " % tags if tags else "" with_group = "in access group '%s' " % group if group else "" + message["ubuntu_pro_info"] = json.dumps(get_ubuntu_pro_info()) + logging.info( u"Queueing message to register with account %r %s%s" "%s a password." % ( @@ -239,8 +252,9 @@ self._reactor.fire("resynchronize-clients") def _handle_registration(self, message): - if message["info"] in ("unknown-account", "max-pending-computers"): - self._reactor.fire("registration-failed", reason=message["info"]) + message_info = maybe_bytes(message["info"]) + if message_info in ("unknown-account", "max-pending-computers"): + self._reactor.fire("registration-failed", reason=message_info) def _handle_unknown_id(self, message): id = self._identity @@ -248,18 +262,9 @@ if clone is None: logging.info("Client has unknown secure-id for account %s." % id.account_name) - else: + else: # Save the secure id as the clone, and clear it so it's renewed logging.info("Client is clone of computer %s" % clone) - # Set a new computer title so when a registration request will be - # made, the pending computer UI will indicate that this is a clone - # of another computer. There's no need to persist the changes since - # a new registration will be requested immediately. - if clone == self._config.computer_title: - title = "%s (clone)" % self._config.computer_title - else: - title = "%s (clone of %s)" % (self._config.computer_title, - clone) - self._config.computer_title = title + self._clone_secure_id = id.secure_id id.secure_id = None id.insecure_id = None diff -Nru landscape-client-19.12/landscape/client/broker/server.py landscape-client-23.02/landscape/client/broker/server.py --- landscape-client-19.12/landscape/client/broker/server.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/server.py 2023-02-07 18:55:50.000000000 +0000 @@ -46,6 +46,7 @@ import logging from twisted.internet.defer import Deferred +from landscape.lib.compat import _PY3 from landscape.lib.twisted_util import gather_results from landscape.client.amp import remote @@ -191,6 +192,14 @@ during the next regularly scheduled exchange. @return: The message identifier created when queuing C{message}. """ + if b"type" in message and _PY3: + # XXX We are getting called by a python2 process. + # This occurs in the the specific case of a landscape-driven + # release upgrade to bionic, where the upgrading process is still + # running under python2.7. Therefore we do backward-compatible + # translation of byte keys in the sent message + message = {k.decode("ascii"): v for k, v in message.items()} + message["type"] = message["type"].decode("ascii") if isinstance(session_id, bool) and message["type"] in ( "operation-result", "change-packages-result"): # XXX This means we're performing a Landscape-driven upgrade and diff -Nru landscape-client-19.12/landscape/client/broker/service.py landscape-client-23.02/landscape/client/broker/service.py --- landscape-client-19.12/landscape/client/broker/service.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/service.py 2023-02-07 18:55:50.000000000 +0000 @@ -82,10 +82,11 @@ def stopService(self): """Stop the broker.""" - self.publisher.stop() + deferred = self.publisher.stop() self.exchanger.stop() self.pinger.stop() super(BrokerService, self).stopService() + return deferred def run(args): diff -Nru landscape-client-19.12/landscape/client/broker/tests/badprivate.ssl landscape-client-23.02/landscape/client/broker/tests/badprivate.ssl --- landscape-client-19.12/landscape/client/broker/tests/badprivate.ssl 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/badprivate.ssl 2023-02-07 18:55:50.000000000 +0000 @@ -1,15 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIICXgIBAAKBgQDGYFWP2Ine2OFIPjX+Tu+S403KW63EWq/I1DYXiezLoUpYPed3 -0tAkAXH1gOwQZbARFlUn0LgvXDSpuQLvgKQZwP/e1D8SvZUZ6nexW+aYlPE9kjd1 -dhK1xpe1h5y09AjCz02xxzcFzrJrJ47uU7vV+FGArE8FFh3hO+dz0/PmZQIDAQAB -AoGBAKfv+983yJfgcO9QwzLULlrilQNfk36r6y6QAG7y84T7uU10spSs4kno80mL -58yF2YTNrC91scdePrMEDikldUVcCqtPYcZKHyw5+4aGaDDO244tznexOQnQcNIe -2BbLFuh+jmJpoFIY/H7EsLQQzn6+6dGPnYGBQfiyitWfAXRNAkEA/ShQkYCRAHgq -g6WBIYsw/ISQydhiMiKrL2ZUXERT+pWU9MoSdMskgyMi3S7wzwJQXkrHA36q8QkL -+H8n5K+f5wJBAMiajfEtv0wRW0awX40qJtuqW3cSKeGHBH9mMObcRJd5OcK6giC/ -Cc5st/ZcuE/8i4r44DfeC+cwY6QdIqI8rdMCQQCKuq78LWJIyZEyt12+ThK4LsVR -d1zIcKsyvHb6YQ9MQPBx/NKEYlZN7tFKOFEKgBAevAe3aJCwqe5/bN8luQB9AkEA -uQVD8bR+AgzoIPS/zJWaLXSc09/e3PIJBfAdHnD+mq7mxWH8b3OD+e5wZjvyi2Ok -2NLfCug0FlGdNVrh/Lz2nQJATdcNvHNzJcWOHe05lo+xAqkjz73FWGpPNrdXRigG -YnjIsZVy4k48xIxPhT2rC44yo1iPEP5EnHCE2bLyUlTAYA== +MIIEowIBAAKCAQEA1f55iafJwi9EEL1jp8lX89SFVsordyhAJzQP+rRElFIcLSXU +2qC4whUErO3h3nKJXkIx4bHaQSn+/UKawr/kSVJI2QT3vimI/JTD5srxZJUrWEgc +slQNihx27jsRfpVeFK2BV2UpBYg65D6UhWyxNmoenBszBRRi11QnOF2VVX0BQLjO +lOP5mysSnq+VGEIEhViVICBKVb3dYle/LTh9uBH4oBBl4RvmzRZ/WUXqVLao8ryr +hciYDmCEpJH6tE4b/aEQNxou7nZqPJ91fkKZh4SmlhzXMFsjpQALZNYAxYZY6Kr4 +9Z+Xw8ckf3qqYfi8KovjCec7gewhCdZNpBxxOQIDAQABAoIBAHvT5DpOmEZAmY9i +OB9oN/fFO18sX5h09yJ4UuLMm36EQP+zC4dzR1YvWWRDxta0yl57yWeDRfs9NOsS +NoGJDq2K6tKBuGYWnMkjwHR1bNe6JbnRCKH8V1VbAUr7bTUlc6pdeCG9TM6BtSpM +OB849Ra6s3m7l3tR/5wAey13oak0PHM19c2Iv3Sniydt9toWyvuVbXWzOuupKMah ++eFkD0wpEgigL7gS4ToYXMqWn2LNHYsUSXKyuhtR0B/Ow6fwwb2KYxG5a5/hFI+E +URxFS148R/BtywUmxbzycTvZbx8XY7/lZjAlwYRCsYEF2EmmRbRGrHWOIuoHBMJu +49OMFu0CgYEA9P/EgmPeuIhrk3WFKnUTvHHTiW/Knv5xWRpkV+pXyGh4l+1F9lrj +WxORh+ws/6IpZevVVkUQxsd/Q4Uda7LlocS/YNSFC+LV471Y854+Wonbun8JBd+9 +GAFtG6o2XOaezp8zf8oDVqnfIx2qpySll3aNY7JrNZngtaE2bKz3sRMCgYEA35pO +47eTLxgIoFwr/M5cCT1nZ2FZUTBET9grrBgE5KTjtNbFAYJh7mP1lhILI58q7Lan +k9m5YoqAGsoKrilA715ks6vBQYWhlsg6p+rCvBTIYp4kxZwhTad5fFqXyhokwUZY +0ZJMtxYxK0xIZIEkuyzdzLiuYrKupFAB+3p+6gMCgYARQoeMjA6fv3ScsdXM1Oys +BPTbJNYId3JyzYouK2M9yiZcxal9HpAP1YQWKExPQhRais+/wSPabSmJDzKwaK0G +xX6aCr7IxJU+8xL2LrrD1Bx3ugVftZBzxX3zSf2Ec/bSJaMSKKAtldATgD6Kgels +jzyMvoARCaMsCIx2AYV9owKBgQCdd6UA9wHfE3TXwbF0mrr0Ats0Ubk91Nj2xcyT +qGKhxoFZlDovAuwGnzyPT+uqTWhERamkFJtaiyEGPKzi08iYCgivA1DY3MvcTOwJ +3uj+3T/1O1u4EmjdsAh9C6uDt3+U4P6hr/74nNdJn7IHnW8JpeIZTyH3/c/BhVqw +CCcikwKBgD7Kbl1AaK+6/+oDYunLOzxfDXkxkiXRDeBpvgOIW1G1F+bNJyO800oW +eZDtGU01Ze8gjNUbbYX39q4rCixtBHuJEwAH8J3grvtaHYpwjfuYgnhaXM1MHJo2 +kxfrQTDg/v3NjaccxCESeV1jjxz5hS2GrvqOvZrwyVtgi/r71cLJ -----END RSA PRIVATE KEY----- diff -Nru landscape-client-19.12/landscape/client/broker/tests/badpublic.ssl landscape-client-23.02/landscape/client/broker/tests/badpublic.ssl --- landscape-client-19.12/landscape/client/broker/tests/badpublic.ssl 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/badpublic.ssl 2023-02-07 18:55:50.000000000 +0000 @@ -1,5 +1,5 @@ -----BEGIN CERTIFICATE----- -MIIDzjCCAzegAwIBAgIJANqT3vXxSVFjMA0GCSqGSIb3DQEBBQUAMIGhMQswCQYD +MIIE0zCCA7ugAwIBAgIJANqT3vXxSVFjMA0GCSqGSIb3DQEBCwUAMIGhMQswCQYD VQQGEwJCUjEPMA0GA1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTEhMB8G A1UEChMYRmFrZSBMYW5kc2NhcGUgKFRlc3RpbmcpMREwDwYDVQQLEwhTZWN1cml0 eTESMBAGA1UEAxMJbG9jYWxob3N0MSQwIgYJKoZIhvcNAQkBFhVhbmRyZWFzQGNh @@ -7,17 +7,22 @@ MAkGA1UEBhMCQlIxDzANBgNVBAgTBlBhcmFuYTERMA8GA1UEBxMIQ3VyaXRpYmEx ITAfBgNVBAoTGEZha2UgTGFuZHNjYXBlIChUZXN0aW5nKTERMA8GA1UECxMIU2Vj dXJpdHkxEjAQBgNVBAMTCWxvY2FsaG9zdDEkMCIGCSqGSIb3DQEJARYVYW5kcmVh -c0BjYW5vbmljYWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGYFWP -2Ine2OFIPjX+Tu+S403KW63EWq/I1DYXiezLoUpYPed30tAkAXH1gOwQZbARFlUn -0LgvXDSpuQLvgKQZwP/e1D8SvZUZ6nexW+aYlPE9kjd1dhK1xpe1h5y09AjCz02x -xzcFzrJrJ47uU7vV+FGArE8FFh3hO+dz0/PmZQIDAQABo4IBCjCCAQYwHQYDVR0O -BBYEFF4A8+YHCLAt19OtWTjIjBKzLUokMIHWBgNVHSMEgc4wgcuAFF4A8+YHCLAt -19OtWTjIjBKzLUokoYGnpIGkMIGhMQswCQYDVQQGEwJCUjEPMA0GA1UECBMGUGFy -YW5hMREwDwYDVQQHEwhDdXJpdGliYTEhMB8GA1UEChMYRmFrZSBMYW5kc2NhcGUg -KFRlc3RpbmcpMREwDwYDVQQLEwhTZWN1cml0eTESMBAGA1UEAxMJbG9jYWxob3N0 -MSQwIgYJKoZIhvcNAQkBFhVhbmRyZWFzQGNhbm9uaWNhbC5jb22CCQDak9718UlR -YzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBABszkA3CCzt+nTOX+A7/ -I98DvI0W1Ss0J+Tq+diLr+kw6Z5ZTj5hrIS/x6XhVHjpim4724UBXA0Sels4JXbw -hhJovuncExce316gAol/9eEzTffZ9mt1jZQy9LL7IAENiobnsj2F65zNaJzXp5UC -rE/h/xIxz9rAmXtVOWHqZLcw +c0BjYW5vbmljYWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +1f55iafJwi9EEL1jp8lX89SFVsordyhAJzQP+rRElFIcLSXU2qC4whUErO3h3nKJ +XkIx4bHaQSn+/UKawr/kSVJI2QT3vimI/JTD5srxZJUrWEgcslQNihx27jsRfpVe +FK2BV2UpBYg65D6UhWyxNmoenBszBRRi11QnOF2VVX0BQLjOlOP5mysSnq+VGEIE +hViVICBKVb3dYle/LTh9uBH4oBBl4RvmzRZ/WUXqVLao8ryrhciYDmCEpJH6tE4b +/aEQNxou7nZqPJ91fkKZh4SmlhzXMFsjpQALZNYAxYZY6Kr49Z+Xw8ckf3qqYfi8 +KovjCec7gewhCdZNpBxxOQIDAQABo4IBCjCCAQYwHQYDVR0OBBYEFF4A8+YHCLAt +19OtWTjIjBKzLUokMIHWBgNVHSMEgc4wgcuAFF4A8+YHCLAt19OtWTjIjBKzLUok +oYGnpIGkMIGhMQswCQYDVQQGEwJCUjEPMA0GA1UECBMGUGFyYW5hMREwDwYDVQQH +EwhDdXJpdGliYTEhMB8GA1UEChMYRmFrZSBMYW5kc2NhcGUgKFRlc3RpbmcpMREw +DwYDVQQLEwhTZWN1cml0eTESMBAGA1UEAxMJbG9jYWxob3N0MSQwIgYJKoZIhvcN +AQkBFhVhbmRyZWFzQGNhbm9uaWNhbC5jb22CCQDak9718UlRYzAMBgNVHRMEBTAD +AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBGwsH3kzRp8hQMlhNBFjzGCfycS8VT/AIr +VIORXzitEEovBqnPM5VT48sx7y7L7e9bmtBU9jv2aqk/PtRgTIl7tFcI5XeVzlsF +HB3A5Y7ytJqp80bIZvKt8gEq2qaBmyIK9VIHBO56Yxb7lQhkWWZmVBeCBbOut6HI +oBC8IX1hck/K3DjQh8gZnuJz7Ut1Pvb7uIGkfHWb6fGuYDcpZXj3uhRAKzRIZgdE +xs28Om1dKO6CDeTVkT2OSPfj21EJ1hNXrPQfXlInbyjYgR72gvqN+kbj9sc1dUr5 +l0McTP+hrRFpRN/rUrqtOWtHFW+INXj/HAuK+i4MlwkQXgPOTmoy -----END CERTIFICATE----- diff -Nru landscape-client-19.12/landscape/client/broker/tests/helpers.py landscape-client-23.02/landscape/client/broker/tests/helpers.py --- landscape-client-19.12/landscape/client/broker/tests/helpers.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/helpers.py 2023-02-07 18:55:50.000000000 +0000 @@ -39,14 +39,16 @@ log_dir = test_case.makeDir() test_case.config_filename = os.path.join(test_case.makeDir(), "client.conf") - open(test_case.config_filename, "w").write( - "[client]\n" - "url = http://localhost:91919\n" - "computer_title = Some Computer\n" - "account_name = some_account\n" - "ping_url = http://localhost:91910\n" - "data_path = %s\n" - "log_dir = %s\n" % (data_path, log_dir)) + + with open(test_case.config_filename, "w") as fh: + fh.write( + "[client]\n" + "url = http://localhost:91919\n" + "computer_title = Some Computer\n" + "account_name = some_account\n" + "ping_url = http://localhost:91910\n" + "data_path = %s\n" + "log_dir = %s\n" % (data_path, log_dir)) bootstrap_list.bootstrap(data_path=data_path, log_dir=log_dir) diff -Nru landscape-client-19.12/landscape/client/broker/tests/private.ssl landscape-client-23.02/landscape/client/broker/tests/private.ssl --- landscape-client-19.12/landscape/client/broker/tests/private.ssl 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/private.ssl 2023-02-07 18:55:50.000000000 +0000 @@ -1,15 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIICWwIBAAKBgQDX2VNEDZHtl5nimNocshar8pBmjqiGn9olCR2LcKifuJY4bFTg -qib+Rr3v2DwDTbOMaquRSxFgwLJLCug3WclsGrYSPIsFCx+k3XhqM61JXEwrKuIp -Js893XHkeg3SEFua/oVfDxNfJttoHW3FbsnDx5964kYwGExjJcH73GInUQIDAQAB -AoGASiM9NEys6Lx/gJMbp2uL2fdwnak2PTc+iCX/XduOL34JKswawyfuSLwnlO/i -fQf9OaeR0k/EYkUNeDUA2bIfOj6wWS8tamnX4fxL7A20y5VyqMMah8mcerZgtPdS -7ZtYCbeijWSKpHgjALc2Hym7R68WZI+IHe0DQkcW6WxOMFkCQQD2jqHZn/Qtd62u -mWVwIx6G7+Po5vzd86KyWWftdUtVCY9DmiX1rmWXbJhLnmaKCLkmHxyBvw7Biarr -ZnCAafebAkEA4B2dSpLi7bAzjCa7JBtzV9kr1FVZOl2vA+9BqTAjCQu0b9VDEm8V -x0061Z8rN7Og3ECGtKH/r3/4RnHUPpwJgwJAdyZQkvHYt4xJc8IPolRmcUFGu4u9 -Eammq1fHgJqZcBvxjvLUe1jvIXFKW+jNltFGYGTSiuUAxYi4/49+uJ/9FwJAGBB1 -/DTrcvQxhMH/5C+iYfNqtmD3tMGscjK1jTIjAOyl0kBG9GrDHuRXBesSW+fIxP2U -uT6P0std4EqGrLZaewJAHT0n/3tXnsPjj+BMlC4ZkRKgPJ4I7zTU1XSlLY5zbMoV -NvtHLlq7ttiarsH95xyge69uV1/zJVj/IiS71YY9PQ== +MIIEogIBAAKCAQEAw1Cf8FvbgP/FBGyCPXrTw23qa1KqmDkEu3q/yqmVpBlHh93K +UZu0T0WT7UJykEwOJqY+5G3Xaw53gWx7Lvz5nsI+X8Womd9NSqUkCiVq/TxeuWVm +07+pohQi8OJMmdfflx2eDX+3xciHdB4+A7baefyGut1vGr1wTYA3pu3/agqNIcAo +2F7LE13vnxgFzXCW0StdQeeT8KapOSJTZ8vxxkEKmXTixN7rpACP6GpD4jLTHZ/L +YUxjfHJdRGBF6TH/4NaA5CnS2yJpGidBmeK8Jid/S54P7HXrcJTrC0xwdYFfUTEI +25mBSqbEfGNqKhkYeJT/Pvy5TGdqAVSr4OjpEwIDAQABAoIBABw/4hI6xwHefJmK +NEA+Lrjagghp2YDQ5m1TcMAYTSuB+IWfP68UDT1V+/JaJQXX6kgOzZPuizTRz9kp +XpvKPTSINctWZG91C9HbFt5c0R+1hqHcF8ZSt29Y6EDdCmVKAu3xe7XKHkN+IJFb ++m5BGVKBgt8uPe6pLcAX5nS/gazNfyQ9s9zypMkCVSFoCOI+KkzqBCYPR/dqJUxP +hnxDt78ndUJL9QIDIGu7aCr9UwQCwSiRTB3+fDt1iPk1RWl+zmqvHUscPdyY1oMe +dvPNbF7Ea1nGjxzVG6+vyRHC1dQYSaqU8Ri26p0MBXGHCl8mq8tqV5ra30Q87ynV +wNu2TvkCgYEA6UCUBoetPoT3ncp/QXOs2obPQJ9W7VRMIFpkiZNGs8Zivp06CY3e +z7FqimDROKsFaYgDwPk5sOpzotnTA1/LsuoHK49Nux7Zn9RQak++6NrSozxvJ1P/ +ArpKkoTZNBbMPDghmib9gKUENjG1+6CkO5Z4tr0l9NyOBLz5j7oW2o8CgYEA1lzl +X2olxIY2nxPlmCHwjXVD90FT9xmcAe4xtU96aXv8v+f+6V2rwSBr8oGI8oyNjZN9 +nLkT9fRVhGi1bf9VZYb8fWB8PtGumZRZcfqYDyMD/0xbT6vEwaNtPtqNf24t8jf3 +ijU+Yx2jIwE7cAtOKcUg0eMGRPzSF8NRgPTaWz0CgYBwXE1yP9VysnbdqfhXPTPd +KOeZh6hGNz9crm6T30BFxaE3lWGpzI+ymRJrimv+0lOPHJhCU0w5Lxd5MVj23SSx +EQ9XKncVVq0a0xnRvIyIezDQtYIN/eZwF/FoV1qSPxEvSRLWwUWIvPUkbhnuFtpG +YhvQW5l3NO+s1KObWtc7fQKBgG4dIBJIU4hFLU/AB8ODQ69WmogrfbdD53iyY8Rw +REBlWWs3ACHeZTj6r5jN44w8mQYtymu0QsWoMjmnE/OiIrrZgV/iLVCTo23u35eG +E5BK+2WsUod1g8e4bIjJ+b+I2H9BMp5DRX3inod/vYmLtSYNxhMq3HCZsk5Uncxx +eq09AoGAfAX7tLtZDxpvyzEEoNdtt3zf9OUYDb74vH0CLAwahDE6ZdK/BVoc+JJB +Cm74/54z1nj7ubLNgMFuJbcgX/xthnuHcPSNH9FnPP//twYiuK1Zy67Q8aqFFxj4 +41H4idM4yLHJX1wjJDbufGHS/CjGg10Iy+gfFzqKqhkRhrdD3aI= -----END RSA PRIVATE KEY----- diff -Nru landscape-client-19.12/landscape/client/broker/tests/public.ssl landscape-client-23.02/landscape/client/broker/tests/public.ssl --- landscape-client-19.12/landscape/client/broker/tests/public.ssl 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/public.ssl 2023-02-07 18:55:50.000000000 +0000 @@ -1,22 +1,27 @@ -----BEGIN CERTIFICATE----- -MIIDnDCCAwWgAwIBAgIJAMjc7CvbvQHcMA0GCSqGSIb3DQEBCwUAMIGRMQswCQYD +MIIEoTCCA4mgAwIBAgIJAMjc7CvbvQHcMA0GCSqGSIb3DQEBCwUAMIGRMQswCQYD VQQGEwJCUjEPMA0GA1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTESMBAG A1UEChMJTGFuZHNjYXBlMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlsb2Nh bGhvc3QxJDAiBgkqhkiG9w0BCQEWFWFuZHJlYXNAY2Fub25pY2FsLmNvbTAeFw0x OTAxMzEyMjI4NTJaFw0yOTAxMjgyMjI4NTJaMIGRMQswCQYDVQQGEwJCUjEPMA0G A1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTESMBAGA1UEChMJTGFuZHNj YXBlMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlsb2NhbGhvc3QxJDAiBgkq -hkiG9w0BCQEWFWFuZHJlYXNAY2Fub25pY2FsLmNvbTCBnzANBgkqhkiG9w0BAQEF -AAOBjQAwgYkCgYEA19lTRA2R7ZeZ4pjaHLIWq/KQZo6ohp/aJQkdi3Con7iWOGxU -4Kom/ka979g8A02zjGqrkUsRYMCySwroN1nJbBq2EjyLBQsfpN14ajOtSVxMKyri -KSbPPd1x5HoN0hBbmv6FXw8TXybbaB1txW7Jw8efeuJGMBhMYyXB+9xiJ1ECAwEA -AaOB+TCB9jAdBgNVHQ4EFgQU3eUz2XxK1J/oavkn/hAvYfGOZM0wgcYGA1UdIwSB -vjCBu4AU3eUz2XxK1J/oavkn/hAvYfGOZM2hgZekgZQwgZExCzAJBgNVBAYTAkJS -MQ8wDQYDVQQIEwZQYXJhbmExETAPBgNVBAcTCEN1cml0aWJhMRIwEAYDVQQKEwlM -YW5kc2NhcGUxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCWxvY2FsaG9zdDEk -MCIGCSqGSIb3DQEJARYVYW5kcmVhc0BjYW5vbmljYWwuY29tggkAyNzsK9u9Adww -DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQCUq4lOtq19Q/NQmSdSQvpB -GWj3NKkpsH6sDlzjfGTVDqbL6buSUq4fTmXfrx5ce/Y+APhfAINAwIL/zjCSV3zI -EdhMG3BqMBmrQ60YfN7Z3drqfFzlg2yEVd/nJwrppjAI58KJgamN12WZ0eQTV8FR -co8+OnqJEOPM8cMg7VEbtQ== +hkiG9w0BCQEWFWFuZHJlYXNAY2Fub25pY2FsLmNvbTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAMNQn/Bb24D/xQRsgj1608Nt6mtSqpg5BLt6v8qplaQZ +R4fdylGbtE9Fk+1CcpBMDiamPuRt12sOd4Fsey78+Z7CPl/FqJnfTUqlJAolav08 +XrllZtO/qaIUIvDiTJnX35cdng1/t8XIh3QePgO22nn8hrrdbxq9cE2AN6bt/2oK +jSHAKNheyxNd758YBc1wltErXUHnk/CmqTkiU2fL8cZBCpl04sTe66QAj+hqQ+Iy +0x2fy2FMY3xyXURgRekx/+DWgOQp0tsiaRonQZnivCYnf0ueD+x163CU6wtMcHWB +X1ExCNuZgUqmxHxjaioZGHiU/z78uUxnagFUq+Do6RMCAwEAAaOB+TCB9jAdBgNV +HQ4EFgQU3eUz2XxK1J/oavkn/hAvYfGOZM0wgcYGA1UdIwSBvjCBu4AU3eUz2XxK +1J/oavkn/hAvYfGOZM2hgZekgZQwgZExCzAJBgNVBAYTAkJSMQ8wDQYDVQQIEwZQ +YXJhbmExETAPBgNVBAcTCEN1cml0aWJhMRIwEAYDVQQKEwlMYW5kc2NhcGUxEDAO +BgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCWxvY2FsaG9zdDEkMCIGCSqGSIb3DQEJ +ARYVYW5kcmVhc0BjYW5vbmljYWwuY29tggkAyNzsK9u9AdwwDAYDVR0TBAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAQEAnHA/iAGqfjEVlB5PJbYkKNasaflJJnT34BUZ +QdArozYCbC3dsysu+BkKEQ74FbDb/ZV0QhtV2/qYRlPsauWIiWU3gYk9wVjJdaBk ++MEViKOLV8CYe7ZA1OFNRQjfWUAiuRJdSsxnvOHcqcxrJT/hFmFz1OWyqcwfHBZY +4+8A7Byuy7x0O3qOWT2gaXpFL9neqtla1nEa1egJTZLrMF8Pi3vIpFthEgQ+yXoW +kBdWiYuMIry7YtAqBb4WgursCRftOCkPRYh3MVryqCDsVtOfGW2RO+YZnQgUpeTX +a6kHrB1hh+p8Az9pPWQVPb4w/jn4L2kEjTNpUxYWVB8Sc8GtGA== -----END CERTIFICATE----- diff -Nru landscape-client-19.12/landscape/client/broker/tests/test_exchange.py landscape-client-23.02/landscape/client/broker/tests/test_exchange.py --- landscape-client-19.12/landscape/client/broker/tests/test_exchange.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/test_exchange.py 2023-02-07 18:55:50.000000000 +0000 @@ -145,6 +145,50 @@ self.mstore.add_pending_offset(1) self.assertFalse(self.mstore.is_pending(message_id)) + def test_send_big_message_trimmed_err(self): + """ + When package reporter sends error, message is trimmed if too long + """ + self.mstore.set_accepted_types(["package-reporter-result"]) + self.exchanger._max_log_text_bytes = 5 + self.exchanger.send({"type": "package-reporter-result", "err": "E"*10, + "code": 0}) + self.exchanger.exchange() + self.assertEqual(len(self.transport.payloads), 1) + messages = self.transport.payloads[0]["messages"] + self.assertIn('TRUNCATED', messages[0]['err']) + self.assertIn('EEEEE', messages[0]['err']) + self.assertNotIn('EEEEEE', messages[0]['err']) + + def test_send_big_message_trimmed_result(self): + """ + When an activity sends result log, message is trimmed if too long + """ + self.mstore.set_accepted_types(["operation-result"]) + self.exchanger._max_log_text_bytes = 5 + self.exchanger.send({"type": "operation-result", "result-text": "E"*10, + "code": 0, "status": 0, "operation-id": 0}) + self.exchanger.exchange() + self.assertEqual(len(self.transport.payloads), 1) + messages = self.transport.payloads[0]["messages"] + self.assertIn('TRUNCATED', messages[0]['result-text']) + self.assertIn('EEEEE', messages[0]['result-text']) + self.assertNotIn('EEEEEE', messages[0]['result-text']) + + def test_send_small_message_not_trimmed(self): + """ + If message is below length, nothing should happen + """ + self.mstore.set_accepted_types(["package-reporter-result"]) + self.exchanger._max_log_text_bytes = 4 + self.exchanger.send({"type": "package-reporter-result", "err": "E"*4, + "code": 0}) + self.exchanger.exchange() + self.assertEqual(len(self.transport.payloads), 1) + messages = self.transport.payloads[0]["messages"] + self.assertNotIn('TRUNCATED', messages[0]['err']) + self.assertIn('EEEE', messages[0]['err']) + def test_wb_include_accepted_types(self): """ Every payload from the client needs to specify an ID which @@ -324,6 +368,88 @@ self.assertEqual(payload["sequence"], 1) self.assertEqual(payload["next-expected-sequence"], 0) + @mock.patch("landscape.client.broker.store.MessageStore" + ".delete_old_messages") + def test_pending_offset_when_next_expected_too_high(self, + mock_rm_all_messages): + ''' + When next expected sequence received from server is too high, then the + pending offset should reset to zero. This will cause the client to + resend the pending messages. + ''' + + self.mstore.set_accepted_types(["data"]) + self.mstore.add({"type": "data", "data": 0}) + self.mstore.add({"type": "data", "data": 1}) + + self.exchanger.exchange() + + self.assertEqual(self.mstore.get_pending_offset(), 2) + + self.mstore.add({"type": "data", "data": 2}) + + self.transport.next_expected_sequence = 100 + + # Confirm pending offset is reset so that messages are sent again + self.exchanger.exchange() + self.assertEqual(self.mstore.get_pending_offset(), 0) + + # This function flushes the queue, otherwise an offset of 0 will resend + # previous messages that were already successful + self.assertTrue(mock_rm_all_messages.called) + + def test_payloads_when_next_expected_too_high(self): + ''' + When next expected sequence received from server is too high, then the + current messages should get sent again since we don't have confirmation + that the server received it. Also previous messages should not get + repeated. + ''' + + self.mstore.set_accepted_types(["data"]) + + message0 = {"type": "data", "data": 0} + self.mstore.add(message0) + self.exchanger.exchange() + + message1 = {"type": "data", "data": 1} + message2 = {"type": "data", "data": 2} + self.mstore.add(message1) + self.mstore.add(message2) + + self.transport.next_expected_sequence = 100 + self.exchanger.exchange() # Resync + self.exchanger.exchange() # Resend + + # Confirm messages is not empty which was the original bug + last_messages = self.transport.payloads[-1]["messages"] + self.assertTrue(last_messages) + + # Confirm earlier messages are not resent + self.assertNotIn(message0["data"], + [m["data"] for m in last_messages]) + + # Confirm contents of payload + self.assertEqual([message1, message2], last_messages) + + def test_resync_when_next_expected_too_high(self): + ''' + When next expected sequence received from the server is too high, then + a resynchronize should happen + ''' + + self.mstore.set_accepted_types(["empty", "resynchronize"]) + self.mstore.add({"type": "empty"}) + self.exchanger.exchange() + + self.transport.next_expected_sequence = 100 + + self.reactor.call_on("resynchronize-clients", lambda scope=None: None) + + self.exchanger.exchange() + self.assertMessage(self.mstore.get_pending_messages()[-1], + {"type": "resynchronize"}) + def test_start_with_urgent_exchange(self): """ Immediately after registration, an urgent exchange should be scheduled. @@ -1088,6 +1214,64 @@ self.exchanger.exchange() self.assertEqual(b"3.2", self.mstore.get_server_api()) + def test_500_backoff(self): + """ + If we get a server error then the exponential backoff is triggered + """ + self.config.urgent_exchange_interval = 10 + self.exchanger._backoff_counter._start_delay = 300 + self.exchanger._backoff_counter._max_delay = 1000 + self.transport.responses.append(HTTPCodeError(503, "")) + self.exchanger.schedule_exchange(urgent=True) + self.reactor.advance(50) + self.assertEqual(len(self.transport.payloads), 1) + self.reactor.advance(400) + self.assertEqual(len(self.transport.payloads), 2) + + def test_429_backoff(self): + """ + HTTP error 429 should also trigger backoff + """ + self.config.urgent_exchange_interval = 10 + self.exchanger._backoff_counter._start_delay = 300 + self.exchanger._backoff_counter._max_delay = 1000 + self.transport.responses.append(HTTPCodeError(429, "")) + self.exchanger.schedule_exchange(urgent=True) + self.reactor.advance(50) + self.assertEqual(len(self.transport.payloads), 1) + + def test_backoff_reset_after_success(self): + """ + If we get a success after a 500 error then backoff should be zero + """ + self.config.urgent_exchange_interval = 10 + self.exchanger._backoff_counter._start_delay = 300 + self.exchanger._backoff_counter._max_delay = 1000 + self.transport.responses.append(HTTPCodeError(500, "")) + self.exchanger.schedule_exchange(urgent=True) + self.reactor.advance(50) + + # Confirm it's not zero after the error + self.assertTrue(self.exchanger._backoff_counter.get_random_delay()) + + server_message = [{"type": "type-R", "whatever": 5678}] + self.transport.responses.append(server_message) + self.exchanger.schedule_exchange(urgent=True) + self.reactor.advance(500) + + # Confirm it is zero after the success + self.assertFalse(self.exchanger._backoff_counter.get_random_delay()) + + def test_400_no_backoff(self): + """ + If we get a 400 error then the backoff should not be triggered + """ + self.config.urgent_exchange_interval = 10 + self.transport.responses.append(HTTPCodeError(400, "")) + self.exchanger.schedule_exchange(urgent=True) + self.reactor.advance(20) + self.assertEqual(len(self.transport.payloads), 2) + class AcceptedTypesMessageExchangeTest(LandscapeTest): diff -Nru landscape-client-19.12/landscape/client/broker/tests/test_registration.py landscape-client-23.02/landscape/client/broker/tests/test_registration.py --- landscape-client-19.12/landscape/client/broker/tests/test_registration.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/test_registration.py 2023-02-07 18:55:50.000000000 +0000 @@ -3,7 +3,7 @@ import socket import mock -from twisted.python.compat import _PY3 +from landscape.lib.compat import _PY3 from landscape.client.broker.registration import RegistrationError, Identity from landscape.client.tests.helpers import LandscapeTest @@ -90,7 +90,7 @@ and insecure ids even if no requests were sent. """ self.exchanger.handle_message( - {"type": "set-id", "id": "abc", "insecure-id": "def"}) + {"type": b"set-id", "id": b"abc", "insecure-id": b"def"}) self.assertEqual(self.identity.secure_id, "abc") self.assertEqual(self.identity.insecure_id, "def") @@ -101,29 +101,58 @@ """ reactor_fire_mock = self.reactor.fire = mock.Mock() self.exchanger.handle_message( - {"type": "set-id", "id": "abc", "insecure-id": "def"}) + {"type": b"set-id", "id": b"abc", "insecure-id": b"def"}) reactor_fire_mock.assert_any_call("registration-done") def test_unknown_id(self): self.identity.secure_id = "old_id" self.identity.insecure_id = "old_id" self.mstore.set_accepted_types(["register"]) - self.exchanger.handle_message({"type": "unknown-id"}) + self.exchanger.handle_message({"type": b"unknown-id"}) self.assertEqual(self.identity.secure_id, None) self.assertEqual(self.identity.insecure_id, None) def test_unknown_id_with_clone(self): """ If the server reports us that we are a clone of another computer, then - set our computer's title accordingly. + make sure we handle it """ self.config.computer_title = "Wu" self.mstore.set_accepted_types(["register"]) - self.exchanger.handle_message({"type": "unknown-id", "clone-of": "Wu"}) - self.assertEqual("Wu (clone)", self.config.computer_title) + self.exchanger.handle_message( + {"type": b"unknown-id", "clone-of": "Wu"}) self.assertIn("Client is clone of computer Wu", self.logfile.getvalue()) + def test_clone_secure_id_saved(self): + """ + Make sure that secure id is saved when theres a clone and existing + value is cleared out + """ + secure_id = "foo" + self.identity.secure_id = secure_id + self.config.computer_title = "Wu" + self.mstore.set_accepted_types(["register"]) + self.exchanger.handle_message( + {"type": b"unknown-id", "clone-of": "Wu"}) + self.assertEqual(self.handler._clone_secure_id, secure_id) + self.assertIsNone(self.identity.secure_id) + + def test_clone_id_in_message(self): + """ + Make sure that the clone id is present in the registration message + """ + secure_id = "foo" + self.identity.secure_id = secure_id + self.config.computer_title = "Wu" + self.mstore.set_accepted_types(["register"]) + self.mstore.set_server_api(b"3.3") # Note this is only for later api + self.exchanger.handle_message( + {"type": b"unknown-id", "clone-of": "Wu"}) + self.reactor.fire("pre-exchange") + messages = self.mstore.get_pending_messages() + self.assertEqual(messages[0]["clone_secure_id"], secure_id) + def test_should_register(self): self.mstore.set_accepted_types(["register"]) self.config.computer_title = "Computer Title" @@ -366,7 +395,7 @@ """ reactor_fire_mock = self.reactor.fire = mock.Mock() self.exchanger.handle_message( - {"type": "registration", "info": "unknown-account"}) + {"type": b"registration", "info": b"unknown-account"}) reactor_fire_mock.assert_called_with( "registration-failed", reason="unknown-account") @@ -378,7 +407,7 @@ """ reactor_fire_mock = self.reactor.fire = mock.Mock() self.exchanger.handle_message( - {"type": "registration", "info": "max-pending-computers"}) + {"type": b"registration", "info": b"max-pending-computers"}) reactor_fire_mock.assert_called_with( "registration-failed", reason="max-pending-computers") @@ -389,7 +418,7 @@ """ reactor_fire_mock = self.reactor.fire = mock.Mock() self.exchanger.handle_message( - {"type": "registration", "info": "blah-blah"}) + {"type": b"registration", "info": b"blah-blah"}) for name, args, kwargs in reactor_fire_mock.mock_calls: self.assertNotEquals("registration-failed", args[0]) @@ -420,13 +449,13 @@ # This should somehow callback the deferred. self.exchanger.handle_message( - {"type": "set-id", "id": "abc", "insecure-id": "def"}) + {"type": b"set-id", "id": b"abc", "insecure-id": b"def"}) self.assertEqual(calls, [1]) # Doing it again to ensure that the deferred isn't called twice. self.exchanger.handle_message( - {"type": "set-id", "id": "abc", "insecure-id": "def"}) + {"type": b"set-id", "id": b"abc", "insecure-id": b"def"}) self.assertEqual(calls, [1]) @@ -448,7 +477,7 @@ # This should somehow callback the deferred. self.exchanger.handle_message( - {"type": "set-id", "id": "abc", "insecure-id": "def"}) + {"type": b"set-id", "id": b"abc", "insecure-id": b"def"}) self.assertEqual(results, [None]) @@ -473,13 +502,13 @@ # This should somehow callback the deferred. self.exchanger.handle_message( - {"type": "registration", "info": "unknown-account"}) + {"type": b"registration", "info": b"unknown-account"}) self.assertEqual(calls, [True]) # Doing it again to ensure that the deferred isn't called twice. self.exchanger.handle_message( - {"type": "registration", "info": "unknown-account"}) + {"type": b"registration", "info": b"unknown-account"}) self.assertEqual(calls, [True]) @@ -505,13 +534,13 @@ d.addErrback(add_call) self.exchanger.handle_message( - {"type": "registration", "info": "max-pending-computers"}) + {"type": b"registration", "info": b"max-pending-computers"}) self.assertEqual(calls, [True]) # Doing it again to ensure that the deferred isn't called twice. self.exchanger.handle_message( - {"type": "registration", "info": "max-pending-computers"}) + {"type": b"registration", "info": b"max-pending-computers"}) self.assertEqual(calls, [True]) diff -Nru landscape-client-19.12/landscape/client/broker/tests/test_server.py landscape-client-23.02/landscape/client/broker/tests/test_server.py --- landscape-client-19.12/landscape/client/broker/tests/test_server.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/test_server.py 2023-02-07 18:55:50.000000000 +0000 @@ -130,6 +130,25 @@ self.assertMessages(self.mstore.get_pending_messages(), [message]) self.assertTrue(self.exchanger.is_urgent()) + def test_send_message_from_py27_upgrader(self): + """ + If we receive release-upgrade results from a py27 release upgrader, + it gets translated to a py3-compatible message. + """ + legacy_message = { + b"type": b"change-packages-result", + b"operation-id": 99, + b"result-code": 123} + self.mstore.set_accepted_types(["change-packages-result"]) + self.broker.send_message(legacy_message, True) + expected = [{ + "type": "change-packages-result", + "operation-id": 99, + "result-code": 123 + }] + self.assertMessages(self.mstore.get_pending_messages(), expected) + self.assertTrue(self.exchanger.is_urgent()) + def test_is_pending(self): """ The L{BrokerServer.is_pending} method indicates if a message with diff -Nru landscape-client-19.12/landscape/client/broker/tests/test_store.py landscape-client-23.02/landscape/client/broker/tests/test_store.py --- landscape-client-19.12/landscape/client/broker/tests/test_store.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/test_store.py 2023-02-07 18:55:50.000000000 +0000 @@ -149,7 +149,7 @@ for i in range(10): self.store.add(dict(type="data", data=intToBytes(i))) il = [m["data"] for m in self.store.get_pending_messages(5)] - self.assertEqual(il, [intToBytes(i) for i in[0, 1, 2, 3, 4]]) + self.assertEqual(il, [intToBytes(i) for i in [0, 1, 2, 3, 4]]) def test_offset(self): self.store.set_pending_offset(5) diff -Nru landscape-client-19.12/landscape/client/broker/tests/test_transport.py landscape-client-23.02/landscape/client/broker/tests/test_transport.py --- landscape-client-19.12/landscape/client/broker/tests/test_transport.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/tests/test_transport.py 2023-02-07 18:55:50.000000000 +0000 @@ -16,7 +16,7 @@ def sibpath(path): - return os.path.join(os.path.dirname(__file__), path) + return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) PRIVKEY = sibpath("private.ssl") diff -Nru landscape-client-19.12/landscape/client/broker/transport.py landscape-client-23.02/landscape/client/broker/transport.py --- landscape-client-19.12/landscape/client/broker/transport.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/broker/transport.py 2023-02-07 18:55:50.000000000 +0000 @@ -6,7 +6,7 @@ import pycurl -from twisted.python.compat import unicode, _PY3 +from landscape.lib.compat import unicode, _PY3 from landscape.lib import bpickle from landscape.lib.fetch import fetch diff -Nru landscape-client-19.12/landscape/client/configuration.py landscape-client-23.02/landscape/client/configuration.py --- landscape-client-19.12/landscape/client/configuration.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/configuration.py 2023-02-07 18:55:50.000000000 +0000 @@ -7,14 +7,15 @@ from __future__ import print_function from functools import partial -import base64 import getpass import io import os import pwd import sys +import textwrap from landscape.lib.compat import input +from landscape.lib import base64 from landscape.lib.tag import is_valid_tag @@ -33,6 +34,9 @@ from landscape.client.broker.service import BrokerService +EXIT_NOT_REGISTERED = 5 + + class ConfigurationError(Exception): """Raised when required configuration values are missing.""" @@ -192,6 +196,11 @@ parser.add_option("--init", action="store_true", default=False, help="Set up the client directories structure " "and exit.") + parser.add_option("--is-registered", action="store_true", + help="Exit with code 0 (success) if client is " + "registered else returns {}. Displays " + "registration info." + .format(EXIT_NOT_REGISTERED)) return parser @@ -513,7 +522,7 @@ # WARNING: ssl_public_certificate is misnamed, it's not the key of the # certificate, but the actual certificate itself. if config.ssl_public_key and config.ssl_public_key.startswith("base64:"): - decoded_cert = base64.decodestring( + decoded_cert = base64.decodebytes( config.ssl_public_key[7:].encode("ascii")) config.ssl_public_key = store_public_key_data( config, decoded_cert) @@ -759,6 +768,26 @@ return bool(identity.secure_id) +def registration_info_text(config, registration_status): + ''' + A simple output displaying whether the client is registered or not, the + account name, and config and data paths + ''' + + config_path = os.path.abspath(config._config_filename) + + text = textwrap.dedent(""" + Registered: {} + Config Path: {} + Data Path {}""" + .format(registration_status, config_path, + config.data_path)) + if registration_status: + text += '\nAccount Name: {}'.format(config.account_name) + + return text + + def main(args, print=print): """Interact with the user and the server to set up client configuration.""" @@ -769,6 +798,18 @@ print_text(str(error), error=True) sys.exit(1) + if config.is_registered: + + registration_status = is_registered(config) + + info_text = registration_info_text(config, registration_status) + print(info_text) + + if registration_status: + sys.exit(0) + else: + sys.exit(EXIT_NOT_REGISTERED) + if os.getuid() != 0: sys.exit("landscape-config must be run as root.") diff -Nru landscape-client-19.12/landscape/client/lockfile.py landscape-client-23.02/landscape/client/lockfile.py --- landscape-client-19.12/landscape/client/lockfile.py 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/landscape/client/lockfile.py 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,52 @@ +import os + +from twisted.python import lockfile + + +def patch_lockfile(): + if lockfile.FilesystemLock is PatchedFilesystemLock: + return + lockfile.FilesystemLock = PatchedFilesystemLock + + +class PatchedFilesystemLock(lockfile.FilesystemLock): + """ + Patched Twisted's FilesystemLock.lock to handle PermissionError + when trying to lock. + """ + + def lock(self): + # XXX Twisted assumes PIDs don't get reused, which is incorrect. + # As such, we pre-check that any existing lock file isn't + # associated to a live process, and that any associated + # process is from landscape. Otherwise, clean up the lock file, + # considering it to be locked to a recycled PID. + # + # Although looking for the process name may seem fragile, it's the + # most acurate info we have since: + # * some process run as root, so the UID is not a reference + # * process may not be spawned by systemd, so cgroups are not reliable + # * python executable is not a reference + clean = True + try: + pid = os.readlink(self.name) + ps_name = get_process_name(int(pid)) + if not ps_name.startswith("landscape"): + os.remove(self.name) + clean = False + except Exception: + # We can't figure the lock state, let FilesystemLock figure it + # out normally. + pass + + result = super(PatchedFilesystemLock, self).lock() + self.clean = self.clean and clean + return result + + +def get_process_name(pid): + """Return a process name from a pid.""" + stat_path = "/proc/{}/stat".format(pid) + with open(stat_path) as stat_file: + stat = stat_file.read() + return stat.partition("(")[2].rpartition(")")[0] diff -Nru landscape-client-19.12/landscape/client/manager/aptsources.py landscape-client-23.02/landscape/client/manager/aptsources.py --- landscape-client-19.12/landscape/client/manager/aptsources.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/manager/aptsources.py 2023-02-07 18:55:50.000000000 +0000 @@ -4,6 +4,7 @@ import grp import shutil import tempfile +import uuid from twisted.internet.defer import succeed @@ -22,6 +23,7 @@ SOURCES_LIST = "/etc/apt/sources.list" SOURCES_LIST_D = "/etc/apt/sources.list.d" + TRUSTED_GPG_D = "/etc/apt/trusted.gpg.d" def register(self, registry): super(AptSources, self).register(registry) @@ -83,16 +85,12 @@ "-----END PGP PUBLIC KEY BLOCK-----"]} """ deferred = succeed(None) + prefix = 'landscape-server-' for key in message["gpg-keys"]: - fd, path = tempfile.mkstemp() - os.close(fd) - with open(path, "w") as key_file: + filename = prefix + str(uuid.uuid4()) + '.asc' + key_path = os.path.join(self.TRUSTED_GPG_D, filename) + with open(key_path, "w") as key_file: key_file.write(key) - deferred.addCallback( - lambda ignore, path=path: - self._run_process("/usr/bin/apt-key", ["add", path])) - deferred.addCallback(self._handle_process_error) - deferred.addBoth(self._remove_and_continue, path) deferred.addErrback(self._handle_process_failure) deferred.addCallback(self._handle_sources, message["sources"]) return self.call_with_operation_result(message, lambda: deferred) diff -Nru landscape-client-19.12/landscape/client/manager/config.py landscape-client-23.02/landscape/client/manager/config.py --- landscape-client-19.12/landscape/client/manager/config.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/manager/config.py 2023-02-07 18:55:50.000000000 +0000 @@ -30,6 +30,14 @@ help="Comma-delimited list of usernames that scripts" " may be run as. Default is to allow all " "users.") + parser.add_option("--script-output-limit", + metavar="SCRIPT_OUTPUT_LIMIT", + type="int", default=512, + help="Maximum allowed output size that scripts" + " can send. " + "Script output will be truncated at that limit." + " Default is 512 (kB)") + return parser @property diff -Nru landscape-client-19.12/landscape/client/manager/keystonetoken.py landscape-client-23.02/landscape/client/manager/keystonetoken.py --- landscape-client-19.12/landscape/client/manager/keystonetoken.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/manager/keystonetoken.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,7 +1,7 @@ import os import logging -from twisted.python.compat import _PY3 +from landscape.lib.compat import _PY3 from landscape.lib.compat import ConfigParser, NoOptionError from landscape.client.monitor.plugin import DataWatcher diff -Nru landscape-client-19.12/landscape/client/manager/scriptexecution.py landscape-client-23.02/landscape/client/manager/scriptexecution.py --- landscape-client-19.12/landscape/client/manager/scriptexecution.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/manager/scriptexecution.py 2023-02-07 18:55:50.000000000 +0000 @@ -115,7 +115,8 @@ } pp = ProcessAccumulationProtocol( - self.registry.reactor, self.size_limit, self.truncation_indicator) + self.registry.reactor, self.registry.config.script_output_limit, + self.truncation_indicator) args = (filename,) self.process_factory.spawnProcess( pp, filename, args=args, uid=uid, gid=gid, path=path, env=env) @@ -127,11 +128,8 @@ class ScriptExecutionPlugin(ManagerPlugin, ScriptRunnerMixin): """A plugin which allows execution of arbitrary shell scripts. - @ivar size_limit: The number of bytes at which to truncate process output. """ - size_limit = 500000 - def register(self, registry): super(ScriptExecutionPlugin, self).register(registry) registry.register_message( @@ -316,7 +314,7 @@ self._size = 0 self.result_deferred = Deferred() self._cancelled = False - self.size_limit = size_limit + self.size_limit = size_limit * 1024 self._truncation_indicator = truncation_indicator.encode("utf-8") self._truncation_offset = len(self._truncation_indicator) self._truncated_size_limit = self.size_limit - self._truncation_offset diff -Nru landscape-client-19.12/landscape/client/manager/service.py landscape-client-23.02/landscape/client/manager/service.py --- landscape-client-19.12/landscape/client/manager/service.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/manager/service.py 2023-02-07 18:55:50.000000000 +0000 @@ -54,8 +54,9 @@ def stopService(self): """Stop the manager and close the connection with the broker.""" self.connector.disconnect() - self.publisher.stop() + deferred = self.publisher.stop() super(ManagerService, self).stopService() + return deferred def run(args): diff -Nru landscape-client-19.12/landscape/client/manager/tests/test_aptsources.py landscape-client-23.02/landscape/client/manager/tests/test_aptsources.py --- landscape-client-19.12/landscape/client/manager/tests/test_aptsources.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/manager/tests/test_aptsources.py 2023-02-07 18:55:50.000000000 +0000 @@ -7,7 +7,6 @@ from landscape.client.manager.aptsources import AptSources from landscape.client.manager.plugin import SUCCEEDED, FAILED -from landscape.lib.twisted_util import gather_results, SignalError from landscape.client.tests.helpers import LandscapeTest, ManagerHelper from landscape.client.package.reporter import find_reporter_command @@ -272,210 +271,23 @@ C{AptSources} runs a process with apt-key for every keys in the message. """ - deferred = Deferred() - def _run_process(command, args, env={}, path=None, uid=None, gid=None): - self.assertEqual("/usr/bin/apt-key", command) - self.assertEqual("add", args[0]) - filename = args[1] - with open(filename) as file: - result = file.read() - self.assertEqual("Some key content", result) - deferred.callback(("ok", "", 0)) - return deferred - - self.sourceslist._run_process = _run_process - - self.manager.dispatch_message( - {"type": "apt-sources-replace", "sources": [], - "gpg-keys": ["Some key content"], "operation-id": 1}) - - return deferred - - def test_import_delete_temporary_files(self): - """ - The files created to be imported by C{apt-key} are removed after the - import. - """ - deferred = Deferred() - filenames = [] - - def _run_process(command, args, env={}, path=None, uid=None, gid=None): - if not filenames: - filenames.append(args[1]) - deferred.callback(("ok", "", 0)) - return deferred - - self.sourceslist._run_process = _run_process + self.sourceslist.TRUSTED_GPG_D = self.makeDir() + gpg_keys = ["key1", "key2"] self.manager.dispatch_message( {"type": "apt-sources-replace", "sources": [], - "gpg-keys": ["Some key content"], "operation-id": 1}) - - self.assertFalse(os.path.exists(filenames[0])) - - return deferred - - def test_failed_import_delete_temporary_files(self): - """ - The files created to be imported by C{apt-key} are removed after the - import, even if there is a failure. - """ - deferred = Deferred() - filenames = [] - - def _run_process(command, args, env={}, path=None, uid=None, gid=None): - filenames.append(args[1]) - deferred.callback(("error", "", 1)) - return deferred - - self.sourceslist._run_process = _run_process - - self.manager.dispatch_message( - {"type": "apt-sources-replace", "sources": [], - "gpg-keys": ["Some key content"], "operation-id": 1}) - - self.assertFalse(os.path.exists(filenames[0])) - - return deferred - - def test_failed_import_reported(self): - """ - If the C{apt-key} command failed for some reasons, the output of the - command is reported and the activity fails. - """ - deferred = Deferred() - - def _run_process(command, args, env={}, path=None, uid=None, gid=None): - deferred.callback(("nok", "some error", 1)) - return deferred - - self.sourceslist._run_process = _run_process - - self.manager.dispatch_message( - {"type": "apt-sources-replace", "sources": [], "gpg-keys": ["key"], + "gpg-keys": gpg_keys, "operation-id": 1}) - service = self.broker_service - msg = "ProcessError: nok\nsome error" - self.assertMessages(service.message_store.get_pending_messages(), - [{"type": "operation-result", - "result-text": msg, "status": FAILED, - "operation-id": 1}]) - return deferred - - def test_signaled_import_reported(self): - """ - If the C{apt-key} fails with a signal, the output of the command is - reported and the activity fails. - """ - deferred = Deferred() - - def _run_process(command, args, env={}, path=None, uid=None, gid=None): - deferred.errback(SignalError("nok", "some error", 1)) - return deferred - - self.sourceslist._run_process = _run_process + keys = [] + gpg_dirpath = self.sourceslist.TRUSTED_GPG_D + for filename in os.listdir(gpg_dirpath): + filepath = os.path.join(gpg_dirpath, filename) + with open(filepath, 'r') as fh: + keys.append(fh.read()) - self.manager.dispatch_message( - {"type": "apt-sources-replace", "sources": [], "gpg-keys": ["key"], - "operation-id": 1}) - - service = self.broker_service - msg = "ProcessError: nok\nsome error" - self.assertMessages(service.message_store.get_pending_messages(), - [{"type": "operation-result", - "result-text": msg, "status": FAILED, - "operation-id": 1}]) - return deferred - - def test_failed_import_no_changes(self): - """ - If the C{apt-key} command failed for some reasons, the current - repositories aren't changed. - """ - deferred = Deferred() - - def _run_process(command, args, env={}, path=None, uid=None, gid=None): - deferred.callback(("nok", "some error", 1)) - return deferred - - self.sourceslist._run_process = _run_process - - with open(self.sourceslist.SOURCES_LIST, "w") as sources: - sources.write("oki\n\ndoki\n#comment\n") - - self.manager.dispatch_message( - {"type": "apt-sources-replace", "sources": [], "gpg-keys": ["key"], - "operation-id": 1}) - - with open(self.sourceslist.SOURCES_LIST) as sources_list: - result = sources_list.read() - - self.assertEqual("oki\n\ndoki\n#comment\n", result) - - return deferred - - def test_multiple_import_sequential(self): - """ - If multiple keys are specified, the imports run sequentially, not in - parallel. - """ - deferred1 = Deferred() - deferred2 = Deferred() - deferreds = [deferred1, deferred2] - - def _run_process(command, args, env={}, path=None, uid=None, gid=None): - if not deferreds: - return succeed(None) - return deferreds.pop(0) - - self.sourceslist._run_process = _run_process - - self.manager.dispatch_message( - {"type": "apt-sources-replace", "sources": [], - "gpg-keys": ["key1", "key2"], "operation-id": 1}) - - self.assertEqual(1, len(deferreds)) - deferred1.callback(("ok", "", 0)) - - self.assertEqual(0, len(deferreds)) - deferred2.callback(("ok", "", 0)) - - service = self.broker_service - self.assertMessages(service.message_store.get_pending_messages(), - [{"type": "operation-result", - "status": SUCCEEDED, "operation-id": 1}]) - return gather_results(deferreds) - - def test_multiple_import_failure(self): - """ - If multiple keys are specified, and that the first one fails, the error - is correctly reported. - """ - deferred1 = Deferred() - deferred2 = Deferred() - deferreds = [deferred1, deferred2] - - def _run_process(command, args, env={}, path=None, uid=None, gid=None): - return deferreds.pop(0) - - self.sourceslist._run_process = _run_process - - self.manager.dispatch_message( - {"type": "apt-sources-replace", "sources": [], - "gpg-keys": ["key1", "key2"], "operation-id": 1}) - - deferred1.callback(("error", "", 1)) - deferred2.callback(("error", "", 1)) - - msg = "ProcessError: error\n" - service = self.broker_service - self.assertMessages(service.message_store.get_pending_messages(), - [{"type": "operation-result", - "result-text": msg, "status": FAILED, - "operation-id": 1}]) - return gather_results(deferreds) + self.assertCountEqual(keys, gpg_keys) def test_run_reporter(self): """ diff -Nru landscape-client-19.12/landscape/client/manager/tests/test_processkiller.py landscape-client-23.02/landscape/client/manager/tests/test_processkiller.py --- landscape-client-19.12/landscape/client/manager/tests/test_processkiller.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/manager/tests/test_processkiller.py 2023-02-07 18:55:50.000000000 +0000 @@ -14,7 +14,7 @@ def get_active_process(): - return subprocess.Popen(["python", "-c", "raw_input()"], + return subprocess.Popen(["python3", "-c", "input()"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) diff -Nru landscape-client-19.12/landscape/client/manager/tests/test_scriptexecution.py landscape-client-23.02/landscape/client/manager/tests/test_scriptexecution.py --- landscape-client-19.12/landscape/client/manager/tests/test_scriptexecution.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/manager/tests/test_scriptexecution.py 2023-02-07 18:55:50.000000000 +0000 @@ -70,7 +70,7 @@ def test_other_interpreter(self): """Non-shell interpreters can be specified.""" - result = self.plugin.run_script("/usr/bin/python", "print 'hi'") + result = self.plugin.run_script("/usr/bin/python3", "print('hi')") result.addCallback(self.assertEqual, "hi\n") return result @@ -492,18 +492,18 @@ """Data returned from the command is limited.""" factory = StubProcessFactory() self.plugin.process_factory = factory - self.plugin.size_limit = 100 + self.manager.config.script_output_limit = 1 result = self.plugin.run_script("/bin/sh", "") # Ultimately we assert that the resulting output is limited to - # 100 bytes and indicates its truncation. + # 1024 bytes and indicates its truncation. result.addCallback(self.assertEqual, - ("x" * 79) + "\n**OUTPUT TRUNCATED**") + ("x" * (1024 - 21)) + "\n**OUTPUT TRUNCATED**") protocol = factory.spawns[0][0] - # Push 200 bytes of output, so we trigger truncation. - protocol.childDataReceived(1, b"x" * 200) + # Push 2kB of output, so we trigger truncation. + protocol.childDataReceived(1, b"x" * (2*1024)) for fd in (0, 1, 2): protocol.childConnectionLost(fd) @@ -515,19 +515,19 @@ """After truncation, no further output is recorded.""" factory = StubProcessFactory() self.plugin.process_factory = factory - self.plugin.size_limit = 100 + self.manager.config.script_output_limit = 1 result = self.plugin.run_script("/bin/sh", "") # Ultimately we assert that the resulting output is limited to - # 100 bytes and indicates its truncation. + # 1024 bytes and indicates its truncation. result.addCallback(self.assertEqual, - ("x" * 79) + "\n**OUTPUT TRUNCATED**") + ("x" * (1024 - 21)) + "\n**OUTPUT TRUNCATED**") protocol = factory.spawns[0][0] - # Push 200 bytes of output, so we trigger truncation. - protocol.childDataReceived(1, b"x" * 200) - # Push 200 bytes more - protocol.childDataReceived(1, b"x" * 200) + # Push 1024 bytes of output, so we trigger truncation. + protocol.childDataReceived(1, b"x" * 1024) + # Push 1024 bytes more + protocol.childDataReceived(1, b"x" * 1024) for fd in (0, 1, 2): protocol.childConnectionLost(fd) diff -Nru landscape-client-19.12/landscape/client/monitor/__init__.py landscape-client-23.02/landscape/client/monitor/__init__.py --- landscape-client-19.12/landscape/client/monitor/__init__.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/monitor/__init__.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,4 +1,4 @@ """ The monitor extracts data about the local machine and sends it in -messages to the Landcsape server via the broker. +messages to the Landscape server via the broker. """ diff -Nru landscape-client-19.12/landscape/client/monitor/computertags.py landscape-client-23.02/landscape/client/monitor/computertags.py --- landscape-client-19.12/landscape/client/monitor/computertags.py 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/landscape/client/monitor/computertags.py 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,29 @@ +import logging +import sys + +from landscape.client.broker.config import BrokerConfiguration +from landscape.client.monitor.plugin import DataWatcher +from landscape.lib.tag import is_valid_tag_list + + +class ComputerTags(DataWatcher): + """Plugin watches config file for changes in computer tags""" + + persist_name = "computer-tags" + message_type = "computer-tags" + message_key = "tags" + run_interval = 3600 # Every hour only when data changed + run_immediately = True + + def __init__(self, args=sys.argv): + super(ComputerTags, self).__init__() + self.args = args # Defined to specify args in unit tests + + def get_data(self): + config = BrokerConfiguration() + config.load(self.args) # Load the default or specified config + tags = config.tags + if not is_valid_tag_list(tags): + tags = None + logging.warning("Invalid tags provided for computer-tags message.") + return tags diff -Nru landscape-client-19.12/landscape/client/monitor/config.py landscape-client-23.02/landscape/client/monitor/config.py --- landscape-client-19.12/landscape/client/monitor/config.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/monitor/config.py 2023-02-07 18:55:50.000000000 +0000 @@ -6,7 +6,7 @@ "Temperature", "PackageMonitor", "UserMonitor", "RebootRequired", "AptPreferences", "NetworkActivity", "NetworkDevice", "UpdateManager", "CPUUsage", "SwiftUsage", - "CephUsage"] + "CephUsage", "ComputerTags", "UbuntuProInfo"] class MonitorConfiguration(Configuration): diff -Nru landscape-client-19.12/landscape/client/monitor/processorinfo.py landscape-client-23.02/landscape/client/monitor/processorinfo.py --- landscape-client-19.12/landscape/client/monitor/processorinfo.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/monitor/processorinfo.py 2023-02-07 18:55:50.000000000 +0000 @@ -87,7 +87,8 @@ dirty = True if dirty: - logging.info("Queueing message with updated processor info.") + logging.info("Queueing updated processor info. Contents:") + logging.info(message) self.registry.broker.send_message( message, self._session_id, urgent=urgent) @@ -313,8 +314,47 @@ return processors +class RISCVMessageFactory: + """Factory for risc-v-based processors provides processor information. + + @param source_filename: The file name of the data source. + """ + + def __init__(self, source_filename): + self._source_filename = source_filename + + def create_message(self): + """Returns a list containing information about each processor.""" + processors = [] + file = open(self._source_filename) + logging.info("Entered RISCVMessageFactory") + + try: + current = None + + for line in file: + parts = line.split(":", 1) + key = parts[0].strip() + + if key == "processor": + current = {"processor-id": int(parts[1].strip())} + processors.append(current) + elif key == "isa": + current["vendor"] = parts[1].strip() + elif key == "uarch": + current["model"] = parts[1].strip() + + finally: + file.close() + + logging.info("RISC-V processor info collected:") + logging.info(processors) + return processors + + message_factories = [("(arm*|aarch64)", ARMMessageFactory), ("ppc(64)?", PowerPCMessageFactory), ("sparc[64]", SparcMessageFactory), ("i[3-7]86|x86_64", X86MessageFactory), - ("s390x", S390XMessageFactory)] + ("s390x", S390XMessageFactory), + ("riscv64", RISCVMessageFactory)] diff -Nru landscape-client-19.12/landscape/client/monitor/service.py landscape-client-23.02/landscape/client/monitor/service.py --- landscape-client-19.12/landscape/client/monitor/service.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/monitor/service.py 2023-02-07 18:55:50.000000000 +0000 @@ -56,10 +56,11 @@ The monitor is flushed to ensure that things like persist databases get saved to disk. """ - self.publisher.stop() + deferred = self.publisher.stop() self.monitor.flush() self.connector.disconnect() super(MonitorService, self).stopService() + return deferred def run(args): diff -Nru landscape-client-19.12/landscape/client/monitor/tests/test_computertags.py landscape-client-23.02/landscape/client/monitor/tests/test_computertags.py --- landscape-client-19.12/landscape/client/monitor/tests/test_computertags.py 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/landscape/client/monitor/tests/test_computertags.py 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,64 @@ +from landscape.client.monitor.computertags import ComputerTags +from landscape.client.tests.helpers import MonitorHelper, LandscapeTest + + +class ComputerTagsTest(LandscapeTest): + + helpers = [MonitorHelper] + + def setUp(self): + super(ComputerTagsTest, self).setUp() + test_sys_args = ['hello.py'] + self.plugin = ComputerTags(args=test_sys_args) + self.monitor.add(self.plugin) + + def test_tags_are_read(self): + """ + Tags are read from the default config path + """ + tags = 'check,linode,profile-test' + file_text = "[client]\ntags = {}".format(tags) + config_filename = self.config.default_config_filenames[0] + self.makeFile(file_text, path=config_filename) + self.assertEqual(self.plugin.get_data(), tags) + + def test_tags_are_read_from_args_path(self): + """ + Tags are read from path specified in command line args + """ + tags = 'check,linode,profile-test' + file_text = "[client]\ntags = {}".format(tags) + filename = self.makeFile(file_text) + test_sys_args = ['hello.py', '--config', filename] + self.plugin.args = test_sys_args + self.assertEqual(self.plugin.get_data(), tags) + + def test_tags_message_sent(self): + """ + Tags message is sent correctly + """ + tags = 'check,linode,profile-test' + file_text = "[client]\ntags = {}".format(tags) + config_filename = self.config.default_config_filenames[0] + self.makeFile(file_text, path=config_filename) + + self.mstore.set_accepted_types(["computer-tags"]) + self.plugin.exchange() + messages = self.mstore.get_pending_messages() + self.assertEqual(messages[0]['tags'], tags) + + def test_invalid_tags(self): + """ + If invalid tag detected then message contents should be None + """ + tags = 'check,lin ode' + file_text = "[client]\ntags = {}".format(tags) + config_filename = self.config.default_config_filenames[0] + self.makeFile(file_text, path=config_filename) + self.assertEqual(self.plugin.get_data(), None) + + def test_empty_tags(self): + """ + Makes sure no errors when tags section is empty + """ + self.assertEqual(self.plugin.get_data(), None) diff -Nru landscape-client-19.12/landscape/client/monitor/tests/test_ubuntuproinfo.py landscape-client-23.02/landscape/client/monitor/tests/test_ubuntuproinfo.py --- landscape-client-19.12/landscape/client/monitor/tests/test_ubuntuproinfo.py 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/landscape/client/monitor/tests/test_ubuntuproinfo.py 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,47 @@ +from unittest import mock + +from landscape.client.monitor.ubuntuproinfo import UbuntuProInfo +from landscape.client.tests.helpers import LandscapeTest, MonitorHelper + + +class UbuntuProInfoTest(LandscapeTest): + """Ubuntu Pro info plugin tests.""" + + helpers = [MonitorHelper] + + def setUp(self): + super(UbuntuProInfoTest, self).setUp() + self.mstore.set_accepted_types(["ubuntu-pro-info"]) + + def test_ubuntu_pro_info(self): + """Tests calling `ua status`.""" + plugin = UbuntuProInfo() + self.monitor.add(plugin) + + with mock.patch("subprocess.run") as run_mock: + run_mock.return_value = mock.Mock( + stdout="\"This is a test\"", + ) + plugin.exchange() + + messages = self.mstore.get_pending_messages() + run_mock.assert_called_once() + self.assertTrue(len(messages) > 0) + self.assertTrue("ubuntu-pro-info" in messages[0]) + self.assertEqual(messages[0]["ubuntu-pro-info"], + "\"This is a test\"") + + def test_ubuntu_pro_info_no_ua(self): + """Tests calling `ua status` when it is not installed.""" + plugin = UbuntuProInfo() + self.monitor.add(plugin) + + with mock.patch("subprocess.run") as run_mock: + run_mock.side_effect = FileNotFoundError() + plugin.exchange() + + messages = self.mstore.get_pending_messages() + run_mock.assert_called_once() + self.assertTrue(len(messages) > 0) + self.assertTrue("ubuntu-pro-info" in messages[0]) + self.assertIn("errors", messages[0]["ubuntu-pro-info"]) diff -Nru landscape-client-19.12/landscape/client/monitor/ubuntuproinfo.py landscape-client-23.02/landscape/client/monitor/ubuntuproinfo.py --- landscape-client-19.12/landscape/client/monitor/ubuntuproinfo.py 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/landscape/client/monitor/ubuntuproinfo.py 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,47 @@ +import json +import subprocess + +from landscape.client.monitor.plugin import DataWatcher + + +class UbuntuProInfo(DataWatcher): + """ + Plugin that captures and reports Ubuntu Pro registration + information. + + We use the `ua` CLI with output formatted as JSON. This is sent + as-is and parsed by Landscape Server because the JSON content is + considered "Experimental" and we don't want to have to change in + both Client and Server in the event that the format changes. + """ + + run_interval = 900 # 15 minutes + message_type = "ubuntu-pro-info" + message_key = message_type + persist_name = message_type + scope = "ubuntu-pro" + run_immediately = True + + def get_data(self): + ubuntu_pro_info = get_ubuntu_pro_info() + + return json.dumps(ubuntu_pro_info, separators=(",", ":")) + + +def get_ubuntu_pro_info(): + try: + completed_process = subprocess.run( + ["ua", "status", "--format", "json"], + encoding="utf8", stdout=subprocess.PIPE) + except FileNotFoundError: + return { + "errors": [{ + "message": "ubuntu-advantage-tools not found.", + "message_code": "tools-error", + "service": None, + "type": "system", + }], + "result": "failure", + } + else: + return json.loads(completed_process.stdout) diff -Nru landscape-client-19.12/landscape/client/package/changer.py landscape-client-23.02/landscape/client/package/changer.py --- landscape-client-19.12/landscape/client/package/changer.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/package/changer.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,5 +1,4 @@ import logging -import base64 import time import os import pwd @@ -14,6 +13,7 @@ UNKNOWN_PACKAGE_DATA_TIMEOUT) from landscape.lib.config import get_bindir +from landscape.lib import base64 from landscape.lib.fs import create_binary_file from landscape.lib.log import log_failure from landscape.client.package.reporter import find_reporter_command @@ -173,7 +173,7 @@ hash_ids = {} for hash, id, deb in binaries: create_binary_file(os.path.join(binaries_path, "%d.deb" % id), - base64.decodestring(deb)) + base64.decodebytes(deb)) hash_ids[hash] = id self._store.set_hash_ids(hash_ids) self._facade.add_channel_deb_dir(binaries_path) diff -Nru landscape-client-19.12/landscape/client/package/reporter.py landscape-client-23.02/landscape/client/package/reporter.py --- landscape-client-19.12/landscape/client/package/reporter.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/package/reporter.py 2023-02-07 18:55:50.000000000 +0000 @@ -3,6 +3,7 @@ except ImportError: import urllib.parse as urlparse +import locale import logging import time import os @@ -866,6 +867,10 @@ def main(args): + # Force UTF-8 encoding only for the reporter, thus allowing libapt-pkg to + # return unmangled descriptions. + locale.setlocale(locale.LC_CTYPE, ("C", "UTF-8")) + if "FAKE_GLOBAL_PACKAGE_STORE" in os.environ: return run_task_handler(FakeGlobalReporter, args) elif "FAKE_PACKAGE_STORE" in os.environ: diff -Nru landscape-client-19.12/landscape/client/package/tests/test_changer.py landscape-client-23.02/landscape/client/package/tests/test_changer.py --- landscape-client-19.12/landscape/client/package/tests/test_changer.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/package/tests/test_changer.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,5 +1,4 @@ # -*- encoding: utf-8 -*- -import base64 import time import sys import os @@ -16,6 +15,7 @@ from landscape.lib.apt.package.testing import ( HASH1, HASH2, HASH3, PKGDEB1, PKGDEB2, AptFacadeHelper, SimpleRepositoryHelper) +from landscape.lib import base64 from landscape.lib.fs import create_text_file, read_text_file, touch_file from landscape.lib.testing import StubProcessFactory, FakeReactor from landscape.client.package.changer import ( @@ -849,9 +849,9 @@ binaries_path = self.config.binaries_path self.assertFileContent(os.path.join(binaries_path, "111.deb"), - base64.decodestring(PKGDEB1)) + base64.decodebytes(PKGDEB1)) self.assertFileContent(os.path.join(binaries_path, "222.deb"), - base64.decodestring(PKGDEB2)) + base64.decodebytes(PKGDEB2)) self.assertEqual( self.facade.get_channels(), self.get_binaries_channels(binaries_path)) diff -Nru landscape-client-19.12/landscape/client/package/tests/test_releaseupgrader.py landscape-client-23.02/landscape/client/package/tests/test_releaseupgrader.py --- landscape-client-19.12/landscape/client/package/tests/test_releaseupgrader.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/package/tests/test_releaseupgrader.py 2023-02-07 18:55:50.000000000 +0000 @@ -433,12 +433,12 @@ upgrade_tool_filename = os.path.join(upgrade_tool_directory, "karmic") child_pid_filename = self.makeFile() fd = open(upgrade_tool_filename, "w") - fd.write("#!/usr/bin/env python\n" + fd.write("#!/usr/bin/env python3\n" "import os\n" "import time\n" "import sys\n" "if __name__ == '__main__':\n" - " print 'First parent'\n" + " print('First parent')\n" " pid = os.fork()\n" " if pid > 0:\n" " time.sleep(0.5)\n" diff -Nru landscape-client-19.12/landscape/client/package/tests/test_reporter.py landscape-client-23.02/landscape/client/package/tests/test_reporter.py --- landscape-client-19.12/landscape/client/package/tests/test_reporter.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/package/tests/test_reporter.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,3 +1,4 @@ +import locale import sys import os import time @@ -1231,6 +1232,31 @@ self.assertEqual("RESULT", main(["ARGS"])) m.assert_called_once_with(PackageReporter, ["ARGS"]) + def test_main_resets_locale(self): + """ + Reporter entry point should reset encoding to utf-8, as libapt-pkg + encodes description with system encoding and python-apt decodes + them as utf-8 (LP: #1827857). + """ + self._add_package_to_deb_dir( + self.repository_dir, "gosa", description=u"GOsa\u00B2") + self.facade.reload_channels() + + # Set the only non-utf8 locale which we're sure exists. + # It behaves slightly differently than the bug, but fails on the + # same condition. + locale.setlocale(locale.LC_CTYPE, (None, None)) + self.addCleanup(locale.resetlocale) + + with mock.patch("landscape.client.package.reporter.run_task_handler"): + main([]) + + # With the actual package, the failure will occur looking up the + # description translation. + pkg = self.facade.get_packages_by_name("gosa")[0] + skel = self.facade.get_package_skeleton(pkg, with_info=True) + self.assertEqual(u"GOsa\u00B2", skel.description) + def test_find_reporter_command_with_bindir(self): self.config.bindir = "/spam/eggs" command = find_reporter_command(self.config) diff -Nru landscape-client-19.12/landscape/client/package/tests/test_taskhandler.py landscape-client-23.02/landscape/client/package/tests/test_taskhandler.py --- landscape-client-19.12/landscape/client/package/tests/test_taskhandler.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/package/tests/test_taskhandler.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,4 +1,5 @@ import os +from subprocess import CalledProcessError from mock import patch, Mock, ANY @@ -103,7 +104,9 @@ self.handler.lsb_release_filename = self.makeFile() # Go! - result = self.handler.use_hash_id_db() + with patch("landscape.lib.lsb_release.check_output") as co_mock: + co_mock.side_effect = CalledProcessError(127, "") + result = self.handler.use_hash_id_db() # The failure should be properly logged logging_mock.assert_called_with( diff -Nru landscape-client-19.12/landscape/client/reactor.py landscape-client-23.02/landscape/client/reactor.py --- landscape-client-19.12/landscape/client/reactor.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/reactor.py 2023-02-07 18:55:50.000000000 +0000 @@ -2,6 +2,9 @@ Extend the regular Twisted reactor with event-handling features. """ from landscape.lib.reactor import EventHandlingReactor +from landscape.client.lockfile import patch_lockfile + +patch_lockfile() class LandscapeReactor(EventHandlingReactor): diff -Nru landscape-client-19.12/landscape/client/tests/test_amp.py landscape-client-23.02/landscape/client/tests/test_amp.py --- landscape-client-19.12/landscape/client/tests/test_amp.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/tests/test_amp.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,9 +1,17 @@ -from twisted.internet.error import ConnectError +import os +import errno +import subprocess +import textwrap + +import mock + +from twisted.internet.error import ConnectError, CannotListenError from twisted.internet.task import Clock from landscape.client.tests.helpers import LandscapeTest from landscape.client.deployment import Configuration from landscape.client.amp import ComponentPublisher, ComponentConnector, remote +from landscape.client.reactor import LandscapeReactor from landscape.lib.amp import MethodCallError from landscape.lib.testing import FakeReactor @@ -159,3 +167,83 @@ effectively a no-op. """ self.connector.disconnect() + + @mock.patch("twisted.python.lockfile.kill") + def test_stale_locks_with_dead_pid(self, mock_kill): + """Publisher starts with stale lock.""" + mock_kill.side_effect = [ + OSError(errno.ESRCH, "No such process")] + sock_path = os.path.join(self.config.sockets_path, u"test.sock") + lock_path = u"{}.lock".format(sock_path) + # fake a PID which does not exist + os.symlink("-1", lock_path) + + component = TestComponent() + # Test the actual Unix reactor implementation. Fakes won't do. + reactor = LandscapeReactor() + publisher = ComponentPublisher(component, reactor, self.config) + + # Shouldn't raise the exception. + publisher.start() + + # ensure stale lock was replaced + self.assertNotEqual("-1", os.readlink(lock_path)) + mock_kill.assert_called_with(-1, 0) + + publisher.stop() + reactor._cleanup() + + @mock.patch("twisted.python.lockfile.kill") + def test_stale_locks_recycled_pid(self, mock_kill): + """Publisher starts with stale lock pointing to recycled process.""" + mock_kill.side_effect = [ + OSError(errno.EPERM, "Operation not permitted")] + sock_path = os.path.join(self.config.sockets_path, u"test.sock") + lock_path = u"{}.lock".format(sock_path) + # fake a PID recycled by a known process which isn't landscape (init) + os.symlink("1", lock_path) + + component = TestComponent() + # Test the actual Unix reactor implementation. Fakes won't do. + reactor = LandscapeReactor() + publisher = ComponentPublisher(component, reactor, self.config) + + # Shouldn't raise the exception. + publisher.start() + + # ensure stale lock was replaced + self.assertNotEqual("1", os.readlink(lock_path)) + mock_kill.assert_not_called() + self.assertFalse(publisher._port.lockFile.clean) + + publisher.stop() + reactor._cleanup() + + @mock.patch("twisted.python.lockfile.kill") + def test_with_valid_lock(self, mock_kill): + """Publisher raises lock error if a valid lock is held.""" + sock_path = os.path.join(self.config.sockets_path, u"test.sock") + lock_path = u"{}.lock".format(sock_path) + # fake a landscape process + app = self.makeFile(textwrap.dedent("""\ + #!/usr/bin/python3 + import time + time.sleep(10) + """), basename="landscape-manager") + os.chmod(app, 0o755) + call = subprocess.Popen([app]) + self.addCleanup(call.terminate) + os.symlink(str(call.pid), lock_path) + + component = TestComponent() + # Test the actual Unix reactor implementation. Fakes won't do. + reactor = LandscapeReactor() + publisher = ComponentPublisher(component, reactor, self.config) + + with self.assertRaises(CannotListenError): + publisher.start() + + # ensure lock was not replaced + self.assertEqual(str(call.pid), os.readlink(lock_path)) + mock_kill.assert_called_with(call.pid, 0) + reactor._cleanup() diff -Nru landscape-client-19.12/landscape/client/tests/test_configuration.py landscape-client-23.02/landscape/client/tests/test_configuration.py --- landscape-client-19.12/landscape/client/tests/test_configuration.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/tests/test_configuration.py 2023-02-07 18:55:50.000000000 +0000 @@ -4,6 +4,7 @@ from functools import partial import os import sys +import textwrap import unittest from twisted.internet.defer import succeed, fail, Deferred @@ -11,7 +12,7 @@ from landscape.lib.compat import ConfigParser from landscape.lib.compat import StringIO -from landscape.client.broker.registration import RegistrationError +from landscape.client.broker.registration import RegistrationError, Identity from landscape.client.broker.tests.helpers import ( RemoteBrokerHelper, BrokerConfigurationHelper) from landscape.client.configuration import ( @@ -21,7 +22,8 @@ ImportOptionError, store_public_key_data, bootstrap_tree, got_connection, success, failure, exchange_failure, handle_registration_errors, done, got_error, report_registration_outcome, - determine_exit_code, is_registered) + determine_exit_code, is_registered, registration_info_text, + EXIT_NOT_REGISTERED) from landscape.lib.amp import MethodCallError from landscape.lib.fetch import HTTPCodeError, PyCurlError from landscape.lib.fs import read_binary_file @@ -2233,3 +2235,72 @@ self.persist.set("registration.secure-id", "super-secure") self.persist.save() self.assertTrue(is_registered(self.config)) + + +class RegistrationInfoTest(LandscapeTest): + + helpers = [BrokerConfigurationHelper] + + def setUp(self): + super(RegistrationInfoTest, self).setUp() + self.custom_args = ['hello.py'] # Fake python script name + self.account_name = 'world' + self.data_path = self.makeDir() + self.config_text = textwrap.dedent(""" + [client] + computer_title = hello + account_name = {} + data_path = {} + """.format(self.account_name, self.data_path)) + + def test_not_registered(self): + '''False when client is not registered''' + config_filename = self.config.default_config_filenames[0] + self.makeFile(self.config_text, path=config_filename) + self.config.load(self.custom_args) + text = registration_info_text(self.config, False) + self.assertIn('False', text) + self.assertNotIn(self.account_name, text) + + def test_registered(self): + ''' + When client is registered, then the text should display as True and + account name should be present + ''' + config_filename = self.config.default_config_filenames[0] + self.makeFile(self.config_text, path=config_filename) + self.config.load(self.custom_args) + text = registration_info_text(self.config, True) + self.assertIn('True', text) + self.assertIn(self.account_name, text) + + def test_custom_config_path(self): + '''The custom config path should show up in the text''' + custom_path = self.makeFile(self.config_text) + self.custom_args += ['-c', custom_path] + self.config.load(self.custom_args) + text = registration_info_text(self.config, False) + self.assertIn(custom_path, text) + + def test_data_path(self): + '''The config data path should show in the text''' + config_filename = self.config.default_config_filenames[0] + self.makeFile(self.config_text, path=config_filename) + self.config.load(self.custom_args) + text = registration_info_text(self.config, False) + self.assertIn(self.data_path, text) + + def test_registered_exit_code(self): + '''Returns exit code zero when client is registered''' + Identity.secure_id = 'test' # Simulate successful registration + exception = self.assertRaises( + SystemExit, main, ["--is-registered", "--silent"], + print=noop_print) + self.assertEqual(0, exception.code) + + def test_not_registered_exit_code(self): + '''Returns special return code when client is not registered''' + exception = self.assertRaises( + SystemExit, main, ["--is-registered", "--silent"], + print=noop_print) + self.assertEqual(EXIT_NOT_REGISTERED, exception.code) diff -Nru landscape-client-19.12/landscape/client/tests/test_lockfile.py landscape-client-23.02/landscape/client/tests/test_lockfile.py --- landscape-client-19.12/landscape/client/tests/test_lockfile.py 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/landscape/client/tests/test_lockfile.py 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,21 @@ +import os +import subprocess +import textwrap + +from landscape.client import lockfile +from landscape.client.tests.helpers import LandscapeTest + + +class LockFileTest(LandscapeTest): + + def test_read_process_name(self): + app = self.makeFile(textwrap.dedent("""\ + #!/usr/bin/python3 + import time + time.sleep(10) + """), basename="my_fancy_app") + os.chmod(app, 0o755) + call = subprocess.Popen([app]) + self.addCleanup(call.terminate) + proc_name = lockfile.get_process_name(call.pid) + self.assertEqual("my_fancy_app", proc_name) diff -Nru landscape-client-19.12/landscape/client/user/provider.py landscape-client-23.02/landscape/client/user/provider.py --- landscape-client-19.12/landscape/client/user/provider.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/client/user/provider.py 2023-02-07 18:55:50.000000000 +0000 @@ -4,7 +4,7 @@ import logging import subprocess -from twisted.python.compat import _PY3 +from landscape.lib.compat import _PY3 class UserManagementError(Exception): diff -Nru landscape-client-19.12/landscape/lib/apt/package/facade.py landscape-client-23.02/landscape/lib/apt/package/facade.py --- landscape-client-19.12/landscape/lib/apt/package/facade.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/apt/package/facade.py 2023-02-07 18:55:50.000000000 +0000 @@ -164,9 +164,10 @@ self._dpkg_status = os.path.join(dpkg_dir, "status") if not os.path.exists(self._dpkg_status): create_text_file(self._dpkg_status, "") - # Apt will fail if it does not have a keyring. It does not care if - # the keyring is empty. - touch_file(os.path.join(apt_dir, "trusted.gpg")) + # Apt will fail if it does not have a keyring. It does not care if + # the keyring is empty. (Do not create one if dir exists LP: #1973202) + if not os.path.isdir(os.path.join(apt_dir, "trusted.gpg.d")): + touch_file(os.path.join(apt_dir, "trusted.gpg")) def _ensure_sub_dir(self, sub_dir): """Ensure that a dir in the Apt root exists.""" @@ -605,9 +606,12 @@ all_info = ["The following packages have unmet dependencies:"] for package in sorted(broken_packages, key=attrgetter("name")): found_dependency_error = False + # Fetch candidate version from our install list because + # apt-2.1.5 resets broken packages candidate. + candidate = next(v._cand for v in self._version_installs + if v.package == package) for dep_type in ["PreDepends", "Depends", "Conflicts", "Breaks"]: - dependencies = package.candidate._cand.depends_list.get( - dep_type, []) + dependencies = candidate.depends_list.get(dep_type, []) for dependency in dependencies: if self._is_dependency_satisfied(dependency, dep_type): continue @@ -744,9 +748,18 @@ # Set the candidate version, so that the version we want to # install actually is the one getting installed. version.package.candidate = version + + # Flag the package as manual if it's a new install, otherwise + # preserve the auto flag. This should preserve explicitly + # installed packages from auto-removal, while allowing upgrades + # of auto-removable packages. + is_manual = ( + not version.package.installed or + not version.package.is_auto_installed) + # Set auto_fix=False to avoid removing the package we asked to # install when we need to resolve dependencies. - version.package.mark_install(auto_fix=False) + version.package.mark_install(auto_fix=False, from_user=is_manual) self._package_installs.add(version.package) fixer.clear(version.package._pkg) fixer.protect(version.package._pkg) diff -Nru landscape-client-19.12/landscape/lib/apt/package/skeleton.py landscape-client-23.02/landscape/lib/apt/package/skeleton.py --- landscape-client-19.12/landscape/lib/apt/package/skeleton.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/apt/package/skeleton.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,9 +1,8 @@ +from landscape.lib.compat import unicode, _PY3 from landscape.lib.hashlib import sha1 import apt_pkg -from twisted.python.compat import unicode, _PY3 - PACKAGE = 1 << 0 PROVIDES = 1 << 1 diff -Nru landscape-client-19.12/landscape/lib/apt/package/testing.py landscape-client-23.02/landscape/lib/apt/package/testing.py --- landscape-client-19.12/landscape/lib/apt/package/testing.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/apt/package/testing.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,4 +1,3 @@ -import base64 import os import time @@ -8,6 +7,7 @@ from landscape.lib.apt.package.facade import AptFacade from landscape.lib.fs import append_binary_file from landscape.lib.fs import create_binary_file +from landscape.lib import base64 class AptFacadeHelper(object): @@ -322,9 +322,9 @@ ).encode("ascii") -HASH1 = base64.decodestring(b"/ezv4AefpJJ8DuYFSq4RiEHJYP4=") -HASH2 = base64.decodestring(b"glP4DwWOfMULm0AkRXYsH/exehc=") -HASH3 = base64.decodestring(b"NJM05mj86veaSInYxxqL1wahods=") +HASH1 = base64.decodebytes(b"/ezv4AefpJJ8DuYFSq4RiEHJYP4=") +HASH2 = base64.decodebytes(b"glP4DwWOfMULm0AkRXYsH/exehc=") +HASH3 = base64.decodebytes(b"NJM05mj86veaSInYxxqL1wahods=") HASH_MINIMAL = b"6\xce\x8f\x1bM\x82MWZ\x1a\xffjAc(\xdb(\xa1\x0eG" HASH_SIMPLE_RELATIONS = ( b"'#\xab&k\xe6\xf5E\xcfB\x9b\xceO7\xe6\xec\xa9\xddY\xaa") @@ -339,7 +339,7 @@ def create_deb(target_dir, pkg_name, pkg_data): """Create a Debian package in the specified C{target_dir}.""" path = os.path.join(target_dir, pkg_name) - data = base64.decodestring(pkg_data) + data = base64.decodebytes(pkg_data) create_binary_file(path, data) diff -Nru landscape-client-19.12/landscape/lib/apt/package/tests/test_facade.py landscape-client-23.02/landscape/lib/apt/package/tests/test_facade.py --- landscape-client-19.12/landscape/lib/apt/package/tests/test_facade.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/apt/package/tests/test_facade.py 2023-02-07 18:55:50.000000000 +0000 @@ -1812,6 +1812,7 @@ self._add_system_package("foo") self.facade.reload_channels() [foo] = self.facade.get_packages_by_name("foo") + self.facade._version_installs.append(foo) self.facade._get_broken_packages = lambda: set([foo.package]) self.assertEqual( ["The following packages have unmet dependencies:", @@ -2601,6 +2602,72 @@ [bar] = self.facade._cache.get_changes() self.assertTrue(bar.marked_upgrade) + def test_changer_upgrade_keeps_auto(self): + """ + An upgrade request should preserve an existing auto flag on the + upgraded package. + """ + self._add_system_package( + "foo", control_fields={"Depends": "bar"}) + self._add_system_package("bar", version="1.0") + deb_dir = self.makeDir() + self._add_package_to_deb_dir(deb_dir, "bar", version="2.0") + self.facade.add_channel_apt_deb( + "file://%s" % deb_dir, "./", trusted=True) + self.facade.reload_channels() + bar_1, bar_2 = sorted(self.facade.get_packages_by_name("bar")) + bar_1.package.mark_auto() + + self.facade.mark_install(bar_2) + self.facade.mark_remove(bar_1) + self.patch_cache_commit() + self.facade.perform_changes() + [bar] = self.facade._cache.get_changes() + self.assertTrue(bar.marked_upgrade) + self.assertTrue(bar.is_auto_installed) + + def test_changer_upgrade_keeps_manual(self): + """ + An upgrade request should mark a package as manual if the installed + version is manual. + """ + self._add_system_package( + "foo", control_fields={"Depends": "bar"}) + self._add_system_package("bar", version="1.0") + deb_dir = self.makeDir() + self._add_package_to_deb_dir(deb_dir, "bar", version="2.0") + self.facade.add_channel_apt_deb( + "file://%s" % deb_dir, "./", trusted=True) + self.facade.reload_channels() + bar_1, bar_2 = sorted(self.facade.get_packages_by_name("bar")) + + self.facade.mark_install(bar_2) + self.facade.mark_remove(bar_1) + self.patch_cache_commit() + self.facade.perform_changes() + [bar] = self.facade._cache.get_changes() + self.assertTrue(bar.marked_upgrade) + self.assertFalse(bar.is_auto_installed) + + def test_changer_install_sets_manual(self): + """ + An installation request should mark the new package as manually + installed. + """ + deb_dir = self.makeDir() + self._add_package_to_deb_dir(deb_dir, "bar", version="2.0") + self.facade.add_channel_apt_deb( + "file://%s" % deb_dir, "./", trusted=True) + self.facade.reload_channels() + bar_2, = self.facade.get_packages_by_name("bar") + + self.facade.mark_install(bar_2) + self.patch_cache_commit() + self.facade.perform_changes() + [bar] = self.facade._cache.get_changes() + self.assertTrue(bar.marked_upgrade) + self.assertFalse(bar.is_auto_installed) + def test_mark_global_upgrade_held_packages(self): """ If a package that is on hold is marked for upgrade, diff -Nru landscape-client-19.12/landscape/lib/apt/package/tests/test_skeleton.py landscape-client-23.02/landscape/lib/apt/package/tests/test_skeleton.py --- landscape-client-19.12/landscape/lib/apt/package/tests/test_skeleton.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/apt/package/tests/test_skeleton.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,3 +1,4 @@ +import locale import unittest from landscape.lib import testing @@ -147,15 +148,22 @@ def test_build_skeleton_with_unicode_and_non_ascii(self): """ If with_unicode and with_info are passed to build_skeleton_apt, - the description is decoded and non-ascii chars replaced. + the description is decoded. """ + # Py2 used to convert to lossy ascii (thus LC_ in Makefile) + # Py3 doesn't, and python3-apt assumes UTF8 (LP: #1827857). + # If you revisit this test, also check reporter.main(), which + # should set this globally to the reporter process. + locale.setlocale(locale.LC_CTYPE, "C.UTF-8") + self.addCleanup(locale.resetlocale) + self._add_package_to_deb_dir( self.skeleton_repository_dir, "pkg", description=u"T\xe9st") self.facade._cache.update(None) self.facade._cache.open(None) pkg = self.get_package("pkg") skeleton = build_skeleton_apt(pkg, with_unicode=True, with_info=True) - self.assertEqual(u"T?st", skeleton.description) + self.assertEqual(u"T\u00E9st", skeleton.description) def test_build_skeleton_minimal(self): """ diff -Nru landscape-client-19.12/landscape/lib/backoff.py landscape-client-23.02/landscape/lib/backoff.py --- landscape-client-19.12/landscape/lib/backoff.py 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/landscape/lib/backoff.py 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,53 @@ +import random + + +class ExponentialBackoff: + """ + Keeps track of a backoff delay that staggers down and staggers up + exponentially. + """ + + def __init__(self, start_delay, max_delay): + + self._error_count = 0 # A tally of server errors + + self._start_delay = start_delay + self._max_delay = max_delay + + def decrease(self): + """Decreases error count with zero being the lowest""" + self._error_count -= 1 + self._error_count = max(self._error_count, 0) + + def increase(self): + """Increases error count but not higher than gives the max delay""" + if self.get_delay() < self._max_delay: + self._error_count += 1 + + def get_delay(self): + """ + Calculates the delay using formula that gives this chart. In this + specific example start is 5 seconds and max is 60 seconds + Count Delay + 0 0 + 1 5 + 2 10 + 3 20 + 4 40 + 5 60 (max) + """ + if self._error_count: + delay = (2 ** (self._error_count - 1)) * self._start_delay + else: + delay = 0 + return min(int(delay), self._max_delay) + + def get_random_delay(self, stagger_fraction=0.25): + """ + Adds randomness to the specified stagger of the delay. For example + for a delay of 12 and 25% stagger, it works out to 9 + rand(0,3) + """ + delay = self.get_delay() + non_random_part = delay * (1-stagger_fraction) + random_part = delay * stagger_fraction * random.random() + return int(non_random_part + random_part) diff -Nru landscape-client-19.12/landscape/lib/base64.py landscape-client-23.02/landscape/lib/base64.py --- landscape-client-19.12/landscape/lib/base64.py 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/landscape/lib/base64.py 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,8 @@ +from __future__ import absolute_import + +from landscape.lib.compat import _PY3 + +if _PY3: + from base64 import decodebytes # noqa +else: + from base64 import decodestring as decodebytes # noqa diff -Nru landscape-client-19.12/landscape/lib/bpickle.py landscape-client-23.02/landscape/lib/bpickle.py --- landscape-client-19.12/landscape/lib/bpickle.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/bpickle.py 2023-02-07 18:55:50.000000000 +0000 @@ -32,7 +32,7 @@ wire compatible and behave the same way (bugs notwithstanding). """ -from twisted.python.compat import _PY3 +from landscape.lib.compat import long, _PY3 dumps_table = {} loads_table = {} diff -Nru landscape-client-19.12/landscape/lib/compat.py landscape-client-23.02/landscape/lib/compat.py --- landscape-client-19.12/landscape/lib/compat.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/compat.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,6 +1,6 @@ # flake8: noqa -from twisted.python.compat import _PY3 +_PY3 = str != bytes if _PY3: @@ -13,6 +13,8 @@ from io import StringIO stringio = cstringio = StringIO from builtins import input + unicode = str + long = int else: import cPickle @@ -24,3 +26,5 @@ stringio = StringIO from cStringIO import StringIO as cstringio input = raw_input + long = long + unicode = unicode diff -Nru landscape-client-19.12/landscape/lib/config.py landscape-client-23.02/landscape/lib/config.py --- landscape-client-19.12/landscape/lib/config.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/config.py 2023-02-07 18:55:50.000000000 +0000 @@ -228,7 +228,7 @@ raise_errors=False, write_empty_values=True) except ConfigObjError as e: logger = getLogger() - logger.warn(str(e)) + logger.warn("ERROR at {}: {}".format(config_source, str(e))) # Good configuration values are recovered here config_obj = e.config return config_obj diff -Nru landscape-client-19.12/landscape/lib/disk.py landscape-client-23.02/landscape/lib/disk.py --- landscape-client-19.12/landscape/lib/disk.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/disk.py 2023-02-07 18:55:50.000000000 +0000 @@ -4,13 +4,13 @@ import re import codecs -from twisted.python.compat import _PY3 +from landscape.lib.compat import _PY3 # List of filesystem types authorized when generating disk use statistics. STABLE_FILESYSTEMS = frozenset( ["ext", "ext2", "ext3", "ext4", "reiserfs", "ntfs", "msdos", "dos", "vfat", - "xfs", "hpfs", "jfs", "ufs", "hfs", "hfsplus", "simfs"]) + "xfs", "hpfs", "jfs", "ufs", "hfs", "hfsplus", "simfs", "drvfs", "lxfs"]) EXTRACT_DEVICE = re.compile("([a-z]+)[0-9]*") diff -Nru landscape-client-19.12/landscape/lib/gpg.py landscape-client-23.02/landscape/lib/gpg.py --- landscape-client-19.12/landscape/lib/gpg.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/gpg.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,6 +1,9 @@ +import itertools import shutil import tempfile +from glob import glob + from twisted.internet.utils import getProcessOutputAndValue @@ -8,14 +11,15 @@ """Raised when the gpg signature for a given file is invalid.""" -def gpg_verify(filename, signature, gpg="/usr/bin/gpg"): +def gpg_verify(filename, signature, gpg="/usr/bin/gpg", apt_dir="/etc/apt"): """Verify the GPG signature of a file. @param filename: Path to the file to verify the signature against. @param signature: Path to signature to use. @param gpg: Optionally, path to the GPG binary to use. + @param apt_dir: Optionally, path to apt trusted keyring. @return: a C{Deferred} resulting in C{True} if the signature is - valid, C{False} otherwise. + valid, C{False} otherwise. """ def remove_gpg_home(ignored): @@ -32,9 +36,17 @@ "code='%d')" % (gpg, out, err, code)) gpg_home = tempfile.mkdtemp() - args = ("--no-options", "--homedir", gpg_home, "--no-default-keyring", - "--ignore-time-conflict", "--keyring", "/etc/apt/trusted.gpg", - "--verify", signature, filename) + keyrings = tuple(itertools.chain(*[ + ("--keyring", keyring) + for keyring in sorted( + glob("{}/trusted.gpg".format(apt_dir)) + + glob("{}/trusted.gpg.d/*.gpg".format(apt_dir)) + ) + ])) + args = ( + "--no-options", "--homedir", gpg_home, "--no-default-keyring", + "--ignore-time-conflict" + ) + keyrings + ("--verify", signature, filename) result = getProcessOutputAndValue(gpg, args=args) result.addBoth(remove_gpg_home) diff -Nru landscape-client-19.12/landscape/lib/lsb_release.py landscape-client-23.02/landscape/lib/lsb_release.py --- landscape-client-19.12/landscape/lib/lsb_release.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/lsb_release.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,26 +1,59 @@ -"""Get information from /etc/lsb_release.""" +"""Get information from /usr/bin/lsb_release.""" +import os +from subprocess import CalledProcessError, check_output -LSB_RELEASE_FILENAME = "/etc/lsb-release" -LSB_RELEASE_INFO_KEYS = {"DISTRIB_ID": "distributor-id", - "DISTRIB_DESCRIPTION": "description", - "DISTRIB_RELEASE": "release", - "DISTRIB_CODENAME": "code-name"} +LSB_RELEASE = "/usr/bin/lsb_release" +LSB_RELEASE_FILENAME = "/etc/lsb_release" +LSB_RELEASE_FILE_KEYS = { + "DISTRIB_ID": "distributor-id", + "DISTRIB_DESCRIPTION": "description", + "DISTRIB_RELEASE": "release", + "DISTRIB_CODENAME": "code-name", +} -def parse_lsb_release(lsb_release_filename): - """Return a C{dict} holding information about the system LSB release. +def parse_lsb_release(lsb_release_filename=None): + """ + Returns a C{dict} holding information about the system LSB release. + Reads from C{lsb_release_filename} if it exists, else calls + C{LSB_RELEASE} + """ + if lsb_release_filename and os.path.exists(lsb_release_filename): + return parse_lsb_release_file(lsb_release_filename) + + with open(os.devnull, 'w') as FNULL: + try: + lsb_info = check_output([LSB_RELEASE, "-as"], stderr=FNULL) + except (CalledProcessError, FileNotFoundError): + # Fall back to reading file, even if it doesn't exist. + return parse_lsb_release_file(lsb_release_filename) + else: + dist, desc, release, code_name, _ = lsb_info.decode().split("\n") + + return { + "distributor-id": dist, + "release": release, + "code-name": code_name, + "description": desc, + } + - @raises: An IOError exception if C{lsb_release_filename} could not be read. +def parse_lsb_release_file(filename): + """ + Returns a C{dict} holding information about the system LSB release + by attempting to parse C{filename}. + + @raises: A FileNotFoundError if C{filename} does not exist. """ - fd = open(lsb_release_filename, "r") info = {} - try: + + with open(filename) as fd: for line in fd: key, value = line.split("=") - if key in LSB_RELEASE_INFO_KEYS: - key = LSB_RELEASE_INFO_KEYS[key.strip()] + + if key in LSB_RELEASE_FILE_KEYS: + key = LSB_RELEASE_FILE_KEYS[key.strip()] value = value.strip().strip('"') info[key] = value - finally: - fd.close() + return info diff -Nru landscape-client-19.12/landscape/lib/message.py landscape-client-23.02/landscape/lib/message.py --- landscape-client-19.12/landscape/lib/message.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/message.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,6 +1,6 @@ """Helpers for reliable persistent message queues.""" -ANCIENT = 1 +RESYNC = object() # Used as a flag indicating a resync is needed def got_next_expected(store, next_expected): @@ -10,6 +10,12 @@ wants next; this will do various things based on what *this* side has in its outbound queue store. + 0. The peer expects a sequence number from the server that is too high, the + difference greater than pending messages in the peer. We flush the older + messages in queue b/c the server does not want old or ancient messages, + however we also reset the offset so that the pending messages are + resent. Then we resynchronize by returning RESYNC. See LP: #1917540 + 1. The peer expects a sequence greater than what we last sent. This is the common case and generally it should be expecting last_sent_sequence+len(messages_sent)+1. @@ -25,18 +31,22 @@ from that message. If the next expected sequence from the server refers to a message - older than we have, then L{ANCIENT} will be returned. + older than we have, then L{RESYNC} will be returned. """ ret = None old_sequence = store.get_sequence() - if next_expected > old_sequence: + if (next_expected - old_sequence) > store.count_pending_messages(): + store.delete_old_messages() # Flush queue from previous iteration + pending_offset = 0 # This means current messages will be resent + ret = RESYNC + elif next_expected > old_sequence: store.delete_old_messages() pending_offset = next_expected - old_sequence elif next_expected < (old_sequence - store.get_pending_offset()): # "Ancient": The other side wants messages we don't have, # so let's just reset our counter to what it expects. pending_offset = 0 - ret = ANCIENT + ret = RESYNC else: # No messages transferred, or # "Old": We'll try to send these old messages that the diff -Nru landscape-client-19.12/landscape/lib/network.py landscape-client-23.02/landscape/lib/network.py --- landscape-client-19.12/landscape/lib/network.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/network.py 2023-02-07 18:55:50.000000000 +0000 @@ -11,7 +11,7 @@ import logging import netifaces -from twisted.python.compat import long +from landscape.lib.compat import long, _PY3 __all__ = ["get_active_device_info", "get_network_traffic"] @@ -35,32 +35,14 @@ return flags & 1 -def get_active_interfaces(): - """Generator yields (active network interface name, address data) tuples. +def is_active(ifaddresses): + """Checks if interface address data has an IP address - Address data is formatted exactly like L{netifaces.ifaddresses}, e.g.:: - - ('eth0', { - AF_LINK: [ - {'addr': '...', 'broadcast': '...'}, ], - AF_INET: [ - {'addr': '...', 'broadcast': '...', 'netmask': '...'}, - {'addr': '...', 'broadcast': '...', 'netmask': '...'}, - ...], - AF_INET6: [ - {'addr': '...', 'netmask': '...'}, - {'addr': '...', 'netmask': '...'}, - ...], }) - - Interfaces with no IP address are ignored. - """ - for interface in netifaces.interfaces(): - ifaddresses = netifaces.ifaddresses(interface) - # Skip interfaces with no IPv4 or IPv6 addresses. - inet_addr = ifaddresses.get(netifaces.AF_INET, [{}])[0].get('addr') - inet6_addr = ifaddresses.get(netifaces.AF_INET6, [{}])[0].get('addr') - if inet_addr or inet6_addr: - yield interface, ifaddresses + @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} + """ + inet_addr = ifaddresses.get(netifaces.AF_INET, [{}])[0].get('addr') + inet6_addr = ifaddresses.get(netifaces.AF_INET6, [{}])[0].get('addr') + return bool(inet_addr or inet6_addr) def get_ip_addresses(ifaddresses): @@ -69,8 +51,7 @@ Returns the same structure as L{ifaddresses}, but filtered to keep IP addresses only. - @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} or - the address data in L{get_active_interfaces}'s output. + @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} """ results = {} if netifaces.AF_INET in ifaddresses: @@ -88,8 +69,7 @@ def get_broadcast_address(ifaddresses): """Return the broadcast address associated to an interface. - @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} or - the address data in L{get_active_interfaces}'s output. + @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} """ return ifaddresses[netifaces.AF_INET][0].get('broadcast', '0.0.0.0') @@ -97,8 +77,7 @@ def get_netmask(ifaddresses): """Return the network mask associated to an interface. - @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} or - the address data in L{get_active_interfaces}'s output. + @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} """ return ifaddresses[netifaces.AF_INET][0].get('netmask', '') @@ -106,8 +85,7 @@ def get_ip_address(ifaddresses): """Return the first IPv4 address associated to the interface. - @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} or - the address data in L{get_active_interfaces}'s output. + @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} """ return ifaddresses[netifaces.AF_INET][0]['addr'] @@ -118,8 +96,7 @@ ie. six colon separated groups of two hexadecimal digits, if available; otherwise an empty string. - @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} or - the address data in L{get_active_interfaces}'s output. + @param ifaddresses: a dict as returned by L{netifaces.ifaddresses} """ if netifaces.AF_LINK in ifaddresses: return ifaddresses[netifaces.AF_LINK][0].get('addr', '') @@ -138,51 +115,104 @@ return struct.unpack("H", data[16:18])[0] -def get_active_device_info(skipped_interfaces=("lo",), - skip_vlan=True, skip_alias=True, extended=False): +def get_default_interfaces(): """ - Returns a dictionary containing information on each active network - interface present on a machine. + Returns a list of interfaces with default routes + """ + default_table = netifaces.gateways()['default'] + interfaces = [gateway[1] for gateway in default_table.values()] + return interfaces + + +def get_filtered_if_info(filters=(), extended=False): + """ + Returns a dictionary containing info on each active network + interface that passes all `filters`. + + A filter is a callable that returns True if the interface should be + skipped. """ results = [] + try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_IP) - for interface, ifaddresses in get_active_interfaces(): - if interface in skipped_interfaces: - continue - if skip_vlan and "." in interface: + + for interface in netifaces.interfaces(): + if any(f(interface) for f in filters): continue - if skip_alias and ":" in interface: + + ifaddresses = netifaces.ifaddresses(interface) + if not is_active(ifaddresses): continue - flags = get_flags(sock, interface.encode()) + + ifencoded = interface.encode() + flags = get_flags(sock, ifencoded) if not is_up(flags): continue - interface_info = {"interface": interface} - interface_info["flags"] = flags - speed, duplex = get_network_interface_speed( - sock, interface.encode()) - interface_info["speed"] = speed - interface_info["duplex"] = duplex + ip_addresses = get_ip_addresses(ifaddresses) + if not extended and netifaces.AF_INET not in ip_addresses: + # Skip interfaces with no IPv4 addr unless extended to + # keep backwards compatibility with single-IPv4 addr + # support. + continue + + ifinfo = {"interface": interface} + ifinfo["flags"] = flags + ifinfo["speed"], ifinfo["duplex"] = get_network_interface_speed( + sock, ifencoded) + if extended: - interface_info["ip_addresses"] = ip_addresses + ifinfo["ip_addresses"] = ip_addresses + if netifaces.AF_INET in ip_addresses: - interface_info["ip_address"] = get_ip_address(ifaddresses) - interface_info["mac_address"] = get_mac_address(ifaddresses) - interface_info["broadcast_address"] = get_broadcast_address( + ifinfo["ip_address"] = get_ip_address(ifaddresses) + ifinfo["mac_address"] = get_mac_address(ifaddresses) + ifinfo["broadcast_address"] = get_broadcast_address( ifaddresses) - interface_info["netmask"] = get_netmask(ifaddresses) - # Skip interfaces with no IPv4 address in non-extended mode - # to keep backwards compatibility with single-IPv4 addr support. - if netifaces.AF_INET in ip_addresses or extended: - results.append(interface_info) + ifinfo["netmask"] = get_netmask(ifaddresses) + + results.append(ifinfo) finally: - del sock + sock.close() return results +def get_active_device_info(skipped_interfaces=("lo",), + skip_vlan=True, skip_alias=True, + extended=False, default_only=False): + def filter_local(interface): + return interface in skipped_interfaces + + def filter_vlan(interface): + return "." in interface + + def filter_alias(interface): + return ":" in interface + + # Get default interfaces here because it could be expensive and + # there's no reason to do it more than once. + default_ifs = get_default_interfaces() + + def filter_default(interface): + return default_only and interface not in default_ifs + + # Tap interfaces can be extremely numerous, slowing us down + # significantly. + def filter_tap(interface): + return interface.startswith("tap") + + return get_filtered_if_info(filters=( + filter_tap, + filter_local, + filter_vlan, + filter_alias, + filter_default, + ), extended=extended) + + def get_network_traffic(source_file="/proc/net/dev"): """ Retrieves an array of information regarding the network activity per @@ -246,13 +276,16 @@ speed = -1 try: fcntl.ioctl(sock, SIOCETHTOOL, packed) # Status ioctl() call - res = status_cmd.tostring() + if _PY3: + res = status_cmd.tobytes() + else: + res = status_cmd.tostring() speed, duplex = struct.unpack("12xHB28x", res) - except IOError as e: + except (IOError, OSError) as e: if e.errno == errno.EPERM: - logging.warn("Could not determine network interface speed, " - "operation not permitted.") - elif e.errno != errno.EOPNOTSUPP: + logging.warning("Could not determine network interface speed, " + "operation not permitted.") + elif e.errno != errno.EOPNOTSUPP and e.errno != errno.EINVAL: raise e speed = -1 duplex = False diff -Nru landscape-client-19.12/landscape/lib/schema.py landscape-client-23.02/landscape/lib/schema.py --- landscape-client-19.12/landscape/lib/schema.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/schema.py 2023-02-07 18:55:50.000000000 +0000 @@ -13,6 +13,12 @@ self.value = value def coerce(self, value): + if isinstance(self.value, str) and isinstance(value, bytes): + try: + value = value.decode() + except UnicodeDecodeError: + pass + if value != self.value: raise InvalidError("%r != %r" % (value, self.value)) return value @@ -65,11 +71,19 @@ class Bytes(object): - """A binary string.""" + """A binary string. + + If the value is a Python3 str (unicode), it will be automatically + encoded. + """ def coerce(self, value): - if not isinstance(value, bytes): - raise InvalidError("%r isn't a bytestring" % (value,)) - return value + if isinstance(value, bytes): + return value + + if isinstance(value, str): + return value.encode() + + raise InvalidError("%r isn't a bytestring" % value) class Unicode(object): diff -Nru landscape-client-19.12/landscape/lib/testing.py landscape-client-23.02/landscape/lib/testing.py --- landscape-client-19.12/landscape/lib/testing.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/testing.py 2023-02-07 18:55:50.000000000 +0000 @@ -15,7 +15,7 @@ from logging import Handler, ERROR, Formatter from twisted.trial.unittest import TestCase from twisted.python.compat import StringType as basestring -from twisted.python.compat import _PY3 +from landscape.lib.compat import _PY3 from twisted.python.failure import Failure from twisted.internet.defer import Deferred from twisted.internet.error import ConnectError diff -Nru landscape-client-19.12/landscape/lib/tests/test_backoff.py landscape-client-23.02/landscape/lib/tests/test_backoff.py --- landscape-client-19.12/landscape/lib/tests/test_backoff.py 1970-01-01 00:00:00.000000000 +0000 +++ landscape-client-23.02/landscape/lib/tests/test_backoff.py 2023-02-07 18:55:50.000000000 +0000 @@ -0,0 +1,37 @@ +from landscape.lib.backoff import ExponentialBackoff +from landscape.client.tests.helpers import LandscapeTest + + +class TestBackoff(LandscapeTest): + + def test_increase(self): + '''Test the delay values start correctly and double''' + backoff_counter = ExponentialBackoff(5, 10) + backoff_counter.increase() + self.assertEqual(backoff_counter.get_delay(), 5) + backoff_counter.increase() + self.assertEqual(backoff_counter.get_delay(), 10) + + def test_min(self): + '''Test the count and the delay never go below zero''' + backoff_counter = ExponentialBackoff(1, 5) + for _ in range(10): + backoff_counter.decrease() + self.assertEqual(backoff_counter.get_delay(), 0) + self.assertEqual(backoff_counter.get_random_delay(), 0) + self.assertEqual(backoff_counter._error_count, 0) + + def test_max(self): + '''Test the delay never goes above max''' + backoff_counter = ExponentialBackoff(1, 5) + for _ in range(10): + backoff_counter.increase() + self.assertEqual(backoff_counter.get_delay(), 5) + + def test_decreased_when_maxed(self): + '''Test the delay goes down one step when maxed''' + backoff_counter = ExponentialBackoff(1, 5) + for _ in range(10): + backoff_counter.increase() + backoff_counter.decrease() + self.assertTrue(backoff_counter.get_delay() < 5) diff -Nru landscape-client-19.12/landscape/lib/tests/test_config.py landscape-client-23.02/landscape/lib/tests/test_config.py --- landscape-client-19.12/landscape/lib/tests/test_config.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/tests/test_config.py 2023-02-07 18:55:50.000000000 +0000 @@ -338,8 +338,10 @@ """)) self.config.load_configuration_file(filename) self.assertEqual(self.config.whatever, "spam") - self.assertIn("WARNING: Duplicate keyword name at line 4.", - self.logfile.getvalue()) + self.assertIn( + f"WARNING: ERROR at {filename}: Duplicate keyword name at line 4.", + self.logfile.getvalue(), + ) def test_duplicate_key(self): """ @@ -354,8 +356,10 @@ filename = self.makeFile(config) self.config.load_configuration_file(filename) self.assertEqual(self.config.computer_title, "frog") - self.assertIn("WARNING: Duplicate keyword name at line 4.", - self.logfile.getvalue()) + self.assertIn( + f"WARNING: ERROR at {filename}: Duplicate keyword name at line 4.", + self.logfile.getvalue(), + ) def test_triplicate_key(self): """ @@ -372,8 +376,13 @@ self.config.load_configuration_file(filename) self.assertEqual(self.config.computer_title, "frog") logged = self.logfile.getvalue() - self.assertIn("WARNING: Parsing failed with several errors.", - logged) + self.assertIn( + ( + f"WARNING: ERROR at {filename}: Parsing failed with several " + "errors." + ), + logged, + ) self.assertIn("First error at line 4.", logged) def test_load_not_found_default_accept_missing(self): diff -Nru landscape-client-19.12/landscape/lib/tests/test_gpg.py landscape-client-23.02/landscape/lib/tests/test_gpg.py --- landscape-client-19.12/landscape/lib/tests/test_gpg.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/tests/test_gpg.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,9 +1,10 @@ import mock import os +import textwrap import unittest from twisted.internet import reactor -from twisted.internet.defer import Deferred +from twisted.internet.defer import Deferred, inlineCallbacks from landscape.lib import testing from landscape.lib.gpg import gpg_verify @@ -16,6 +17,8 @@ L{gpg_verify} runs the given gpg binary and returns C{True} if the provided signature is valid. """ + aptdir = self.makeDir() + os.mknod("{}/trusted.gpg".format(aptdir)) gpg_options = self.makeFile() gpg = self.makeFile("#!/bin/sh\n" "touch $3/trustdb.gpg\n" @@ -27,14 +30,15 @@ @mock.patch("tempfile.mkdtemp") def do_test(mkdtemp_mock): mkdtemp_mock.return_value = gpg_home - result = gpg_verify("/some/file", "/some/signature", gpg=gpg) + result = gpg_verify( + "/some/file", "/some/signature", gpg=gpg, apt_dir=aptdir) def check_result(ignored): self.assertEqual( open(gpg_options).read(), "--no-options --homedir %s --no-default-keyring " - "--ignore-time-conflict --keyring /etc/apt/trusted.gpg " - "--verify /some/signature /some/file" % gpg_home) + "--ignore-time-conflict --keyring %s/trusted.gpg " + "--verify /some/signature /some/file" % (gpg_home, aptdir)) self.assertFalse(os.path.exists(gpg_home)) result.addCallback(check_result) @@ -70,3 +74,38 @@ reactor.callWhenRunning(do_test) return deferred + + @inlineCallbacks + def test_gpg_verify_trusted_dir(self): + """ + gpg_verify uses keys from the trusted.gpg.d if such a folder exists. + """ + apt_dir = self.makeDir() + os.mkdir("{}/trusted.gpg.d".format(apt_dir)) + os.mknod("{}/trusted.gpg.d/foo.gpg".format(apt_dir)) + os.mknod("{}/trusted.gpg.d/baz.gpg".format(apt_dir)) + os.mknod("{}/trusted.gpg.d/bad.gpg~".format(apt_dir)) + + gpg_call = self.makeFile() + fake_gpg = self.makeFile(textwrap.dedent("""\ + #!/bin/sh + touch $3/trustdb.gpg + echo -n $@ > {} + """).format(gpg_call)) + os.chmod(fake_gpg, 0o755) + gpg_home = self.makeDir() + + with mock.patch("tempfile.mkdtemp", return_value=gpg_home): + yield gpg_verify( + "/some/file", "/some/signature", gpg=fake_gpg, apt_dir=apt_dir) + + expected = ( + "--no-options --homedir {gpg_home} --no-default-keyring " + "--ignore-time-conflict " + "--keyring {apt_dir}/trusted.gpg.d/baz.gpg " + "--keyring {apt_dir}/trusted.gpg.d/foo.gpg " + "--verify /some/signature /some/file" + ).format(gpg_home=gpg_home, apt_dir=apt_dir) + with open(gpg_call) as call: + self.assertEqual(expected, call.read()) + self.assertFalse(os.path.exists(gpg_home)) diff -Nru landscape-client-19.12/landscape/lib/tests/test_lsb_release.py landscape-client-23.02/landscape/lib/tests/test_lsb_release.py --- landscape-client-19.12/landscape/lib/tests/test_lsb_release.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/tests/test_lsb_release.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,4 +1,6 @@ import unittest +from subprocess import CalledProcessError +from unittest import mock from landscape.lib import testing from landscape.lib.lsb_release import parse_lsb_release @@ -7,6 +9,30 @@ class LsbReleaseTest(testing.FSTestCase, unittest.TestCase): def test_parse_lsb_release(self): + with mock.patch("landscape.lib.lsb_release.check_output") as co_mock: + co_mock.return_value = (b"Ubuntu\nUbuntu 22.04.1 LTS\n22.04\njammy" + b"\n") + lsb_release = parse_lsb_release() + + self.assertEqual(lsb_release, + {"distributor-id": "Ubuntu", + "description": "Ubuntu 22.04.1 LTS", + "release": "22.04", + "code-name": "jammy"}) + + def test_parse_lsb_release_debian(self): + with mock.patch("landscape.lib.lsb_release.check_output") as co_mock: + co_mock.return_value = (b"Debian\nDebian GNU/Linux 11 (bullseye)\n" + b"11\nbullseye\n") + lsb_release = parse_lsb_release() + + self.assertEqual(lsb_release, + {"distributor-id": "Debian", + "description": "Debian GNU/Linux 11 (bullseye)", + "release": "11", + "code-name": "bullseye"}) + + def test_parse_lsb_release_file(self): """ L{parse_lsb_release} returns a C{dict} holding information from the given LSB release file. @@ -17,18 +43,33 @@ "DISTRIB_DESCRIPTION=" "\"Ubuntu 6.06.1 LTS\"\n") - self.assertEqual(parse_lsb_release(lsb_release_filename), + with mock.patch("landscape.lib.lsb_release.check_output") as co_mock: + co_mock.side_effect = CalledProcessError(127, "") + lsb_release = parse_lsb_release(lsb_release_filename) + + self.assertEqual(lsb_release, {"distributor-id": "Ubuntu", "description": "Ubuntu 6.06.1 LTS", "release": "6.06", "code-name": "dapper"}) - def test_parse_lsb_release_with_missing_or_extra_fields(self): + def test_parse_lsb_release_file_with_missing_or_extra_fields(self): """ L{parse_lsb_release} ignores lines not matching the map of known keys, and returns only keys with an actual value. """ lsb_release_filename = self.makeFile("DISTRIB_ID=Ubuntu\n" "FOO=Bar\n") - self.assertEqual(parse_lsb_release(lsb_release_filename), - {"distributor-id": "Ubuntu"}) + + with mock.patch("landscape.lib.lsb_release.check_output") as co_mock: + co_mock.side_effect = CalledProcessError(127, "") + lsb_release = parse_lsb_release(lsb_release_filename) + + self.assertEqual(lsb_release, {"distributor-id": "Ubuntu"}) + + def test_parse_lsb_release_file_not_found(self): + with mock.patch("landscape.lib.lsb_release.check_output") as co_mock: + co_mock.side_effect = CalledProcessError(127, "") + + self.assertRaises(FileNotFoundError, parse_lsb_release, + "TheresNoWayThisFileExists") diff -Nru landscape-client-19.12/landscape/lib/tests/test_network.py landscape-client-23.02/landscape/lib/tests/test_network.py --- landscape-client-19.12/landscape/lib/tests/test_network.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/tests/test_network.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,7 +1,7 @@ import socket import unittest -from mock import patch, ANY, mock_open +from unittest.mock import ANY, DEFAULT, patch, mock_open from netifaces import ( AF_INET, AF_INET6, @@ -14,8 +14,8 @@ from landscape.lib import testing from landscape.lib.network import ( - get_network_traffic, get_active_device_info, get_active_interfaces, - get_fqdn, get_network_interface_speed, is_up) + get_active_device_info, get_filtered_if_info, get_fqdn, + get_network_interface_speed, get_network_traffic, is_up) class BaseTestCase(testing.HelperTestCase, unittest.TestCase): @@ -148,33 +148,62 @@ 'duplex': True, 'ip_addresses': {AF_INET6: [{'addr': '2001::1'}]}}]) + @patch("landscape.lib.network.get_default_interfaces") + @patch("landscape.lib.network.get_network_interface_speed") + @patch("landscape.lib.network.get_flags") + @patch("landscape.lib.network.netifaces.ifaddresses") + @patch("landscape.lib.network.netifaces.interfaces") + def test_default_only_interface( + self, mock_interfaces, mock_ifaddresses, mock_get_flags, + mock_get_network_interface_speed, mock_get_default_interfaces): + default_iface = "test_iface" + mock_get_default_interfaces.return_value = [default_iface] + mock_get_network_interface_speed.return_value = (100, True) + mock_get_flags.return_value = 4163 + mock_interfaces.return_value = [default_iface] + mock_ifaddresses.return_value = {AF_INET: [{"addr": "192.168.0.50"}]} + device_info = get_active_device_info(extended=False, default_only=True) + interfaces = [i["interface"] for i in device_info] + self.assertIn(default_iface, interfaces) + self.assertEqual(len(interfaces), 1) + def test_skip_loopback(self): """The C{lo} interface is not reported by L{get_active_device_info}.""" device_info = get_active_device_info() interfaces = [i["interface"] for i in device_info] self.assertNotIn("lo", interfaces) - @patch("landscape.lib.network.get_active_interfaces") - def test_skip_vlan(self, mock_get_active_interfaces): + @patch("landscape.lib.network.netifaces.interfaces") + def test_skip_vlan(self, mock_interfaces): """VLAN interfaces are not reported by L{get_active_device_info}.""" - mock_get_active_interfaces.side_effect = lambda: ( - list(get_active_interfaces()) + [("eth0.1", {})]) + mock_interfaces.side_effect = lambda: ( + _interfaces() + ["eth0.1"]) device_info = get_active_device_info() - self.assertTrue(mock_get_active_interfaces.called) interfaces = [i["interface"] for i in device_info] self.assertNotIn("eth0.1", interfaces) - @patch("landscape.lib.network.get_active_interfaces") - def test_skip_alias(self, mock_get_active_interfaces): + @patch("landscape.lib.network.netifaces.interfaces") + def test_skip_alias(self, mock_interfaces): """Interface aliases are not reported by L{get_active_device_info}.""" - mock_get_active_interfaces.side_effect = lambda: ( - list(get_active_interfaces()) + [("eth0:foo", {})]) + mock_interfaces.side_effect = lambda: ( + _interfaces() + ["eth0:foo"]) device_info = get_active_device_info() interfaces = [i["interface"] for i in device_info] self.assertNotIn("eth0:foo", interfaces) @patch("landscape.lib.network.netifaces.ifaddresses") @patch("landscape.lib.network.netifaces.interfaces") + def test_no_extra_netifaces_calls(self, mock_interfaces, mock_ifaddresses): + """ + Make sure filtered out interfaces aren't used in netiface calls due to + their impact on sysinfo/login time with a large amount of interfaces. + """ + mock_interfaces.return_value = ["eth0:foo"] + get_active_device_info() + assert not mock_ifaddresses.called + + @patch("landscape.lib.network.netifaces.ifaddresses") + @patch("landscape.lib.network.netifaces.interfaces") def test_skip_iface_with_no_addr(self, mock_interfaces, mock_ifaddresses): mock_interfaces.return_value = _interfaces() + ["test_iface"] mock_ifaddresses.side_effect = lambda iface: ( @@ -283,6 +312,53 @@ self.assertFalse(is_up(2 + 64 + 4096)) self.assertFalse(is_up(0b11111111111110)) + def test_get_filtered_if_info(self): + def filter_tap(interface): + return interface.startswith("tap") + + with patch.multiple( + "landscape.lib.network", + get_flags=DEFAULT, + get_network_interface_speed=DEFAULT, + netifaces=DEFAULT, + ) as mocks: + mocks["netifaces"].interfaces.return_value = [ + "tap0123", + "test_iface", + "tap4567", + "test_iface2", + ] + mocks["get_flags"].return_value = 4163 + mocks["get_network_interface_speed"].return_value = (100, True) + device_info = get_filtered_if_info(filters=(filter_tap,), + extended=True) + + self.assertEqual(len(device_info), 2) + self.assertTrue(all("tap" not in i["interface"] for i in device_info)) + + def test_get_active_device_info_filtered_taps(self): + """ + Tests that tap network interfaces are filtered out. + """ + with patch.multiple( + "landscape.lib.network", + get_flags=DEFAULT, + get_network_interface_speed=DEFAULT, + netifaces=DEFAULT, + ) as mocks: + mocks["netifaces"].interfaces.return_value = [ + "tap0123", + "test_iface", + "tap4567", + "test_iface2", + ] + mocks["get_flags"].return_value = 4163 + mocks["get_network_interface_speed"].return_value = (100, True) + device_info = get_active_device_info(extended=True) + + self.assertEqual(len(device_info), 2) + self.assertTrue(all("tap" not in i["interface"] for i in device_info)) + # exact output of cat /proc/net/dev snapshot with line continuations for pep8 test_proc_net_dev_output = """\ @@ -370,6 +446,7 @@ mock_unpack.assert_called_with("12xHB28x", ANY) self.assertEqual((100, False), result) + sock.close() @patch("struct.unpack") @patch("fcntl.ioctl") @@ -389,6 +466,7 @@ mock_unpack.assert_called_with("12xHB28x", ANY) self.assertEqual((0, False), result) + sock.close() @patch("fcntl.ioctl") def test_get_network_interface_speed_not_supported(self, mock_ioctl): @@ -411,6 +489,7 @@ mock_ioctl.assert_called_with(ANY, ANY, ANY) self.assertEqual((-1, False), result) + sock.close() @patch("fcntl.ioctl") def test_get_network_interface_speed_not_permitted(self, mock_ioctl): @@ -433,6 +512,7 @@ mock_ioctl.assert_called_with(ANY, ANY, ANY) self.assertEqual((-1, False), result) + sock.close() @patch("fcntl.ioctl") def test_get_network_interface_speed_other_io_error(self, mock_ioctl): @@ -450,3 +530,4 @@ mock_ioctl.side_effect = theerror self.assertRaises(IOError, get_network_interface_speed, sock, b"eth0") + sock.close() diff -Nru landscape-client-19.12/landscape/lib/tests/test_schema.py landscape-client-23.02/landscape/lib/tests/test_schema.py --- landscape-client-19.12/landscape/lib/tests/test_schema.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/tests/test_schema.py 2023-02-07 18:55:50.000000000 +0000 @@ -69,8 +69,8 @@ def test_string(self): self.assertEqual(Bytes().coerce(b"foo"), b"foo") - def test_string_bad_unicode(self): - self.assertRaises(InvalidError, Bytes().coerce, u"foo") + def test_string_unicode(self): + self.assertEqual(Bytes().coerce(u"foo"), b"foo") def test_string_bad_anything(self): self.assertRaises(InvalidError, Bytes().coerce, object()) diff -Nru landscape-client-19.12/landscape/lib/tests/test_vm_info.py landscape-client-23.02/landscape/lib/tests/test_vm_info.py --- landscape-client-19.12/landscape/lib/tests/test_vm_info.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/tests/test_vm_info.py 2023-02-07 18:55:50.000000000 +0000 @@ -165,9 +165,9 @@ """ cpuinfo_path = os.path.join(self.proc_path, "cpuinfo") cpuinfo = ( - "platform : Some Machine\n" - "model : Some CPU (emulated by qemu)\n" - "machine : Some Machine (emulated by qemu)\n") + "platform : Some Machine\n" + "model : Some CPU (emulated by qemu)\n" + "machine : Some Machine (emulated by qemu)\n") self.makeFile(path=cpuinfo_path, content=cpuinfo) self.assertEqual(b"kvm", get_vm_info(root_path=self.root_path)) @@ -184,6 +184,18 @@ self.make_dmi_info("product_name", "KVM") self.assertEqual(b"kvm", get_vm_info(root_path=self.root_path)) + def test_get_vm_info_with_rhev(self): + """get_vm_info returns 'kvm' if running under RHEV Hypervisor.""" + self.make_dmi_info("product_name", "RHEV Hypervisor") + self.make_dmi_info("sys_vendor", "Red Hat") + self.assertEqual(b"kvm", get_vm_info(root_path=self.root_path)) + + def test_get_vm_info_with_parallels(self): + """get_vm_info returns 'kvm' if running under Parallels""" + self.make_dmi_info("product_name", "Parallels Virtual Platform") + self.make_dmi_info("sys_vendor", "Parallels Software International") + self.assertEqual(b"kvm", get_vm_info(root_path=self.root_path)) + class GetContainerInfoTest(BaseTestCase): diff -Nru landscape-client-19.12/landscape/lib/user.py landscape-client-23.02/landscape/lib/user.py --- landscape-client-19.12/landscape/lib/user.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/user.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,7 +1,7 @@ import os.path import pwd -from twisted.python.compat import _PY3 +from landscape.lib.compat import _PY3 from landscape.lib.encoding import encode_if_needed diff -Nru landscape-client-19.12/landscape/lib/vm_info.py landscape-client-23.02/landscape/lib/vm_info.py --- landscape-client-19.12/landscape/lib/vm_info.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/lib/vm_info.py 2023-02-07 18:55:50.000000000 +0000 @@ -66,9 +66,6 @@ # We need bytes here as required by the message schema. vendor = read_binary_file(sys_vendor_path, limit=1024).lower() - # 2018-01: AWS and DO are now returning custom sys_vendor names - # instead of qemu. If this becomes a trend, it may be worth also checking - # dmi/id/chassis_vendor which seems to unchanged (bochs). content_vendors_map = ( (b"amazon ec2", b"kvm"), (b"bochs", b"kvm"), @@ -81,6 +78,8 @@ (b"qemu", b"kvm"), (b"kvm", b"kvm"), (b"vmware", b"vmware"), + (b"rhev", b"kvm"), + (b"parallels", b"kvm") ) for name, vm_type in content_vendors_map: if name in vendor: diff -Nru landscape-client-19.12/landscape/message_schemas/server_bound.py landscape-client-23.02/landscape/message_schemas/server_bound.py --- landscape-client-19.12/landscape/message_schemas/server_bound.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/message_schemas/server_bound.py 2023-02-07 18:55:50.000000000 +0000 @@ -19,7 +19,7 @@ "NETWORK_DEVICE", "NETWORK_ACTIVITY", "REBOOT_REQUIRED_INFO", "UPDATE_MANAGER_INFO", "CPU_USAGE", "CEPH_USAGE", "SWIFT_USAGE", "SWIFT_DEVICE_INFO", "KEYSTONE_TOKEN", - "JUJU_UNITS_INFO", "CLOUD_METADATA", + "JUJU_UNITS_INFO", "CLOUD_METADATA", "COMPUTER_TAGS", "UBUNTU_PRO_INFO", ] @@ -219,10 +219,13 @@ "juju-info": KeyDict({"environment-uuid": Unicode(), "api-addresses": List(Unicode()), "machine-id": Unicode()}), - "access_group": Unicode()}, + "access_group": Unicode(), + "clone_secure_id": Any(Unicode(), Constant(None)), + "ubuntu_pro_info": Unicode()}, api=b"3.3", optional=["registration_password", "hostname", "tags", "vm-info", - "container-info", "access_group", "juju-info"]) + "container-info", "access_group", "juju-info", + "clone_secure_id", "ubuntu_pro_info"]) # XXX The register-provisioned-machine message is obsolete, it's kept around @@ -504,6 +507,13 @@ UPDATE_MANAGER_INFO = Message("update-manager-info", {"prompt": Unicode()}) +COMPUTER_TAGS = Message( + "computer-tags", + {"tags": Any(Unicode(), Constant(None))}) + +UBUNTU_PRO_INFO = Message( + "ubuntu-pro-info", + {"ubuntu-pro-info": Unicode()}) message_schemas = ( ACTIVE_PROCESS_INFO, COMPUTER_UPTIME, CLIENT_UPTIME, @@ -518,4 +528,4 @@ NETWORK_DEVICE, NETWORK_ACTIVITY, REBOOT_REQUIRED_INFO, UPDATE_MANAGER_INFO, CPU_USAGE, CEPH_USAGE, SWIFT_USAGE, SWIFT_DEVICE_INFO, KEYSTONE_TOKEN, - JUJU_UNITS_INFO, CLOUD_METADATA) + JUJU_UNITS_INFO, CLOUD_METADATA, COMPUTER_TAGS, UBUNTU_PRO_INFO) diff -Nru landscape-client-19.12/landscape/message_schemas/test_message.py landscape-client-23.02/landscape/message_schemas/test_message.py --- landscape-client-19.12/landscape/message_schemas/test_message.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/message_schemas/test_message.py 2023-02-07 18:55:50.000000000 +0000 @@ -1,6 +1,6 @@ import unittest -from landscape.lib.schema import Int +from landscape.lib.schema import Constant, Int from landscape.message_schemas.message import Message @@ -13,6 +13,14 @@ schema.coerce({"type": "foo", "data": 3}), {"type": "foo", "data": 3}) + def test_coerce_bytes_to_str(self): + """ + The L{Constant} schema type recognizes bytestrings that decode to + matching strings. + """ + constant = Constant("register") + self.assertEqual(constant.coerce(b"register"), "register") + def test_timestamp(self): """L{Message} schemas should accept C{timestamp} keys.""" schema = Message("bar", {}) diff -Nru landscape-client-19.12/landscape/sysinfo/load.py landscape-client-23.02/landscape/sysinfo/load.py --- landscape-client-19.12/landscape/sysinfo/load.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/sysinfo/load.py 2023-02-07 18:55:50.000000000 +0000 @@ -9,5 +9,6 @@ self._sysinfo = sysinfo def run(self): - self._sysinfo.add_header("System load", str(os.getloadavg()[0])) + self._sysinfo.add_header( + "System load", str(round(os.getloadavg()[0], 2))) return succeed(None) diff -Nru landscape-client-19.12/landscape/sysinfo/network.py landscape-client-23.02/landscape/sysinfo/network.py --- landscape-client-19.12/landscape/sysinfo/network.py 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/landscape/sysinfo/network.py 2023-02-07 18:55:50.000000000 +0000 @@ -16,7 +16,8 @@ def __init__(self, get_device_info=None): if get_device_info is None: - get_device_info = partial(get_active_device_info, extended=True) + get_device_info = partial(get_active_device_info, + extended=True, default_only=True) self._get_device_info = get_device_info def register(self, sysinfo): diff -Nru landscape-client-19.12/man/landscape-config.1 landscape-client-23.02/man/landscape-config.1 --- landscape-client-19.12/man/landscape-config.1 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/man/landscape-config.1 2023-02-07 18:55:50.000000000 +0000 @@ -1,5 +1,5 @@ .\" Text automatically generated by txt2man -.TH landscape-config 1 "14 February 2019" "" "" +.TH landscape-config 1 "07 February 2022" "" "" .SH NAME \fBlandscape-config \fP- configure the Landscape management client \fB @@ -181,6 +181,12 @@ .B \fB--otp\fP=OTP The one-time password (OTP) to use in cloud configuration. +.TP +.B +\fB--is-registered\fP +Exit with code 0 (success) if client +is registered else returns 5. Display +registration info. .SH CLOUD Landscape has some cloud features that become available when the EC2 or diff -Nru landscape-client-19.12/man/landscape-config.txt landscape-client-23.02/man/landscape-config.txt --- landscape-client-19.12/man/landscape-config.txt 2019-11-29 14:21:45.000000000 +0000 +++ landscape-client-23.02/man/landscape-config.txt 2023-02-07 18:55:50.000000000 +0000 @@ -75,6 +75,9 @@ --silent Run without manual interaction. --disable Stop running clients and disable start at boot. --otp=OTP The one-time password (OTP) to use in cloud configuration. + --is-registered Exit with code 0 (success) if client + is registered else returns 5. Display + registration info. CLOUD