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
=== added file 'hooks/config-changed'
--- hooks/config-changed 1970-01-01 00:00:00 +0000
+++ hooks/config-changed 2015-03-09 11:56:41 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2import sys
3
4from lib.config import ConfigHook
5
6
7if __name__ == "__main__":
8 hook = ConfigHook()
9 sys.exit(hook())
010
=== renamed file 'hooks/lib/install.py' => 'hooks/lib/apt.py'
--- hooks/lib/install.py 2015-01-28 11:53:18 +0000
+++ hooks/lib/apt.py 2015-03-09 11:56:41 +0000
@@ -1,31 +1,138 @@
1import glob
2import hashlib
3import os
4import shutil
5import subprocess
6
1from charmhelpers import fetch7from charmhelpers import fetch
8from charmhelpers.core import hookenv
29
3from lib.hook import Hook, HookError10from lib.hook import HookError
411
5PACKAGES = ("landscape-server",)12PACKAGES = ("landscape-server",)
613PACKAGES_DEV = ("dpkg-dev", "pbuilder")
714TARBALL = "landscape-server_*.tar.gz"
8class InstallHook(Hook):15
9 """Execute install hook logic."""16# XXX Default options taken from charmhelpers, there's no way to just
1017# extend them.
11 def __init__(self, fetch=fetch, **kwargs):18DEFAULT_INSTALL_OPTIONS = ("--option=Dpkg::Options::=--force-confold",)
12 super(InstallHook, self).__init__(**kwargs)19
20# Shell commands to build the debs and publish them in a local repository
21BUILD_LOCAL_ARCHIVE = """
22dch -v 9999:$(dpkg-parsechangelog|grep ^Version:|cut -d ' ' -f 2) \
23 development --distribution $(lsb_release -cs) &&
24/usr/lib/pbuilder/pbuilder-satisfydepends &&
25dpkg-buildpackage -us -uc &&
26mv ../*.deb . &&
27dpkg-scanpackages -m . /dev/null > Packages &&
28cat Packages | bzip2 -9 > Packages.bz2 &&
29cat Packages | gzip -9 > Packages.gz &&
30dpkg-scansources . > Sources &&
31cat Sources | bzip2 -9 > Sources.bz2 &&
32cat Sources | gzip -9 > Sources.gz &&
33apt-ftparchive release . > Release
34"""
35
36
37class Apt(object):
38 """Perform APT-related tasks as setting sources and installing packages.
39
40 This is a thin facade around C{charmhelpers.fetch}, offering some
41 additional features, like building Landscape packages from a local
42 tarball.
43 """
44
45 def __init__(self, hookenv=hookenv, fetch=fetch, subprocess=subprocess):
46 self._hookenv = hookenv
13 self._fetch = fetch47 self._fetch = fetch
1448 self._subprocess = subprocess
15 def _run(self):49
16 self._configure_sources()50 def set_sources(self):
17 self._install_packages()
18
19 def _configure_sources(self):
20 """Configure the extra APT sources to use."""51 """Configure the extra APT sources to use."""
52 needs_update = False
53 if self._set_remote_source():
54 needs_update = True
55 if self._set_local_source():
56 needs_update = True
57 if needs_update:
58 self._fetch.apt_update(fatal=True)
59
60 def install_packages(self):
61 """Install the needed packages."""
62 options = list(DEFAULT_INSTALL_OPTIONS)
63 if self._get_local_tarball() is not None:
64 # We don't sign the locally built repository, so we need to tell
65 # apt-get that we don't care.
66 options.append("--allow-unauthenticated")
67 packages = self._fetch.filter_installed_packages(PACKAGES)
68 self._fetch.apt_install(packages, options=options, fatal=True)
69
70 def _set_remote_source(self):
71 """Set the remote APT repository to use, if new or changed."""
21 config = self._hookenv.config()72 config = self._hookenv.config()
22 source = config.get("source")73 source = config.get("source")
23 if not source:74 if not source:
24 raise HookError("No source config parameter defined")75 raise HookError("No source config parameter defined")
76 previous_source = config.previous("source")
77
78 # Check if we're setting the source for the first time, or replacing
79 # an existing value. In the latter case we'll no-op if the value is the
80 # same or take care to remove it from sources.list if it's not.
81 previous_source = config.previous("source")
82 if previous_source is not None:
83 if previous_source == source:
84 return False
85 self._subprocess.check_call(
86 ["add-apt-repository", "--remove", "--yes", previous_source])
87
25 self._fetch.add_source(source, config.get("key"))88 self._fetch.add_source(source, config.get("key"))
26 self._fetch.apt_update(fatal=True)89
2790 return True
28 def _install_packages(self):91
29 """Install the needed packages."""92 def _set_local_source(self):
30 packages = self._fetch.filter_installed_packages(PACKAGES)93 """Set the local APT repository for the Landscape tarball, if any."""
94 tarball = self._get_local_tarball()
95 if tarball is None:
96 return False
97
98 if not self._is_tarball_new(tarball):
99 return False
100
101 packages = self._fetch.filter_installed_packages(PACKAGES_DEV)
31 self._fetch.apt_install(packages, fatal=True)102 self._fetch.apt_install(packages, fatal=True)
103
104 build_dir = os.path.join(self._hookenv.charm_dir(), "build")
105 shutil.rmtree(build_dir, ignore_errors=True)
106 os.mkdir(build_dir)
107
108 self._subprocess.check_call(
109 ["tar", "--strip=1", "-xf", tarball], cwd=build_dir)
110 self._subprocess.check_call(
111 BUILD_LOCAL_ARCHIVE, shell=True, cwd=build_dir)
112
113 self._fetch.add_source("deb file://%s/ ./" % build_dir)
114
115 return True
116
117 def _get_local_tarball(self):
118 """Return the local Landscape tarball if any, C{None} otherwise."""
119 matches = glob.glob(os.path.join(self._hookenv.charm_dir(), TARBALL))
120 return matches[0] if matches else None
121
122 def _is_tarball_new(self, tarball):
123 """Check if this is a new tarball and we need to build it."""
124 with open(tarball, "r") as fd:
125 digest = hashlib.md5(fd.read()).hexdigest()
126
127 md5sum = tarball + ".md5sum"
128 if os.path.exists(md5sum):
129 with open(md5sum, "r") as fd:
130 if fd.read() == digest:
131 # The checksum matches, so it's not a new tarball
132 return False
133
134 # Update the md5sum file, since this is a new tarball.
135 with open(md5sum, "w") as fd:
136 fd.write(digest)
137
138 return True
32139
=== added file 'hooks/lib/config.py'
--- hooks/lib/config.py 1970-01-01 00:00:00 +0000
+++ hooks/lib/config.py 2015-03-09 11:56:41 +0000
@@ -0,0 +1,23 @@
1import subprocess
2
3from charmhelpers import fetch
4from charmhelpers.core import hookenv
5
6from lib.hook import Hook
7from lib.apt import Apt
8
9
10class ConfigHook(Hook):
11 """Execute config-changed hook logic."""
12
13 def __init__(self, hookenv=hookenv, fetch=fetch, subprocess=subprocess):
14 super(ConfigHook, self).__init__(hookenv=hookenv)
15 self._fetch = fetch
16 self._subprocess = subprocess
17
18 def _run(self):
19 # Re-set APT sources, if the have changed.
20 apt = Apt(
21 hookenv=self._hookenv, fetch=self._fetch,
22 subprocess=self._subprocess)
23 apt.set_sources()
024
=== added file 'hooks/lib/install.py'
--- hooks/lib/install.py 1970-01-01 00:00:00 +0000
+++ hooks/lib/install.py 2015-03-09 11:56:41 +0000
@@ -0,0 +1,24 @@
1import subprocess
2
3from charmhelpers import fetch
4from charmhelpers.core import hookenv
5
6from lib.hook import Hook
7from lib.apt import Apt
8
9
10class InstallHook(Hook):
11 """Execute install hook logic."""
12
13 def __init__(self, hookenv=hookenv, fetch=fetch, subprocess=subprocess):
14 super(InstallHook, self).__init__(hookenv=hookenv)
15 self._fetch = fetch
16 self._subprocess = subprocess
17
18 def _run(self):
19 # Set APT sources and install Landscape packages
20 apt = Apt(
21 hookenv=self._hookenv, fetch=self._fetch,
22 subprocess=self._subprocess)
23 apt.set_sources()
24 apt.install_packages()
025
=== modified file 'hooks/lib/tests/helpers.py'
--- hooks/lib/tests/helpers.py 2015-01-29 09:55:59 +0000
+++ hooks/lib/tests/helpers.py 2015-03-09 11:56:41 +0000
@@ -26,7 +26,7 @@
26 # upstream.26 # upstream.
27 charm_dir = self.useFixture(TempDir())27 charm_dir = self.useFixture(TempDir())
28 self.useFixture(EnvironmentVariable("CHARM_DIR", charm_dir.path))28 self.useFixture(EnvironmentVariable("CHARM_DIR", charm_dir.path))
29 self.hookenv = HookenvStub()29 self.hookenv = HookenvStub(charm_dir.path)
3030
31 if self.with_hookenv_monkey_patch:31 if self.with_hookenv_monkey_patch:
32 self._monkey_patch_hookenv()32 self._monkey_patch_hookenv()
3333
=== modified file 'hooks/lib/tests/stubs.py'
--- hooks/lib/tests/stubs.py 2015-03-02 12:39:25 +0000
+++ hooks/lib/tests/stubs.py 2015-03-09 11:56:41 +0000
@@ -9,10 +9,11 @@
9 unit = "landscape-server/0"9 unit = "landscape-server/0"
10 relid = None10 relid = None
1111
12 def __init__(self):12 def __init__(self, charm_dir):
13 self.messages = []13 self.messages = []
14 self.relations = {}14 self.relations = {}
15 self._config = Config()15 self._config = Config()
16 self._charm_dir = charm_dir
1617
17 def config(self):18 def config(self):
18 return self._config19 return self._config
@@ -58,6 +59,9 @@
58 def relation_set(self, rid=None, relation_settings=None, **kwargs):59 def relation_set(self, rid=None, relation_settings=None, **kwargs):
59 self.relations[rid] = relation_settings60 self.relations[rid] = relation_settings
6061
62 def charm_dir(self):
63 return self._charm_dir
64
6165
62class FetchStub(object):66class FetchStub(object):
63 """Provide a testable stub for C{charmhelpers.fetch}."""67 """Provide a testable stub for C{charmhelpers.fetch}."""
@@ -78,8 +82,8 @@
78 self.filtered.append(packages)82 self.filtered.append(packages)
79 return packages83 return packages
8084
81 def apt_install(self, packages, fatal=False):85 def apt_install(self, packages, options=None, fatal=False):
82 self.installed.append((packages, fatal))86 self.installed.append((packages, options, fatal))
8387
8488
85class ClusterStub(object):89class ClusterStub(object):
8690
=== renamed file 'hooks/lib/tests/test_install.py' => 'hooks/lib/tests/test_apt.py'
--- hooks/lib/tests/test_install.py 2015-01-21 20:37:36 +0000
+++ hooks/lib/tests/test_apt.py 2015-03-09 11:56:41 +0000
@@ -1,37 +1,110 @@
1from charmhelpers.core.hookenv import ERROR1import os
22
3from lib.install import InstallHook, PACKAGES3from lib.apt import Apt, PACKAGES, BUILD_LOCAL_ARCHIVE, DEFAULT_INSTALL_OPTIONS
4from lib.tests.stubs import FetchStub4from lib.hook import HookError
5from lib.tests.stubs import FetchStub, SubprocessStub
5from lib.tests.helpers import HookenvTest6from lib.tests.helpers import HookenvTest
67
78
8class InstallHookTest(HookenvTest):9class AptTest(HookenvTest):
910
10 def setUp(self):11 def setUp(self):
11 super(InstallHookTest, self).setUp()12 super(AptTest, self).setUp()
12 self.fetch = FetchStub()13 self.fetch = FetchStub()
13 self.hook = InstallHook(14 self.subprocess = SubprocessStub()
14 fetch=self.fetch,15 self.apt = Apt(
15 hookenv=self.hookenv)16 hookenv=self.hookenv, fetch=self.fetch, subprocess=self.subprocess)
1617
17 def test_no_source(self):18 def test_no_source(self):
18 """19 """
19 If no APT source is defined the install hook logs an error20 If no APT source is defined, we fail with a L{HookError}.
20 message and exists with code 1.21 """
21 """22 with self.assertRaises(HookError) as error:
22 self.assertEqual(1, self.hook())23 self.apt.set_sources()
23 self.assertEqual(24 self.assertEqual(
24 ("No source config parameter defined", ERROR),25 "No source config parameter defined", str(error.exception))
25 self.hookenv.messages[-1])26
2627 def test_set_sources(self):
27 def test_add_source(self):28 """
28 """29 The C{set_sources} method adds the configured APT source and
29 The install hook adds the configured APT source and refreshes it.30 refreshes it.
30 """31 """
31 self.hookenv.config()["source"] = "ppa:landscape/14.10"32 self.hookenv.config()["source"] = "ppa:landscape/14.10"
32 self.assertEqual(0, self.hook())33 self.apt.set_sources()
33 self.assertEqual([("ppa:landscape/14.10", None)], self.fetch.sources)34 self.assertEqual([("ppa:landscape/14.10", None)], self.fetch.sources)
34 self.assertEqual([True], self.fetch.updates)35 self.assertEqual([True], self.fetch.updates)
36
37 def test_set_sources_not_changed(self):
38 """
39 The C{set_sources} method is a no-op if the source config hasn't
40 changed.
41 """
42 config = self.hookenv.config()
43 config["source"] = "ppa:landscape/14.10"
44 config.save()
45 config.load_previous()
46 self.apt.set_sources()
47 self.assertEqual([], self.fetch.sources)
48 self.assertEqual([], self.fetch.updates)
49
50 def test_set_sources_replace(self):
51 """
52 The C{set_sources} method removes any previous source before setting
53 the new one.
54 """
55 config = self.hookenv.config()
56 config["source"] = "ppa:landscape/14.10"
57 config.save()
58 config.load_previous()
59 config["source"] = "ppa:landscape/15.01"
60 self.apt.set_sources()
61 self.assertEqual(
62 ["add-apt-repository", "--remove", "--yes", "ppa:landscape/14.10"],
63 self.subprocess.calls[0][0])
64 self.assertEqual([("ppa:landscape/15.01", None)], self.fetch.sources)
65 self.assertEqual([True], self.fetch.updates)
66
67 def test_local_tarball(self):
68 """
69 If a Landscape tarball is found, the C{set_sources} method builds local
70 repository with the relevant deb packages.
71 """
72 self.hookenv.config()["source"] = "ppa:landscape/14.10"
73 tarball = os.path.join(
74 self.hookenv.charm_dir(), "landscape-server_1.2.3.tar.gz")
75 with open(tarball, "w") as fd:
76 fd.write("")
77 self.apt.set_sources()
78
79 build_dir = os.path.join(self.hookenv.charm_dir(), "build")
80
81 self.assertEqual(
82 [(["tar", "--strip=1", "-xf", tarball], {"cwd": build_dir}),
83 (BUILD_LOCAL_ARCHIVE, {"shell": True, "cwd": build_dir})],
84 self.subprocess.calls)
85
86 self.assertEqual(
87 [("ppa:landscape/14.10", None),
88 ("deb file://%s/build/ ./" % self.hookenv.charm_dir(), None)],
89 self.fetch.sources)
90
91 def test_local_tarball_not_new(self):
92 """
93 If the landscape tarball hasn't changed, it won't be built.
94 """
95 self.hookenv.config()["source"] = "ppa:landscape/14.10"
96 tarball = os.path.join(
97 self.hookenv.charm_dir(), "landscape-server_1.2.3.tar.gz")
98 with open(tarball, "w") as fd:
99 fd.write("data")
100 self.apt.set_sources()
101
102 # Reset the recorded sources and subprocess calls and run again
103 self.subprocess.calls[:] = []
104 self.fetch.sources[:] = []
105 self.apt.set_sources()
106 self.assertEqual([], self.subprocess.calls)
107 self.assertEqual([("ppa:landscape/14.10", None)], self.fetch.sources)
35108
36 def test_packages(self):109 def test_packages(self):
37 """110 """
@@ -41,9 +114,24 @@
41114
42 def test_install(self):115 def test_install(self):
43 """116 """
44 The install hook installs the required packages.117 The C{install_packages} method installs the required packages.
45 """118 """
46 self.hookenv.config()["source"] = "ppa:landscape/14.10"119 self.hookenv.config()["source"] = "ppa:landscape/14.10"
47 self.assertEqual(0, self.hook())120 self.apt.install_packages()
48 self.assertEqual([PACKAGES], self.fetch.filtered)121 self.assertEqual([PACKAGES], self.fetch.filtered)
49 self.assertEqual([(PACKAGES, True)], self.fetch.installed)122 options = list(DEFAULT_INSTALL_OPTIONS)
123 self.assertEqual([(PACKAGES, options, True)], self.fetch.installed)
124
125 def test_install_with_local_tarball(self):
126 """
127 The C{install_packages} method allows unauthenticated packages if we
128 have a locally built repository.
129 """
130 tarball = os.path.join(
131 self.hookenv.charm_dir(), "landscape-server_1.2.3.tar.gz")
132 with open(tarball, "w") as fd:
133 fd.write("")
134 self.hookenv.config()["source"] = "ppa:landscape/14.10"
135 self.apt.install_packages()
136 options = list(DEFAULT_INSTALL_OPTIONS) + ["--allow-unauthenticated"]
137 self.assertEqual([(PACKAGES, options, True)], self.fetch.installed)
50138
=== added file 'hooks/lib/tests/test_config.py'
--- hooks/lib/tests/test_config.py 1970-01-01 00:00:00 +0000
+++ hooks/lib/tests/test_config.py 2015-03-09 11:56:41 +0000
@@ -0,0 +1,25 @@
1from lib.tests.helpers import HookenvTest
2from lib.tests.stubs import FetchStub, SubprocessStub
3from lib.config import ConfigHook
4
5
6class ConfigHookTest(HookenvTest):
7
8 def setUp(self):
9 super(ConfigHookTest, self).setUp()
10 self.fetch = FetchStub()
11 self.subprocess = SubprocessStub()
12 self.hook = ConfigHook(
13 hookenv=self.hookenv, fetch=self.fetch, subprocess=self.subprocess)
14
15 def test_run(self):
16 """
17 The L{ConfigHook} re-configures APT sources if the have changed.
18 """
19 config = self.hookenv.config()
20 config["source"] = "ppa:landscape/14.10"
21 config.save()
22 config.load_previous()
23 config["source"] = "ppa:landscape/15.01"
24 self.assertEqual(0, self.hook())
25 self.assertTrue(len(self.fetch.sources) > 0)
026
=== added file 'hooks/lib/tests/test_install.py'
--- hooks/lib/tests/test_install.py 1970-01-01 00:00:00 +0000
+++ hooks/lib/tests/test_install.py 2015-03-09 11:56:41 +0000
@@ -0,0 +1,23 @@
1from lib.tests.helpers import HookenvTest
2from lib.tests.stubs import FetchStub, SubprocessStub
3from lib.install import InstallHook
4
5
6class InstallHookTest(HookenvTest):
7
8 def setUp(self):
9 super(InstallHookTest, self).setUp()
10 self.fetch = FetchStub()
11 self.subprocess = SubprocessStub()
12 self.hook = InstallHook(
13 hookenv=self.hookenv, fetch=self.fetch, subprocess=self.subprocess)
14
15 def test_run(self):
16 """
17 The L{InstallHook} configures APT sources and install the needed
18 packages.
19 """
20 self.hookenv.config()["source"] = "ppa:landscape/14.10"
21 self.assertEqual(0, self.hook())
22 self.assertNotEqual([], self.fetch.sources)
23 self.assertNotEqual([], self.fetch.installed)

Subscribers

People subscribed via source and target branches