Merge ~silverdrake11/ubuntu/+source/landscape-client:ubuntu/mantic-devel into ubuntu/+source/landscape-client:ubuntu/mantic-devel

Proposed by Kevin Nasto
Status: Merged
Merged at revision: 08afd8dad6238cb5f8f50865e928296f20169759
Proposed branch: ~silverdrake11/ubuntu/+source/landscape-client:ubuntu/mantic-devel
Merge into: ubuntu/+source/landscape-client:ubuntu/mantic-devel
Diff against target: 57671 lines (+22146/-11153)
300 files modified
.flake8 (+5/-0)
.gitignore (+6/-0)
.pre-commit-config.yaml (+43/-0)
LICENSE (+5/-5)
Makefile (+45/-4)
README (+103/-2)
debian/README.source (+1/-1)
debian/changelog (+37/-15)
debian/copyright (+0/-1)
debian/landscape-client.postrm (+1/-1)
debian/watch (+1/-1)
dev/landscape-client-vm (+2/-2)
dev/null (+0/-66)
dev/upload-to-ppa (+1/-1)
display_py2_testresults (+6/-4)
example.conf (+17/-1)
landscape-client.conf (+0/-1)
landscape/__init__.py (+2/-2)
landscape/client/accumulate.py (+15/-8)
landscape/client/amp.py (+9/-7)
landscape/client/broker/amp.py (+24/-19)
landscape/client/broker/client.py (+44/-25)
landscape/client/broker/config.py (+62/-31)
landscape/client/broker/exchange.py (+109/-65)
landscape/client/broker/exchangestore.py (+21/-8)
landscape/client/broker/ping.py (+40/-20)
landscape/client/broker/registration.py (+62/-43)
landscape/client/broker/server.py (+35/-19)
landscape/client/broker/service.py (+60/-24)
landscape/client/broker/store.py (+78/-19)
landscape/client/broker/tests/helpers.py (+82/-45)
landscape/client/broker/tests/test_amp.py (+28/-19)
landscape/client/broker/tests/test_client.py (+33/-18)
landscape/client/broker/tests/test_config.py (+28/-17)
landscape/client/broker/tests/test_exchange.py (+276/-154)
landscape/client/broker/tests/test_exchangestore.py (+21/-12)
landscape/client/broker/tests/test_ping.py (+42/-27)
landscape/client/broker/tests/test_registration.py (+136/-85)
landscape/client/broker/tests/test_server.py (+109/-54)
landscape/client/broker/tests/test_service.py (+7/-7)
landscape/client/broker/tests/test_store.py (+173/-68)
landscape/client/broker/tests/test_transport.py (+75/-35)
landscape/client/broker/transport.py (+60/-27)
landscape/client/configuration.py (+326/-232)
landscape/client/deployment.py (+95/-50)
landscape/client/lockfile.py (+2/-2)
landscape/client/manager/aptsources.py (+43/-21)
landscape/client/manager/config.py (+41/-22)
landscape/client/manager/customgraph.py (+75/-34)
landscape/client/manager/fakepackagemanager.py (+40/-24)
landscape/client/manager/hardwareinfo.py (+10/-4)
landscape/client/manager/keystonetoken.py (+23/-14)
landscape/client/manager/manager.py (+2/-2)
landscape/client/manager/packagemanager.py (+30/-16)
landscape/client/manager/plugin.py (+17/-9)
landscape/client/manager/processkiller.py (+35/-21)
landscape/client/manager/scriptexecution.py (+97/-55)
landscape/client/manager/service.py (+21/-13)
landscape/client/manager/shutdownmanager.py (+47/-29)
landscape/client/manager/snapmanager.py (+271/-0)
landscape/client/manager/store.py (+30/-16)
landscape/client/manager/tests/test_aptsources.py (+237/-92)
landscape/client/manager/tests/test_config.py (+26/-13)
landscape/client/manager/tests/test_customgraph.py (+460/-229)
landscape/client/manager/tests/test_fakepackagemanager.py (+39/-20)
landscape/client/manager/tests/test_hardwareinfo.py (+9/-6)
landscape/client/manager/tests/test_keystonetoken.py (+20/-15)
landscape/client/manager/tests/test_manager.py (+2/-2)
landscape/client/manager/tests/test_packagemanager.py (+51/-29)
landscape/client/manager/tests/test_plugin.py (+44/-17)
landscape/client/manager/tests/test_processkiller.py (+173/-79)
landscape/client/manager/tests/test_scriptexecution.py (+377/-176)
landscape/client/manager/tests/test_service.py (+8/-6)
landscape/client/manager/tests/test_shutdownmanager.py (+48/-18)
landscape/client/manager/tests/test_snapmanager.py (+259/-0)
landscape/client/manager/tests/test_store.py (+10/-12)
landscape/client/manager/tests/test_usermanager.py (+1068/-563)
landscape/client/manager/usermanager.py (+70/-44)
landscape/client/monitor/activeprocessinfo.py (+18/-10)
landscape/client/monitor/aptpreferences.py (+10/-7)
landscape/client/monitor/cephusage.py (+49/-22)
landscape/client/monitor/computerinfo.py (+75/-38)
landscape/client/monitor/computertags.py (+1/-1)
landscape/client/monitor/computeruptime.py (+24/-13)
landscape/client/monitor/config.py (+32/-11)
landscape/client/monitor/cpuusage.py (+40/-19)
landscape/client/monitor/livepatch.py (+71/-0)
landscape/client/monitor/loadaverage.py (+34/-15)
landscape/client/monitor/memoryinfo.py (+37/-16)
landscape/client/monitor/monitor.py (+10/-5)
landscape/client/monitor/mountinfo.py (+48/-28)
landscape/client/monitor/networkactivity.py (+30/-13)
landscape/client/monitor/networkdevice.py (+12/-9)
landscape/client/monitor/packagemonitor.py (+54/-29)
landscape/client/monitor/plugin.py (+15/-7)
landscape/client/monitor/processorinfo.py (+44/-23)
landscape/client/monitor/rebootrequired.py (+14/-8)
landscape/client/monitor/service.py (+41/-17)
landscape/client/monitor/snapmonitor.py (+44/-0)
landscape/client/monitor/swiftusage.py (+37/-18)
landscape/client/monitor/temperature.py (+40/-18)
landscape/client/monitor/tests/test_activeprocessinfo.py (+679/-288)
landscape/client/monitor/tests/test_aptpreferences.py (+84/-41)
landscape/client/monitor/tests/test_cephusage.py (+28/-14)
landscape/client/monitor/tests/test_computerinfo.py (+108/-56)
landscape/client/monitor/tests/test_computertags.py (+14/-13)
landscape/client/monitor/tests/test_computeruptime.py (+112/-40)
landscape/client/monitor/tests/test_config.py (+6/-4)
landscape/client/monitor/tests/test_cpuusage.py (+14/-10)
landscape/client/monitor/tests/test_livepatch.py (+140/-0)
landscape/client/monitor/tests/test_loadaverage.py (+52/-23)
landscape/client/monitor/tests/test_memoryinfo.py (+48/-22)
landscape/client/monitor/tests/test_monitor.py (+12/-6)
landscape/client/monitor/tests/test_mountinfo.py (+333/-139)
landscape/client/monitor/tests/test_networkactivity.py (+46/-25)
landscape/client/monitor/tests/test_networkdevice.py (+7/-6)
landscape/client/monitor/tests/test_packagemonitor.py (+40/-26)
landscape/client/monitor/tests/test_plugin.py (+31/-14)
landscape/client/monitor/tests/test_processorinfo.py (+106/-78)
landscape/client/monitor/tests/test_rebootrequired.py (+49/-27)
landscape/client/monitor/tests/test_service.py (+11/-9)
landscape/client/monitor/tests/test_snapmonitor.py (+50/-0)
landscape/client/monitor/tests/test_swiftusage.py (+154/-91)
landscape/client/monitor/tests/test_temperature.py (+59/-35)
landscape/client/monitor/tests/test_ubuntuproinfo.py (+5/-5)
landscape/client/monitor/tests/test_ubuntuprorebootrequired.py (+29/-0)
landscape/client/monitor/tests/test_updatemanager.py (+16/-10)
landscape/client/monitor/tests/test_usermonitor.py (+327/-144)
landscape/client/monitor/ubuntuproinfo.py (+11/-7)
landscape/client/monitor/ubuntuprorebootrequired.py (+27/-0)
landscape/client/monitor/updatemanager.py (+11/-12)
landscape/client/monitor/usermonitor.py (+40/-21)
landscape/client/package/changer.py (+134/-69)
landscape/client/package/releaseupgrader.py (+101/-49)
landscape/client/package/reporter.py (+200/-126)
landscape/client/package/taskhandler.py (+42/-25)
landscape/client/package/tests/test_changer.py (+705/-355)
landscape/client/package/tests/test_releaseupgrader.py (+349/-214)
landscape/client/package/tests/test_reporter.py (+663/-303)
landscape/client/package/tests/test_taskhandler.py (+82/-39)
landscape/client/patch.py (+8/-6)
landscape/client/reactor.py (+1/-1)
landscape/client/service.py (+30/-17)
landscape/client/serviceconfig.py (+130/-0)
landscape/client/snap/__init__.py (+0/-0)
landscape/client/snap/http.py (+302/-0)
landscape/client/snap/tests/__init__.py (+0/-0)
landscape/client/snap/tests/test_http.py (+51/-0)
landscape/client/tests/clock.py (+66/-46)
landscape/client/tests/helpers.py (+76/-53)
landscape/client/tests/subunit.py (+133/-106)
landscape/client/tests/test_accumulate.py (+3/-2)
landscape/client/tests/test_amp.py (+33/-27)
landscape/client/tests/test_configuration.py (+1241/-862)
landscape/client/tests/test_deployment.py (+44/-29)
landscape/client/tests/test_diff.py (+4/-4)
landscape/client/tests/test_lockfile.py (+7/-3)
landscape/client/tests/test_patch.py (+11/-8)
landscape/client/tests/test_reactor.py (+1/-2)
landscape/client/tests/test_service.py (+14/-11)
landscape/client/tests/test_serviceconfig.py (+223/-0)
landscape/client/tests/test_watchdog.py (+377/-226)
landscape/client/upgraders/__init__.py (+5/-2)
landscape/client/upgraders/tests/test_broker.py (+1/-3)
landscape/client/upgraders/tests/test_monitor.py (+1/-3)
landscape/client/upgraders/tests/test_package.py (+1/-3)
landscape/client/user/changes.py (+13/-7)
landscape/client/user/management.py (+142/-68)
landscape/client/user/provider.py (+83/-35)
landscape/client/user/tests/helpers.py (+78/-35)
landscape/client/user/tests/test_changes.py (+121/-63)
landscape/client/user/tests/test_management.py (+406/-168)
landscape/client/user/tests/test_provider.py (+503/-230)
landscape/client/watchdog.py (+187/-96)
landscape/constants.py (+15/-2)
landscape/lib/amp.py (+94/-50)
landscape/lib/apt/package/facade.py (+178/-107)
landscape/lib/apt/package/skeleton.py (+89/-44)
landscape/lib/apt/package/store.py (+111/-84)
landscape/lib/apt/package/testing.py (+243/-186)
landscape/lib/apt/package/tests/test_facade.py (+1063/-411)
landscape/lib/apt/package/tests/test_skeleton.py (+90/-37)
landscape/lib/apt/package/tests/test_store.py (+87/-55)
landscape/lib/backoff.py (+1/-1)
landscape/lib/base64.py (+0/-2)
landscape/lib/bootstrap.py (+4/-8)
landscape/lib/bpickle.py (+67/-63)
landscape/lib/cli.py (+9/-4)
landscape/lib/cloud.py (+8/-4)
landscape/lib/compat.py (+5/-0)
landscape/lib/config.py (+52/-33)
landscape/lib/disk.py (+44/-17)
landscape/lib/fd.py (+0/-1)
landscape/lib/fetch.py (+40/-19)
landscape/lib/format.py (+5/-5)
landscape/lib/fs.py (+0/-1)
landscape/lib/gpg.py (+25/-14)
landscape/lib/jiffies.py (+3/-3)
landscape/lib/juju.py (+3/-4)
landscape/lib/lock.py (+2/-2)
landscape/lib/log.py (+0/-2)
landscape/lib/logging.py (+51/-23)
landscape/lib/lsb_release.py (+4/-3)
landscape/lib/message.py (+3/-2)
landscape/lib/monitor.py (+70/-37)
landscape/lib/network.py (+63/-37)
landscape/lib/persist.py (+53/-35)
landscape/lib/plugin.py (+2/-4)
landscape/lib/process.py (+35/-17)
landscape/lib/reactor.py (+31/-19)
landscape/lib/schema.py (+63/-36)
landscape/lib/scriptcontent.py (+1/-1)
landscape/lib/sequenceranges.py (+9/-8)
landscape/lib/store.py (+3/-0)
landscape/lib/sysstats.py (+42/-26)
landscape/lib/testing.py (+184/-115)
landscape/lib/tests/test_amp.py (+128/-84)
landscape/lib/tests/test_backoff.py (+5/-6)
landscape/lib/tests/test_bootstrap.py (+17/-12)
landscape/lib/tests/test_bpickle.py (+11/-13)
landscape/lib/tests/test_cloud.py (+56/-27)
landscape/lib/tests/test_config.py (+113/-75)
landscape/lib/tests/test_disk.py (+42/-24)
landscape/lib/tests/test_encoding.py (+12/-11)
landscape/lib/tests/test_fd.py (+4/-5)
landscape/lib/tests/test_fetch.py (+253/-170)
landscape/lib/tests/test_format.py (+15/-11)
landscape/lib/tests/test_fs.py (+35/-27)
landscape/lib/tests/test_gpg.py (+37/-21)
landscape/lib/tests/test_juju.py (+43/-25)
landscape/lib/tests/test_lock.py (+4/-4)
landscape/lib/tests/test_logging.py (+22/-10)
landscape/lib/tests/test_lsb_release.py (+46/-29)
landscape/lib/tests/test_monitor.py (+54/-34)
landscape/lib/tests/test_network.py (+217/-138)
landscape/lib/tests/test_persist.py (+94/-74)
landscape/lib/tests/test_plugin.py (+8/-6)
landscape/lib/tests/test_process.py (+33/-24)
landscape/lib/tests/test_reactor.py (+26/-12)
landscape/lib/tests/test_schema.py (+55/-32)
landscape/lib/tests/test_scriptcontent.py (+7/-6)
landscape/lib/tests/test_sequenceranges.py (+25/-21)
landscape/lib/tests/test_sysstats.py (+82/-48)
landscape/lib/tests/test_tag.py (+23/-19)
landscape/lib/tests/test_timestamp.py (+1/-1)
landscape/lib/tests/test_twisted_util.py (+27/-12)
landscape/lib/tests/test_versioning.py (+2/-3)
landscape/lib/tests/test_vm_info.py (+10/-8)
landscape/lib/tests/test_warning.py (+9/-6)
landscape/lib/twisted_util.py (+43/-18)
landscape/lib/user.py (+1/-2)
landscape/lib/versioning.py (+6/-4)
landscape/lib/vm_info.py (+4/-3)
landscape/lib/warning.py (+0/-1)
landscape/message_schemas/__init__.py (+0/-1)
landscape/message_schemas/message.py (+8/-4)
landscape/message_schemas/server_bound.py (+682/-374)
landscape/message_schemas/test_message.py (+15/-9)
landscape/sysinfo/deployment.py (+67/-29)
landscape/sysinfo/disk.py (+26/-16)
landscape/sysinfo/landscapelink.py (+3/-3)
landscape/sysinfo/load.py (+4/-3)
landscape/sysinfo/loggedinusers.py (+2/-2)
landscape/sysinfo/memory.py (+9/-6)
landscape/sysinfo/network.py (+15/-7)
landscape/sysinfo/processes.py (+18/-8)
landscape/sysinfo/sysinfo.py (+40/-21)
landscape/sysinfo/temperature.py (+4/-5)
landscape/sysinfo/testplugin.py (+1/-2)
landscape/sysinfo/tests/test_deployment.py (+93/-52)
landscape/sysinfo/tests/test_disk.py (+113/-50)
landscape/sysinfo/tests/test_landscapelink.py (+7/-5)
landscape/sysinfo/tests/test_load.py (+4/-7)
landscape/sysinfo/tests/test_loggedinusers.py (+14/-10)
landscape/sysinfo/tests/test_memory.py (+8/-7)
landscape/sysinfo/tests/test_network.py (+54/-39)
landscape/sysinfo/tests/test_processes.py (+38/-17)
landscape/sysinfo/tests/test_sysinfo.py (+182/-136)
landscape/sysinfo/tests/test_temperature.py (+11/-9)
man/landscape-client.1 (+4/-4)
man/landscape-client.txt (+4/-4)
man/landscape-config.1 (+28/-20)
man/landscape-config.txt (+18/-14)
man/landscape-sysinfo.txt (+6/-7)
pqm-tests.sh (+0/-1)
pyproject.toml (+13/-0)
scripts/landscape-broker (+3/-1)
scripts/landscape-client (+5/-2)
scripts/landscape-config (+4/-1)
scripts/landscape-manager (+3/-1)
scripts/landscape-monitor (+3/-1)
scripts/landscape-package-changer (+3/-1)
scripts/landscape-package-reporter (+3/-1)
scripts/landscape-release-upgrader (+3/-1)
scripts/landscape-sysinfo (+6/-2)
setup.py (+34/-29)
setup_client.py (+30/-29)
setup_lib.py (+19/-18)
setup_sysinfo.py (+8/-9)
snap/snapcraft.yaml (+98/-0)
Reviewer Review Type Date Requested Status
Lucas Kanashiro (community) Approve
git-ubuntu import Pending
Review via email: mp+449306@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Kevin Nasto (silverdrake11) wrote :
Revision history for this message
Lucas Kanashiro (lucaskanashiro) wrote :

Thanks for this MP Kevin!

Initially, I needed to update the d/watch to be able to fetch the upstream tarball:

diff --git a/debian/watch b/debian/watch
index 3592b51..ba7ea91 100644
--- a/debian/watch
+++ b/debian/watch
@@ -1,3 +1,3 @@
 version=4
 opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/landscape-client-$1\.tar\.gz/ \
- https://github.com/CanonicalLtd/landscape-client/tags .*/?(\d{2}\.\d.\d)\.tar\.gz
+ https://github.com/CanonicalLtd/landscape-client/tags .*/?(\d{2}\.\d{2})\.tar\.gz

I'd recommend to incorporate that change to your package.

I talked to Kevin privately and also asked him to update this line:

https://github.com/canonical/landscape-client/blob/23.08/landscape/__init__.py#L2

to 23.08 (new upstream release).

The only other change related to the package itself that I found is in this commit:

commit a0ecf857a8a883d4c1bb67e5c563de7c6047cc50
Author: Kevin Nasto <email address hidden>
Date: Fri Mar 10 13:25:12 2023 -0600

    Changed gpg file name to be less generic (LP: #2008432) (#137)

This is the change and I believe it is fine to get in:

diff --git a/debian/landscape-client.postrm b/debian/landscape-client.postrm
index 10f79b8..aa34ed2 100644
--- a/debian/landscape-client.postrm
+++ b/debian/landscape-client.postrm
@@ -29,7 +29,7 @@ case "$1" in
     rm -f "${LOG_DIR}/package-reporter.log"*
     rm -f "${LOG_DIR}/package-changer.log"*

- rm -f "${GPG_DIR}/landscape-server"*.asc
+ rm -f "${GPG_DIR}/landscape-server-mirror"*.asc

     rm -rf "${DATA_DIR}/client"
     rm -rf "${DATA_DIR}/.gnupg"

There are 2 lintian errors which are red flags in a first glance:

E: landscape-client source: alien-tag dh_python-is-obsolete [debian/source.lintian-overrides:4]
N:
N: The given override refers to an unknown tag.
N:
N: Please refer to Format of override files (Section 2.4.1) in the Lintian
N: User's Manual for details.
N:
N: Visibility: error
N: Show-Always: yes
N: Check: debian/lintian-overrides/mystery
N:
N:
E: landscape-common: depends-on-obsolete-package Depends: lsb-base
N:
N: The package depends on a package that has been superseded. If the
N: superseded package is part of an ORed group, it should not be the first
N: package in the group.
N:
N: Visibility: error
N: Show-Always: no
N: Check: fields/package-relations
N:
N:

The first one seems to me you added a lintian overrides which will be used just in some ubuntu releases. But it seems this tag does not exist anymore, at least in Mantic's lintian version. I'd remove that TBH.

The second one is about the dependency on lsb-base. I'd consider its removal as well since it is obsolete, but if you are thinking about backporting this to other releases, please check if it is obsolete there as well.

review: Needs Information
Revision history for this message
Lucas Kanashiro (lucaskanashiro) :
review: Needs Fixing
Revision history for this message
Lucas Kanashiro (lucaskanashiro) wrote :

Thanks for addressing my comments Kevin!

Since you decided to remove the dependency on lsb-base, I think you should remove it from here as well:

https://github.com/canonical/landscape-client/blob/23.08/setup_lib.py#L21

review: Needs Fixing
Revision history for this message
Lucas Kanashiro (lucaskanashiro) wrote :

As discussed with Kevin, let's keep lsb-base so he can test it later to make sure it will not impact other supported ubuntu releases.

There are also some improvements that can be made in the package reported by lintian, I will not block this MP because of this, the feature freeze is coming, let's get this uploaded.

review: Approve
Revision history for this message
Lucas Kanashiro (lucaskanashiro) wrote :

Package uploaded:

Uploading landscape-client_23.08-0ubuntu1.dsc
Uploading landscape-client_23.08.orig.tar.gz
Uploading landscape-client_23.08-0ubuntu1.debian.tar.xz
Uploading landscape-client_23.08-0ubuntu1_source.buildinfo
Uploading landscape-client_23.08-0ubuntu1_source.changes

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/.flake8 b/.flake8
0new file mode 1006440new file mode 100644
index 0000000..c3a3cd2
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,5 @@
1[flake8]
2max-line-length = 79
3max-complexity = 18
4select = B,C,E,F,W,T4,B9
5ignore = E203, W503
diff --git a/.gitignore b/.gitignore
index ca1df98..8ce4558 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,9 @@ tags
25_last_py2_res25_last_py2_res
26*.pyc26*.pyc
27.cache27.cache
28_trial_temp*
29*.snap
30landscape-client_amd64*.txt
31landscape-client_arm64*.txt
32landscape-client_ppc64el*.txt
33landscape-client_s390x*.txt
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
28new file mode 10064434new file mode 100644
index 0000000..df96450
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,43 @@
1# See https://pre-commit.com for more information
2# See https://pre-commit.com/hooks.html for more hooks
3repos:
4- repo: https://github.com/pre-commit/pre-commit-hooks
5 rev: v4.4.0
6 hooks:
7 - id: trailing-whitespace
8 - id: end-of-file-fixer
9 - id: check-yaml
10 - id: check-added-large-files
11 - id: debug-statements
12- repo: https://github.com/pre-commit/pre-commit-hooks
13 rev: v2.3.0
14 hooks:
15 - id: flake8
16 args:
17 - "--max-line-length=79"
18 - "--select=B,C,E,F,W,T4,B9"
19 - "--ignore=E203,W503"
20- repo: https://github.com/psf/black
21 rev: 22.12.0
22 hooks:
23 - id: black
24 args:
25 - --line-length=79
26 - --include='\.pyi?$'
27- repo: https://github.com/asottile/reorder_python_imports
28 rev: v2.3.0
29 hooks:
30 - id: reorder-python-imports
31 args: [--py3-plus]
32- repo: https://github.com/asottile/add-trailing-comma
33 rev: v2.0.1
34 hooks:
35 - id: add-trailing-comma
36 args: [--py36-plus]
37exclude: >
38 (?x)(
39 \.git
40 | \.csv$
41 | \.__pycache__
42 | \.log$
43 )
diff --git a/LICENSE b/LICENSE
index 5b6e7c6..dcfa4c2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -55,7 +55,7 @@ patent must be licensed for everyone's free use or not licensed at all.
5555
56 The precise terms and conditions for copying, distribution and56 The precise terms and conditions for copying, distribution and
57modification follow.57modification follow.
58
5958
59
60 GNU GENERAL PUBLIC LICENSE60 GNU GENERAL PUBLIC LICENSE
61 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION61 TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
6262
@@ -110,7 +110,7 @@ above, provided that you also meet all of these conditions:
110 License. (Exception: if the Program itself is interactive but110 License. (Exception: if the Program itself is interactive but
111 does not normally print such an announcement, your work based on111 does not normally print such an announcement, your work based on
112 the Program is not required to print an announcement.)112 the Program is not required to print an announcement.)
113
114113
114
115These requirements apply to the modified work as a whole. If115These requirements apply to the modified work as a whole. If
116identifiable sections of that work are not derived from the Program,116identifiable sections of that work are not derived from the Program,
117and can be reasonably considered independent and separate works in117and can be reasonably considered independent and separate works in
@@ -168,7 +168,7 @@ access to copy from a designated place, then offering equivalent
168access to copy the source code from the same place counts as168access to copy the source code from the same place counts as
169distribution of the source code, even though third parties are not169distribution of the source code, even though third parties are not
170compelled to copy the source along with the object code.170compelled to copy the source along with the object code.
171
172171
172
173 4. You may not copy, modify, sublicense, or distribute the Program173 4. You may not copy, modify, sublicense, or distribute the Program
174except as expressly provided under this License. Any attempt174except as expressly provided under this License. Any attempt
175otherwise to copy, modify, sublicense or distribute the Program is175otherwise to copy, modify, sublicense or distribute the Program is
@@ -225,7 +225,7 @@ impose that choice.
225225
226This section is intended to make thoroughly clear what is believed to226This section is intended to make thoroughly clear what is believed to
227be a consequence of the rest of this License.227be a consequence of the rest of this License.
228
229228
229
230 8. If the distribution and/or use of the Program is restricted in230 8. If the distribution and/or use of the Program is restricted in
231certain countries either by patents or by copyrighted interfaces, the231certain countries either by patents or by copyrighted interfaces, the
232original copyright holder who places the Program under this License232original copyright holder who places the Program under this License
@@ -278,7 +278,7 @@ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278POSSIBILITY OF SUCH DAMAGES.278POSSIBILITY OF SUCH DAMAGES.
279279
280 END OF TERMS AND CONDITIONS280 END OF TERMS AND CONDITIONS
281
282281
282
283 How to Apply These Terms to Your New Programs283 How to Apply These Terms to Your New Programs
284284
285 If you develop a new program, and you want it to be of the greatest285 If you develop a new program, and you want it to be of the greatest
diff --git a/Makefile b/Makefile
index fe183ec..09ade0e 100644
--- a/Makefile
+++ b/Makefile
@@ -2,9 +2,15 @@ PYDOCTOR ?= pydoctor
2TXT2MAN ?= txt2man2TXT2MAN ?= txt2man
3PYTHON2 ?= python23PYTHON2 ?= python2
4PYTHON3 ?= python34PYTHON3 ?= python3
5SNAPCRAFT = SNAPCRAFT_BUILD_INFO=1 snapcraft
5TRIAL ?= -m twisted.trial6TRIAL ?= -m twisted.trial
6TRIAL_ARGS ?=7TRIAL_ARGS ?=
78
9# PEP8 rules ignored:
10# W503 https://www.flake8rules.com/rules/W503.html
11# E203 Whitespace before ':' (enforced by Black)
12PEP8_IGNORED = W503,E203
13
8.PHONY: help14.PHONY: help
9help: ## Print help about available targets15help: ## Print help about available targets
10 @grep -h -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'16 @grep -h -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
@@ -15,11 +21,15 @@ depends: depends3 ## py2 is deprecated
1521
16.PHONY: depends222.PHONY: depends2
17depends2:23depends2:
18 sudo apt-get -y install python-twisted-core python-distutils-extra python-mock python-configobj python-netifaces python-pycurl24 sudo apt-get -y install python-twisted-core python-distutils-extra python-mock python-configobj python-netifaces python-pycurl python-pip
25 pip install pre-commit
26 pre-commit install
1927
20.PHONY: depends328.PHONY: depends3
21depends3:29depends3:
22 sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl30 sudo apt-get -y install python3-twisted python3-distutils-extra python3-mock python3-configobj python3-netifaces python3-pycurl python3-pip
31 pip3 install pre-commit
32 pre-commit install
2333
24all: build34all: build
2535
@@ -56,13 +66,15 @@ coverage:
5666
57.PHONY: lint67.PHONY: lint
58lint:68lint:
59 $(PYTHON3) -m flake8 --ignore E24,E121,E123,E125,E126,E221,E226,E266,E704,E265,W504 \69 $(PYTHON3) -m flake8 --ignore $(PEP8_IGNORED) `find landscape -name \*.py`
60 `find landscape -name \*.py`
6170
62.PHONY: pyflakes71.PHONY: pyflakes
63pyflakes:72pyflakes:
64 -pyflakes `find landscape -name \*.py`73 -pyflakes `find landscape -name \*.py`
6574
75pre-commit:
76 -pre-commit run -a
77
66clean:78clean:
67 -find landscape -name __pycache__ -exec rm -rf {} \;79 -find landscape -name __pycache__ -exec rm -rf {} \;
68 -find landscape -name \*.pyc -exec rm -f {} \;80 -find landscape -name \*.pyc -exec rm -f {} \;
@@ -115,6 +127,35 @@ tags:
115etags:127etags:
116 -etags --languages=python -R .128 -etags --languages=python -R .
117129
130snap-install:
131 sudo snap install --devmode landscape-client_0.1_amd64.snap
132.PHONY: snap-install
133
134snap-remote-build:
135 snapcraft remote-build
136.PHONY: snap-remote-build
137
138snap-remove:
139 sudo snap remove --purge landscape-client
140.PHONY: snap-remove
141
142snap-shell: snap-install
143 sudo snap run --shell landscape-client.landscape-client
144.PHONY: snap-shell
145
146snap-debug:
147 $(SNAPCRAFT) -v --debug
148.PHONY: snap-debug
149
150snap-clean: snap-remove
151 $(SNAPCRAFT) clean
152 -rm landscape-client_0.1_amd64.snap
153.PHONY: snap-clean
154
155snap:
156 $(SNAPCRAFT)
157.PHONY: snap
158
118include Makefile.packaging159include Makefile.packaging
119160
120.DEFAULT_GOAL := help161.DEFAULT_GOAL := help
diff --git a/README b/README
index 06be0e1..8751ef3 100644
--- a/README
+++ b/README
@@ -5,7 +5,7 @@
55
6Add our beta PPA to get the latest updates to the landscape-client package6Add our beta PPA to get the latest updates to the landscape-client package
77
8#### Add repo to an Ubuntu series 8#### Add repo to an Ubuntu series
9```9```
10sudo add-apt-repository ppa:landscape/self-hosted-beta10sudo add-apt-repository ppa:landscape/self-hosted-beta
11```11```
@@ -50,7 +50,7 @@ monitor_only = true
5050
51## Running51## Running
5252
53Now you can complete the configuration of your client and register with the 53Now you can complete the configuration of your client and register with the
54Landscape service. There are two ways to do this:54Landscape service. There are two ways to do this:
5555
561. `sudo landscape-config` and answer interactive prompts to finalize your configuration561. `sudo landscape-config` and answer interactive prompts to finalize your configuration
@@ -86,3 +86,104 @@ Before opening a PR, make sure to run the full testsuite and lint
86make check386make check3
87make lint87make lint
88```88```
89
90### Building the Landscape Client snap
91
92First, you need to ensure that you have the appropriate tools installed:
93```
94$ sudo snap install snapcraft --classic
95$ lxd init --auto
96```
97
98There are various make targets defined to assist in the lifecycle of
99building and testing the snap. To simply build the snap with the minimum
100of debug information displayed:
101```
102$ make snap
103```
104
105If you would prefer to see more information displayed showing the progress
106of the build, and would like to get dropped into a debug shell within the
107snap container in the event of an error:
108```
109$ make snap-debug
110```
111
112To install the resulting snap:
113```
114$ make snap-install
115```
116
117To remove a previously installed snap:
118```
119$ make snap-remove
120```
121
122To clean the intermediate files as well as the snap itself from the local
123build environment:
124```
125$ make snap-clean
126```
127
128To enter into a shell environment within the snap container:
129```
130$ make snap-shell
131```
132
133If you wish to upload the snap to the store, you will first need to get
134credentials with the store that allow you to log in locally and publish
135binaries. This can be done at:
136
137https://snapcraft.io/docs/creating-your-developer-account
138
139After obtaining and confirming your store credentials, you then need to
140log in using the snapcraft tool:
141```
142$ snapcraft login
143```
144
145Since snapcraft version 7.x and higher, the credentials are stored in the
146gnome keyring on your local workstation. If you are building in an
147environment without a GUI (e.g. in a multipass or lxc container), you
148will need to install the gnome keyring tools:
149```
150$ sudo apt install gnome-keyring
151```
152
153You will then need to initialze the default keyring as follows:
154```
155$ dbus-run-session -- bash
156$ gnome-keyring-daemon --unlock
157```
158The gnome-keyring-daemon will prompt you (without a prompt) to type in
159the initial unlock password (typically the same password for the account
160you are using - if you are using the default multipass or lxc "ubuntu"
161login, use sudo passwd ubuntu to set it to a known value before doing
162the above step).
163
164Type the login password and hit <ENTER> followed by <CTRL>+D to end
165the input.
166
167At this point, you should be able to log into snapcraft:
168```
169$ snapcraft login
170```
171You will be prompted for your UbuntuOne email address, password and,
172if set up this way, your second factor for authentication. If you
173are successful, you should be able to query your login credentials
174with:
175```
176$ snapcraft whoami
177```
178A common mistake that first-time users of this process might make
179is that after running the gnome-keyring-daemon command, they will
180exit the dbus session shell. Do NOT do that. Do all subsequent
181work in that bash shell that dbus set up for you because it will
182have access to your gnome-keyring.
183
184If you need to leave the environment and get back in, keep in mind
185that you do not have to be logged into the store UNLESS you are
186uploading the snap or otherwise manipulating things in your store
187account. You will need to repeat the dbus-run-session and
188gnome-keyring-daemon steps BEFORE logging in if you do need to be
189logged into the store.
diff --git a/debian/README.source b/debian/README.source
index d21d7e4..20371fc 100644
--- a/debian/README.source
+++ b/debian/README.source
@@ -12,4 +12,4 @@ appreciated if you also updated the UPSTREAM_VERSION variable in
12landscape/__init__.py to include the entire new client version number. This12landscape/__init__.py to include the entire new client version number. This
13helps us keep track of exact version of clients in use. There's no need to13helps us keep track of exact version of clients in use. There's no need to
14update the DEBIAN_REVISION variable, as it gets automatically set at build14update the DEBIAN_REVISION variable, as it gets automatically set at build
15time. 15time.
diff --git a/debian/changelog b/debian/changelog
index 4ba984f..2479e40 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,25 @@
1landscape-client (23.08-0ubuntu1) mantic; urgency=medium
2
3 * New upstream release 23.08
4 - Restructure landscape-config wizard (LP: #2031673)
5 - Add SnapMonitor, SnapManager, and related machinery (LP: #2031682)
6 - Limit message data directory size (LP: #2004591)
7 - Add zfs and btrfs to stable fs-lists (LP: #1499104)
8 - Test for broken control file (LP: #1813442)
9 - Config man page and help info fixes (LP: #1662222)
10 - Removed unnecesary select to the database (LP: #1994951)
11 - Last apt-key removal (LP: #1990435)
12 - Added a logging level check to provide a better error (LP: #2027521)
13 - Broker is able to bootstrap the landscape-client folder (LP: #1868730)
14 - Changed gpg file name to be less generic (LP: #2008432)
15 - Improve Dependency data is read from the deb packages (LP: #1813442)
16 - Fix error in swift plugin (LP: #2031674)
17 - Repository profiles do not overwrite all configurations (LP: #2031680)
18 - Remove old plugins (LP: #1989968)
19 - Send ubuntu pro reboot output (LP: #2031684)
20
21 -- Kevin Nasto <kevin.nasto@canonical.com> Thu, 17 Aug 2023 13:41:21 -0500
22
1landscape-client (23.02-0ubuntu1) lunar; urgency=medium23landscape-client (23.02-0ubuntu1) lunar; urgency=medium
224
3 * New upstream release 23.02:25 * New upstream release 23.02:
@@ -934,11 +956,11 @@ landscape-client (1.0.21.1-0ubuntu2) intrepid; urgency=low
934956
935landscape-client (1.0.21.1-0ubuntu1) intrepid; urgency=low957landscape-client (1.0.21.1-0ubuntu1) intrepid; urgency=low
936958
937 * New upstream version: 959 * New upstream version:
938 * Add ok-no-register option to landscape-config script to not fail if960 * Add ok-no-register option to landscape-config script to not fail if
939 dbus isn't started or landscape-client isn't running.961 dbus isn't started or landscape-client isn't running.
940 * lower timeout related to package management in landscape.962 * lower timeout related to package management in landscape.
941 * debian/control: Depend on cron. 963 * debian/control: Depend on cron.
942 * debian/landscape-client.postinst: use ok-no-register option so that the964 * debian/landscape-client.postinst: use ok-no-register option so that the
943 postinst script doesn't fail when run from the installer. (LP: #274573).965 postinst script doesn't fail when run from the installer. (LP: #274573).
944966
@@ -986,7 +1008,7 @@ landscape-client (1.0.18-0ubuntu4) intrepid; urgency=low
986 [ Mathias Gug ]1008 [ Mathias Gug ]
987 * debian/landscape-common.postinst, debian/landscape-common.prerm: don't1009 * debian/landscape-common.postinst, debian/landscape-common.prerm: don't
988 call update-motd init script as it's no longer available in the1010 call update-motd init script as it's no longer available in the
989 update-motd package. Call directly /usr/sbin/update-motd instead. 1011 update-motd package. Call directly /usr/sbin/update-motd instead.
990 (LP: #271854)1012 (LP: #271854)
9911013
992 -- Mathias Gug <mathiaz@ubuntu.com> Thu, 18 Sep 2008 16:47:08 -04001014 -- Mathias Gug <mathiaz@ubuntu.com> Thu, 18 Sep 2008 16:47:08 -0400
@@ -1006,15 +1028,15 @@ landscape-client (1.0.18-0ubuntu2) intrepid; urgency=low
1006 command. A landscape account is not required to use this package.1028 command. A landscape account is not required to use this package.
1007 - landscape-client: has all the binaries required to run the1029 - landscape-client: has all the binaries required to run the
1008 landscape-client. Requires a landscape account.1030 landscape-client. Requires a landscape account.
1009 - debian/control: 1031 - debian/control:
1010 + move some dependencies to landscape-client so that1032 + move some dependencies to landscape-client so that
1011 landscape-common doesn't install unecessary packages in the 1033 landscape-common doesn't install unecessary packages in the
1012 default -server install.1034 default -server install.
1013 + move python-gobject to a Pre-Depends so that landscape-config can1035 + move python-gobject to a Pre-Depends so that landscape-config can
1014 register the system during the postinst (LP: #268838).1036 register the system during the postinst (LP: #268838).
1015 * debian/control:1037 * debian/control:
1016 - depend on python-smartpm instead of smartpm-core.1038 - depend on python-smartpm instead of smartpm-core.
1017 * debian/landscape-client.postrm: delete /etc/landscape/client.conf when 1039 * debian/landscape-client.postrm: delete /etc/landscape/client.conf when
1018 the package is purged.1040 the package is purged.
1019 * debian/landscape-client.postinst: remove sysinfo_in_motd debconf question1041 * debian/landscape-client.postinst: remove sysinfo_in_motd debconf question
1020 as it wasn't used.1042 as it wasn't used.
@@ -1030,7 +1052,7 @@ landscape-client (1.0.18-0ubuntu2) intrepid; urgency=low
10301052
1031landscape-client (1.0.18-0ubuntu1) intrepid; urgency=low1053landscape-client (1.0.18-0ubuntu1) intrepid; urgency=low
10321054
1033 * New upstream release 1055 * New upstream release
10341056
1035 -- Rick Clark <rick.clark@ubuntu.com> Mon, 08 Sep 2008 16:35:57 -05001057 -- Rick Clark <rick.clark@ubuntu.com> Mon, 08 Sep 2008 16:35:57 -0500
10361058
@@ -1149,7 +1171,7 @@ landscape-client (1.0.11-hardy1-landscape1) hardy; urgency=low
11491171
1150landscape-client (1.0.10-hardy1-landscape1) hardy; urgency=low1172landscape-client (1.0.10-hardy1-landscape1) hardy; urgency=low
11511173
1152 * Change the utilisation of the csv module to be compatible with 1174 * Change the utilisation of the csv module to be compatible with
1153 python 2.4 as used in Dapper.1175 python 2.4 as used in Dapper.
11541176
1155 -- Andreas Hasenack <andreas@canonical.com> Thu, 12 Jun 2008 17:58:05 +00001177 -- Andreas Hasenack <andreas@canonical.com> Thu, 12 Jun 2008 17:58:05 +0000
@@ -1398,7 +1420,7 @@ landscape-client (0.10.3-1ubuntu1) feisty; urgency=low
1398 fixed (#114829).1420 fixed (#114829).
13991421
1400 -- Landscape Team <landscape-team@canonical.com> Mon, 15 May 2007 16:31:00 -07001422 -- Landscape Team <landscape-team@canonical.com> Mon, 15 May 2007 16:31:00 -0700
1401 1423
1402landscape-client (0.10.2-1ubuntu1) feisty; urgency=low1424landscape-client (0.10.2-1ubuntu1) feisty; urgency=low
14031425
1404 * New upstream release.1426 * New upstream release.
@@ -1412,7 +1434,7 @@ landscape-client (0.10.1-1ubuntu1) feisty; urgency=low
1412 * Minor fix in package management plugin timings.1434 * Minor fix in package management plugin timings.
14131435
1414 -- Landscape Team <landscape-devel@lists.canonical.com> Thu, 10 May 2007 10:00:00 -07001436 -- Landscape Team <landscape-devel@lists.canonical.com> Thu, 10 May 2007 10:00:00 -0700
1415 1437
1416landscape-client (0.10.0-1ubuntu1) feisty; urgency=low1438landscape-client (0.10.0-1ubuntu1) feisty; urgency=low
14171439
1418 * New upstream release.1440 * New upstream release.
@@ -1422,9 +1444,9 @@ landscape-client (0.10.0-1ubuntu1) feisty; urgency=low
1422 * The client uses the new operations system. Support for the now1444 * The client uses the new operations system. Support for the now
1423 unused action info system is gone.1445 unused action info system is gone.
1424 * A minor bpickle bug was fixed.1446 * A minor bpickle bug was fixed.
1425 1447
1426 -- Jamshed Kakar <jamshed.kakar@canonical.com> Mon, 7 May 2007 20:16:00 -07001448 -- Jamshed Kakar <jamshed.kakar@canonical.com> Mon, 7 May 2007 20:16:00 -0700
1427 1449
1428landscape-client (0.9.6-1ubuntu1) feisty; urgency=low1450landscape-client (0.9.6-1ubuntu1) feisty; urgency=low
14291451
1430 * New upstream release.1452 * New upstream release.
@@ -1507,7 +1529,7 @@ landscape-client (0.8.1-1) unstable; urgency=low
1507 * New upstream release.1529 * New upstream release.
1508 * Account registration log message no longer exposes account1530 * Account registration log message no longer exposes account
1509 password.1531 password.
1510 1532
1511 -- Jamshed Kakar <jamshed.kakar@canonical.com> Thu, 18 Jan 2007 23:07:00 -08001533 -- Jamshed Kakar <jamshed.kakar@canonical.com> Thu, 18 Jan 2007 23:07:00 -0800
15121534
1513landscape-client (0.8.0-1) unstable; urgency=low1535landscape-client (0.8.0-1) unstable; urgency=low
@@ -1518,7 +1540,7 @@ landscape-client (0.8.0-1) unstable; urgency=low
1518 * Client includes an SSL certificate to verify the server with.1540 * Client includes an SSL certificate to verify the server with.
1519 * Package depends on python2.4-pycurl, which is necessary for the1541 * Package depends on python2.4-pycurl, which is necessary for the
1520 new SSL support.1542 new SSL support.
1521 1543
1522 -- Jamshed Kakar <jamshed.kakar@canonical.com> Thu, 18 Jan 2007 10:48:00 -08001544 -- Jamshed Kakar <jamshed.kakar@canonical.com> Thu, 18 Jan 2007 10:48:00 -0800
15231545
1524landscape-client (0.7.0-1) unstable; urgency=low1546landscape-client (0.7.0-1) unstable; urgency=low
@@ -1529,7 +1551,7 @@ landscape-client (0.7.0-1) unstable; urgency=low
1529 added to control log verbosity.1551 added to control log verbosity.
1530 * Package depends on perl-modules, which is necessary for bare-bones1552 * Package depends on perl-modules, which is necessary for bare-bones
1531 server installs.1553 server installs.
1532 1554
1533 -- Jamshed Kakar <jamshed.kakar@canonical.com> Tue, 2 Jan 2007 12:54:00 -08001555 -- Jamshed Kakar <jamshed.kakar@canonical.com> Tue, 2 Jan 2007 12:54:00 -0800
15341556
1535landscape-client (0.6.1-1) unstable; urgency=low1557landscape-client (0.6.1-1) unstable; urgency=low
diff --git a/debian/copyright b/debian/copyright
index 479d234..8b528c8 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -33,4 +33,3 @@ License:
3333
34On Debian systems, the complete text of the GNU General34On Debian systems, the complete text of the GNU General
35Public License can be found in `/usr/share/common-licenses/GPL-2'.35Public License can be found in `/usr/share/common-licenses/GPL-2'.
36
diff --git a/debian/landscape-client.postrm b/debian/landscape-client.postrm
index 10f79b8..aa34ed2 100644
--- a/debian/landscape-client.postrm
+++ b/debian/landscape-client.postrm
@@ -29,7 +29,7 @@ case "$1" in
29 rm -f "${LOG_DIR}/package-reporter.log"*29 rm -f "${LOG_DIR}/package-reporter.log"*
30 rm -f "${LOG_DIR}/package-changer.log"*30 rm -f "${LOG_DIR}/package-changer.log"*
3131
32 rm -f "${GPG_DIR}/landscape-server"*.asc32 rm -f "${GPG_DIR}/landscape-server-mirror"*.asc
3333
34 rm -rf "${DATA_DIR}/client"34 rm -rf "${DATA_DIR}/client"
35 rm -rf "${DATA_DIR}/.gnupg"35 rm -rf "${DATA_DIR}/.gnupg"
diff --git a/debian/source.lintian-overrides b/debian/source.lintian-overrides
36deleted file mode 10064436deleted file mode 100644
index 720d1be..0000000
--- a/debian/source.lintian-overrides
+++ /dev/null
@@ -1,4 +0,0 @@
1# we use dh_python or dh_python2 depending on the ubuntu release
2# the package is being built on, this is detected dynamically
3# in the rules file
4landscape-client source: dh_python-is-obsolete
5\ No newline at end of file0\ No newline at end of file
diff --git a/debian/watch b/debian/watch
index 3592b51..ba7ea91 100644
--- a/debian/watch
+++ b/debian/watch
@@ -1,3 +1,3 @@
1version=41version=4
2opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/landscape-client-$1\.tar\.gz/ \2opts=filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/landscape-client-$1\.tar\.gz/ \
3 https://github.com/CanonicalLtd/landscape-client/tags .*/?(\d{2}\.\d.\d)\.tar\.gz3 https://github.com/CanonicalLtd/landscape-client/tags .*/?(\d{2}\.\d{2})\.tar\.gz
diff --git a/dev/landscape-client-vm b/dev/landscape-client-vm
index 4d13b6f..a2a1e57 100755
--- a/dev/landscape-client-vm
+++ b/dev/landscape-client-vm
@@ -37,7 +37,7 @@ landscape-devel account on the Landscape staging server (or you can specify
37another account with the --account parameter).37another account with the --account parameter).
3838
39The built VM will be stored under ./build/intrepid along with some other39The built VM will be stored under ./build/intrepid along with some other
40files. To launch the VM, cd to ./build/intrepid and issue: 40files. To launch the VM, cd to ./build/intrepid and issue:
41$ ./run41$ ./run
42Once it's booted you can log into it with:42Once it's booted you can log into it with:
43$ ./ssh43$ ./ssh
@@ -136,7 +136,7 @@ cat > script <<EOF
136#!/bin/sh -e136#!/bin/sh -e
137chown landscape /etc/landscape/client.conf137chown landscape /etc/landscape/client.conf
138chmod 600 /etc/landscape/client.conf138chmod 600 /etc/landscape/client.conf
139apt-key add /root/ppa-key139cp /root/ppa-key /etc/apt/trusted.gpg.d/landscape-server-mirror-root-ppa-key.asc
140echo "RUN=1" > /etc/default/landscape-client140echo "RUN=1" > /etc/default/landscape-client
141EOF141EOF
142chmod 755 script142chmod 755 script
diff --git a/dev/upload-to-ppa b/dev/upload-to-ppa
index 2cc01b8..f65b42d 100755
--- a/dev/upload-to-ppa
+++ b/dev/upload-to-ppa
@@ -112,7 +112,7 @@ for release in $releases; do
112 else112 else
113 message="Built for $codename, no source changes"113 message="Built for $codename, no source changes"
114 fi114 fi
115 cp debian/changelog ../ 115 cp debian/changelog ../
116 dch --force-distribution -b -v $version -D $codename -m $message116 dch --force-distribution -b -v $version -D $codename -m $message
117 dpkg-buildpackage -S $source_opt -k$key117 dpkg-buildpackage -S $source_opt -k$key
118 dput $ppa ../${package}_${version}_source.changes118 dput $ppa ../${package}_${version}_source.changes
diff --git a/display_py2_testresults b/display_py2_testresults
index 021681d..e683c1d 100755
--- a/display_py2_testresults
+++ b/display_py2_testresults
@@ -1,5 +1,5 @@
1#!/usr/bin/python1#!/usr/bin/python
2with open('_last_py2_res', 'r') as py2res:2with open("_last_py2_res", "r") as py2res:
3 lines = py2res.readlines()3 lines = py2res.readlines()
44
5lastline = lines[-1]5lastline = lines[-1]
@@ -7,7 +7,9 @@ lastline = lines[-1]
7time, total, total, err, fail, skip = lastline.split()7time, total, total, err, fail, skip = lastline.split()
88
9if "".join((err, fail, skip)) != "000":9if "".join((err, fail, skip)) != "000":
10 print("Python 2: \033[91mFAILED\033[0m (skips={}, failures={}, "10 print(
11 "errors={}, total={})".format(skip, fail, err, total))11 "Python 2: \033[91mFAILED\033[0m (skips={}, failures={}, "
12 "errors={}, total={})".format(skip, fail, err, total),
13 )
12else:14else:
13 print("Python 2: \033[92mOK\033[0m (total={})".format(total))15 print(f"Python 2: \033[92mOK\033[0m (total={total})")
diff --git a/example.conf b/example.conf
index 4b7fd94..16b710e 100644
--- a/example.conf
+++ b/example.conf
@@ -77,7 +77,6 @@ ignore_sigusr1 = False
77#77#
78# ActiveProcessInfo - lists active processes78# ActiveProcessInfo - lists active processes
79# ComputerInfo - various information79# ComputerInfo - various information
80# HardwareInventory - information provided by the "lshw" command
81# LoadAverage - load information80# LoadAverage - load information
82# MemoryInfo - memory information81# MemoryInfo - memory information
83# MountInfo - information about mount points (space available, used)82# MountInfo - information about mount points (space available, used)
@@ -86,9 +85,17 @@ ignore_sigusr1 = False
86# PackageMonitor - packages installed, available, versions85# PackageMonitor - packages installed, available, versions
87# UserMonitor - users, groups86# UserMonitor - users, groups
88# RebootRequired - whether a reboot is required or not87# RebootRequired - whether a reboot is required or not
88# AptPreferences - system APT preferences configuration
89# NetworkActivity - network information (TX, RX)89# NetworkActivity - network information (TX, RX)
90# NetworkDevice - a list of connected network devices90# NetworkDevice - a list of connected network devices
91# UpdateManager - controls when distribution upgrades are prompted91# UpdateManager - controls when distribution upgrades are prompted
92# CPUUsage - CPU usage information
93# SwiftUsage - Swift cluster usage
94# CephUsage - Ceph usage information
95# ComputerTags - changes in computer tags
96# UbuntuProInfo - Ubuntu Pro registration information
97# LivePatch - Livepath status information
98# UbuntuProRebootRequired - informs if the system needs to be rebooted
92#99#
93# The special value "ALL" is an alias for the full list of plugins.100# The special value "ALL" is an alias for the full list of plugins.
94monitor_plugins = ALL101monitor_plugins = ALL
@@ -130,6 +137,9 @@ apt_update_interval = 21600
130# The number of seconds between package monitor runs.137# The number of seconds between package monitor runs.
131package_monitor_interval = 1800138package_monitor_interval = 1800
132139
140# The number of seconds between snap monitor runs.
141snap_monitor_interval = 1800
142
133# The URL of the http proxy to use, if any.143# The URL of the http proxy to use, if any.
134# This value is optional.144# This value is optional.
135#145#
@@ -182,3 +192,9 @@ script_users = ALL
182# The default is 512kB192# The default is 512kB
183# 2MB is allowed in this example193# 2MB is allowed in this example
184#script_output_limit=2048194#script_output_limit=2048
195
196# Whether files in /etc/apt/sources.list.d are removed when a repository
197# profile is added to this machine.
198#
199# The default is True
200#manage_sources_list_d = True
diff --git a/landscape-client.conf b/landscape-client.conf
index cf96100..af4faaf 100644
--- a/landscape-client.conf
+++ b/landscape-client.conf
@@ -9,4 +9,3 @@ log_dir = /tmp/landscape/
9log_level = debug9log_level = debug
10pid_file = /tmp/landscape/landscape-client.pid10pid_file = /tmp/landscape/landscape-client.pid
11ping_url = http://localhost:8081/ping11ping_url = http://localhost:8081/ping
12
diff --git a/landscape/__init__.py b/landscape/__init__.py
index 23f8f79..1077067 100644
--- a/landscape/__init__.py
+++ b/landscape/__init__.py
@@ -1,6 +1,6 @@
1DEBIAN_REVISION = ""1DEBIAN_REVISION = ""
2UPSTREAM_VERSION = "23.02"2UPSTREAM_VERSION = "23.08"
3VERSION = "%s%s" % (UPSTREAM_VERSION, DEBIAN_REVISION)3VERSION = f"{UPSTREAM_VERSION}{DEBIAN_REVISION}"
44
5# The minimum server API version that all Landscape servers are known to speak5# The minimum server API version that all Landscape servers are known to speak
6# and support. It can serve as fallback in case higher versions are not there.6# and support. It can serve as fallback in case higher versions are not there.
diff --git a/landscape/client/accumulate.py b/landscape/client/accumulate.py
index 2d17c06..ce14c88 100644
--- a/landscape/client/accumulate.py
+++ b/landscape/client/accumulate.py
@@ -72,24 +72,31 @@ representative data at each step boundary.
72"""72"""
7373
7474
75class Accumulator(object):75class Accumulator:
76
77 def __init__(self, persist, step_size):76 def __init__(self, persist, step_size):
78 self._persist = persist77 self._persist = persist
79 self._step_size = step_size78 self._step_size = step_size
8079
81 def __call__(self, new_timestamp, new_free_space, key):80 def __call__(self, new_timestamp, new_free_space, key):
82 previous_timestamp, accumulated_value = self._persist.get(key, (0, 0))81 previous_timestamp, accumulated_value = self._persist.get(key, (0, 0))
83 accumulated_value, step_data = \82 accumulated_value, step_data = accumulate(
84 accumulate(previous_timestamp, accumulated_value,83 previous_timestamp,
85 new_timestamp, new_free_space, self._step_size)84 accumulated_value,
85 new_timestamp,
86 new_free_space,
87 self._step_size,
88 )
86 self._persist.set(key, (new_timestamp, accumulated_value))89 self._persist.set(key, (new_timestamp, accumulated_value))
87 return step_data90 return step_data
8891
8992
90def accumulate(previous_timestamp, accumulated_value,93def accumulate(
91 new_timestamp, new_value,94 previous_timestamp,
92 step_size):95 accumulated_value,
96 new_timestamp,
97 new_value,
98 step_size,
99):
93 previous_step = previous_timestamp // step_size100 previous_step = previous_timestamp // step_size
94 new_step = new_timestamp // step_size101 new_step = new_timestamp // step_size
95 step_boundary = new_step * step_size102 step_boundary = new_step * step_size
diff --git a/landscape/client/amp.py b/landscape/client/amp.py
index df4ef92..5b5d448 100644
--- a/landscape/client/amp.py
+++ b/landscape/client/amp.py
@@ -10,14 +10,15 @@ This module implements a few conveniences built around L{landscape.lib.amp} to
10let the various services connect to each other in an easy and idiomatic way,10let the various services connect to each other in an easy and idiomatic way,
11and have them respond to standard requests like "ping" or "exit".11and have them respond to standard requests like "ping" or "exit".
12"""12"""
13import os
14import logging13import logging
14import os
1515
16from landscape.lib.amp import (16from landscape.lib.amp import MethodCallClientFactory
17 MethodCallClientFactory, MethodCallServerFactory, RemoteObject)17from landscape.lib.amp import MethodCallServerFactory
18from landscape.lib.amp import RemoteObject
1819
1920
20class ComponentPublisher(object):21class ComponentPublisher:
21 """Publish a Landscape client component using a UNIX socket.22 """Publish a Landscape client component using a UNIX socket.
2223
23 Other Landscape client processes can then connect to the socket and invoke24 Other Landscape client processes can then connect to the socket and invoke
@@ -72,7 +73,7 @@ def remote(method):
72 return method73 return method
7374
7475
75class ComponentConnector(object):76class ComponentConnector:
76 """Utility superclass for creating connections with a Landscape component.77 """Utility superclass for creating connections with a Landscape component.
7778
78 @cvar component: The class of the component to connect to, it is expected79 @cvar component: The class of the component to connect to, it is expected
@@ -90,6 +91,7 @@ class ComponentConnector(object):
9091
91 @see: L{MethodCallClientFactory}.92 @see: L{MethodCallClientFactory}.
92 """93 """
94
93 factory = MethodCallClientFactory95 factory = MethodCallClientFactory
94 component = None # Must be defined by sub-classes96 component = None # Must be defined by sub-classes
95 remote = RemoteObject97 remote = RemoteObject
@@ -123,14 +125,14 @@ class ComponentConnector(object):
123 factory.factor = factor125 factory.factor = factor
124126
125 def fire_reconnect(ignored):127 def fire_reconnect(ignored):
126 self._reactor.fire("%s-reconnect" % self.component.name)128 self._reactor.fire(f"{self.component.name}-reconnect")
127129
128 def connected(remote):130 def connected(remote):
129 factory.notifyOnConnect(fire_reconnect)131 factory.notifyOnConnect(fire_reconnect)
130 return remote132 return remote
131133
132 def log_error(failure):134 def log_error(failure):
133 logging.error("Error while connecting to %s", self.component.name)135 logging.error(f"Error while connecting to {self.component.name}")
134 return failure136 return failure
135137
136 socket_path = _get_socket_path(self.component, self._config)138 socket_path = _get_socket_path(self.component, self._config)
diff --git a/landscape/client/broker/amp.py b/landscape/client/broker/amp.py
index 94a1fa2..2278b08 100644
--- a/landscape/client/broker/amp.py
+++ b/landscape/client/broker/amp.py
@@ -1,16 +1,19 @@
1from twisted.internet.defer import maybeDeferred, execute, succeed1from twisted.internet.defer import execute
2from twisted.internet.defer import maybeDeferred
3from twisted.internet.defer import succeed
2from twisted.python.compat import iteritems4from twisted.python.compat import iteritems
35
4from landscape.lib.amp import RemoteObject, MethodCallArgument6from landscape.client.amp import ComponentConnector
5from landscape.client.amp import ComponentConnector, get_remote_methods7from landscape.client.amp import get_remote_methods
6from landscape.client.broker.server import BrokerServer
7from landscape.client.broker.client import BrokerClient8from landscape.client.broker.client import BrokerClient
8from landscape.client.monitor.monitor import Monitor9from landscape.client.broker.server import BrokerServer
9from landscape.client.manager.manager import Manager10from landscape.client.manager.manager import Manager
11from landscape.client.monitor.monitor import Monitor
12from landscape.lib.amp import MethodCallArgument
13from landscape.lib.amp import RemoteObject
1014
1115
12class RemoteBroker(RemoteObject):16class RemoteBroker(RemoteObject):
13
14 def call_if_accepted(self, type, callable, *args):17 def call_if_accepted(self, type, callable, *args):
15 """Call C{callable} if C{type} is an accepted message type."""18 """Call C{callable} if C{type} is an accepted message type."""
16 deferred_types = self.get_accepted_message_types()19 deferred_types = self.get_accepted_message_types()
@@ -18,6 +21,7 @@ class RemoteBroker(RemoteObject):
18 def got_accepted_types(result):21 def got_accepted_types(result):
19 if type in result:22 if type in result:
20 return callable(*args)23 return callable(*args)
24
21 deferred_types.addCallback(got_accepted_types)25 deferred_types.addCallback(got_accepted_types)
22 return deferred_types26 return deferred_types
2327
@@ -30,11 +34,10 @@ class RemoteBroker(RemoteObject):
30 callable will be fired.34 callable will be fired.
31 """35 """
32 result = self.listen_events(list(handlers.keys()))36 result = self.listen_events(list(handlers.keys()))
33 return result.addCallback(37 return result.addCallback(lambda args: handlers[args[0]](**args[1]))
34 lambda args: handlers[args[0]](**args[1]))
3538
3639
37class FakeRemoteBroker(object):40class FakeRemoteBroker:
38 """Looks like L{RemoteBroker}, but actually talks to local objects."""41 """Looks like L{RemoteBroker}, but actually talks to local objects."""
3942
40 def __init__(self, exchanger, message_store, broker_server):43 def __init__(self, exchanger, message_store, broker_server):
@@ -48,16 +51,19 @@ class FakeRemoteBroker(object):
48 that they're encodable with AMP.51 that they're encodable with AMP.
49 """52 """
50 original = getattr(self.broker_server, name, None)53 original = getattr(self.broker_server, name, None)
51 if (name in get_remote_methods(self.broker_server) and54 if (
52 original is not None and55 name in get_remote_methods(self.broker_server)
53 callable(original)56 and original is not None
54 ):57 and callable(original)
58 ):
59
55 def method(*args, **kwargs):60 def method(*args, **kwargs):
56 for arg in args:61 for arg in args:
57 assert MethodCallArgument.check(arg)62 assert MethodCallArgument.check(arg)
58 for k, v in iteritems(kwargs):63 for k, v in iteritems(kwargs):
59 assert MethodCallArgument.check(v)64 assert MethodCallArgument.check(v)
60 return execute(original, *args, **kwargs)65 return execute(original, *args, **kwargs)
66
61 return method67 return method
62 else:68 else:
63 raise AttributeError(name)69 raise AttributeError(name)
@@ -76,8 +82,7 @@ class FakeRemoteBroker(object):
76 callable will be fired.82 callable will be fired.
77 """83 """
78 result = self.broker_server.listen_events(handlers.keys())84 result = self.broker_server.listen_events(handlers.keys())
79 return result.addCallback(85 return result.addCallback(lambda args: handlers[args[0]](**args[1]))
80 lambda args: handlers[args[0]](**args[1]))
8186
82 def register(self):87 def register(self):
83 return succeed(None)88 return succeed(None)
@@ -114,8 +119,8 @@ def get_component_registry():
114 RemoteBrokerConnector,119 RemoteBrokerConnector,
115 RemoteClientConnector,120 RemoteClientConnector,
116 RemoteMonitorConnector,121 RemoteMonitorConnector,
117 RemoteManagerConnector122 RemoteManagerConnector,
118 ]123 ]
119 return dict(124 return {
120 (connector.component.name, connector)125 connector.component.name: connector for connector in all_connectors
121 for connector in all_connectors)126 }
diff --git a/landscape/client/broker/client.py b/landscape/client/broker/client.py
index 042c18a..edbc2c2 100644
--- a/landscape/client/broker/client.py
+++ b/landscape/client/broker/client.py
@@ -1,19 +1,23 @@
1from logging import info, exception, error, debug
2import sys
3import random1import random
2import sys
3from logging import debug
4from logging import error
5from logging import exception
6from logging import info
47
5from twisted.internet.defer import maybeDeferred, succeed8from twisted.internet.defer import maybeDeferred
9from twisted.internet.defer import succeed
610
11from landscape.client.amp import remote
7from landscape.lib.format import format_object12from landscape.lib.format import format_object
8from landscape.lib.twisted_util import gather_results13from landscape.lib.twisted_util import gather_results
9from landscape.client.amp import remote
1014
1115
12class HandlerNotFoundError(Exception):16class HandlerNotFoundError(Exception):
13 """A handler for the given message type was not found."""17 """A handler for the given message type was not found."""
1418
1519
16class BrokerClientPlugin(object):20class BrokerClientPlugin:
17 """A convenience for writing L{BrokerClient} plugins.21 """A convenience for writing L{BrokerClient} plugins.
1822
19 This provides a register method which will set up a bunch of23 This provides a register method which will set up a bunch of
@@ -32,6 +36,7 @@ class BrokerClientPlugin(object):
32 sent. See L{landscape.broker.server.BrokerServer.send_message} for36 sent. See L{landscape.broker.server.BrokerServer.send_message} for
33 more details.37 more details.
34 """38 """
39
35 run_interval = 540 run_interval = 5
36 run_immediately = False41 run_immediately = False
37 scope = None # Global scope42 scope = None # Global scope
@@ -58,8 +63,10 @@ class BrokerClientPlugin(object):
58 if acceptance:63 if acceptance:
59 return callable(*args, **kwargs)64 return callable(*args, **kwargs)
6065
61 self.client.reactor.call_on(("message-type-acceptance-changed", type),66 self.client.reactor.call_on(
62 acceptance_changed)67 ("message-type-acceptance-changed", type),
68 acceptance_changed,
69 )
6370
64 def _resynchronize(self, scopes=None):71 def _resynchronize(self, scopes=None):
65 """72 """
@@ -106,18 +113,27 @@ class BrokerClientPlugin(object):
106 if self.run_immediately:113 if self.run_immediately:
107 self._run_with_error_log()114 self._run_with_error_log()
108 if self.run_interval is not None:115 if self.run_interval is not None:
109 delay = (random.random() * self.run_interval *116 delay = (
110 self.client.config.stagger_launch)117 random.random()
111 debug("delaying start of %s for %d seconds",118 * self.run_interval
112 format_object(self), delay)119 * self.client.config.stagger_launch
120 )
121 debug(
122 "delaying start of %s for %d seconds",
123 format_object(self),
124 delay,
125 )
113 self._loop = self.client.reactor.call_later(126 self._loop = self.client.reactor.call_later(
114 delay, self._start_loop)127 delay,
128 self._start_loop,
129 )
115130
116 def _start_loop(self):131 def _start_loop(self):
117 """Launch the client loop."""132 """Launch the client loop."""
118 self._loop = self.client.reactor.call_every(133 self._loop = self.client.reactor.call_every(
119 self.run_interval,134 self.run_interval,
120 self._run_with_error_log)135 self._run_with_error_log,
136 )
121137
122 def _run_with_error_log(self):138 def _run_with_error_log(self):
123 """Wrap self.run in a Deferred with a logging error handler."""139 """Wrap self.run in a Deferred with a logging error handler."""
@@ -134,7 +150,7 @@ class BrokerClientPlugin(object):
134 return failure150 return failure
135151
136152
137class BrokerClient(object):153class BrokerClient:
138 """Basic plugin registry for clients that have to deal with the broker.154 """Basic plugin registry for clients that have to deal with the broker.
139155
140 This knows about the needs of a client when dealing with the Landscape156 This knows about the needs of a client when dealing with the Landscape
@@ -148,10 +164,11 @@ class BrokerClient(object):
148164
149 @param reactor: A L{LandscapeReactor}.165 @param reactor: A L{LandscapeReactor}.
150 """166 """
167
151 name = "client"168 name = "client"
152169
153 def __init__(self, reactor, config):170 def __init__(self, reactor, config):
154 super(BrokerClient, self).__init__()171 super().__init__()
155 self.reactor = reactor172 self.reactor = reactor
156 self.broker = None173 self.broker = None
157 self.config = config174 self.config = config
@@ -179,7 +196,7 @@ class BrokerClient(object):
179 """196 """
180 info("Registering plugin %s.", format_object(plugin))197 info("Registering plugin %s.", format_object(plugin))
181 self._plugins.append(plugin)198 self._plugins.append(plugin)
182 if hasattr(plugin, 'plugin_name'):199 if hasattr(plugin, "plugin_name"):
183 self._plugin_names[plugin.plugin_name] = plugin200 self._plugin_names[plugin.plugin_name] = plugin
184 plugin.register(self)201 plugin.register(self)
185202
@@ -210,15 +227,16 @@ class BrokerClient(object):
210 @return: The return value of the handler, if found.227 @return: The return value of the handler, if found.
211 @raises: HandlerNotFoundError if the handler was not found228 @raises: HandlerNotFoundError if the handler was not found
212 """229 """
213 type = message["type"]230 typ = message["type"]
214 handler = self._registered_messages.get(type)231 handler = self._registered_messages.get(typ)
215 if handler is None:232 if handler is None:
216 raise HandlerNotFoundError(type)233 raise HandlerNotFoundError(typ)
217 try:234 try:
218 return handler(message)235 return handler(message)
219 except Exception:236 except Exception:
220 exception("Error running message handler for type %r: %r"237 exception(
221 % (type, handler))238 f"Error running message handler for type {typ!r}: {handler!r}",
239 )
222240
223 @remote241 @remote
224 def message(self, message):242 def message(self, message):
@@ -259,8 +277,9 @@ class BrokerClient(object):
259 results = self.reactor.fire((event_type, message_type), acceptance)277 results = self.reactor.fire((event_type, message_type), acceptance)
260 else:278 else:
261 results = self.reactor.fire(event_type, *args, **kwargs)279 results = self.reactor.fire(event_type, *args, **kwargs)
262 return gather_results([280 return gather_results(
263 maybeDeferred(lambda x: x, result) for result in results])281 [maybeDeferred(lambda x: x, result) for result in results],
282 )
264283
265 def handle_reconnect(self):284 def handle_reconnect(self):
266 """Called when the connection with the broker is established again.285 """Called when the connection with the broker is established again.
@@ -273,8 +292,8 @@ class BrokerClient(object):
273 - Re-register ourselves as client, so the broker knows we exist and292 - Re-register ourselves as client, so the broker knows we exist and
274 will talk to us firing events and dispatching messages.293 will talk to us firing events and dispatching messages.
275 """294 """
276 for type in self._registered_messages:295 for typ in self._registered_messages:
277 self.broker.register_client_accepted_message_type(type)296 self.broker.register_client_accepted_message_type(typ)
278 self.broker.register_client(self.name)297 self.broker.register_client(self.name)
279298
280 @remote299 @remote
diff --git a/landscape/client/broker/config.py b/landscape/client/broker/config.py
index 71e65e1..6b222fb 100644
--- a/landscape/client/broker/config.py
+++ b/landscape/client/broker/config.py
@@ -1,5 +1,4 @@
1"""Configuration class for the broker."""1"""Configuration class for the broker."""
2
3import os2import os
43
5from landscape.client.deployment import Configuration4from landscape.client.deployment import Configuration
@@ -12,7 +11,7 @@ class BrokerConfiguration(Configuration):
12 """11 """
1312
14 def __init__(self):13 def __init__(self):
15 super(BrokerConfiguration, self).__init__()14 super().__init__()
16 self._original_http_proxy = os.environ.get("http_proxy")15 self._original_http_proxy = os.environ.get("http_proxy")
17 self._original_https_proxy = os.environ.get("https_proxy")16 self._original_https_proxy = os.environ.get("https_proxy")
1817
@@ -33,35 +32,67 @@ class BrokerConfiguration(Configuration):
33 - C{http_proxy}32 - C{http_proxy}
34 - C{https_proxy}33 - C{https_proxy}
35 """34 """
36 parser = super(BrokerConfiguration, self).make_parser()35 parser = super().make_parser()
3736
38 parser.add_option("-a", "--account-name", metavar="NAME",37 parser.add_option(
39 help="The account this computer belongs to.")38 "-a",
40 parser.add_option("-p", "--registration-key", metavar="KEY",39 "--account-name",
41 help="The account-wide key used for "40 metavar="NAME",
42 "registering clients.")41 help="The account this computer belongs to.",
43 parser.add_option("-t", "--computer-title", metavar="TITLE",42 )
44 help="The title of this computer")43 parser.add_option(
45 parser.add_option("--exchange-interval", default=15 * 60, type="int",44 "-p",
46 metavar="INTERVAL",45 "--registration-key",
47 help="The number of seconds between server "46 metavar="KEY",
48 "exchanges.")47 help="The account-wide key used for " "registering clients.",
49 parser.add_option("--urgent-exchange-interval", default=1 * 60,48 )
50 type="int", metavar="INTERVAL",49 parser.add_option(
51 help="The number of seconds between urgent server "50 "-t",
52 "exchanges.")51 "--computer-title",
53 parser.add_option("--ping-interval", default=30, type="int",52 metavar="TITLE",
54 metavar="INTERVAL",53 help="The title of this computer",
55 help="The number of seconds between pings.")54 )
56 parser.add_option("--http-proxy", metavar="URL",55 parser.add_option(
57 help="The URL of the HTTP proxy, if one is needed.")56 "--exchange-interval",
58 parser.add_option("--https-proxy", metavar="URL",57 default=15 * 60,
59 help="The URL of the HTTPS proxy, if one is needed.")58 type="int",
60 parser.add_option("--access-group", default="",59 metavar="INTERVAL",
61 help="Suggested access group for this computer.")60 help="The number of seconds between server " "exchanges.",
62 parser.add_option("--tags",61 )
63 help="Comma separated list of tag names to be sent "62 parser.add_option(
64 "to the server.")63 "--urgent-exchange-interval",
64 default=1 * 60,
65 type="int",
66 metavar="INTERVAL",
67 help="The number of seconds between urgent server " "exchanges.",
68 )
69 parser.add_option(
70 "--ping-interval",
71 default=30,
72 type="int",
73 metavar="INTERVAL",
74 help="The number of seconds between pings.",
75 )
76 parser.add_option(
77 "--http-proxy",
78 metavar="URL",
79 help="The URL of the HTTP proxy, if one is needed.",
80 )
81 parser.add_option(
82 "--https-proxy",
83 metavar="URL",
84 help="The URL of the HTTPS proxy, if one is needed.",
85 )
86 parser.add_option(
87 "--access-group",
88 default="",
89 help="Suggested access group for this computer.",
90 )
91 parser.add_option(
92 "--tags",
93 help="Comma separated list of tag names to be sent "
94 "to the server.",
95 )
6596
66 return parser97 return parser
6798
@@ -78,7 +109,7 @@ class BrokerConfiguration(Configuration):
78 C{http_proxy} and C{https_proxy} environment variables based on109 C{http_proxy} and C{https_proxy} environment variables based on
79 that config data.110 that config data.
80 """111 """
81 super(BrokerConfiguration, self).load(args)112 super().load(args)
82 if self.http_proxy:113 if self.http_proxy:
83 os.environ["http_proxy"] = self.http_proxy114 os.environ["http_proxy"] = self.http_proxy
84 elif self._original_http_proxy:115 elif self._original_http_proxy:
diff --git a/landscape/client/broker/exchange.py b/landscape/client/broker/exchange.py
index 924cb9a..31f32ff 100644
--- a/landscape/client/broker/exchange.py
+++ b/landscape/client/broker/exchange.py
@@ -342,23 +342,28 @@ Diagram::
342 14. Schedule exchange342 14. Schedule exchange
343343
344"""344"""
345import time
346import logging345import logging
347from landscape.lib.hashlib import md5346import time
348347
349from twisted.internet.defer import Deferred, succeed348from twisted.internet.defer import Deferred
350from landscape.lib.compat import _PY3349from twisted.internet.defer import succeed
351350
351from landscape import CLIENT_API
352from landscape import DEFAULT_SERVER_API
353from landscape import SERVER_API
352from landscape.lib.backoff import ExponentialBackoff354from landscape.lib.backoff import ExponentialBackoff
353from landscape.lib.fetch import HTTPCodeError, PyCurlError355from landscape.lib.compat import _PY3
356from landscape.lib.fetch import HTTPCodeError
357from landscape.lib.fetch import PyCurlError
354from landscape.lib.format import format_delta358from landscape.lib.format import format_delta
355from landscape.lib.message import got_next_expected, RESYNC359from landscape.lib.hashlib import md5
356from landscape.lib.versioning import is_version_higher, sort_versions360from landscape.lib.message import got_next_expected
357361from landscape.lib.message import RESYNC
358from landscape import DEFAULT_SERVER_API, SERVER_API, CLIENT_API362from landscape.lib.versioning import is_version_higher
363from landscape.lib.versioning import sort_versions
359364
360365
361class MessageExchange(object):366class MessageExchange:
362 """Schedule and handle message exchanges with the server.367 """Schedule and handle message exchanges with the server.
363368
364 The L{MessageExchange} is the place where messages are sent to go out369 The L{MessageExchange} is the place where messages are sent to go out
@@ -376,8 +381,16 @@ class MessageExchange(object):
376 # The highest server API that we are capable of speaking381 # The highest server API that we are capable of speaking
377 _api = SERVER_API382 _api = SERVER_API
378383
379 def __init__(self, reactor, store, transport, registration_info,384 def __init__(
380 exchange_store, config, max_messages=100):385 self,
386 reactor,
387 store,
388 transport,
389 registration_info,
390 exchange_store,
391 config,
392 max_messages=100,
393 ):
381 """394 """
382 @param reactor: The L{LandscapeReactor} used to fire events in response395 @param reactor: The L{LandscapeReactor} used to fire events in response
383 to messages received by the server.396 to messages received by the server.
@@ -421,15 +434,16 @@ class MessageExchange(object):
421 A message is considered obsolete if the secure ID changed since it was434 A message is considered obsolete if the secure ID changed since it was
422 received.435 received.
423 """436 """
424 if 'operation-id' not in message:437 if "operation-id" not in message:
425 return False438 return False
426439
427 operation_id = message['operation-id']440 operation_id = message["operation-id"]
428 context = self._exchange_store.get_message_context(operation_id)441 context = self._exchange_store.get_message_context(operation_id)
429 if context is None:442 if context is None:
430 logging.warning(443 logging.warning(
431 "No message context for message with operation-id: %s"444 "No message context for message with "
432 % operation_id)445 f"operation-id: {operation_id}",
446 )
433 return False447 return False
434448
435 # Compare the current secure ID with the one that was in effect when449 # Compare the current secure ID with the one that was in effect when
@@ -450,7 +464,7 @@ class MessageExchange(object):
450 max_bytes = self._max_log_text_bytes464 max_bytes = self._max_log_text_bytes
451 if len(value) > max_bytes:465 if len(value) > max_bytes:
452 value = value[:max_bytes]466 value = value[:max_bytes]
453 value += '...MESSAGE TRUNCATED DUE TO SIZE'467 value += "...MESSAGE TRUNCATED DUE TO SIZE"
454 message[field] = value468 message[field] = value
455469
456 def send(self, message, urgent=False):470 def send(self, message, urgent=False):
@@ -463,14 +477,15 @@ class MessageExchange(object):
463 """477 """
464 if self._message_is_obsolete(message):478 if self._message_is_obsolete(message):
465 logging.info(479 logging.info(
466 "Response message with operation-id %s was discarded "480 "Response message with operation-id "
467 "because the client's secure ID has changed in the meantime"481 f"{message.get('operation-id')} was discarded "
468 % message.get('operation-id'))482 "because the client's secure ID has changed in the meantime",
483 )
469 return None484 return None
470485
471 # These fields sometimes have really long output we need to trim486 # These fields sometimes have really long output we need to trim
472 self.truncate_message_field('err', message)487 self.truncate_message_field("err", message)
473 self.truncate_message_field('result-text', message)488 self.truncate_message_field("result-text", message)
474489
475 if "timestamp" not in message:490 if "timestamp" not in message:
476 message["timestamp"] = int(self._reactor.time())491 message["timestamp"] = int(self._reactor.time())
@@ -535,12 +550,16 @@ class MessageExchange(object):
535 def _handle_set_intervals(self, message):550 def _handle_set_intervals(self, message):
536 if "exchange" in message:551 if "exchange" in message:
537 self._config.exchange_interval = message["exchange"]552 self._config.exchange_interval = message["exchange"]
538 logging.info("Exchange interval set to %d seconds." %553 logging.info(
539 self._config.exchange_interval)554 "Exchange interval set "
555 f"to {self._config.exchange_interval:d} seconds.",
556 )
540 if "urgent-exchange" in message:557 if "urgent-exchange" in message:
541 self._config.urgent_exchange_interval = message["urgent-exchange"]558 self._config.urgent_exchange_interval = message["urgent-exchange"]
542 logging.info("Urgent exchange interval set to %d seconds." %559 logging.info(
543 self._config.urgent_exchange_interval)560 "Urgent exchange interval set "
561 f"to {self._config.urgent_exchange_interval:d} seconds.",
562 )
544 self._config.write()563 self._config.write()
545564
546 def exchange(self):565 def exchange(self):
@@ -565,19 +584,24 @@ class MessageExchange(object):
565584
566 start_time = time.time()585 start_time = time.time()
567 if self._urgent_exchange:586 if self._urgent_exchange:
568 logging.info("Starting urgent message exchange with %s."587 logging.info(
569 % self._transport.get_url())588 "Starting urgent message exchange "
589 f"with {self._transport.get_url()}.",
590 )
570 else:591 else:
571 logging.info("Starting message exchange with %s."592 logging.info(
572 % self._transport.get_url())593 f"Starting message exchange with {self._transport.get_url()}.",
594 )
573595
574 deferred = Deferred()596 deferred = Deferred()
575597
576 def exchange_completed():598 def exchange_completed():
577 self.schedule_exchange(force=True)599 self.schedule_exchange(force=True)
578 self._reactor.fire("exchange-done")600 self._reactor.fire("exchange-done")
579 logging.info("Message exchange completed in %s.",601 logging.info(
580 format_delta(time.time() - start_time))602 "Message exchange completed in %s.",
603 format_delta(time.time() - start_time),
604 )
581 deferred.callback(None)605 deferred.callback(None)
582606
583 def handle_result(result):607 def handle_result(result):
@@ -627,7 +651,7 @@ class MessageExchange(object):
627 # The error returned is an SSL error, most likely the server651 # The error returned is an SSL error, most likely the server
628 # is using a self-signed certificate. Let's fire a special652 # is using a self-signed certificate. Let's fire a special
629 # event so that the GUI can display a nice message.653 # event so that the GUI can display a nice message.
630 logging.error("Message exchange failed: %s" % error.message)654 logging.error(f"Message exchange failed: {error.message}")
631 ssl_error = True655 ssl_error = True
632656
633 self._reactor.fire("exchange-failed", ssl_error=ssl_error)657 self._reactor.fire("exchange-failed", ssl_error=ssl_error)
@@ -636,16 +660,19 @@ class MessageExchange(object):
636 logging.info("Message exchange failed.")660 logging.info("Message exchange failed.")
637 exchange_completed()661 exchange_completed()
638662
639 self._reactor.call_in_thread(handle_result, handle_failure,663 self._reactor.call_in_thread(
640 self._transport.exchange, payload,664 handle_result,
641 self._registration_info.secure_id,665 handle_failure,
642 self._get_exchange_token(),666 self._transport.exchange,
643 payload.get("server-api"))667 payload,
668 self._registration_info.secure_id,
669 self._get_exchange_token(),
670 payload.get("server-api"),
671 )
644 return deferred672 return deferred
645673
646 def is_urgent(self):674 def is_urgent(self):
647 """Return a bool showing whether there is an urgent exchange scheduled.675 """Return bool showing whether there is an urgent exchange scheduled"""
648 """
649 return self._urgent_exchange676 return self._urgent_exchange
650677
651 def schedule_exchange(self, urgent=False, force=False):678 def schedule_exchange(self, urgent=False, force=False):
@@ -666,9 +693,12 @@ class MessageExchange(object):
666 # The 'not self._exchanging' check below is currently untested.693 # The 'not self._exchanging' check below is currently untested.
667 # It's a bit tricky to test as it is preventing rehooking 'exchange'694 # It's a bit tricky to test as it is preventing rehooking 'exchange'
668 # while there's a background thread doing the exchange itself.695 # while there's a background thread doing the exchange itself.
669 if (not self._exchanging and696 if not self._exchanging and (
670 (force or self._exchange_id is None or697 force
671 urgent and not self._urgent_exchange)):698 or self._exchange_id is None
699 or urgent
700 and not self._urgent_exchange
701 ):
672 if urgent:702 if urgent:
673 self._urgent_exchange = True703 self._urgent_exchange = True
674 if self._exchange_id:704 if self._exchange_id:
@@ -680,18 +710,24 @@ class MessageExchange(object):
680 interval = self._config.exchange_interval710 interval = self._config.exchange_interval
681 backoff_delay = self._backoff_counter.get_random_delay()711 backoff_delay = self._backoff_counter.get_random_delay()
682 if backoff_delay:712 if backoff_delay:
683 logging.warning("Server is busy. Backing off client for {} "713 logging.warning(
684 "seconds".format(backoff_delay))714 "Server is busy. Backing off client for {} "
715 "seconds".format(backoff_delay),
716 )
685 interval += backoff_delay717 interval += backoff_delay
686718
687 if self._notification_id is not None:719 if self._notification_id is not None:
688 self._reactor.cancel_call(self._notification_id)720 self._reactor.cancel_call(self._notification_id)
689 notification_interval = interval - 10721 notification_interval = interval - 10
690 self._notification_id = self._reactor.call_later(722 self._notification_id = self._reactor.call_later(
691 notification_interval, self._notify_impending_exchange)723 notification_interval,
724 self._notify_impending_exchange,
725 )
692726
693 self._exchange_id = self._reactor.call_later(727 self._exchange_id = self._reactor.call_later(
694 interval, self.exchange)728 interval,
729 self.exchange,
730 )
695731
696 def _get_exchange_token(self):732 def _get_exchange_token(self):
697 """Get the token given us by the server at the last exchange.733 """Get the token given us by the server at the last exchange.
@@ -743,13 +779,15 @@ class MessageExchange(object):
743 del messages[i:]779 del messages[i:]
744 else:780 else:
745 server_api = store.get_server_api()781 server_api = store.get_server_api()
746 payload = {"server-api": server_api,782 payload = {
747 "client-api": CLIENT_API,783 "server-api": server_api,
748 "sequence": store.get_sequence(),784 "client-api": CLIENT_API,
749 "accepted-types": accepted_types_digest,785 "sequence": store.get_sequence(),
750 "messages": messages,786 "accepted-types": accepted_types_digest,
751 "total-messages": total_messages,787 "messages": messages,
752 "next-expected-sequence": store.get_server_sequence()}788 "total-messages": total_messages,
789 "next-expected-sequence": store.get_server_sequence(),
790 }
753 accepted_client_types = self.get_client_accepted_message_types()791 accepted_client_types = self.get_client_accepted_message_types()
754 accepted_client_types_hash = self._hash_types(accepted_client_types)792 accepted_client_types_hash = self._hash_types(accepted_client_types)
755 if accepted_client_types_hash != self._client_accepted_types_hash:793 if accepted_client_types_hash != self._client_accepted_types_hash:
@@ -776,7 +814,8 @@ class MessageExchange(object):
776 """814 """
777 message_store = self._message_store815 message_store = self._message_store
778 self._client_accepted_types_hash = result.get(816 self._client_accepted_types_hash = result.get(
779 "client-accepted-types-hash")817 "client-accepted-types-hash",
818 )
780 next_expected = result.get("next-expected-sequence")819 next_expected = result.get("next-expected-sequence")
781 old_sequence = message_store.get_sequence()820 old_sequence = message_store.get_sequence()
782 if next_expected is None:821 if next_expected is None:
@@ -793,8 +832,10 @@ class MessageExchange(object):
793 # let's fire an event to tell all the plugins that they832 # let's fire an event to tell all the plugins that they
794 # ought to generate new messages so the server gets some833 # ought to generate new messages so the server gets some
795 # up-to-date data.834 # up-to-date data.
796 logging.info("Server asked for ancient data: resynchronizing all "835 logging.info(
797 "state with the server.")836 "Server asked for ancient data: resynchronizing all "
837 "state with the server.",
838 )
798 self.send({"type": "resynchronize"})839 self.send({"type": "resynchronize"})
799 self._reactor.fire("resynchronize-clients")840 self._reactor.fire("resynchronize-clients")
800841
@@ -808,8 +849,9 @@ class MessageExchange(object):
808 if new_uuid and isinstance(new_uuid, bytes):849 if new_uuid and isinstance(new_uuid, bytes):
809 new_uuid = new_uuid.decode("ascii")850 new_uuid = new_uuid.decode("ascii")
810 if new_uuid != old_uuid:851 if new_uuid != old_uuid:
811 logging.info("Server UUID changed (old=%s, new=%s)."852 logging.info(
812 % (old_uuid, new_uuid))853 f"Server UUID changed (old={old_uuid}, new={new_uuid}).",
854 )
813 self._reactor.fire("server-uuid-changed", old_uuid, new_uuid)855 self._reactor.fire("server-uuid-changed", old_uuid, new_uuid)
814 message_store.set_server_uuid(new_uuid)856 message_store.set_server_uuid(new_uuid)
815857
@@ -874,12 +916,14 @@ class MessageExchange(object):
874 Any message handlers registered with L{register_message} will916 Any message handlers registered with L{register_message} will
875 be called.917 be called.
876 """918 """
877 if 'operation-id' in message:919 if "operation-id" in message:
878 # This is a message that requires a response. Store the secure ID920 # This is a message that requires a response. Store the secure ID
879 # so we can check for obsolete results later.921 # so we can check for obsolete results later.
880 self._exchange_store.add_message_context(922 self._exchange_store.add_message_context(
881 message['operation-id'], self._registration_info.secure_id,923 message["operation-id"],
882 message['type'])924 self._registration_info.secure_id,
925 message["type"],
926 )
883927
884 self._reactor.fire("message", message)928 self._reactor.fire("message", message)
885 # This has plan interference! but whatever.929 # This has plan interference! but whatever.
@@ -903,9 +947,9 @@ def get_accepted_types_diff(old_types, new_types):
903 stable_types = old_types & new_types947 stable_types = old_types & new_types
904 removed_types = old_types - new_types948 removed_types = old_types - new_types
905 diff = []949 diff = []
906 diff.extend(["+%s" % type for type in added_types])950 diff.extend([f"+{typ}" for typ in added_types])
907 diff.extend(["%s" % type for type in stable_types])951 diff.extend([f"{typ}" for typ in stable_types])
908 diff.extend(["-%s" % type for type in removed_types])952 diff.extend([f"-{typ}" for typ in removed_types])
909 return " ".join(diff)953 return " ".join(diff)
910954
911955
diff --git a/landscape/client/broker/exchangestore.py b/landscape/client/broker/exchangestore.py
index aaa9192..4fb855a 100644
--- a/landscape/client/broker/exchangestore.py
+++ b/landscape/client/broker/exchangestore.py
@@ -9,7 +9,7 @@ except ImportError:
9from landscape.lib.store import with_cursor9from landscape.lib.store import with_cursor
1010
1111
12class MessageContext(object):12class MessageContext:
13 """Stores a context for incoming messages that require a response.13 """Stores a context for incoming messages that require a response.
1414
15 The context consists of15 The context consists of
@@ -39,10 +39,11 @@ class MessageContext(object):
39 def remove(self, cursor):39 def remove(self, cursor):
40 cursor.execute(40 cursor.execute(
41 "DELETE FROM message_context WHERE operation_id=?",41 "DELETE FROM message_context WHERE operation_id=?",
42 (self.operation_id,))42 (self.operation_id,),
43 )
4344
4445
45class ExchangeStore(object):46class ExchangeStore:
46 """Message meta data required by the L{MessageExchange}.47 """Message meta data required by the L{MessageExchange}.
4748
48 The implementation uses a SQLite database as backend, with a single table49 The implementation uses a SQLite database as backend, with a single table
@@ -51,6 +52,7 @@ class ExchangeStore(object):
5152
52 @param filename: The name of the file that contains the sqlite database.53 @param filename: The name of the file that contains the sqlite database.
53 """54 """
55
54 _db = None56 _db = None
5557
56 def __init__(self, filename):58 def __init__(self, filename):
@@ -61,13 +63,20 @@ class ExchangeStore(object):
6163
62 @with_cursor64 @with_cursor
63 def add_message_context(65 def add_message_context(
64 self, cursor, operation_id, secure_id, message_type):66 self,
67 cursor,
68 operation_id,
69 secure_id,
70 message_type,
71 ):
65 """Add a L{MessageContext} with the given data."""72 """Add a L{MessageContext} with the given data."""
66 params = (operation_id, secure_id, message_type, time.time())73 params = (operation_id, secure_id, message_type, time.time())
67 cursor.execute(74 cursor.execute(
68 "INSERT INTO message_context "75 "INSERT INTO message_context "
69 " (operation_id, secure_id, message_type, timestamp) "76 " (operation_id, secure_id, message_type, timestamp) "
70 " VALUES (?,?,?,?)", params)77 " VALUES (?,?,?,?)",
78 params,
79 )
71 return MessageContext(self._db, *params)80 return MessageContext(self._db, *params)
7281
73 @with_cursor82 @with_cursor
@@ -75,7 +84,9 @@ class ExchangeStore(object):
75 """The L{MessageContext} for the given C{operation_id} or C{None}."""84 """The L{MessageContext} for the given C{operation_id} or C{None}."""
76 cursor.execute(85 cursor.execute(
77 "SELECT operation_id, secure_id, message_type, timestamp "86 "SELECT operation_id, secure_id, message_type, timestamp "
78 "FROM message_context WHERE operation_id=?", (operation_id,))87 "FROM message_context WHERE operation_id=?",
88 (operation_id,),
89 )
79 row = cursor.fetchone()90 row = cursor.fetchone()
80 if row:91 if row:
81 return MessageContext(self._db, *row)92 return MessageContext(self._db, *row)
@@ -101,10 +112,12 @@ def ensure_exchange_schema(db):
101 "CREATE TABLE message_context"112 "CREATE TABLE message_context"
102 " (id INTEGER PRIMARY KEY, timestamp TIMESTAMP, "113 " (id INTEGER PRIMARY KEY, timestamp TIMESTAMP, "
103 " secure_id TEXT NOT NULL, operation_id INTEGER NOT NULL, "114 " secure_id TEXT NOT NULL, operation_id INTEGER NOT NULL, "
104 " message_type text NOT NULL)")115 " message_type text NOT NULL)",
116 )
105 cursor.execute(117 cursor.execute(
106 "CREATE UNIQUE INDEX msgctx_operationid_idx ON "118 "CREATE UNIQUE INDEX msgctx_operationid_idx ON "
107 "message_context(operation_id)")119 "message_context(operation_id)",
120 )
108 except (sqlite3.OperationalError, sqlite3.DatabaseError):121 except (sqlite3.OperationalError, sqlite3.DatabaseError):
109 cursor.close()122 cursor.close()
110 db.rollback()123 db.rollback()
diff --git a/landscape/client/broker/ping.py b/landscape/client/broker/ping.py
index 5c8062e..153be3c 100644
--- a/landscape/client/broker/ping.py
+++ b/landscape/client/broker/ping.py
@@ -48,7 +48,7 @@ from landscape.lib.fetch import fetch
48from landscape.lib.log import log_failure48from landscape.lib.log import log_failure
4949
5050
51class PingClient(object):51class PingClient:
52 """An HTTP client which knows how to talk to the ping server."""52 """An HTTP client which knows how to talk to the ping server."""
5353
54 def __init__(self, reactor, get_page=None):54 def __init__(self, reactor, get_page=None):
@@ -74,10 +74,16 @@ class PingClient(object):
7474
75 def errback(type, value, tb):75 def errback(type, value, tb):
76 page_deferred.errback(Failure(value, type, tb))76 page_deferred.errback(Failure(value, type, tb))
77 self._reactor.call_in_thread(page_deferred.callback, errback,77
78 self.get_page, url,78 self._reactor.call_in_thread(
79 post=True, data=data,79 page_deferred.callback,
80 headers=headers)80 errback,
81 self.get_page,
82 url,
83 post=True,
84 data=data,
85 headers=headers,
86 )
81 page_deferred.addCallback(self._got_result)87 page_deferred.addCallback(self._got_result)
82 return page_deferred88 return page_deferred
83 return defer.succeed(False)89 return defer.succeed(False)
@@ -92,7 +98,7 @@ class PingClient(object):
92 return True98 return True
9399
94100
95class Pinger(object):101class Pinger:
96 """102 """
97 A plugin which pings the Landscape server with HTTP requests to103 A plugin which pings the Landscape server with HTTP requests to
98 see if a full exchange should be initiated.104 see if a full exchange should be initiated.
@@ -107,8 +113,14 @@ class Pinger(object):
107 scheduled ping.113 scheduled ping.
108 """114 """
109115
110 def __init__(self, reactor, identity, exchanger, config,116 def __init__(
111 ping_client_factory=PingClient):117 self,
118 reactor,
119 identity,
120 exchanger,
121 config,
122 ping_client_factory=PingClient,
123 ):
112 self._config = config124 self._config = config
113 self._identity = identity125 self._identity = identity
114 self._reactor = reactor126 self._reactor = reactor
@@ -132,33 +144,42 @@ class Pinger(object):
132 def ping(self):144 def ping(self):
133 """Perform a ping; if there are messages, fire an exchange."""145 """Perform a ping; if there are messages, fire an exchange."""
134 deferred = self._ping_client.ping(146 deferred = self._ping_client.ping(
135 self._config.ping_url, self._identity.insecure_id)147 self._config.ping_url,
148 self._identity.insecure_id,
149 )
136 deferred.addCallback(self._got_result)150 deferred.addCallback(self._got_result)
137 deferred.addErrback(self._got_error)151 deferred.addErrback(self._got_error)
138 deferred.addBoth(lambda _: self._schedule())152 deferred.addBoth(lambda _: self._schedule())
139153
140 def _got_result(self, exchange):154 def _got_result(self, exchange):
141 if exchange:155 if exchange:
142 info("Ping indicates message available. "156 info(
143 "Scheduling an urgent exchange.")157 "Ping indicates message available. "
158 "Scheduling an urgent exchange.",
159 )
144 self._exchanger.schedule_exchange(urgent=True)160 self._exchanger.schedule_exchange(urgent=True)
145161
146 def _got_error(self, failure):162 def _got_error(self, failure):
147 log_failure(failure,163 log_failure(
148 "Error contacting ping server at %s" %164 failure,
149 (self._ping_client.url,))165 f"Error contacting ping server at {self._ping_client.url}",
166 )
150167
151 def _schedule(self):168 def _schedule(self):
152 """Schedule a new ping using the current ping interval."""169 """Schedule a new ping using the current ping interval."""
153 self._call_id = self._reactor.call_later(self._config.ping_interval,170 self._call_id = self._reactor.call_later(
154 self.ping)171 self._config.ping_interval,
172 self.ping,
173 )
155174
156 def _handle_set_intervals(self, message):175 def _handle_set_intervals(self, message):
157 if message["type"] == "set-intervals" and "ping" in message:176 if message["type"] == "set-intervals" and "ping" in message:
158 self._config.ping_interval = message["ping"]177 self._config.ping_interval = message["ping"]
159 self._config.write()178 self._config.write()
160 info("Ping interval set to %d seconds." %179 info(
161 self._config.ping_interval)180 f"Ping interval set to {self._config.ping_interval:d} "
181 "seconds.",
182 )
162 if self._call_id is not None:183 if self._call_id is not None:
163 self._reactor.cancel_call(self._call_id)184 self._reactor.cancel_call(self._call_id)
164 self._schedule()185 self._schedule()
@@ -170,8 +191,7 @@ class Pinger(object):
170 self._call_id = None191 self._call_id = None
171192
172193
173class FakePinger(object):194class FakePinger:
174
175 def __init__(self, *args, **kwargs):195 def __init__(self, *args, **kwargs):
176 pass196 pass
177197
diff --git a/landscape/client/broker/registration.py b/landscape/client/broker/registration.py
index 864ce99..c21186e 100644
--- a/landscape/client/broker/registration.py
+++ b/landscape/client/broker/registration.py
@@ -16,10 +16,11 @@ from twisted.internet.defer import Deferred
16from landscape.client.broker.exchange import maybe_bytes16from landscape.client.broker.exchange import maybe_bytes
17from landscape.client.monitor.ubuntuproinfo import get_ubuntu_pro_info17from landscape.client.monitor.ubuntuproinfo import get_ubuntu_pro_info
18from landscape.lib.juju import get_juju_info18from landscape.lib.juju import get_juju_info
19from landscape.lib.tag import is_valid_tag_list
20from landscape.lib.network import get_fqdn19from landscape.lib.network import get_fqdn
21from landscape.lib.vm_info import get_vm_info, get_container_info20from landscape.lib.tag import is_valid_tag_list
22from landscape.lib.versioning import is_version_higher21from landscape.lib.versioning import is_version_higher
22from landscape.lib.vm_info import get_container_info
23from landscape.lib.vm_info import get_vm_info
2324
2425
25class RegistrationError(Exception):26class RegistrationError(Exception):
@@ -31,7 +32,6 @@ class RegistrationError(Exception):
3132
3233
33def persist_property(name):34def persist_property(name):
34
35 def get(self):35 def get(self):
36 value = self._persist.get(name)36 value = self._persist.get(name)
37 try:37 try:
@@ -46,14 +46,13 @@ def persist_property(name):
4646
4747
48def config_property(name):48def config_property(name):
49
50 def get(self):49 def get(self):
51 return getattr(self._config, name)50 return getattr(self._config, name)
5251
53 return property(get)52 return property(get)
5453
5554
56class Identity(object):55class Identity:
57 """Maintains details about the identity of this Landscape client.56 """Maintains details about the identity of this Landscape client.
5857
59 @ivar secure_id: A server-provided ID for secure message exchange.58 @ivar secure_id: A server-provided ID for secure message exchange.
@@ -83,7 +82,7 @@ class Identity(object):
83 self._persist = persist.root_at("registration")82 self._persist = persist.root_at("registration")
8483
8584
86class RegistrationHandler(object):85class RegistrationHandler:
87 """86 """
88 An object from which registration can be requested of the server,87 An object from which registration can be requested of the server,
89 and which will handle forced ID changes from the server.88 and which will handle forced ID changes from the server.
@@ -91,8 +90,16 @@ class RegistrationHandler(object):
91 L{register} should be used to perform initial registration.90 L{register} should be used to perform initial registration.
92 """91 """
9392
94 def __init__(self, config, identity, reactor, exchange, pinger,93 def __init__(
95 message_store, fetch_async=None):94 self,
95 config,
96 identity,
97 reactor,
98 exchange,
99 pinger,
100 message_store,
101 fetch_async=None,
102 ):
96 self._config = config103 self._config = config
97 self._identity = identity104 self._identity = identity
98 self._reactor = reactor105 self._reactor = reactor
@@ -104,8 +111,10 @@ class RegistrationHandler(object):
104 self._reactor.call_on("exchange-done", self._handle_exchange_done)111 self._reactor.call_on("exchange-done", self._handle_exchange_done)
105 self._exchange.register_message("set-id", self._handle_set_id)112 self._exchange.register_message("set-id", self._handle_set_id)
106 self._exchange.register_message("unknown-id", self._handle_unknown_id)113 self._exchange.register_message("unknown-id", self._handle_unknown_id)
107 self._exchange.register_message("registration",114 self._exchange.register_message(
108 self._handle_registration)115 "registration",
116 self._handle_registration,
117 )
109 self._should_register = None118 self._should_register = None
110 self._fetch_async = fetch_async119 self._fetch_async = fetch_async
111 self._juju_data = None120 self._juju_data = None
@@ -116,9 +125,11 @@ class RegistrationHandler(object):
116 if id.secure_id:125 if id.secure_id:
117 return False126 return False
118127
119 return bool(id.computer_title and128 return bool(
120 id.account_name and129 id.computer_title
121 self._message_store.accepts("register"))130 and id.account_name
131 and self._message_store.accepts("register"),
132 )
122133
123 def register(self):134 def register(self):
124 """135 """
@@ -186,14 +197,16 @@ class RegistrationHandler(object):
186 tags = None197 tags = None
187 logging.error("Invalid tags provided for registration.")198 logging.error("Invalid tags provided for registration.")
188199
189 message = {"type": "register",200 message = {
190 "hostname": get_fqdn(),201 "type": "register",
191 "account_name": account_name,202 "hostname": get_fqdn(),
192 "computer_title": identity.computer_title,203 "account_name": account_name,
193 "registration_password": identity.registration_key,204 "computer_title": identity.computer_title,
194 "tags": tags,205 "registration_password": identity.registration_key,
195 "container-info": get_container_info(),206 "tags": tags,
196 "vm-info": get_vm_info()}207 "container-info": get_container_info(),
208 "vm-info": get_vm_info(),
209 }
197210
198 if self._clone_secure_id:211 if self._clone_secure_id:
199 # We use the secure id here because the registration is encrypted212 # We use the secure id here because the registration is encrypted
@@ -216,19 +229,20 @@ class RegistrationHandler(object):
216 message["juju-info"] = {229 message["juju-info"] = {
217 "environment-uuid": self._juju_data["environment-uuid"],230 "environment-uuid": self._juju_data["environment-uuid"],
218 "api-addresses": self._juju_data["api-addresses"],231 "api-addresses": self._juju_data["api-addresses"],
219 "machine-id": self._juju_data["machine-id"]}232 "machine-id": self._juju_data["machine-id"],
233 }
220234
221 # The computer is a normal computer, possibly a container.235 # The computer is a normal computer, possibly a container.
222 with_word = "with" if bool(registration_key) else "without"236 with_word = "with" if bool(registration_key) else "without"
223 with_tags = "and tags %s " % tags if tags else ""237 with_tags = f"and tags {tags} " if tags else ""
224 with_group = "in access group '%s' " % group if group else ""238 with_group = f"in access group '{group}' " if group else ""
225239
226 message["ubuntu_pro_info"] = json.dumps(get_ubuntu_pro_info())240 message["ubuntu_pro_info"] = json.dumps(get_ubuntu_pro_info())
227241
228 logging.info(242 logging.info(
229 u"Queueing message to register with account %r %s%s"243 f"Queueing message to register with account {account_name!r} "
230 "%s a password." % (244 f"{with_group}{with_tags}{with_word} a password.",
231 account_name, with_group, with_tags, with_word))245 )
232 self._exchange.send(message)246 self._exchange.send(message)
233247
234 def _handle_set_id(self, message):248 def _handle_set_id(self, message):
@@ -239,15 +253,18 @@ class RegistrationHandler(object):
239253
240 Fire C{"registration-done"} and C{"resynchronize-clients"}.254 Fire C{"registration-done"} and C{"resynchronize-clients"}.
241 """255 """
242 id = self._identity256 cid = self._identity
243 if id.secure_id:257 if cid.secure_id:
244 logging.info("Overwriting secure_id with '%s'" % id.secure_id)258 logging.info(f"Overwriting secure_id with '{cid.secure_id}'")
245259
246 id.secure_id = message.get("id")260 cid.secure_id = message.get("id")
247 id.insecure_id = message.get("insecure-id")261 cid.insecure_id = message.get("insecure-id")
248 logging.info("Using new secure-id ending with %s for account %s.",262 logging.info(
249 id.secure_id[-10:], id.account_name)263 "Using new secure-id ending with %s for account %s.",
250 logging.debug("Using new secure-id: %s", id.secure_id)264 cid.secure_id[-10:],
265 cid.account_name,
266 )
267 logging.debug("Using new secure-id: %s", cid.secure_id)
251 self._reactor.fire("registration-done")268 self._reactor.fire("registration-done")
252 self._reactor.fire("resynchronize-clients")269 self._reactor.fire("resynchronize-clients")
253270
@@ -257,19 +274,21 @@ class RegistrationHandler(object):
257 self._reactor.fire("registration-failed", reason=message_info)274 self._reactor.fire("registration-failed", reason=message_info)
258275
259 def _handle_unknown_id(self, message):276 def _handle_unknown_id(self, message):
260 id = self._identity277 cid = self._identity
261 clone = message.get("clone-of")278 clone = message.get("clone-of")
262 if clone is None:279 if clone is None:
263 logging.info("Client has unknown secure-id for account %s."280 logging.info(
264 % id.account_name)281 "Client has unknown secure-id for account "
282 f"{cid.account_name}.",
283 )
265 else: # Save the secure id as the clone, and clear it so it's renewed284 else: # Save the secure id as the clone, and clear it so it's renewed
266 logging.info("Client is clone of computer %s" % clone)285 logging.info(f"Client is clone of computer {clone}")
267 self._clone_secure_id = id.secure_id286 self._clone_secure_id = cid.secure_id
268 id.secure_id = None287 cid.secure_id = None
269 id.insecure_id = None288 cid.insecure_id = None
270289
271290
272class RegistrationResponse(object):291class RegistrationResponse:
273 """A helper for dealing with the response of a single registration request.292 """A helper for dealing with the response of a single registration request.
274293
275 @ivar deferred: The L{Deferred} that will be fired as per294 @ivar deferred: The L{Deferred} that will be fired as per
diff --git a/landscape/client/broker/server.py b/landscape/client/broker/server.py
index 1f8d708..766c6a0 100644
--- a/landscape/client/broker/server.py
+++ b/landscape/client/broker/server.py
@@ -42,15 +42,14 @@ Diagram::
42 : exchange42 : exchange
4343
44"""44"""
45
46import logging45import logging
4746
48from twisted.internet.defer import Deferred47from twisted.internet.defer import Deferred
49from landscape.lib.compat import _PY3
5048
51from landscape.lib.twisted_util import gather_results
52from landscape.client.amp import remote49from landscape.client.amp import remote
53from landscape.client.manager.manager import FAILED50from landscape.client.manager.manager import FAILED
51from landscape.lib.compat import _PY3
52from landscape.lib.twisted_util import gather_results
5453
5554
56def event(method):55def event(method):
@@ -71,7 +70,7 @@ def event(method):
71 return broadcast_event70 return broadcast_event
7271
7372
74class BrokerServer(object):73class BrokerServer:
75 """74 """
76 A broker server capable of handling messages from plugins connected using75 A broker server capable of handling messages from plugins connected using
77 the L{BrokerProtocol}.76 the L{BrokerProtocol}.
@@ -82,11 +81,20 @@ class BrokerServer(object):
82 @param registration: The {RegistrationHandler}.81 @param registration: The {RegistrationHandler}.
83 @param message_store: The broker's L{MessageStore}.82 @param message_store: The broker's L{MessageStore}.
84 """83 """
84
85 name = "broker"85 name = "broker"
8686
87 def __init__(self, config, reactor, exchange, registration,87 def __init__(
88 message_store, pinger):88 self,
89 config,
90 reactor,
91 exchange,
92 registration,
93 message_store,
94 pinger,
95 ):
89 from landscape.client.broker.amp import get_component_registry96 from landscape.client.broker.amp import get_component_registry
97
90 self.connectors_registry = get_component_registry()98 self.connectors_registry = get_component_registry()
91 self._config = config99 self._config = config
92 self._reactor = reactor100 self._reactor = reactor
@@ -99,8 +107,10 @@ class BrokerServer(object):
99107
100 reactor.call_on("message", self.broadcast_message)108 reactor.call_on("message", self.broadcast_message)
101 reactor.call_on("impending-exchange", self.impending_exchange)109 reactor.call_on("impending-exchange", self.impending_exchange)
102 reactor.call_on("message-type-acceptance-changed",110 reactor.call_on(
103 self.message_type_acceptance_changed)111 "message-type-acceptance-changed",
112 self.message_type_acceptance_changed,
113 )
104 reactor.call_on("server-uuid-changed", self.server_uuid_changed)114 reactor.call_on("server-uuid-changed", self.server_uuid_changed)
105 reactor.call_on("package-data-changed", self.package_data_changed)115 reactor.call_on("package-data-changed", self.package_data_changed)
106 reactor.call_on("resynchronize-clients", self.resynchronize)116 reactor.call_on("resynchronize-clients", self.resynchronize)
@@ -201,7 +211,9 @@ class BrokerServer(object):
201 message = {k.decode("ascii"): v for k, v in message.items()}211 message = {k.decode("ascii"): v for k, v in message.items()}
202 message["type"] = message["type"].decode("ascii")212 message["type"] = message["type"].decode("ascii")
203 if isinstance(session_id, bool) and message["type"] in (213 if isinstance(session_id, bool) and message["type"] in (
204 "operation-result", "change-packages-result"):214 "operation-result",
215 "change-packages-result",
216 ):
205 # XXX This means we're performing a Landscape-driven upgrade and217 # XXX This means we're performing a Landscape-driven upgrade and
206 # we're being invoked by a package-changer or release-upgrader218 # we're being invoked by a package-changer or release-upgrader
207 # process that is running code which doesn't know about the219 # process that is running code which doesn't know about the
@@ -218,7 +230,8 @@ class BrokerServer(object):
218230
219 if session_id is None:231 if session_id is None:
220 raise RuntimeError(232 raise RuntimeError(
221 "Session ID must be set before attempting to send a message")233 "Session ID must be set before attempting to send a message",
234 )
222 if self._message_store.is_valid_session_id(session_id):235 if self._message_store.is_valid_session_id(session_id):
223 return self._exchanger.send(message, urgent=urgent)236 return self._exchanger.send(message, urgent=urgent)
224237
@@ -320,7 +333,6 @@ class BrokerServer(object):
320 calls = []333 calls = []
321334
322 def get_handler(event_type):335 def get_handler(event_type):
323
324 def handler(**kwargs):336 def handler(**kwargs):
325 for call in calls:337 for call in calls:
326 self._reactor.cancel_call(call)338 self._reactor.cancel_call(call)
@@ -367,26 +379,30 @@ class BrokerServer(object):
367 indicating as such.379 indicating as such.
368 """380 """
369 opid = message.get("operation-id")381 opid = message.get("operation-id")
370 if (True not in results and382 if (
371 opid is not None and383 True not in results
372 message["type"] != "resynchronize"384 and opid is not None
373 ):385 and message["type"] != "resynchronize"
386 ):
374387
375 mtype = message["type"]388 mtype = message["type"]
376 logging.error("Nobody handled the %s message." % (mtype,))389 logging.error(f"Nobody handled the {mtype} message.")
377390
378 result_text = """\391 result_text = """\
379Landscape client failed to handle this request (%s) because the392Landscape client failed to handle this request ({}) because the
380plugin which should handle it isn't available. This could mean that the393plugin which should handle it isn't available. This could mean that the
381plugin has been intentionally disabled, or that the client isn't running394plugin has been intentionally disabled, or that the client isn't running
382properly, or you may be running an older version of the client that doesn't395properly, or you may be running an older version of the client that doesn't
383support this feature.396support this feature.
384""" % (mtype,)397""".format(
398 mtype,
399 )
385 response = {400 response = {
386 "type": "operation-result",401 "type": "operation-result",
387 "status": FAILED,402 "status": FAILED,
388 "result-text": result_text,403 "result-text": result_text,
389 "operation-id": opid}404 "operation-id": opid,
405 }
390 self._exchanger.send(response, urgent=True)406 self._exchanger.send(response, urgent=True)
391407
392 @remote408 @remote
diff --git a/landscape/client/broker/service.py b/landscape/client/broker/service.py
index 7e36149..9c86267 100644
--- a/landscape/client/broker/service.py
+++ b/landscape/client/broker/service.py
@@ -1,17 +1,19 @@
1"""Deployment code for the monitor."""1"""Deployment code for the monitor."""
2
3import os2import os
43
5from landscape.client.service import LandscapeService, run_landscape_service
6from landscape.client.amp import ComponentPublisher4from landscape.client.amp import ComponentPublisher
7from landscape.client.broker.registration import RegistrationHandler, Identity
8from landscape.client.broker.config import BrokerConfiguration5from landscape.client.broker.config import BrokerConfiguration
9from landscape.client.broker.transport import HTTPTransport
10from landscape.client.broker.exchange import MessageExchange6from landscape.client.broker.exchange import MessageExchange
11from landscape.client.broker.exchangestore import ExchangeStore7from landscape.client.broker.exchangestore import ExchangeStore
12from landscape.client.broker.ping import Pinger8from landscape.client.broker.ping import Pinger
13from landscape.client.broker.store import get_default_message_store9from landscape.client.broker.registration import Identity
10from landscape.client.broker.registration import RegistrationHandler
14from landscape.client.broker.server import BrokerServer11from landscape.client.broker.server import BrokerServer
12from landscape.client.broker.store import get_default_message_store
13from landscape.client.broker.transport import HTTPTransport
14from landscape.client.service import LandscapeService
15from landscape.client.service import run_landscape_service
16from landscape.client.watchdog import bootstrap_list
1517
1618
17class BrokerService(LandscapeService):19class BrokerService(LandscapeService):
@@ -44,48 +46,82 @@ class BrokerService(LandscapeService):
44 service_name = BrokerServer.name46 service_name = BrokerServer.name
4547
46 def __init__(self, config):48 def __init__(self, config):
49 self._config = config
47 self.persist_filename = os.path.join(50 self.persist_filename = os.path.join(
48 config.data_path, "%s.bpickle" % (self.service_name,))51 config.data_path,
49 super(BrokerService, self).__init__(config)52 f"{self.service_name}.bpickle",
53 )
54 super().__init__(config)
5055
51 self.transport = self.transport_factory(56 self.transport = self.transport_factory(
52 self.reactor, config.url, config.ssl_public_key)57 self.reactor,
58 config.url,
59 config.ssl_public_key,
60 )
53 self.message_store = get_default_message_store(61 self.message_store = get_default_message_store(
54 self.persist, config.message_store_path)62 self.persist,
63 config.message_store_path,
64 )
55 self.identity = Identity(self.config, self.persist)65 self.identity = Identity(self.config, self.persist)
56 exchange_store = ExchangeStore(self.config.exchange_store_path)66 exchange_store = ExchangeStore(self.config.exchange_store_path)
57 self.exchanger = MessageExchange(67 self.exchanger = MessageExchange(
58 self.reactor, self.message_store, self.transport, self.identity,68 self.reactor,
59 exchange_store, config)69 self.message_store,
70 self.transport,
71 self.identity,
72 exchange_store,
73 config,
74 )
60 self.pinger = self.pinger_factory(75 self.pinger = self.pinger_factory(
61 self.reactor, self.identity, self.exchanger, config)76 self.reactor,
77 self.identity,
78 self.exchanger,
79 config,
80 )
62 self.registration = RegistrationHandler(81 self.registration = RegistrationHandler(
63 config, self.identity, self.reactor, self.exchanger, self.pinger,82 config,
64 self.message_store)83 self.identity,
65 self.broker = BrokerServer(self.config, self.reactor, self.exchanger,84 self.reactor,
66 self.registration, self.message_store,85 self.exchanger,
67 self.pinger)86 self.pinger,
68 self.publisher = ComponentPublisher(self.broker, self.reactor,87 self.message_store,
69 self.config)88 )
7089 self.broker = BrokerServer(
71 def startService(self):90 self.config,
91 self.reactor,
92 self.exchanger,
93 self.registration,
94 self.message_store,
95 self.pinger,
96 )
97 self.publisher = ComponentPublisher(
98 self.broker,
99 self.reactor,
100 self.config,
101 )
102
103 def startService(self): # noqa: N802
72 """Start the broker.104 """Start the broker.
73105
74 Create a L{BrokerServer} listening on C{broker_socket_path} for clients106 Create a L{BrokerServer} listening on C{broker_socket_path} for clients
75 connecting with the L{BrokerServerConnector}, and start the107 connecting with the L{BrokerServerConnector}, and start the
76 L{MessageExchange} and L{Pinger} services.108 L{MessageExchange} and L{Pinger} services.
77 """109 """
78 super(BrokerService, self).startService()110 super().startService()
111 bootstrap_list.bootstrap(
112 data_path=self._config.data_path,
113 log_dir=self._config.log_dir,
114 )
79 self.publisher.start()115 self.publisher.start()
80 self.exchanger.start()116 self.exchanger.start()
81 self.pinger.start()117 self.pinger.start()
82118
83 def stopService(self):119 def stopService(self): # noqa: N802
84 """Stop the broker."""120 """Stop the broker."""
85 deferred = self.publisher.stop()121 deferred = self.publisher.stop()
86 self.exchanger.stop()122 self.exchanger.stop()
87 self.pinger.stop()123 self.pinger.stop()
88 super(BrokerService, self).stopService()124 super().stopService()
89 return deferred125 return deferred
90126
91127
diff --git a/landscape/client/broker/store.py b/landscape/client/broker/store.py
index ebab5cf..7557d45 100644
--- a/landscape/client/broker/store.py
+++ b/landscape/client/broker/store.py
@@ -91,25 +91,28 @@ See L{MessageStore} for details about how messages are stored on the file
91system and L{landscape.lib.message.got_next_expected} to check how the91system and L{landscape.lib.message.got_next_expected} to check how the
92strategy for updating the pending offset and the sequence is implemented.92strategy for updating the pending offset and the sequence is implemented.
93"""93"""
94
95import itertools94import itertools
96import logging95import logging
97import os96import os
97import shutil
98import traceback
98import uuid99import uuid
99100
100from twisted.python.compat import iteritems101from twisted.python.compat import iteritems
101102
102from landscape import DEFAULT_SERVER_API103from landscape import DEFAULT_SERVER_API
103from landscape.lib import bpickle104from landscape.lib import bpickle
104from landscape.lib.fs import create_binary_file, read_binary_file105from landscape.lib.fs import create_binary_file
105from landscape.lib.versioning import sort_versions, is_version_higher106from landscape.lib.fs import read_binary_file
107from landscape.lib.versioning import is_version_higher
108from landscape.lib.versioning import sort_versions
106109
107110
108HELD = "h"111HELD = "h"
109BROKEN = "b"112BROKEN = "b"
110113
111114
112class MessageStore(object):115class MessageStore:
113 """A message store which stores its messages in a file system hierarchy.116 """A message store which stores its messages in a file system hierarchy.
114117
115 Beside the "sequence" and the "pending offset" values described in the118 Beside the "sequence" and the "pending offset" values described in the
@@ -134,9 +137,12 @@ class MessageStore(object):
134 # in case the server supports it.137 # in case the server supports it.
135 _api = DEFAULT_SERVER_API138 _api = DEFAULT_SERVER_API
136139
137 def __init__(self, persist, directory, directory_size=1000):140 def __init__(self, persist, directory, directory_size=1000, max_dirs=4,
141 max_size_mb=400):
138 self._directory = directory142 self._directory = directory
139 self._directory_size = directory_size143 self._directory_size = directory_size
144 self._max_dirs = max_dirs # Maximum number of directories in store
145 self._max_size_mb = max_size_mb # Maximum size of message store
140 self._schemas = {}146 self._schemas = {}
141 self._original_persist = persist147 self._original_persist = persist
142 self._persist = persist.root_at("message-store")148 self._persist = persist.root_at("message-store")
@@ -273,7 +279,7 @@ class MessageStore(object):
273 logging.exception(e)279 logging.exception(e)
274 self._add_flags(filename, BROKEN)280 self._add_flags(filename, BROKEN)
275 else:281 else:
276 if u"type" not in message:282 if "type" not in message:
277 # Special case to decode keys for messages which were283 # Special case to decode keys for messages which were
278 # serialized by py27 prior to py3 upgrade, and having284 # serialized by py27 prior to py3 upgrade, and having
279 # implicit byte message keys. Message may still get285 # implicit byte message keys. Message may still get
@@ -281,8 +287,9 @@ class MessageStore(object):
281 # broker. (lp: #1718689)287 # broker. (lp: #1718689)
282 message = {288 message = {
283 (k if isinstance(k, str) else k.decode("ascii")): v289 (k if isinstance(k, str) else k.decode("ascii")): v
284 for k, v in message.items()}290 for k, v in message.items()
285 message[u"type"] = message[u"type"].decode("ascii")291 }
292 message["type"] = message["type"].decode("ascii")
286293
287 unknown_type = message["type"] not in accepted_types294 unknown_type = message["type"] not in accepted_types
288 unknown_api = not is_version_higher(server_api, message["api"])295 unknown_api = not is_version_higher(server_api, message["api"])
@@ -292,10 +299,53 @@ class MessageStore(object):
292 messages.append(message)299 messages.append(message)
293 return messages300 return messages
294301
302 def get_messages_total_size(self):
303 """Get total size of messages directory"""
304 sizes = []
305 for dirname in os.listdir(self._directory):
306 dirpath = os.path.join(self._directory, dirname)
307 dirsize = sum(file.stat().st_size for file in os.scandir(dirpath))
308 sizes.append(dirsize)
309 return sum(sizes)
310
311 def delete_messages_over_limit(self):
312 """
313 Delete messages dirs if there's any over the max, which happens if
314 messages are queued up but not able to be sent
315 """
316
317 cur_dirs = os.listdir(self._directory)
318 cur_dirs.sort(key=int) # Since you could have 0, .., 9, 10
319 num_dirs = len(cur_dirs)
320
321 num_dirs_to_delete = max(0, num_dirs - self._max_dirs) # No negatives
322 dirs_to_delete = cur_dirs[:num_dirs_to_delete] # Chop off beginning
323
324 for dirname in dirs_to_delete:
325 dirpath = os.path.join(self._directory, dirname)
326 try:
327 logging.debug(f"Trimming message store: {dirpath}")
328 shutil.rmtree(dirpath)
329 except Exception: # We want to continue like normal if any error
330 logging.warning(traceback.format_exc())
331 logging.warning("Unable to delete message directory!")
332 logging.warning(dirpath)
333
334 # Something is wrong if after deleting a bunch of files, we are still
335 # using too much space. Rather then look around for big files, we just
336 # start over.
337 num_bytes = self.get_messages_total_size()
338 num_mb = num_bytes / 1e6
339 if num_mb > self._max_size_mb:
340 logging.warning("Messages too large! Clearing all messages!")
341 self.delete_all_messages()
342
295 def delete_old_messages(self):343 def delete_old_messages(self):
296 """Delete messages which are unlikely to be needed in the future."""344 """Delete messages which are unlikely to be needed in the future."""
297 for fn in itertools.islice(self._walk_messages(exclude=HELD + BROKEN),345 for fn in itertools.islice(
298 self.get_pending_offset()):346 self._walk_messages(exclude=HELD + BROKEN),
347 self.get_pending_offset(),
348 ):
299 os.unlink(fn)349 os.unlink(fn)
300 containing_dir = os.path.split(fn)[0]350 containing_dir = os.path.split(fn)[0]
301 if not os.listdir(containing_dir):351 if not os.listdir(containing_dir):
@@ -326,9 +376,9 @@ class MessageStore(object):
326 pending_offset = self.get_pending_offset()376 pending_offset = self.get_pending_offset()
327 for filename in self._walk_messages(exclude=BROKEN):377 for filename in self._walk_messages(exclude=BROKEN):
328 flags = self._get_flags(filename)378 flags = self._get_flags(filename)
329 if ((HELD in flags or i >= pending_offset) and379 if (HELD in flags or i >= pending_offset) and os.stat(
330 os.stat(filename).st_ino == message_id380 filename,
331 ):381 ).st_ino == message_id:
332 return True382 return True
333 if BROKEN not in flags and HELD not in flags:383 if BROKEN not in flags and HELD not in flags:
334 i += 1384 i += 1
@@ -347,7 +397,8 @@ class MessageStore(object):
347 if not self._persist.has("first-failure-time"):397 if not self._persist.has("first-failure-time"):
348 self._persist.set("first-failure-time", timestamp)398 self._persist.set("first-failure-time", timestamp)
349 continued_failure_time = timestamp - self._persist.get(399 continued_failure_time = timestamp - self._persist.get(
350 "first-failure-time")400 "first-failure-time",
401 )
351 if self._persist.get("blackhole-messages"):402 if self._persist.get("blackhole-messages"):
352 # Already added the resync message403 # Already added the resync message
353 return404 return
@@ -357,7 +408,8 @@ class MessageStore(object):
357 self._persist.set("blackhole-messages", True)408 self._persist.set("blackhole-messages", True)
358 logging.warning(409 logging.warning(
359 "Unable to succesfully communicate with Landscape server "410 "Unable to succesfully communicate with Landscape server "
360 "for more than a week. Waiting for resync.")411 "for more than a week. Waiting for resync.",
412 )
361413
362 def add(self, message):414 def add(self, message):
363 """Queue a message for delivery.415 """Queue a message for delivery.
@@ -373,6 +425,8 @@ class MessageStore(object):
373 logging.debug("Dropped message, awaiting resync.")425 logging.debug("Dropped message, awaiting resync.")
374 return426 return
375427
428 self.delete_messages_over_limit()
429
376 server_api = self.get_server_api()430 server_api = self.get_server_api()
377431
378 if "api" not in message:432 if "api" not in message:
@@ -432,7 +486,8 @@ class MessageStore(object):
432 """Walk the files which are definitely pending."""486 """Walk the files which are definitely pending."""
433 pending_offset = self.get_pending_offset()487 pending_offset = self.get_pending_offset()
434 for i, filename in enumerate(488 for i, filename in enumerate(
435 self._walk_messages(exclude=HELD + BROKEN)):489 self._walk_messages(exclude=HELD + BROKEN),
490 ):
436 if i >= pending_offset:491 if i >= pending_offset:
437 yield filename492 yield filename
438493
@@ -443,12 +498,15 @@ class MessageStore(object):
443 for message_dir in message_dirs:498 for message_dir in message_dirs:
444 for filename in self._get_sorted_filenames(message_dir):499 for filename in self._get_sorted_filenames(message_dir):
445 flags = set(self._get_flags(filename))500 flags = set(self._get_flags(filename))
446 if (not exclude or not exclude & flags):501 if not exclude or not exclude & flags:
447 yield self._message_dir(message_dir, filename)502 yield self._message_dir(message_dir, filename)
448503
449 def _get_sorted_filenames(self, dir=""):504 def _get_sorted_filenames(self, dir=""):
450 message_files = [x for x in os.listdir(self._message_dir(dir))505 message_files = [
451 if not x.endswith(".tmp")]506 x
507 for x in os.listdir(self._message_dir(dir))
508 if not x.endswith(".tmp")
509 ]
452 message_files.sort(key=lambda x: int(x.split("_")[0]))510 message_files.sort(key=lambda x: int(x.split("_")[0]))
453 return message_files511 return message_files
454512
@@ -549,6 +607,7 @@ def get_default_message_store(*args, **kwargs):
549 Get a L{MessageStore} object with all Landscape message schemas added.607 Get a L{MessageStore} object with all Landscape message schemas added.
550 """608 """
551 from landscape.message_schemas.server_bound import message_schemas609 from landscape.message_schemas.server_bound import message_schemas
610
552 store = MessageStore(*args, **kwargs)611 store = MessageStore(*args, **kwargs)
553 for schema in message_schemas:612 for schema in message_schemas:
554 store.add_schema(schema)613 store.add_schema(schema)
diff --git a/landscape/client/broker/tests/helpers.py b/landscape/client/broker/tests/helpers.py
index 367a03f..2ce52b0 100644
--- a/landscape/client/broker/tests/helpers.py
+++ b/landscape/client/broker/tests/helpers.py
@@ -7,23 +7,24 @@ connected to remote test L{BrokerClient}.
7"""7"""
8import os8import os
99
10from landscape.lib.persist import Persist
11from landscape.lib.testing import FakeReactor
12from landscape.client.watchdog import bootstrap_list
13from landscape.client.amp import ComponentPublisher10from landscape.client.amp import ComponentPublisher
14from landscape.client.broker.transport import FakeTransport11from landscape.client.broker.amp import RemoteBrokerConnector
12from landscape.client.broker.client import BrokerClient
13from landscape.client.broker.config import BrokerConfiguration
15from landscape.client.broker.exchange import MessageExchange14from landscape.client.broker.exchange import MessageExchange
16from landscape.client.broker.exchangestore import ExchangeStore15from landscape.client.broker.exchangestore import ExchangeStore
17from landscape.client.broker.store import get_default_message_store
18from landscape.client.broker.registration import Identity, RegistrationHandler
19from landscape.client.broker.ping import Pinger16from landscape.client.broker.ping import Pinger
20from landscape.client.broker.config import BrokerConfiguration17from landscape.client.broker.registration import Identity
18from landscape.client.broker.registration import RegistrationHandler
21from landscape.client.broker.server import BrokerServer19from landscape.client.broker.server import BrokerServer
22from landscape.client.broker.amp import RemoteBrokerConnector20from landscape.client.broker.store import get_default_message_store
23from landscape.client.broker.client import BrokerClient21from landscape.client.broker.transport import FakeTransport
22from landscape.client.watchdog import bootstrap_list
23from landscape.lib.persist import Persist
24from landscape.lib.testing import FakeReactor
2425
2526
26class BrokerConfigurationHelper(object):27class BrokerConfigurationHelper:
27 """Setup a L{BrokerConfiguration} instance with some test config values.28 """Setup a L{BrokerConfiguration} instance with some test config values.
2829
29 The following attributes will be set on your test case:30 The following attributes will be set on your test case:
@@ -37,8 +38,10 @@ class BrokerConfigurationHelper(object):
37 def set_up(self, test_case):38 def set_up(self, test_case):
38 data_path = test_case.makeDir()39 data_path = test_case.makeDir()
39 log_dir = test_case.makeDir()40 log_dir = test_case.makeDir()
40 test_case.config_filename = os.path.join(test_case.makeDir(),41 test_case.config_filename = os.path.join(
41 "client.conf")42 test_case.makeDir(),
43 "client.conf",
44 )
4245
43 with open(test_case.config_filename, "w") as fh:46 with open(test_case.config_filename, "w") as fh:
44 fh.write(47 fh.write(
@@ -47,8 +50,9 @@ class BrokerConfigurationHelper(object):
47 "computer_title = Some Computer\n"50 "computer_title = Some Computer\n"
48 "account_name = some_account\n"51 "account_name = some_account\n"
49 "ping_url = http://localhost:91910\n"52 "ping_url = http://localhost:91910\n"
50 "data_path = %s\n"53 f"data_path = {data_path}\n"
51 "log_dir = %s\n" % (data_path, log_dir))54 f"log_dir = {log_dir}\n",
55 )
5256
53 bootstrap_list.bootstrap(data_path=data_path, log_dir=log_dir)57 bootstrap_list.bootstrap(data_path=data_path, log_dir=log_dir)
5458
@@ -87,20 +91,31 @@ class ExchangeHelper(BrokerConfigurationHelper):
87 """91 """
8892
89 def set_up(self, test_case):93 def set_up(self, test_case):
90 super(ExchangeHelper, self).set_up(test_case)94 super().set_up(test_case)
91 test_case.persist_filename = test_case.makePersistFile()95 test_case.persist_filename = test_case.makePersistFile()
92 test_case.persist = Persist(filename=test_case.persist_filename)96 test_case.persist = Persist(filename=test_case.persist_filename)
93 test_case.mstore = get_default_message_store(97 test_case.mstore = get_default_message_store(
94 test_case.persist, test_case.config.message_store_path)98 test_case.persist,
99 test_case.config.message_store_path,
100 )
95 test_case.identity = Identity(test_case.config, test_case.persist)101 test_case.identity = Identity(test_case.config, test_case.persist)
96 test_case.transport = FakeTransport(None, test_case.config.url,102 test_case.transport = FakeTransport(
97 test_case.config.ssl_public_key)103 None,
104 test_case.config.url,
105 test_case.config.ssl_public_key,
106 )
98 test_case.reactor = FakeReactor()107 test_case.reactor = FakeReactor()
99 test_case.exchange_store = ExchangeStore(108 test_case.exchange_store = ExchangeStore(
100 test_case.config.exchange_store_path)109 test_case.config.exchange_store_path,
110 )
101 test_case.exchanger = MessageExchange(111 test_case.exchanger = MessageExchange(
102 test_case.reactor, test_case.mstore, test_case.transport,112 test_case.reactor,
103 test_case.identity, test_case.exchange_store, test_case.config)113 test_case.mstore,
114 test_case.transport,
115 test_case.identity,
116 test_case.exchange_store,
117 test_case.config,
118 )
104119
105120
106class RegistrationHelper(ExchangeHelper):121class RegistrationHelper(ExchangeHelper):
@@ -116,16 +131,27 @@ class RegistrationHelper(ExchangeHelper):
116 """131 """
117132
118 def set_up(self, test_case):133 def set_up(self, test_case):
119 super(RegistrationHelper, self).set_up(test_case)134 super().set_up(test_case)
120 test_case.pinger = Pinger(test_case.reactor, test_case.identity,135 test_case.pinger = Pinger(
121 test_case.exchanger, test_case.config)136 test_case.reactor,
137 test_case.identity,
138 test_case.exchanger,
139 test_case.config,
140 )
122 test_case.config.cloud = getattr(test_case, "cloud", False)141 test_case.config.cloud = getattr(test_case, "cloud", False)
123 if hasattr(test_case, "juju_contents"):142 if hasattr(test_case, "juju_contents"):
124 test_case.makeFile(143 test_case.makeFile(
125 test_case.juju_contents, path=test_case.config.juju_filename)144 test_case.juju_contents,
145 path=test_case.config.juju_filename,
146 )
126 test_case.handler = RegistrationHandler(147 test_case.handler = RegistrationHandler(
127 test_case.config, test_case.identity, test_case.reactor,148 test_case.config,
128 test_case.exchanger, test_case.pinger, test_case.mstore)149 test_case.identity,
150 test_case.reactor,
151 test_case.exchanger,
152 test_case.pinger,
153 test_case.mstore,
154 )
129155
130156
131class BrokerServerHelper(RegistrationHelper):157class BrokerServerHelper(RegistrationHelper):
@@ -139,10 +165,15 @@ class BrokerServerHelper(RegistrationHelper):
139 """165 """
140166
141 def set_up(self, test_case):167 def set_up(self, test_case):
142 super(BrokerServerHelper, self).set_up(test_case)168 super().set_up(test_case)
143 test_case.broker = BrokerServer(test_case.config, test_case.reactor,169 test_case.broker = BrokerServer(
144 test_case.exchanger, test_case.handler,170 test_case.config,
145 test_case.mstore, test_case.pinger)171 test_case.reactor,
172 test_case.exchanger,
173 test_case.handler,
174 test_case.mstore,
175 test_case.pinger,
176 )
146177
147178
148class RemoteBrokerHelper(BrokerServerHelper):179class RemoteBrokerHelper(BrokerServerHelper):
@@ -168,13 +199,17 @@ class RemoteBrokerHelper(BrokerServerHelper):
168 """199 """
169200
170 def set_up(self, test_case):201 def set_up(self, test_case):
171 super(RemoteBrokerHelper, self).set_up(test_case)202 super().set_up(test_case)
172203
173 self._publisher = ComponentPublisher(test_case.broker,204 self._publisher = ComponentPublisher(
174 test_case.reactor,205 test_case.broker,
175 test_case.config)206 test_case.reactor,
176 self._connector = RemoteBrokerConnector(test_case.reactor,207 test_case.config,
177 test_case.config)208 )
209 self._connector = RemoteBrokerConnector(
210 test_case.reactor,
211 test_case.config,
212 )
178213
179 self._publisher.start()214 self._publisher.start()
180 deferred = self._connector.connect()215 deferred = self._connector.connect()
@@ -183,7 +218,7 @@ class RemoteBrokerHelper(BrokerServerHelper):
183 def tear_down(self, test_case):218 def tear_down(self, test_case):
184 self._connector.disconnect()219 self._connector.disconnect()
185 self._publisher.stop()220 self._publisher.stop()
186 super(RemoteBrokerHelper, self).tear_down(test_case)221 super().tear_down(test_case)
187222
188223
189class BrokerClientHelper(RemoteBrokerHelper):224class BrokerClientHelper(RemoteBrokerHelper):
@@ -204,7 +239,7 @@ class BrokerClientHelper(RemoteBrokerHelper):
204 """239 """
205240
206 def set_up(self, test_case):241 def set_up(self, test_case):
207 super(BrokerClientHelper, self).set_up(test_case)242 super().set_up(test_case)
208 # The client needs its own reactor to avoid infinite loops243 # The client needs its own reactor to avoid infinite loops
209 # when the broker broadcasts and event244 # when the broker broadcasts and event
210 test_case.client_reactor = FakeReactor()245 test_case.client_reactor = FakeReactor()
@@ -227,10 +262,12 @@ class RemoteClientHelper(BrokerClientHelper):
227 """262 """
228263
229 def set_up(self, test_case):264 def set_up(self, test_case):
230 super(RemoteClientHelper, self).set_up(test_case)265 super().set_up(test_case)
231 self._client_publisher = ComponentPublisher(test_case.client,266 self._client_publisher = ComponentPublisher(
232 test_case.reactor,267 test_case.client,
233 test_case.config)268 test_case.reactor,
269 test_case.config,
270 )
234 self._client_publisher.start()271 self._client_publisher.start()
235 test_case.remote.register_client("client")272 test_case.remote.register_client("client")
236 test_case.remote_client = test_case.broker.get_client("client")273 test_case.remote_client = test_case.broker.get_client("client")
@@ -239,4 +276,4 @@ class RemoteClientHelper(BrokerClientHelper):
239 def tear_down(self, test_case):276 def tear_down(self, test_case):
240 self._client_connector.disconnect()277 self._client_connector.disconnect()
241 self._client_publisher.stop()278 self._client_publisher.stop()
242 super(RemoteClientHelper, self).tear_down(test_case)279 super().tear_down(test_case)
diff --git a/landscape/client/broker/tests/test_amp.py b/landscape/client/broker/tests/test_amp.py
index a017dc1..bc992b4 100644
--- a/landscape/client/broker/tests/test_amp.py
+++ b/landscape/client/broker/tests/test_amp.py
@@ -1,10 +1,10 @@
1import mock1from unittest import mock
22
3from landscape.client.broker.tests.helpers import RemoteBrokerHelper
4from landscape.client.broker.tests.helpers import RemoteClientHelper
5from landscape.client.tests.helpers import DEFAULT_ACCEPTED_TYPES
6from landscape.client.tests.helpers import LandscapeTest
3from landscape.lib.amp import MethodCallError7from landscape.lib.amp import MethodCallError
4from landscape.client.tests.helpers import (
5 LandscapeTest, DEFAULT_ACCEPTED_TYPES)
6from landscape.client.broker.tests.helpers import (
7 RemoteBrokerHelper, RemoteClientHelper)
88
99
10class RemoteBrokerTest(LandscapeTest):10class RemoteBrokerTest(LandscapeTest):
@@ -41,13 +41,13 @@ class RemoteBrokerTest(LandscapeTest):
4141
42 session_id = self.successResultOf(self.remote.get_session_id())42 session_id = self.successResultOf(self.remote.get_session_id())
43 message_id = self.successResultOf(43 message_id = self.successResultOf(
44 self.remote.send_message(message, session_id))44 self.remote.send_message(message, session_id),
45 )
4546
46 self.assertTrue(isinstance(message_id, int))47 self.assertTrue(isinstance(message_id, int))
47 self.assertTrue(self.mstore.is_pending(message_id))48 self.assertTrue(self.mstore.is_pending(message_id))
48 self.assertFalse(self.exchanger.is_urgent())49 self.assertFalse(self.exchanger.is_urgent())
49 self.assertMessages(self.mstore.get_pending_messages(),50 self.assertMessages(self.mstore.get_pending_messages(), [message])
50 [message])
5151
52 def test_send_message_with_urgent(self):52 def test_send_message_with_urgent(self):
53 """53 """
@@ -56,8 +56,9 @@ class RemoteBrokerTest(LandscapeTest):
56 message = {"type": "test"}56 message = {"type": "test"}
57 self.mstore.set_accepted_types(["test"])57 self.mstore.set_accepted_types(["test"])
58 session_id = self.successResultOf(self.remote.get_session_id())58 session_id = self.successResultOf(self.remote.get_session_id())
59 message_id = self.successResultOf(self.remote.send_message(59 message_id = self.successResultOf(
60 message, session_id, urgent=True))60 self.remote.send_message(message, session_id, urgent=True),
61 )
61 self.assertTrue(isinstance(message_id, int))62 self.assertTrue(isinstance(message_id, int))
62 self.assertTrue(self.exchanger.is_urgent())63 self.assertTrue(self.exchanger.is_urgent())
6364
@@ -95,8 +96,9 @@ class RemoteBrokerTest(LandscapeTest):
95 a L{Deferred}.96 a L{Deferred}.
96 """97 """
97 # This should make the registration succeed98 # This should make the registration succeed
98 self.transport.responses.append([{"type": "set-id", "id": "abc",99 self.transport.responses.append(
99 "insecure-id": "def"}])100 [{"type": "set-id", "id": "abc", "insecure-id": "def"}],
101 )
100 result = self.remote.register()102 result = self.remote.register()
101 return self.assertSuccess(result, None)103 return self.assertSuccess(result, None)
102104
@@ -130,7 +132,8 @@ class RemoteBrokerTest(LandscapeTest):
130 self.assertEqual(response, None)132 self.assertEqual(response, None)
131 self.assertEqual(133 self.assertEqual(
132 self.exchanger.get_client_accepted_message_types(),134 self.exchanger.get_client_accepted_message_types(),
133 sorted(["type"] + DEFAULT_ACCEPTED_TYPES))135 sorted(["type"] + DEFAULT_ACCEPTED_TYPES),
136 )
134137
135 result = self.remote.register_client_accepted_message_type("type")138 result = self.remote.register_client_accepted_message_type("type")
136 return result.addCallback(assert_response)139 return result.addCallback(assert_response)
@@ -159,8 +162,11 @@ class RemoteBrokerTest(LandscapeTest):
159 The L{RemoteBroker.call_if_accepted} method doesn't do anything if the162 The L{RemoteBroker.call_if_accepted} method doesn't do anything if the
160 given message type is not accepted.163 given message type is not accepted.
161 """164 """
162 function = (lambda: 1 / 0)165
163 result = self.remote.call_if_accepted("test", function)166 def division_by_zero():
167 raise ZeroDivisionError()
168
169 result = self.remote.call_if_accepted("test", division_by_zero)
164 return self.assertSuccess(result, None)170 return self.assertSuccess(result, None)
165171
166 def test_listen_events(self):172 def test_listen_events(self):
@@ -181,8 +187,9 @@ class RemoteBrokerTest(LandscapeTest):
181 """187 """
182 callback1 = mock.Mock()188 callback1 = mock.Mock()
183 callback2 = mock.Mock(return_value=123)189 callback2 = mock.Mock(return_value=123)
184 deferred = self.remote.call_on_event({"event1": callback1,190 deferred = self.remote.call_on_event(
185 "event2": callback2})191 {"event1": callback1, "event2": callback2},
192 )
186 self.reactor.call_later(0.05, self.reactor.fire, "event2")193 self.reactor.call_later(0.05, self.reactor.fire, "event2")
187 self.reactor.advance(0.05)194 self.reactor.advance(0.05)
188 self.remote._factory.fake_connection.flush()195 self.remote._factory.fake_connection.flush()
@@ -228,8 +235,10 @@ class RemoteClientTest(LandscapeTest):
228 a L{Deferred}.235 a L{Deferred}.
229 """236 """
230 handler = mock.Mock()237 handler = mock.Mock()
231 with mock.patch.object(self.client.broker,238 with mock.patch.object(
232 "register_client_accepted_message_type") as m:239 self.client.broker,
240 "register_client_accepted_message_type",
241 ) as m:
233 # We need to register a test message handler to let the dispatch242 # We need to register a test message handler to let the dispatch
234 # message method call succeed243 # message method call succeed
235 self.client.register_message("test", handler)244 self.client.register_message("test", handler)
diff --git a/landscape/client/broker/tests/test_client.py b/landscape/client/broker/tests/test_client.py
index 3295b02..219cc0c 100644
--- a/landscape/client/broker/tests/test_client.py
+++ b/landscape/client/broker/tests/test_client.py
@@ -1,14 +1,14 @@
1import mock1from unittest import mock
22
3from twisted.internet import reactor3from twisted.internet import reactor
4from twisted.internet.defer import Deferred4from twisted.internet.defer import Deferred
55
6from landscape.lib.twisted_util import gather_results6from landscape.client.broker.client import BrokerClientPlugin
7from landscape.client.tests.helpers import (7from landscape.client.broker.client import HandlerNotFoundError
8 LandscapeTest, DEFAULT_ACCEPTED_TYPES)
9from landscape.client.broker.tests.helpers import BrokerClientHelper8from landscape.client.broker.tests.helpers import BrokerClientHelper
10from landscape.client.broker.client import (9from landscape.client.tests.helpers import DEFAULT_ACCEPTED_TYPES
11 BrokerClientPlugin, HandlerNotFoundError)10from landscape.client.tests.helpers import LandscapeTest
11from landscape.lib.twisted_util import gather_results
1212
1313
14class BrokerClientTest(LandscapeTest):14class BrokerClientTest(LandscapeTest):
@@ -45,7 +45,8 @@ class BrokerClientTest(LandscapeTest):
45 getting a session id.45 getting a session id.
46 """46 """
47 test_session_id = self.successResultOf(47 test_session_id = self.successResultOf(
48 self.client.broker.get_session_id(scope="test"))48 self.client.broker.get_session_id(scope="test"),
49 )
49 plugin = BrokerClientPlugin()50 plugin = BrokerClientPlugin()
50 plugin.scope = "test"51 plugin.scope = "test"
51 self.client.add(plugin)52 self.client.add(plugin)
@@ -130,8 +131,10 @@ class BrokerClientTest(LandscapeTest):
130 If a plugin has a run method, the reactor will call it every131 If a plugin has a run method, the reactor will call it every
131 run_interval, but will stop and log if it raises unhandled exceptions.132 run_interval, but will stop and log if it raises unhandled exceptions.
132 """133 """
134
133 class RunFailure(Exception):135 class RunFailure(Exception):
134 pass136 pass
137
135 # log helper should not complain on the error we're testing138 # log helper should not complain on the error we're testing
136 self.log_helper.ignore_errors("BrokerClientPlugin.*")139 self.log_helper.ignore_errors("BrokerClientPlugin.*")
137 plugin = BrokerClientPlugin()140 plugin = BrokerClientPlugin()
@@ -146,7 +149,8 @@ class BrokerClientTest(LandscapeTest):
146 # message entry that would be present on a live client.149 # message entry that would be present on a live client.
147 self.assertIn(150 self.assertIn(
148 "ERROR: BrokerClientPlugin raised an uncaught exception",151 "ERROR: BrokerClientPlugin raised an uncaught exception",
149 self.logfile.getvalue())152 self.logfile.getvalue(),
153 )
150154
151 def test_run_interval_blocked_during_resynch(self):155 def test_run_interval_blocked_during_resynch(self):
152 """156 """
@@ -220,7 +224,8 @@ class BrokerClientTest(LandscapeTest):
220 def got_result(result):224 def got_result(result):
221 self.assertEqual(225 self.assertEqual(
222 self.exchanger.get_client_accepted_message_types(),226 self.exchanger.get_client_accepted_message_types(),
223 sorted(["bar", "foo"] + DEFAULT_ACCEPTED_TYPES))227 sorted(["bar", "foo"] + DEFAULT_ACCEPTED_TYPES),
228 )
224229
225 return gather_results([result1, result2]).addCallback(got_result)230 return gather_results([result1, result2]).addCallback(got_result)
226231
@@ -251,8 +256,10 @@ class BrokerClientTest(LandscapeTest):
251256
252 def dispatch_message(result):257 def dispatch_message(result):
253 self.assertIs(self.client.dispatch_message(message), None)258 self.assertIs(self.client.dispatch_message(message), None)
254 self.assertTrue("Error running message handler for type 'foo'" in259 self.assertTrue(
255 self.logfile.getvalue())260 "Error running message handler for type 'foo'"
261 in self.logfile.getvalue(),
262 )
256 handle_message.assert_called_once_with(message)263 handle_message.assert_called_once_with(message)
257264
258 result = self.client.register_message("foo", handle_message)265 result = self.client.register_message("foo", handle_message)
@@ -263,8 +270,11 @@ class BrokerClientTest(LandscapeTest):
263 L{BrokerClient.dispatch_message} raises an error if no handler was270 L{BrokerClient.dispatch_message} raises an error if no handler was
264 found for the given message.271 found for the given message.
265 """272 """
266 error = self.assertRaises(HandlerNotFoundError,273 error = self.assertRaises(
267 self.client.dispatch_message, {"type": "x"})274 HandlerNotFoundError,
275 self.client.dispatch_message,
276 {"type": "x"},
277 )
268 self.assertEqual(str(error), "x")278 self.assertEqual(str(error), "x")
269279
270 def test_message(self):280 def test_message(self):
@@ -326,8 +336,9 @@ class BrokerClientTest(LandscapeTest):
326 self.client.add(plugin1)336 self.client.add(plugin1)
327 self.client.add(plugin2)337 self.client.add(plugin2)
328 self.client.exchange()338 self.client.exchange()
329 self.assertTrue("Error during plugin exchange" in339 self.assertTrue(
330 self.logfile.getvalue())340 "Error during plugin exchange" in self.logfile.getvalue(),
341 )
331 self.assertTrue("ZeroDivisionError" in self.logfile.getvalue())342 self.assertTrue("ZeroDivisionError" in self.logfile.getvalue())
332 plugin1.exchange.assert_called_once_with()343 plugin1.exchange.assert_called_once_with()
333 plugin2.exchange.assert_called_once_with()344 plugin2.exchange.assert_called_once_with()
@@ -342,8 +353,10 @@ class BrokerClientTest(LandscapeTest):
342 plugin.exchange = mock.Mock()353 plugin.exchange = mock.Mock()
343 self.client.add(plugin)354 self.client.add(plugin)
344 self.client_reactor.fire("impending-exchange")355 self.client_reactor.fire("impending-exchange")
345 self.assertTrue("Got notification of impending exchange. "356 self.assertTrue(
346 "Notifying all plugins." in self.logfile.getvalue())357 "Got notification of impending exchange. "
358 "Notifying all plugins." in self.logfile.getvalue(),
359 )
347 plugin.exchange.assert_called_once_with()360 plugin.exchange.assert_called_once_with()
348361
349 def test_fire_event(self):362 def test_fire_event(self):
@@ -415,7 +428,9 @@ class BrokerClientTest(LandscapeTest):
415 calls = [mock.call("bar"), mock.call("foo")]428 calls = [mock.call("bar"), mock.call("foo")]
416429
417 broker.register_client_accepted_message_type.assert_has_calls(430 broker.register_client_accepted_message_type.assert_has_calls(
418 calls, any_order=True)431 calls,
432 any_order=True,
433 )
419 broker.register_client.assert_called_once_with("client")434 broker.register_client.assert_called_once_with("client")
420435
421 return gather_results([result1, result2]).addCallback(got_result)436 return gather_results([result1, result2]).addCallback(got_result)
diff --git a/landscape/client/broker/tests/test_config.py b/landscape/client/broker/tests/test_config.py
index 0b60fc4..b5236ae 100644
--- a/landscape/client/broker/tests/test_config.py
+++ b/landscape/client/broker/tests/test_config.py
@@ -1,8 +1,8 @@
1import os1import os
22
3from landscape.client.broker.config import BrokerConfiguration3from landscape.client.broker.config import BrokerConfiguration
4from landscape.lib.testing import EnvironSaverHelper
5from landscape.client.tests.helpers import LandscapeTest4from landscape.client.tests.helpers import LandscapeTest
5from landscape.lib.testing import EnvironSaverHelper
66
77
8class ConfigurationTests(LandscapeTest):8class ConfigurationTests(LandscapeTest):
@@ -20,9 +20,16 @@ class ConfigurationTests(LandscapeTest):
20 del os.environ["https_proxy"]20 del os.environ["https_proxy"]
2121
22 configuration = BrokerConfiguration()22 configuration = BrokerConfiguration()
23 configuration.load(["--http-proxy", "foo",23 configuration.load(
24 "--https-proxy", "bar",24 [
25 "--url", "whatever"])25 "--http-proxy",
26 "foo",
27 "--https-proxy",
28 "bar",
29 "--url",
30 "whatever",
31 ],
32 )
26 self.assertEqual(os.environ["http_proxy"], "foo")33 self.assertEqual(os.environ["http_proxy"], "foo")
27 self.assertEqual(os.environ["https_proxy"], "bar")34 self.assertEqual(os.environ["https_proxy"], "bar")
2835
@@ -53,9 +60,9 @@ class ConfigurationTests(LandscapeTest):
53 os.environ["https_proxy"] = "originals"60 os.environ["https_proxy"] = "originals"
5461
55 configuration = BrokerConfiguration()62 configuration = BrokerConfiguration()
56 configuration.load(["--http-proxy", "x",63 configuration.load(
57 "--https-proxy", "y",64 ["--http-proxy", "x", "--https-proxy", "y", "--url", "whatever"],
58 "--url", "whatever"])65 )
59 self.assertEqual(os.environ["http_proxy"], "x")66 self.assertEqual(os.environ["http_proxy"], "x")
60 self.assertEqual(os.environ["https_proxy"], "y")67 self.assertEqual(os.environ["https_proxy"], "y")
6168
@@ -74,10 +81,12 @@ class ConfigurationTests(LandscapeTest):
74 The 'urgent_exchange_interval, 'exchange_interval' and 'ping_interval'81 The 'urgent_exchange_interval, 'exchange_interval' and 'ping_interval'
75 values specified in the configuration file are converted to integers.82 values specified in the configuration file are converted to integers.
76 """83 """
77 filename = self.makeFile("[client]\n"84 filename = self.makeFile(
78 "urgent_exchange_interval = 12\n"85 "[client]\n"
79 "exchange_interval = 34\n"86 "urgent_exchange_interval = 12\n"
80 "ping_interval = 6\n")87 "exchange_interval = 34\n"
88 "ping_interval = 6\n",
89 )
8190
82 configuration = BrokerConfiguration()91 configuration = BrokerConfiguration()
83 configuration.load(["--config", filename, "--url", "whatever"])92 configuration.load(["--config", filename, "--url", "whatever"])
@@ -91,8 +100,9 @@ class ConfigurationTests(LandscapeTest):
91 The 'tags' value specified in the configuration file is not converted100 The 'tags' value specified in the configuration file is not converted
92 to a list (it must be a string). See bug #1228301.101 to a list (it must be a string). See bug #1228301.
93 """102 """
94 filename = self.makeFile("[client]\n"103 filename = self.makeFile(
95 "tags = check,linode,profile-test")104 "[client]\ntags = check,linode,profile-test",
105 )
96106
97 configuration = BrokerConfiguration()107 configuration = BrokerConfiguration()
98 configuration.load(["--config", filename, "--url", "whatever"])108 configuration.load(["--config", filename, "--url", "whatever"])
@@ -104,8 +114,7 @@ class ConfigurationTests(LandscapeTest):
104 The 'access_group' value specified in the configuration file is114 The 'access_group' value specified in the configuration file is
105 passed through.115 passed through.
106 """116 """
107 filename = self.makeFile("[client]\n"117 filename = self.makeFile("[client]\naccess_group = webserver")
108 "access_group = webserver")
109118
110 configuration = BrokerConfiguration()119 configuration = BrokerConfiguration()
111 configuration.load(["--config", filename, "--url", "whatever"])120 configuration.load(["--config", filename, "--url", "whatever"])
@@ -122,5 +131,7 @@ class ConfigurationTests(LandscapeTest):
122 configuration = BrokerConfiguration()131 configuration = BrokerConfiguration()
123 configuration.load(["--config", filename])132 configuration.load(["--config", filename])
124133
125 self.assertEqual(configuration.url,134 self.assertEqual(
126 "https://landscape.canonical.com/message-system")135 configuration.url,
136 "https://landscape.canonical.com/message-system",
137 )
diff --git a/landscape/client/broker/tests/test_exchange.py b/landscape/client/broker/tests/test_exchange.py
index 3736bd4..bc7fb5f 100644
--- a/landscape/client/broker/tests/test_exchange.py
+++ b/landscape/client/broker/tests/test_exchange.py
@@ -1,22 +1,23 @@
1import mock1from unittest import mock
22
3from landscape import CLIENT_API3from landscape import CLIENT_API
4from landscape.lib.persist import Persist
5from landscape.lib.fetch import HTTPCodeError, PyCurlError
6from landscape.lib.hashlib import md5
7from landscape.lib.schema import Int
8from landscape.message_schemas.message import Message
9from landscape.client.broker.config import BrokerConfiguration4from landscape.client.broker.config import BrokerConfiguration
10from landscape.client.broker.exchange import (5from landscape.client.broker.exchange import get_accepted_types_diff
11 get_accepted_types_diff, MessageExchange)6from landscape.client.broker.exchange import MessageExchange
12from landscape.client.broker.transport import FakeTransport
13from landscape.client.broker.store import MessageStore
14from landscape.client.broker.ping import Pinger7from landscape.client.broker.ping import Pinger
15from landscape.client.broker.registration import RegistrationHandler8from landscape.client.broker.registration import RegistrationHandler
16from landscape.client.tests.helpers import (
17 LandscapeTest, DEFAULT_ACCEPTED_TYPES)
18from landscape.client.broker.tests.helpers import ExchangeHelper
19from landscape.client.broker.server import BrokerServer9from landscape.client.broker.server import BrokerServer
10from landscape.client.broker.store import MessageStore
11from landscape.client.broker.tests.helpers import ExchangeHelper
12from landscape.client.broker.transport import FakeTransport
13from landscape.client.tests.helpers import DEFAULT_ACCEPTED_TYPES
14from landscape.client.tests.helpers import LandscapeTest
15from landscape.lib.fetch import HTTPCodeError
16from landscape.lib.fetch import PyCurlError
17from landscape.lib.hashlib import md5
18from landscape.lib.persist import Persist
19from landscape.lib.schema import Int
20from landscape.message_schemas.message import Message
2021
2122
22class MessageExchangeTest(LandscapeTest):23class MessageExchangeTest(LandscapeTest):
@@ -24,11 +25,11 @@ class MessageExchangeTest(LandscapeTest):
24 helpers = [ExchangeHelper]25 helpers = [ExchangeHelper]
2526
26 def setUp(self):27 def setUp(self):
27 super(MessageExchangeTest, self).setUp()28 super().setUp()
28 self.mstore.add_schema(Message("empty", {}))29 self.mstore.add_schema(Message("empty", {}))
29 self.mstore.add_schema(Message("data", {"data": Int()}))30 self.mstore.add_schema(Message("data", {"data": Int()}))
30 self.mstore.add_schema(Message("holdme", {}))31 self.mstore.add_schema(Message("holdme", {}))
31 self.identity.secure_id = 'needs-to-be-set-for-tests-to-pass'32 self.identity.secure_id = "needs-to-be-set-for-tests-to-pass"
3233
33 def wait_for_exchange(self, urgent=False, factor=1, delta=0):34 def wait_for_exchange(self, urgent=False, factor=1, delta=0):
34 if urgent:35 if urgent:
@@ -52,9 +53,14 @@ class MessageExchangeTest(LandscapeTest):
52 session IDs are expired, so any new messages being sent with those IDs53 session IDs are expired, so any new messages being sent with those IDs
53 will be discarded.54 will be discarded.
54 """55 """
55 broker = BrokerServer(self.config, self.reactor,56 broker = BrokerServer(
56 self.exchanger, None,57 self.config,
57 self.mstore, None)58 self.reactor,
59 self.exchanger,
60 None,
61 self.mstore,
62 None,
63 )
5864
59 disk_session_id = self.mstore.get_session_id(scope="disk")65 disk_session_id = self.mstore.get_session_id(scope="disk")
60 package_session_id = self.mstore.get_session_id(scope="package")66 package_session_id = self.mstore.get_session_id(scope="package")
@@ -72,9 +78,14 @@ class MessageExchangeTest(LandscapeTest):
72 When a resynchronisation event occurs with a scope existing session IDs78 When a resynchronisation event occurs with a scope existing session IDs
73 for that scope are expired, all other session IDs are unaffected.79 for that scope are expired, all other session IDs are unaffected.
74 """80 """
75 broker = BrokerServer(self.config, self.reactor,81 broker = BrokerServer(
76 self.exchanger, None,82 self.config,
77 self.mstore, None)83 self.reactor,
84 self.exchanger,
85 None,
86 self.mstore,
87 None,
88 )
7889
79 disk_session_id = self.mstore.get_session_id(scope="disk")90 disk_session_id = self.mstore.get_session_id(scope="disk")
80 package_session_id = self.mstore.get_session_id(scope="package")91 package_session_id = self.mstore.get_session_id(scope="package")
@@ -105,9 +116,10 @@ class MessageExchangeTest(LandscapeTest):
105 self.exchanger.exchange()116 self.exchanger.exchange()
106 self.assertEqual(len(self.transport.payloads), 1)117 self.assertEqual(len(self.transport.payloads), 1)
107 messages = self.transport.payloads[0]["messages"]118 messages = self.transport.payloads[0]["messages"]
108 self.assertEqual(messages, [{"type": "empty",119 self.assertEqual(
109 "timestamp": 0,120 messages,
110 "api": b"3.2"}])121 [{"type": "empty", "timestamp": 0, "api": b"3.2"}],
122 )
111123
112 def test_send_urgent(self):124 def test_send_urgent(self):
113 """125 """
@@ -118,8 +130,10 @@ class MessageExchangeTest(LandscapeTest):
118 self.exchanger.send({"type": "empty"}, urgent=True)130 self.exchanger.send({"type": "empty"}, urgent=True)
119 self.wait_for_exchange(urgent=True)131 self.wait_for_exchange(urgent=True)
120 self.assertEqual(len(self.transport.payloads), 1)132 self.assertEqual(len(self.transport.payloads), 1)
121 self.assertMessages(self.transport.payloads[0]["messages"],133 self.assertMessages(
122 [{"type": "empty"}])134 self.transport.payloads[0]["messages"],
135 [{"type": "empty"}],
136 )
123137
124 def test_send_urgent_wont_reschedule(self):138 def test_send_urgent_wont_reschedule(self):
125 """139 """
@@ -132,8 +146,10 @@ class MessageExchangeTest(LandscapeTest):
132 self.exchanger.send({"type": "empty"}, urgent=True)146 self.exchanger.send({"type": "empty"}, urgent=True)
133 self.wait_for_exchange(urgent=True, factor=0.5)147 self.wait_for_exchange(urgent=True, factor=0.5)
134 self.assertEqual(len(self.transport.payloads), 1)148 self.assertEqual(len(self.transport.payloads), 1)
135 self.assertMessages(self.transport.payloads[0]["messages"],149 self.assertMessages(
136 [{"type": "empty"}, {"type": "empty"}])150 self.transport.payloads[0]["messages"],
151 [{"type": "empty"}, {"type": "empty"}],
152 )
137153
138 def test_send_returns_message_id(self):154 def test_send_returns_message_id(self):
139 """155 """
@@ -151,14 +167,15 @@ class MessageExchangeTest(LandscapeTest):
151 """167 """
152 self.mstore.set_accepted_types(["package-reporter-result"])168 self.mstore.set_accepted_types(["package-reporter-result"])
153 self.exchanger._max_log_text_bytes = 5169 self.exchanger._max_log_text_bytes = 5
154 self.exchanger.send({"type": "package-reporter-result", "err": "E"*10,170 self.exchanger.send(
155 "code": 0})171 {"type": "package-reporter-result", "err": "E" * 10, "code": 0},
172 )
156 self.exchanger.exchange()173 self.exchanger.exchange()
157 self.assertEqual(len(self.transport.payloads), 1)174 self.assertEqual(len(self.transport.payloads), 1)
158 messages = self.transport.payloads[0]["messages"]175 messages = self.transport.payloads[0]["messages"]
159 self.assertIn('TRUNCATED', messages[0]['err'])176 self.assertIn("TRUNCATED", messages[0]["err"])
160 self.assertIn('EEEEE', messages[0]['err'])177 self.assertIn("EEEEE", messages[0]["err"])
161 self.assertNotIn('EEEEEE', messages[0]['err'])178 self.assertNotIn("EEEEEE", messages[0]["err"])
162179
163 def test_send_big_message_trimmed_result(self):180 def test_send_big_message_trimmed_result(self):
164 """181 """
@@ -166,14 +183,21 @@ class MessageExchangeTest(LandscapeTest):
166 """183 """
167 self.mstore.set_accepted_types(["operation-result"])184 self.mstore.set_accepted_types(["operation-result"])
168 self.exchanger._max_log_text_bytes = 5185 self.exchanger._max_log_text_bytes = 5
169 self.exchanger.send({"type": "operation-result", "result-text": "E"*10,186 self.exchanger.send(
170 "code": 0, "status": 0, "operation-id": 0})187 {
188 "type": "operation-result",
189 "result-text": "E" * 10,
190 "code": 0,
191 "status": 0,
192 "operation-id": 0,
193 },
194 )
171 self.exchanger.exchange()195 self.exchanger.exchange()
172 self.assertEqual(len(self.transport.payloads), 1)196 self.assertEqual(len(self.transport.payloads), 1)
173 messages = self.transport.payloads[0]["messages"]197 messages = self.transport.payloads[0]["messages"]
174 self.assertIn('TRUNCATED', messages[0]['result-text'])198 self.assertIn("TRUNCATED", messages[0]["result-text"])
175 self.assertIn('EEEEE', messages[0]['result-text'])199 self.assertIn("EEEEE", messages[0]["result-text"])
176 self.assertNotIn('EEEEEE', messages[0]['result-text'])200 self.assertNotIn("EEEEEE", messages[0]["result-text"])
177201
178 def test_send_small_message_not_trimmed(self):202 def test_send_small_message_not_trimmed(self):
179 """203 """
@@ -181,13 +205,14 @@ class MessageExchangeTest(LandscapeTest):
181 """205 """
182 self.mstore.set_accepted_types(["package-reporter-result"])206 self.mstore.set_accepted_types(["package-reporter-result"])
183 self.exchanger._max_log_text_bytes = 4207 self.exchanger._max_log_text_bytes = 4
184 self.exchanger.send({"type": "package-reporter-result", "err": "E"*4,208 self.exchanger.send(
185 "code": 0})209 {"type": "package-reporter-result", "err": "E" * 4, "code": 0},
210 )
186 self.exchanger.exchange()211 self.exchanger.exchange()
187 self.assertEqual(len(self.transport.payloads), 1)212 self.assertEqual(len(self.transport.payloads), 1)
188 messages = self.transport.payloads[0]["messages"]213 messages = self.transport.payloads[0]["messages"]
189 self.assertNotIn('TRUNCATED', messages[0]['err'])214 self.assertNotIn("TRUNCATED", messages[0]["err"])
190 self.assertIn('EEEE', messages[0]['err'])215 self.assertIn("EEEE", messages[0]["err"])
191216
192 def test_wb_include_accepted_types(self):217 def test_wb_include_accepted_types(self):
193 """218 """
@@ -204,7 +229,8 @@ class MessageExchangeTest(LandscapeTest):
204 types.229 types.
205 """230 """
206 self.exchanger.handle_message(231 self.exchanger.handle_message(
207 {"type": "accepted-types", "types": ["foo"]})232 {"type": "accepted-types", "types": ["foo"]},
233 )
208 self.assertEqual(self.mstore.get_accepted_types(), ["foo"])234 self.assertEqual(self.mstore.get_accepted_types(), ["foo"])
209235
210 def test_message_type_acceptance_changed_event(self):236 def test_message_type_acceptance_changed_event(self):
@@ -212,13 +238,18 @@ class MessageExchangeTest(LandscapeTest):
212238
213 def callback(type, accepted):239 def callback(type, accepted):
214 stash.append((type, accepted))240 stash.append((type, accepted))
241
215 self.reactor.call_on("message-type-acceptance-changed", callback)242 self.reactor.call_on("message-type-acceptance-changed", callback)
216 self.exchanger.handle_message(243 self.exchanger.handle_message(
217 {"type": "accepted-types", "types": ["a", "b"]})244 {"type": "accepted-types", "types": ["a", "b"]},
245 )
218 self.exchanger.handle_message(246 self.exchanger.handle_message(
219 {"type": "accepted-types", "types": ["b", "c"]})247 {"type": "accepted-types", "types": ["b", "c"]},
220 self.assertCountEqual(stash, [("a", True), ("b", True),248 )
221 ("a", False), ("c", True)])249 self.assertCountEqual(
250 stash,
251 [("a", True), ("b", True), ("a", False), ("c", True)],
252 )
222253
223 def test_wb_accepted_types_roundtrip(self):254 def test_wb_accepted_types_roundtrip(self):
224 """255 """
@@ -226,11 +257,11 @@ class MessageExchangeTest(LandscapeTest):
226 should affect its future payloads.257 should affect its future payloads.
227 """258 """
228 self.exchanger.handle_message(259 self.exchanger.handle_message(
229 {"type": "accepted-types", "types": ["ack", "bar"]})260 {"type": "accepted-types", "types": ["ack", "bar"]},
261 )
230 payload = self.exchanger._make_payload()262 payload = self.exchanger._make_payload()
231 self.assertIn("accepted-types", payload)263 self.assertIn("accepted-types", payload)
232 self.assertEqual(payload["accepted-types"],264 self.assertEqual(payload["accepted-types"], md5(b"ack;bar").digest())
233 md5(b"ack;bar").digest())
234265
235 def test_accepted_types_causes_urgent_if_held_messages_exist(self):266 def test_accepted_types_causes_urgent_if_held_messages_exist(self):
236 """267 """
@@ -240,11 +271,14 @@ class MessageExchangeTest(LandscapeTest):
240 self.exchanger.send({"type": "holdme"})271 self.exchanger.send({"type": "holdme"})
241 self.assertEqual(self.mstore.get_pending_messages(), [])272 self.assertEqual(self.mstore.get_pending_messages(), [])
242 self.exchanger.handle_message(273 self.exchanger.handle_message(
243 {"type": "accepted-types", "types": ["holdme"]})274 {"type": "accepted-types", "types": ["holdme"]},
275 )
244 self.wait_for_exchange(urgent=True)276 self.wait_for_exchange(urgent=True)
245 self.assertEqual(len(self.transport.payloads), 1)277 self.assertEqual(len(self.transport.payloads), 1)
246 self.assertMessages(self.transport.payloads[0]["messages"],278 self.assertMessages(
247 [{"type": "holdme"}])279 self.transport.payloads[0]["messages"],
280 [{"type": "holdme"}],
281 )
248282
249 def test_accepted_types_no_urgent_without_held(self):283 def test_accepted_types_no_urgent_without_held(self):
250 """284 """
@@ -253,8 +287,10 @@ class MessageExchangeTest(LandscapeTest):
253 """287 """
254 self.exchanger.send({"type": "holdme"})288 self.exchanger.send({"type": "holdme"})
255 self.assertEqual(self.transport.payloads, [])289 self.assertEqual(self.transport.payloads, [])
256 self.reactor.fire("message",290 self.reactor.fire(
257 {"type": "accepted-types", "types": ["irrelevant"]})291 "message",
292 {"type": "accepted-types", "types": ["irrelevant"]},
293 )
258 self.assertEqual(len(self.transport.payloads), 0)294 self.assertEqual(len(self.transport.payloads), 0)
259295
260 def test_sequence_is_committed_immediately(self):296 def test_sequence_is_committed_immediately(self):
@@ -294,8 +330,7 @@ class MessageExchangeTest(LandscapeTest):
294 def handler(message):330 def handler(message):
295 Persist(filename=self.persist_filename)331 Persist(filename=self.persist_filename)
296 store = MessageStore(self.persist, self.config.message_store_path)332 store = MessageStore(self.persist, self.config.message_store_path)
297 self.assertEqual(store.get_server_sequence(),333 self.assertEqual(store.get_server_sequence(), self.message_counter)
298 self.message_counter)
299 self.message_counter += 1334 self.message_counter += 1
300 handled.append(True)335 handled.append(True)
301336
@@ -324,8 +359,10 @@ class MessageExchangeTest(LandscapeTest):
324 self.wait_for_exchange(urgent=True)359 self.wait_for_exchange(urgent=True)
325360
326 self.assertEqual(len(self.transport.payloads), 2)361 self.assertEqual(len(self.transport.payloads), 2)
327 self.assertMessages(self.transport.payloads[1]["messages"],362 self.assertMessages(
328 [{"type": "empty"}])363 self.transport.payloads[1]["messages"],
364 [{"type": "empty"}],
365 )
329366
330 def test_server_expects_older_messages(self):367 def test_server_expects_older_messages(self):
331 """368 """
@@ -361,22 +398,29 @@ class MessageExchangeTest(LandscapeTest):
361 self.assertEqual(exchanged, [True])398 self.assertEqual(exchanged, [True])
362399
363 payload = self.transport.payloads[-1]400 payload = self.transport.payloads[-1]
364 self.assertMessages(payload["messages"],401 self.assertMessages(
365 [{"type": "data", "data": 1},402 payload["messages"],
366 {"type": "data", "data": 2},403 [
367 {"type": "data", "data": 3}])404 {"type": "data", "data": 1},
405 {"type": "data", "data": 2},
406 {"type": "data", "data": 3},
407 ],
408 )
368 self.assertEqual(payload["sequence"], 1)409 self.assertEqual(payload["sequence"], 1)
369 self.assertEqual(payload["next-expected-sequence"], 0)410 self.assertEqual(payload["next-expected-sequence"], 0)
370411
371 @mock.patch("landscape.client.broker.store.MessageStore"412 @mock.patch(
372 ".delete_old_messages")413 "landscape.client.broker.store.MessageStore.delete_old_messages",
373 def test_pending_offset_when_next_expected_too_high(self,414 )
374 mock_rm_all_messages):415 def test_pending_offset_when_next_expected_too_high(
375 '''416 self,
417 mock_rm_all_messages,
418 ):
419 """
376 When next expected sequence received from server is too high, then the420 When next expected sequence received from server is too high, then the
377 pending offset should reset to zero. This will cause the client to421 pending offset should reset to zero. This will cause the client to
378 resend the pending messages.422 resend the pending messages.
379 '''423 """
380424
381 self.mstore.set_accepted_types(["data"])425 self.mstore.set_accepted_types(["data"])
382 self.mstore.add({"type": "data", "data": 0})426 self.mstore.add({"type": "data", "data": 0})
@@ -399,12 +443,12 @@ class MessageExchangeTest(LandscapeTest):
399 self.assertTrue(mock_rm_all_messages.called)443 self.assertTrue(mock_rm_all_messages.called)
400444
401 def test_payloads_when_next_expected_too_high(self):445 def test_payloads_when_next_expected_too_high(self):
402 '''446 """
403 When next expected sequence received from server is too high, then the447 When next expected sequence received from server is too high, then the
404 current messages should get sent again since we don't have confirmation448 current messages should get sent again since we don't have confirmation
405 that the server received it. Also previous messages should not get449 that the server received it. Also previous messages should not get
406 repeated.450 repeated.
407 '''451 """
408452
409 self.mstore.set_accepted_types(["data"])453 self.mstore.set_accepted_types(["data"])
410454
@@ -426,17 +470,16 @@ class MessageExchangeTest(LandscapeTest):
426 self.assertTrue(last_messages)470 self.assertTrue(last_messages)
427471
428 # Confirm earlier messages are not resent472 # Confirm earlier messages are not resent
429 self.assertNotIn(message0["data"],473 self.assertNotIn(message0["data"], [m["data"] for m in last_messages])
430 [m["data"] for m in last_messages])
431474
432 # Confirm contents of payload475 # Confirm contents of payload
433 self.assertEqual([message1, message2], last_messages)476 self.assertEqual([message1, message2], last_messages)
434477
435 def test_resync_when_next_expected_too_high(self):478 def test_resync_when_next_expected_too_high(self):
436 '''479 """
437 When next expected sequence received from the server is too high, then480 When next expected sequence received from the server is too high, then
438 a resynchronize should happen481 a resynchronize should happen
439 '''482 """
440483
441 self.mstore.set_accepted_types(["empty", "resynchronize"])484 self.mstore.set_accepted_types(["empty", "resynchronize"])
442 self.mstore.add({"type": "empty"})485 self.mstore.add({"type": "empty"})
@@ -447,17 +490,24 @@ class MessageExchangeTest(LandscapeTest):
447 self.reactor.call_on("resynchronize-clients", lambda scope=None: None)490 self.reactor.call_on("resynchronize-clients", lambda scope=None: None)
448491
449 self.exchanger.exchange()492 self.exchanger.exchange()
450 self.assertMessage(self.mstore.get_pending_messages()[-1],493 self.assertMessage(
451 {"type": "resynchronize"})494 self.mstore.get_pending_messages()[-1],
495 {"type": "resynchronize"},
496 )
452497
453 def test_start_with_urgent_exchange(self):498 def test_start_with_urgent_exchange(self):
454 """499 """
455 Immediately after registration, an urgent exchange should be scheduled.500 Immediately after registration, an urgent exchange should be scheduled.
456 """501 """
457 transport = FakeTransport()502 transport = FakeTransport()
458 exchanger = MessageExchange(self.reactor, self.mstore, transport,503 exchanger = MessageExchange(
459 self.identity, self.exchange_store,504 self.reactor,
460 self.config)505 self.mstore,
506 transport,
507 self.identity,
508 self.exchange_store,
509 self.config,
510 )
461 exchanger.start()511 exchanger.start()
462 self.wait_for_exchange(urgent=True)512 self.wait_for_exchange(urgent=True)
463 self.assertEqual(len(transport.payloads), 1)513 self.assertEqual(len(transport.payloads), 1)
@@ -499,13 +549,17 @@ class MessageExchangeTest(LandscapeTest):
499 self.mstore.record_success = mock_record_success549 self.mstore.record_success = mock_record_success
500550
501 exchanger = MessageExchange(551 exchanger = MessageExchange(
502 self.reactor, self.mstore, self.transport,552 self.reactor,
503 self.identity, self.exchange_store, self.config)553 self.mstore,
554 self.transport,
555 self.identity,
556 self.exchange_store,
557 self.config,
558 )
504 exchanger.exchange()559 exchanger.exchange()
505560
506 mock_record_success.assert_called_with(mock.ANY)561 mock_record_success.assert_called_with(mock.ANY)
507 self.assertTrue(562 self.assertTrue(type(mock_record_success.call_args[0][0]) is int)
508 type(mock_record_success.call_args[0][0]) is int)
509563
510 def test_ancient_causes_resynchronize(self):564 def test_ancient_causes_resynchronize(self):
511 """565 """
@@ -530,15 +584,20 @@ class MessageExchangeTest(LandscapeTest):
530 # should come AFTER the "resynchronize" message that is generated584 # should come AFTER the "resynchronize" message that is generated
531 # by the exchange code itself.585 # by the exchange code itself.
532 self.mstore.add({"type": "data", "data": 999})586 self.mstore.add({"type": "data", "data": 999})
587
533 self.reactor.call_on("resynchronize-clients", resynchronize)588 self.reactor.call_on("resynchronize-clients", resynchronize)
534589
535 # This exchange call will notice the server is asking for an old590 # This exchange call will notice the server is asking for an old
536 # message and fire the event:591 # message and fire the event:
537 self.exchanger.exchange()592 self.exchanger.exchange()
538 self.assertMessages(self.mstore.get_pending_messages(),593 self.assertMessages(
539 [{"type": "empty"},594 self.mstore.get_pending_messages(),
540 {"type": "resynchronize"},595 [
541 {"type": "data", "data": 999}])596 {"type": "empty"},
597 {"type": "resynchronize"},
598 {"type": "data", "data": 999},
599 ],
600 )
542601
543 def test_resynchronize_msg_causes_resynchronize_response_then_event(self):602 def test_resynchronize_msg_causes_resynchronize_response_then_event(self):
544 """603 """
@@ -551,15 +610,20 @@ class MessageExchangeTest(LandscapeTest):
551610
552 def resynchronized(scopes=None):611 def resynchronized(scopes=None):
553 self.mstore.add({"type": "empty"})612 self.mstore.add({"type": "empty"})
613
554 self.reactor.call_on("resynchronize-clients", resynchronized)614 self.reactor.call_on("resynchronize-clients", resynchronized)
555615
556 self.transport.responses.append([{"type": "resynchronize",616 self.transport.responses.append(
557 "operation-id": 123}])617 [{"type": "resynchronize", "operation-id": 123}],
618 )
558 self.exchanger.exchange()619 self.exchanger.exchange()
559 self.assertMessages(self.mstore.get_pending_messages(),620 self.assertMessages(
560 [{"type": "resynchronize",621 self.mstore.get_pending_messages(),
561 "operation-id": 123},622 [
562 {"type": "empty"}])623 {"type": "resynchronize", "operation-id": 123},
624 {"type": "empty"},
625 ],
626 )
563627
564 def test_scopes_are_copied_from_incoming_resynchronize_messages(self):628 def test_scopes_are_copied_from_incoming_resynchronize_messages(self):
565 """629 """
@@ -574,9 +638,15 @@ class MessageExchangeTest(LandscapeTest):
574638
575 self.reactor.call_on("resynchronize-clients", resynchronized)639 self.reactor.call_on("resynchronize-clients", resynchronized)
576640
577 self.transport.responses.append([{"type": "resynchronize",641 self.transport.responses.append(
578 "operation-id": 123,642 [
579 "scopes": ["disk", "users"]}])643 {
644 "type": "resynchronize",
645 "operation-id": 123,
646 "scopes": ["disk", "users"],
647 },
648 ],
649 )
580 self.exchanger.exchange()650 self.exchanger.exchange()
581 self.assertEqual(["disk", "users"], fired_scopes)651 self.assertEqual(["disk", "users"], fired_scopes)
582652
@@ -622,8 +692,10 @@ class MessageExchangeTest(LandscapeTest):
622692
623 def test_old_sequence_id_does_not_cause_resynchronize(self):693 def test_old_sequence_id_does_not_cause_resynchronize(self):
624 resynchronized = []694 resynchronized = []
625 self.reactor.call_on("resynchronize",695 self.reactor.call_on(
626 lambda: resynchronized.append(True))696 "resynchronize",
697 lambda: resynchronized.append(True),
698 )
627699
628 self.mstore.set_accepted_types(["empty"])700 self.mstore.set_accepted_types(["empty"])
629 self.mstore.add({"type": "empty"})701 self.mstore.add({"type": "empty"})
@@ -663,9 +735,10 @@ class MessageExchangeTest(LandscapeTest):
663 self.exchanger.exchange()735 self.exchanger.exchange()
664736
665 payload = self.transport.payloads[-1]737 payload = self.transport.payloads[-1]
666 self.assertMessages(payload["messages"],738 self.assertMessages(
667 [{"type": "a", "api": b"1.0"},739 payload["messages"],
668 {"type": "b", "api": b"1.0"}])740 [{"type": "a", "api": b"1.0"}, {"type": "b", "api": b"1.0"}],
741 )
669 self.assertEqual(payload.get("client-api"), CLIENT_API)742 self.assertEqual(payload.get("client-api"), CLIENT_API)
670 self.assertEqual(payload.get("server-api"), b"1.0")743 self.assertEqual(payload.get("server-api"), b"1.0")
671 self.assertEqual(self.transport.message_api, b"1.0")744 self.assertEqual(self.transport.message_api, b"1.0")
@@ -673,9 +746,10 @@ class MessageExchangeTest(LandscapeTest):
673 self.exchanger.exchange()746 self.exchanger.exchange()
674747
675 payload = self.transport.payloads[-1]748 payload = self.transport.payloads[-1]
676 self.assertMessages(payload["messages"],749 self.assertMessages(
677 [{"type": "c", "api": b"1.1"},750 payload["messages"],
678 {"type": "d", "api": b"1.1"}])751 [{"type": "c", "api": b"1.1"}, {"type": "d", "api": b"1.1"}],
752 )
679 self.assertEqual(payload.get("client-api"), CLIENT_API)753 self.assertEqual(payload.get("client-api"), CLIENT_API)
680 self.assertEqual(payload.get("server-api"), b"1.1")754 self.assertEqual(payload.get("server-api"), b"1.1")
681 self.assertEqual(self.transport.message_api, b"1.1")755 self.assertEqual(self.transport.message_api, b"1.1")
@@ -732,9 +806,15 @@ class MessageExchangeTest(LandscapeTest):
732 the total-messages is equivalent to the total number of messages806 the total-messages is equivalent to the total number of messages
733 pending.807 pending.
734 """808 """
735 exchanger = MessageExchange(self.reactor, self.mstore, self.transport,809 exchanger = MessageExchange(
736 self.identity, self.exchange_store,810 self.reactor,
737 self.config, max_messages=1)811 self.mstore,
812 self.transport,
813 self.identity,
814 self.exchange_store,
815 self.config,
816 max_messages=1,
817 )
738 self.mstore.set_accepted_types(["empty"])818 self.mstore.set_accepted_types(["empty"])
739 self.mstore.add({"type": "empty"})819 self.mstore.add({"type": "empty"})
740 self.mstore.add({"type": "empty"})820 self.mstore.add({"type": "empty"})
@@ -763,9 +843,14 @@ class MessageExchangeTest(LandscapeTest):
763 # fixture has an urgent exchange interval of 10 seconds, which makes843 # fixture has an urgent exchange interval of 10 seconds, which makes
764 # testing this awkward.844 # testing this awkward.
765 self.config.urgent_exchange_interval = 20845 self.config.urgent_exchange_interval = 20
766 exchanger = MessageExchange(self.reactor, self.mstore, self.transport,846 exchanger = MessageExchange(
767 self.identity, self.exchange_store,847 self.reactor,
768 self.config)848 self.mstore,
849 self.transport,
850 self.identity,
851 self.exchange_store,
852 self.config,
853 )
769 exchanger.schedule_exchange(urgent=True)854 exchanger.schedule_exchange(urgent=True)
770 events = []855 events = []
771 self.reactor.call_on("impending-exchange", lambda: events.append(True))856 self.reactor.call_on("impending-exchange", lambda: events.append(True))
@@ -783,9 +868,14 @@ class MessageExchangeTest(LandscapeTest):
783 """868 """
784 self.config.exchange_interval = 60 * 60869 self.config.exchange_interval = 60 * 60
785 self.config.urgent_exchange_interval = 20870 self.config.urgent_exchange_interval = 20
786 exchanger = MessageExchange(self.reactor, self.mstore, self.transport,871 exchanger = MessageExchange(
787 self.identity, self.exchange_store,872 self.reactor,
788 self.config)873 self.mstore,
874 self.transport,
875 self.identity,
876 self.exchange_store,
877 self.config,
878 )
789 events = []879 events = []
790 self.reactor.call_on("impending-exchange", lambda: events.append(True))880 self.reactor.call_on("impending-exchange", lambda: events.append(True))
791 # This call will:881 # This call will:
@@ -806,11 +896,12 @@ class MessageExchangeTest(LandscapeTest):
806 # schedule a regular exchange.896 # schedule a regular exchange.
807 # Let's make sure that that *original* impending-exchange event has897 # Let's make sure that that *original* impending-exchange event has
808 # been cancelled:898 # been cancelled:
809 TIME_UNTIL_EXCHANGE = 60 * 60899 time_until_exchange = 60 * 60
810 TIME_UNTIL_NOTIFY = 10900 time_until_notify = 10
811 TIME_ADVANCED = 20 # time that we've already advanced901 time_advanced = 20
812 self.reactor.advance(TIME_UNTIL_EXCHANGE -902 self.reactor.advance(
813 (TIME_UNTIL_NOTIFY + TIME_ADVANCED))903 time_until_exchange - (time_until_notify + time_advanced),
904 )
814 self.assertEqual(events, [True])905 self.assertEqual(events, [True])
815 # Ok, so no new events means that the original call was906 # Ok, so no new events means that the original call was
816 # cancelled. great.907 # cancelled. great.
@@ -877,8 +968,13 @@ class MessageExchangeTest(LandscapeTest):
877 the L{MessageExchange} are changed, the configuration values as well,968 the L{MessageExchange} are changed, the configuration values as well,
878 and the configuration is written to disk to be persisted.969 and the configuration is written to disk to be persisted.
879 """970 """
880 server_message = [{"type": "set-intervals",971 server_message = [
881 "urgent-exchange": 1234, "exchange": 5678}]972 {
973 "type": "set-intervals",
974 "urgent-exchange": 1234,
975 "exchange": 5678,
976 },
977 ]
882 self.transport.responses.append(server_message)978 self.transport.responses.append(server_message)
883979
884 self.exchanger.exchange()980 self.exchanger.exchange()
@@ -993,6 +1089,7 @@ class MessageExchangeTest(LandscapeTest):
9931089
994 def server_uuid_changed(old_uuid, new_uuid):1090 def server_uuid_changed(old_uuid, new_uuid):
995 called.append((old_uuid, new_uuid))1091 called.append((old_uuid, new_uuid))
1092
996 self.reactor.call_on("server-uuid-changed", server_uuid_changed)1093 self.reactor.call_on("server-uuid-changed", server_uuid_changed)
9971094
998 # Set it for the first time, and it should emit the event1095 # Set it for the first time, and it should emit the event
@@ -1028,6 +1125,7 @@ class MessageExchangeTest(LandscapeTest):
10281125
1029 def server_uuid_changed(old_uuid, new_uuid):1126 def server_uuid_changed(old_uuid, new_uuid):
1030 called.append((old_uuid, new_uuid))1127 called.append((old_uuid, new_uuid))
1128
1031 self.reactor.call_on("server-uuid-changed", server_uuid_changed)1129 self.reactor.call_on("server-uuid-changed", server_uuid_changed)
10321130
1033 self.mstore.set_server_uuid("the-uuid")1131 self.mstore.set_server_uuid("the-uuid")
@@ -1039,8 +1137,10 @@ class MessageExchangeTest(LandscapeTest):
1039 self.transport.extra["server-uuid"] = "the-uuid"1137 self.transport.extra["server-uuid"] = "the-uuid"
1040 self.exchanger.exchange()1138 self.exchanger.exchange()
10411139
1042 self.assertIn("INFO: Server UUID changed (old=None, new=the-uuid).",1140 self.assertIn(
1043 self.logfile.getvalue())1141 "INFO: Server UUID changed (old=None, new=the-uuid).",
1142 self.logfile.getvalue(),
1143 )
10441144
1045 # An exchange with the same UUID shouldn't be logged.1145 # An exchange with the same UUID shouldn't be logged.
1046 self.logfile.truncate(0)1146 self.logfile.truncate(0)
@@ -1063,9 +1163,11 @@ class MessageExchangeTest(LandscapeTest):
1063 [message] = messages1163 [message] = messages
1064 self.assertIsNot(1164 self.assertIsNot(
1065 None,1165 None,
1066 self.exchange_store.get_message_context(message['operation-id']))1166 self.exchange_store.get_message_context(message["operation-id"]),
1167 )
1067 message_context = self.exchange_store.get_message_context(1168 message_context = self.exchange_store.get_message_context(
1068 message['operation-id'])1169 message["operation-id"],
1170 )
1069 self.assertEqual(message_context.operation_id, 123456)1171 self.assertEqual(message_context.operation_id, 123456)
1070 self.assertEqual(message_context.message_type, "type-R")1172 self.assertEqual(message_context.message_type, "type-R")
10711173
@@ -1099,12 +1201,13 @@ class MessageExchangeTest(LandscapeTest):
1099 self.exchanger.exchange()1201 self.exchanger.exchange()
11001202
1101 # Change the secure ID so that the response message gets discarded.1203 # Change the secure ID so that the response message gets discarded.
1102 self.identity.secure_id = 'brand-new'1204 self.identity.secure_id = "brand-new"
1103 ids_before = self.exchange_store.all_operation_ids()1205 ids_before = self.exchange_store.all_operation_ids()
11041206
1105 self.mstore.set_accepted_types(["resynchronize"])1207 self.mstore.set_accepted_types(["resynchronize"])
1106 message_id = self.exchanger.send(1208 message_id = self.exchanger.send(
1107 {"type": "resynchronize", "operation-id": 234567})1209 {"type": "resynchronize", "operation-id": 234567},
1210 )
1108 self.exchanger.exchange()1211 self.exchanger.exchange()
1109 self.assertEqual(2, len(self.transport.payloads))1212 self.assertEqual(2, len(self.transport.payloads))
1110 messages = self.transport.payloads[1]["messages"]1213 messages = self.transport.payloads[1]["messages"]
@@ -1112,13 +1215,14 @@ class MessageExchangeTest(LandscapeTest):
1112 self.assertIs(None, message_id)1215 self.assertIs(None, message_id)
1113 expected_log_entry = (1216 expected_log_entry = (
1114 "Response message with operation-id 234567 was discarded because "1217 "Response message with operation-id 234567 was discarded because "
1115 "the client's secure ID has changed in the meantime")1218 "the client's secure ID has changed in the meantime"
1219 )
1116 self.assertIn(expected_log_entry, self.logfile.getvalue())1220 self.assertIn(expected_log_entry, self.logfile.getvalue())
11171221
1118 # The MessageContext was removed after utilisation.1222 # The MessageContext was removed after utilisation.
1119 ids_after = self.exchange_store.all_operation_ids()1223 ids_after = self.exchange_store.all_operation_ids()
1120 self.assertEqual(len(ids_after), len(ids_before) - 1)1224 self.assertEqual(len(ids_after), len(ids_before) - 1)
1121 self.assertNotIn('234567', ids_after)1225 self.assertNotIn("234567", ids_after)
11221226
1123 def test_error_exchanging_causes_failed_exchange(self):1227 def test_error_exchanging_causes_failed_exchange(self):
1124 """1228 """
@@ -1135,13 +1239,14 @@ class MessageExchangeTest(LandscapeTest):
1135 self.exchanger.exchange()1239 self.exchanger.exchange()
1136 self.assertEqual([None], events)1240 self.assertEqual([None], events)
11371241
1138 def test_SSL_error_exchanging_causes_failed_exchange(self):1242 def test_SSL_error_exchanging_causes_failed_exchange(self): # noqa: N802
1139 """1243 """
1140 If an SSL error occurs when exchanging, the 'exchange-failed'1244 If an SSL error occurs when exchanging, the 'exchange-failed'
1141 event should be fired with the optional "ssl_error" flag set to True.1245 event should be fired with the optional "ssl_error" flag set to True.
1142 """1246 """
1143 self.log_helper.ignore_errors("Message exchange failed: Failed to "1247 self.log_helper.ignore_errors(
1144 "communicate.")1248 "Message exchange failed: Failed to communicate.",
1249 )
1145 events = []1250 events = []
11461251
1147 def failed_exchange(ssl_error):1252 def failed_exchange(ssl_error):
@@ -1149,8 +1254,9 @@ class MessageExchangeTest(LandscapeTest):
1149 events.append(None)1254 events.append(None)
11501255
1151 self.reactor.call_on("exchange-failed", failed_exchange)1256 self.reactor.call_on("exchange-failed", failed_exchange)
1152 self.transport.responses.append(PyCurlError(60,1257 self.transport.responses.append(
1153 "Failed to communicate."))1258 PyCurlError(60, "Failed to communicate."),
1259 )
1154 self.exchanger.exchange()1260 self.exchanger.exchange()
1155 self.assertEqual([None], events)1261 self.assertEqual([None], events)
11561262
@@ -1167,8 +1273,9 @@ class MessageExchangeTest(LandscapeTest):
1167 events.append(None)1273 events.append(None)
11681274
1169 self.reactor.call_on("exchange-failed", failed_exchange)1275 self.reactor.call_on("exchange-failed", failed_exchange)
1170 self.transport.responses.append(PyCurlError(10, # Not 601276 self.transport.responses.append(
1171 "Failed to communicate."))1277 PyCurlError(10, "Failed to communicate."), # Not 60
1278 )
1172 self.exchanger.exchange()1279 self.exchanger.exchange()
1173 self.assertEqual([None], events)1280 self.assertEqual([None], events)
11741281
@@ -1278,14 +1385,23 @@ class AcceptedTypesMessageExchangeTest(LandscapeTest):
1278 helpers = [ExchangeHelper]1385 helpers = [ExchangeHelper]
12791386
1280 def setUp(self):1387 def setUp(self):
1281 super(AcceptedTypesMessageExchangeTest, self).setUp()1388 super().setUp()
1282 self.pinger = Pinger(self.reactor, self.identity, self.exchanger,1389 self.pinger = Pinger(
1283 self.config)1390 self.reactor,
1391 self.identity,
1392 self.exchanger,
1393 self.config,
1394 )
1284 # The __init__ method of RegistrationHandler registers a few default1395 # The __init__ method of RegistrationHandler registers a few default
1285 # message types that we want to catch as well1396 # message types that we want to catch as well
1286 self.handler = RegistrationHandler(1397 self.handler = RegistrationHandler(
1287 self.config, self.identity, self.reactor, self.exchanger,1398 self.config,
1288 self.pinger, self.mstore)1399 self.identity,
1400 self.reactor,
1401 self.exchanger,
1402 self.pinger,
1403 self.mstore,
1404 )
12891405
1290 def test_register_accepted_message_type(self):1406 def test_register_accepted_message_type(self):
1291 self.exchanger.register_client_accepted_message_type("type-B")1407 self.exchanger.register_client_accepted_message_type("type-B")
@@ -1293,9 +1409,10 @@ class AcceptedTypesMessageExchangeTest(LandscapeTest):
1293 self.exchanger.register_client_accepted_message_type("type-C")1409 self.exchanger.register_client_accepted_message_type("type-C")
1294 self.exchanger.register_client_accepted_message_type("type-A")1410 self.exchanger.register_client_accepted_message_type("type-A")
1295 types = self.exchanger.get_client_accepted_message_types()1411 types = self.exchanger.get_client_accepted_message_types()
1296 self.assertEqual(types,1412 self.assertEqual(
1297 sorted(["type-A", "type-B", "type-C"] +1413 types,
1298 DEFAULT_ACCEPTED_TYPES))1414 sorted(["type-A", "type-B", "type-C"] + DEFAULT_ACCEPTED_TYPES),
1415 )
12991416
1300 def test_exchange_sends_message_type_when_no_hash(self):1417 def test_exchange_sends_message_type_when_no_hash(self):
1301 self.exchanger.register_client_accepted_message_type("type-A")1418 self.exchanger.register_client_accepted_message_type("type-A")
@@ -1303,15 +1420,17 @@ class AcceptedTypesMessageExchangeTest(LandscapeTest):
1303 self.exchanger.exchange()1420 self.exchanger.exchange()
1304 self.assertEqual(1421 self.assertEqual(
1305 self.transport.payloads[0]["client-accepted-types"],1422 self.transport.payloads[0]["client-accepted-types"],
1306 sorted(["type-A", "type-B"] + DEFAULT_ACCEPTED_TYPES))1423 sorted(["type-A", "type-B"] + DEFAULT_ACCEPTED_TYPES),
1424 )
13071425
1308 def test_exchange_does_not_send_message_types_when_hash_matches(self):1426 def test_exchange_does_not_send_message_types_when_hash_matches(self):
1309 self.exchanger.register_client_accepted_message_type("type-A")1427 self.exchanger.register_client_accepted_message_type("type-A")
1310 self.exchanger.register_client_accepted_message_type("type-B")1428 self.exchanger.register_client_accepted_message_type("type-B")
1311 types = sorted(["type-A", "type-B"] + DEFAULT_ACCEPTED_TYPES)1429 types = sorted(["type-A", "type-B"] + DEFAULT_ACCEPTED_TYPES)
1312 accepted_types_digest = md5(";".join(types).encode("ascii")).digest()1430 accepted_types_digest = md5(";".join(types).encode("ascii")).digest()
1313 self.transport.extra["client-accepted-types-hash"] = \1431 self.transport.extra[
1314 accepted_types_digest1432 "client-accepted-types-hash"
1433 ] = accepted_types_digest
1315 self.exchanger.exchange()1434 self.exchanger.exchange()
1316 self.exchanger.exchange()1435 self.exchanger.exchange()
1317 self.assertNotIn("client-accepted-types", self.transport.payloads[1])1436 self.assertNotIn("client-accepted-types", self.transport.payloads[1])
@@ -1327,7 +1446,8 @@ class AcceptedTypesMessageExchangeTest(LandscapeTest):
1327 self.exchanger.exchange()1446 self.exchanger.exchange()
1328 self.assertEqual(1447 self.assertEqual(
1329 self.transport.payloads[1]["client-accepted-types"],1448 self.transport.payloads[1]["client-accepted-types"],
1330 sorted(["type-A", "type-B"] + DEFAULT_ACCEPTED_TYPES))1449 sorted(["type-A", "type-B"] + DEFAULT_ACCEPTED_TYPES),
1450 )
13311451
1332 def test_exchange_sends_new_accepted_types_hash(self):1452 def test_exchange_sends_new_accepted_types_hash(self):
1333 """1453 """
@@ -1342,7 +1462,8 @@ class AcceptedTypesMessageExchangeTest(LandscapeTest):
1342 self.exchanger.exchange()1462 self.exchanger.exchange()
1343 self.assertEqual(1463 self.assertEqual(
1344 self.transport.payloads[1]["client-accepted-types"],1464 self.transport.payloads[1]["client-accepted-types"],
1345 sorted(["type-A", "type-B"] + DEFAULT_ACCEPTED_TYPES))1465 sorted(["type-A", "type-B"] + DEFAULT_ACCEPTED_TYPES),
1466 )
13461467
1347 def test_exchange_sends_new_types_when_server_screws_up(self):1468 def test_exchange_sends_new_types_when_server_screws_up(self):
1348 """1469 """
@@ -1359,7 +1480,8 @@ class AcceptedTypesMessageExchangeTest(LandscapeTest):
1359 self.exchanger.exchange()1480 self.exchanger.exchange()
1360 self.assertEqual(1481 self.assertEqual(
1361 self.transport.payloads[2]["client-accepted-types"],1482 self.transport.payloads[2]["client-accepted-types"],
1362 sorted(["type-A"] + DEFAULT_ACCEPTED_TYPES))1483 sorted(["type-A"] + DEFAULT_ACCEPTED_TYPES),
1484 )
13631485
1364 def test_register_message_adds_accepted_type(self):1486 def test_register_message_adds_accepted_type(self):
1365 """1487 """
@@ -1373,7 +1495,6 @@ class AcceptedTypesMessageExchangeTest(LandscapeTest):
13731495
13741496
1375class GetAcceptedTypesDiffTest(LandscapeTest):1497class GetAcceptedTypesDiffTest(LandscapeTest):
1376
1377 def test_diff_empty(self):1498 def test_diff_empty(self):
1378 self.assertEqual(get_accepted_types_diff([], []), "")1499 self.assertEqual(get_accepted_types_diff([], []), "")
13791500
@@ -1387,6 +1508,7 @@ class GetAcceptedTypesDiffTest(LandscapeTest):
1387 self.assertEqual(get_accepted_types_diff(["ooga"], ["ooga"]), "ooga")1508 self.assertEqual(get_accepted_types_diff(["ooga"], ["ooga"]), "ooga")
13881509
1389 def test_diff_complex(self):1510 def test_diff_complex(self):
1390 self.assertEqual(get_accepted_types_diff(["foo", "bar"],1511 self.assertEqual(
1391 ["foo", "ooga"]),1512 get_accepted_types_diff(["foo", "bar"], ["foo", "ooga"]),
1392 "+ooga foo -bar")1513 "+ooga foo -bar",
1514 )
diff --git a/landscape/client/broker/tests/test_exchangestore.py b/landscape/client/broker/tests/test_exchangestore.py
index 45354db..76a7d9f 100644
--- a/landscape/client/broker/tests/test_exchangestore.py
+++ b/landscape/client/broker/tests/test_exchangestore.py
@@ -14,7 +14,7 @@ class ExchangeStoreTest(LandscapeTest):
14 """Unit tests for the C{ExchangeStore}."""14 """Unit tests for the C{ExchangeStore}."""
1515
16 def setUp(self):16 def setUp(self):
17 super(ExchangeStoreTest, self).setUp()17 super().setUp()
1818
19 self.filename = self.makeFile()19 self.filename = self.makeFile()
20 self.store1 = ExchangeStore(self.filename)20 self.store1 = ExchangeStore(self.filename)
@@ -23,19 +23,21 @@ class ExchangeStoreTest(LandscapeTest):
23 def test_add_message_context(self):23 def test_add_message_context(self):
24 """Adding a message context works correctly."""24 """Adding a message context works correctly."""
25 now = time.time()25 now = time.time()
26 self.store1.add_message_context(123, 'abc', 'change-packages')26 self.store1.add_message_context(123, "abc", "change-packages")
2727
28 db = sqlite3.connect(self.store2._filename)28 db = sqlite3.connect(self.store2._filename)
29 cursor = db.cursor()29 cursor = db.cursor()
30 cursor.execute(30 cursor.execute(
31 "SELECT operation_id, secure_id, message_type, timestamp "31 "SELECT operation_id, secure_id, message_type, timestamp "
32 "FROM message_context WHERE operation_id=?", (123,))32 "FROM message_context WHERE operation_id=?",
33 (123,),
34 )
33 results = cursor.fetchall()35 results = cursor.fetchall()
34 self.assertEqual(1, len(results))36 self.assertEqual(1, len(results))
35 [row] = results37 [row] = results
36 self.assertEqual(123, row[0])38 self.assertEqual(123, row[0])
37 self.assertEqual('abc', row[1])39 self.assertEqual("abc", row[1])
38 self.assertEqual('change-packages', row[2])40 self.assertEqual("change-packages", row[2])
39 self.assertTrue(row[3] > now)41 self.assertTrue(row[3] > now)
4042
41 def test_add_message_context_with_duplicate_operation_id(self):43 def test_add_message_context_with_duplicate_operation_id(self):
@@ -43,18 +45,22 @@ class ExchangeStoreTest(LandscapeTest):
43 self.store1.add_message_context(123, "abc", "change-packages")45 self.store1.add_message_context(123, "abc", "change-packages")
44 self.assertRaises(46 self.assertRaises(
45 (sqlite3.IntegrityError, sqlite3.OperationalError),47 (sqlite3.IntegrityError, sqlite3.OperationalError),
46 self.store1.add_message_context, 123, "def", "change-packages")48 self.store1.add_message_context,
49 123,
50 "def",
51 "change-packages",
52 )
4753
48 def test_get_message_context(self):54 def test_get_message_context(self):
49 """55 """
50 Accessing a C{MessageContext} with an existing C{operation-id} works.56 Accessing a C{MessageContext} with an existing C{operation-id} works.
51 """57 """
52 now = time.time()58 now = time.time()
53 self.store1.add_message_context(234, 'bcd', 'change-packages')59 self.store1.add_message_context(234, "bcd", "change-packages")
54 context = self.store2.get_message_context(234)60 context = self.store2.get_message_context(234)
55 self.assertEqual(234, context.operation_id)61 self.assertEqual(234, context.operation_id)
56 self.assertEqual('bcd', context.secure_id)62 self.assertEqual("bcd", context.secure_id)
57 self.assertEqual('change-packages', context.message_type)63 self.assertEqual("change-packages", context.message_type)
58 self.assertTrue(context.timestamp > now)64 self.assertTrue(context.timestamp > now)
5965
60 def test_get_message_context_with_nonexistent_operation_id(self):66 def test_get_message_context_with_nonexistent_operation_id(self):
@@ -65,7 +71,10 @@ class ExchangeStoreTest(LandscapeTest):
65 def test_message_context_remove(self):71 def test_message_context_remove(self):
66 """C{MessageContext}s are deleted correctly."""72 """C{MessageContext}s are deleted correctly."""
67 context = self.store1.add_message_context(73 context = self.store1.add_message_context(
68 345, 'opq', 'change-packages')74 345,
75 "opq",
76 "change-packages",
77 )
69 context.remove()78 context.remove()
70 self.assertIs(None, self.store1.get_message_context(345))79 self.assertIs(None, self.store1.get_message_context(345))
7180
@@ -78,7 +87,7 @@ class ExchangeStoreTest(LandscapeTest):
7887
79 def test_all_operation_ids(self):88 def test_all_operation_ids(self):
80 """C{all_operation_ids} works correctly."""89 """C{all_operation_ids} works correctly."""
81 self.store1.add_message_context(456, 'cde', 'change-packages')90 self.store1.add_message_context(456, "cde", "change-packages")
82 self.assertEqual([456], self.store2.all_operation_ids())91 self.assertEqual([456], self.store2.all_operation_ids())
83 self.store2.add_message_context(567, 'def', 'change-packages')92 self.store2.add_message_context(567, "def", "change-packages")
84 self.assertEqual([456, 567], self.store1.all_operation_ids())93 self.assertEqual([456, 567], self.store1.all_operation_ids())
diff --git a/landscape/client/broker/tests/test_ping.py b/landscape/client/broker/tests/test_ping.py
index b095b5a..843b984 100644
--- a/landscape/client/broker/tests/test_ping.py
+++ b/landscape/client/broker/tests/test_ping.py
@@ -1,15 +1,15 @@
1from landscape.client.tests.helpers import LandscapeTest
2
3from twisted.internet.defer import fail1from twisted.internet.defer import fail
42
3from landscape.client.broker.ping import PingClient
4from landscape.client.broker.ping import Pinger
5from landscape.client.broker.tests.helpers import ExchangeHelper
6from landscape.client.tests.helpers import LandscapeTest
5from landscape.lib import bpickle7from landscape.lib import bpickle
6from landscape.lib.fetch import fetch8from landscape.lib.fetch import fetch
7from landscape.lib.testing import FakeReactor9from landscape.lib.testing import FakeReactor
8from landscape.client.broker.ping import PingClient, Pinger
9from landscape.client.broker.tests.helpers import ExchangeHelper
1010
1111
12class FakePageGetter(object):12class FakePageGetter:
13 """An fake web client."""13 """An fake web client."""
1414
15 def __init__(self, response):15 def __init__(self, response):
@@ -39,9 +39,8 @@ class FakePageGetter(object):
3939
4040
41class PingClientTest(LandscapeTest):41class PingClientTest(LandscapeTest):
42
43 def setUp(self):42 def setUp(self):
44 super(PingClientTest, self).setUp()43 super().setUp()
45 self.reactor = FakeReactor()44 self.reactor = FakeReactor()
4645
47 def test_default_get_page(self):46 def test_default_get_page(self):
@@ -64,8 +63,15 @@ class PingClientTest(LandscapeTest):
64 pinger.ping(url, insecure_id)63 pinger.ping(url, insecure_id)
65 self.assertEqual(64 self.assertEqual(
66 client.fetches,65 client.fetches,
67 [(url, True, {"Content-Type": "application/x-www-form-urlencoded"},66 [
68 "insecure_id=10")])67 (
68 url,
69 True,
70 {"Content-Type": "application/x-www-form-urlencoded"},
71 "insecure_id=10",
72 ),
73 ],
74 )
6975
70 def test_ping_no_insecure_id(self):76 def test_ping_no_insecure_id(self):
71 """77 """
@@ -100,6 +106,7 @@ class PingClientTest(LandscapeTest):
100106
101 def errback(failure):107 def errback(failure):
102 failures.append(failure)108 failures.append(failure)
109
103 d.addErrback(errback)110 d.addErrback(errback)
104 self.assertEqual(len(failures), 1)111 self.assertEqual(len(failures), 1)
105 self.assertEqual(failures[0].getErrorMessage(), "That's a failure!")112 self.assertEqual(failures[0].getErrorMessage(), "That's a failure!")
@@ -116,7 +123,7 @@ class PingerTest(LandscapeTest):
116 install_exchanger = False123 install_exchanger = False
117124
118 def setUp(self):125 def setUp(self):
119 super(PingerTest, self).setUp()126 super().setUp()
120 self.page_getter = FakePageGetter(None)127 self.page_getter = FakePageGetter(None)
121128
122 def factory(reactor):129 def factory(reactor):
@@ -125,21 +132,25 @@ class PingerTest(LandscapeTest):
125 self.config.ping_url = "http://localhost:8081/whatever"132 self.config.ping_url = "http://localhost:8081/whatever"
126 self.config.ping_interval = 10133 self.config.ping_interval = 10
127134
128 self.pinger = Pinger(self.reactor,135 self.pinger = Pinger(
129 self.identity,136 self.reactor,
130 self.exchanger,137 self.identity,
131 self.config,138 self.exchanger,
132 ping_client_factory=factory)139 self.config,
140 ping_client_factory=factory,
141 )
133142
134 def test_default_ping_client(self):143 def test_default_ping_client(self):
135 """144 """
136 The C{ping_client_factory} argument to L{Pinger} should be optional,145 The C{ping_client_factory} argument to L{Pinger} should be optional,
137 and default to L{PingClient}.146 and default to L{PingClient}.
138 """147 """
139 pinger = Pinger(self.reactor,148 pinger = Pinger(
140 self.identity,149 self.reactor,
141 self.exchanger,150 self.identity,
142 self.config)151 self.exchanger,
152 self.config,
153 )
143 self.assertEqual(pinger.ping_client_factory, PingClient)154 self.assertEqual(pinger.ping_client_factory, PingClient)
144155
145 def test_occasional_ping(self):156 def test_occasional_ping(self):
@@ -195,7 +206,7 @@ class PingerTest(LandscapeTest):
195 self.log_helper.ignore_errors(ZeroDivisionError)206 self.log_helper.ignore_errors(ZeroDivisionError)
196 self.identity.insecure_id = 42207 self.identity.insecure_id = 42
197208
198 class BadPingClient(object):209 class BadPingClient:
199 def __init__(self, *args, **kwargs):210 def __init__(self, *args, **kwargs):
200 pass211 pass
201212
@@ -204,11 +215,13 @@ class PingerTest(LandscapeTest):
204 return fail(ZeroDivisionError("Couldn't fetch page"))215 return fail(ZeroDivisionError("Couldn't fetch page"))
205216
206 self.config.ping_url = "http://foo.com/"217 self.config.ping_url = "http://foo.com/"
207 pinger = Pinger(self.reactor,218 pinger = Pinger(
208 self.identity,219 self.reactor,
209 self.exchanger,220 self.identity,
210 self.config,221 self.exchanger,
211 ping_client_factory=BadPingClient)222 self.config,
223 ping_client_factory=BadPingClient,
224 )
212 pinger.start()225 pinger.start()
213226
214 self.reactor.advance(30)227 self.reactor.advance(30)
@@ -238,8 +251,10 @@ class PingerTest(LandscapeTest):
238 self.assertEqual(len(self.page_getter.fetches), 1)251 self.assertEqual(len(self.page_getter.fetches), 1)
239252
240 def test_get_url(self):253 def test_get_url(self):
241 self.assertEqual(self.pinger.get_url(),254 self.assertEqual(
242 "http://localhost:8081/whatever")255 self.pinger.get_url(),
256 "http://localhost:8081/whatever",
257 )
243258
244 def test_config_url(self):259 def test_config_url(self):
245 """260 """
diff --git a/landscape/client/broker/tests/test_registration.py b/landscape/client/broker/tests/test_registration.py
index 8ca0cb4..fe1d256 100644
--- a/landscape/client/broker/tests/test_registration.py
+++ b/landscape/client/broker/tests/test_registration.py
@@ -1,14 +1,14 @@
1import json1import json
2import logging2import logging
3import socket3import socket
4import mock4from unittest import mock
55
6from landscape.lib.compat import _PY36from landscape.client.broker.registration import Identity
77from landscape.client.broker.registration import RegistrationError
8from landscape.client.broker.registration import RegistrationError, Identity8from landscape.client.broker.tests.helpers import BrokerConfigurationHelper
9from landscape.client.broker.tests.helpers import RegistrationHelper
9from landscape.client.tests.helpers import LandscapeTest10from landscape.client.tests.helpers import LandscapeTest
10from landscape.client.broker.tests.helpers import (11from landscape.lib.compat import _PY3
11 BrokerConfigurationHelper, RegistrationHelper)
12from landscape.lib.persist import Persist12from landscape.lib.persist import Persist
1313
1414
@@ -17,33 +17,43 @@ class IdentityTest(LandscapeTest):
17 helpers = [BrokerConfigurationHelper]17 helpers = [BrokerConfigurationHelper]
1818
19 def setUp(self):19 def setUp(self):
20 super(IdentityTest, self).setUp()20 super().setUp()
21 self.persist = Persist(filename=self.makePersistFile())21 self.persist = Persist(filename=self.makePersistFile())
22 self.identity = Identity(self.config, self.persist)22 self.identity = Identity(self.config, self.persist)
2323
24 def check_persist_property(self, attr, persist_name):24 def check_persist_property(self, attr, persist_name):
25 value = "VALUE"25 value = "VALUE"
26 self.assertEqual(getattr(self.identity, attr), None,26 self.assertEqual(
27 "%r attribute should default to None, not %r" %27 getattr(self.identity, attr),
28 (attr, getattr(self.identity, attr)))28 None,
29 f"{attr!r} attribute should default to None, "
30 f"not {getattr(self.identity, attr)!r}",
31 )
29 setattr(self.identity, attr, value)32 setattr(self.identity, attr, value)
30 self.assertEqual(getattr(self.identity, attr), value,
31 "%r attribute should be %r, not %r" %
32 (attr, value, getattr(self.identity, attr)))
33 self.assertEqual(33 self.assertEqual(
34 self.persist.get(persist_name), value,34 getattr(self.identity, attr),
35 "%r not set to %r in persist" % (persist_name, value))35 value,
36 f"{attr!r} attribute should be {value!r}, "
37 f"not {getattr(self.identity, attr)!r}",
38 )
39 self.assertEqual(
40 self.persist.get(persist_name),
41 value,
42 f"{persist_name!r} not set to {value!r} in persist",
43 )
3644
37 def check_config_property(self, attr):45 def check_config_property(self, attr):
38 value = "VALUE"46 value = "VALUE"
39 setattr(self.config, attr, value)47 setattr(self.config, attr, value)
40 self.assertEqual(getattr(self.identity, attr), value,48 self.assertEqual(
41 "%r attribute should be %r, not %r" %49 getattr(self.identity, attr),
42 (attr, value, getattr(self.identity, attr)))50 value,
51 f"{attr!r} attribute should be {value!r}, "
52 f"not {getattr(self.identity, attr)!r}",
53 )
4354
44 def test_secure_id(self):55 def test_secure_id(self):
45 self.check_persist_property("secure_id",56 self.check_persist_property("secure_id", "registration.secure-id")
46 "registration.secure-id")
4757
48 def test_secure_id_as_unicode(self):58 def test_secure_id_as_unicode(self):
49 """secure-id is expected to be retrieved as unicode."""59 """secure-id is expected to be retrieved as unicode."""
@@ -51,8 +61,7 @@ class IdentityTest(LandscapeTest):
51 self.assertEqual(self.identity.secure_id, "spam")61 self.assertEqual(self.identity.secure_id, "spam")
5262
53 def test_insecure_id(self):63 def test_insecure_id(self):
54 self.check_persist_property("insecure_id",64 self.check_persist_property("insecure_id", "registration.insecure-id")
55 "registration.insecure-id")
5665
57 def test_computer_title(self):66 def test_computer_title(self):
58 self.check_config_property("computer_title")67 self.check_config_property("computer_title")
@@ -75,7 +84,7 @@ class RegistrationHandlerTestBase(LandscapeTest):
75 helpers = [RegistrationHelper]84 helpers = [RegistrationHelper]
7685
77 def setUp(self):86 def setUp(self):
78 super(RegistrationHandlerTestBase, self).setUp()87 super().setUp()
79 logging.getLogger().setLevel(logging.INFO)88 logging.getLogger().setLevel(logging.INFO)
80 self.hostname = "ooga.local"89 self.hostname = "ooga.local"
81 self.addCleanup(setattr, socket, "getfqdn", socket.getfqdn)90 self.addCleanup(setattr, socket, "getfqdn", socket.getfqdn)
@@ -83,14 +92,14 @@ class RegistrationHandlerTestBase(LandscapeTest):
8392
8493
85class RegistrationHandlerTest(RegistrationHandlerTestBase):94class RegistrationHandlerTest(RegistrationHandlerTestBase):
86
87 def test_server_initiated_id_changing(self):95 def test_server_initiated_id_changing(self):
88 """96 """
89 The server must be able to ask a client to change its secure97 The server must be able to ask a client to change its secure
90 and insecure ids even if no requests were sent.98 and insecure ids even if no requests were sent.
91 """99 """
92 self.exchanger.handle_message(100 self.exchanger.handle_message(
93 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"})101 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"},
102 )
94 self.assertEqual(self.identity.secure_id, "abc")103 self.assertEqual(self.identity.secure_id, "abc")
95 self.assertEqual(self.identity.insecure_id, "def")104 self.assertEqual(self.identity.insecure_id, "def")
96105
@@ -101,7 +110,8 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
101 """110 """
102 reactor_fire_mock = self.reactor.fire = mock.Mock()111 reactor_fire_mock = self.reactor.fire = mock.Mock()
103 self.exchanger.handle_message(112 self.exchanger.handle_message(
104 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"})113 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"},
114 )
105 reactor_fire_mock.assert_any_call("registration-done")115 reactor_fire_mock.assert_any_call("registration-done")
106116
107 def test_unknown_id(self):117 def test_unknown_id(self):
@@ -120,9 +130,12 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
120 self.config.computer_title = "Wu"130 self.config.computer_title = "Wu"
121 self.mstore.set_accepted_types(["register"])131 self.mstore.set_accepted_types(["register"])
122 self.exchanger.handle_message(132 self.exchanger.handle_message(
123 {"type": b"unknown-id", "clone-of": "Wu"})133 {"type": b"unknown-id", "clone-of": "Wu"},
124 self.assertIn("Client is clone of computer Wu",134 )
125 self.logfile.getvalue())135 self.assertIn(
136 "Client is clone of computer Wu",
137 self.logfile.getvalue(),
138 )
126139
127 def test_clone_secure_id_saved(self):140 def test_clone_secure_id_saved(self):
128 """141 """
@@ -134,7 +147,8 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
134 self.config.computer_title = "Wu"147 self.config.computer_title = "Wu"
135 self.mstore.set_accepted_types(["register"])148 self.mstore.set_accepted_types(["register"])
136 self.exchanger.handle_message(149 self.exchanger.handle_message(
137 {"type": b"unknown-id", "clone-of": "Wu"})150 {"type": b"unknown-id", "clone-of": "Wu"},
151 )
138 self.assertEqual(self.handler._clone_secure_id, secure_id)152 self.assertEqual(self.handler._clone_secure_id, secure_id)
139 self.assertIsNone(self.identity.secure_id)153 self.assertIsNone(self.identity.secure_id)
140154
@@ -148,7 +162,8 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
148 self.mstore.set_accepted_types(["register"])162 self.mstore.set_accepted_types(["register"])
149 self.mstore.set_server_api(b"3.3") # Note this is only for later api163 self.mstore.set_server_api(b"3.3") # Note this is only for later api
150 self.exchanger.handle_message(164 self.exchanger.handle_message(
151 {"type": b"unknown-id", "clone-of": "Wu"})165 {"type": b"unknown-id", "clone-of": "Wu"},
166 )
152 self.reactor.fire("pre-exchange")167 self.reactor.fire("pre-exchange")
153 messages = self.mstore.get_pending_messages()168 messages = self.mstore.get_pending_messages()
154 self.assertEqual(messages[0]["clone_secure_id"], secure_id)169 self.assertEqual(messages[0]["clone_secure_id"], secure_id)
@@ -192,9 +207,11 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
192 messages = self.mstore.get_pending_messages()207 messages = self.mstore.get_pending_messages()
193 self.assertEqual(1, len(messages))208 self.assertEqual(1, len(messages))
194 self.assertEqual("register", messages[0]["type"])209 self.assertEqual("register", messages[0]["type"])
195 self.assertEqual(self.logfile.getvalue().strip(),210 self.assertEqual(
196 "INFO: Queueing message to register with account "211 self.logfile.getvalue().strip(),
197 "'account_name' without a password.")212 "INFO: Queueing message to register with account "
213 "'account_name' without a password.",
214 )
198215
199 @mock.patch("landscape.client.broker.registration.get_vm_info")216 @mock.patch("landscape.client.broker.registration.get_vm_info")
200 def test_queue_message_on_exchange_with_vm_info(self, get_vm_info_mock):217 def test_queue_message_on_exchange_with_vm_info(self, get_vm_info_mock):
@@ -210,14 +227,18 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
210 self.reactor.fire("pre-exchange")227 self.reactor.fire("pre-exchange")
211 messages = self.mstore.get_pending_messages()228 messages = self.mstore.get_pending_messages()
212 self.assertEqual(b"vmware", messages[0]["vm-info"])229 self.assertEqual(b"vmware", messages[0]["vm-info"])
213 self.assertEqual(self.logfile.getvalue().strip(),230 self.assertEqual(
214 "INFO: Queueing message to register with account "231 self.logfile.getvalue().strip(),
215 "'account_name' without a password.")232 "INFO: Queueing message to register with account "
233 "'account_name' without a password.",
234 )
216 get_vm_info_mock.assert_called_once_with()235 get_vm_info_mock.assert_called_once_with()
217236
218 @mock.patch("landscape.client.broker.registration.get_container_info")237 @mock.patch("landscape.client.broker.registration.get_container_info")
219 def test_queue_message_on_exchange_with_lxc_container(238 def test_queue_message_on_exchange_with_lxc_container(
220 self, get_container_info_mock):239 self,
240 get_container_info_mock,
241 ):
221 """242 """
222 If the client is running in an LXC container, the information is243 If the client is running in an LXC container, the information is
223 included in the registration message.244 included in the registration message.
@@ -241,9 +262,11 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
241 messages = self.mstore.get_pending_messages()262 messages = self.mstore.get_pending_messages()
242 password = messages[0]["registration_password"]263 password = messages[0]["registration_password"]
243 self.assertEqual("SEKRET", password)264 self.assertEqual("SEKRET", password)
244 self.assertEqual(self.logfile.getvalue().strip(),265 self.assertEqual(
245 "INFO: Queueing message to register with account "266 self.logfile.getvalue().strip(),
246 "'account_name' with a password.")267 "INFO: Queueing message to register with account "
268 "'account_name' with a password.",
269 )
247270
248 def test_queue_message_on_exchange_with_tags(self):271 def test_queue_message_on_exchange_with_tags(self):
249 """272 """
@@ -254,14 +277,16 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
254 self.config.computer_title = "Computer Title"277 self.config.computer_title = "Computer Title"
255 self.config.account_name = "account_name"278 self.config.account_name = "account_name"
256 self.config.registration_key = "SEKRET"279 self.config.registration_key = "SEKRET"
257 self.config.tags = u"computer,tag"280 self.config.tags = "computer,tag"
258 self.reactor.fire("pre-exchange")281 self.reactor.fire("pre-exchange")
259 messages = self.mstore.get_pending_messages()282 messages = self.mstore.get_pending_messages()
260 self.assertEqual("computer,tag", messages[0]["tags"])283 self.assertEqual("computer,tag", messages[0]["tags"])
261 self.assertEqual(self.logfile.getvalue().strip(),284 self.assertEqual(
262 "INFO: Queueing message to register with account "285 self.logfile.getvalue().strip(),
263 "'account_name' and tags computer,tag with a "286 "INFO: Queueing message to register with account "
264 "password.")287 "'account_name' and tags computer,tag with a "
288 "password.",
289 )
265290
266 def test_queue_message_on_exchange_with_invalid_tags(self):291 def test_queue_message_on_exchange_with_invalid_tags(self):
267 """292 """
@@ -273,14 +298,16 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
273 self.config.computer_title = "Computer Title"298 self.config.computer_title = "Computer Title"
274 self.config.account_name = "account_name"299 self.config.account_name = "account_name"
275 self.config.registration_key = "SEKRET"300 self.config.registration_key = "SEKRET"
276 self.config.tags = u"<script>alert()</script>"301 self.config.tags = "<script>alert()</script>"
277 self.reactor.fire("pre-exchange")302 self.reactor.fire("pre-exchange")
278 messages = self.mstore.get_pending_messages()303 messages = self.mstore.get_pending_messages()
279 self.assertIs(None, messages[0]["tags"])304 self.assertIs(None, messages[0]["tags"])
280 self.assertEqual(self.logfile.getvalue().strip(),305 self.assertEqual(
281 "ERROR: Invalid tags provided for registration.\n "306 self.logfile.getvalue().strip(),
282 "INFO: Queueing message to register with account "307 "ERROR: Invalid tags provided for registration.\n "
283 "'account_name' with a password.")308 "INFO: Queueing message to register with account "
309 "'account_name' with a password.",
310 )
284311
285 def test_queue_message_on_exchange_with_unicode_tags(self):312 def test_queue_message_on_exchange_with_unicode_tags(self):
286 """313 """
@@ -291,10 +318,10 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
291 self.config.computer_title = "Computer Title"318 self.config.computer_title = "Computer Title"
292 self.config.account_name = "account_name"319 self.config.account_name = "account_name"
293 self.config.registration_key = "SEKRET"320 self.config.registration_key = "SEKRET"
294 self.config.tags = u"prova\N{LATIN SMALL LETTER J WITH CIRCUMFLEX}o"321 self.config.tags = "prova\N{LATIN SMALL LETTER J WITH CIRCUMFLEX}o"
295 self.reactor.fire("pre-exchange")322 self.reactor.fire("pre-exchange")
296 messages = self.mstore.get_pending_messages()323 messages = self.mstore.get_pending_messages()
297 expected = u"prova\N{LATIN SMALL LETTER J WITH CIRCUMFLEX}o"324 expected = "prova\N{LATIN SMALL LETTER J WITH CIRCUMFLEX}o"
298 self.assertEqual(expected, messages[0]["tags"])325 self.assertEqual(expected, messages[0]["tags"])
299326
300 logs = self.logfile.getvalue().strip()327 logs = self.logfile.getvalue().strip()
@@ -306,10 +333,12 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
306 # here, to circumvent that problem.333 # here, to circumvent that problem.
307 if _PY3:334 if _PY3:
308 logs = logs.encode("utf-8")335 logs = logs.encode("utf-8")
309 self.assertEqual(logs,336 self.assertEqual(
310 b"INFO: Queueing message to register with account "337 logs,
311 b"'account_name' and tags prova\xc4\xb5o "338 b"INFO: Queueing message to register with account "
312 b"with a password.")339 b"'account_name' and tags prova\xc4\xb5o "
340 b"with a password.",
341 )
313342
314 def test_queue_message_on_exchange_with_access_group(self):343 def test_queue_message_on_exchange_with_access_group(self):
315 """344 """
@@ -318,15 +347,17 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
318 """347 """
319 self.mstore.set_accepted_types(["register"])348 self.mstore.set_accepted_types(["register"])
320 self.config.account_name = "account_name"349 self.config.account_name = "account_name"
321 self.config.access_group = u"dinosaurs"350 self.config.access_group = "dinosaurs"
322 self.config.tags = u"server,london"351 self.config.tags = "server,london"
323 self.reactor.fire("pre-exchange")352 self.reactor.fire("pre-exchange")
324 messages = self.mstore.get_pending_messages()353 messages = self.mstore.get_pending_messages()
325 self.assertEqual("dinosaurs", messages[0]["access_group"])354 self.assertEqual("dinosaurs", messages[0]["access_group"])
326 self.assertEqual(self.logfile.getvalue().strip(),355 self.assertEqual(
327 "INFO: Queueing message to register with account "356 self.logfile.getvalue().strip(),
328 "'account_name' in access group 'dinosaurs' and "357 "INFO: Queueing message to register with account "
329 "tags server,london without a password.")358 "'account_name' in access group 'dinosaurs' and "
359 "tags server,london without a password.",
360 )
330361
331 def test_queue_message_on_exchange_with_empty_access_group(self):362 def test_queue_message_on_exchange_with_empty_access_group(self):
332 """363 """
@@ -334,7 +365,7 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
334 an "access_group" key.365 an "access_group" key.
335 """366 """
336 self.mstore.set_accepted_types(["register"])367 self.mstore.set_accepted_types(["register"])
337 self.config.access_group = u""368 self.config.access_group = ""
338 self.reactor.fire("pre-exchange")369 self.reactor.fire("pre-exchange")
339 messages = self.mstore.get_pending_messages()370 messages = self.mstore.get_pending_messages()
340 # Make sure the key does not appear in the outgoing message.371 # Make sure the key does not appear in the outgoing message.
@@ -368,8 +399,7 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
368 self.assertEqual(messages[0]["type"], "register")399 self.assertEqual(messages[0]["type"], "register")
369400
370 def test_no_message_when_should_register_is_false(self):401 def test_no_message_when_should_register_is_false(self):
371 """If we already have a secure id, do not queue a register message.402 """If we already have a secure id, do not queue a register message."""
372 """
373 self.mstore.set_accepted_types(["register"])403 self.mstore.set_accepted_types(["register"])
374 self.config.computer_title = "Computer Title"404 self.config.computer_title = "Computer Title"
375 self.config.account_name = "account_name"405 self.config.account_name = "account_name"
@@ -395,9 +425,12 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
395 """425 """
396 reactor_fire_mock = self.reactor.fire = mock.Mock()426 reactor_fire_mock = self.reactor.fire = mock.Mock()
397 self.exchanger.handle_message(427 self.exchanger.handle_message(
398 {"type": b"registration", "info": b"unknown-account"})428 {"type": b"registration", "info": b"unknown-account"},
429 )
399 reactor_fire_mock.assert_called_with(430 reactor_fire_mock.assert_called_with(
400 "registration-failed", reason="unknown-account")431 "registration-failed",
432 reason="unknown-account",
433 )
401434
402 def test_registration_failed_event_max_pending_computers(self):435 def test_registration_failed_event_max_pending_computers(self):
403 """436 """
@@ -407,9 +440,12 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
407 """440 """
408 reactor_fire_mock = self.reactor.fire = mock.Mock()441 reactor_fire_mock = self.reactor.fire = mock.Mock()
409 self.exchanger.handle_message(442 self.exchanger.handle_message(
410 {"type": b"registration", "info": b"max-pending-computers"})443 {"type": b"registration", "info": b"max-pending-computers"},
444 )
411 reactor_fire_mock.assert_called_with(445 reactor_fire_mock.assert_called_with(
412 "registration-failed", reason="max-pending-computers")446 "registration-failed",
447 reason="max-pending-computers",
448 )
413449
414 def test_registration_failed_event_not_fired_when_uncertain(self):450 def test_registration_failed_event_not_fired_when_uncertain(self):
415 """451 """
@@ -418,7 +454,8 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
418 """454 """
419 reactor_fire_mock = self.reactor.fire = mock.Mock()455 reactor_fire_mock = self.reactor.fire = mock.Mock()
420 self.exchanger.handle_message(456 self.exchanger.handle_message(
421 {"type": b"registration", "info": b"blah-blah"})457 {"type": b"registration", "info": b"blah-blah"},
458 )
422 for name, args, kwargs in reactor_fire_mock.mock_calls:459 for name, args, kwargs in reactor_fire_mock.mock_calls:
423 self.assertNotEquals("registration-failed", args[0])460 self.assertNotEquals("registration-failed", args[0])
424461
@@ -449,13 +486,15 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
449486
450 # This should somehow callback the deferred.487 # This should somehow callback the deferred.
451 self.exchanger.handle_message(488 self.exchanger.handle_message(
452 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"})489 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"},
490 )
453491
454 self.assertEqual(calls, [1])492 self.assertEqual(calls, [1])
455493
456 # Doing it again to ensure that the deferred isn't called twice.494 # Doing it again to ensure that the deferred isn't called twice.
457 self.exchanger.handle_message(495 self.exchanger.handle_message(
458 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"})496 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"},
497 )
459498
460 self.assertEqual(calls, [1])499 self.assertEqual(calls, [1])
461500
@@ -477,7 +516,8 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
477516
478 # This should somehow callback the deferred.517 # This should somehow callback the deferred.
479 self.exchanger.handle_message(518 self.exchanger.handle_message(
480 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"})519 {"type": b"set-id", "id": b"abc", "insecure-id": b"def"},
520 )
481521
482 self.assertEqual(results, [None])522 self.assertEqual(results, [None])
483523
@@ -502,13 +542,15 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
502542
503 # This should somehow callback the deferred.543 # This should somehow callback the deferred.
504 self.exchanger.handle_message(544 self.exchanger.handle_message(
505 {"type": b"registration", "info": b"unknown-account"})545 {"type": b"registration", "info": b"unknown-account"},
546 )
506547
507 self.assertEqual(calls, [True])548 self.assertEqual(calls, [True])
508549
509 # Doing it again to ensure that the deferred isn't called twice.550 # Doing it again to ensure that the deferred isn't called twice.
510 self.exchanger.handle_message(551 self.exchanger.handle_message(
511 {"type": b"registration", "info": b"unknown-account"})552 {"type": b"registration", "info": b"unknown-account"},
553 )
512554
513 self.assertEqual(calls, [True])555 self.assertEqual(calls, [True])
514556
@@ -534,13 +576,15 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
534 d.addErrback(add_call)576 d.addErrback(add_call)
535577
536 self.exchanger.handle_message(578 self.exchanger.handle_message(
537 {"type": b"registration", "info": b"max-pending-computers"})579 {"type": b"registration", "info": b"max-pending-computers"},
580 )
538581
539 self.assertEqual(calls, [True])582 self.assertEqual(calls, [True])
540583
541 # Doing it again to ensure that the deferred isn't called twice.584 # Doing it again to ensure that the deferred isn't called twice.
542 self.exchanger.handle_message(585 self.exchanger.handle_message(
543 {"type": b"registration", "info": b"max-pending-computers"})586 {"type": b"registration", "info": b"max-pending-computers"},
587 )
544588
545 self.assertEqual(calls, [True])589 self.assertEqual(calls, [True])
546590
@@ -575,9 +619,13 @@ class RegistrationHandlerTest(RegistrationHandlerTestBase):
575619
576class JujuRegistrationHandlerTest(RegistrationHandlerTestBase):620class JujuRegistrationHandlerTest(RegistrationHandlerTestBase):
577621
578 juju_contents = json.dumps({"environment-uuid": "DEAD-BEEF",622 juju_contents = json.dumps(
579 "machine-id": "1",623 {
580 "api-addresses": "10.0.3.1:17070"})624 "environment-uuid": "DEAD-BEEF",
625 "machine-id": "1",
626 "api-addresses": "10.0.3.1:17070",
627 },
628 )
581629
582 def test_juju_info_added_when_present(self):630 def test_juju_info_added_when_present(self):
583 """631 """
@@ -593,10 +641,13 @@ class JujuRegistrationHandlerTest(RegistrationHandlerTestBase):
593641
594 messages = self.mstore.get_pending_messages()642 messages = self.mstore.get_pending_messages()
595 self.assertEqual(643 self.assertEqual(
596 {"environment-uuid": "DEAD-BEEF",644 {
597 "machine-id": "1",645 "environment-uuid": "DEAD-BEEF",
598 "api-addresses": ["10.0.3.1:17070"]},646 "machine-id": "1",
599 messages[0]["juju-info"])647 "api-addresses": ["10.0.3.1:17070"],
648 },
649 messages[0]["juju-info"],
650 )
600651
601 def test_juju_info_skipped_with_old_server(self):652 def test_juju_info_skipped_with_old_server(self):
602 """653 """
diff --git a/landscape/client/broker/tests/test_server.py b/landscape/client/broker/tests/test_server.py
index 99fa65a..13461eb 100644
--- a/landscape/client/broker/tests/test_server.py
+++ b/landscape/client/broker/tests/test_server.py
@@ -1,23 +1,23 @@
1import random1import random
2from unittest.mock import Mock
23
3from configobj import ConfigObj4from configobj import ConfigObj
4from mock import Mock5from twisted.internet.defer import fail
5from twisted.internet.defer import succeed, fail6from twisted.internet.defer import succeed
67
7from landscape.client.manager.manager import FAILED8from landscape.client.broker.tests.helpers import BrokerServerHelper
8from landscape.client.tests.helpers import (9from landscape.client.broker.tests.helpers import RemoteClientHelper
9 LandscapeTest, DEFAULT_ACCEPTED_TYPES)
10from landscape.client.broker.tests.helpers import (
11 BrokerServerHelper, RemoteClientHelper)
12from landscape.client.broker.tests.test_ping import FakePageGetter10from landscape.client.broker.tests.test_ping import FakePageGetter
11from landscape.client.manager.manager import FAILED
12from landscape.client.tests.helpers import DEFAULT_ACCEPTED_TYPES
13from landscape.client.tests.helpers import LandscapeTest
1314
1415
15class FakeClient(object):16class FakeClient:
16 pass17 pass
1718
1819
19class FakeCreator(object):20class FakeCreator:
20
21 def __init__(self, reactor, config):21 def __init__(self, reactor, config):
22 pass22 pass
2323
@@ -105,7 +105,11 @@ class BrokerServerTest(LandscapeTest):
105 message = {"type": "test"}105 message = {"type": "test"}
106 self.mstore.set_accepted_types(["test"])106 self.mstore.set_accepted_types(["test"])
107 self.assertRaises(107 self.assertRaises(
108 RuntimeError, self.broker.send_message, message, None)108 RuntimeError,
109 self.broker.send_message,
110 message,
111 None,
112 )
109113
110 def test_send_message_with_old_release_upgrader(self):114 def test_send_message_with_old_release_upgrader(self):
111 """115 """
@@ -123,8 +127,11 @@ class BrokerServerTest(LandscapeTest):
123 If we receive a message from an old package-changer process that127 If we receive a message from an old package-changer process that
124 doesn't know about session IDs, we just let the message in.128 doesn't know about session IDs, we just let the message in.
125 """129 """
126 message = {"type": "change-packages-result", "operation-id": 99,130 message = {
127 "result-code": 123}131 "type": "change-packages-result",
132 "operation-id": 99,
133 "result-code": 123,
134 }
128 self.mstore.set_accepted_types(["change-packages-result"])135 self.mstore.set_accepted_types(["change-packages-result"])
129 self.broker.send_message(message, True)136 self.broker.send_message(message, True)
130 self.assertMessages(self.mstore.get_pending_messages(), [message])137 self.assertMessages(self.mstore.get_pending_messages(), [message])
@@ -138,14 +145,17 @@ class BrokerServerTest(LandscapeTest):
138 legacy_message = {145 legacy_message = {
139 b"type": b"change-packages-result",146 b"type": b"change-packages-result",
140 b"operation-id": 99,147 b"operation-id": 99,
141 b"result-code": 123}148 b"result-code": 123,
149 }
142 self.mstore.set_accepted_types(["change-packages-result"])150 self.mstore.set_accepted_types(["change-packages-result"])
143 self.broker.send_message(legacy_message, True)151 self.broker.send_message(legacy_message, True)
144 expected = [{152 expected = [
145 "type": "change-packages-result",153 {
146 "operation-id": 99,154 "type": "change-packages-result",
147 "result-code": 123155 "operation-id": 99,
148 }]156 "result-code": 123,
157 },
158 ]
149 self.assertMessages(self.mstore.get_pending_messages(), expected)159 self.assertMessages(self.mstore.get_pending_messages(), expected)
150 self.assertTrue(self.exchanger.is_urgent())160 self.assertTrue(self.exchanger.is_urgent())
151161
@@ -176,9 +186,11 @@ class BrokerServerTest(LandscapeTest):
176 self.assertEqual(len(self.broker.get_clients()), 1)186 self.assertEqual(len(self.broker.get_clients()), 1)
177 self.assertEqual(len(self.broker.get_connectors()), 1)187 self.assertEqual(len(self.broker.get_connectors()), 1)
178 self.assertTrue(188 self.assertTrue(
179 isinstance(self.broker.get_client("test"), FakeClient))189 isinstance(self.broker.get_client("test"), FakeClient),
190 )
180 self.assertTrue(191 self.assertTrue(
181 isinstance(self.broker.get_connector("test"), FakeCreator))192 isinstance(self.broker.get_connector("test"), FakeCreator),
193 )
182194
183 self.broker.connectors_registry = {"test": FakeCreator}195 self.broker.connectors_registry = {"test": FakeCreator}
184 result = self.broker.register_client("test")196 result = self.broker.register_client("test")
@@ -190,8 +202,10 @@ class BrokerServerTest(LandscapeTest):
190 of each registered client, and returns a deferred resulting in C{None}202 of each registered client, and returns a deferred resulting in C{None}
191 if all C{exit} calls were successful.203 if all C{exit} calls were successful.
192 """204 """
193 self.broker.connectors_registry = {"foo": FakeCreator,205 self.broker.connectors_registry = {
194 "bar": FakeCreator}206 "foo": FakeCreator,
207 "bar": FakeCreator,
208 }
195 self.broker.register_client("foo")209 self.broker.register_client("foo")
196 self.broker.register_client("bar")210 self.broker.register_client("bar")
197 for client in self.broker.get_clients():211 for client in self.broker.get_clients():
@@ -203,8 +217,10 @@ class BrokerServerTest(LandscapeTest):
203 The L{BrokerServer.stop_clients} method calls the C{exit} method of217 The L{BrokerServer.stop_clients} method calls the C{exit} method of
204 each registered client, and raises an exception if any calls fail.218 each registered client, and raises an exception if any calls fail.
205 """219 """
206 self.broker.connectors_registry = {"foo": FakeCreator,220 self.broker.connectors_registry = {
207 "bar": FakeCreator}221 "foo": FakeCreator,
222 "bar": FakeCreator,
223 }
208 self.broker.register_client("foo")224 self.broker.register_client("foo")
209 self.broker.register_client("bar")225 self.broker.register_client("bar")
210 [client1, client2] = self.broker.get_clients()226 [client1, client2] = self.broker.get_clients()
@@ -221,8 +237,12 @@ class BrokerServerTest(LandscapeTest):
221 config_obj["client"]["computer_title"] = "New Title"237 config_obj["client"]["computer_title"] = "New Title"
222 config_obj.write()238 config_obj.write()
223 result = self.broker.reload_configuration()239 result = self.broker.reload_configuration()
224 result.addCallback(lambda x: self.assertEqual(240 result.addCallback(
225 self.config.computer_title, "New Title"))241 lambda x: self.assertEqual(
242 self.config.computer_title,
243 "New Title",
244 ),
245 )
226 return result246 return result
227247
228 def test_reload_configuration_stops_clients(self):248 def test_reload_configuration_stops_clients(self):
@@ -230,8 +250,10 @@ class BrokerServerTest(LandscapeTest):
230 The L{BrokerServer.reload_configuration} method forces the config250 The L{BrokerServer.reload_configuration} method forces the config
231 file associated with the broker server to be reloaded.251 file associated with the broker server to be reloaded.
232 """252 """
233 self.broker.connectors_registry = {"foo": FakeCreator,253 self.broker.connectors_registry = {
234 "bar": FakeCreator}254 "foo": FakeCreator,
255 "bar": FakeCreator,
256 }
235 self.broker.register_client("foo")257 self.broker.register_client("foo")
236 self.broker.register_client("bar")258 self.broker.register_client("bar")
237 for client in self.broker.get_clients():259 for client in self.broker.get_clients():
@@ -245,8 +267,9 @@ class BrokerServerTest(LandscapeTest):
245 """267 """
246 registered = self.broker.register()268 registered = self.broker.register()
247 # This should callback the deferred.269 # This should callback the deferred.
248 self.exchanger.handle_message({"type": "set-id", "id": "abc",270 self.exchanger.handle_message(
249 "insecure-id": "def"})271 {"type": "set-id", "id": "abc", "insecure-id": "def"},
272 )
250 return self.assertSuccess(registered)273 return self.assertSuccess(registered)
251274
252 def test_get_accepted_types_empty(self):275 def test_get_accepted_types_empty(self):
@@ -263,8 +286,10 @@ class BrokerServerTest(LandscapeTest):
263 message types accepted by the Landscape server.286 message types accepted by the Landscape server.
264 """287 """
265 self.mstore.set_accepted_types(["foo", "bar"])288 self.mstore.set_accepted_types(["foo", "bar"])
266 self.assertEqual(sorted(self.broker.get_accepted_message_types()),289 self.assertEqual(
267 ["bar", "foo"])290 sorted(self.broker.get_accepted_message_types()),
291 ["bar", "foo"],
292 )
268293
269 def test_get_server_uuid_with_unset_uuid(self):294 def test_get_server_uuid_with_unset_uuid(self):
270 """295 """
@@ -288,8 +313,10 @@ class BrokerServerTest(LandscapeTest):
288 """313 """
289 self.broker.register_client_accepted_message_type("type1")314 self.broker.register_client_accepted_message_type("type1")
290 self.broker.register_client_accepted_message_type("type2")315 self.broker.register_client_accepted_message_type("type2")
291 self.assertEqual(self.exchanger.get_client_accepted_message_types(),316 self.assertEqual(
292 sorted(["type1", "type2"] + DEFAULT_ACCEPTED_TYPES))317 self.exchanger.get_client_accepted_message_types(),
318 sorted(["type1", "type2"] + DEFAULT_ACCEPTED_TYPES),
319 )
293320
294 def test_fire_event(self):321 def test_fire_event(self):
295 """322 """
@@ -304,8 +331,10 @@ class BrokerServerTest(LandscapeTest):
304 """331 """
305 The L{BrokerServer.exit} method stops all registered clients.332 The L{BrokerServer.exit} method stops all registered clients.
306 """333 """
307 self.broker.connectors_registry = {"foo": FakeCreator,334 self.broker.connectors_registry = {
308 "bar": FakeCreator}335 "foo": FakeCreator,
336 "bar": FakeCreator,
337 }
309 self.broker.register_client("foo")338 self.broker.register_client("foo")
310 self.broker.register_client("bar")339 self.broker.register_client("bar")
311 for client in self.broker.get_clients():340 for client in self.broker.get_clients():
@@ -430,8 +459,10 @@ class EventTest(LandscapeTest):
430 """459 """
431 callback = Mock(return_value="foo")460 callback = Mock(return_value="foo")
432 self.client_reactor.call_on("resynchronize", callback)461 self.client_reactor.call_on("resynchronize", callback)
433 return self.assertSuccess(self.broker.resynchronize(["foo"]),462 return self.assertSuccess(
434 [["foo"]])463 self.broker.resynchronize(["foo"]),
464 [["foo"]],
465 )
435466
436 def test_impending_exchange(self):467 def test_impending_exchange(self):
437 """468 """
@@ -448,7 +479,9 @@ class EventTest(LandscapeTest):
448 plugin.exchange.assert_called_once_with()479 plugin.exchange.assert_called_once_with()
449480
450 deferred = self.assertSuccess(481 deferred = self.assertSuccess(
451 self.broker.impending_exchange(), [[None]])482 self.broker.impending_exchange(),
483 [[None]],
484 )
452 deferred.addCallback(assert_called)485 deferred.addCallback(assert_called)
453 return deferred486 return deferred
454487
@@ -464,12 +497,15 @@ class EventTest(LandscapeTest):
464 self.remote.register_client = Mock()497 self.remote.register_client = Mock()
465498
466 def assert_called_made(ignored):499 def assert_called_made(ignored):
467 self.remote.register_client_accepted_message_type\500 self.remote.register_client_accepted_message_type.assert_called_once_with( # noqa: E501
468 .assert_called_once_with("type")501 "type",
502 )
469 self.remote.register_client.assert_called_once_with("client")503 self.remote.register_client.assert_called_once_with("client")
470504
471 deferred = self.assertSuccess(505 deferred = self.assertSuccess(
472 self.broker.broker_reconnect(), [[None]])506 self.broker.broker_reconnect(),
507 [[None]],
508 )
473 return deferred.addCallback(assert_called_made)509 return deferred.addCallback(assert_called_made)
474510
475 registered = self.client.register_message("type", lambda x: None)511 registered = self.client.register_message("type", lambda x: None)
@@ -488,7 +524,9 @@ class EventTest(LandscapeTest):
488524
489 self.client_reactor.call_on("server-uuid-changed", callback)525 self.client_reactor.call_on("server-uuid-changed", callback)
490 deferred = self.assertSuccess(526 deferred = self.assertSuccess(
491 self.broker.server_uuid_changed(None, "abc"), [[return_value]])527 self.broker.server_uuid_changed(None, "abc"),
528 [[return_value]],
529 )
492 return deferred.addCallback(assert_called)530 return deferred.addCallback(assert_called)
493531
494 def test_message_type_acceptance_changed(self):532 def test_message_type_acceptance_changed(self):
@@ -499,7 +537,9 @@ class EventTest(LandscapeTest):
499 return_value = random.randint(1, 100)537 return_value = random.randint(1, 100)
500 callback = Mock(return_value=return_value)538 callback = Mock(return_value=return_value)
501 self.client_reactor.call_on(539 self.client_reactor.call_on(
502 ("message-type-acceptance-changed", "type"), callback)540 ("message-type-acceptance-changed", "type"),
541 callback,
542 )
503 result = self.broker.message_type_acceptance_changed("type", True)543 result = self.broker.message_type_acceptance_changed("type", True)
504 return self.assertSuccess(result, [[return_value]])544 return self.assertSuccess(result, [[return_value]])
505545
@@ -512,7 +552,9 @@ class EventTest(LandscapeTest):
512 callback = Mock(return_value=return_value)552 callback = Mock(return_value=return_value)
513 self.client_reactor.call_on("package-data-changed", callback)553 self.client_reactor.call_on("package-data-changed", callback)
514 return self.assertSuccess(554 return self.assertSuccess(
515 self.broker.package_data_changed(), [[return_value]])555 self.broker.package_data_changed(),
556 [[return_value]],
557 )
516558
517559
518class HandlersTest(LandscapeTest):560class HandlersTest(LandscapeTest):
@@ -520,7 +562,7 @@ class HandlersTest(LandscapeTest):
520 helpers = [BrokerServerHelper]562 helpers = [BrokerServerHelper]
521563
522 def setUp(self):564 def setUp(self):
523 super(HandlersTest, self).setUp()565 super().setUp()
524 self.broker.connectors_registry = {"test": FakeCreator}566 self.broker.connectors_registry = {"test": FakeCreator}
525 self.broker.register_client("test")567 self.broker.register_client("test")
526 self.client = self.broker.get_client("test")568 self.client = self.broker.get_client("test")
@@ -549,18 +591,25 @@ class HandlersTest(LandscapeTest):
549 result = self.reactor.fire("message", message)591 result = self.reactor.fire("message", message)
550 result = [i for i in result if i is not None][0]592 result = [i for i in result if i is not None][0]
551593
552 class StartsWith(object):594 class StartsWith:
553
554 def __eq__(self, other):595 def __eq__(self, other):
555 return other.startswith(596 return other.startswith(
556 "Landscape client failed to handle this request (foobar)")597 "Landscape client failed to handle this request (foobar)",
598 )
557599
558 def broadcasted(ignored):600 def broadcasted(ignored):
559 self.client.message.assert_called_once_with(message)601 self.client.message.assert_called_once_with(message)
560 self.assertMessages(602 self.assertMessages(
561 self.mstore.get_pending_messages(),603 self.mstore.get_pending_messages(),
562 [{"type": "operation-result", "status": FAILED,604 [
563 "result-text": StartsWith(), "operation-id": 4}])605 {
606 "type": "operation-result",
607 "status": FAILED,
608 "result-text": StartsWith(),
609 "operation-id": 4,
610 },
611 ],
612 )
564613
565 result.addCallback(broadcasted)614 result.addCallback(broadcasted)
566 return result615 return result
@@ -582,7 +631,10 @@ class HandlersTest(LandscapeTest):
582 self.client.fire_event = Mock(return_value=succeed(None))631 self.client.fire_event = Mock(return_value=succeed(None))
583 self.reactor.fire("message-type-acceptance-changed", "test", True)632 self.reactor.fire("message-type-acceptance-changed", "test", True)
584 self.client.fire_event.assert_called_once_with(633 self.client.fire_event.assert_called_once_with(
585 "message-type-acceptance-changed", "test", True)634 "message-type-acceptance-changed",
635 "test",
636 True,
637 )
586638
587 def test_server_uuid_changed(self):639 def test_server_uuid_changed(self):
588 """640 """
@@ -592,7 +644,10 @@ class HandlersTest(LandscapeTest):
592 self.client.fire_event = Mock(return_value=succeed(None))644 self.client.fire_event = Mock(return_value=succeed(None))
593 self.reactor.fire("server-uuid-changed", None, 123)645 self.reactor.fire("server-uuid-changed", None, 123)
594 self.client.fire_event.assert_called_once_with(646 self.client.fire_event.assert_called_once_with(
595 "server-uuid-changed", None, 123)647 "server-uuid-changed",
648 None,
649 123,
650 )
596651
597 def test_package_data_changed(self):652 def test_package_data_changed(self):
598 """653 """
diff --git a/landscape/client/broker/tests/test_service.py b/landscape/client/broker/tests/test_service.py
index 580f64e..e99f9ac 100644
--- a/landscape/client/broker/tests/test_service.py
+++ b/landscape/client/broker/tests/test_service.py
@@ -1,12 +1,11 @@
1import os1import os
2from unittest.mock import Mock
23
3from mock import Mock4from landscape.client.broker.amp import RemoteBrokerConnector
4
5from landscape.client.tests.helpers import LandscapeTest
6from landscape.client.broker.tests.helpers import BrokerConfigurationHelper
7from landscape.client.broker.service import BrokerService5from landscape.client.broker.service import BrokerService
6from landscape.client.broker.tests.helpers import BrokerConfigurationHelper
8from landscape.client.broker.transport import HTTPTransport7from landscape.client.broker.transport import HTTPTransport
9from landscape.client.broker.amp import RemoteBrokerConnector8from landscape.client.tests.helpers import LandscapeTest
10from landscape.lib.testing import FakeReactor9from landscape.lib.testing import FakeReactor
1110
1211
@@ -15,7 +14,7 @@ class BrokerServiceTest(LandscapeTest):
15 helpers = [BrokerConfigurationHelper]14 helpers = [BrokerConfigurationHelper]
1615
17 def setUp(self):16 def setUp(self):
18 super(BrokerServiceTest, self).setUp()17 super().setUp()
1918
20 class FakeBrokerService(BrokerService):19 class FakeBrokerService(BrokerService):
21 reactor_factory = FakeReactor20 reactor_factory = FakeReactor
@@ -28,7 +27,8 @@ class BrokerServiceTest(LandscapeTest):
28 """27 """
29 self.assertEqual(28 self.assertEqual(
30 self.service.persist.filename,29 self.service.persist.filename,
31 os.path.join(self.config.data_path, "broker.bpickle"))30 os.path.join(self.config.data_path, "broker.bpickle"),
31 )
3232
33 def test_transport(self):33 def test_transport(self):
34 """34 """
diff --git a/landscape/client/broker/tests/test_store.py b/landscape/client/broker/tests/test_store.py
index 208c75c..b61edf3 100644
--- a/landscape/client/broker/tests/test_store.py
+++ b/landscape/client/broker/tests/test_store.py
@@ -1,22 +1,22 @@
1import os1import os
2import mock2from unittest import mock
3
43
5from twisted.python.compat import intToBytes4from twisted.python.compat import intToBytes
65
6from landscape.client.broker.store import MessageStore
7from landscape.client.tests.helpers import LandscapeTest
7from landscape.lib.bpickle import dumps8from landscape.lib.bpickle import dumps
8from landscape.lib.persist import Persist9from landscape.lib.persist import Persist
9from landscape.lib.schema import InvalidError, Int, Bytes, Unicode10from landscape.lib.schema import Bytes
11from landscape.lib.schema import Int
12from landscape.lib.schema import InvalidError
13from landscape.lib.schema import Unicode
10from landscape.message_schemas.message import Message14from landscape.message_schemas.message import Message
11from landscape.client.broker.store import MessageStore
12
13from landscape.client.tests.helpers import LandscapeTest
1415
1516
16class MessageStoreTest(LandscapeTest):17class MessageStoreTest(LandscapeTest):
17
18 def setUp(self):18 def setUp(self):
19 super(MessageStoreTest, self).setUp()19 super().setUp()
20 self.temp_dir = self.makeDir()20 self.temp_dir = self.makeDir()
21 self.persist_filename = self.makeFile()21 self.persist_filename = self.makeFile()
22 self.store = self.create_store()22 self.store = self.create_store()
@@ -137,13 +137,87 @@ class MessageStoreTest(LandscapeTest):
137 self.assertEqual(self.store.get_pending_offset(), 0)137 self.assertEqual(self.store.get_pending_offset(), 0)
138 self.assertEqual(self.store.get_pending_messages(), [])138 self.assertEqual(self.store.get_pending_messages(), [])
139139
140 def test_messages_over_limit(self):
141 """
142 Create six messages, two per directory. Since there is a limit of
143 one directory then only the last 2 messages should be in the queue
144 """
145
146 self.store._directory_size = 2
147 self.store._max_dirs = 1
148 for num in range(6): # 0,1 2,3 4,5
149 message = {"type": "data", "data": f"{num}".encode()}
150 self.store.add(message)
151 messages = self.store.get_pending_messages(200)
152 self.assertMessages(
153 messages,
154 [{"type": "data", "data": b"4"}, {"type": "data", "data": b"5"}],
155 )
156
157 def test_messages_under_limit(self):
158 """
159 Create six messages, all of which should be in the queue, since 3
160 directories are active
161 """
162
163 self.store._directory_size = 2
164 self.store._max_dirs = 3
165 messages_sent = []
166 for num in range(6): # 0,1 2,3 4,5
167 message = {"type": "data", "data": f"{num}".encode()}
168 messages_sent.append(message)
169 self.store.add(message)
170 messages = self.store.get_pending_messages(200)
171 self.assertMessages(messages, messages_sent)
172
173 def test_messages_over_mb(self):
174 """
175 Create three messages with the second one being very large. The
176 max size should get triggered, so all should be cleared except for the
177 last one.
178 """
179
180 self.store._directory_size = 2
181 self.store._max_size_mb = 0.01
182 self.store.add({"type": "data", "data": b"a"})
183 self.store.add({"type": "data", "data": b"b" * 15000})
184 self.store.add({"type": "data", "data": b"c"})
185 messages = self.store.get_pending_messages(200)
186 self.assertMessages(
187 messages,
188 [{"type": "data", "data": b"c"}],
189 )
190
191 @mock.patch("shutil.rmtree")
192 def test_exception_on_message_limit(self, rmtree_mock):
193 """
194 If an exception occurs while deleting it shouldn't affect the next
195 message sent
196 """
197 rmtree_mock.side_effect = IOError("Error!")
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches