Merge ~mitchburton/ubuntu/+source/landscape-client:ubuntu/noble-devel into ubuntu/+source/landscape-client:ubuntu/noble-devel
- Git
- lp:~mitchburton/ubuntu/+source/landscape-client
- ubuntu/noble-devel
- Merge into ubuntu/noble-devel
Status: | Merged |
---|---|
Approved by: | Andreas Hasenack |
Approved revision: | 6a9c0e4a9ac1cd2c01ebd93dca12f862c0d5b694 |
Merge reported by: | Andreas Hasenack |
Merged at revision: | 6a9c0e4a9ac1cd2c01ebd93dca12f862c0d5b694 |
Proposed branch: | ~mitchburton/ubuntu/+source/landscape-client:ubuntu/noble-devel |
Merge into: | ubuntu/+source/landscape-client:ubuntu/noble-devel |
Diff against target: |
14522 lines (+8965/-1267) 133 files modified
.github/workflows/ci.yml (+8/-16) .github/workflows/codecov.yml (+19/-0) .gitmodules (+3/-0) Makefile (+16/-38) Makefile.packaging (+2/-2) README (+14/-3) debian/changelog (+31/-0) debian/control (+4/-3) debian/landscape-common.postinst (+2/-4) debian/landscape-common.prerm (+0/-1) debian/rules (+1/-1) debian/watch (+3/-2) dev/null (+0/-92) example.conf (+20/-4) landscape/__init__.py (+1/-1) landscape/client/__init__.py (+11/-0) landscape/client/broker/client.py (+5/-1) landscape/client/broker/config.py (+10/-3) landscape/client/broker/ping.py (+8/-4) landscape/client/broker/registration.py (+4/-4) landscape/client/broker/store.py (+9/-3) landscape/client/broker/tests/test_config.py (+27/-3) landscape/client/broker/tests/test_exchange.py (+4/-4) landscape/client/broker/tests/test_ping.py (+8/-4) landscape/client/broker/tests/test_registration.py (+45/-1) landscape/client/broker/tests/test_store.py (+3/-3) landscape/client/configuration.py (+41/-17) landscape/client/deployment.py (+79/-1) landscape/client/manager/aptsources.py (+4/-2) landscape/client/manager/config.py (+2/-0) landscape/client/manager/hardwareinfo.py (+1/-1) landscape/client/manager/scriptexecution.py (+17/-7) landscape/client/manager/service.py (+22/-7) landscape/client/manager/shutdownmanager.py (+79/-157) landscape/client/manager/snapmanager.py (+56/-43) landscape/client/manager/snapservicesmanager.py (+60/-0) landscape/client/manager/tests/test_aptsources.py (+43/-0) landscape/client/manager/tests/test_config.py (+2/-0) landscape/client/manager/tests/test_processkiller.py (+1/-1) landscape/client/manager/tests/test_scriptexecution.py (+44/-7) landscape/client/manager/tests/test_service.py (+45/-6) landscape/client/manager/tests/test_shutdownmanager.py (+25/-169) landscape/client/manager/tests/test_snapmanager.py (+164/-40) landscape/client/manager/tests/test_snapservicesmanager.py (+333/-0) landscape/client/manager/tests/test_ubuntuproinfo.py (+151/-0) landscape/client/manager/tests/test_usermanager.py (+132/-2) landscape/client/manager/ubuntuproinfo.py (+102/-0) landscape/client/manager/usermanager.py (+12/-15) landscape/client/monitor/computerinfo.py (+37/-6) landscape/client/monitor/config.py (+1/-1) landscape/client/monitor/processorinfo.py (+28/-13) landscape/client/monitor/service.py (+6/-3) landscape/client/monitor/snapmonitor.py (+21/-10) landscape/client/monitor/snapservicesmonitor.py (+28/-0) landscape/client/monitor/tests/test_computerinfo.py (+84/-35) landscape/client/monitor/tests/test_processorinfo.py (+74/-2) landscape/client/monitor/tests/test_service.py (+29/-0) landscape/client/monitor/tests/test_snapmonitor.py (+75/-12) landscape/client/monitor/tests/test_snapservicesmonitor.py (+127/-0) landscape/client/monitor/tests/test_temperature.py (+2/-2) landscape/client/monitor/tests/test_usermonitor.py (+45/-0) landscape/client/monitor/updatemanager.py (+1/-1) landscape/client/monitor/usermonitor.py (+9/-2) landscape/client/package/changer.py (+19/-22) landscape/client/package/releaseupgrader.py (+10/-8) landscape/client/package/reporter.py (+9/-10) landscape/client/package/taskhandler.py (+6/-10) landscape/client/package/tests/test_changer.py (+10/-141) landscape/client/package/tests/test_releaseupgrader.py (+10/-13) landscape/client/package/tests/test_reporter.py (+42/-33) landscape/client/package/tests/test_taskhandler.py (+12/-25) landscape/client/serviceconfig.py (+2/-2) landscape/client/snap_http (+1/-0) landscape/client/snap_utils.py (+86/-0) landscape/client/tests/test_configuration.py (+86/-9) landscape/client/tests/test_deployment.py (+283/-0) landscape/client/tests/test_serviceconfig.py (+2/-0) landscape/client/tests/test_snap_utils.py (+175/-0) landscape/client/tests/test_watchdog.py (+21/-1) landscape/client/user/management.py (+107/-12) landscape/client/user/tests/helpers.py (+60/-12) landscape/client/user/tests/test_management.py (+251/-36) landscape/client/watchdog.py (+19/-29) landscape/lib/bpickle.py (+12/-2) landscape/lib/format.py (+22/-0) landscape/lib/os_release.py (+64/-0) landscape/lib/sysstats.py (+28/-34) landscape/lib/testing.py (+2/-2) landscape/lib/tests/test_bpickle.py (+8/-2) landscape/lib/tests/test_format.py (+89/-0) landscape/lib/tests/test_os_release.py (+137/-0) landscape/lib/tests/test_reactor.py (+3/-3) landscape/lib/tests/test_schema.py (+2/-1) landscape/lib/tests/test_sysstats.py (+128/-16) landscape/message_schemas/server_bound.py (+59/-1) landscape/sysinfo/tests/test_temperature.py (+9/-9) setup_client.py (+2/-1) setup_lib.py (+0/-1) snap-http/.github/workflows/daily.yml (+11/-0) snap-http/.github/workflows/integration-test.yml (+34/-0) snap-http/.github/workflows/test.yml (+46/-0) snap-http/.gitignore (+15/-0) snap-http/CHANGELOG.md (+17/-0) snap-http/LICENSE (+339/-0) snap-http/Makefile (+6/-0) snap-http/README.md (+50/-0) snap-http/poetry.lock (+411/-0) snap-http/pyproject.toml (+40/-0) snap-http/snap_http/__init__.py (+53/-0) snap-http/snap_http/api.py (+443/-0) snap-http/snap_http/http.py (+111/-0) snap-http/snap_http/types.py (+164/-0) snap-http/tests/integration/conftest.py (+61/-0) snap-http/tests/integration/test_api.py (+590/-0) snap-http/tests/integration/test_snap/setup.py (+16/-0) snap-http/tests/integration/test_snap/snap/hooks/configure (+24/-0) snap-http/tests/integration/test_snap/snap/hooks/default-configure (+1/-0) snap-http/tests/integration/test_snap/snap/snapcraft.yaml (+28/-0) snap-http/tests/integration/test_snap/test_snap/__init__.py (+0/-0) snap-http/tests/integration/test_snap/test_snap/__main__.py (+3/-0) snap-http/tests/integration/test_snap/test_snap/bye-svc (+11/-0) snap-http/tests/integration/test_snap/test_snap/hello-svc (+11/-0) snap-http/tests/integration/test_snap/test_snap/main.py (+2/-0) snap-http/tests/unit/__init__.py (+0/-0) snap-http/tests/unit/test_api.py (+1841/-0) snap-http/tests/unit/test_http.py (+292/-0) snap-http/tests/unit/test_types.py (+110/-0) snap-http/tests/utils.py (+69/-0) snap-http/tox.ini (+18/-0) snap/hooks/configure (+3/-0) snap/hooks/default-configure (+33/-0) snap/hooks/install (+5/-0) snap/snapcraft.yaml (+91/-73) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andreas Hasenack | Approve | ||
git-ubuntu import | Pending | ||
Review via email: mp+461404@code.launchpad.net |
Commit message
release landscape-client 24.02 for noble
Description of the change
This change imports the latest release of landscape-client, 24.02 and releases it for noble.
A build of this release can be found in this PPA: https:/
Andreas Hasenack (ahasenack) wrote : | # |
Andreas Hasenack (ahasenack) wrote : | # |
Please use ubuntu/devel as the MP target in the future, for devel releases. Right now it won't matter, but it's customary to use ubuntu/devel in these cases, and <release-
Andreas Hasenack (ahasenack) wrote : | # |
I downloaded the landscape-client tarball via uscan, which uses the debian/watch file:
$ uscan --download-
Newest version of landscape-client on remote site is 24.02, specified download version is 24.02
Successfully symlinked ../landscape-
This tarball however does not match the contents of the branch here:
$ dpkg-buildpackage -S -I -i -nc -d
dpkg-buildpackage: info: source package landscape-client
dpkg-buildpackage: info: source version 24.02-0ubuntu0
dpkg-buildpackage: info: source distribution noble
dpkg-buildpackage: info: source changed by Mitch Burton <email address hidden>
dpkg-source -I -i --before-build .
dpkg-buildpackage: warning: building a source package without cleaning up as you asked; it might contain undesired files
dpkg-source -I -i -b .
dpkg-source: info: using source format '3.0 (quilt)'
dpkg-source: info: building landscape-client using existing ./landscape-
dpkg-source: error: cannot represent change to snap-http/
dpkg-source: error: new version is symlink to configure
dpkg-source: error: old version is nonexistent
dpkg-source: warning: newly created empty file 'snap-http/
dpkg-source: warning: newly created empty file 'snap-http/
dpkg-source: warning: newly created empty file 'snap-http/
dpkg-source: warning: newly created empty file 'snap-http/
dpkg-source: error: unrepresentable changes to source
dpkg-buildpackage: error: dpkg-source -I -i -b . subprocess returned exit status 1
Indeed, there are differences:
$ diff -uNr landscape-
diff: landscape-
.github/
.github/
.github/
.gitignore | 15
CHANGELOG.md | 17
LICENSE | 339 +++++++++++++++++++
Makefile | 6
README.md | 50 ++
poetry.lock | 411 +++++++
pyproject.toml | 40 ++
snap_http/
snap_http/api.py | 443 +++++++
snap_http/http.py | 111 ++++++
snap_http/types.py | 164 +++++++++
tests/
tests/
Andreas Hasenack (ahasenack) wrote : | # |
I remember you mentioning the snap work, and that a lot of it does not "make sense" for debs. But the source tarball that you, upstream, release, must match what is used by the deb to build the package. So you either make separate releases (snap-only, and everything else), or include the snap bits in the tarball used by the deb source package, or something else.
Andreas Hasenack (ahasenack) : | # |
Andreas Hasenack (ahasenack) : | # |
Andreas Hasenack (ahasenack) wrote : | # |
Ah, disregard the test errors from above. That was another version I was building by mistake.
Mitch Burton (mitchburton) wrote : | # |
The uscan difference is because the watch file is pointing at the tags page in Github and not the releases page. We've moved to using the latter for release artifacts. I will correct shortly.
- 58a9f27... by Mitch Burton
-
d/watch: use github releases tarball instead of tag tarball
Mitch Burton (mitchburton) wrote : | # |
$ uscan --download-
Newest version of landscape-client on remote site is 24.02, specified download version is 24.02
Leaving ../landscape-
$ diff -uNr landscape-
0 files changed
Andreas Hasenack (ahasenack) : | # |
Mitch Burton (mitchburton) wrote : | # |
changed version to -0ubuntu1 and fixed changelog indentation
Andreas Hasenack (ahasenack) wrote : | # |
Inline question about expandvars().
Andreas Hasenack (ahasenack) wrote : | # |
--- a/snap/
+++ b/snap/
@@ -1,98 +1,116 @@
name: landscape-client
base: core22
-version: '0.1'
+version: '23.08'
Does this version need to be 24.02?
Mitch Burton (mitchburton) wrote : | # |
> --- a/snap/
> +++ b/snap/
> @@ -1,98 +1,116 @@
> name: landscape-client
> base: core22
> -version: '0.1'
> +version: '23.08'
>
> Does this version need to be 24.02?
Yes, this should be updated. It only affects the snap so I don't think it's impactful for the deb upload. Should I patch it here, anyways?
Andreas Hasenack (ahasenack) wrote : | # |
> > --- a/snap/
> > +++ b/snap/
> > @@ -1,98 +1,116 @@
> > name: landscape-client
> > base: core22
> > -version: '0.1'
> > +version: '23.08'
> >
> > Does this version need to be 24.02?
> Yes, this should be updated. It only affects the snap so I don't think it's
> impactful for the deb upload. Should I patch it here, anyways?
If it's a no-op for the deb, then no need.
Mitch Burton (mitchburton) wrote : | # |
inline response for expandvars
Andreas Hasenack (ahasenack) : | # |
Mitch Burton (mitchburton) : | # |
Mitch Burton (mitchburton) : | # |
Andreas Hasenack (ahasenack) wrote : | # |
d/changelog:
This new upstream version is adding snap-http/, which has its own LICENSE file. It is the same license as landscape-client itself (debian/copyright), so that's ok. SRU reviewers might want an explicit update to d/copyright about it, though. I'm unsure.
The new snap-http code should be called out in d/changelog, together with other debian/* changes:
- the build-dependency changes (python3-dbus)
- the runtime dependency changes (lsb-base dropped, python3-setuptools added, python3-dbus added)
- dropped patches
- d/watch update
The amount of detail you want in d/changelog about the *upstream* changes is up to you. You should definitely highlight important changes in upstream code. But packaging changes, in general, we like to have described. This will receive much more scrutiny in an SRU. In fact, it's grounds for a rejection if a packaging change (anything in debian/*) is not described in the changelog.
Mitch Burton (mitchburton) wrote : | # |
I've updated the changelog to call out the specific changes to d/control, d/watch, and the addition of snap-http. Hopefully I've added enough detail in there.
Thanks for the additional feedback on how this might look for SRU review.
Andreas Hasenack (ahasenack) wrote : | # |
+1. And let's get #2055348 closed soon.
Andreas Hasenack (ahasenack) wrote : | # |
Sponsored:
Uploading landscape-
Uploading landscape-
Uploading landscape-
Uploading landscape-
Uploading landscape-
Andreas Hasenack (ahasenack) wrote : | # |
This migrated already, closing MP.
Preview Diff
1 | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml |
2 | index 54fd142..38efb55 100644 |
3 | --- a/.github/workflows/ci.yml |
4 | +++ b/.github/workflows/ci.yml |
5 | @@ -1,34 +1,26 @@ |
6 | name: ci |
7 | on: [pull_request, workflow_dispatch] |
8 | jobs: |
9 | - check3: |
10 | + check: |
11 | runs-on: ${{ matrix.os }} |
12 | strategy: |
13 | matrix: |
14 | os: ["ubuntu-22.04", "ubuntu-20.04"] |
15 | steps: |
16 | - - uses: actions/checkout@v2 |
17 | + - uses: actions/checkout@v4 |
18 | + with: |
19 | + submodules: true |
20 | - run: | |
21 | make depends |
22 | # -common seems a catch-22, but this is just a shortcut to |
23 | # initialize user and dirs, some used through tests. |
24 | sudo apt-get -y install landscape-common |
25 | - - run: make check3 TRIAL=/usr/bin/trial3 |
26 | + - run: make check TRIAL=/usr/bin/trial3 |
27 | lint: |
28 | runs-on: ubuntu-latest |
29 | steps: |
30 | - - uses: actions/checkout@v2 |
31 | + - uses: actions/checkout@v4 |
32 | + with: |
33 | + submodules: true |
34 | - run: make depends |
35 | - run: make lint |
36 | - coverage: |
37 | - runs-on: ubuntu-latest |
38 | - steps: |
39 | - - uses: actions/checkout@v2 |
40 | - - run: | |
41 | - make depends |
42 | - # -common seems a catch-22, but this is just a shortcut to |
43 | - # initialize user and dirs, some used through tests. |
44 | - sudo apt-get -y install landscape-common |
45 | - - run: make coverage TRIAL=/usr/bin/trial3 |
46 | - - name: upload |
47 | - uses: codecov/codecov-action@v1 |
48 | diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml |
49 | new file mode 100644 |
50 | index 0000000..74ec228 |
51 | --- /dev/null |
52 | +++ b/.github/workflows/codecov.yml |
53 | @@ -0,0 +1,19 @@ |
54 | +name: Codecov upload |
55 | + |
56 | +on: [push, pull_request] |
57 | + |
58 | +jobs: |
59 | + coverage: |
60 | + runs-on: ubuntu-latest |
61 | + steps: |
62 | + - uses: actions/checkout@v4 |
63 | + with: |
64 | + submodules: true |
65 | + - run: | |
66 | + make depends |
67 | + # -common seems a catch-22, but this is just a shortcut to |
68 | + # initialize user and dirs, some used through tests. |
69 | + sudo apt-get -y install landscape-common |
70 | + - run: make coverage TRIAL=/usr/bin/trial3 |
71 | + - name: upload |
72 | + uses: codecov/codecov-action@v3 |
73 | diff --git a/.gitmodules b/.gitmodules |
74 | new file mode 100644 |
75 | index 0000000..ab50d1c |
76 | --- /dev/null |
77 | +++ b/.gitmodules |
78 | @@ -0,0 +1,3 @@ |
79 | +[submodule "snap-http"] |
80 | + path = snap-http |
81 | + url = https://github.com/Perfect5th/snap-http |
82 | diff --git a/Makefile b/Makefile |
83 | index 09ade0e..9626920 100644 |
84 | --- a/Makefile |
85 | +++ b/Makefile |
86 | @@ -1,10 +1,10 @@ |
87 | PYDOCTOR ?= pydoctor |
88 | TXT2MAN ?= txt2man |
89 | -PYTHON2 ?= python2 |
90 | -PYTHON3 ?= python3 |
91 | +PYTHON ?= python3 |
92 | SNAPCRAFT = SNAPCRAFT_BUILD_INFO=1 snapcraft |
93 | TRIAL ?= -m twisted.trial |
94 | TRIAL_ARGS ?= |
95 | +PRE_COMMIT ?= $(HOME)/.local/bin/pre-commit |
96 | |
97 | # PEP8 rules ignored: |
98 | # W503 https://www.flake8rules.com/rules/W503.html |
99 | @@ -16,57 +16,35 @@ help: ## Print help about available targets |
100 | @grep -h -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' |
101 | |
102 | .PHONY: depends |
103 | -depends: depends3 ## py2 is deprecated |
104 | - sudo apt-get -y install python3-flake8 python3-coverage |
105 | - |
106 | -.PHONY: depends2 |
107 | -depends2: |
108 | - sudo apt-get -y install python-twisted-core python-distutils-extra python-mock python-configobj python-netifaces python-pycurl python-pip |
109 | +depends: |
110 | + sudo apt-get -y install python3-configobj python3-coverage python3-distutils-extra\ |
111 | + python3-flake8 python3-mock python3-netifaces python3-pip python3-pycurl python3-twisted\ |
112 | + net-tools |
113 | pip install pre-commit |
114 | - pre-commit install |
115 | - |
116 | -.PHONY: depends3 |
117 | -depends3: |
118 | - sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl python3-pip |
119 | - pip3 install pre-commit |
120 | - pre-commit install |
121 | + $(PRE_COMMIT) install |
122 | |
123 | all: build |
124 | |
125 | .PHONY: build |
126 | -build: build2 build3 ## Build. |
127 | - |
128 | -.PHONY: build2 |
129 | -build2: |
130 | - $(PYTHON2) setup.py build_ext -i |
131 | - |
132 | -.PHONY: build3 |
133 | -build3: |
134 | - $(PYTHON3) setup.py build_ext -i |
135 | - |
136 | -.PHONY: check |
137 | -check: check2 check3 ## Run all the tests. |
138 | - |
139 | -.PHONY: check2 |
140 | -check2: build2 |
141 | - PYTHONPATH=$(PYTHONPATH):$(CURDIR) LC_ALL=C $(PYTHON2) $(TRIAL) --unclean-warnings $(TRIAL_ARGS) landscape |
142 | +build: |
143 | + $(PYTHON) setup.py build_ext -i |
144 | |
145 | # trial3 does not support threading via `-j` at the moment |
146 | # so we ignore TRIAL_ARGS. |
147 | # TODO: Respect $TRIAL_ARGS once trial3 is fixed. |
148 | -.PHONY: check3 |
149 | -check3: TRIAL_ARGS= |
150 | -check3: build3 |
151 | - PYTHONPATH=$(PYTHONPATH):$(CURDIR) LC_ALL=C $(PYTHON3) $(TRIAL) --unclean-warnings $(TRIAL_ARGS) landscape |
152 | +.PHONY: check |
153 | +check: TRIAL_ARGS= |
154 | +check: build |
155 | + PYTHONPATH=$(PYTHONPATH):$(CURDIR) LC_ALL=C $(PYTHON) $(TRIAL) --unclean-warnings $(TRIAL_ARGS) landscape |
156 | |
157 | .PHONY: coverage |
158 | coverage: |
159 | - PYTHONPATH=$(PYTHONPATH):$(CURDIR) LC_ALL=C $(PYTHON3) -m coverage run $(TRIAL) --unclean-warnings landscape |
160 | - PYTHONPATH=$(PYTHONPATH):$(CURDIR) LC_ALL=C $(PYTHON3) -m coverage xml |
161 | + PYTHONPATH=$(PYTHONPATH):$(CURDIR) LC_ALL=C $(PYTHON) -m coverage run $(TRIAL) --unclean-warnings landscape |
162 | + PYTHONPATH=$(PYTHONPATH):$(CURDIR) LC_ALL=C $(PYTHON) -m coverage xml |
163 | |
164 | .PHONY: lint |
165 | lint: |
166 | - $(PYTHON3) -m flake8 --ignore $(PEP8_IGNORED) `find landscape -name \*.py` |
167 | + $(PYTHON) -m flake8 --ignore $(PEP8_IGNORED) `find landscape -name \*.py` |
168 | |
169 | .PHONY: pyflakes |
170 | pyflakes: |
171 | diff --git a/Makefile.packaging b/Makefile.packaging |
172 | index 5a43ce7..d3c90ad 100644 |
173 | --- a/Makefile.packaging |
174 | +++ b/Makefile.packaging |
175 | @@ -60,8 +60,8 @@ releasetarball: |
176 | |
177 | .PHONY: sdist |
178 | sdist: clean |
179 | - mkdir -p sdist |
180 | - git archive --prefix landscape-client-$(TARBALL_VERSION)/ HEAD | tar -x -C sdist |
181 | + mkdir -p sdist/landscape-client-$(TARBALL_VERSION) |
182 | + git ls-files --recurse-submodules | xargs -I {} cp -r --parents {} sdist/landscape-client-$(TARBALL_VERSION) |
183 | rm -rf sdist/landscape-client-$(TARBALL_VERSION)/debian |
184 | sed -i -e "s/^UPSTREAM_VERSION.*/UPSTREAM_VERSION = \"$(TARBALL_VERSION)\"/g" \ |
185 | sdist/landscape-client-$(TARBALL_VERSION)/landscape/__init__.py |
186 | diff --git a/README b/README |
187 | index 8751ef3..c8a4737 100644 |
188 | --- a/README |
189 | +++ b/README |
190 | @@ -58,6 +58,12 @@ Landscape service. There are two ways to do this: |
191 | |
192 | ## Developing |
193 | |
194 | +After cloning the repository, make sure you run the following command to pull the `snap-http` submodule: |
195 | + |
196 | +```shell |
197 | +git submodule update --init |
198 | +``` |
199 | + |
200 | To run the full test suite, run the following command: |
201 | |
202 | ``` |
203 | @@ -81,12 +87,17 @@ system bus. |
204 | $ sudo ./scripts/landscape-client -c root-client.conf |
205 | ``` |
206 | |
207 | -Before opening a PR, make sure to run the full testsuite and lint |
208 | +Before opening a PR, make sure to run the full test suite and lint: |
209 | ``` |
210 | -make check3 |
211 | +make check |
212 | make lint |
213 | ``` |
214 | |
215 | +You can run a specific test by running the following (for example): |
216 | +``` |
217 | +python3 -m twisted.trial landscape.client.broker.tests.test_client.BrokerClientTest.test_ping |
218 | +``` |
219 | + |
220 | ### Building the Landscape Client snap |
221 | |
222 | First, you need to ensure that you have the appropriate tools installed: |
223 | @@ -158,7 +169,7 @@ $ gnome-keyring-daemon --unlock |
224 | The gnome-keyring-daemon will prompt you (without a prompt) to type in |
225 | the initial unlock password (typically the same password for the account |
226 | you are using - if you are using the default multipass or lxc "ubuntu" |
227 | -login, use sudo passwd ubuntu to set it to a known value before doing |
228 | +login, use sudo passwd ubuntu to set it to a known value before doing |
229 | the above step). |
230 | |
231 | Type the login password and hit <ENTER> followed by <CTRL>+D to end |
232 | diff --git a/debian/changelog b/debian/changelog |
233 | index 5643e19..6aa9e2c 100644 |
234 | --- a/debian/changelog |
235 | +++ b/debian/changelog |
236 | @@ -1,3 +1,34 @@ |
237 | +landscape-client (24.02-0ubuntu1) noble; urgency=medium |
238 | + |
239 | + * New upstream release 24.02 |
240 | + - d/control: added python3-dbus to Build-Depends |
241 | + - d/control: added python3-dbus to landscape-client Depends. Added |
242 | + python3-setuptools to landscape-common Depends. Removed lsb-base from |
243 | + landscape-common Depends. |
244 | + - d/control: use dephelper-compat 12, removed d/compat |
245 | + - d/patches: removed patches that have been applied upstream |
246 | + - d/watch: watch Github releases page instead of tags |
247 | + - snap-http: included new submodule under identical license |
248 | + - use dbus in shutdown manager instead of subprocess |
249 | + - improve messages about manager plugin configuration |
250 | + - add getting and settings snap configurations |
251 | + - read temperature from hwmon devices |
252 | + - add snapd device information to ComputerInfo message |
253 | + - add snap services management |
254 | + - d/landscape-common.postinst d/landscape-common.prerm: do not call |
255 | + update-motd when installing/removing landscape-sysinfo.wrapper |
256 | + (LP: #1855544) |
257 | + - Livepatch status reporting bug |
258 | + - bpickle: guard against negative string/bytestring lengths |
259 | + - Makefile: remove check2, depends2, and references to python2 |
260 | + - add path for os-release inside a snap |
261 | + - computer title generation for zero-touch deployment on Core |
262 | + - user management on Core |
263 | + - deploy system user assertion to device |
264 | + - remote script execution for snaps |
265 | + |
266 | + -- Mitch Burton <mitch.burton@canonical.com> Thu, 27 Feb 2024 16:46:56 -0800 |
267 | + |
268 | landscape-client (23.08-0ubuntu4) noble; urgency=medium |
269 | |
270 | * d/p/0003-fix-cpuinfo-and-tests.patch: fix ARM and RISCV cpuinfo parsing; |
271 | diff --git a/debian/control b/debian/control |
272 | index c70ceae..3a859a6 100644 |
273 | --- a/debian/control |
274 | +++ b/debian/control |
275 | @@ -8,7 +8,7 @@ Build-Depends: debhelper-compat (= 12), po-debconf, libdistro-info-perl, |
276 | lsb-release, gawk, net-tools, |
277 | python3-apt, python3-twisted, python3-configobj, |
278 | python3-pycurl, python3-netifaces, python3-yaml, |
279 | - ubuntu-advantage-tools, locales-all |
280 | + ubuntu-advantage-tools, locales-all, python3-dbus |
281 | Standards-Version: 4.4.0 |
282 | Homepage: https://github.com/CanonicalLtd/landscape-client |
283 | |
284 | @@ -22,11 +22,11 @@ Depends: ${python3:Depends}, ${misc:Depends}, ${extra:Depends}, |
285 | python3-gdbm, |
286 | python3-netifaces, |
287 | lsb-release, |
288 | - lsb-base, |
289 | adduser, |
290 | bc, |
291 | lshw, |
292 | - libpam-modules |
293 | + libpam-modules, |
294 | + python3-setuptools |
295 | Description: Landscape administration system client - Common files |
296 | Landscape is a web-based tool for managing Ubuntu systems. This |
297 | package is necessary if you want your machine to be managed in a |
298 | @@ -42,6 +42,7 @@ Depends: ${python3:Depends}, ${misc:Depends}, ${extra:Depends}, |
299 | ${shlibs:Depends}, |
300 | landscape-common (= ${binary:Version}), |
301 | python3-pycurl, |
302 | + python3-dbus |
303 | Description: Landscape administration system client |
304 | Landscape is a web-based tool for managing Ubuntu systems. This |
305 | package is necessary if you want your machine to be managed in a |
306 | diff --git a/debian/landscape-common.postinst b/debian/landscape-common.postinst |
307 | index e78c193..09826f5 100755 |
308 | --- a/debian/landscape-common.postinst |
309 | +++ b/debian/landscape-common.postinst |
310 | @@ -90,18 +90,16 @@ case "$1" in |
311 | WRAPPER=/usr/share/landscape/landscape-sysinfo.wrapper |
312 | PROFILE_LOCATION=/etc/profile.d/50-landscape-sysinfo.sh |
313 | UPDATE_MOTD_LOCATION=/etc/update-motd.d/50-landscape-sysinfo |
314 | - |
315 | if [ "$RET" = "Cache sysinfo in /etc/motd" ]; then |
316 | rm -f $PROFILE_LOCATION 2>/dev/null || true |
317 | ln -sf $WRAPPER $UPDATE_MOTD_LOCATION |
318 | - update-motd 2>/dev/null || true |
319 | + $WRAPPER >/dev/null 2>&1 || true |
320 | elif [ "$RET" = "Run sysinfo on every login" ]; then |
321 | rm -f $UPDATE_MOTD_LOCATION 2>/dev/null || true |
322 | - update-motd 2>/dev/null || true |
323 | ln -sf $WRAPPER $PROFILE_LOCATION |
324 | + $WRAPPER >/dev/null 2>&1 || true |
325 | else |
326 | rm -f $UPDATE_MOTD_LOCATION 2>/dev/null || true |
327 | - update-motd 2>/dev/null || true |
328 | rm -f $PROFILE_LOCATION || true |
329 | fi |
330 | |
331 | diff --git a/debian/landscape-common.prerm b/debian/landscape-common.prerm |
332 | index 049aa63..14e35dc 100644 |
333 | --- a/debian/landscape-common.prerm |
334 | +++ b/debian/landscape-common.prerm |
335 | @@ -20,7 +20,6 @@ set -e |
336 | case "$1" in |
337 | remove|upgrade|deconfigure) |
338 | rm -f /etc/update-motd.d/50-landscape-sysinfo 2>/dev/null || true |
339 | - update-motd 2>/dev/null || true |
340 | rm -f /etc/profile.d/landscape-sysinfo.sh 2>/dev/null || true |
341 | ;; |
342 | |
343 | diff --git a/debian/patches/0001-start-service-during-config.patch b/debian/patches/0001-start-service-during-config.patch |
344 | deleted file mode 100644 |
345 | index d3fb5f9..0000000 |
346 | --- a/debian/patches/0001-start-service-during-config.patch |
347 | +++ /dev/null |
348 | @@ -1,269 +0,0 @@ |
349 | -Description: Allow landscape-config to start landscape-client systemd service |
350 | -Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/landscape-client/+bug/2040189 |
351 | -Author: Mitch Burton <mitch.burton@canonical.com> |
352 | -Origin: upstream, https://github.com/canonical/landscape-client/commit/0da6b4b64c7ca50c109279bd42633c537458fcf4 |
353 | -Reviewed-by: Kevin Nasto <kevin.nasto@canonical.com> |
354 | -Applied-Upstream: 23.10 |
355 | -Last-Update: 2023-11-07 |
356 | ---- |
357 | -This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ |
358 | ---- a/landscape/client/configuration.py |
359 | -+++ b/landscape/client/configuration.py |
360 | -@@ -624,8 +624,10 @@ |
361 | - decode_base64_ssl_public_certificate(config) |
362 | - config.write() |
363 | - # Restart the client to ensure that it's using the new configuration. |
364 | -+ |
365 | - if not config.no_start: |
366 | - try: |
367 | -+ set_secure_id(config, "registering") |
368 | - ServiceConfig.restart_landscape() |
369 | - except ServiceConfigException as exc: |
370 | - print_text(str(exc), error=True) |
371 | -@@ -796,13 +798,14 @@ |
372 | - # Results will be things like "success" or "ssl-error". |
373 | - result = results[0] |
374 | - |
375 | -- if isinstance(result, SystemExit): |
376 | -- raise result |
377 | -- |
378 | - # If there was an error and the caller requested that errors be reported |
379 | - # to the on_error callable, then do so. |
380 | - if result != "success" and on_error is not None: |
381 | - on_error(1) |
382 | -+ |
383 | -+ if isinstance(result, SystemExit): |
384 | -+ raise result |
385 | -+ |
386 | - return result |
387 | - |
388 | - |
389 | -@@ -883,6 +886,21 @@ |
390 | - return text |
391 | - |
392 | - |
393 | -+def set_secure_id(config, new_id): |
394 | -+ """Persists a secure id in the identity data file. This is used to indicate |
395 | -+ whether we are currently in the process of registering. |
396 | -+ """ |
397 | -+ persist = Persist( |
398 | -+ filename=os.path.join( |
399 | -+ config.data_path, |
400 | -+ f"{BrokerService.service_name}.bpickle", |
401 | -+ ), |
402 | -+ ) |
403 | -+ identity = Identity(config, persist) |
404 | -+ identity.secure_id = new_id |
405 | -+ persist.save() |
406 | -+ |
407 | -+ |
408 | - def main(args, print=print): |
409 | - """Interact with the user and the server to set up client configuration.""" |
410 | - |
411 | -@@ -927,7 +945,11 @@ |
412 | - # Attempt to register the client. |
413 | - reactor = LandscapeReactor() |
414 | - if config.silent: |
415 | -- result = register(config, reactor) |
416 | -+ result = register( |
417 | -+ config, |
418 | -+ reactor, |
419 | -+ on_error=lambda _: set_secure_id(config, None), |
420 | -+ ) |
421 | - report_registration_outcome(result, print=print) |
422 | - sys.exit(determine_exit_code(result)) |
423 | - else: |
424 | -@@ -937,6 +959,10 @@ |
425 | - default=default_answer, |
426 | - ) |
427 | - if answer: |
428 | -- result = register(config, reactor) |
429 | -+ result = register( |
430 | -+ config, |
431 | -+ reactor, |
432 | -+ on_error=lambda _: set_secure_id(config, None), |
433 | -+ ) |
434 | - report_registration_outcome(result, print=print) |
435 | - sys.exit(determine_exit_code(result)) |
436 | ---- a/landscape/client/tests/test_configuration.py |
437 | -+++ b/landscape/client/tests/test_configuration.py |
438 | -@@ -34,6 +34,7 @@ |
439 | - from landscape.client.configuration import register |
440 | - from landscape.client.configuration import registration_info_text |
441 | - from landscape.client.configuration import report_registration_outcome |
442 | -+from landscape.client.configuration import set_secure_id |
443 | - from landscape.client.configuration import setup |
444 | - from landscape.client.configuration import show_help |
445 | - from landscape.client.configuration import store_public_key_data |
446 | -@@ -738,12 +739,17 @@ |
447 | - bootstrap_tree_patcher = mock.patch( |
448 | - "landscape.client.configuration.bootstrap_tree", |
449 | - ) |
450 | -+ set_secure_id_patch = mock.patch( |
451 | -+ "landscape.client.configuration.set_secure_id", |
452 | -+ ) |
453 | - self.mock_getuid = getuid_patcher.start() |
454 | - self.mock_bootstrap_tree = bootstrap_tree_patcher.start() |
455 | -+ set_secure_id_patch.start() |
456 | - |
457 | - def cleanup(): |
458 | - getuid_patcher.stop() |
459 | - bootstrap_tree_patcher.stop() |
460 | -+ set_secure_id_patch.stop() |
461 | - |
462 | - self.addCleanup(cleanup) |
463 | - |
464 | -@@ -1191,7 +1197,11 @@ |
465 | - ) |
466 | - self.assertEqual(0, exception.code) |
467 | - mock_setup.assert_called_once_with(mock.ANY) |
468 | -- mock_register.assert_called_once_with(mock.ANY, mock.ANY) |
469 | -+ mock_register.assert_called_once_with( |
470 | -+ mock.ANY, |
471 | -+ mock.ANY, |
472 | -+ on_error=mock.ANY, |
473 | -+ ) |
474 | - |
475 | - @mock.patch("landscape.client.configuration.input", return_value="y") |
476 | - @mock.patch( |
477 | -@@ -1219,7 +1229,11 @@ |
478 | - ) |
479 | - self.assertEqual(0, exception.code) |
480 | - mock_setup.assert_called_once_with(mock.ANY) |
481 | -- mock_register.assert_called_once_with(mock.ANY, mock.ANY) |
482 | -+ mock_register.assert_called_once_with( |
483 | -+ mock.ANY, |
484 | -+ mock.ANY, |
485 | -+ on_error=mock.ANY, |
486 | -+ ) |
487 | - mock_input.assert_called_once_with( |
488 | - "\nRequest a new registration for this computer now? [Y/n]: ", |
489 | - ) |
490 | -@@ -1256,7 +1270,11 @@ |
491 | - ) |
492 | - self.assertEqual(2, exception.code) |
493 | - mock_setup.assert_called_once_with(mock.ANY) |
494 | -- mock_register.assert_called_once_with(mock.ANY, mock.ANY) |
495 | -+ mock_register.assert_called_once_with( |
496 | -+ mock.ANY, |
497 | -+ mock.ANY, |
498 | -+ on_error=mock.ANY, |
499 | -+ ) |
500 | - mock_input.assert_called_once_with( |
501 | - "\nRequest a new registration for this computer now? [Y/n]: ", |
502 | - ) |
503 | -@@ -1295,7 +1313,11 @@ |
504 | - ) |
505 | - self.assertEqual(0, exception.code) |
506 | - mock_setup.assert_called_once_with(mock.ANY) |
507 | -- mock_register.assert_called_once_with(mock.ANY, mock.ANY) |
508 | -+ mock_register.assert_called_once_with( |
509 | -+ mock.ANY, |
510 | -+ mock.ANY, |
511 | -+ on_error=mock.ANY, |
512 | -+ ) |
513 | - mock_input.assert_not_called() |
514 | - |
515 | - self.assertEqual( |
516 | -@@ -1333,7 +1355,11 @@ |
517 | - ) |
518 | - self.assertEqual(2, exception.code) |
519 | - mock_setup.assert_called_once_with(mock.ANY) |
520 | -- mock_register.assert_called_once_with(mock.ANY, mock.ANY) |
521 | -+ mock_register.assert_called_once_with( |
522 | -+ mock.ANY, |
523 | -+ mock.ANY, |
524 | -+ on_error=mock.ANY, |
525 | -+ ) |
526 | - mock_input.assert_not_called() |
527 | - # Note that the error is output via sys.stderr. |
528 | - self.assertEqual( |
529 | -@@ -1378,7 +1404,11 @@ |
530 | - mock_serviceconfig.set_start_on_boot.assert_called_once_with(True) |
531 | - mock_serviceconfig.restart_landscape.assert_called_once_with() |
532 | - mock_setup_script().run.assert_called_once_with() |
533 | -- mock_register.assert_called_once_with(mock.ANY, mock.ANY) |
534 | -+ mock_register.assert_called_once_with( |
535 | -+ mock.ANY, |
536 | -+ mock.ANY, |
537 | -+ on_error=mock.ANY, |
538 | -+ ) |
539 | - mock_input.assert_called_with( |
540 | - "\nRequest a new registration for this computer now? [Y/n]: ", |
541 | - ) |
542 | -@@ -1457,7 +1487,11 @@ |
543 | - print=noop_print, |
544 | - ) |
545 | - mock_setup.assert_called_once_with(mock.ANY) |
546 | -- mock_register.assert_called_once_with(mock.ANY, mock.ANY) |
547 | -+ mock_register.assert_called_once_with( |
548 | -+ mock.ANY, |
549 | -+ mock.ANY, |
550 | -+ on_error=mock.ANY, |
551 | -+ ) |
552 | - mock_input.assert_called_once_with( |
553 | - "\nRequest a new registration for this computer now? [Y/n]: ", |
554 | - ) |
555 | -@@ -1477,7 +1511,11 @@ |
556 | - print=noop_print, |
557 | - ) |
558 | - mock_setup.assert_called_once_with(mock.ANY) |
559 | -- mock_register.assert_called_once_with(mock.ANY, mock.ANY) |
560 | -+ mock_register.assert_called_once_with( |
561 | -+ mock.ANY, |
562 | -+ mock.ANY, |
563 | -+ on_error=mock.ANY, |
564 | -+ ) |
565 | - mock_input.assert_not_called() |
566 | - |
567 | - @mock.patch("landscape.client.configuration.input") |
568 | -@@ -2290,6 +2328,27 @@ |
569 | - # We ask for retries because networks aren't reliable. |
570 | - self.assertEqual(99, connector.max_retries) |
571 | - |
572 | -+ def test_register_got_error(self): |
573 | -+ """If there is an error from the connection, raises `SystemExit`.""" |
574 | -+ reactor = mock.Mock() |
575 | -+ connector_factory = mock.Mock() |
576 | -+ results = [] |
577 | -+ |
578 | -+ def add_error(): |
579 | -+ results.append(SystemExit()) |
580 | -+ |
581 | -+ reactor.run.side_effect = add_error |
582 | -+ |
583 | -+ self.assertRaises( |
584 | -+ SystemExit, |
585 | -+ register, |
586 | -+ self.config, |
587 | -+ reactor, |
588 | -+ connector_factory, |
589 | -+ max_retries=2, |
590 | -+ results=results, |
591 | -+ ) |
592 | -+ |
593 | - @mock.patch("landscape.client.configuration.LandscapeReactor") |
594 | - def test_register_without_reactor(self, mock_reactor): |
595 | - """If no reactor is passed, a LandscapeReactor will be instantiated. |
596 | -@@ -2683,3 +2742,21 @@ |
597 | - print=noop_print, |
598 | - ) |
599 | - self.assertEqual(EXIT_NOT_REGISTERED, exception.code) |
600 | -+ |
601 | -+ |
602 | -+class SetSecureIdTest(LandscapeTest): |
603 | -+ """Tests for the `set_secure_id` function.""" |
604 | -+ |
605 | -+ @mock.patch("landscape.client.configuration.Persist") |
606 | -+ @mock.patch("landscape.client.configuration.Identity") |
607 | -+ def test_function(self, Identity, Persist): |
608 | -+ config = mock.Mock(data_path="/tmp/landscape") |
609 | -+ |
610 | -+ set_secure_id(config, "fancysecureid") |
611 | -+ |
612 | -+ Persist.assert_called_once_with( |
613 | -+ filename="/tmp/landscape/broker.bpickle", |
614 | -+ ) |
615 | -+ Persist().save.assert_called_once_with() |
616 | -+ Identity.assert_called_once_with(config, Persist()) |
617 | -+ self.assertEqual(Identity().secure_id, "fancysecureid") |
618 | diff --git a/debian/patches/0002-fix-broken-build-tests.patch b/debian/patches/0002-fix-broken-build-tests.patch |
619 | deleted file mode 100644 |
620 | index 8a118b3..0000000 |
621 | --- a/debian/patches/0002-fix-broken-build-tests.patch |
622 | +++ /dev/null |
623 | @@ -1,54 +0,0 @@ |
624 | -Description: Fix tests that do not pass in debian build environment |
625 | - In environments where the run-as user's home directory does not exist, such |
626 | - as sbuild, fall back to expecting the script path to be the root directory. |
627 | - Mock snapd, as it does not run in an accessible way in some environments. |
628 | -Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/landscape-client/+bug/2044181 |
629 | -Author: Mitch Burton <mitch.burton@canonical.com> |
630 | -Last-Update: 2023-11-22 |
631 | ---- |
632 | -This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ |
633 | ---- a/landscape/client/manager/tests/test_scriptexecution.py |
634 | -+++ b/landscape/client/manager/tests/test_scriptexecution.py |
635 | -@@ -463,6 +463,9 @@ |
636 | - gid = info.pw_gid |
637 | - path = info.pw_dir |
638 | - |
639 | -+ if not os.path.exists(path): |
640 | -+ path = "/" |
641 | -+ |
642 | - return self._run_script(username, uid, gid, path) |
643 | - |
644 | - def test_user_no_home(self): |
645 | ---- a/landscape/client/monitor/tests/test_snapmonitor.py |
646 | -+++ b/landscape/client/monitor/tests/test_snapmonitor.py |
647 | -@@ -16,7 +16,12 @@ |
648 | - |
649 | - def test_get_data(self): |
650 | - """Tests getting installed snap data.""" |
651 | -+ snap_http_mock = Mock( |
652 | -+ spec=SnapHttp, |
653 | -+ get_snaps=Mock(return_value={"result": []}), |
654 | -+ ) |
655 | - plugin = SnapMonitor() |
656 | -+ plugin._snap_http = snap_http_mock |
657 | - self.monitor.add(plugin) |
658 | - |
659 | - plugin.exchange() |
660 | ---- a/landscape/client/snap/tests/test_http.py |
661 | -+++ b/landscape/client/snap/tests/test_http.py |
662 | -@@ -19,6 +19,15 @@ |
663 | - def test_get_snaps(self): |
664 | - """get_snaps() returns a dict with a list of installed snaps.""" |
665 | - http = SnapHttp() |
666 | -+ |
667 | -+ def fill_buff(curl, buff, **kwargs): |
668 | -+ buff.write( |
669 | -+ b'{"result": [{"id": "foo", "name": "bar", ' |
670 | -+ b'"publisher": "baz"}]}' |
671 | -+ ) |
672 | -+ |
673 | -+ http._perform = Mock(side_effect=fill_buff) |
674 | -+ |
675 | - result = http.get_snaps()["result"] |
676 | - |
677 | - self.assertTrue(isinstance(result, list)) |
678 | diff --git a/debian/patches/0003-fix-cpuinfo-and-tests.patch b/debian/patches/0003-fix-cpuinfo-and-tests.patch |
679 | deleted file mode 100644 |
680 | index 341a89e..0000000 |
681 | --- a/debian/patches/0003-fix-cpuinfo-and-tests.patch |
682 | +++ /dev/null |
683 | @@ -1,134 +0,0 @@ |
684 | -Description: Fix parsing processor IDs on ARM64 systems. |
685 | -Author: Mitch Burton <mitch.burton@canonical.com> |
686 | -Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/landscape-client/+bug/2046620 |
687 | -Last-Update: 2024-01-02 |
688 | ---- |
689 | -This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ |
690 | ---- a/landscape/client/monitor/processorinfo.py |
691 | -+++ b/landscape/client/monitor/processorinfo.py |
692 | -@@ -181,30 +181,43 @@ |
693 | - def create_message(self): |
694 | - """Returns a list containing information about each processor.""" |
695 | - processors = [] |
696 | -- file = open(self._source_filename) |
697 | -+ regexp = re.compile(r"(?P<key>.*?)\s*:\s*(?P<value>.*)") |
698 | -+ current = {} |
699 | - |
700 | -- try: |
701 | -- regexp = re.compile(r"(?P<key>.*?)\s*:\s*(?P<value>.*)") |
702 | -- current = {} |
703 | -+ with open(self._source_filename) as fp: |
704 | -+ for line in fp: |
705 | -+ line = line.strip() |
706 | -+ |
707 | -+ if not line: |
708 | -+ if current: |
709 | -+ processors.append(current.copy()) |
710 | -+ current = {} |
711 | -+ |
712 | -+ continue |
713 | - |
714 | -- for line in file: |
715 | - match = regexp.match(line.strip()) |
716 | -+ |
717 | - if match: |
718 | - key = match.group("key") |
719 | - value = match.group("value") |
720 | - |
721 | -- if key == "Processor": |
722 | -- # ARM doesn't support SMP, thus no processor-id in |
723 | -- # the cpuinfo |
724 | -- current["processor-id"] = 0 |
725 | -+ if key == "processor": |
726 | -+ current["processor-id"] = int(value) |
727 | -+ if "model" not in current: |
728 | -+ current["model"] = "arm" |
729 | -+ elif key == "Processor": |
730 | - current["model"] = value |
731 | - elif key == "Cache size": |
732 | - current["cache-size"] = int(value) |
733 | - |
734 | -- if current: |
735 | -- processors.append(current) |
736 | -- finally: |
737 | -- file.close() |
738 | -+ if current: |
739 | -+ processors.append(current) |
740 | -+ |
741 | -+ # Older ARM machines may not have processor-ids, but we need them, so |
742 | -+ # we set missing ones to 0. |
743 | -+ for processor in processors: |
744 | -+ if "processor-id" not in processor: |
745 | -+ processor["processor-id"] = 0 |
746 | - |
747 | - return processors |
748 | - |
749 | -@@ -357,6 +370,8 @@ |
750 | - |
751 | - if key == "processor": |
752 | - current = {"processor-id": int(parts[1].strip())} |
753 | -+ # A placeholder in case there is no model provided. |
754 | -+ current["model"] = "riscv" |
755 | - processors.append(current) |
756 | - elif key == "isa": |
757 | - current["vendor"] = parts[1].strip() |
758 | ---- a/landscape/client/monitor/tests/test_processorinfo.py |
759 | -+++ b/landscape/client/monitor/tests/test_processorinfo.py |
760 | -@@ -30,8 +30,7 @@ |
761 | - self.assertTrue(len(message["processors"]) > 0) |
762 | - |
763 | - for processor in message["processors"]: |
764 | -- self.assertTrue("processor-id" in processor) |
765 | -- self.assertTrue("model" in processor) |
766 | -+ self.assertIn("processor-id", processor) |
767 | - |
768 | - def test_call_on_accepted(self): |
769 | - """ |
770 | ---- a/landscape/client/package/reporter.py |
771 | -+++ b/landscape/client/package/reporter.py |
772 | -@@ -381,7 +381,11 @@ |
773 | - env["http_proxy"] = self._config.http_proxy |
774 | - if self._config.https_proxy: |
775 | - env["https_proxy"] = self._config.https_proxy |
776 | -- result = spawn_process(self.apt_update_filename, env=env) |
777 | -+ |
778 | -+ try: |
779 | -+ result = spawn_process(self.apt_update_filename, env=env) |
780 | -+ except Exception as e: |
781 | -+ return deferred.callback((b"", str(e).encode(), e.errno)) |
782 | - |
783 | - def callback(args, deferred): |
784 | - return deferred.callback(args) |
785 | ---- a/landscape/client/package/tests/test_releaseupgrader.py |
786 | -+++ b/landscape/client/package/tests/test_releaseupgrader.py |
787 | -@@ -391,7 +391,8 @@ |
788 | - reactor.callWhenRunning(do_test) |
789 | - |
790 | - def cleanup(ignored): |
791 | -- os.environ = env_backup |
792 | -+ os.environ.clear() |
793 | -+ os.environ.update(env_backup) |
794 | - return ignored |
795 | - |
796 | - return deferred.addBoth(cleanup) |
797 | -@@ -451,7 +452,8 @@ |
798 | - reactor.callWhenRunning(do_test) |
799 | - |
800 | - def cleanup(ignored): |
801 | -- os.environ = env_backup |
802 | -+ os.environ.clear() |
803 | -+ os.environ.update(env_backup) |
804 | - return ignored |
805 | - |
806 | - return deferred.addBoth(cleanup) |
807 | ---- a/landscape/client/tests/test_watchdog.py |
808 | -+++ b/landscape/client/tests/test_watchdog.py |
809 | -@@ -792,7 +792,7 @@ |
810 | - "GRACEFUL_WAIT_PERIOD", |
811 | - landscape.client.watchdog.GRACEFUL_WAIT_PERIOD, |
812 | - ) |
813 | -- landscape.client.watchdog.GRACEFUL_WAIT_PERIOD = 0.2 |
814 | -+ landscape.client.watchdog.GRACEFUL_WAIT_PERIOD = 1 |
815 | - self.daemon.start() |
816 | - |
817 | - def got_result(result): |
818 | diff --git a/debian/patches/series b/debian/patches/series |
819 | deleted file mode 100644 |
820 | index e28a21f..0000000 |
821 | --- a/debian/patches/series |
822 | +++ /dev/null |
823 | @@ -1,3 +0,0 @@ |
824 | -0001-start-service-during-config.patch |
825 | -0002-fix-broken-build-tests.patch |
826 | -0003-fix-cpuinfo-and-tests.patch |
827 | diff --git a/debian/rules b/debian/rules |
828 | index cc680cd..3be827c 100755 |
829 | --- a/debian/rules |
830 | +++ b/debian/rules |
831 | @@ -26,4 +26,4 @@ override_dh_installsystemd: |
832 | dh_installsystemd |
833 | |
834 | override_dh_auto_test: |
835 | - HOME=$(shell mktemp -d) && make check3 |
836 | + HOME=$(shell mktemp -d) && make check |
837 | diff --git a/debian/watch b/debian/watch |
838 | index ba7ea91..a8377cc 100644 |
839 | --- a/debian/watch |
840 | +++ b/debian/watch |
841 | @@ -1,3 +1,4 @@ |
842 | version=4 |
843 | -opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/landscape-client-$1\.tar\.gz/ \ |
844 | - https://github.com/CanonicalLtd/landscape-client/tags .*/?(\d{2}\.\d{2})\.tar\.gz |
845 | +opts="searchmode=plain" \ |
846 | +https://api.github.com/repos/canonical/landscape-client/releases?per_page=50 \ |
847 | +https://github.com/canonical/landscape-client/releases/download/[^/]+/landscape-client_@ANY_VERSION@.orig\.tar\.gz |
848 | diff --git a/example.conf b/example.conf |
849 | index 16b710e..c7e71b8 100644 |
850 | --- a/example.conf |
851 | +++ b/example.conf |
852 | @@ -96,6 +96,8 @@ ignore_sigusr1 = False |
853 | # UbuntuProInfo - Ubuntu Pro registration information |
854 | # LivePatch - Livepath status information |
855 | # UbuntuProRebootRequired - informs if the system needs to be rebooted |
856 | +# SnapMonitor - manage installed snaps |
857 | +# SnapServicesMonitor - manage snap services |
858 | # |
859 | # The special value "ALL" is an alias for the full list of plugins. |
860 | monitor_plugins = ALL |
861 | @@ -166,9 +168,9 @@ cloud = True |
862 | |
863 | # MANAGER OPTIONS |
864 | |
865 | -# A comma-separated list of monitor plugins to use. |
866 | +# A comma-separated list of manager plugins to use. |
867 | # |
868 | -# Currently available monitor plugins are: |
869 | +# Currently available manager plugins are: |
870 | # |
871 | # ProcessKiller |
872 | # PackageManager |
873 | @@ -176,14 +178,23 @@ cloud = True |
874 | # ShutdownManager |
875 | # AptSources |
876 | # HardwareInfo |
877 | +# KeystoneToken |
878 | +# SnapManager |
879 | +# SnapServicesManager |
880 | # |
881 | -# The special vale "ALL" is an alias for the full list of plugins. |
882 | +# The special value "ALL" is an alias for the entire list of plugins above and is the default. |
883 | manager_plugins = ALL |
884 | |
885 | +# A comma-separated list of manager plugins to use in addition to the default ones. |
886 | +# |
887 | +# The ScriptExecution manager plugin is not enabled by default. |
888 | +# The following example would enable it. |
889 | +#include_manager_plugins = ScriptExecution |
890 | + |
891 | # A comma-separated list of usernames that scripts can run as. |
892 | # |
893 | # By default, all usernames are allowed. |
894 | -script_users = ALL |
895 | +#script_users = ALL |
896 | |
897 | |
898 | # The maximum script output length transmitted to landscape |
899 | @@ -198,3 +209,8 @@ script_users = ALL |
900 | # |
901 | # The default is True |
902 | #manage_sources_list_d = True |
903 | + |
904 | +# Set this for WSL instances managed by Landscape. The value |
905 | +# should match the uid assigned to the host machine. |
906 | +# For all other computers, do not set this parameter. |
907 | +#hostagent_uid = the-uid-of-the-host-machine |
908 | diff --git a/landscape/__init__.py b/landscape/__init__.py |
909 | index 1077067..6e296ac 100644 |
910 | --- a/landscape/__init__.py |
911 | +++ b/landscape/__init__.py |
912 | @@ -1,5 +1,5 @@ |
913 | DEBIAN_REVISION = "" |
914 | -UPSTREAM_VERSION = "23.08" |
915 | +UPSTREAM_VERSION = "24.02" |
916 | VERSION = f"{UPSTREAM_VERSION}{DEBIAN_REVISION}" |
917 | |
918 | # The minimum server API version that all Landscape servers are known to speak |
919 | diff --git a/landscape/client/__init__.py b/landscape/client/__init__.py |
920 | index e69de29..ccb5857 100644 |
921 | --- a/landscape/client/__init__.py |
922 | +++ b/landscape/client/__init__.py |
923 | @@ -0,0 +1,11 @@ |
924 | +import os |
925 | + |
926 | +IS_SNAP = os.getenv("LANDSCAPE_CLIENT_SNAP") |
927 | +IS_CORE = os.getenv("SNAP_SAVE_DATA") is not None |
928 | + |
929 | +USER = "root" if IS_SNAP else "landscape" |
930 | +GROUP = "root" if IS_SNAP else "landscape" |
931 | + |
932 | +DEFAULT_CONFIG = ( |
933 | + "/etc/landscape-client.conf" if IS_SNAP else "/etc/landscape/client.conf" |
934 | +) |
935 | diff --git a/landscape/client/broker/client.py b/landscape/client/broker/client.py |
936 | index edbc2c2..5d3dd4a 100644 |
937 | --- a/landscape/client/broker/client.py |
938 | +++ b/landscape/client/broker/client.py |
939 | @@ -1,5 +1,6 @@ |
940 | import random |
941 | import sys |
942 | +import traceback |
943 | from logging import debug |
944 | from logging import error |
945 | from logging import exception |
946 | @@ -142,11 +143,14 @@ class BrokerClientPlugin: |
947 | |
948 | def _error_log(self, failure): |
949 | """Errback to log and reraise uncaught run errors.""" |
950 | - msg = "{} raised an uncaught exception".format(type(self).__name__) |
951 | + cls = type(self).__name__ |
952 | + msg = f"{cls} raised an uncaught exception" |
953 | if sys.exc_info() == (None, None, None): |
954 | error(msg) |
955 | else: |
956 | exception(msg) |
957 | + debug(traceback.format_exc(limit=15)) |
958 | + |
959 | return failure |
960 | |
961 | |
962 | diff --git a/landscape/client/broker/config.py b/landscape/client/broker/config.py |
963 | index 6b222fb..c050c22 100644 |
964 | --- a/landscape/client/broker/config.py |
965 | +++ b/landscape/client/broker/config.py |
966 | @@ -31,6 +31,7 @@ class BrokerConfiguration(Configuration): |
967 | - C{urgent_exchange_interval} (C{1*60}) |
968 | - C{http_proxy} |
969 | - C{https_proxy} |
970 | + - C{hostagent_uid} |
971 | """ |
972 | parser = super().make_parser() |
973 | |
974 | @@ -44,7 +45,7 @@ class BrokerConfiguration(Configuration): |
975 | "-p", |
976 | "--registration-key", |
977 | metavar="KEY", |
978 | - help="The account-wide key used for " "registering clients.", |
979 | + help="The account-wide key used for registering clients.", |
980 | ) |
981 | parser.add_option( |
982 | "-t", |
983 | @@ -57,14 +58,14 @@ class BrokerConfiguration(Configuration): |
984 | default=15 * 60, |
985 | type="int", |
986 | metavar="INTERVAL", |
987 | - help="The number of seconds between server " "exchanges.", |
988 | + help="The number of seconds between server exchanges.", |
989 | ) |
990 | parser.add_option( |
991 | "--urgent-exchange-interval", |
992 | default=1 * 60, |
993 | type="int", |
994 | metavar="INTERVAL", |
995 | - help="The number of seconds between urgent server " "exchanges.", |
996 | + help="The number of seconds between urgent server exchanges.", |
997 | ) |
998 | parser.add_option( |
999 | "--ping-interval", |
1000 | @@ -93,6 +94,12 @@ class BrokerConfiguration(Configuration): |
1001 | help="Comma separated list of tag names to be sent " |
1002 | "to the server.", |
1003 | ) |
1004 | + parser.add_option( |
1005 | + "--hostagent-uid", |
1006 | + help="Only set this value if this computer is a WSL instance " |
1007 | + "managed by Landscape, in which case set it to be the uid that " |
1008 | + "Landscape assigned to the host machine.", |
1009 | + ) |
1010 | |
1011 | return parser |
1012 | |
1013 | diff --git a/landscape/client/broker/ping.py b/landscape/client/broker/ping.py |
1014 | index 153be3c..e96ab7d 100644 |
1015 | --- a/landscape/client/broker/ping.py |
1016 | +++ b/landscape/client/broker/ping.py |
1017 | @@ -51,11 +51,12 @@ from landscape.lib.log import log_failure |
1018 | class PingClient: |
1019 | """An HTTP client which knows how to talk to the ping server.""" |
1020 | |
1021 | - def __init__(self, reactor, get_page=None): |
1022 | + def __init__(self, reactor, get_page=None, cainfo=None): |
1023 | if get_page is None: |
1024 | get_page = fetch |
1025 | self._reactor = reactor |
1026 | self.get_page = get_page |
1027 | + self._cainfo = cainfo |
1028 | |
1029 | def ping(self, url, insecure_id): |
1030 | """Ask the question: are there messages for this computer ID? |
1031 | @@ -83,6 +84,7 @@ class PingClient: |
1032 | post=True, |
1033 | data=data, |
1034 | headers=headers, |
1035 | + cainfo=self._cainfo, |
1036 | ) |
1037 | page_deferred.addCallback(self._got_result) |
1038 | return page_deferred |
1039 | @@ -94,8 +96,7 @@ class PingClient: |
1040 | the response indicates that their are messages waiting for |
1041 | this computer, False otherwise. |
1042 | """ |
1043 | - if bpickle.loads(webtext) == {"messages": True}: |
1044 | - return True |
1045 | + return bpickle.loads(webtext) == {"messages": True} |
1046 | |
1047 | |
1048 | class Pinger: |
1049 | @@ -138,7 +139,10 @@ class Pinger: |
1050 | |
1051 | def start(self): |
1052 | """Start pinging.""" |
1053 | - self._ping_client = self.ping_client_factory(self._reactor) |
1054 | + self._ping_client = self.ping_client_factory( |
1055 | + self._reactor, |
1056 | + cainfo=self._config.ssl_public_key, |
1057 | + ) |
1058 | self._schedule() |
1059 | |
1060 | def ping(self): |
1061 | diff --git a/landscape/client/broker/registration.py b/landscape/client/broker/registration.py |
1062 | index c21186e..060a16b 100644 |
1063 | --- a/landscape/client/broker/registration.py |
1064 | +++ b/landscape/client/broker/registration.py |
1065 | @@ -8,13 +8,11 @@ the machinery in this module will notice that we have no identification |
1066 | credentials yet and that the server accepts registration messages, so it |
1067 | will craft an appropriate one and send it out. |
1068 | """ |
1069 | -import json |
1070 | import logging |
1071 | |
1072 | from twisted.internet.defer import Deferred |
1073 | |
1074 | from landscape.client.broker.exchange import maybe_bytes |
1075 | -from landscape.client.monitor.ubuntuproinfo import get_ubuntu_pro_info |
1076 | from landscape.lib.juju import get_juju_info |
1077 | from landscape.lib.network import get_fqdn |
1078 | from landscape.lib.tag import is_valid_tag_list |
1079 | @@ -76,6 +74,7 @@ class Identity: |
1080 | registration_key = config_property("registration_key") |
1081 | tags = config_property("tags") |
1082 | access_group = config_property("access_group") |
1083 | + hostagent_uid = config_property("hostagent_uid") |
1084 | |
1085 | def __init__(self, config, persist): |
1086 | self._config = config |
1087 | @@ -190,6 +189,7 @@ class RegistrationHandler: |
1088 | tags = identity.tags |
1089 | group = identity.access_group |
1090 | registration_key = identity.registration_key |
1091 | + hostagent_uid = identity.hostagent_uid |
1092 | |
1093 | self._message_store.delete_all_messages() |
1094 | |
1095 | @@ -217,6 +217,8 @@ class RegistrationHandler: |
1096 | |
1097 | if group: |
1098 | message["access_group"] = group |
1099 | + if hostagent_uid: |
1100 | + message["hostagent_uid"] = hostagent_uid |
1101 | |
1102 | server_api = self._message_store.get_server_api() |
1103 | # If we have juju data to send and if the server is recent enough to |
1104 | @@ -237,8 +239,6 @@ class RegistrationHandler: |
1105 | with_tags = f"and tags {tags} " if tags else "" |
1106 | with_group = f"in access group '{group}' " if group else "" |
1107 | |
1108 | - message["ubuntu_pro_info"] = json.dumps(get_ubuntu_pro_info()) |
1109 | - |
1110 | logging.info( |
1111 | f"Queueing message to register with account {account_name!r} " |
1112 | f"{with_group}{with_tags}{with_word} a password.", |
1113 | diff --git a/landscape/client/broker/store.py b/landscape/client/broker/store.py |
1114 | index 7557d45..ede2514 100644 |
1115 | --- a/landscape/client/broker/store.py |
1116 | +++ b/landscape/client/broker/store.py |
1117 | @@ -137,8 +137,14 @@ class MessageStore: |
1118 | # in case the server supports it. |
1119 | _api = DEFAULT_SERVER_API |
1120 | |
1121 | - def __init__(self, persist, directory, directory_size=1000, max_dirs=4, |
1122 | - max_size_mb=400): |
1123 | + def __init__( |
1124 | + self, |
1125 | + persist, |
1126 | + directory, |
1127 | + directory_size=1000, |
1128 | + max_dirs=4, |
1129 | + max_size_mb=400, |
1130 | + ): |
1131 | self._directory = directory |
1132 | self._directory_size = directory_size |
1133 | self._max_dirs = max_dirs # Maximum number of directories in store |
1134 | @@ -407,7 +413,7 @@ class MessageStore: |
1135 | self.add({"type": "resynchronize"}) |
1136 | self._persist.set("blackhole-messages", True) |
1137 | logging.warning( |
1138 | - "Unable to succesfully communicate with Landscape server " |
1139 | + "Unable to successfully communicate with Landscape server " |
1140 | "for more than a week. Waiting for resync.", |
1141 | ) |
1142 | |
1143 | diff --git a/landscape/client/broker/tests/test_config.py b/landscape/client/broker/tests/test_config.py |
1144 | index b5236ae..db03701 100644 |
1145 | --- a/landscape/client/broker/tests/test_config.py |
1146 | +++ b/landscape/client/broker/tests/test_config.py |
1147 | @@ -12,7 +12,7 @@ class ConfigurationTests(LandscapeTest): |
1148 | def test_loading_sets_http_proxies(self): |
1149 | """ |
1150 | The L{BrokerConfiguration.load} method sets the 'http_proxy' and |
1151 | - 'https_proxy' enviroment variables to the provided values. |
1152 | + 'https_proxy' environment variables to the provided values. |
1153 | """ |
1154 | if "http_proxy" in os.environ: |
1155 | del os.environ["http_proxy"] |
1156 | @@ -36,7 +36,7 @@ class ConfigurationTests(LandscapeTest): |
1157 | def test_loading_without_http_proxies_does_not_touch_environment(self): |
1158 | """ |
1159 | The L{BrokerConfiguration.load} method doesn't override the |
1160 | - 'http_proxy' and 'https_proxy' enviroment variables if they |
1161 | + 'http_proxy' and 'https_proxy' environment variables if they |
1162 | are already set and no new value was specified. |
1163 | """ |
1164 | os.environ["http_proxy"] = "heyo" |
1165 | @@ -71,7 +71,7 @@ class ConfigurationTests(LandscapeTest): |
1166 | self.assertEqual(os.environ["https_proxy"], "originals") |
1167 | |
1168 | def test_default_exchange_intervals(self): |
1169 | - """Exchange intervales are set to sane defaults.""" |
1170 | + """Exchange intervals are set to sane defaults.""" |
1171 | configuration = BrokerConfiguration() |
1172 | self.assertEqual(60, configuration.urgent_exchange_interval) |
1173 | self.assertEqual(900, configuration.exchange_interval) |
1174 | @@ -135,3 +135,27 @@ class ConfigurationTests(LandscapeTest): |
1175 | configuration.url, |
1176 | "https://landscape.canonical.com/message-system", |
1177 | ) |
1178 | + |
1179 | + def test_hostagent_uid_handling(self): |
1180 | + """ |
1181 | + The 'hostagent_uid' value specified in the configuration file is |
1182 | + passed through. |
1183 | + """ |
1184 | + filename = self.makeFile("[client]\nhostagent_uid = AWESOME COMPUTER") |
1185 | + |
1186 | + configuration = BrokerConfiguration() |
1187 | + configuration.load(["--config", filename, "--url", "whatever"]) |
1188 | + |
1189 | + self.assertEqual(configuration.hostagent_uid, "AWESOME COMPUTER") |
1190 | + |
1191 | + def test_missing_hostagent_uid_is_none(self): |
1192 | + """ |
1193 | + Test that if we don't explicitly pass a hostagent_uid, then this value |
1194 | + is None. |
1195 | + """ |
1196 | + filename = self.makeFile("[client]\n") |
1197 | + |
1198 | + configuration = BrokerConfiguration() |
1199 | + configuration.load(["--config", filename, "--url", "whatever"]) |
1200 | + |
1201 | + self.assertIsNone(configuration.hostagent_uid) |
1202 | diff --git a/landscape/client/broker/tests/test_exchange.py b/landscape/client/broker/tests/test_exchange.py |
1203 | index bc7fb5f..dd396c3 100644 |
1204 | --- a/landscape/client/broker/tests/test_exchange.py |
1205 | +++ b/landscape/client/broker/tests/test_exchange.py |
1206 | @@ -565,7 +565,7 @@ class MessageExchangeTest(LandscapeTest): |
1207 | """ |
1208 | If the server asks for messages that we no longer have, the message |
1209 | exchange plugin should send a message to the server indicating that a |
1210 | - resynchronization is occuring and then fire a "resynchronize-clients" |
1211 | + resynchronization is occurring and then fire a "resynchronize-clients" |
1212 | reactor message, so that plugins can generate new data -- if the server |
1213 | got out of synch with the client, then we're best off synchronizing |
1214 | everything back to it. |
1215 | @@ -859,7 +859,7 @@ class MessageExchangeTest(LandscapeTest): |
1216 | self.reactor.advance(1) |
1217 | self.assertEqual(events, [True]) |
1218 | |
1219 | - def test_impending_exchange_gets_reschudeled_with_urgent_reschedule(self): |
1220 | + def test_impending_exchange_gets_rescheduled_with_urgent_reschedule(self): |
1221 | """ |
1222 | When an urgent exchange is scheduled after a regular exchange was |
1223 | scheduled but before it executed, the old C{impending-exchange} event |
1224 | @@ -955,7 +955,7 @@ class MessageExchangeTest(LandscapeTest): |
1225 | self.wait_for_exchange() |
1226 | self.assertFalse(self.transport.payloads) |
1227 | |
1228 | - def test_stop_twice_doesnt_break(self): |
1229 | + def test_stop_twice_does_not_break(self): |
1230 | self.exchanger.schedule_exchange() |
1231 | self.exchanger.stop() |
1232 | self.exchanger.stop() |
1233 | @@ -1014,7 +1014,7 @@ class MessageExchangeTest(LandscapeTest): |
1234 | |
1235 | def test_register_message(self): |
1236 | """ |
1237 | - The exchanger expsoses a mechanism for subscribing to messages |
1238 | + The exchanger exposes a mechanism for subscribing to messages |
1239 | of a particular type. |
1240 | """ |
1241 | messages = [] |
1242 | diff --git a/landscape/client/broker/tests/test_ping.py b/landscape/client/broker/tests/test_ping.py |
1243 | index 843b984..fe08dee 100644 |
1244 | --- a/landscape/client/broker/tests/test_ping.py |
1245 | +++ b/landscape/client/broker/tests/test_ping.py |
1246 | @@ -16,7 +16,7 @@ class FakePageGetter: |
1247 | self.response = response |
1248 | self.fetches = [] |
1249 | |
1250 | - def get_page(self, url, post, headers, data): |
1251 | + def get_page(self, url, post, headers, data, cainfo=None): |
1252 | """ |
1253 | A method which is supposed to act like a limited version of |
1254 | L{landscape.lib.fetch.fetch}. |
1255 | @@ -27,7 +27,7 @@ class FakePageGetter: |
1256 | self.fetches.append((url, post, headers, data)) |
1257 | return bpickle.dumps(self.response) |
1258 | |
1259 | - def failing_get_page(self, url, post, headers, data): |
1260 | + def failing_get_page(self, url, post, headers, data, cainfo=None): |
1261 | """ |
1262 | A method which is supposed to act like a limited version of |
1263 | L{landscape.lib.fetch.fetch}. |
1264 | @@ -126,8 +126,12 @@ class PingerTest(LandscapeTest): |
1265 | super().setUp() |
1266 | self.page_getter = FakePageGetter(None) |
1267 | |
1268 | - def factory(reactor): |
1269 | - return PingClient(reactor, get_page=self.page_getter.get_page) |
1270 | + def factory(reactor, **kwargs): |
1271 | + return PingClient( |
1272 | + reactor, |
1273 | + get_page=self.page_getter.get_page, |
1274 | + **kwargs, |
1275 | + ) |
1276 | |
1277 | self.config.ping_url = "http://localhost:8081/whatever" |
1278 | self.config.ping_interval = 10 |
1279 | diff --git a/landscape/client/broker/tests/test_registration.py b/landscape/client/broker/tests/test_registration.py |
1280 | index fe1d256..1e236fe 100644 |
1281 | --- a/landscape/client/broker/tests/test_registration.py |
1282 | +++ b/landscape/client/broker/tests/test_registration.py |
1283 | @@ -78,6 +78,9 @@ class IdentityTest(LandscapeTest): |
1284 | def test_access_group(self): |
1285 | self.check_config_property("access_group") |
1286 | |
1287 | + def test_hostagent_uid(self): |
1288 | + self.check_config_property("hostagent_uid") |
1289 | + |
1290 | |
1291 | class RegistrationHandlerTestBase(LandscapeTest): |
1292 | |
1293 | @@ -383,6 +386,47 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase): |
1294 | # Make sure the key does not appear in the outgoing message. |
1295 | self.assertNotIn("access_group", messages[0]) |
1296 | |
1297 | + def test_queue_message_on_exchange_with_hostagent_uid(self): |
1298 | + """ |
1299 | + If the admin has defined a hostagent_uid for this computer, we send |
1300 | + it to the server. |
1301 | + """ |
1302 | + self.mstore.set_accepted_types(["register"]) |
1303 | + # hostagent_uid is introduced in the 3.3 message schema |
1304 | + self.mstore.set_server_api(b"3.3") |
1305 | + self.config.account_name = "account_name" |
1306 | + self.config.hostagent_uid = "dinosaur computer" |
1307 | + self.config.tags = "server,london" |
1308 | + self.reactor.fire("pre-exchange") |
1309 | + messages = self.mstore.get_pending_messages() |
1310 | + self.assertEqual("dinosaur computer", messages[0]["hostagent_uid"]) |
1311 | + |
1312 | + def test_queue_message_on_exchange_with_empty_hostagent_uid(self): |
1313 | + """ |
1314 | + If the hostagent_uid is "", then the outgoing message does not define |
1315 | + a "hostagent_uid" key. |
1316 | + """ |
1317 | + self.mstore.set_accepted_types(["register"]) |
1318 | + # hostagent_uid is introduced in the 3.3 message schema |
1319 | + self.mstore.set_server_api(b"3.3") |
1320 | + self.config.hostagent_uid = "" |
1321 | + self.reactor.fire("pre-exchange") |
1322 | + messages = self.mstore.get_pending_messages() |
1323 | + self.assertNotIn("hostagent_uid", messages[0]) |
1324 | + |
1325 | + def test_queue_message_on_exchange_with_none_hostagent_uid(self): |
1326 | + """ |
1327 | + If the hostagent_uid is None, then the outgoing message does not define |
1328 | + a "hostagent_uid" key. |
1329 | + """ |
1330 | + self.mstore.set_accepted_types(["register"]) |
1331 | + # hostagent_uid is introduced in the 3.3 message schema |
1332 | + self.mstore.set_server_api(b"3.3") |
1333 | + self.config.hostagent_uid = None |
1334 | + self.reactor.fire("pre-exchange") |
1335 | + messages = self.mstore.get_pending_messages() |
1336 | + self.assertNotIn("hostagent_uid", messages[0]) |
1337 | + |
1338 | def test_queueing_registration_message_resets_message_store(self): |
1339 | """ |
1340 | When a registration message is queued, the store is reset |
1341 | @@ -457,7 +501,7 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase): |
1342 | {"type": b"registration", "info": b"blah-blah"}, |
1343 | ) |
1344 | for name, args, kwargs in reactor_fire_mock.mock_calls: |
1345 | - self.assertNotEquals("registration-failed", args[0]) |
1346 | + self.assertNotEqual("registration-failed", args[0]) |
1347 | |
1348 | def test_register_resets_ids(self): |
1349 | self.identity.secure_id = "foo" |
1350 | diff --git a/landscape/client/broker/tests/test_store.py b/landscape/client/broker/tests/test_store.py |
1351 | index b61edf3..5dd2fbb 100644 |
1352 | --- a/landscape/client/broker/tests/test_store.py |
1353 | +++ b/landscape/client/broker/tests/test_store.py |
1354 | @@ -700,11 +700,11 @@ class MessageStoreTest(LandscapeTest): |
1355 | """Messages stop accumulating after one week of not being sent.""" |
1356 | self.store.record_failure(0) |
1357 | self.store.record_failure(7 * 24 * 60 * 60) |
1358 | - self.assertIsNot(None, self.store.add({"type": "empty"})) |
1359 | + self.assertIsNotNone(self.store.add({"type": "empty"})) |
1360 | self.store.record_failure((7 * 24 * 60 * 60) + 1) |
1361 | - self.assertIs(None, self.store.add({"type": "empty"})) |
1362 | + self.assertIsNone(self.store.add({"type": "empty"})) |
1363 | self.assertIn( |
1364 | - "WARNING: Unable to succesfully communicate with " |
1365 | + "WARNING: Unable to successfully communicate with " |
1366 | "Landscape server for more than a week. Waiting for " |
1367 | "resync.", |
1368 | self.logfile.getvalue(), |
1369 | diff --git a/landscape/client/configuration.py b/landscape/client/configuration.py |
1370 | index 621e797..759b45f 100644 |
1371 | --- a/landscape/client/configuration.py |
1372 | +++ b/landscape/client/configuration.py |
1373 | @@ -13,6 +13,8 @@ import textwrap |
1374 | from functools import partial |
1375 | from urllib.parse import urlparse |
1376 | |
1377 | +from landscape.client import GROUP |
1378 | +from landscape.client import USER |
1379 | from landscape.client.broker.amp import RemoteBrokerConnector |
1380 | from landscape.client.broker.config import BrokerConfiguration |
1381 | from landscape.client.broker.registration import Identity |
1382 | @@ -197,14 +199,15 @@ class LandscapeSetupConfiguration(BrokerConfiguration): |
1383 | "--script-users", |
1384 | metavar="USERS", |
1385 | help="A comma-separated list of users to allow " |
1386 | - "scripts to run. To allow scripts to be run " |
1387 | + "scripts to run. To allow scripts to be run " |
1388 | "by any user, enter: ALL", |
1389 | ) |
1390 | parser.add_option( |
1391 | "--include-manager-plugins", |
1392 | metavar="PLUGINS", |
1393 | default="", |
1394 | - help="A comma-separated list of manager plugins to " "load.", |
1395 | + help="A comma-separated list of manager plugins " |
1396 | + "to enable in addition to the defaults.", |
1397 | ) |
1398 | parser.add_option( |
1399 | "-n", |
1400 | @@ -228,13 +231,13 @@ class LandscapeSetupConfiguration(BrokerConfiguration): |
1401 | "--disable", |
1402 | action="store_true", |
1403 | default=False, |
1404 | - help="Stop running clients and disable start at " "boot.", |
1405 | + help="Stop running clients and disable start at boot.", |
1406 | ) |
1407 | parser.add_option( |
1408 | "--init", |
1409 | action="store_true", |
1410 | default=False, |
1411 | - help="Set up the client directories structure " "and exit.", |
1412 | + help="Set up the client directories structure and exit.", |
1413 | ) |
1414 | parser.add_option( |
1415 | "--is-registered", |
1416 | @@ -565,7 +568,7 @@ def check_account_name_and_password(config): |
1417 | if config.silent and not config.no_start: |
1418 | if not (config.get("account_name") and config.get("computer_title")): |
1419 | raise ConfigurationError( |
1420 | - "An account name and computer title are " "required.", |
1421 | + "An account name and computer title are required.", |
1422 | ) |
1423 | |
1424 | |
1425 | @@ -624,8 +627,10 @@ def setup(config): |
1426 | decode_base64_ssl_public_certificate(config) |
1427 | config.write() |
1428 | # Restart the client to ensure that it's using the new configuration. |
1429 | + |
1430 | if not config.no_start: |
1431 | try: |
1432 | + set_secure_id(config, "registering") |
1433 | ServiceConfig.restart_landscape() |
1434 | except ServiceConfigException as exc: |
1435 | print_text(str(exc), error=True) |
1436 | @@ -643,13 +648,8 @@ def setup(config): |
1437 | def bootstrap_tree(config): |
1438 | """Create the client directories tree.""" |
1439 | bootstrap_list = [ |
1440 | - BootstrapDirectory("$data_path", "landscape", "root", 0o755), |
1441 | - BootstrapDirectory( |
1442 | - "$annotations_path", |
1443 | - "landscape", |
1444 | - "landscape", |
1445 | - 0o755, |
1446 | - ), |
1447 | + BootstrapDirectory("$data_path", USER, "root", 0o755), |
1448 | + BootstrapDirectory("$annotations_path", USER, GROUP, 0o755), |
1449 | ] |
1450 | BootstrapList(bootstrap_list).bootstrap( |
1451 | data_path=config.data_path, |
1452 | @@ -796,13 +796,14 @@ def register( |
1453 | # Results will be things like "success" or "ssl-error". |
1454 | result = results[0] |
1455 | |
1456 | - if isinstance(result, SystemExit): |
1457 | - raise result |
1458 | - |
1459 | # If there was an error and the caller requested that errors be reported |
1460 | # to the on_error callable, then do so. |
1461 | if result != "success" and on_error is not None: |
1462 | on_error(1) |
1463 | + |
1464 | + if isinstance(result, SystemExit): |
1465 | + raise result |
1466 | + |
1467 | return result |
1468 | |
1469 | |
1470 | @@ -883,6 +884,21 @@ def registration_info_text(config, registration_status): |
1471 | return text |
1472 | |
1473 | |
1474 | +def set_secure_id(config, new_id): |
1475 | + """Persists a secure id in the identity data file. This is used to indicate |
1476 | + whether we are currently in the process of registering. |
1477 | + """ |
1478 | + persist = Persist( |
1479 | + filename=os.path.join( |
1480 | + config.data_path, |
1481 | + f"{BrokerService.service_name}.bpickle", |
1482 | + ), |
1483 | + ) |
1484 | + identity = Identity(config, persist) |
1485 | + identity.secure_id = new_id |
1486 | + persist.save() |
1487 | + |
1488 | + |
1489 | def main(args, print=print): |
1490 | """Interact with the user and the server to set up client configuration.""" |
1491 | |
1492 | @@ -927,7 +943,11 @@ def main(args, print=print): |
1493 | # Attempt to register the client. |
1494 | reactor = LandscapeReactor() |
1495 | if config.silent: |
1496 | - result = register(config, reactor) |
1497 | + result = register( |
1498 | + config, |
1499 | + reactor, |
1500 | + on_error=lambda _: set_secure_id(config, None), |
1501 | + ) |
1502 | report_registration_outcome(result, print=print) |
1503 | sys.exit(determine_exit_code(result)) |
1504 | else: |
1505 | @@ -937,6 +957,10 @@ def main(args, print=print): |
1506 | default=default_answer, |
1507 | ) |
1508 | if answer: |
1509 | - result = register(config, reactor) |
1510 | + result = register( |
1511 | + config, |
1512 | + reactor, |
1513 | + on_error=lambda _: set_secure_id(config, None), |
1514 | + ) |
1515 | report_registration_outcome(result, print=print) |
1516 | sys.exit(determine_exit_code(result)) |
1517 | diff --git a/landscape/client/deployment.py b/landscape/client/deployment.py |
1518 | index db1f110..28bc830 100644 |
1519 | --- a/landscape/client/deployment.py |
1520 | +++ b/landscape/client/deployment.py |
1521 | @@ -1,13 +1,23 @@ |
1522 | +import json |
1523 | import os.path |
1524 | +import subprocess |
1525 | import sys |
1526 | +from datetime import datetime |
1527 | +from datetime import timezone |
1528 | from optparse import SUPPRESS_HELP |
1529 | |
1530 | from twisted.logger import globalLogBeginner |
1531 | |
1532 | from landscape import VERSION |
1533 | +from landscape.client import DEFAULT_CONFIG |
1534 | +from landscape.client import snap_http |
1535 | +from landscape.client.snap_utils import get_snap_info |
1536 | from landscape.client.upgraders import UPGRADE_MANAGERS |
1537 | from landscape.lib import logging |
1538 | from landscape.lib.config import BaseConfiguration as _BaseConfiguration |
1539 | +from landscape.lib.format import expandvars |
1540 | +from landscape.lib.network import get_active_device_info |
1541 | +from landscape.lib.network import get_fqdn |
1542 | from landscape.lib.persist import Persist |
1543 | |
1544 | |
1545 | @@ -37,7 +47,7 @@ class BaseConfiguration(_BaseConfiguration): |
1546 | |
1547 | version = VERSION |
1548 | |
1549 | - default_config_filename = "/etc/landscape/client.conf" |
1550 | + default_config_filename = DEFAULT_CONFIG |
1551 | if _is_script(): |
1552 | default_config_filenames = ( |
1553 | "landscape-client.conf", |
1554 | @@ -188,6 +198,25 @@ class Configuration(BaseConfiguration): |
1555 | backwards-compatibility.""" |
1556 | return os.path.join(self.data_path, "juju-info.json") |
1557 | |
1558 | + def auto_configure(self): |
1559 | + """Automatically configure the client snap.""" |
1560 | + client_conf = snap_http.get_conf("landscape-client").result |
1561 | + auto_enroll_conf = client_conf.get("auto-register", {}) |
1562 | + |
1563 | + enabled = auto_enroll_conf.get("enabled", False) |
1564 | + configured = auto_enroll_conf.get("configured", False) |
1565 | + if not enabled or configured: |
1566 | + return |
1567 | + |
1568 | + title = generate_computer_title(auto_enroll_conf) |
1569 | + if title: |
1570 | + self.computer_title = title |
1571 | + self.write() |
1572 | + |
1573 | + auto_enroll_conf["configured"] = True |
1574 | + client_conf["auto-register"] = auto_enroll_conf |
1575 | + snap_http.set_conf("landscape-client", client_conf) |
1576 | + |
1577 | |
1578 | def get_versioned_persist(service): |
1579 | """Get a L{Persist} database with upgrade rules applied. |
1580 | @@ -203,3 +232,52 @@ def get_versioned_persist(service): |
1581 | upgrade_manager.initialize(persist) |
1582 | persist.save(service.persist_filename) |
1583 | return persist |
1584 | + |
1585 | + |
1586 | +def generate_computer_title(auto_enroll_config): |
1587 | + """Generate the computer title. |
1588 | + |
1589 | + This follows the LA017 specification and falls back to `hostname` |
1590 | + if generating the title fails due to missing data. |
1591 | + """ |
1592 | + snap_info = get_snap_info() |
1593 | + wait_for_serial = auto_enroll_config.get("wait-for-serial-as", True) |
1594 | + if "serial" not in snap_info and wait_for_serial: |
1595 | + return |
1596 | + |
1597 | + hostname = get_fqdn() |
1598 | + wait_for_hostname = auto_enroll_config.get("wait-for-hostname", False) |
1599 | + if hostname == "localhost" and wait_for_hostname: |
1600 | + return |
1601 | + |
1602 | + nics = get_active_device_info(default_only=True) |
1603 | + nic = nics[0] if nics else {} |
1604 | + |
1605 | + lshw = subprocess.run( |
1606 | + ["lshw", "-json", "-quiet", "-c", "system"], |
1607 | + capture_output=True, |
1608 | + text=True, |
1609 | + ) |
1610 | + hardware = json.loads(lshw.stdout)[0] |
1611 | + |
1612 | + computer_title_pattern = auto_enroll_config.get( |
1613 | + "computer-title-pattern", |
1614 | + "${hostname}", |
1615 | + ) |
1616 | + title = expandvars( |
1617 | + computer_title_pattern, |
1618 | + serial=snap_info.get("serial", ""), |
1619 | + model=snap_info.get("model", ""), |
1620 | + brand=snap_info.get("brand", ""), |
1621 | + hostname=hostname, |
1622 | + ip=nic.get("ip_address", ""), |
1623 | + mac=nic.get("mac_address", ""), |
1624 | + prodiden=hardware.get("product", ""), |
1625 | + serialno=hardware.get("serial", ""), |
1626 | + datetime=datetime.now(timezone.utc), |
1627 | + ) |
1628 | + |
1629 | + if title == "": # on the off-chance substitute values are missing |
1630 | + title = hostname |
1631 | + |
1632 | + return title |
1633 | diff --git a/landscape/client/manager/aptsources.py b/landscape/client/manager/aptsources.py |
1634 | index baa641f..179fa72 100644 |
1635 | --- a/landscape/client/manager/aptsources.py |
1636 | +++ b/landscape/client/manager/aptsources.py |
1637 | @@ -8,6 +8,8 @@ import uuid |
1638 | |
1639 | from twisted.internet.defer import succeed |
1640 | |
1641 | +from landscape.client import GROUP |
1642 | +from landscape.client import USER |
1643 | from landscape.client.manager.plugin import ManagerPlugin |
1644 | from landscape.client.package.reporter import find_reporter_command |
1645 | from landscape.constants import FALSE_VALUES |
1646 | @@ -167,8 +169,8 @@ class AptSources(ManagerPlugin): |
1647 | args.append(f"--config={self.registry.config.config}") |
1648 | |
1649 | if os.getuid() == 0: |
1650 | - uid = pwd.getpwnam("landscape").pw_uid |
1651 | - gid = grp.getgrnam("landscape").gr_gid |
1652 | + uid = pwd.getpwnam(USER).pw_uid |
1653 | + gid = grp.getgrnam(GROUP).gr_gid |
1654 | else: |
1655 | uid = None |
1656 | gid = None |
1657 | diff --git a/landscape/client/manager/config.py b/landscape/client/manager/config.py |
1658 | index 2f8df8b..f89ebbc 100644 |
1659 | --- a/landscape/client/manager/config.py |
1660 | +++ b/landscape/client/manager/config.py |
1661 | @@ -13,6 +13,8 @@ ALL_PLUGINS = [ |
1662 | "HardwareInfo", |
1663 | "KeystoneToken", |
1664 | "SnapManager", |
1665 | + "SnapServicesManager", |
1666 | + "UbuntuProInfo", |
1667 | ] |
1668 | |
1669 | |
1670 | diff --git a/landscape/client/manager/hardwareinfo.py b/landscape/client/manager/hardwareinfo.py |
1671 | index b475ee5..35b504d 100644 |
1672 | --- a/landscape/client/manager/hardwareinfo.py |
1673 | +++ b/landscape/client/manager/hardwareinfo.py |
1674 | @@ -12,7 +12,7 @@ class HardwareInfo(ManagerPlugin): |
1675 | message_type = "hardware-info" |
1676 | run_interval = 60 * 60 * 24 |
1677 | run_immediately = True |
1678 | - command = "/usr/bin/lshw" |
1679 | + command = "lshw" |
1680 | |
1681 | def register(self, registry): |
1682 | super().register(registry) |
1683 | diff --git a/landscape/client/manager/scriptexecution.py b/landscape/client/manager/scriptexecution.py |
1684 | index ac591d7..6856185 100644 |
1685 | --- a/landscape/client/manager/scriptexecution.py |
1686 | +++ b/landscape/client/manager/scriptexecution.py |
1687 | @@ -18,6 +18,7 @@ from twisted.internet.protocol import ProcessProtocol |
1688 | from twisted.python.compat import unicode |
1689 | |
1690 | from landscape import VERSION |
1691 | +from landscape.client import IS_SNAP |
1692 | from landscape.client.manager.plugin import FAILED |
1693 | from landscape.client.manager.plugin import ManagerPlugin |
1694 | from landscape.client.manager.plugin import SUCCEEDED |
1695 | @@ -85,6 +86,7 @@ class ScriptRunnerMixin: |
1696 | if process_factory is None: |
1697 | from twisted.internet import reactor as process_factory |
1698 | self.process_factory = process_factory |
1699 | + self.IS_SNAP = IS_SNAP |
1700 | |
1701 | def is_user_allowed(self, user): |
1702 | allowed_users = self.registry.config.get_allowed_script_users() |
1703 | @@ -96,8 +98,9 @@ class ScriptRunnerMixin: |
1704 | # It would be nice to use fchown(2) and fchmod(2), but they're not |
1705 | # available in python and using it with ctypes is pretty tedious, not |
1706 | # to mention we can't get errno. |
1707 | + # Don't attempt to change file owner if the client is a snap |
1708 | os.chmod(filename, 0o700) |
1709 | - if uid is not None: |
1710 | + if not self.IS_SNAP and uid is not None: |
1711 | os.chown(filename, uid, gid) |
1712 | |
1713 | script = build_script(shell, code) |
1714 | @@ -172,7 +175,8 @@ class ScriptExecutionPlugin(ManagerPlugin, ScriptRunnerMixin): |
1715 | def _handle_execute_script(self, message): |
1716 | opid = message["operation-id"] |
1717 | try: |
1718 | - user = message["username"] |
1719 | + user = message["username"] if not self.IS_SNAP else "root" |
1720 | + |
1721 | if not self.is_user_allowed(user): |
1722 | return self._respond( |
1723 | FAILED, |
1724 | @@ -245,11 +249,11 @@ class ScriptExecutionPlugin(ManagerPlugin, ScriptRunnerMixin): |
1725 | full_filename = os.path.join(attachment_dir, filename) |
1726 | with open(full_filename, "wb") as attachment: |
1727 | os.chmod(full_filename, 0o600) |
1728 | - if uid is not None: |
1729 | + if not self.IS_SNAP and uid is not None: |
1730 | os.chown(full_filename, uid, gid) |
1731 | attachment.write(data) |
1732 | os.chmod(attachment_dir, 0o700) |
1733 | - if uid is not None: |
1734 | + if not self.IS_SNAP and uid is not None: |
1735 | os.chown(attachment_dir, uid, gid) |
1736 | returnValue(attachment_dir) |
1737 | |
1738 | @@ -296,9 +300,15 @@ class ScriptExecutionPlugin(ManagerPlugin, ScriptRunnerMixin): |
1739 | "USER": user or "", |
1740 | "HOME": path or "", |
1741 | } |
1742 | - for locale_var in ("LANG", "LC_ALL", "LC_CTYPE"): |
1743 | - if locale_var in os.environ: |
1744 | - env[locale_var] = os.environ[locale_var] |
1745 | + for env_var in ( |
1746 | + "LANG", |
1747 | + "LC_ALL", |
1748 | + "LC_CTYPE", |
1749 | + "LD_LIBRARY_PATH", |
1750 | + "PYTHONPATH", |
1751 | + ): |
1752 | + if env_var in os.environ: |
1753 | + env[env_var] = os.environ[env_var] |
1754 | if server_supplied_env: |
1755 | env.update(server_supplied_env) |
1756 | old_umask = os.umask(0o022) |
1757 | diff --git a/landscape/client/manager/service.py b/landscape/client/manager/service.py |
1758 | index 7690442..c289685 100644 |
1759 | --- a/landscape/client/manager/service.py |
1760 | +++ b/landscape/client/manager/service.py |
1761 | @@ -1,3 +1,5 @@ |
1762 | +import logging |
1763 | + |
1764 | from twisted.python.reflect import namedClass |
1765 | |
1766 | from landscape.client.amp import ComponentPublisher |
1767 | @@ -28,13 +30,26 @@ class ManagerService(LandscapeService): |
1768 | |
1769 | def get_plugins(self): |
1770 | """Return instances of all the plugins enabled in the configuration.""" |
1771 | - return [ |
1772 | - namedClass( |
1773 | - "landscape.client.manager." |
1774 | - f"{plugin_name.lower()}.{plugin_name}", |
1775 | - )() |
1776 | - for plugin_name in self.config.plugin_factories |
1777 | - ] |
1778 | + plugins = [] |
1779 | + |
1780 | + for plugin_name in self.config.plugin_factories: |
1781 | + try: |
1782 | + plugin = namedClass( |
1783 | + "landscape.client.manager." |
1784 | + f"{plugin_name.lower()}.{plugin_name}", |
1785 | + ) |
1786 | + plugins.append(plugin()) |
1787 | + except ModuleNotFoundError: |
1788 | + logging.warning( |
1789 | + f"Invalid manager plugin specified: '{plugin_name}'" |
1790 | + "See `example.conf` for a full list of monitor plugins.", |
1791 | + ) |
1792 | + except Exception as exc: |
1793 | + logging.warning( |
1794 | + f"Unable to load manager plugin '{plugin_name}': {exc}", |
1795 | + ) |
1796 | + |
1797 | + return plugins |
1798 | |
1799 | def startService(self): # noqa: N802 |
1800 | """Start the manager service. |
1801 | diff --git a/landscape/client/manager/shutdownmanager.py b/landscape/client/manager/shutdownmanager.py |
1802 | index eba2be7..bd38533 100644 |
1803 | --- a/landscape/client/manager/shutdownmanager.py |
1804 | +++ b/landscape/client/manager/shutdownmanager.py |
1805 | @@ -1,94 +1,106 @@ |
1806 | import logging |
1807 | |
1808 | -from twisted.internet.defer import Deferred |
1809 | -from twisted.internet.error import ProcessDone |
1810 | -from twisted.internet.protocol import ProcessProtocol |
1811 | +import dbus |
1812 | +from twisted.internet import reactor |
1813 | +from twisted.internet import task |
1814 | |
1815 | from landscape.client.manager.plugin import FAILED |
1816 | from landscape.client.manager.plugin import ManagerPlugin |
1817 | from landscape.client.manager.plugin import SUCCEEDED |
1818 | |
1819 | |
1820 | -class ShutdownFailedError(Exception): |
1821 | - """Raised when a call to C{/sbin/shutdown} fails. |
1822 | - |
1823 | - @ivar data: The data that the process printed before failing. |
1824 | +class ShutdownManager(ManagerPlugin): |
1825 | """ |
1826 | + Plugin that either shuts down or reboots the device. |
1827 | |
1828 | - def __init__(self, data): |
1829 | - self.data = data |
1830 | + In both cases, the manager sends the success command |
1831 | + before attempting the shutdown/reboot. |
1832 | + With reboot - the call is instanteous but the success |
1833 | + message will be send as soon as the device comes back up. |
1834 | |
1835 | + For shutdown there is a 120 second delay between |
1836 | + sending the success and firing the shutdown. |
1837 | + This is usually sufficent. |
1838 | + """ |
1839 | |
1840 | -class ShutdownManager(ManagerPlugin): |
1841 | - def __init__(self, process_factory=None): |
1842 | - if process_factory is None: |
1843 | - from twisted.internet import reactor as process_factory |
1844 | - self._process_factory = process_factory |
1845 | + def __init__(self, dbus_provider=None, shutdown_delay=120): |
1846 | + if dbus_provider is None: |
1847 | + self.dbus_sysbus = dbus.SystemBus() |
1848 | + else: |
1849 | + self.dbus_sysbus = dbus_provider.SystemBus() |
1850 | |
1851 | - def register(self, registry): |
1852 | - """Add this plugin to C{registry}. |
1853 | + self.shutdown_delay = shutdown_delay |
1854 | |
1855 | - The shutdown manager handles C{shutdown} activity messages broadcast |
1856 | - from the server. |
1857 | - """ |
1858 | + def register(self, registry): |
1859 | super().register(registry) |
1860 | - registry.register_message("shutdown", self.perform_shutdown) |
1861 | + self.config = registry.config |
1862 | |
1863 | - def perform_shutdown(self, message): |
1864 | - """Request a system restart or shutdown. |
1865 | + registry.register_message("shutdown", self._handle_shutdown) |
1866 | |
1867 | - If the call to C{/sbin/shutdown} runs without errors the activity |
1868 | - specified in the message will be responded as succeeded. Otherwise, |
1869 | - it will be responded as failed. |
1870 | + def _handle_shutdown(self, message, DBus_System_Bus=None): |
1871 | + """ |
1872 | + Choose shutdown or reboot |
1873 | """ |
1874 | operation_id = message["operation-id"] |
1875 | reboot = message["reboot"] |
1876 | - protocol = ShutdownProcessProtocol() |
1877 | - protocol.set_timeout(self.registry.reactor) |
1878 | - protocol.result.addCallback(self._respond_success, operation_id) |
1879 | - protocol.result.addErrback(self._respond_failure, operation_id, reboot) |
1880 | - command, args = self._get_command_and_args(protocol, reboot) |
1881 | - self._process_factory.spawnProcess(protocol, command, args=args) |
1882 | - |
1883 | - def _respond_success(self, data, operation_id): |
1884 | - logging.info("Shutdown request succeeded.") |
1885 | + |
1886 | + if reboot: |
1887 | + logging.info("Reboot Requested") |
1888 | + deferred = self._respond_reboot_success( |
1889 | + "Reboot requested of the system", |
1890 | + operation_id, |
1891 | + ) |
1892 | + return deferred |
1893 | + else: |
1894 | + logging.info("Shutdown Requested") |
1895 | + deferred = self._respond_shutdown_success( |
1896 | + "Shutdown requested of the system", |
1897 | + operation_id, |
1898 | + ) |
1899 | + return deferred |
1900 | + |
1901 | + def _Reboot(self, _, Dbus_System_bus=None): |
1902 | + logging.info("Sending Reboot Command") |
1903 | + |
1904 | + bus_object = self.dbus_sysbus.get_object( |
1905 | + "org.freedesktop.login1", |
1906 | + "/org/freedesktop/login1", |
1907 | + ) |
1908 | + bus_object.Reboot( |
1909 | + True, |
1910 | + dbus_interface="org.freedesktop.login1.Manager", |
1911 | + ) |
1912 | + |
1913 | + def _Shutdown(self): |
1914 | + logging.info("Sending Shutdown Command") |
1915 | + bus_object = self.dbus_sysbus.get_object( |
1916 | + "org.freedesktop.login1", |
1917 | + "/org/freedesktop/login1", |
1918 | + ) |
1919 | + bus_object.PowerOff( |
1920 | + True, |
1921 | + dbus_interface="org.freedesktop.login1.Manager", |
1922 | + ) |
1923 | + |
1924 | + def _respond_reboot_success(self, data, operation_id): |
1925 | deferred = self._respond(SUCCEEDED, data, operation_id) |
1926 | - # After sending the result to the server, stop accepting messages and |
1927 | - # wait for the reboot/shutdown. |
1928 | - deferred.addCallback(lambda _: self.registry.broker.stop_exchanger()) |
1929 | + deferred.addCallback(self._Reboot) |
1930 | + deferred.addErrback(self._respond_fail) |
1931 | return deferred |
1932 | |
1933 | - def _respond_failure(self, failure, operation_id, reboot): |
1934 | - logging.info("Shutdown request failed.") |
1935 | - failure_report = "\n".join( |
1936 | - [ |
1937 | - failure.value.data, |
1938 | - "", |
1939 | - "Attempting to force {operation}. Please note that if this " |
1940 | - "succeeds, Landscape will have no way of knowing and will " |
1941 | - "still mark this activity as having failed. It is recommended " |
1942 | - "you check the state of the machine manually to determine " |
1943 | - "whether {operation} succeeded.".format( |
1944 | - operation="reboot" if reboot else "shutdown", |
1945 | - ), |
1946 | - ], |
1947 | - ) |
1948 | - deferred = self._respond(FAILED, failure_report, operation_id) |
1949 | - # Add another callback spawning the poweroff or reboot command (which |
1950 | - # seem more reliable in aberrant situations like a post-trusty release |
1951 | - # upgrade where upstart has been replaced with systemd). If this |
1952 | - # succeeds, we won't have any opportunity to report it and if it fails |
1953 | - # we'll already have responded indicating we're attempting to force |
1954 | - # the operation so either way there's no sense capturing output |
1955 | - protocol = ProcessProtocol() |
1956 | - command, args = self._get_command_and_args(protocol, reboot, True) |
1957 | - deferred.addCallback( |
1958 | - lambda _: self._process_factory.spawnProcess( |
1959 | - protocol, |
1960 | - command, |
1961 | - args=args, |
1962 | - ), |
1963 | + def _respond_shutdown_success(self, data, operation_id): |
1964 | + deferred = self._respond(SUCCEEDED, data, operation_id) |
1965 | + self.shutdown_deferred = task.deferLater( |
1966 | + reactor, |
1967 | + self.shutdown_delay, |
1968 | + self._Shutdown, |
1969 | ) |
1970 | + deferred.addErrback(self._respond_fail) |
1971 | + return deferred |
1972 | + |
1973 | + def _respond_fail(self, data, operation_id): |
1974 | + logging.info("Shutdown/Reboot request failed.") |
1975 | + deferred = self._respond(FAILED, data, operation_id) |
1976 | return deferred |
1977 | |
1978 | def _respond(self, status, data, operation_id): |
1979 | @@ -103,93 +115,3 @@ class ShutdownManager(ManagerPlugin): |
1980 | self._session_id, |
1981 | True, |
1982 | ) |
1983 | - |
1984 | - def _get_command_and_args(self, protocol, reboot, force=False): |
1985 | - """ |
1986 | - Returns a C{command, args} 2-tuple suitable for use with |
1987 | - L{IReactorProcess.spawnProcess}. |
1988 | - """ |
1989 | - minutes = None if force else f"+{protocol.delay//60:d}" |
1990 | - args = { |
1991 | - (False, False): [ |
1992 | - "/sbin/shutdown", |
1993 | - "-h", |
1994 | - minutes, |
1995 | - "Landscape is shutting down the system", |
1996 | - ], |
1997 | - (False, True): [ |
1998 | - "/sbin/shutdown", |
1999 | - "-r", |
2000 | - minutes, |
2001 | - "Landscape is rebooting the system", |
2002 | - ], |
2003 | - (True, False): ["/sbin/poweroff"], |
2004 | - (True, True): ["/sbin/reboot"], |
2005 | - }[force, reboot] |
2006 | - return args[0], args |
2007 | - |
2008 | - |
2009 | -class ShutdownProcessProtocol(ProcessProtocol): |
2010 | - """A ProcessProtocol for calling C{/sbin/shutdown}. |
2011 | - |
2012 | - C{shutdown} doesn't return immediately when a time specification is |
2013 | - provided. Failures are reported immediately after it starts and return a |
2014 | - non-zero exit code. The process protocol calls C{shutdown} and waits for |
2015 | - failures for C{timeout} seconds. If no failures are reported it fires |
2016 | - C{result}'s callback with whatever output was received from the process. |
2017 | - If failures are reported C{result}'s errback is fired. |
2018 | - |
2019 | - @ivar result: A L{Deferred} fired when C{shutdown} fails or |
2020 | - succeeds. |
2021 | - @ivar reboot: A flag indicating whether a shutdown or reboot should be |
2022 | - performed. Default is C{False}. |
2023 | - @ivar delay: The time in seconds from now to schedule the shutdown. |
2024 | - Default is 240 seconds. The time will be converted to minutes using |
2025 | - integer division when passed to C{shutdown}. |
2026 | - """ |
2027 | - |
2028 | - def __init__(self, reboot=False, delay=240): |
2029 | - self.result = Deferred() |
2030 | - self.reboot = reboot |
2031 | - self.delay = delay |
2032 | - self._data = [] |
2033 | - self._waiting = True |
2034 | - |
2035 | - def get_data(self): |
2036 | - """Get the data printed by the subprocess.""" |
2037 | - return b"".join(self._data).decode("utf-8", "replace") |
2038 | - |
2039 | - def set_timeout(self, reactor, timeout=10): |
2040 | - """ |
2041 | - Set the error checking timeout, after which C{result}'s callback will |
2042 | - be fired. |
2043 | - """ |
2044 | - reactor.call_later(timeout, self._succeed) |
2045 | - |
2046 | - def childDataReceived(self, fd, data): # noqa: N802 |
2047 | - """Some data was received from the child. |
2048 | - |
2049 | - Add it to our buffer to pass to C{result} when it's fired. |
2050 | - """ |
2051 | - if self._waiting: |
2052 | - self._data.append(data) |
2053 | - |
2054 | - def processEnded(self, reason): # noqa: N802 |
2055 | - """Fire back the C{result} L{Deferred}. |
2056 | - |
2057 | - C{result}'s callback will be fired with the string of data received |
2058 | - from the subprocess, or if the subprocess failed C{result}'s errback |
2059 | - will be fired with the string of data received from the subprocess. |
2060 | - """ |
2061 | - if self._waiting: |
2062 | - if reason.check(ProcessDone): |
2063 | - self._succeed() |
2064 | - else: |
2065 | - self.result.errback(ShutdownFailedError(self.get_data())) |
2066 | - self._waiting = False |
2067 | - |
2068 | - def _succeed(self): |
2069 | - """Fire C{result}'s callback with data accumulated from the process.""" |
2070 | - if self._waiting: |
2071 | - self.result.callback(self.get_data()) |
2072 | - self._waiting = False |
2073 | diff --git a/landscape/client/manager/snapmanager.py b/landscape/client/manager/snapmanager.py |
2074 | index d34bafa..b4214c4 100644 |
2075 | --- a/landscape/client/manager/snapmanager.py |
2076 | +++ b/landscape/client/manager/snapmanager.py |
2077 | @@ -3,49 +3,22 @@ from collections import deque |
2078 | |
2079 | from twisted.internet import task |
2080 | |
2081 | +from landscape.client import snap_http |
2082 | from landscape.client.manager.plugin import FAILED |
2083 | from landscape.client.manager.plugin import ManagerPlugin |
2084 | from landscape.client.manager.plugin import SUCCEEDED |
2085 | -from landscape.client.snap.http import INCOMPLETE_STATUSES |
2086 | -from landscape.client.snap.http import SnapdHttpException |
2087 | -from landscape.client.snap.http import SnapHttp |
2088 | -from landscape.client.snap.http import SUCCESS_STATUSES |
2089 | +from landscape.client.snap_http import INCOMPLETE_STATUSES |
2090 | +from landscape.client.snap_http import SnapdHttpException |
2091 | +from landscape.client.snap_http import SUCCESS_STATUSES |
2092 | |
2093 | |
2094 | -class SnapManager(ManagerPlugin): |
2095 | - """ |
2096 | - Plugin that updates the state of snaps on this machine, installing, |
2097 | - removing, refreshing, enabling, and disabling them in response to messages. |
2098 | - |
2099 | - Changes trigger SnapMonitor to send an updated state message immediately. |
2100 | - """ |
2101 | +class BaseSnapManager(ManagerPlugin): |
2102 | + """Base class that provides machinery for snap manager tasks.""" |
2103 | |
2104 | def __init__(self): |
2105 | super().__init__() |
2106 | |
2107 | - self._snap_http = SnapHttp() |
2108 | - self.SNAP_METHODS = { |
2109 | - "install-snaps": self._snap_http.install_snap, |
2110 | - "install-snaps-batch": self._snap_http.install_snaps, |
2111 | - "remove-snaps": self._snap_http.remove_snap, |
2112 | - "remove-snaps-batch": self._snap_http.remove_snaps, |
2113 | - "refresh-snaps": self._snap_http.refresh_snap, |
2114 | - "refresh-snaps-batch": self._snap_http.refresh_snaps, |
2115 | - "hold-snaps": self._snap_http.hold_snap, |
2116 | - "hold-snaps-batch": self._snap_http.hold_snaps, |
2117 | - "unhold-snaps": self._snap_http.unhold_snap, |
2118 | - "unhold-snaps-batch": self._snap_http.unhold_snaps, |
2119 | - } |
2120 | - |
2121 | - def register(self, registry): |
2122 | - super().register(registry) |
2123 | - self.config = registry.config |
2124 | - |
2125 | - registry.register_message("install-snaps", self._handle_snap_task) |
2126 | - registry.register_message("remove-snaps", self._handle_snap_task) |
2127 | - registry.register_message("refresh-snaps", self._handle_snap_task) |
2128 | - registry.register_message("hold-snaps", self._handle_snap_task) |
2129 | - registry.register_message("unhold-snaps", self._handle_snap_task) |
2130 | + self.SNAP_METHODS = {} |
2131 | |
2132 | def _handle_snap_task(self, message): |
2133 | """ |
2134 | @@ -84,7 +57,7 @@ class SnapManager(ManagerPlugin): |
2135 | snaps, |
2136 | **snap_args, |
2137 | ) |
2138 | - queue.append((response["change"], "BATCH")) |
2139 | + queue.append((response.change, "BATCH")) |
2140 | except SnapdHttpException as e: |
2141 | result = e.json["result"] |
2142 | logging.error( |
2143 | @@ -128,7 +101,7 @@ class SnapManager(ManagerPlugin): |
2144 | name, |
2145 | **snap_args, |
2146 | ) |
2147 | - queue.append((response["change"], name)) |
2148 | + queue.append((response.change, name)) |
2149 | except SnapdHttpException as e: |
2150 | result = e.json["result"] |
2151 | logging.error( |
2152 | @@ -161,7 +134,7 @@ class SnapManager(ManagerPlugin): |
2153 | logging.info("Polling snapd for status of pending snap changes") |
2154 | |
2155 | try: |
2156 | - result = self._snap_http.check_changes().get("result", []) |
2157 | + result = snap_http.check_changes().result |
2158 | result_dict = {c["id"]: c for c in result} |
2159 | except SnapdHttpException as e: |
2160 | logging.error(f"Error checking status of snap changes: {e}") |
2161 | @@ -206,7 +179,7 @@ class SnapManager(ManagerPlugin): |
2162 | |
2163 | response = snap_method(*args, **kwargs) |
2164 | |
2165 | - if "change" not in response: |
2166 | + if response.change is None: |
2167 | raise SnapdHttpException(response) |
2168 | |
2169 | return response |
2170 | @@ -243,17 +216,57 @@ class SnapManager(ManagerPlugin): |
2171 | |
2172 | logging.debug("Sending snap-action-done response") |
2173 | |
2174 | - # Kick off an immediate SnapMonitor message as well. |
2175 | - self._send_installed_snap_update() |
2176 | + # Kick off an immediate monitor message as well. |
2177 | + self._send_snap_update() |
2178 | return self.registry.broker.send_message( |
2179 | message, |
2180 | self._session_id, |
2181 | True, |
2182 | ) |
2183 | |
2184 | - def _send_installed_snap_update(self): |
2185 | + def _send_snap_update(self): |
2186 | + """Kick off an immediate monitor message.""" |
2187 | + |
2188 | + |
2189 | +class SnapManager(BaseSnapManager): |
2190 | + """ |
2191 | + Plugin that updates the state of snaps on this machine, installing, |
2192 | + removing, refreshing, enabling, and disabling them in response to messages. |
2193 | + |
2194 | + Changes trigger SnapMonitor to send an updated state message immediately. |
2195 | + """ |
2196 | + |
2197 | + def __init__(self): |
2198 | + super().__init__() |
2199 | + |
2200 | + self.SNAP_METHODS = { |
2201 | + "install-snaps": snap_http.install, |
2202 | + "install-snaps-batch": snap_http.install_all, |
2203 | + "remove-snaps": snap_http.remove, |
2204 | + "remove-snaps-batch": snap_http.remove_all, |
2205 | + "refresh-snaps": snap_http.refresh, |
2206 | + "refresh-snaps-batch": snap_http.refresh_all, |
2207 | + "hold-snaps": snap_http.hold, |
2208 | + "hold-snaps-batch": snap_http.hold_all, |
2209 | + "unhold-snaps": snap_http.unhold, |
2210 | + "unhold-snaps-batch": snap_http.unhold_all, |
2211 | + "set-snap-config": snap_http.set_conf, |
2212 | + } |
2213 | + |
2214 | + def register(self, registry): |
2215 | + super().register(registry) |
2216 | + self.config = registry.config |
2217 | + |
2218 | + registry.register_message("install-snaps", self._handle_snap_task) |
2219 | + registry.register_message("remove-snaps", self._handle_snap_task) |
2220 | + registry.register_message("refresh-snaps", self._handle_snap_task) |
2221 | + registry.register_message("hold-snaps", self._handle_snap_task) |
2222 | + registry.register_message("unhold-snaps", self._handle_snap_task) |
2223 | + registry.register_message("set-snap-config", self._handle_snap_task) |
2224 | + |
2225 | + def _send_snap_update(self): |
2226 | try: |
2227 | - installed_snaps = self._snap_http.get_snaps() |
2228 | + installed_snaps = snap_http.list().result |
2229 | except SnapdHttpException as e: |
2230 | logging.error( |
2231 | f"Unable to list installed snaps after snap change: {e}", |
2232 | @@ -264,7 +277,7 @@ class SnapManager(ManagerPlugin): |
2233 | return self.registry.broker.send_message( |
2234 | { |
2235 | "type": "snaps", |
2236 | - "snaps": installed_snaps, |
2237 | + "snaps": {"installed": installed_snaps}, |
2238 | }, |
2239 | self._session_id, |
2240 | True, |
2241 | diff --git a/landscape/client/manager/snapservicesmanager.py b/landscape/client/manager/snapservicesmanager.py |
2242 | new file mode 100644 |
2243 | index 0000000..149e171 |
2244 | --- /dev/null |
2245 | +++ b/landscape/client/manager/snapservicesmanager.py |
2246 | @@ -0,0 +1,60 @@ |
2247 | +import logging |
2248 | + |
2249 | +from landscape.client import snap_http |
2250 | +from landscape.client.manager.snapmanager import BaseSnapManager |
2251 | +from landscape.client.snap_http import SnapdHttpException |
2252 | + |
2253 | + |
2254 | +class SnapServicesManager(BaseSnapManager): |
2255 | + """ |
2256 | + Plugin that updates the state of snap services on this machine, starting, |
2257 | + stopping, restarting, enabling, disabling, and reloading them in response |
2258 | + to messages. |
2259 | + |
2260 | + Changes trigger SnapServicesMonitor to send an updated state message |
2261 | + immediately. |
2262 | + """ |
2263 | + |
2264 | + def __init__(self): |
2265 | + super().__init__() |
2266 | + |
2267 | + self.SNAP_METHODS = { |
2268 | + "start-snap-service": snap_http.start, |
2269 | + "start-snap-service-batch": snap_http.start_all, |
2270 | + "stop-snap-service": snap_http.stop, |
2271 | + "stop-snap-service-batch": snap_http.stop_all, |
2272 | + "restart-snap-service": snap_http.restart, |
2273 | + "restart-snap-service-batch": snap_http.restart_all, |
2274 | + } |
2275 | + |
2276 | + def register(self, registry): |
2277 | + super().register(registry) |
2278 | + self.config = registry.config |
2279 | + |
2280 | + message_types = [ |
2281 | + "start-snap-service", |
2282 | + "start-snap-service-batch", |
2283 | + "stop-snap-service", |
2284 | + "stop-snap-service-batch", |
2285 | + "restart-snap-service", |
2286 | + "restart-snap-service-batch", |
2287 | + ] |
2288 | + for msg_type in message_types: |
2289 | + registry.register_message(msg_type, self._handle_snap_task) |
2290 | + |
2291 | + def _send_snap_update(self): |
2292 | + try: |
2293 | + services = snap_http.get_apps(services_only=True).result |
2294 | + except SnapdHttpException as e: |
2295 | + logging.error(f"Unable to list services: {e}") |
2296 | + return |
2297 | + |
2298 | + if services: |
2299 | + return self.registry.broker.send_message( |
2300 | + { |
2301 | + "type": "snap-services", |
2302 | + "services": {"running": services}, |
2303 | + }, |
2304 | + self._session_id, |
2305 | + True, |
2306 | + ) |
2307 | diff --git a/landscape/client/manager/tests/test_aptsources.py b/landscape/client/manager/tests/test_aptsources.py |
2308 | index 4903594..aa648be 100644 |
2309 | --- a/landscape/client/manager/tests/test_aptsources.py |
2310 | +++ b/landscape/client/manager/tests/test_aptsources.py |
2311 | @@ -456,3 +456,46 @@ class AptSourcesTests(LandscapeTest): |
2312 | ) |
2313 | |
2314 | return deferred |
2315 | + |
2316 | + def test_run_reporter_snap(self): |
2317 | + """After receiving a message, `AptSources` in a snap triggers a |
2318 | + reporter run as root to have the new packages reported to the server. |
2319 | + """ |
2320 | + deferred = Deferred() |
2321 | + |
2322 | + def _run_process(command, args, env={}, path=None, uid=None, gid=None): |
2323 | + self.assertEqual( |
2324 | + find_reporter_command(self.manager.config), |
2325 | + command, |
2326 | + ) |
2327 | + self.assertEqual( |
2328 | + [ |
2329 | + "--force-apt-update", |
2330 | + f"--config={self.manager.config.config}", |
2331 | + ], |
2332 | + args, |
2333 | + ) |
2334 | + self.assertEqual(uid, 0) |
2335 | + self.assertEqual(gid, 0) |
2336 | + deferred.callback(("ok", "", 0)) |
2337 | + return deferred |
2338 | + |
2339 | + self.sourceslist._run_process = _run_process |
2340 | + |
2341 | + with mock.patch.multiple( |
2342 | + "landscape.client.manager.aptsources", |
2343 | + USER="root", |
2344 | + GROUP="root", |
2345 | + ): |
2346 | + with mock.patch("os.getuid") as getuid: |
2347 | + getuid.return_value = 0 |
2348 | + self.manager.dispatch_message( |
2349 | + { |
2350 | + "type": "apt-sources-replace", |
2351 | + "sources": [], |
2352 | + "gpg-keys": [], |
2353 | + "operation-id": 1, |
2354 | + }, |
2355 | + ) |
2356 | + |
2357 | + return deferred |
2358 | diff --git a/landscape/client/manager/tests/test_config.py b/landscape/client/manager/tests/test_config.py |
2359 | index da0c052..085bd09 100644 |
2360 | --- a/landscape/client/manager/tests/test_config.py |
2361 | +++ b/landscape/client/manager/tests/test_config.py |
2362 | @@ -21,6 +21,8 @@ class ManagerConfigurationTest(LandscapeTest): |
2363 | "HardwareInfo", |
2364 | "KeystoneToken", |
2365 | "SnapManager", |
2366 | + "SnapServicesManager", |
2367 | + "UbuntuProInfo", |
2368 | ], |
2369 | ALL_PLUGINS, |
2370 | ) |
2371 | diff --git a/landscape/client/manager/tests/test_processkiller.py b/landscape/client/manager/tests/test_processkiller.py |
2372 | index ff906cd..f8408f8 100644 |
2373 | --- a/landscape/client/manager/tests/test_processkiller.py |
2374 | +++ b/landscape/client/manager/tests/test_processkiller.py |
2375 | @@ -99,7 +99,7 @@ class ProcessKillerTests(LandscapeTest): |
2376 | signaller.register(self.manager) |
2377 | popen = get_active_process() |
2378 | process_info = process_info_factory.get_process_info(popen.pid) |
2379 | - self.assertNotEquals(process_info, None) |
2380 | + self.assertNotEqual(process_info, None) |
2381 | start_time = process_info["start-time"] |
2382 | |
2383 | self.manager.dispatch_message( |
2384 | diff --git a/landscape/client/manager/tests/test_scriptexecution.py b/landscape/client/manager/tests/test_scriptexecution.py |
2385 | index e560f49..043c80d 100644 |
2386 | --- a/landscape/client/manager/tests/test_scriptexecution.py |
2387 | +++ b/landscape/client/manager/tests/test_scriptexecution.py |
2388 | @@ -42,7 +42,13 @@ def get_default_environment(): |
2389 | "USER": username, |
2390 | "HOME": home, |
2391 | } |
2392 | - for var in {"LANG", "LC_ALL", "LC_CTYPE"}: |
2393 | + for var in { |
2394 | + "LANG", |
2395 | + "LC_ALL", |
2396 | + "LC_CTYPE", |
2397 | + "LD_LIBRARY_PATH", |
2398 | + "PYTHONPATH", |
2399 | + }: |
2400 | if var in os.environ: |
2401 | env[var] = os.environ[var] |
2402 | return env |
2403 | @@ -415,9 +421,14 @@ class RunScriptTests(LandscapeTest): |
2404 | result.addCallback(check) |
2405 | return result |
2406 | |
2407 | - def _run_script(self, username, uid, gid, path): |
2408 | - expected_uid = uid if uid != os.getuid() else None |
2409 | - expected_gid = gid if gid != os.getgid() else None |
2410 | + def _run_script(self, username, uid, gid, path, from_snap=None): |
2411 | + |
2412 | + if from_snap: |
2413 | + expected_gid = None |
2414 | + expected_uid = None |
2415 | + else: |
2416 | + expected_uid = uid if uid != os.getuid() else None |
2417 | + expected_gid = gid if gid != os.getgid() else None |
2418 | |
2419 | factory = StubProcessFactory() |
2420 | self.plugin.process_factory = factory |
2421 | @@ -426,6 +437,7 @@ class RunScriptTests(LandscapeTest): |
2422 | patch_chown = mock.patch("os.chown") |
2423 | mock_chown = patch_chown.start() |
2424 | |
2425 | + self.plugin.IS_SNAP = from_snap |
2426 | result = self.plugin.run_script("/bin/sh", "echo hi", user=username) |
2427 | |
2428 | self.assertEqual(len(factory.spawns), 1) |
2429 | @@ -441,14 +453,18 @@ class RunScriptTests(LandscapeTest): |
2430 | protocol.processEnded(Failure(ProcessDone(0))) |
2431 | |
2432 | def check(result): |
2433 | - mock_chown.assert_called_with() |
2434 | + if from_snap: |
2435 | + mock_chown.assert_not_called() |
2436 | + else: |
2437 | + mock_chown.assert_called() |
2438 | + |
2439 | self.assertEqual(result, "foobar") |
2440 | |
2441 | def cleanup(result): |
2442 | patch_chown.stop() |
2443 | return result |
2444 | |
2445 | - return result.addErrback(check).addBoth(cleanup) |
2446 | + return result.addCallback(check).addBoth(cleanup) |
2447 | |
2448 | def test_user(self): |
2449 | """ |
2450 | @@ -463,7 +479,28 @@ class RunScriptTests(LandscapeTest): |
2451 | gid = info.pw_gid |
2452 | path = info.pw_dir |
2453 | |
2454 | - return self._run_script(username, uid, gid, path) |
2455 | + if not os.path.exists(path): |
2456 | + path = "/" |
2457 | + |
2458 | + return self._run_script(username, uid, gid, path, from_snap=None) |
2459 | + |
2460 | + def test_user_from_snap(self): |
2461 | + """ |
2462 | + Running a script as a particular user calls |
2463 | + C{IReactorProcess.spawnProcess} with an appropriate C{uid} argument, |
2464 | + with the user's primary group as the C{gid} argument and with the user |
2465 | + home as C{path} argument. |
2466 | + """ |
2467 | + uid = os.getuid() |
2468 | + info = pwd.getpwuid(uid) |
2469 | + username = info.pw_name |
2470 | + gid = info.pw_gid |
2471 | + path = info.pw_dir |
2472 | + |
2473 | + if not os.path.exists(path): |
2474 | + path = "/" |
2475 | + |
2476 | + return self._run_script(username, uid, gid, path, from_snap=True) |
2477 | |
2478 | def test_user_no_home(self): |
2479 | """ |
2480 | diff --git a/landscape/client/manager/tests/test_service.py b/landscape/client/manager/tests/test_service.py |
2481 | index 91b0211..44b2a32 100644 |
2482 | --- a/landscape/client/manager/tests/test_service.py |
2483 | +++ b/landscape/client/manager/tests/test_service.py |
2484 | @@ -1,3 +1,5 @@ |
2485 | +from unittest import mock |
2486 | + |
2487 | from landscape.client.manager.config import ALL_PLUGINS |
2488 | from landscape.client.manager.config import ManagerConfiguration |
2489 | from landscape.client.manager.processkiller import ProcessKiller |
2490 | @@ -11,22 +13,31 @@ class ManagerServiceTest(LandscapeTest): |
2491 | |
2492 | helpers = [FakeBrokerServiceHelper] |
2493 | |
2494 | + class FakeManagerService(ManagerService): |
2495 | + reactor_factory = FakeReactor |
2496 | + |
2497 | def setUp(self): |
2498 | super().setUp() |
2499 | config = ManagerConfiguration() |
2500 | config.load(["-c", self.config_filename]) |
2501 | |
2502 | - class FakeManagerService(ManagerService): |
2503 | - reactor_factory = FakeReactor |
2504 | + self.service = self.FakeManagerService(config) |
2505 | |
2506 | - self.service = FakeManagerService(config) |
2507 | - |
2508 | - def test_plugins(self): |
2509 | + @mock.patch("dbus.SystemBus") |
2510 | + def test_plugins(self, system_bus_mock): |
2511 | """ |
2512 | By default the L{ManagerService.plugins} list holds an instance of |
2513 | every enabled manager plugin. |
2514 | + |
2515 | + We mock `dbus` because in some build environments that run these tests, |
2516 | + such as buildd, SystemBus is not available. |
2517 | """ |
2518 | - self.assertEqual(len(self.service.plugins), len(ALL_PLUGINS)) |
2519 | + config = ManagerConfiguration() |
2520 | + config.load(["-c", self.config_filename]) |
2521 | + service = self.FakeManagerService(config) |
2522 | + |
2523 | + self.assertEqual(len(service.plugins), len(ALL_PLUGINS)) |
2524 | + system_bus_mock.assert_called_once_with() |
2525 | |
2526 | def test_get_plugins(self): |
2527 | """ |
2528 | @@ -37,6 +48,34 @@ class ManagerServiceTest(LandscapeTest): |
2529 | [plugin] = self.service.get_plugins() |
2530 | self.assertTrue(isinstance(plugin, ProcessKiller)) |
2531 | |
2532 | + def test_get_plugins_module_not_found(self): |
2533 | + """If a module is not found, a warning is logged.""" |
2534 | + self.service.config.load(["--manager-plugins", "TotallyDoesNotExist"]) |
2535 | + |
2536 | + with self.assertLogs(level="WARN") as cm: |
2537 | + plugins = self.service.get_plugins() |
2538 | + |
2539 | + self.assertEqual(len(plugins), 0) |
2540 | + self.assertIn("Invalid manager plugin", cm.output[0]) |
2541 | + self.assertIn("TotallyDoesNotExist", cm.output[0]) |
2542 | + |
2543 | + def test_get_plugins_other_exception(self): |
2544 | + """If loading a plugin fails for another reason, a warning is logged, |
2545 | + with the exception. |
2546 | + """ |
2547 | + self.service.config.load(["--manager-plugins", "ProcessKiller"]) |
2548 | + |
2549 | + with self.assertLogs(level="WARN") as cm: |
2550 | + with mock.patch( |
2551 | + "landscape.client.manager.service.namedClass", |
2552 | + ) as namedClass: |
2553 | + namedClass.side_effect = Exception("Is there life on Mars?") |
2554 | + plugins = self.service.get_plugins() |
2555 | + |
2556 | + self.assertEqual(len(plugins), 0) |
2557 | + self.assertIn("Unable to load", cm.output[0]) |
2558 | + self.assertIn("Mars?", cm.output[0]) |
2559 | + |
2560 | def test_start_service(self): |
2561 | """ |
2562 | The L{ManagerService.startService} method connects to the broker, |
2563 | diff --git a/landscape/client/manager/tests/test_shutdownmanager.py b/landscape/client/manager/tests/test_shutdownmanager.py |
2564 | index 4d2a28e..005b0cc 100644 |
2565 | --- a/landscape/client/manager/tests/test_shutdownmanager.py |
2566 | +++ b/landscape/client/manager/tests/test_shutdownmanager.py |
2567 | @@ -1,15 +1,10 @@ |
2568 | -from twisted.internet.error import ProcessDone |
2569 | -from twisted.internet.error import ProcessTerminated |
2570 | -from twisted.internet.protocol import ProcessProtocol |
2571 | -from twisted.python.failure import Failure |
2572 | +from unittest.mock import Mock |
2573 | + |
2574 | +import twisted.internet.defer |
2575 | |
2576 | -from landscape.client.manager.plugin import FAILED |
2577 | -from landscape.client.manager.plugin import SUCCEEDED |
2578 | from landscape.client.manager.shutdownmanager import ShutdownManager |
2579 | -from landscape.client.manager.shutdownmanager import ShutdownProcessProtocol |
2580 | from landscape.client.tests.helpers import LandscapeTest |
2581 | from landscape.client.tests.helpers import ManagerHelper |
2582 | -from landscape.lib.testing import StubProcessFactory |
2583 | |
2584 | |
2585 | class ShutdownManagerTest(LandscapeTest): |
2586 | @@ -18,179 +13,40 @@ class ShutdownManagerTest(LandscapeTest): |
2587 | |
2588 | def setUp(self): |
2589 | super().setUp() |
2590 | + |
2591 | self.broker_service.message_store.set_accepted_types( |
2592 | ["shutdown", "operation-result"], |
2593 | ) |
2594 | self.broker_service.pinger.start() |
2595 | - self.process_factory = StubProcessFactory() |
2596 | - self.plugin = ShutdownManager(process_factory=self.process_factory) |
2597 | + |
2598 | + self.dbus_mock = Mock() |
2599 | + self.dbus_sysbus_mock = Mock() |
2600 | + self.dbus_mock.get_object.return_value = self.dbus_sysbus_mock |
2601 | + self.plugin = ShutdownManager(self.dbus_mock, shutdown_delay=0) |
2602 | self.manager.add(self.plugin) |
2603 | |
2604 | - def test_restart(self): |
2605 | - """ |
2606 | - C{shutdown} processes run until the shutdown is to be performed. The |
2607 | - L{ShutdownProcessProtocol} watches a process for errors, for 10 |
2608 | - seconds by default, and if none occur the activity is marked as |
2609 | - L{SUCCEEDED}. Data printed by the process is included in the |
2610 | - activity's result text. |
2611 | - """ |
2612 | + def test_reboot(self): |
2613 | message = {"type": "shutdown", "reboot": True, "operation-id": 100} |
2614 | - self.plugin.perform_shutdown(message) |
2615 | - [arguments] = self.process_factory.spawns |
2616 | - protocol = arguments[0] |
2617 | - self.assertTrue(isinstance(protocol, ShutdownProcessProtocol)) |
2618 | - self.assertEqual( |
2619 | - arguments[1:3], |
2620 | - ( |
2621 | - "/sbin/shutdown", |
2622 | - [ |
2623 | - "/sbin/shutdown", |
2624 | - "-r", |
2625 | - "+4", |
2626 | - "Landscape is rebooting the system", |
2627 | - ], |
2628 | - ), |
2629 | - ) |
2630 | + deferred = self.plugin._handle_shutdown(message) |
2631 | |
2632 | - def restart_performed(ignore): |
2633 | - self.assertTrue(self.broker_service.exchanger.is_urgent()) |
2634 | - self.assertEqual( |
2635 | - self.broker_service.message_store.get_pending_messages(), |
2636 | - [ |
2637 | - { |
2638 | - "type": "operation-result", |
2639 | - "api": b"3.2", |
2640 | - "operation-id": 100, |
2641 | - "timestamp": 10, |
2642 | - "status": SUCCEEDED, |
2643 | - "result-text": "Data may arrive in batches.", |
2644 | - }, |
2645 | - ], |
2646 | - ) |
2647 | + def check(_): |
2648 | + self.plugin.dbus_sysbus.get_object.assert_called_once() |
2649 | + self.plugin.dbus_sysbus.get_object().Reboot.assert_called_once() |
2650 | |
2651 | - protocol.result.addCallback(restart_performed) |
2652 | - protocol.childDataReceived(0, b"Data may arrive ") |
2653 | - protocol.childDataReceived(0, b"in batches.") |
2654 | - # We need to advance both reactors to simulate that fact they |
2655 | - # are loosely in sync with each other |
2656 | - self.broker_service.reactor.advance(10) |
2657 | - self.manager.reactor.advance(10) |
2658 | - return protocol.result |
2659 | + deferred.addCallback(check) |
2660 | + return deferred |
2661 | |
2662 | def test_shutdown(self): |
2663 | - """ |
2664 | - C{shutdown} messages have a flag that indicates whether a reboot or |
2665 | - shutdown has been requested. The C{shutdown} command is called |
2666 | - appropriately. |
2667 | - """ |
2668 | message = {"type": "shutdown", "reboot": False, "operation-id": 100} |
2669 | - self.plugin.perform_shutdown(message) |
2670 | - [arguments] = self.process_factory.spawns |
2671 | - self.assertEqual( |
2672 | - arguments[1:3], |
2673 | - ( |
2674 | - "/sbin/shutdown", |
2675 | - [ |
2676 | - "/sbin/shutdown", |
2677 | - "-h", |
2678 | - "+4", |
2679 | - "Landscape is shutting down the system", |
2680 | - ], |
2681 | - ), |
2682 | - ) |
2683 | - |
2684 | - def test_restart_fails(self): |
2685 | - """ |
2686 | - If an error occurs before the error checking timeout the activity will |
2687 | - be failed. Data printed by the process prior to the failure is |
2688 | - included in the activity's result text. |
2689 | - """ |
2690 | - message = {"type": "shutdown", "reboot": True, "operation-id": 100} |
2691 | - self.plugin.perform_shutdown(message) |
2692 | - |
2693 | - def restart_failed(message_id): |
2694 | - self.assertTrue(self.broker_service.exchanger.is_urgent()) |
2695 | - messages = self.broker_service.message_store.get_pending_messages() |
2696 | - self.assertEqual(len(messages), 1) |
2697 | - message = messages[0] |
2698 | - self.assertEqual(message["type"], "operation-result") |
2699 | - self.assertEqual(message["api"], b"3.2") |
2700 | - self.assertEqual(message["operation-id"], 100) |
2701 | - self.assertEqual(message["timestamp"], 0) |
2702 | - self.assertEqual(message["status"], FAILED) |
2703 | - self.assertIn("Failure text is reported.", message["result-text"]) |
2704 | - |
2705 | - # Check that after failing, we attempt to force the shutdown by |
2706 | - # switching the binary called |
2707 | - [spawn1_args, spawn2_args] = self.process_factory.spawns |
2708 | - protocol = spawn2_args[0] |
2709 | - self.assertIsInstance(protocol, ProcessProtocol) |
2710 | - self.assertEqual( |
2711 | - spawn2_args[1:3], |
2712 | - ("/sbin/reboot", ["/sbin/reboot"]), |
2713 | - ) |
2714 | - |
2715 | - [arguments] = self.process_factory.spawns |
2716 | - protocol = arguments[0] |
2717 | - protocol.result.addCallback(restart_failed) |
2718 | - protocol.childDataReceived(0, b"Failure text is reported.") |
2719 | - protocol.processEnded(Failure(ProcessTerminated(exitCode=1))) |
2720 | - return protocol.result |
2721 | + deferred = self.plugin._handle_shutdown(message) |
2722 | |
2723 | - def test_process_ends_after_timeout(self): |
2724 | - """ |
2725 | - If the process ends after the error checking timeout has passed |
2726 | - C{result} will not be re-fired. |
2727 | - """ |
2728 | - message = {"type": "shutdown", "reboot": False, "operation-id": 100} |
2729 | - self.plugin.perform_shutdown(message) |
2730 | - |
2731 | - stash = [] |
2732 | - |
2733 | - def restart_performed(ignore): |
2734 | - self.assertEqual(stash, []) |
2735 | - stash.append(True) |
2736 | - |
2737 | - [arguments] = self.process_factory.spawns |
2738 | - protocol = arguments[0] |
2739 | - protocol.result.addCallback(restart_performed) |
2740 | - self.manager.reactor.advance(10) |
2741 | - protocol.processEnded(Failure(ProcessTerminated(exitCode=1))) |
2742 | - return protocol.result |
2743 | - |
2744 | - def test_process_data_is_not_collected_after_firing_result(self): |
2745 | - """ |
2746 | - Data printed in the sub-process is not collected after C{result} has |
2747 | - been fired. |
2748 | - """ |
2749 | - message = {"type": "shutdown", "reboot": False, "operation-id": 100} |
2750 | - self.plugin.perform_shutdown(message) |
2751 | - |
2752 | - [arguments] = self.process_factory.spawns |
2753 | - protocol = arguments[0] |
2754 | - protocol.childDataReceived(0, b"Data may arrive ") |
2755 | - protocol.childDataReceived(0, b"in batches.") |
2756 | - self.manager.reactor.advance(10) |
2757 | - self.assertEqual(protocol.get_data(), "Data may arrive in batches.") |
2758 | - protocol.childDataReceived(0, b"Even when you least expect it.") |
2759 | - self.assertEqual(protocol.get_data(), "Data may arrive in batches.") |
2760 | - |
2761 | - def test_restart_stops_exchanger(self): |
2762 | - """ |
2763 | - After a successful shutdown, the broker stops processing new messages. |
2764 | - """ |
2765 | - message = {"type": "shutdown", "reboot": False, "operation-id": 100} |
2766 | - self.plugin.perform_shutdown(message) |
2767 | + def check(_): |
2768 | + self.plugin.dbus_sysbus.get_object.assert_called_once() |
2769 | + self.plugin.dbus_sysbus.get_object().PowerOff.assert_called_once() |
2770 | |
2771 | - [arguments] = self.process_factory.spawns |
2772 | - protocol = arguments[0] |
2773 | - protocol.processEnded(Failure(ProcessDone(status=0))) |
2774 | - self.broker_service.reactor.advance(100) |
2775 | - self.manager.reactor.advance(100) |
2776 | + self.plugin.shutdown_deferred.addCallback(check) |
2777 | + return deferred |
2778 | |
2779 | - # New messages will not be exchanged after a reboot process is in |
2780 | - # process. |
2781 | - self.manager.broker.exchanger.schedule_exchange() |
2782 | - payloads = self.manager.broker.exchanger._transport.payloads |
2783 | - self.assertEqual(0, len(payloads)) |
2784 | - return protocol.result |
2785 | + def test_shutdown_failed(self): |
2786 | + deferred = self.plugin._respond_fail("", 100) |
2787 | + self.assertIsInstance(deferred, twisted.internet.defer.Deferred) |
2788 | diff --git a/landscape/client/manager/tests/test_snapmanager.py b/landscape/client/manager/tests/test_snapmanager.py |
2789 | index 1ce52f1..70cd6c3 100644 |
2790 | --- a/landscape/client/manager/tests/test_snapmanager.py |
2791 | +++ b/landscape/client/manager/tests/test_snapmanager.py |
2792 | @@ -3,8 +3,8 @@ from unittest import mock |
2793 | from landscape.client.manager.manager import FAILED |
2794 | from landscape.client.manager.manager import SUCCEEDED |
2795 | from landscape.client.manager.snapmanager import SnapManager |
2796 | -from landscape.client.snap.http import SnapdHttpException |
2797 | -from landscape.client.snap.http import SnapHttp as OrigSnapHttp |
2798 | +from landscape.client.snap_http import SnapdHttpException |
2799 | +from landscape.client.snap_http import SnapdResponse |
2800 | from landscape.client.tests.helpers import LandscapeTest |
2801 | from landscape.client.tests.helpers import ManagerHelper |
2802 | |
2803 | @@ -15,13 +15,10 @@ class SnapManagerTest(LandscapeTest): |
2804 | def setUp(self): |
2805 | super().setUp() |
2806 | |
2807 | - self.snap_http = mock.Mock(spec_set=OrigSnapHttp) |
2808 | - self.SnapHttp = mock.patch( |
2809 | - "landscape.client.manager.snapmanager.SnapHttp", |
2810 | + self.snap_http = mock.patch( |
2811 | + "landscape.client.manager.snapmanager.snap_http", |
2812 | ).start() |
2813 | |
2814 | - self.SnapHttp.return_value = self.snap_http |
2815 | - |
2816 | self.broker_service.message_store.set_accepted_types( |
2817 | ["operation-result"], |
2818 | ) |
2819 | @@ -42,34 +39,37 @@ class SnapManagerTest(LandscapeTest): |
2820 | |
2821 | def install_snap(name, revision=None, channel=None, classic=False): |
2822 | if name == "hello": |
2823 | - return {"change": "1"} |
2824 | + return SnapdResponse("async", 200, "OK", None, change="1") |
2825 | |
2826 | if name == "goodbye": |
2827 | - return {"change": "2"} |
2828 | + return SnapdResponse("async", 200, "OK", None, change="2") |
2829 | |
2830 | return mock.DEFAULT |
2831 | |
2832 | - self.snap_http.install_snap.side_effect = install_snap |
2833 | - self.snap_http.check_changes.return_value = { |
2834 | - "result": [ |
2835 | + self.snap_http.install.side_effect = install_snap |
2836 | + self.snap_http.check_changes.return_value = SnapdResponse( |
2837 | + "sync", |
2838 | + "200", |
2839 | + "OK", |
2840 | + [ |
2841 | {"id": "1", "status": "Done"}, |
2842 | {"id": "2", "status": "Done"}, |
2843 | ], |
2844 | - } |
2845 | - self.snap_http.get_snaps.return_value = {"installed": []} |
2846 | + ) |
2847 | + self.snap_http.list.return_value = SnapdResponse("sync", 200, "OK", []) |
2848 | |
2849 | result = self.manager.dispatch_message( |
2850 | { |
2851 | "type": "install-snaps", |
2852 | "operation-id": 123, |
2853 | "snaps": [ |
2854 | - {"name": "hello", "revision": 9001}, |
2855 | + {"name": "hello", "args": {"revision": 9001}}, |
2856 | {"name": "goodbye"}, |
2857 | ], |
2858 | }, |
2859 | ) |
2860 | |
2861 | - def got_result(r): |
2862 | + def got_result(_): |
2863 | self.assertMessages( |
2864 | self.broker_service.message_store.get_pending_messages(), |
2865 | [ |
2866 | @@ -90,12 +90,24 @@ class SnapManagerTest(LandscapeTest): |
2867 | When no channels or revisions are specified, snaps are installed |
2868 | via a single call to snapd. |
2869 | """ |
2870 | - self.snap_http.install_snaps.return_value = {"change": "1"} |
2871 | - self.snap_http.check_changes.return_value = { |
2872 | - "result": [{"id": "1", "status": "Done"}], |
2873 | - } |
2874 | - self.snap_http.get_snaps.return_value = { |
2875 | - "installed": [ |
2876 | + self.snap_http.install_all.return_value = SnapdResponse( |
2877 | + "async", |
2878 | + 202, |
2879 | + "Accepted", |
2880 | + None, |
2881 | + change="1", |
2882 | + ) |
2883 | + self.snap_http.check_changes.return_value = SnapdResponse( |
2884 | + "sync", |
2885 | + 200, |
2886 | + "OK", |
2887 | + [{"id": "1", "status": "Done"}], |
2888 | + ) |
2889 | + self.snap_http.list.return_value = SnapdResponse( |
2890 | + "sync", |
2891 | + 200, |
2892 | + "OK", |
2893 | + [ |
2894 | { |
2895 | "name": "hello", |
2896 | "id": "test", |
2897 | @@ -106,7 +118,7 @@ class SnapManagerTest(LandscapeTest): |
2898 | "version": "1.2.3", |
2899 | }, |
2900 | ], |
2901 | - } |
2902 | + ) |
2903 | |
2904 | result = self.manager.dispatch_message( |
2905 | { |
2906 | @@ -119,7 +131,7 @@ class SnapManagerTest(LandscapeTest): |
2907 | }, |
2908 | ) |
2909 | |
2910 | - def got_result(r): |
2911 | + def got_result(_): |
2912 | self.assertMessages( |
2913 | self.broker_service.message_store.get_pending_messages(), |
2914 | [ |
2915 | @@ -136,10 +148,10 @@ class SnapManagerTest(LandscapeTest): |
2916 | return result.addCallback(got_result) |
2917 | |
2918 | def test_install_snap_immediate_error(self): |
2919 | - self.snap_http.install_snaps.side_effect = SnapdHttpException( |
2920 | + self.snap_http.install_all.side_effect = SnapdHttpException( |
2921 | b'{"result": "whoops"}', |
2922 | ) |
2923 | - self.snap_http.get_snaps.return_value = {"installed": []} |
2924 | + self.snap_http.list.return_value = SnapdResponse("sync", 200, "OK", []) |
2925 | |
2926 | result = self.manager.dispatch_message( |
2927 | { |
2928 | @@ -151,7 +163,7 @@ class SnapManagerTest(LandscapeTest): |
2929 | |
2930 | self.log_helper.ignore_errors(r".+whoops$") |
2931 | |
2932 | - def got_result(r): |
2933 | + def got_result(_): |
2934 | self.assertMessages( |
2935 | self.broker_service.message_store.get_pending_messages(), |
2936 | [ |
2937 | @@ -168,9 +180,20 @@ class SnapManagerTest(LandscapeTest): |
2938 | return result.addCallback(got_result) |
2939 | |
2940 | def test_install_snap_no_status(self): |
2941 | - self.snap_http.install_snaps.return_value = {"change": "1"} |
2942 | - self.snap_http.check_changes.return_value = {"result": []} |
2943 | - self.snap_http.get_snaps.return_value = {"installed": []} |
2944 | + self.snap_http.install_all.return_value = SnapdResponse( |
2945 | + "async", |
2946 | + 202, |
2947 | + "Accepted", |
2948 | + None, |
2949 | + change="1", |
2950 | + ) |
2951 | + self.snap_http.check_changes.return_value = SnapdResponse( |
2952 | + "sync", |
2953 | + 200, |
2954 | + "OK", |
2955 | + [], |
2956 | + ) |
2957 | + self.snap_http.list.return_value = SnapdResponse("sync", 200, "OK", []) |
2958 | |
2959 | result = self.manager.dispatch_message( |
2960 | { |
2961 | @@ -180,7 +203,7 @@ class SnapManagerTest(LandscapeTest): |
2962 | }, |
2963 | ) |
2964 | |
2965 | - def got_result(r): |
2966 | + def got_result(_): |
2967 | self.assertMessages( |
2968 | self.broker_service.message_store.get_pending_messages(), |
2969 | [ |
2970 | @@ -197,9 +220,15 @@ class SnapManagerTest(LandscapeTest): |
2971 | return result.addCallback(got_result) |
2972 | |
2973 | def test_install_snap_check_error(self): |
2974 | - self.snap_http.install_snaps.return_value = {"change": "1"} |
2975 | + self.snap_http.install_all.return_value = SnapdResponse( |
2976 | + "async", |
2977 | + 200, |
2978 | + "Accepted", |
2979 | + None, |
2980 | + change="1", |
2981 | + ) |
2982 | self.snap_http.check_changes.side_effect = SnapdHttpException("whoops") |
2983 | - self.snap_http.get_snaps.return_value = {"installed": []} |
2984 | + self.snap_http.list.return_value = SnapdResponse("sync", 200, "OK", []) |
2985 | |
2986 | result = self.manager.dispatch_message( |
2987 | { |
2988 | @@ -211,7 +240,7 @@ class SnapManagerTest(LandscapeTest): |
2989 | |
2990 | self.log_helper.ignore_errors(r".+whoops$") |
2991 | |
2992 | - def got_result(r): |
2993 | + def got_result(_): |
2994 | self.assertMessages( |
2995 | self.broker_service.message_store.get_pending_messages(), |
2996 | [ |
2997 | @@ -228,11 +257,20 @@ class SnapManagerTest(LandscapeTest): |
2998 | return result.addCallback(got_result) |
2999 | |
3000 | def test_remove_snap(self): |
3001 | - self.snap_http.remove_snaps.return_value = {"change": "1"} |
3002 | - self.snap_http.check_changes.return_value = { |
3003 | - "result": [{"id": "1", "status": "Done"}], |
3004 | - } |
3005 | - self.snap_http.get_snaps.return_value = {"installed": []} |
3006 | + self.snap_http.remove_all.return_value = SnapdResponse( |
3007 | + "async", |
3008 | + 202, |
3009 | + "Accepted", |
3010 | + None, |
3011 | + change="1", |
3012 | + ) |
3013 | + self.snap_http.check_changes.return_value = SnapdResponse( |
3014 | + "sync", |
3015 | + 200, |
3016 | + "OK", |
3017 | + [{"id": "1", "status": "Done"}], |
3018 | + ) |
3019 | + self.snap_http.list.return_value = SnapdResponse("sync", 200, "OK", []) |
3020 | |
3021 | result = self.manager.dispatch_message( |
3022 | { |
3023 | @@ -242,7 +280,7 @@ class SnapManagerTest(LandscapeTest): |
3024 | }, |
3025 | ) |
3026 | |
3027 | - def got_result(r): |
3028 | + def got_result(_): |
3029 | self.assertMessages( |
3030 | self.broker_service.message_store.get_pending_messages(), |
3031 | [ |
3032 | @@ -257,3 +295,89 @@ class SnapManagerTest(LandscapeTest): |
3033 | ) |
3034 | |
3035 | return result.addCallback(got_result) |
3036 | + |
3037 | + def test_set_config(self): |
3038 | + self.snap_http.set_conf.return_value = SnapdResponse( |
3039 | + "async", |
3040 | + 202, |
3041 | + "Accepted", |
3042 | + None, |
3043 | + change="1", |
3044 | + ) |
3045 | + self.snap_http.check_changes.return_value = SnapdResponse( |
3046 | + "sync", |
3047 | + "200", |
3048 | + "OK", |
3049 | + [{"id": "1", "status": "Done"}], |
3050 | + ) |
3051 | + self.snap_http.list.return_value = SnapdResponse("sync", 200, "OK", []) |
3052 | + |
3053 | + result = self.manager.dispatch_message( |
3054 | + { |
3055 | + "type": "set-snap-config", |
3056 | + "operation-id": 123, |
3057 | + "snaps": [ |
3058 | + { |
3059 | + "name": "hello", |
3060 | + "args": { |
3061 | + "config": {"foo": {"bar": "qux", "baz": "quux"}}, |
3062 | + }, |
3063 | + }, |
3064 | + ], |
3065 | + }, |
3066 | + ) |
3067 | + |
3068 | + def got_result(_): |
3069 | + self.assertMessages( |
3070 | + self.broker_service.message_store.get_pending_messages(), |
3071 | + [ |
3072 | + { |
3073 | + "type": "operation-result", |
3074 | + "status": SUCCEEDED, |
3075 | + "result-text": "{'completed': ['hello'], " |
3076 | + "'errored': [], 'errors': {}}", |
3077 | + "operation-id": 123, |
3078 | + }, |
3079 | + ], |
3080 | + ) |
3081 | + |
3082 | + return result.addCallback(got_result) |
3083 | + |
3084 | + def test_set_config_sync_error(self): |
3085 | + self.snap_http.set_conf.side_effect = SnapdHttpException( |
3086 | + b'{"result": "whoops"}', |
3087 | + ) |
3088 | + self.snap_http.list.return_value = SnapdResponse("sync", 200, "OK", []) |
3089 | + |
3090 | + result = self.manager.dispatch_message( |
3091 | + { |
3092 | + "type": "set-snap-config", |
3093 | + "operation-id": 123, |
3094 | + "snaps": [ |
3095 | + { |
3096 | + "name": "hello", |
3097 | + "args": { |
3098 | + "config": {"foo": {"bar": "qux", "baz": "quux"}}, |
3099 | + }, |
3100 | + }, |
3101 | + ], |
3102 | + }, |
3103 | + ) |
3104 | + |
3105 | + def got_result(_): |
3106 | + self.assertMessages( |
3107 | + self.broker_service.message_store.get_pending_messages(), |
3108 | + [ |
3109 | + { |
3110 | + "type": "operation-result", |
3111 | + "status": FAILED, |
3112 | + "result-text": ( |
3113 | + "{'completed': [], 'errored': [], " |
3114 | + "'errors': {'hello': 'whoops'}}" |
3115 | + ), |
3116 | + "operation-id": 123, |
3117 | + }, |
3118 | + ], |
3119 | + ) |
3120 | + |
3121 | + return result.addCallback(got_result) |
3122 | diff --git a/landscape/client/manager/tests/test_snapservicesmanager.py b/landscape/client/manager/tests/test_snapservicesmanager.py |
3123 | new file mode 100644 |
3124 | index 0000000..6198f6c |
3125 | --- /dev/null |
3126 | +++ b/landscape/client/manager/tests/test_snapservicesmanager.py |
3127 | @@ -0,0 +1,333 @@ |
3128 | +import sys |
3129 | +from unittest import mock |
3130 | + |
3131 | +from landscape.client.manager.manager import FAILED |
3132 | +from landscape.client.manager.manager import SUCCEEDED |
3133 | +from landscape.client.manager.snapservicesmanager import SnapServicesManager |
3134 | +from landscape.client.snap_http import SnapdHttpException |
3135 | +from landscape.client.snap_http import SnapdResponse |
3136 | +from landscape.client.tests.helpers import LandscapeTest |
3137 | +from landscape.client.tests.helpers import ManagerHelper |
3138 | + |
3139 | + |
3140 | +class SnapServicesManagerTest(LandscapeTest): |
3141 | + helpers = [ManagerHelper] |
3142 | + |
3143 | + def setUp(self): |
3144 | + super().setUp() |
3145 | + |
3146 | + self.snap_http = mock.patch( |
3147 | + "landscape.client.manager.snapservicesmanager.snap_http", |
3148 | + ).start() |
3149 | + |
3150 | + self.broker_service.message_store.set_accepted_types( |
3151 | + ["operation-result"], |
3152 | + ) |
3153 | + self.plugin = SnapServicesManager() |
3154 | + self.manager.add(self.plugin) |
3155 | + |
3156 | + self.manager.config.snapd_poll_attempts = 2 |
3157 | + self.manager.config.snapd_poll_interval = 0.1 |
3158 | + |
3159 | + def tearDown(self): |
3160 | + mock.patch.stopall() |
3161 | + |
3162 | + @mock.patch("landscape.client.manager.snapmanager.snap_http") |
3163 | + def test_start_service(self, mock_base_snap_http): |
3164 | + self.snap_http.start.return_value = SnapdResponse( |
3165 | + "async", |
3166 | + 202, |
3167 | + "Accepted", |
3168 | + None, |
3169 | + change="1", |
3170 | + ) |
3171 | + mock_base_snap_http.check_changes.return_value = SnapdResponse( |
3172 | + "sync", |
3173 | + 200, |
3174 | + "OK", |
3175 | + [{"id": "1", "status": "Done"}], |
3176 | + ) |
3177 | + self.snap_http.get_apps.return_value = SnapdResponse( |
3178 | + "sync", |
3179 | + 200, |
3180 | + "OK", |
3181 | + [ |
3182 | + { |
3183 | + "snap": "test-snap", |
3184 | + "name": "bye-svc", |
3185 | + "daemon": "simple", |
3186 | + "daemon-scope": "system", |
3187 | + "enabled": True, |
3188 | + }, |
3189 | + ], |
3190 | + ) |
3191 | + |
3192 | + result = self.manager.dispatch_message( |
3193 | + { |
3194 | + "type": "start-snap-service", |
3195 | + "operation-id": 123, |
3196 | + "snaps": [ |
3197 | + {"name": "test-snap.hello-svc", "args": {"enable": True}}, |
3198 | + ], |
3199 | + }, |
3200 | + ) |
3201 | + |
3202 | + def got_result(_): |
3203 | + self.assertMessages( |
3204 | + self.broker_service.message_store.get_pending_messages(), |
3205 | + [ |
3206 | + { |
3207 | + "type": "operation-result", |
3208 | + "status": SUCCEEDED, |
3209 | + "result-text": "{'completed': ['test-snap.hello-svc']," |
3210 | + " 'errored': [], 'errors': {}}", |
3211 | + "operation-id": 123, |
3212 | + }, |
3213 | + ], |
3214 | + ) |
3215 | + |
3216 | + self.snap_http.start.assert_called_once_with( |
3217 | + "test-snap.hello-svc", |
3218 | + enable=True, |
3219 | + ) |
3220 | + |
3221 | + return result.addCallback(got_result) |
3222 | + |
3223 | + def test_start_service_error(self): |
3224 | + self.snap_http.start.side_effect = SnapdHttpException( |
3225 | + b'{"result": "snap idonotexist not found"}', |
3226 | + ) |
3227 | + self.snap_http.get_apps.return_value = SnapdResponse( |
3228 | + "sync", |
3229 | + 200, |
3230 | + "OK", |
3231 | + [], |
3232 | + ) |
3233 | + |
3234 | + result = self.manager.dispatch_message( |
3235 | + { |
3236 | + "type": "start-snap-service", |
3237 | + "operation-id": 123, |
3238 | + "snaps": [{"name": "idonotexist", "args": {"enable": True}}], |
3239 | + }, |
3240 | + ) |
3241 | + |
3242 | + self.log_helper.ignore_errors(r".+idonotexist$") |
3243 | + |
3244 | + def got_result(_): |
3245 | + self.assertMessages( |
3246 | + self.broker_service.message_store.get_pending_messages(), |
3247 | + [ |
3248 | + { |
3249 | + "type": "operation-result", |
3250 | + "status": FAILED, |
3251 | + "result-text": "{'completed': [], " |
3252 | + "'errored': [], 'errors': {'idonotexist': " |
3253 | + "'snap idonotexist not found'}}", |
3254 | + "operation-id": 123, |
3255 | + }, |
3256 | + ], |
3257 | + ) |
3258 | + |
3259 | + self.snap_http.start.assert_called_once_with( |
3260 | + "idonotexist", |
3261 | + enable=True, |
3262 | + ) |
3263 | + |
3264 | + return result.addCallback(got_result) |
3265 | + |
3266 | + @mock.patch("landscape.client.manager.snapmanager.snap_http") |
3267 | + def test_stop_service_batch(self, mock_base_snap_http): |
3268 | + self.snap_http.stop_all.return_value = SnapdResponse( |
3269 | + "async", |
3270 | + 202, |
3271 | + "Accepted", |
3272 | + None, |
3273 | + change="1", |
3274 | + ) |
3275 | + mock_base_snap_http.check_changes.return_value = SnapdResponse( |
3276 | + "sync", |
3277 | + 200, |
3278 | + "OK", |
3279 | + [{"id": "1", "status": "Done"}], |
3280 | + ) |
3281 | + self.snap_http.get_apps.return_value = SnapdResponse( |
3282 | + "sync", |
3283 | + 200, |
3284 | + "OK", |
3285 | + [ |
3286 | + { |
3287 | + "snap": "lxd", |
3288 | + "name": "lxd", |
3289 | + "daemon": "simple", |
3290 | + "daemon-scope": "system", |
3291 | + }, |
3292 | + { |
3293 | + "snap": "landscape-client", |
3294 | + "name": "landscape-client", |
3295 | + "daemon": "simple", |
3296 | + "daemon-scope": "system", |
3297 | + }, |
3298 | + ], |
3299 | + ) |
3300 | + |
3301 | + result = self.manager.dispatch_message( |
3302 | + { |
3303 | + "type": "stop-snap-service", |
3304 | + "operation-id": 123, |
3305 | + "snaps": [ |
3306 | + {"name": "lxd"}, |
3307 | + {"name": "landscape-client"}, |
3308 | + ], |
3309 | + "args": {"disable": False}, |
3310 | + }, |
3311 | + ) |
3312 | + |
3313 | + def got_result(_): |
3314 | + self.assertMessages( |
3315 | + self.broker_service.message_store.get_pending_messages(), |
3316 | + [ |
3317 | + { |
3318 | + "type": "operation-result", |
3319 | + "status": SUCCEEDED, |
3320 | + "result-text": "{'completed': ['BATCH'], " |
3321 | + "'errored': [], 'errors': {}}", |
3322 | + "operation-id": 123, |
3323 | + }, |
3324 | + ], |
3325 | + ) |
3326 | + |
3327 | + self.snap_http.stop_all.assert_called_once_with( |
3328 | + ["lxd", "landscape-client"], |
3329 | + disable=False, |
3330 | + ) |
3331 | + |
3332 | + return result.addCallback(got_result) |
3333 | + |
3334 | + @mock.patch("landscape.client.manager.snapmanager.snap_http") |
3335 | + def test_restart_service(self, mock_base_snap_http): |
3336 | + self.snap_http.restart_all.return_value = SnapdResponse( |
3337 | + "async", |
3338 | + 202, |
3339 | + "Accepted", |
3340 | + None, |
3341 | + change="1", |
3342 | + ) |
3343 | + mock_base_snap_http.check_changes.return_value = SnapdResponse( |
3344 | + "sync", |
3345 | + 200, |
3346 | + "OK", |
3347 | + [{"id": "1", "status": "Done"}], |
3348 | + ) |
3349 | + self.snap_http.get_apps.return_value = SnapdResponse( |
3350 | + "sync", |
3351 | + 200, |
3352 | + "OK", |
3353 | + [ |
3354 | + { |
3355 | + "snap": "lxd", |
3356 | + "name": "lxd", |
3357 | + "daemon": "simple", |
3358 | + "daemon-scope": "system", |
3359 | + }, |
3360 | + { |
3361 | + "snap": "test-snap", |
3362 | + "name": "bye-svc", |
3363 | + "daemon": "simple", |
3364 | + "daemon-scope": "system", |
3365 | + }, |
3366 | + ], |
3367 | + ) |
3368 | + |
3369 | + result = self.manager.dispatch_message( |
3370 | + { |
3371 | + "type": "restart-snap-service", |
3372 | + "operation-id": 123, |
3373 | + "snaps": [ |
3374 | + {"name": "test-snap"}, |
3375 | + {"name": "lxd"}, |
3376 | + ], |
3377 | + }, |
3378 | + ) |
3379 | + |
3380 | + def got_result(_): |
3381 | + self.assertMessages( |
3382 | + self.broker_service.message_store.get_pending_messages(), |
3383 | + [ |
3384 | + { |
3385 | + "type": "operation-result", |
3386 | + "status": SUCCEEDED, |
3387 | + "result-text": "{'completed': ['BATCH'], " |
3388 | + "'errored': [], 'errors': {}}", |
3389 | + "operation-id": 123, |
3390 | + }, |
3391 | + ], |
3392 | + ) |
3393 | + |
3394 | + self.snap_http.restart_all.assert_called_once_with( |
3395 | + ["test-snap", "lxd"], |
3396 | + ) |
3397 | + |
3398 | + return result.addCallback(got_result) |
3399 | + |
3400 | + @mock.patch("landscape.client.manager.snapmanager.snap_http") |
3401 | + def test_restart_service_update_failure(self, mock_base_snap_http): |
3402 | + """ |
3403 | + Test when the client runs the operation successfully but |
3404 | + `_send_snap_update` fails. |
3405 | + """ |
3406 | + self.snap_http.restart_all.return_value = SnapdResponse( |
3407 | + "async", |
3408 | + 202, |
3409 | + "Accepted", |
3410 | + None, |
3411 | + change="1", |
3412 | + ) |
3413 | + mock_base_snap_http.check_changes.return_value = SnapdResponse( |
3414 | + "sync", |
3415 | + 200, |
3416 | + "OK", |
3417 | + [{"id": "1", "status": "Done"}], |
3418 | + ) |
3419 | + self.snap_http.get_apps.side_effect = SnapdHttpException( |
3420 | + "An error occurred.", |
3421 | + ) |
3422 | + mock_logger = mock.Mock() |
3423 | + self.patch(sys.modules["logging"], "error", mock_logger) |
3424 | + |
3425 | + result = self.manager.dispatch_message( |
3426 | + { |
3427 | + "type": "restart-snap-service", |
3428 | + "operation-id": 123, |
3429 | + "snaps": [ |
3430 | + {"name": "test-snap"}, |
3431 | + {"name": "lxd"}, |
3432 | + ], |
3433 | + }, |
3434 | + ) |
3435 | + |
3436 | + self.log_helper.ignore_errors(r".+error$") |
3437 | + |
3438 | + def got_result(_): |
3439 | + self.assertMessages( |
3440 | + self.broker_service.message_store.get_pending_messages(), |
3441 | + [ |
3442 | + { |
3443 | + "type": "operation-result", |
3444 | + "status": SUCCEEDED, |
3445 | + "result-text": "{'completed': ['BATCH'], " |
3446 | + "'errored': [], 'errors': {}}", |
3447 | + "operation-id": 123, |
3448 | + }, |
3449 | + ], |
3450 | + ) |
3451 | + |
3452 | + mock_logger.assert_called_once_with( |
3453 | + "Unable to list services: An error occurred.", |
3454 | + ) |
3455 | + |
3456 | + self.snap_http.restart_all.assert_called_once_with( |
3457 | + ["test-snap", "lxd"], |
3458 | + ) |
3459 | + |
3460 | + return result.addCallback(got_result) |
3461 | diff --git a/landscape/client/manager/tests/test_ubuntuproinfo.py b/landscape/client/manager/tests/test_ubuntuproinfo.py |
3462 | new file mode 100644 |
3463 | index 0000000..10c60eb |
3464 | --- /dev/null |
3465 | +++ b/landscape/client/manager/tests/test_ubuntuproinfo.py |
3466 | @@ -0,0 +1,151 @@ |
3467 | +from unittest import mock |
3468 | + |
3469 | +from landscape.client.manager.ubuntuproinfo import get_ubuntu_pro_info |
3470 | +from landscape.client.manager.ubuntuproinfo import UbuntuProInfo |
3471 | +from landscape.client.tests.helpers import LandscapeTest |
3472 | +from landscape.client.tests.helpers import MonitorHelper |
3473 | + |
3474 | + |
3475 | +class UbuntuProInfoTest(LandscapeTest): |
3476 | + """Ubuntu Pro info plugin tests.""" |
3477 | + |
3478 | + helpers = [MonitorHelper] |
3479 | + |
3480 | + def setUp(self): |
3481 | + super().setUp() |
3482 | + self.mstore.set_accepted_types(["ubuntu-pro-info"]) |
3483 | + |
3484 | + def test_ubuntu_pro_info(self): |
3485 | + """Tests calling `ua status`.""" |
3486 | + plugin = UbuntuProInfo() |
3487 | + |
3488 | + with mock.patch("subprocess.run") as run_mock: |
3489 | + run_mock.return_value = mock.Mock( |
3490 | + stdout='"This is a test"', |
3491 | + ) |
3492 | + self.monitor.add(plugin) |
3493 | + plugin.run() |
3494 | + |
3495 | + run_mock.assert_called() |
3496 | + messages = self.mstore.get_pending_messages() |
3497 | + self.assertTrue(len(messages) > 0) |
3498 | + self.assertTrue("ubuntu-pro-info" in messages[0]) |
3499 | + self.assertEqual(messages[0]["ubuntu-pro-info"], '"This is a test"') |
3500 | + |
3501 | + def test_ubuntu_pro_info_no_pro(self): |
3502 | + """Tests calling `pro status` when it is not installed.""" |
3503 | + plugin = UbuntuProInfo() |
3504 | + self.monitor.add(plugin) |
3505 | + |
3506 | + with mock.patch("subprocess.run") as run_mock: |
3507 | + run_mock.side_effect = FileNotFoundError() |
3508 | + plugin.run() |
3509 | + |
3510 | + messages = self.mstore.get_pending_messages() |
3511 | + run_mock.assert_called_once() |
3512 | + self.assertTrue(len(messages) > 0) |
3513 | + self.assertTrue("ubuntu-pro-info" in messages[0]) |
3514 | + self.assertIn("errors", messages[0]["ubuntu-pro-info"]) |
3515 | + |
3516 | + def test_get_ubuntu_pro_info_core(self): |
3517 | + """In Ubuntu Core, there is no pro info, so return a reasonable erro |
3518 | + message. |
3519 | + """ |
3520 | + with mock.patch( |
3521 | + "landscape.client.manager.ubuntuproinfo.IS_CORE", |
3522 | + new="1", |
3523 | + ): |
3524 | + result = get_ubuntu_pro_info() |
3525 | + |
3526 | + self.assertIn("errors", result) |
3527 | + self.assertIn("not available", result["errors"][0]["message"]) |
3528 | + self.assertEqual(result["result"], "failure") |
3529 | + |
3530 | + def test_persistence_unchanged_data(self): |
3531 | + """If data hasn't changed, a new message is not sent""" |
3532 | + plugin = UbuntuProInfo() |
3533 | + self.monitor.add(plugin) |
3534 | + data = '"Initial data!"' |
3535 | + |
3536 | + with mock.patch("subprocess.run") as run_mock: |
3537 | + run_mock.return_value = mock.Mock( |
3538 | + stdout=data, |
3539 | + ) |
3540 | + plugin.run() |
3541 | + |
3542 | + messages = self.mstore.get_pending_messages() |
3543 | + run_mock.assert_called_once() |
3544 | + self.assertEqual(1, len(messages)) |
3545 | + self.assertTrue("ubuntu-pro-info" in messages[0]) |
3546 | + self.assertEqual(messages[0]["ubuntu-pro-info"], data) |
3547 | + |
3548 | + with mock.patch("subprocess.run") as run_mock: |
3549 | + run_mock.return_value = mock.Mock( |
3550 | + stdout=data, |
3551 | + ) |
3552 | + plugin.run() |
3553 | + |
3554 | + run_mock.assert_called_once() |
3555 | + messages = self.mstore.get_pending_messages() |
3556 | + self.assertEqual(1, len(messages)) |
3557 | + |
3558 | + def test_persistence_changed_data(self): |
3559 | + """New data will be sent in a new message in the queue""" |
3560 | + plugin = UbuntuProInfo() |
3561 | + self.monitor.add(plugin) |
3562 | + |
3563 | + with mock.patch("subprocess.run") as run_mock: |
3564 | + run_mock.return_value = mock.Mock( |
3565 | + stdout='"Initial data!"', |
3566 | + ) |
3567 | + plugin.run() |
3568 | + |
3569 | + messages = self.mstore.get_pending_messages() |
3570 | + run_mock.assert_called_once() |
3571 | + self.assertEqual(1, len(messages)) |
3572 | + self.assertTrue("ubuntu-pro-info" in messages[0]) |
3573 | + self.assertEqual(messages[0]["ubuntu-pro-info"], '"Initial data!"') |
3574 | + |
3575 | + with mock.patch("subprocess.run") as run_mock: |
3576 | + run_mock.return_value = mock.Mock( |
3577 | + stdout='"New data!"', |
3578 | + ) |
3579 | + plugin.run() |
3580 | + |
3581 | + run_mock.assert_called_once() |
3582 | + messages = self.mstore.get_pending_messages() |
3583 | + self.assertEqual(2, len(messages)) |
3584 | + self.assertEqual(messages[1]["ubuntu-pro-info"], '"New data!"') |
3585 | + |
3586 | + def test_persistence_reset(self): |
3587 | + """Resetting the plugin will allow a message with identical data to |
3588 | + be sent""" |
3589 | + plugin = UbuntuProInfo() |
3590 | + self.monitor.add(plugin) |
3591 | + data = '"Initial data!"' |
3592 | + |
3593 | + with mock.patch("subprocess.run") as run_mock: |
3594 | + run_mock.return_value = mock.Mock( |
3595 | + stdout=data, |
3596 | + ) |
3597 | + plugin.run() |
3598 | + |
3599 | + messages = self.mstore.get_pending_messages() |
3600 | + run_mock.assert_called_once() |
3601 | + self.assertEqual(1, len(messages)) |
3602 | + self.assertTrue("ubuntu-pro-info" in messages[0]) |
3603 | + self.assertEqual(messages[0]["ubuntu-pro-info"], data) |
3604 | + |
3605 | + plugin._reset() |
3606 | + |
3607 | + with mock.patch("subprocess.run") as run_mock: |
3608 | + run_mock.return_value = mock.Mock( |
3609 | + stdout=data, |
3610 | + ) |
3611 | + plugin.run() |
3612 | + |
3613 | + run_mock.assert_called_once() |
3614 | + messages = self.mstore.get_pending_messages() |
3615 | + self.assertEqual(2, len(messages)) |
3616 | + self.assertTrue("ubuntu-pro-info" in messages[1]) |
3617 | + self.assertEqual(messages[1]["ubuntu-pro-info"], data) |
3618 | diff --git a/landscape/client/manager/tests/test_usermanager.py b/landscape/client/manager/tests/test_usermanager.py |
3619 | index fe02d03..d4742c5 100644 |
3620 | --- a/landscape/client/manager/tests/test_usermanager.py |
3621 | +++ b/landscape/client/manager/tests/test_usermanager.py |
3622 | @@ -1,5 +1,6 @@ |
3623 | import os |
3624 | from unittest.mock import Mock |
3625 | +from unittest.mock import patch |
3626 | |
3627 | from landscape.client.manager.plugin import FAILED |
3628 | from landscape.client.manager.plugin import SUCCEEDED |
3629 | @@ -9,6 +10,7 @@ from landscape.client.monitor.usermonitor import UserMonitor |
3630 | from landscape.client.tests.helpers import LandscapeTest |
3631 | from landscape.client.tests.helpers import ManagerHelper |
3632 | from landscape.client.user.provider import UserManagementError |
3633 | +from landscape.client.user.tests.helpers import FakeSnapdUserManagement |
3634 | from landscape.client.user.tests.helpers import FakeUserManagement |
3635 | from landscape.client.user.tests.helpers import FakeUserProvider |
3636 | from landscape.lib.persist import Persist |
3637 | @@ -30,20 +32,25 @@ sbarnes:$1$q7sz09uw$q.A3526M/SHu8vUb.Jo1A/:13349:0:99999:7::: |
3638 | ) |
3639 | accepted_types = ["operation-result", "users"] |
3640 | self.broker_service.message_store.set_accepted_types(accepted_types) |
3641 | + self.plugins = [] |
3642 | |
3643 | def tearDown(self): |
3644 | super().tearDown() |
3645 | for plugin in self.plugins: |
3646 | plugin.stop() |
3647 | |
3648 | - def setup_environment(self, users, groups, shadow_file): |
3649 | + def setup_environment(self, users, groups, shadow_file, is_core=False): |
3650 | provider = FakeUserProvider( |
3651 | users=users, |
3652 | groups=groups, |
3653 | shadow_file=shadow_file, |
3654 | ) |
3655 | user_monitor = UserMonitor(provider=provider) |
3656 | - management = FakeUserManagement(provider=provider) |
3657 | + |
3658 | + if is_core: |
3659 | + management = FakeSnapdUserManagement(provider=provider) |
3660 | + else: |
3661 | + management = FakeUserManagement(provider=provider) |
3662 | user_manager = UserManager( |
3663 | management=management, |
3664 | shadow_file=shadow_file, |
3665 | @@ -378,6 +385,64 @@ class UserOperationsMessagingTest(UserGroupTestBase): |
3666 | result.addCallback(handle_callback) |
3667 | return result |
3668 | |
3669 | + @patch("landscape.client.manager.usermanager.IS_CORE", "1") |
3670 | + def test_add_user_event_on_core(self): |
3671 | + """ |
3672 | + When an C{add-user} event is received the user should be |
3673 | + added. Two messages should be generated: a C{users} message |
3674 | + with details about the change and an C{operation-result} with |
3675 | + details of the outcome of the operation. |
3676 | + """ |
3677 | + |
3678 | + def handle_callback(result): |
3679 | + messages = self.broker_service.message_store.get_pending_messages() |
3680 | + self.assertMessages( |
3681 | + messages, |
3682 | + [ |
3683 | + { |
3684 | + "type": "operation-result", |
3685 | + "status": SUCCEEDED, |
3686 | + "operation-id": 123, |
3687 | + "timestamp": 0, |
3688 | + "result-text": "add_user succeeded", |
3689 | + }, |
3690 | + { |
3691 | + "timestamp": 0, |
3692 | + "type": "users", |
3693 | + "operation-id": 123, |
3694 | + "create-users": [ |
3695 | + { |
3696 | + "home-phone": None, |
3697 | + "username": "john-doe", |
3698 | + "uid": 1000, |
3699 | + "enabled": True, |
3700 | + "location": None, |
3701 | + "work-phone": None, |
3702 | + "name": "john.doe@example.com", |
3703 | + "primary-gid": 1000, |
3704 | + }, |
3705 | + ], |
3706 | + }, |
3707 | + ], |
3708 | + ) |
3709 | + |
3710 | + shadow_file = self.makeFile("""st3v3nmw:*:19758:0:99999:7:::""") |
3711 | + self.setup_environment([], [], shadow_file, is_core=True) |
3712 | + |
3713 | + result = self.manager.dispatch_message( |
3714 | + { |
3715 | + "type": "add-user", |
3716 | + "username": "john-doe", |
3717 | + "email": "john.doe@example.com", |
3718 | + "sudoer": False, |
3719 | + "force-managed": True, |
3720 | + "operation-id": 123, |
3721 | + }, |
3722 | + ) |
3723 | + |
3724 | + result.addCallback(handle_callback) |
3725 | + return result |
3726 | + |
3727 | def test_edit_user_event(self): |
3728 | """ |
3729 | When a C{edit-user} message is received the user should be |
3730 | @@ -834,6 +899,71 @@ class UserOperationsMessagingTest(UserGroupTestBase): |
3731 | result.addCallback(handle_callback) |
3732 | return result |
3733 | |
3734 | + @patch("landscape.client.manager.usermanager.IS_CORE", "1") |
3735 | + def test_remove_user_event_on_core(self): |
3736 | + """ |
3737 | + When a C{remove-user} event is received, the user should be removed. |
3738 | + Two messages should be generated: a C{users} message with details |
3739 | + about the change and an C{operation-result} with details of the |
3740 | + outcome of the operation. |
3741 | + """ |
3742 | + |
3743 | + def handle_callback(result): |
3744 | + messages = self.broker_service.message_store.get_pending_messages() |
3745 | + self.assertEqual(len(messages), 3) |
3746 | + # Ignore the message created by plugin.run. |
3747 | + self.assertMessages( |
3748 | + [messages[2], messages[1]], |
3749 | + [ |
3750 | + { |
3751 | + "timestamp": 0, |
3752 | + "delete-users": ["john-doe"], |
3753 | + "type": "users", |
3754 | + "operation-id": 39, |
3755 | + }, |
3756 | + { |
3757 | + "type": "operation-result", |
3758 | + "status": SUCCEEDED, |
3759 | + "operation-id": 39, |
3760 | + "timestamp": 0, |
3761 | + "result-text": "remove_user succeeded", |
3762 | + }, |
3763 | + ], |
3764 | + ) |
3765 | + |
3766 | + users = [ |
3767 | + ( |
3768 | + "john-doe", |
3769 | + "x", |
3770 | + 1000, |
3771 | + 1000, |
3772 | + "john.doe@example.com,BtrGAhK,,", |
3773 | + "/home/user", |
3774 | + "/bin/zsh", |
3775 | + ), |
3776 | + ( |
3777 | + "jane-doe", |
3778 | + "x", |
3779 | + 1001, |
3780 | + 1001, |
3781 | + "jane.doe@example.com,BtrGAhK,,", |
3782 | + "/home/user", |
3783 | + "/bin/zsh", |
3784 | + ), |
3785 | + ] |
3786 | + shadow_file = self.makeFile("""st3v3nmw:*:19758:0:99999:7:::""") |
3787 | + self.setup_environment(users, [], shadow_file, is_core=True) |
3788 | + result = self.manager.dispatch_message( |
3789 | + { |
3790 | + "username": "john-doe", |
3791 | + "delete-home": True, |
3792 | + "type": "remove-user", |
3793 | + "operation-id": 39, |
3794 | + }, |
3795 | + ) |
3796 | + result.addCallback(handle_callback) |
3797 | + return result |
3798 | + |
3799 | def test_lock_user_event(self): |
3800 | """ |
3801 | When a C{lock-user} event is received the user should be |
3802 | diff --git a/landscape/client/manager/ubuntuproinfo.py b/landscape/client/manager/ubuntuproinfo.py |
3803 | new file mode 100644 |
3804 | index 0000000..1881c34 |
3805 | --- /dev/null |
3806 | +++ b/landscape/client/manager/ubuntuproinfo.py |
3807 | @@ -0,0 +1,102 @@ |
3808 | +import json |
3809 | +import subprocess |
3810 | +from pathlib import Path |
3811 | + |
3812 | +from landscape.client import IS_CORE |
3813 | +from landscape.client.manager.plugin import ManagerPlugin |
3814 | +from landscape.lib.persist import Persist |
3815 | + |
3816 | + |
3817 | +class UbuntuProInfo(ManagerPlugin): |
3818 | + """ |
3819 | + Plugin that captures and reports Ubuntu Pro registration |
3820 | + information. |
3821 | + |
3822 | + We use the `pro` CLI with output formatted as JSON. This is sent |
3823 | + as-is and parsed by Landscape Server because the JSON content is |
3824 | + considered "Experimental" and we don't want to have to change in |
3825 | + both Client and Server in the event that the format changes. |
3826 | + """ |
3827 | + |
3828 | + message_type = "ubuntu-pro-info" |
3829 | + run_interval = 900 # 15 minutes |
3830 | + |
3831 | + def register(self, registry): |
3832 | + super().register(registry) |
3833 | + self._persist_filename = Path( |
3834 | + self.registry.config.data_path, |
3835 | + "ubuntu-pro-info.bpickle", |
3836 | + ) |
3837 | + self._persist = Persist(filename=self._persist_filename) |
3838 | + self.call_on_accepted(self.message_type, self.send_message) |
3839 | + |
3840 | + def run(self): |
3841 | + return self.registry.broker.call_if_accepted( |
3842 | + self.message_type, |
3843 | + self.send_message, |
3844 | + ) |
3845 | + |
3846 | + def send_message(self): |
3847 | + """Send a message to the broker if the data has changed since the last |
3848 | + call""" |
3849 | + result = self.get_data() |
3850 | + if not result: |
3851 | + return |
3852 | + message = {"type": self.message_type, "ubuntu-pro-info": result} |
3853 | + return self.registry.broker.send_message(message, self._session_id) |
3854 | + |
3855 | + def get_data(self): |
3856 | + """Persist data to avoid sending messages if result hasn't changed""" |
3857 | + ubuntu_pro_info = get_ubuntu_pro_info() |
3858 | + |
3859 | + if self._persist.get("data") != ubuntu_pro_info: |
3860 | + self._persist.set("data", ubuntu_pro_info) |
3861 | + return json.dumps(ubuntu_pro_info, separators=(",", ":")) |
3862 | + |
3863 | + def _reset(self): |
3864 | + """Reset the persist.""" |
3865 | + self._persist.remove("data") |
3866 | + |
3867 | + |
3868 | +def get_ubuntu_pro_info() -> dict: |
3869 | + """Query ua tools for Ubuntu Pro status as JSON, parsing it to a dict. |
3870 | + |
3871 | + If we are running on Ubuntu Core, Pro does not exist - returns a message |
3872 | + indicating this. |
3873 | + """ |
3874 | + if IS_CORE: |
3875 | + return _ubuntu_pro_error_message( |
3876 | + "Ubuntu Pro is not available on Ubuntu Core.", |
3877 | + "core-unsupported", |
3878 | + ) |
3879 | + |
3880 | + try: |
3881 | + completed_process = subprocess.run( |
3882 | + ["pro", "status", "--format", "json"], |
3883 | + encoding="utf8", |
3884 | + stdout=subprocess.PIPE, |
3885 | + ) |
3886 | + except FileNotFoundError: |
3887 | + return _ubuntu_pro_error_message( |
3888 | + "ubuntu pro tools not found.", |
3889 | + "tools-error", |
3890 | + ) |
3891 | + else: |
3892 | + return json.loads(completed_process.stdout) |
3893 | + |
3894 | + |
3895 | +def _ubuntu_pro_error_message(message: str, code: str) -> dict: |
3896 | + """Marshall `message` and `code` into a format matching that expected from |
3897 | + an error from ua tools. |
3898 | + """ |
3899 | + return { |
3900 | + "errors": [ |
3901 | + { |
3902 | + "message": message, |
3903 | + "message_code": code, |
3904 | + "service": None, |
3905 | + "type": "system", |
3906 | + }, |
3907 | + ], |
3908 | + "result": "failure", |
3909 | + } |
3910 | diff --git a/landscape/client/manager/usermanager.py b/landscape/client/manager/usermanager.py |
3911 | index 8984ccf..5e0608d 100644 |
3912 | --- a/landscape/client/manager/usermanager.py |
3913 | +++ b/landscape/client/manager/usermanager.py |
3914 | @@ -1,10 +1,12 @@ |
3915 | import logging |
3916 | |
3917 | +from landscape.client import IS_CORE |
3918 | from landscape.client.amp import ComponentConnector |
3919 | from landscape.client.amp import ComponentPublisher |
3920 | from landscape.client.amp import remote |
3921 | from landscape.client.manager.plugin import ManagerPlugin |
3922 | from landscape.client.monitor.usermonitor import RemoteUserMonitorConnector |
3923 | +from landscape.client.user.management import SnapdUserManagement |
3924 | from landscape.client.user.management import UserManagement |
3925 | |
3926 | |
3927 | @@ -13,7 +15,14 @@ class UserManager(ManagerPlugin): |
3928 | name = "usermanager" |
3929 | |
3930 | def __init__(self, management=None, shadow_file="/etc/shadow"): |
3931 | - self._management = management or UserManagement() |
3932 | + if IS_CORE: |
3933 | + management = management or SnapdUserManagement() |
3934 | + shadow_file = shadow_file or "/var/lib/extrausers/shadow" |
3935 | + else: |
3936 | + management = management or UserManagement() |
3937 | + shadow_file = shadow_file |
3938 | + |
3939 | + self._management = management |
3940 | self._shadow_file = shadow_file |
3941 | self._message_types = { |
3942 | "add-user": self._add_user, |
3943 | @@ -107,16 +116,7 @@ class UserManager(ManagerPlugin): |
3944 | |
3945 | def _add_user(self, message): |
3946 | """Run an C{add-user} operation.""" |
3947 | - return self._management.add_user( |
3948 | - message["username"], |
3949 | - message["name"], |
3950 | - message["password"], |
3951 | - message["require-password-reset"], |
3952 | - message["primary-group-name"], |
3953 | - message["location"], |
3954 | - message["work-number"], |
3955 | - message["home-number"], |
3956 | - ) |
3957 | + return self._management.add_user(message) |
3958 | |
3959 | def _edit_user(self, message): |
3960 | """Run an C{edit-user} operation.""" |
3961 | @@ -140,10 +140,7 @@ class UserManager(ManagerPlugin): |
3962 | |
3963 | def _remove_user(self, message): |
3964 | """Run a C{remove-user} operation.""" |
3965 | - return self._management.remove_user( |
3966 | - message["username"], |
3967 | - message["delete-home"], |
3968 | - ) |
3969 | + return self._management.remove_user(message) |
3970 | |
3971 | def _add_group(self, message): |
3972 | """Run an C{add-group} operation.""" |
3973 | diff --git a/landscape/client/monitor/computerinfo.py b/landscape/client/monitor/computerinfo.py |
3974 | index dfda40b..c1b353e 100644 |
3975 | --- a/landscape/client/monitor/computerinfo.py |
3976 | +++ b/landscape/client/monitor/computerinfo.py |
3977 | @@ -5,12 +5,13 @@ from twisted.internet.defer import inlineCallbacks |
3978 | from twisted.internet.defer import returnValue |
3979 | |
3980 | from landscape.client.monitor.plugin import MonitorPlugin |
3981 | +from landscape.client.snap_utils import get_snap_info |
3982 | from landscape.lib.cloud import fetch_ec2_meta_data |
3983 | from landscape.lib.fetch import fetch_async |
3984 | from landscape.lib.fs import read_text_file |
3985 | -from landscape.lib.lsb_release import LSB_RELEASE_FILENAME |
3986 | -from landscape.lib.lsb_release import parse_lsb_release |
3987 | from landscape.lib.network import get_fqdn |
3988 | +from landscape.lib.os_release import get_os_filename |
3989 | +from landscape.lib.os_release import parse_os_release |
3990 | |
3991 | METADATA_RETRY_MAX = 3 # Number of retries to get EC2 meta-data |
3992 | |
3993 | @@ -29,13 +30,13 @@ class ComputerInfo(MonitorPlugin): |
3994 | self, |
3995 | get_fqdn=get_fqdn, |
3996 | meminfo_filename="/proc/meminfo", |
3997 | - lsb_release_filename=LSB_RELEASE_FILENAME, |
3998 | + os_release_filename=get_os_filename(), |
3999 | root_path="/", |
4000 | fetch_async=fetch_async, |
4001 | ): |
4002 | self._get_fqdn = get_fqdn |
4003 | self._meminfo_filename = meminfo_filename |
4004 | - self._lsb_release_filename = lsb_release_filename |
4005 | + self._os_release_filename = os_release_filename |
4006 | self._root_path = root_path |
4007 | self._cloud_instance_metadata = None |
4008 | self._cloud_retries = 0 |
4009 | @@ -59,6 +60,11 @@ class ComputerInfo(MonitorPlugin): |
4010 | self.send_cloud_instance_metadata_message, |
4011 | True, |
4012 | ) |
4013 | + self.call_on_accepted( |
4014 | + "snap-info", |
4015 | + self.send_snap_message, |
4016 | + True, |
4017 | + ) |
4018 | |
4019 | def send_computer_message(self, urgent=False): |
4020 | message = self._create_computer_info_message() |
4021 | @@ -96,6 +102,17 @@ class ComputerInfo(MonitorPlugin): |
4022 | urgent=urgent, |
4023 | ) |
4024 | |
4025 | + def send_snap_message(self, urgent=False): |
4026 | + message = self._create_snap_info_message() |
4027 | + if message: |
4028 | + message["type"] = "snap-info" |
4029 | + logging.info("Queueing message with updated snap info.") |
4030 | + self.registry.broker.send_message( |
4031 | + message, |
4032 | + self._session_id, |
4033 | + urgent=urgent, |
4034 | + ) |
4035 | + |
4036 | def exchange(self, urgent=False): |
4037 | broker = self.registry.broker |
4038 | broker.call_if_accepted( |
4039 | @@ -113,6 +130,11 @@ class ComputerInfo(MonitorPlugin): |
4040 | self.send_cloud_instance_metadata_message, |
4041 | urgent, |
4042 | ) |
4043 | + broker.call_if_accepted( |
4044 | + "snap-info", |
4045 | + self.send_snap_message, |
4046 | + urgent, |
4047 | + ) |
4048 | |
4049 | def _create_computer_info_message(self): |
4050 | message = {} |
4051 | @@ -160,7 +182,7 @@ class ComputerInfo(MonitorPlugin): |
4052 | def _get_distribution_info(self): |
4053 | """Get details about the distribution.""" |
4054 | message = {} |
4055 | - message.update(parse_lsb_release(self._lsb_release_filename)) |
4056 | + message.update(parse_os_release(self._os_release_filename)) |
4057 | return message |
4058 | |
4059 | @inlineCallbacks |
4060 | @@ -171,7 +193,6 @@ class ComputerInfo(MonitorPlugin): |
4061 | self._cloud_instance_metadata is None |
4062 | and self._cloud_retries < METADATA_RETRY_MAX |
4063 | ): |
4064 | - |
4065 | self._cloud_instance_metadata = yield self._fetch_ec2_meta_data() |
4066 | message = self._cloud_instance_metadata |
4067 | returnValue(message) |
4068 | @@ -196,3 +217,13 @@ class ComputerInfo(MonitorPlugin): |
4069 | deferred.addCallback(log_success) |
4070 | deferred.addErrback(log_no_meta_data_found) |
4071 | return deferred |
4072 | + |
4073 | + def _create_snap_info_message(self): |
4074 | + """Create message with the snapd serial metadata.""" |
4075 | + message = {} |
4076 | + snap_info = get_snap_info() |
4077 | + if snap_info: |
4078 | + self._add_if_new(message, "brand", snap_info["brand"]) |
4079 | + self._add_if_new(message, "model", snap_info["model"]) |
4080 | + self._add_if_new(message, "serial", snap_info["serial"]) |
4081 | + return message |
4082 | diff --git a/landscape/client/monitor/config.py b/landscape/client/monitor/config.py |
4083 | index e6c347a..ebf54eb 100644 |
4084 | --- a/landscape/client/monitor/config.py |
4085 | +++ b/landscape/client/monitor/config.py |
4086 | @@ -20,10 +20,10 @@ ALL_PLUGINS = [ |
4087 | "SwiftUsage", |
4088 | "CephUsage", |
4089 | "ComputerTags", |
4090 | - "UbuntuProInfo", |
4091 | "LivePatch", |
4092 | "UbuntuProRebootRequired", |
4093 | "SnapMonitor", |
4094 | + "SnapServicesMonitor", |
4095 | ] |
4096 | |
4097 | |
4098 | diff --git a/landscape/client/monitor/processorinfo.py b/landscape/client/monitor/processorinfo.py |
4099 | index 2fdacc8..1081ced 100644 |
4100 | --- a/landscape/client/monitor/processorinfo.py |
4101 | +++ b/landscape/client/monitor/processorinfo.py |
4102 | @@ -181,30 +181,43 @@ class ARMMessageFactory: |
4103 | def create_message(self): |
4104 | """Returns a list containing information about each processor.""" |
4105 | processors = [] |
4106 | - file = open(self._source_filename) |
4107 | + regexp = re.compile(r"(?P<key>.*?)\s*:\s*(?P<value>.*)") |
4108 | + current = {} |
4109 | |
4110 | - try: |
4111 | - regexp = re.compile(r"(?P<key>.*?)\s*:\s*(?P<value>.*)") |
4112 | - current = {} |
4113 | + with open(self._source_filename) as fp: |
4114 | + for line in fp: |
4115 | + line = line.strip() |
4116 | + |
4117 | + if not line: |
4118 | + if current: |
4119 | + processors.append(current.copy()) |
4120 | + current = {} |
4121 | + |
4122 | + continue |
4123 | |
4124 | - for line in file: |
4125 | match = regexp.match(line.strip()) |
4126 | + |
4127 | if match: |
4128 | key = match.group("key") |
4129 | value = match.group("value") |
4130 | |
4131 | - if key == "Processor": |
4132 | - # ARM doesn't support SMP, thus no processor-id in |
4133 | - # the cpuinfo |
4134 | - current["processor-id"] = 0 |
4135 | + if key == "processor": |
4136 | + current["processor-id"] = int(value) |
4137 | + if "model" not in current: |
4138 | + current["model"] = "arm" |
4139 | + elif key == "Processor": |
4140 | current["model"] = value |
4141 | elif key == "Cache size": |
4142 | current["cache-size"] = int(value) |
4143 | |
4144 | - if current: |
4145 | - processors.append(current) |
4146 | - finally: |
4147 | - file.close() |
4148 | + if current: |
4149 | + processors.append(current) |
4150 | + |
4151 | + # Older ARM machines may not have processor-ids, but we need them, so |
4152 | + # we set missing ones to 0. |
4153 | + for processor in processors: |
4154 | + if "processor-id" not in processor: |
4155 | + processor["processor-id"] = 0 |
4156 | |
4157 | return processors |
4158 | |
4159 | @@ -357,6 +370,8 @@ class RISCVMessageFactory: |
4160 | |
4161 | if key == "processor": |
4162 | current = {"processor-id": int(parts[1].strip())} |
4163 | + # A placeholder in case there is no model provided. |
4164 | + current["model"] = "riscv" |
4165 | processors.append(current) |
4166 | elif key == "isa": |
4167 | current["vendor"] = parts[1].strip() |
4168 | diff --git a/landscape/client/monitor/service.py b/landscape/client/monitor/service.py |
4169 | index 1822360..c0971f3 100644 |
4170 | --- a/landscape/client/monitor/service.py |
4171 | +++ b/landscape/client/monitor/service.py |
4172 | @@ -46,14 +46,17 @@ class MonitorService(LandscapeService): |
4173 | try: |
4174 | plugin = namedClass( |
4175 | "landscape.client.monitor." |
4176 | - f"{plugin_name.lower()}.{plugin_name}" |
4177 | + f"{plugin_name.lower()}.{plugin_name}", |
4178 | ) |
4179 | plugins.append(plugin()) |
4180 | except ModuleNotFoundError: |
4181 | logging.warning( |
4182 | - "Invalid monitor plugin specified: '{}'. " |
4183 | + f"Invalid monitor plugin specified: '{plugin_name}'. " |
4184 | "See `example.conf` for a full list of monitor plugins.", |
4185 | - plugin_name, |
4186 | + ) |
4187 | + except Exception as exc: |
4188 | + logging.warning( |
4189 | + f"Unable to load monitor plugin '{plugin_name}': {exc}", |
4190 | ) |
4191 | |
4192 | return plugins |
4193 | diff --git a/landscape/client/monitor/snapmonitor.py b/landscape/client/monitor/snapmonitor.py |
4194 | index 6a55a68..ef5c94e 100644 |
4195 | --- a/landscape/client/monitor/snapmonitor.py |
4196 | +++ b/landscape/client/monitor/snapmonitor.py |
4197 | @@ -1,8 +1,9 @@ |
4198 | +import json |
4199 | import logging |
4200 | |
4201 | +from landscape.client import snap_http |
4202 | from landscape.client.monitor.plugin import DataWatcher |
4203 | -from landscape.client.snap.http import SnapdHttpException |
4204 | -from landscape.client.snap.http import SnapHttp |
4205 | +from landscape.client.snap_http import SnapdHttpException |
4206 | from landscape.message_schemas.server_bound import SNAPS |
4207 | |
4208 | |
4209 | @@ -13,31 +14,41 @@ class SnapMonitor(DataWatcher): |
4210 | persist_name = message_type |
4211 | scope = "snaps" |
4212 | |
4213 | - def __init__(self, *args, **kwargs): |
4214 | - super().__init__(*args, **kwargs) |
4215 | - |
4216 | - self._snap_http = SnapHttp() |
4217 | - |
4218 | def register(self, registry): |
4219 | self.config = registry.config |
4220 | # The default interval is 30 minutes. |
4221 | self.run_interval = self.config.snap_monitor_interval |
4222 | |
4223 | - super(SnapMonitor, self).register(registry) |
4224 | + super().register(registry) |
4225 | |
4226 | def get_data(self): |
4227 | try: |
4228 | - snaps = self._snap_http.get_snaps() |
4229 | + snaps = snap_http.list().result |
4230 | except SnapdHttpException as e: |
4231 | logging.error(f"Unable to list installed snaps: {e}") |
4232 | return |
4233 | |
4234 | + for i in range(len(snaps)): |
4235 | + snap_name = snaps[i]["name"] |
4236 | + try: |
4237 | + config = snap_http.get_conf(snap_name).result |
4238 | + except SnapdHttpException as e: |
4239 | + logging.warning( |
4240 | + f"Unable to get config for snap {snap_name}: {e}", |
4241 | + ) |
4242 | + config = {} |
4243 | + |
4244 | + snaps[i]["config"] = json.dumps(config) |
4245 | + |
4246 | # We get a lot of extra info from snapd. To avoid caching it all |
4247 | # or invalidating the cache on timestamp changes, we use Message |
4248 | # coercion to strip out the unnecessaries, then sort on the snap |
4249 | # IDs to order the list. |
4250 | data = SNAPS.coerce( |
4251 | - {"type": "snaps", "snaps": {"installed": snaps["result"]}}, |
4252 | + { |
4253 | + "type": "snaps", |
4254 | + "snaps": {"installed": snaps}, |
4255 | + }, |
4256 | ) |
4257 | data["snaps"]["installed"].sort(key=lambda x: x["id"]) |
4258 | |
4259 | diff --git a/landscape/client/monitor/snapservicesmonitor.py b/landscape/client/monitor/snapservicesmonitor.py |
4260 | new file mode 100644 |
4261 | index 0000000..66d5655 |
4262 | --- /dev/null |
4263 | +++ b/landscape/client/monitor/snapservicesmonitor.py |
4264 | @@ -0,0 +1,28 @@ |
4265 | +import logging |
4266 | + |
4267 | +from landscape.client import snap_http |
4268 | +from landscape.client.monitor.plugin import DataWatcher |
4269 | +from landscape.client.snap_http import SnapdHttpException |
4270 | + |
4271 | + |
4272 | +class SnapServicesMonitor(DataWatcher): |
4273 | + |
4274 | + message_type = "snap-services" |
4275 | + message_key = "services" |
4276 | + persist_name = message_type |
4277 | + scope = "snaps" |
4278 | + |
4279 | + def register(self, registry): |
4280 | + self.config = registry.config |
4281 | + self.run_interval = 60 # 1 minute |
4282 | + super().register(registry) |
4283 | + |
4284 | + def get_data(self): |
4285 | + try: |
4286 | + services = snap_http.get_apps(services_only=True).result |
4287 | + except SnapdHttpException as e: |
4288 | + logging.warning(f"Unable to list services: {e}") |
4289 | + services = [] |
4290 | + services.sort(key=lambda x: x["name"]) |
4291 | + |
4292 | + return {"running": services} |
4293 | diff --git a/landscape/client/monitor/tests/test_computerinfo.py b/landscape/client/monitor/tests/test_computerinfo.py |
4294 | index d43a9b7..0742316 100644 |
4295 | --- a/landscape/client/monitor/tests/test_computerinfo.py |
4296 | +++ b/landscape/client/monitor/tests/test_computerinfo.py |
4297 | @@ -14,12 +14,19 @@ from landscape.lib.fetch import HTTPCodeError |
4298 | from landscape.lib.fetch import PyCurlError |
4299 | from landscape.lib.fs import create_text_file |
4300 | |
4301 | -SAMPLE_LSB_RELEASE = ( |
4302 | - "DISTRIB_ID=Ubuntu\n" |
4303 | - "DISTRIB_RELEASE=6.06\n" |
4304 | - "DISTRIB_CODENAME=dapper\n" |
4305 | - 'DISTRIB_DESCRIPTION="Ubuntu 6.06.1 LTS"\n' |
4306 | -) |
4307 | +SAMPLE_OS_RELEASE = """PRETTY_NAME="Ubuntu 22.04.3 LTS" |
4308 | +NAME="Ubuntu" |
4309 | +VERSION_ID="22.04" |
4310 | +VERSION="22.04.3 LTS (Jammy Jellyfish)" |
4311 | +VERSION_CODENAME=codename |
4312 | +ID=ubuntu |
4313 | +ID_LIKE=debian |
4314 | +HOME_URL="https://www.ubuntu.com/" |
4315 | +SUPPORT_URL="https://help.ubuntu.com/" |
4316 | +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" |
4317 | +PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" |
4318 | +UBUNTU_CODENAME=codename |
4319 | +""" |
4320 | |
4321 | |
4322 | def get_fqdn(): |
4323 | @@ -27,7 +34,6 @@ def get_fqdn(): |
4324 | |
4325 | |
4326 | class ComputerInfoTest(LandscapeTest): |
4327 | - |
4328 | helpers = [MonitorHelper] |
4329 | |
4330 | sample_memory_info = """ |
4331 | @@ -58,7 +64,7 @@ VmallocChunk: 107432 kB |
4332 | |
4333 | def setUp(self): |
4334 | LandscapeTest.setUp(self) |
4335 | - self.lsb_release_filename = self.makeFile(SAMPLE_LSB_RELEASE) |
4336 | + self.os_release_filename = self.makeFile(SAMPLE_OS_RELEASE) |
4337 | self.query_results = {} |
4338 | |
4339 | def fetch_stub(url, **kwargs): |
4340 | @@ -100,7 +106,7 @@ VmallocChunk: 107432 kB |
4341 | messages = self.mstore.get_pending_messages() |
4342 | self.assertEqual(len(messages), 1) |
4343 | self.assertEqual(messages[0]["type"], "computer-info") |
4344 | - self.assertNotEquals(len(messages[0]["hostname"]), 0) |
4345 | + self.assertNotEqual(len(messages[0]["hostname"]), 0) |
4346 | self.assertTrue(re.search(r"\w", messages[0]["hostname"])) |
4347 | |
4348 | def test_only_report_changed_hostnames(self): |
4349 | @@ -223,16 +229,16 @@ VmallocChunk: 107432 kB |
4350 | the distribution data reported by the plugin. |
4351 | """ |
4352 | self.mstore.set_accepted_types(["distribution-info"]) |
4353 | - plugin = ComputerInfo(lsb_release_filename=self.lsb_release_filename) |
4354 | + plugin = ComputerInfo(os_release_filename=self.os_release_filename) |
4355 | self.monitor.add(plugin) |
4356 | |
4357 | plugin.exchange() |
4358 | message = self.mstore.get_pending_messages()[0] |
4359 | self.assertEqual(message["type"], "distribution-info") |
4360 | self.assertEqual(message["distributor-id"], "Ubuntu") |
4361 | - self.assertEqual(message["description"], "Ubuntu 6.06.1 LTS") |
4362 | - self.assertEqual(message["release"], "6.06") |
4363 | - self.assertEqual(message["code-name"], "dapper") |
4364 | + self.assertEqual(message["description"], "Ubuntu 22.04.3 LTS") |
4365 | + self.assertEqual(message["release"], "22.04") |
4366 | + self.assertEqual(message["code-name"], "codename") |
4367 | |
4368 | def test_distribution_reported_only_once(self): |
4369 | """ |
4370 | @@ -258,23 +264,23 @@ VmallocChunk: 107432 kB |
4371 | the server. |
4372 | """ |
4373 | self.mstore.set_accepted_types(["distribution-info"]) |
4374 | - plugin = ComputerInfo(lsb_release_filename=self.lsb_release_filename) |
4375 | + plugin = ComputerInfo(os_release_filename=self.os_release_filename) |
4376 | self.monitor.add(plugin) |
4377 | |
4378 | plugin.exchange() |
4379 | message = self.mstore.get_pending_messages()[0] |
4380 | self.assertEqual(message["type"], "distribution-info") |
4381 | self.assertEqual(message["distributor-id"], "Ubuntu") |
4382 | - self.assertEqual(message["description"], "Ubuntu 6.06.1 LTS") |
4383 | - self.assertEqual(message["release"], "6.06") |
4384 | - self.assertEqual(message["code-name"], "dapper") |
4385 | + self.assertEqual(message["description"], "Ubuntu 22.04.3 LTS") |
4386 | + self.assertEqual(message["release"], "22.04") |
4387 | + self.assertEqual(message["code-name"], "codename") |
4388 | |
4389 | - plugin._lsb_release_filename = self.makeFile( |
4390 | + plugin._os_release_filename = self.makeFile( |
4391 | """\ |
4392 | -DISTRIB_ID=Ubuntu |
4393 | -DISTRIB_RELEASE=6.10 |
4394 | -DISTRIB_CODENAME=edgy |
4395 | -DISTRIB_DESCRIPTION="Ubuntu 6.10" |
4396 | +NAME=Ubuntu |
4397 | +VERSION_ID=6.10 |
4398 | +VERSION_CODENAME=edgy |
4399 | +PRETTY_NAME="Ubuntu 6.10" |
4400 | """, |
4401 | ) |
4402 | plugin.exchange() |
4403 | @@ -287,25 +293,25 @@ DISTRIB_DESCRIPTION="Ubuntu 6.10" |
4404 | |
4405 | def test_unknown_distribution_key(self): |
4406 | self.mstore.set_accepted_types(["distribution-info"]) |
4407 | - lsb_release_filename = self.makeFile( |
4408 | + os_release_filename = self.makeFile( |
4409 | """\ |
4410 | -DISTRIB_ID=Ubuntu |
4411 | -DISTRIB_RELEASE=6.10 |
4412 | -DISTRIB_CODENAME=edgy |
4413 | -DISTRIB_DESCRIPTION="Ubuntu 6.10" |
4414 | +NAME=Ubuntu |
4415 | +VERSION_ID=22.04 |
4416 | +VERSION_CODENAME=codename |
4417 | +PRETTY_NAME="Ubuntu 22.04.3 LTS" |
4418 | DISTRIB_NEW_UNEXPECTED_KEY=ooga |
4419 | """, |
4420 | ) |
4421 | - plugin = ComputerInfo(lsb_release_filename=lsb_release_filename) |
4422 | + plugin = ComputerInfo(os_release_filename=os_release_filename) |
4423 | self.monitor.add(plugin) |
4424 | |
4425 | plugin.exchange() |
4426 | message = self.mstore.get_pending_messages()[0] |
4427 | self.assertEqual(message["type"], "distribution-info") |
4428 | self.assertEqual(message["distributor-id"], "Ubuntu") |
4429 | - self.assertEqual(message["description"], "Ubuntu 6.10") |
4430 | - self.assertEqual(message["release"], "6.10") |
4431 | - self.assertEqual(message["code-name"], "edgy") |
4432 | + self.assertEqual(message["description"], "Ubuntu 22.04.3 LTS") |
4433 | + self.assertEqual(message["release"], "22.04") |
4434 | + self.assertEqual(message["code-name"], "codename") |
4435 | |
4436 | def test_resynchronize(self): |
4437 | """ |
4438 | @@ -317,7 +323,7 @@ DISTRIB_NEW_UNEXPECTED_KEY=ooga |
4439 | plugin = ComputerInfo( |
4440 | get_fqdn=get_fqdn, |
4441 | meminfo_filename=meminfo_filename, |
4442 | - lsb_release_filename=self.lsb_release_filename, |
4443 | + os_release_filename=self.os_release_filename, |
4444 | root_path=self.makeDir(), |
4445 | fetch_async=self.fetch_func, |
4446 | ) |
4447 | @@ -335,10 +341,10 @@ DISTRIB_NEW_UNEXPECTED_KEY=ooga |
4448 | |
4449 | dist_info = { |
4450 | "type": "distribution-info", |
4451 | - "code-name": "dapper", |
4452 | - "description": "Ubuntu 6.06.1 LTS", |
4453 | + "code-name": "codename", |
4454 | + "description": "Ubuntu 22.04.3 LTS", |
4455 | "distributor-id": "Ubuntu", |
4456 | - "release": "6.06", |
4457 | + "release": "22.04", |
4458 | } |
4459 | self.assertMessages( |
4460 | self.mstore.get_pending_messages(), |
4461 | @@ -553,3 +559,46 @@ DISTRIB_NEW_UNEXPECTED_KEY=ooga |
4462 | }, |
4463 | result, |
4464 | ) |
4465 | + |
4466 | + @mock.patch("landscape.client.snap_utils.get_assertions") |
4467 | + def test_snap_info(self, mock_get_assertions): |
4468 | + """Test getting the snap info message.""" |
4469 | + mock_get_assertions.return_value = [ |
4470 | + { |
4471 | + "authority-id": "canonical", |
4472 | + "brand-id": "canonical", |
4473 | + "model": "pc-amd64", |
4474 | + "serial": "03961d5d-26e5-443f-838d-6db046126bea", |
4475 | + }, |
4476 | + ] |
4477 | + |
4478 | + self.mstore.set_accepted_types(["snap-info"]) |
4479 | + plugin = ComputerInfo(fetch_async=self.fetch_func) |
4480 | + self.monitor.add(plugin) |
4481 | + plugin.exchange() |
4482 | + messages = self.mstore.get_pending_messages() |
4483 | + self.assertEqual(len(messages), 1) |
4484 | + self.assertEqual(messages[0]["type"], "snap-info") |
4485 | + self.assertEqual(messages[0]["brand"], "canonical") |
4486 | + self.assertEqual(messages[0]["model"], "pc-amd64") |
4487 | + self.assertEqual( |
4488 | + messages[0]["serial"], |
4489 | + "03961d5d-26e5-443f-838d-6db046126bea", |
4490 | + ) |
4491 | + |
4492 | + @mock.patch("landscape.client.snap_utils.get_assertions") |
4493 | + def test_snap_info_no_results(self, mock_get_assertions): |
4494 | + """Test getting the snap info message when there are no results. |
4495 | + |
4496 | + No results can happen when: |
4497 | + - A SnapdHttpException occurs |
4498 | + - No serial assertion is found |
4499 | + """ |
4500 | + mock_get_assertions.return_value = None |
4501 | + |
4502 | + self.mstore.set_accepted_types(["snap-info"]) |
4503 | + plugin = ComputerInfo(fetch_async=self.fetch_func) |
4504 | + self.monitor.add(plugin) |
4505 | + plugin.exchange() |
4506 | + messages = self.mstore.get_pending_messages() |
4507 | + self.assertEqual(len(messages), 0) |
4508 | diff --git a/landscape/client/monitor/tests/test_processorinfo.py b/landscape/client/monitor/tests/test_processorinfo.py |
4509 | index 4a2bb8e..ffe47ce 100644 |
4510 | --- a/landscape/client/monitor/tests/test_processorinfo.py |
4511 | +++ b/landscape/client/monitor/tests/test_processorinfo.py |
4512 | @@ -30,8 +30,7 @@ class ProcessorInfoTest(LandscapeTest): |
4513 | self.assertTrue(len(message["processors"]) > 0) |
4514 | |
4515 | for processor in message["processors"]: |
4516 | - self.assertTrue("processor-id" in processor) |
4517 | - self.assertTrue("model" in processor) |
4518 | + self.assertIn("processor-id", processor) |
4519 | |
4520 | def test_call_on_accepted(self): |
4521 | """ |
4522 | @@ -326,6 +325,22 @@ Hardware : Foundation-v8A |
4523 | ) |
4524 | self.assertEqual(processor_0["processor-id"], 0) |
4525 | |
4526 | + def test_no_model_default(self): |
4527 | + """If there is no 'Processor' field, then the model defaults to |
4528 | + 'arm'. |
4529 | + """ |
4530 | + filename = self.makeFile("processor : 0\n") |
4531 | + plugin = ProcessorInfo( |
4532 | + machine_name="aarch64", |
4533 | + source_filename=filename, |
4534 | + ) |
4535 | + message = plugin.create_message() |
4536 | + self.assertEqual(message["type"], "processor-info") |
4537 | + self.assertTrue(len(message["processors"]) == 1) |
4538 | + |
4539 | + processor_0 = message["processors"][0] |
4540 | + self.assertEqual(processor_0["model"], "arm") |
4541 | + |
4542 | |
4543 | class SparcMessageTest(LandscapeTest): |
4544 | """Tests for sparc-specific message builder.""" |
4545 | @@ -631,3 +646,60 @@ bogomips : 1198.25 |
4546 | |
4547 | self.mstore.set_accepted_types(["processor-info"]) |
4548 | self.assertMessages(list(self.mstore.get_pending_messages()), []) |
4549 | + |
4550 | + |
4551 | +class RISCVMessageTest(LandscapeTest): |
4552 | + """Test for RISCV-specific message handling.""" |
4553 | + |
4554 | + helpers = [MonitorHelper] |
4555 | + |
4556 | + VISION_FIVE = """ |
4557 | +processor : 0 |
4558 | +hart : 0 |
4559 | +isa : rv64imafdc |
4560 | +mmu : sv39 |
4561 | +uarch : sifive,u74-mc |
4562 | +mvendorid : 0x489 |
4563 | +marchid : 0x8000000000000007 |
4564 | +mimpid : 0x20190531 |
4565 | + |
4566 | +processor : 1 |
4567 | +hart : 1 |
4568 | +isa : rv64imafdc |
4569 | +mmu : sv39 |
4570 | +uarch : sifive,u74-mc |
4571 | +mvendorid : 0x489 |
4572 | +marchid : 0x8000000000000007 |
4573 | +mimpid : 0x20190531 |
4574 | + |
4575 | + """ |
4576 | + |
4577 | + def setUp(self): |
4578 | + super().setUp() |
4579 | + |
4580 | + self.mstore.set_accepted_types(["processor-info"]) |
4581 | + |
4582 | + def test_read_sample_data(self): |
4583 | + """Ensure the plugin can parse a simple /proc/cpuinfo from a VisionFive |
4584 | + v1. |
4585 | + """ |
4586 | + filename = self.makeFile(self.VISION_FIVE) |
4587 | + plugin = ProcessorInfo( |
4588 | + machine_name="riscv64", |
4589 | + source_filename=filename, |
4590 | + ) |
4591 | + message = plugin.create_message() |
4592 | + self.assertEqual(message["type"], "processor-info") |
4593 | + self.assertEqual(len(message["processors"]), 2) |
4594 | + |
4595 | + processor_0, processor_1 = message["processors"] |
4596 | + |
4597 | + self.assertEqual(len(processor_0), 3) |
4598 | + self.assertEqual(processor_0["processor-id"], 0) |
4599 | + self.assertEqual(processor_0["model"], "sifive,u74-mc") |
4600 | + self.assertEqual(processor_0["vendor"], "rv64imafdc") |
4601 | + |
4602 | + self.assertEqual(len(processor_1), 3) |
4603 | + self.assertEqual(processor_1["processor-id"], 1) |
4604 | + self.assertEqual(processor_1["model"], "sifive,u74-mc") |
4605 | + self.assertEqual(processor_1["vendor"], "rv64imafdc") |
4606 | diff --git a/landscape/client/monitor/tests/test_service.py b/landscape/client/monitor/tests/test_service.py |
4607 | index 67289b6..8104ba6 100644 |
4608 | --- a/landscape/client/monitor/tests/test_service.py |
4609 | +++ b/landscape/client/monitor/tests/test_service.py |
4610 | @@ -1,4 +1,5 @@ |
4611 | from unittest.mock import Mock |
4612 | +from unittest.mock import patch |
4613 | |
4614 | from landscape.client.monitor.computerinfo import ComputerInfo |
4615 | from landscape.client.monitor.config import ALL_PLUGINS |
4616 | @@ -44,6 +45,34 @@ class MonitorServiceTest(LandscapeTest): |
4617 | self.assertTrue(isinstance(plugins[0], ComputerInfo)) |
4618 | self.assertTrue(isinstance(plugins[1], LoadAverage)) |
4619 | |
4620 | + def test_get_plugins_module_not_found(self): |
4621 | + """If a module is not found, a warning is logged.""" |
4622 | + self.service.config.load(["--monitor-plugins", "TotallyDoesNotExist"]) |
4623 | + |
4624 | + with self.assertLogs(level="WARN") as cm: |
4625 | + plugins = self.service.get_plugins() |
4626 | + |
4627 | + self.assertEqual(len(plugins), 0) |
4628 | + self.assertIn("Invalid monitor plugin", cm.output[0]) |
4629 | + self.assertIn("TotallyDoesNotExist", cm.output[0]) |
4630 | + |
4631 | + def test_get_plugins_other_exception(self): |
4632 | + """If loading a plugin fails for another reason, a warning is logged, |
4633 | + with the exception. |
4634 | + """ |
4635 | + self.service.config.load(["--monitor-plugins", "ComputerInfo"]) |
4636 | + |
4637 | + with self.assertLogs(level="WARN") as cm: |
4638 | + with patch( |
4639 | + "landscape.client.monitor.service.namedClass", |
4640 | + ) as namedClass: |
4641 | + namedClass.side_effect = Exception("Is there life on Mars?") |
4642 | + plugins = self.service.get_plugins() |
4643 | + |
4644 | + self.assertEqual(len(plugins), 0) |
4645 | + self.assertIn("Unable to load", cm.output[0]) |
4646 | + self.assertIn("Mars?", cm.output[0]) |
4647 | + |
4648 | def test_start_service(self): |
4649 | """ |
4650 | The L{MonitorService.startService} method connects to the broker, |
4651 | diff --git a/landscape/client/monitor/tests/test_snapmonitor.py b/landscape/client/monitor/tests/test_snapmonitor.py |
4652 | index 086aec6..b112702 100644 |
4653 | --- a/landscape/client/monitor/tests/test_snapmonitor.py |
4654 | +++ b/landscape/client/monitor/tests/test_snapmonitor.py |
4655 | @@ -1,8 +1,10 @@ |
4656 | -from unittest.mock import Mock |
4657 | +from unittest.mock import patch |
4658 | |
4659 | from landscape.client.monitor.snapmonitor import SnapMonitor |
4660 | -from landscape.client.snap.http import SnapdHttpException, SnapHttp |
4661 | -from landscape.client.tests.helpers import LandscapeTest, MonitorHelper |
4662 | +from landscape.client.snap_http import SnapdHttpException |
4663 | +from landscape.client.snap_http import SnapdResponse |
4664 | +from landscape.client.tests.helpers import LandscapeTest |
4665 | +from landscape.client.tests.helpers import MonitorHelper |
4666 | |
4667 | |
4668 | class SnapMonitorTest(LandscapeTest): |
4669 | @@ -11,11 +13,19 @@ class SnapMonitorTest(LandscapeTest): |
4670 | helpers = [MonitorHelper] |
4671 | |
4672 | def setUp(self): |
4673 | - super(SnapMonitorTest, self).setUp() |
4674 | + super().setUp() |
4675 | self.mstore.set_accepted_types(["snaps"]) |
4676 | |
4677 | - def test_get_data(self): |
4678 | + @patch("landscape.client.monitor.snapmonitor.snap_http") |
4679 | + def test_get_data(self, snap_http_mock): |
4680 | """Tests getting installed snap data.""" |
4681 | + snap_http_mock.list.return_value = SnapdResponse("sync", 200, "OK", []) |
4682 | + snap_http_mock.get_apps.return_value = SnapdResponse( |
4683 | + "sync", |
4684 | + 200, |
4685 | + "OK", |
4686 | + [], |
4687 | + ) |
4688 | plugin = SnapMonitor() |
4689 | self.monitor.add(plugin) |
4690 | |
4691 | @@ -30,15 +40,13 @@ class SnapMonitorTest(LandscapeTest): |
4692 | """ |
4693 | Tests that we return no data if there is an error getting it. |
4694 | """ |
4695 | - snap_http_mock = Mock( |
4696 | - spec=SnapHttp, |
4697 | - get_snaps=Mock(side_effect=SnapdHttpException) |
4698 | - ) |
4699 | plugin = SnapMonitor() |
4700 | - plugin._snap_http = snap_http_mock |
4701 | self.monitor.add(plugin) |
4702 | |
4703 | - with self.assertLogs(level="ERROR") as cm: |
4704 | + with patch( |
4705 | + "landscape.client.monitor.snapmonitor.snap_http", |
4706 | + ) as snap_http_mock, self.assertLogs(level="ERROR") as cm: |
4707 | + snap_http_mock.list.side_effect = SnapdHttpException |
4708 | plugin.exchange() |
4709 | |
4710 | messages = self.mstore.get_pending_messages() |
4711 | @@ -46,5 +54,60 @@ class SnapMonitorTest(LandscapeTest): |
4712 | self.assertEqual(len(messages), 0) |
4713 | self.assertEqual( |
4714 | cm.output, |
4715 | - ["ERROR:root:Unable to list installed snaps: "] |
4716 | + ["ERROR:root:Unable to list installed snaps: "], |
4717 | + ) |
4718 | + |
4719 | + @patch("landscape.client.monitor.snapmonitor.snap_http") |
4720 | + def test_get_snap_config(self, snap_http_mock): |
4721 | + """Tests that we can get and coerce snap config.""" |
4722 | + plugin = SnapMonitor() |
4723 | + self.monitor.add(plugin) |
4724 | + |
4725 | + snap_http_mock.list.return_value = SnapdResponse( |
4726 | + "sync", |
4727 | + 200, |
4728 | + "OK", |
4729 | + [ |
4730 | + { |
4731 | + "name": "test-snap", |
4732 | + "revision": "1", |
4733 | + "confinement": "strict", |
4734 | + "version": "v1.0", |
4735 | + "id": "123", |
4736 | + }, |
4737 | + ], |
4738 | + ) |
4739 | + snap_http_mock.get_conf.return_value = SnapdResponse( |
4740 | + "sync", |
4741 | + 200, |
4742 | + "OK", |
4743 | + { |
4744 | + "foo": {"baz": "default", "qux": [1, True, 2.0]}, |
4745 | + "bar": "enabled", |
4746 | + }, |
4747 | + ) |
4748 | + snap_http_mock.get_apps.return_value = SnapdResponse( |
4749 | + "sync", |
4750 | + 200, |
4751 | + "OK", |
4752 | + [], |
4753 | + ) |
4754 | + plugin.exchange() |
4755 | + |
4756 | + messages = self.mstore.get_pending_messages() |
4757 | + |
4758 | + self.assertTrue(len(messages) > 0) |
4759 | + self.assertDictEqual( |
4760 | + messages[0]["snaps"]["installed"][0], |
4761 | + { |
4762 | + "name": "test-snap", |
4763 | + "revision": "1", |
4764 | + "confinement": "strict", |
4765 | + "version": "v1.0", |
4766 | + "id": "123", |
4767 | + "config": ( |
4768 | + '{"foo": {"baz": "default", "qux": [1, true, 2.0]}, ' |
4769 | + '"bar": "enabled"}' |
4770 | + ), |
4771 | + }, |
4772 | ) |
4773 | diff --git a/landscape/client/monitor/tests/test_snapservicesmonitor.py b/landscape/client/monitor/tests/test_snapservicesmonitor.py |
4774 | new file mode 100644 |
4775 | index 0000000..4c86cb3 |
4776 | --- /dev/null |
4777 | +++ b/landscape/client/monitor/tests/test_snapservicesmonitor.py |
4778 | @@ -0,0 +1,127 @@ |
4779 | +from unittest.mock import patch |
4780 | + |
4781 | +from landscape.client.monitor.snapservicesmonitor import SnapServicesMonitor |
4782 | +from landscape.client.snap_http import SnapdHttpException |
4783 | +from landscape.client.snap_http import SnapdResponse |
4784 | +from landscape.client.tests.helpers import LandscapeTest |
4785 | +from landscape.client.tests.helpers import MonitorHelper |
4786 | + |
4787 | + |
4788 | +class SnapServicesMonitorTest(LandscapeTest): |
4789 | + |
4790 | + helpers = [MonitorHelper] |
4791 | + |
4792 | + def setUp(self): |
4793 | + super().setUp() |
4794 | + self.mstore.set_accepted_types(["snap-services"]) |
4795 | + |
4796 | + @patch("landscape.client.monitor.snapservicesmonitor.snap_http") |
4797 | + def test_get_data(self, snap_http_mock): |
4798 | + """Tests getting running snap services data.""" |
4799 | + snap_http_mock.get_apps.return_value = SnapdResponse( |
4800 | + "sync", |
4801 | + 200, |
4802 | + "OK", |
4803 | + [ |
4804 | + { |
4805 | + "snap": "test-snap", |
4806 | + "name": "bye-svc", |
4807 | + "daemon": "simple", |
4808 | + "daemon-scope": "system", |
4809 | + }, |
4810 | + ], |
4811 | + ) |
4812 | + |
4813 | + plugin = SnapServicesMonitor() |
4814 | + self.monitor.add(plugin) |
4815 | + |
4816 | + plugin.exchange() |
4817 | + |
4818 | + messages = self.mstore.get_pending_messages() |
4819 | + |
4820 | + self.assertTrue(len(messages) > 0) |
4821 | + self.assertIn("running", messages[0]["services"]) |
4822 | + |
4823 | + @patch("landscape.client.monitor.snapservicesmonitor.snap_http") |
4824 | + def test_get_snap_services(self, snap_http_mock): |
4825 | + """Tests that we can get and coerce snap services.""" |
4826 | + plugin = SnapServicesMonitor() |
4827 | + self.monitor.add(plugin) |
4828 | + self.maxDiff = None |
4829 | + |
4830 | + services = [ |
4831 | + { |
4832 | + "snap": "test-snap", |
4833 | + "name": "bye-svc", |
4834 | + "daemon": "simple", |
4835 | + "daemon-scope": "system", |
4836 | + }, |
4837 | + { |
4838 | + "snap": "test-snap", |
4839 | + "name": "hello-svc", |
4840 | + "daemon": "simple", |
4841 | + "daemon-scope": "system", |
4842 | + "active": True, |
4843 | + }, |
4844 | + { |
4845 | + "activators": [ |
4846 | + { |
4847 | + "Active": True, |
4848 | + "Enabled": True, |
4849 | + "Name": "unix", |
4850 | + "Type": "socket", |
4851 | + }, |
4852 | + ], |
4853 | + "daemon": "simple", |
4854 | + "daemon-scope": "system", |
4855 | + "enabled": True, |
4856 | + "name": "user-daemon", |
4857 | + "snap": "lxd", |
4858 | + }, |
4859 | + ] |
4860 | + snap_http_mock.list.return_value = SnapdResponse("sync", 200, "OK", []) |
4861 | + snap_http_mock.get_conf.return_value = SnapdResponse( |
4862 | + "sync", |
4863 | + 200, |
4864 | + "OK", |
4865 | + {}, |
4866 | + ) |
4867 | + snap_http_mock.get_apps.return_value = SnapdResponse( |
4868 | + "sync", |
4869 | + 200, |
4870 | + "OK", |
4871 | + services, |
4872 | + ) |
4873 | + plugin.exchange() |
4874 | + |
4875 | + messages = self.mstore.get_pending_messages() |
4876 | + |
4877 | + self.assertTrue(len(messages) > 0) |
4878 | + self.assertCountEqual(messages[0]["services"]["running"], services) |
4879 | + |
4880 | + @patch("landscape.client.monitor.snapservicesmonitor.snap_http") |
4881 | + def test_get_snap_services_error(self, snap_http_mock): |
4882 | + """Tests that we can get and coerce snap services.""" |
4883 | + plugin = SnapServicesMonitor() |
4884 | + self.monitor.add(plugin) |
4885 | + |
4886 | + snap_http_mock.list.return_value = SnapdResponse("sync", 200, "OK", []) |
4887 | + snap_http_mock.get_conf.return_value = SnapdResponse( |
4888 | + "sync", |
4889 | + 200, |
4890 | + "OK", |
4891 | + {}, |
4892 | + ) |
4893 | + |
4894 | + with self.assertLogs(level="WARNING") as cm: |
4895 | + snap_http_mock.get_apps.side_effect = SnapdHttpException |
4896 | + plugin.exchange() |
4897 | + |
4898 | + messages = self.mstore.get_pending_messages() |
4899 | + |
4900 | + self.assertTrue(len(messages) > 0) |
4901 | + self.assertEqual( |
4902 | + cm.output, |
4903 | + ["WARNING:root:Unable to list services: "], |
4904 | + ) |
4905 | + self.assertCountEqual(messages[0]["services"]["running"], []) |
4906 | diff --git a/landscape/client/monitor/tests/test_temperature.py b/landscape/client/monitor/tests/test_temperature.py |
4907 | index 93e3b6f..472fa46 100644 |
4908 | --- a/landscape/client/monitor/tests/test_temperature.py |
4909 | +++ b/landscape/client/monitor/tests/test_temperature.py |
4910 | @@ -5,10 +5,10 @@ from unittest import mock |
4911 | from landscape.client.monitor.temperature import Temperature |
4912 | from landscape.client.tests.helpers import LandscapeTest |
4913 | from landscape.client.tests.helpers import MonitorHelper |
4914 | -from landscape.lib.tests.test_sysstats import ThermalZoneTest |
4915 | +from landscape.lib.tests.test_sysstats import SysfsThermalZoneTest |
4916 | |
4917 | |
4918 | -class TemperatureTestWithSampleData(ThermalZoneTest, LandscapeTest): |
4919 | +class TemperatureTestWithSampleData(SysfsThermalZoneTest, LandscapeTest): |
4920 | """Tests for the temperature plugin.""" |
4921 | |
4922 | helpers = [MonitorHelper] |
4923 | diff --git a/landscape/client/monitor/tests/test_ubuntuproinfo.py b/landscape/client/monitor/tests/test_ubuntuproinfo.py |
4924 | deleted file mode 100644 |
4925 | index 1b8675f..0000000 |
4926 | --- a/landscape/client/monitor/tests/test_ubuntuproinfo.py |
4927 | +++ /dev/null |
4928 | @@ -1,47 +0,0 @@ |
4929 | -from unittest import mock |
4930 | - |
4931 | -from landscape.client.monitor.ubuntuproinfo import UbuntuProInfo |
4932 | -from landscape.client.tests.helpers import LandscapeTest |
4933 | -from landscape.client.tests.helpers import MonitorHelper |
4934 | - |
4935 | - |
4936 | -class UbuntuProInfoTest(LandscapeTest): |
4937 | - """Ubuntu Pro info plugin tests.""" |
4938 | - |
4939 | - helpers = [MonitorHelper] |
4940 | - |
4941 | - def setUp(self): |
4942 | - super().setUp() |
4943 | - self.mstore.set_accepted_types(["ubuntu-pro-info"]) |
4944 | - |
4945 | - def test_ubuntu_pro_info(self): |
4946 | - """Tests calling `ua status`.""" |
4947 | - plugin = UbuntuProInfo() |
4948 | - self.monitor.add(plugin) |
4949 | - |
4950 | - with mock.patch("subprocess.run") as run_mock: |
4951 | - run_mock.return_value = mock.Mock( |
4952 | - stdout='"This is a test"', |
4953 | - ) |
4954 | - plugin.exchange() |
4955 | - |
4956 | - messages = self.mstore.get_pending_messages() |
4957 | - run_mock.assert_called_once() |
4958 | - self.assertTrue(len(messages) > 0) |
4959 | - self.assertTrue("ubuntu-pro-info" in messages[0]) |
4960 | - self.assertEqual(messages[0]["ubuntu-pro-info"], '"This is a test"') |
4961 | - |
4962 | - def test_ubuntu_pro_info_no_ua(self): |
4963 | - """Tests calling `ua status` when it is not installed.""" |
4964 | - plugin = UbuntuProInfo() |
4965 | - self.monitor.add(plugin) |
4966 | - |
4967 | - with mock.patch("subprocess.run") as run_mock: |
4968 | - run_mock.side_effect = FileNotFoundError() |
4969 | - plugin.exchange() |
4970 | - |
4971 | - messages = self.mstore.get_pending_messages() |
4972 | - run_mock.assert_called_once() |
4973 | - self.assertTrue(len(messages) > 0) |
4974 | - self.assertTrue("ubuntu-pro-info" in messages[0]) |
4975 | - self.assertIn("errors", messages[0]["ubuntu-pro-info"]) |
4976 | diff --git a/landscape/client/monitor/tests/test_usermonitor.py b/landscape/client/monitor/tests/test_usermonitor.py |
4977 | index 1fddd9a..5a009d5 100644 |
4978 | --- a/landscape/client/monitor/tests/test_usermonitor.py |
4979 | +++ b/landscape/client/monitor/tests/test_usermonitor.py |
4980 | @@ -1,6 +1,7 @@ |
4981 | import os |
4982 | from unittest.mock import ANY |
4983 | from unittest.mock import Mock |
4984 | +from unittest.mock import patch |
4985 | |
4986 | from twisted.internet.defer import fail |
4987 | |
4988 | @@ -208,6 +209,50 @@ class UserMonitorTest(LandscapeTest): |
4989 | ], |
4990 | ) |
4991 | |
4992 | + @patch("landscape.client.monitor.usermonitor.IS_CORE", "1") |
4993 | + def test_new_message_after_resynchronize_event_on_core(self): |
4994 | + """ |
4995 | + When a 'resynchronize' reactor event is fired, a new session is |
4996 | + created and the UserMonitor creates a new message. |
4997 | + """ |
4998 | + self.provider.users = [ |
4999 | + ( |
5000 | + "john-doe", |
Grabbing a review slot