Merge lp:~bjornt/landscape-client-charm/update-source into lp:landscape-client-charm

Proposed by Björn Tillenius
Status: Merged
Approved by: Björn Tillenius
Approved revision: 71
Merged at revision: 64
Proposed branch: lp:~bjornt/landscape-client-charm/update-source
Merge into: lp:landscape-client-charm
Diff against target: 426 lines (+209/-37)
4 files modified
hooks/hooks.py (+13/-6)
hooks/install.py (+18/-19)
hooks/stubs.py (+81/-0)
hooks/test_hooks.py (+97/-12)
To merge this branch: bzr merge lp:~bjornt/landscape-client-charm/update-source
Reviewer Review Type Date Requested Status
🤖 Landscape Builder test results Approve
Free Ekanayaka (community) Approve
Alberto Donato (community) Approve
Review via email: mp+318891@code.launchpad.net

Commit message

Add the new origin/source and upgrade landscape-client if the origin is
changed after the deploy.

Before origin was read only during the install hook, and we ignored any
changes to it after that.

Description of the change

Add the new origin/source and upgrade landscape-client if the origin is
changed after the deploy.

Before origin was read only during the install hook, and we ignored any
changes to it after that.

Most of the changes made are related to testing. I had to copy some
stubs from another charm and pass then along the hooks. It's not ideal,
but it's hard to make it better at this point.

I also removed the use of JujuBroker from the hooks I modified, since
that was written before charmhelpers existed.

Testing instructions:

    juju deploy ubuntu
    juju deploy --series xenial ./
    juju add-relation ubuntu landscape-client

    Wait for ubuntu and landscape-client to be deployed
    (landscape-client will enter a blocked state)

    Optionally: juju config landscape-client account-name=$youraccount
    juju config landscape-client origin=ppa:landscape/trunk

    Confirm that the 17.02 version of landscape-client has been
    installed on the unit.

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 67
Branch: lp:~bjornt/landscape-client-charm/update-source
Jenkins: https://ci.lscape.net/job/latch-test-trusty/742/

review: Approve (test results)
Revision history for this message
Alberto Donato (ack) wrote :

Nice, +1

A couple of nits inline.

review: Approve
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

+1

review: Approve
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Attempt to merge into lp:landscape-client-charm failed due to conflicts:

text conflict in hooks/install.py

70. By Björn Tillenius

merge trunk, resolve conflicts.

71. By Björn Tillenius

Lint.

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 71
Branch: lp:~bjornt/landscape-client-charm/update-source
Jenkins: https://ci.lscape.net/job/latch-test-trusty/848/

review: Approve (test results)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'hooks/hooks.py'
2--- hooks/hooks.py 2016-05-25 10:10:43 +0000
3+++ hooks/hooks.py 2017-03-14 09:50:28 +0000
4@@ -10,10 +10,14 @@
5 from shutil import rmtree
6 from subprocess import CalledProcessError
7
8+from charmhelpers import fetch
9+from charmhelpers.core import hookenv
10+
11 from common import (
12 write_json_file, JujuBroker, LandscapeBroker, chown)
13 from ceph import (
14 get_ceph_client_path, write_ceph_client_keyring, write_ceph_client_config)
15+from install import install_landscape_client
16
17
18 CERT_FILE = "/etc/ssl/certs/landscape_server_ca.crt"
19@@ -26,29 +30,32 @@
20
21 @hooks.hook("registration-relation-joined", "registration-relation-changed")
22 def registration_relation(juju_broker=JUJU_BROKER,
23+ hookenv=hookenv, fetch=fetch,
24 landscape_broker=LANDSCAPE_BROKER):
25 data = juju_broker.get_relation_config()
26 if "ping-url" in data:
27- config_changed(relation_data=data, juju_broker=juju_broker,
28+ config_changed(relation_data=data, hookenv=hookenv, fetch=fetch,
29 landscape_broker=landscape_broker)
30
31
32 @hooks.hook("config-changed")
33-def config_changed(relation_data=None, juju_broker=JUJU_BROKER,
34+def config_changed(relation_data=None, hookenv=hookenv, fetch=fetch,
35 landscape_broker=LANDSCAPE_BROKER):
36 """
37 @param relation_data: data from the context of a relation, it should
38 match the data in the config
39 """
40- juju_broker.log("In config-changed for %s" % landscape_broker.local_unit)
41+ hookenv.log("In config-changed for %s" % landscape_broker.local_unit)
42 if relation_data is None:
43 relation_data = {}
44- service_config = juju_broker.get_service_config()
45+ service_config = hookenv.config()
46+ if service_config.changed("origin"):
47+ install_landscape_client(service_config["origin"], fetch=fetch)
48 # Only update client config with service configs that have been set
49- service_config = dict(
50+ set_config = dict(
51 (key, value.strip()) for key, value in service_config.iteritems()
52 if value is not None)
53- relation_data.update(service_config)
54+ relation_data.update(set_config)
55 if relation_data.get("ssl-public-key", "").startswith("base64:"):
56 _write_certificate(relation_data["ssl-public-key"][7:], CERT_FILE)
57 relation_data["ssl-public-key"] = CERT_FILE
58
59=== modified file 'hooks/install.py'
60--- hooks/install.py 2017-03-03 23:03:48 +0000
61+++ hooks/install.py 2017-03-14 09:50:28 +0000
62@@ -13,30 +13,29 @@
63
64 from charmhelpers.core.hookenv import (
65 Hooks, UnregisteredHookError, log)
66-from charmhelpers.fetch import (
67- apt_install, add_source, apt_update)
68-# TODO fix charhelpers to expose this retry command logic somewhere nice.
69-from charmhelpers.fetch.ubuntu import (_run_apt_command)
70+from charmhelpers import fetch
71 from charmhelpers.core.hookenv import config
72
73
74 hooks = Hooks()
75
76
77+def install_landscape_client(origin, fetch=fetch):
78+ """Update the origin and install/upgrade landscape-client."""
79+ if origin not in ["distro", None]:
80+ add_apt_source(origin, fetch=fetch)
81+
82+ fetch.apt_update()
83+ fetch.apt_install(["landscape-client"])
84+
85+
86 @hooks.hook("install")
87 def install():
88 charm_config = config()
89- apt_install(["apt-transport-https", "wget"])
90-
91- origin = charm_config.get("origin")
92- if origin == "distro":
93- origin = None
94-
95- if origin is not None:
96- add_apt_source(origin)
97-
98- apt_update()
99- apt_install(["landscape-client"])
100+ fetch.apt_install(["apt-transport-https", "wget"])
101+
102+ install_landscape_client(charm_config.get("origin"))
103+
104 data_path = charm_config.get("data-path")
105 if not data_path:
106 data_path = "/var/lib/landscape/client"
107@@ -51,7 +50,7 @@
108 def build_from_launchpad(url):
109 """The charm will install the code from the passed lp branch.
110 """
111- apt_install(["devscripts", "bzr", "pbuilder"], fatal=True)
112+ fetch.apt_install(["devscripts", "bzr", "pbuilder"], fatal=True)
113 subprocess.check_call(["rm", "-rf", "landscape-client-source"])
114 subprocess.check_call(["bzr", "branch", url, "landscape-client-source"])
115 os.chdir("landscape-client-source")
116@@ -64,11 +63,11 @@
117 glob("../landscape-common_*.deb")[0]])
118 # The _run_with_retries will ensure the command is retried in case we can't
119 # acquire the lock for some reason.
120- _run_apt_command(["apt-get", "-f", "install"], fatal=True)
121+ fetch._run_apt_command(["apt-get", "-f", "install"], fatal=True)
122 os.chdir("..")
123
124
125-def add_apt_source(url):
126+def add_apt_source(url, fetch=fetch):
127 """Add an apt source entry, with passed in key.
128
129 This is a thin wrapper over the charmhelper "add_source" method to allow
130@@ -87,7 +86,7 @@
131 if key == "":
132 log("Archive key for '%s' is empty.", level="WARNING")
133
134- return add_source(url, key)
135+ return fetch.add_source(url, key)
136
137 if __name__ == '__main__':
138 try:
139
140=== added file 'hooks/stubs.py'
141--- hooks/stubs.py 1970-01-01 00:00:00 +0000
142+++ hooks/stubs.py 2017-03-14 09:50:28 +0000
143@@ -0,0 +1,81 @@
144+import os.path
145+from collections import namedtuple
146+
147+from charmhelpers.core.hookenv import (
148+ Config,
149+ CRITICAL,
150+ ERROR,
151+ WARNING,
152+ INFO,
153+ DEBUG,
154+)
155+
156+LEVELS = (CRITICAL, ERROR, WARNING, INFO, DEBUG)
157+
158+
159+class HookenvStub:
160+ """Testable stub for charmhelpers.core.hookenv."""
161+
162+ def __init__(self, charm_dir):
163+ self.messages = []
164+ self.port = None
165+ self._config = Config()
166+ self._charm_dir = charm_dir
167+ self.action_messages = []
168+ self.action_details = []
169+ self.statuses = []
170+
171+ def config(self):
172+ return self._config
173+
174+ def charm_dir(self):
175+ return self._charm_dir
176+
177+ def log(self, message, level=None):
178+ if level is not None:
179+ assert level in LEVELS
180+ self.messages.append((message, level))
181+
182+ def open_port(self, port):
183+ self.port = port
184+
185+ def action_fail(self, message):
186+ self.action_messages.append(message)
187+
188+ def action_set(self, details):
189+ self.action_details.append(details)
190+
191+ def status_set(self, status, message):
192+ self.statuses.append((status, message.decode("utf-8")))
193+
194+ def local_unit(self):
195+ return os.path.basename(self._charm_dir) + "/0"
196+
197+
198+Update = namedtuple("Update", ["fatal"])
199+Install = namedtuple("Install", ["packages", "options", "fatal"])
200+
201+
202+class FetchStub:
203+ """Testable stub for charmhelpers.fetch."""
204+
205+ def __init__(self, needs_update=False):
206+ self.needs_update = needs_update
207+ self.updates = []
208+ self.installs = []
209+ self.package_status = {}
210+ self.sources = []
211+
212+ def apt_update(self, fatal=False):
213+ self.needs_update = False
214+ self.updates.append(Update(fatal))
215+
216+ def apt_install(self, packages, options=None, fatal=False):
217+ if self.needs_update:
218+ raise AssertionError("APT cache wasn't updated.")
219+ for package in packages:
220+ self.package_status[package] = "install"
221+ self.installs.append(Install(packages, options, fatal))
222+
223+ def add_source(self, url, key=None):
224+ self.sources.append((url, key))
225
226=== modified file 'hooks/test_hooks.py'
227--- hooks/test_hooks.py 2016-05-25 10:37:20 +0000
228+++ hooks/test_hooks.py 2017-03-14 09:50:28 +0000
229@@ -10,6 +10,7 @@
230 from landscape.lib.fs import read_file
231 import hooks
232 from common import LandscapeBroker, JujuBroker, write_json_file
233+from stubs import FetchStub, HookenvStub
234
235
236 class FakeJujuBroker(JujuBroker):
237@@ -66,6 +67,10 @@
238 self.landscape_broker.config.write()
239 self.workload_state = None
240 self.workload_message = None
241+ self.charm_dir = self.mkdtemp()
242+ os.environ["CHARM_DIR"] = self.charm_dir
243+ self.hookenv = HookenvStub(self.charm_dir)
244+ self.fetch = FetchStub(needs_update=True)
245
246 def status_set(workload_state, message):
247 self.workload_state = workload_state
248@@ -82,6 +87,12 @@
249 self.addCleanup(rmtree, directory)
250 return directory
251
252+ def config(self):
253+ """Return the base application config."""
254+ config = self.hookenv.config()
255+ config.update({"origin": "distro"})
256+ return config
257+
258
259 class MigrateOldJujuInfoTest(HookTestCase):
260
261@@ -427,12 +438,13 @@
262 A base64 encoded SSL cert in the configuration is decoded.
263 """
264 self.touch_juju_info_file()
265- self.juju_broker.commands["config-get"] = {
266+ config = self.config()
267+ config.update({
268 "ssl-public-key": "base64:%s" % "foo".encode("base64"),
269 "account-name": "standalone",
270- "ping-url": "http://127.0.0.1"}
271+ "ping-url": "http://127.0.0.1"})
272
273- hooks.config_changed(juju_broker=self.juju_broker,
274+ hooks.config_changed(hookenv=self.hookenv, fetch=self.fetch,
275 landscape_broker=self.landscape_broker)
276 self.assertEqual(
277 hooks.CERT_FILE, self.landscape_broker.config.ssl_public_key)
278@@ -447,12 +459,13 @@
279 If account-name is not set, the unit transitions to the blocked state.
280 """
281 self.touch_juju_info_file()
282- self.juju_broker.commands["config-get"] = {
283+ config = self.config()
284+ config.update({
285 "ssl-public-key": "base64:%s" % "foo".encode("base64"),
286 "account-name": "",
287- "ping-url": "http://127.0.0.1"}
288+ "ping-url": "http://127.0.0.1"})
289
290- hooks.config_changed(juju_broker=self.juju_broker,
291+ hooks.config_changed(hookenv=self.hookenv, fetch=self.fetch,
292 landscape_broker=self.landscape_broker)
293 self.assertEqual("blocked", self.workload_state)
294 self.assertEqual(
295@@ -464,12 +477,13 @@
296 If the juju-info file hasn't been written yet, the unit transitions to
297 the maintenance status.
298 """
299- self.juju_broker.commands["config-get"] = {
300+ config = self.config()
301+ config.update({
302 "ssl-public-key": "base64:%s" % "foo".encode("base64"),
303 "account-name": "standalone",
304- "ping-url": "http://127.0.0.1"}
305+ "ping-url": "http://127.0.0.1"})
306
307- hooks.config_changed(juju_broker=self.juju_broker,
308+ hooks.config_changed(hookenv=self.hookenv, fetch=self.fetch,
309 landscape_broker=self.landscape_broker)
310 self.assertEqual("maintenance", self.workload_state)
311 self.assertEqual(
312@@ -478,12 +492,13 @@
313
314 def test_whitespace_removed_from_config_settings(self):
315 """Leading and trailing whitespace is removed from config settings"""
316- self.juju_broker.commands["config-get"] = {
317+ config = self.config()
318+ config.update({
319 "ssl-public-key": " base64:%s" % "foo".encode("base64"),
320 "account-name": " standalone\t",
321 "registration-key": " mykey\n",
322- "ping-url": " \thttp://127.0.0.1\n "}
323- hooks.config_changed(juju_broker=self.juju_broker,
324+ "ping-url": " \thttp://127.0.0.1\n "})
325+ hooks.config_changed(hookenv=self.hookenv, fetch=self.fetch,
326 landscape_broker=self.landscape_broker)
327 self.assertEqual(
328 hooks.CERT_FILE, self.landscape_broker.config.ssl_public_key)
329@@ -496,6 +511,73 @@
330 self.assertEqual(
331 "http://127.0.0.1", self.landscape_broker.config.ping_url)
332
333+ def test_update_origin(self):
334+ """"
335+ If origin is set, the origin will be added as an apt source and
336+ landscape-client will be installed.
337+ """
338+ config = self.config()
339+ config.update({"origin": "ppa:landscape-client/stable"})
340+ hooks.config_changed(hookenv=self.hookenv, fetch=self.fetch,
341+ landscape_broker=self.landscape_broker)
342+ self.assertEqual(
343+ [("ppa:landscape-client/stable", None)], self.fetch.sources)
344+ [install] = self.fetch.installs
345+ self.assertEqual(["landscape-client"], install.packages)
346+
347+ def test_update_origin_twice(self):
348+ """"
349+ If origin hasn't been changed since the last time the
350+ config-changed hook ran, the origin won't be added again, and
351+ landscape-client won't be installed again.
352+ """
353+ config = self.config()
354+ config.update({"origin": "ppa:landscape-client/stable"})
355+ hooks.config_changed(hookenv=self.hookenv, fetch=self.fetch,
356+ landscape_broker=self.landscape_broker)
357+ # Saving and loading the config is taken care of by
358+ # Hooks.execute(), which we can't use in the test.
359+ config.save()
360+ config.load_previous()
361+ hooks.config_changed(hookenv=self.hookenv, fetch=self.fetch,
362+ landscape_broker=self.landscape_broker)
363+ # The single origin and the landscape-client install were added
364+ # by the first config-changed hook execution.
365+ self.assertEqual(
366+ [("ppa:landscape-client/stable", None)], self.fetch.sources)
367+ self.assertEqual(1, len(self.fetch.installs))
368+ [install] = self.fetch.installs
369+ self.assertEqual(["landscape-client"], install.packages)
370+
371+ def test_update_origin_with_key(self):
372+ """
373+ If the origin contains a key, the key will be added together
374+ with the apt source.
375+ """
376+ config = self.config()
377+ config.update({"origin": "ppa:landscape-client/stable|key-id"})
378+ hooks.config_changed(hookenv=self.hookenv, fetch=self.fetch,
379+ landscape_broker=self.landscape_broker)
380+ self.assertEqual(
381+ [("ppa:landscape-client/stable", "key-id")], self.fetch.sources)
382+
383+ def test_update_origin_with_account_name(self):
384+ """
385+ If the account-name is set and the client already registered,
386+ updating the origin will result in the apt source being added
387+ and landscape-client installed/upgraded.
388+ """
389+ config = self.config()
390+ config.update({
391+ "account-name": "example",
392+ "origin": "ppa:landscape-client/stable"})
393+ hooks.config_changed(hookenv=self.hookenv, fetch=self.fetch,
394+ landscape_broker=self.landscape_broker)
395+ self.assertEqual(
396+ [("ppa:landscape-client/stable", None)], self.fetch.sources)
397+ [install] = self.fetch.installs
398+ self.assertEqual(["landscape-client"], install.packages)
399+
400
401 class RegistrationRelationJoinedTest(HookTestCase):
402 """Test for the registration-relation-joined hook."""
403@@ -508,6 +590,7 @@
404 self.addCleanup(setattr, hooks, "CERT_FILE", hooks.CERT_FILE)
405 tmp_dir = self.mkdtemp()
406 hooks.CERT_FILE = "%s/cert" % tmp_dir
407+ self.config()
408
409 def test_ssl_cert_decoded(self):
410 """
411@@ -518,6 +601,7 @@
412 "account-name": "standalone",
413 "ping-url": "http://127.0.0.1/ping"}
414 hooks.registration_relation(juju_broker=self.juju_broker,
415+ hookenv=self.hookenv, fetch=self.fetch,
416 landscape_broker=self.landscape_broker)
417 self.assertEqual(
418 hooks.CERT_FILE, self.landscape_broker.config.ssl_public_key)
419@@ -533,6 +617,7 @@
420 "account-name": "standalone",
421 "ping-url": "http://127.0.0.1/ping"}
422 hooks.registration_relation(juju_broker=self.juju_broker,
423+ hookenv=self.hookenv, fetch=self.fetch,
424 landscape_broker=self.landscape_broker)
425 self.assertEqual("/etc/ssl/my-cert.pem",
426 self.landscape_broker.config.ssl_public_key)

Subscribers

People subscribed via source and target branches

to all changes: