Merge lp:~free.ekanayaka/landscape-charm/install-from-deb into lp:~landscape/landscape-charm/trunk

Proposed by Free Ekanayaka
Status: Merged
Approved by: Free Ekanayaka
Approved revision: 242
Merged at revision: 231
Proposed branch: lp:~free.ekanayaka/landscape-charm/install-from-deb
Merge into: lp:~landscape/landscape-charm/trunk
Diff against target: 508 lines (+353/-50)
9 files modified
hooks/config-changed (+9/-0)
hooks/lib/apt.py (+126/-19)
hooks/lib/config.py (+23/-0)
hooks/lib/install.py (+24/-0)
hooks/lib/tests/helpers.py (+1/-1)
hooks/lib/tests/stubs.py (+7/-3)
hooks/lib/tests/test_apt.py (+115/-27)
hooks/lib/tests/test_config.py (+25/-0)
hooks/lib/tests/test_install.py (+23/-0)
To merge this branch: bzr merge lp:~free.ekanayaka/landscape-charm/install-from-deb
Reviewer Review Type Date Requested Status
Adam Collard (community) Approve
Björn Tillenius (community) Approve
🤖 Landscape Builder test results Approve
Review via email: mp+252072@code.launchpad.net

Commit message

This branch adds support for installing landscape-server packages built from a local tarball.

Description of the change

This branch adds support for installing landscape-server packages built from a local tarball.

To test it:

bzr branch lp:~free.ekanayaka/landscape-charm/install-from-deb
mkdir trusty
cd trusty
ln -s ../install-from-deb landscape-server
cd ..
# Let's build a tarball from a branch that hasn't landed yet
bzr branch lp:~free.ekanayaka/landscape/pingserver-head
cd pingserver-head
make build
cd standalone
make standalone-package-source
mv landscape-server_*.tar.gz ../../install-from-deb
cd ../..
# Copy this https://pastebin.canonical.com/127051/ to deployer.yaml
juju-deployer -vdW -w 180 -c deployer.yaml landscape

Then notice that the deployed package was built from the tarball, and
the pingserver actually answers HEAD requests.

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

Command: make ci-test
Result: Success
Revno: 240
Branch: lp:~free.ekanayaka/landscape-charm/install-from-deb
Jenkins: https://ci.lscape.net/job/latch-test/148/

review: Approve (test results)
Revision history for this message
Adam Collard (adam-collard) :
review: Needs Fixing
241. By Free Ekanayaka

Use hashlib instead of os.system

242. By Free Ekanayaka

Address review comments

Revision history for this message
Free Ekanayaka (free.ekanayaka) :
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: make ci-test
Result: Success
Revno: 242
Branch: lp:~free.ekanayaka/landscape-charm/install-from-deb
Jenkins: https://ci.lscape.net/job/latch-test/172/

review: Approve (test results)
Revision history for this message
Björn Tillenius (bjornt) wrote :

+1 with Adam's comments addressed. Ideally I would have like some more functional tests, since checking subprocess call parameters is really fragile. But I appreciate that issuing real commands isn't trivial.

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

> +1 with Adam's comments addressed. Ideally I would have like some more
> functional tests, since checking subprocess call parameters is really fragile.
> But I appreciate that issuing real commands isn't trivial.

Right. Note that this code is only meaningful for development, is not production code.

Revision history for this message
Adam Collard (adam-collard) wrote :

+1 thanks

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'hooks/config-changed'
2--- hooks/config-changed 1970-01-01 00:00:00 +0000
3+++ hooks/config-changed 2015-03-09 11:56:41 +0000
4@@ -0,0 +1,9 @@
5+#!/usr/bin/python
6+import sys
7+
8+from lib.config import ConfigHook
9+
10+
11+if __name__ == "__main__":
12+ hook = ConfigHook()
13+ sys.exit(hook())
14
15=== renamed file 'hooks/lib/install.py' => 'hooks/lib/apt.py'
16--- hooks/lib/install.py 2015-01-28 11:53:18 +0000
17+++ hooks/lib/apt.py 2015-03-09 11:56:41 +0000
18@@ -1,31 +1,138 @@
19+import glob
20+import hashlib
21+import os
22+import shutil
23+import subprocess
24+
25 from charmhelpers import fetch
26+from charmhelpers.core import hookenv
27
28-from lib.hook import Hook, HookError
29+from lib.hook import HookError
30
31 PACKAGES = ("landscape-server",)
32-
33-
34-class InstallHook(Hook):
35- """Execute install hook logic."""
36-
37- def __init__(self, fetch=fetch, **kwargs):
38- super(InstallHook, self).__init__(**kwargs)
39+PACKAGES_DEV = ("dpkg-dev", "pbuilder")
40+TARBALL = "landscape-server_*.tar.gz"
41+
42+# XXX Default options taken from charmhelpers, there's no way to just
43+# extend them.
44+DEFAULT_INSTALL_OPTIONS = ("--option=Dpkg::Options::=--force-confold",)
45+
46+# Shell commands to build the debs and publish them in a local repository
47+BUILD_LOCAL_ARCHIVE = """
48+dch -v 9999:$(dpkg-parsechangelog|grep ^Version:|cut -d ' ' -f 2) \
49+ development --distribution $(lsb_release -cs) &&
50+/usr/lib/pbuilder/pbuilder-satisfydepends &&
51+dpkg-buildpackage -us -uc &&
52+mv ../*.deb . &&
53+dpkg-scanpackages -m . /dev/null > Packages &&
54+cat Packages | bzip2 -9 > Packages.bz2 &&
55+cat Packages | gzip -9 > Packages.gz &&
56+dpkg-scansources . > Sources &&
57+cat Sources | bzip2 -9 > Sources.bz2 &&
58+cat Sources | gzip -9 > Sources.gz &&
59+apt-ftparchive release . > Release
60+"""
61+
62+
63+class Apt(object):
64+ """Perform APT-related tasks as setting sources and installing packages.
65+
66+ This is a thin facade around C{charmhelpers.fetch}, offering some
67+ additional features, like building Landscape packages from a local
68+ tarball.
69+ """
70+
71+ def __init__(self, hookenv=hookenv, fetch=fetch, subprocess=subprocess):
72+ self._hookenv = hookenv
73 self._fetch = fetch
74-
75- def _run(self):
76- self._configure_sources()
77- self._install_packages()
78-
79- def _configure_sources(self):
80+ self._subprocess = subprocess
81+
82+ def set_sources(self):
83 """Configure the extra APT sources to use."""
84+ needs_update = False
85+ if self._set_remote_source():
86+ needs_update = True
87+ if self._set_local_source():
88+ needs_update = True
89+ if needs_update:
90+ self._fetch.apt_update(fatal=True)
91+
92+ def install_packages(self):
93+ """Install the needed packages."""
94+ options = list(DEFAULT_INSTALL_OPTIONS)
95+ if self._get_local_tarball() is not None:
96+ # We don't sign the locally built repository, so we need to tell
97+ # apt-get that we don't care.
98+ options.append("--allow-unauthenticated")
99+ packages = self._fetch.filter_installed_packages(PACKAGES)
100+ self._fetch.apt_install(packages, options=options, fatal=True)
101+
102+ def _set_remote_source(self):
103+ """Set the remote APT repository to use, if new or changed."""
104 config = self._hookenv.config()
105 source = config.get("source")
106 if not source:
107 raise HookError("No source config parameter defined")
108+ previous_source = config.previous("source")
109+
110+ # Check if we're setting the source for the first time, or replacing
111+ # an existing value. In the latter case we'll no-op if the value is the
112+ # same or take care to remove it from sources.list if it's not.
113+ previous_source = config.previous("source")
114+ if previous_source is not None:
115+ if previous_source == source:
116+ return False
117+ self._subprocess.check_call(
118+ ["add-apt-repository", "--remove", "--yes", previous_source])
119+
120 self._fetch.add_source(source, config.get("key"))
121- self._fetch.apt_update(fatal=True)
122-
123- def _install_packages(self):
124- """Install the needed packages."""
125- packages = self._fetch.filter_installed_packages(PACKAGES)
126+
127+ return True
128+
129+ def _set_local_source(self):
130+ """Set the local APT repository for the Landscape tarball, if any."""
131+ tarball = self._get_local_tarball()
132+ if tarball is None:
133+ return False
134+
135+ if not self._is_tarball_new(tarball):
136+ return False
137+
138+ packages = self._fetch.filter_installed_packages(PACKAGES_DEV)
139 self._fetch.apt_install(packages, fatal=True)
140+
141+ build_dir = os.path.join(self._hookenv.charm_dir(), "build")
142+ shutil.rmtree(build_dir, ignore_errors=True)
143+ os.mkdir(build_dir)
144+
145+ self._subprocess.check_call(
146+ ["tar", "--strip=1", "-xf", tarball], cwd=build_dir)
147+ self._subprocess.check_call(
148+ BUILD_LOCAL_ARCHIVE, shell=True, cwd=build_dir)
149+
150+ self._fetch.add_source("deb file://%s/ ./" % build_dir)
151+
152+ return True
153+
154+ def _get_local_tarball(self):
155+ """Return the local Landscape tarball if any, C{None} otherwise."""
156+ matches = glob.glob(os.path.join(self._hookenv.charm_dir(), TARBALL))
157+ return matches[0] if matches else None
158+
159+ def _is_tarball_new(self, tarball):
160+ """Check if this is a new tarball and we need to build it."""
161+ with open(tarball, "r") as fd:
162+ digest = hashlib.md5(fd.read()).hexdigest()
163+
164+ md5sum = tarball + ".md5sum"
165+ if os.path.exists(md5sum):
166+ with open(md5sum, "r") as fd:
167+ if fd.read() == digest:
168+ # The checksum matches, so it's not a new tarball
169+ return False
170+
171+ # Update the md5sum file, since this is a new tarball.
172+ with open(md5sum, "w") as fd:
173+ fd.write(digest)
174+
175+ return True
176
177=== added file 'hooks/lib/config.py'
178--- hooks/lib/config.py 1970-01-01 00:00:00 +0000
179+++ hooks/lib/config.py 2015-03-09 11:56:41 +0000
180@@ -0,0 +1,23 @@
181+import subprocess
182+
183+from charmhelpers import fetch
184+from charmhelpers.core import hookenv
185+
186+from lib.hook import Hook
187+from lib.apt import Apt
188+
189+
190+class ConfigHook(Hook):
191+ """Execute config-changed hook logic."""
192+
193+ def __init__(self, hookenv=hookenv, fetch=fetch, subprocess=subprocess):
194+ super(ConfigHook, self).__init__(hookenv=hookenv)
195+ self._fetch = fetch
196+ self._subprocess = subprocess
197+
198+ def _run(self):
199+ # Re-set APT sources, if the have changed.
200+ apt = Apt(
201+ hookenv=self._hookenv, fetch=self._fetch,
202+ subprocess=self._subprocess)
203+ apt.set_sources()
204
205=== added file 'hooks/lib/install.py'
206--- hooks/lib/install.py 1970-01-01 00:00:00 +0000
207+++ hooks/lib/install.py 2015-03-09 11:56:41 +0000
208@@ -0,0 +1,24 @@
209+import subprocess
210+
211+from charmhelpers import fetch
212+from charmhelpers.core import hookenv
213+
214+from lib.hook import Hook
215+from lib.apt import Apt
216+
217+
218+class InstallHook(Hook):
219+ """Execute install hook logic."""
220+
221+ def __init__(self, hookenv=hookenv, fetch=fetch, subprocess=subprocess):
222+ super(InstallHook, self).__init__(hookenv=hookenv)
223+ self._fetch = fetch
224+ self._subprocess = subprocess
225+
226+ def _run(self):
227+ # Set APT sources and install Landscape packages
228+ apt = Apt(
229+ hookenv=self._hookenv, fetch=self._fetch,
230+ subprocess=self._subprocess)
231+ apt.set_sources()
232+ apt.install_packages()
233
234=== modified file 'hooks/lib/tests/helpers.py'
235--- hooks/lib/tests/helpers.py 2015-01-29 09:55:59 +0000
236+++ hooks/lib/tests/helpers.py 2015-03-09 11:56:41 +0000
237@@ -26,7 +26,7 @@
238 # upstream.
239 charm_dir = self.useFixture(TempDir())
240 self.useFixture(EnvironmentVariable("CHARM_DIR", charm_dir.path))
241- self.hookenv = HookenvStub()
242+ self.hookenv = HookenvStub(charm_dir.path)
243
244 if self.with_hookenv_monkey_patch:
245 self._monkey_patch_hookenv()
246
247=== modified file 'hooks/lib/tests/stubs.py'
248--- hooks/lib/tests/stubs.py 2015-03-02 12:39:25 +0000
249+++ hooks/lib/tests/stubs.py 2015-03-09 11:56:41 +0000
250@@ -9,10 +9,11 @@
251 unit = "landscape-server/0"
252 relid = None
253
254- def __init__(self):
255+ def __init__(self, charm_dir):
256 self.messages = []
257 self.relations = {}
258 self._config = Config()
259+ self._charm_dir = charm_dir
260
261 def config(self):
262 return self._config
263@@ -58,6 +59,9 @@
264 def relation_set(self, rid=None, relation_settings=None, **kwargs):
265 self.relations[rid] = relation_settings
266
267+ def charm_dir(self):
268+ return self._charm_dir
269+
270
271 class FetchStub(object):
272 """Provide a testable stub for C{charmhelpers.fetch}."""
273@@ -78,8 +82,8 @@
274 self.filtered.append(packages)
275 return packages
276
277- def apt_install(self, packages, fatal=False):
278- self.installed.append((packages, fatal))
279+ def apt_install(self, packages, options=None, fatal=False):
280+ self.installed.append((packages, options, fatal))
281
282
283 class ClusterStub(object):
284
285=== renamed file 'hooks/lib/tests/test_install.py' => 'hooks/lib/tests/test_apt.py'
286--- hooks/lib/tests/test_install.py 2015-01-21 20:37:36 +0000
287+++ hooks/lib/tests/test_apt.py 2015-03-09 11:56:41 +0000
288@@ -1,37 +1,110 @@
289-from charmhelpers.core.hookenv import ERROR
290+import os
291
292-from lib.install import InstallHook, PACKAGES
293-from lib.tests.stubs import FetchStub
294+from lib.apt import Apt, PACKAGES, BUILD_LOCAL_ARCHIVE, DEFAULT_INSTALL_OPTIONS
295+from lib.hook import HookError
296+from lib.tests.stubs import FetchStub, SubprocessStub
297 from lib.tests.helpers import HookenvTest
298
299
300-class InstallHookTest(HookenvTest):
301+class AptTest(HookenvTest):
302
303 def setUp(self):
304- super(InstallHookTest, self).setUp()
305+ super(AptTest, self).setUp()
306 self.fetch = FetchStub()
307- self.hook = InstallHook(
308- fetch=self.fetch,
309- hookenv=self.hookenv)
310+ self.subprocess = SubprocessStub()
311+ self.apt = Apt(
312+ hookenv=self.hookenv, fetch=self.fetch, subprocess=self.subprocess)
313
314 def test_no_source(self):
315 """
316- If no APT source is defined the install hook logs an error
317- message and exists with code 1.
318- """
319- self.assertEqual(1, self.hook())
320- self.assertEqual(
321- ("No source config parameter defined", ERROR),
322- self.hookenv.messages[-1])
323-
324- def test_add_source(self):
325- """
326- The install hook adds the configured APT source and refreshes it.
327- """
328- self.hookenv.config()["source"] = "ppa:landscape/14.10"
329- self.assertEqual(0, self.hook())
330- self.assertEqual([("ppa:landscape/14.10", None)], self.fetch.sources)
331- self.assertEqual([True], self.fetch.updates)
332+ If no APT source is defined, we fail with a L{HookError}.
333+ """
334+ with self.assertRaises(HookError) as error:
335+ self.apt.set_sources()
336+ self.assertEqual(
337+ "No source config parameter defined", str(error.exception))
338+
339+ def test_set_sources(self):
340+ """
341+ The C{set_sources} method adds the configured APT source and
342+ refreshes it.
343+ """
344+ self.hookenv.config()["source"] = "ppa:landscape/14.10"
345+ self.apt.set_sources()
346+ self.assertEqual([("ppa:landscape/14.10", None)], self.fetch.sources)
347+ self.assertEqual([True], self.fetch.updates)
348+
349+ def test_set_sources_not_changed(self):
350+ """
351+ The C{set_sources} method is a no-op if the source config hasn't
352+ changed.
353+ """
354+ config = self.hookenv.config()
355+ config["source"] = "ppa:landscape/14.10"
356+ config.save()
357+ config.load_previous()
358+ self.apt.set_sources()
359+ self.assertEqual([], self.fetch.sources)
360+ self.assertEqual([], self.fetch.updates)
361+
362+ def test_set_sources_replace(self):
363+ """
364+ The C{set_sources} method removes any previous source before setting
365+ the new one.
366+ """
367+ config = self.hookenv.config()
368+ config["source"] = "ppa:landscape/14.10"
369+ config.save()
370+ config.load_previous()
371+ config["source"] = "ppa:landscape/15.01"
372+ self.apt.set_sources()
373+ self.assertEqual(
374+ ["add-apt-repository", "--remove", "--yes", "ppa:landscape/14.10"],
375+ self.subprocess.calls[0][0])
376+ self.assertEqual([("ppa:landscape/15.01", None)], self.fetch.sources)
377+ self.assertEqual([True], self.fetch.updates)
378+
379+ def test_local_tarball(self):
380+ """
381+ If a Landscape tarball is found, the C{set_sources} method builds local
382+ repository with the relevant deb packages.
383+ """
384+ self.hookenv.config()["source"] = "ppa:landscape/14.10"
385+ tarball = os.path.join(
386+ self.hookenv.charm_dir(), "landscape-server_1.2.3.tar.gz")
387+ with open(tarball, "w") as fd:
388+ fd.write("")
389+ self.apt.set_sources()
390+
391+ build_dir = os.path.join(self.hookenv.charm_dir(), "build")
392+
393+ self.assertEqual(
394+ [(["tar", "--strip=1", "-xf", tarball], {"cwd": build_dir}),
395+ (BUILD_LOCAL_ARCHIVE, {"shell": True, "cwd": build_dir})],
396+ self.subprocess.calls)
397+
398+ self.assertEqual(
399+ [("ppa:landscape/14.10", None),
400+ ("deb file://%s/build/ ./" % self.hookenv.charm_dir(), None)],
401+ self.fetch.sources)
402+
403+ def test_local_tarball_not_new(self):
404+ """
405+ If the landscape tarball hasn't changed, it won't be built.
406+ """
407+ self.hookenv.config()["source"] = "ppa:landscape/14.10"
408+ tarball = os.path.join(
409+ self.hookenv.charm_dir(), "landscape-server_1.2.3.tar.gz")
410+ with open(tarball, "w") as fd:
411+ fd.write("data")
412+ self.apt.set_sources()
413+
414+ # Reset the recorded sources and subprocess calls and run again
415+ self.subprocess.calls[:] = []
416+ self.fetch.sources[:] = []
417+ self.apt.set_sources()
418+ self.assertEqual([], self.subprocess.calls)
419+ self.assertEqual([("ppa:landscape/14.10", None)], self.fetch.sources)
420
421 def test_packages(self):
422 """
423@@ -41,9 +114,24 @@
424
425 def test_install(self):
426 """
427- The install hook installs the required packages.
428+ The C{install_packages} method installs the required packages.
429 """
430 self.hookenv.config()["source"] = "ppa:landscape/14.10"
431- self.assertEqual(0, self.hook())
432+ self.apt.install_packages()
433 self.assertEqual([PACKAGES], self.fetch.filtered)
434- self.assertEqual([(PACKAGES, True)], self.fetch.installed)
435+ options = list(DEFAULT_INSTALL_OPTIONS)
436+ self.assertEqual([(PACKAGES, options, True)], self.fetch.installed)
437+
438+ def test_install_with_local_tarball(self):
439+ """
440+ The C{install_packages} method allows unauthenticated packages if we
441+ have a locally built repository.
442+ """
443+ tarball = os.path.join(
444+ self.hookenv.charm_dir(), "landscape-server_1.2.3.tar.gz")
445+ with open(tarball, "w") as fd:
446+ fd.write("")
447+ self.hookenv.config()["source"] = "ppa:landscape/14.10"
448+ self.apt.install_packages()
449+ options = list(DEFAULT_INSTALL_OPTIONS) + ["--allow-unauthenticated"]
450+ self.assertEqual([(PACKAGES, options, True)], self.fetch.installed)
451
452=== added file 'hooks/lib/tests/test_config.py'
453--- hooks/lib/tests/test_config.py 1970-01-01 00:00:00 +0000
454+++ hooks/lib/tests/test_config.py 2015-03-09 11:56:41 +0000
455@@ -0,0 +1,25 @@
456+from lib.tests.helpers import HookenvTest
457+from lib.tests.stubs import FetchStub, SubprocessStub
458+from lib.config import ConfigHook
459+
460+
461+class ConfigHookTest(HookenvTest):
462+
463+ def setUp(self):
464+ super(ConfigHookTest, self).setUp()
465+ self.fetch = FetchStub()
466+ self.subprocess = SubprocessStub()
467+ self.hook = ConfigHook(
468+ hookenv=self.hookenv, fetch=self.fetch, subprocess=self.subprocess)
469+
470+ def test_run(self):
471+ """
472+ The L{ConfigHook} re-configures APT sources if the have changed.
473+ """
474+ config = self.hookenv.config()
475+ config["source"] = "ppa:landscape/14.10"
476+ config.save()
477+ config.load_previous()
478+ config["source"] = "ppa:landscape/15.01"
479+ self.assertEqual(0, self.hook())
480+ self.assertTrue(len(self.fetch.sources) > 0)
481
482=== added file 'hooks/lib/tests/test_install.py'
483--- hooks/lib/tests/test_install.py 1970-01-01 00:00:00 +0000
484+++ hooks/lib/tests/test_install.py 2015-03-09 11:56:41 +0000
485@@ -0,0 +1,23 @@
486+from lib.tests.helpers import HookenvTest
487+from lib.tests.stubs import FetchStub, SubprocessStub
488+from lib.install import InstallHook
489+
490+
491+class InstallHookTest(HookenvTest):
492+
493+ def setUp(self):
494+ super(InstallHookTest, self).setUp()
495+ self.fetch = FetchStub()
496+ self.subprocess = SubprocessStub()
497+ self.hook = InstallHook(
498+ hookenv=self.hookenv, fetch=self.fetch, subprocess=self.subprocess)
499+
500+ def test_run(self):
501+ """
502+ The L{InstallHook} configures APT sources and install the needed
503+ packages.
504+ """
505+ self.hookenv.config()["source"] = "ppa:landscape/14.10"
506+ self.assertEqual(0, self.hook())
507+ self.assertNotEqual([], self.fetch.sources)
508+ self.assertNotEqual([], self.fetch.installed)

Subscribers

People subscribed via source and target branches