Merge lp:~bjornt/landscape-client/apt-channel-api into lp:~landscape/landscape-client/trunk

Proposed by Björn Tillenius
Status: Merged
Approved by: Free Ekanayaka
Approved revision: 384
Merged at revision: 366
Proposed branch: lp:~bjornt/landscape-client/apt-channel-api
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 304 lines (+194/-5)
5 files modified
landscape/lib/fs.py (+13/-0)
landscape/lib/tests/test_fs.py (+23/-1)
landscape/package/facade.py (+42/-0)
landscape/package/tests/helpers.py (+1/-0)
landscape/package/tests/test_facade.py (+115/-4)
To merge this branch: bzr merge lp:~bjornt/landscape-client/apt-channel-api
Reviewer Review Type Date Requested Status
Free Ekanayaka (community) Approve
Alberto Donato (community) Approve
Review via email: mp+77313@code.launchpad.net

Description of the change

Add support for adding and removing deb URLs in AptFacade.

add_channel_apt_deb() and reset_channels() were implemented.
add_channel() was omitted, since it's only used by SmartFacade
internally.

I filed bug 861345 for implementing add_channel_deb_dir, since we have
to do some extra work to make sure apt can use the dir.

To post a comment you must log in.
Revision history for this message
Alberto Donato (ack) wrote :

Nice, +1!

#1:
+ sources_file_path = sources_dir + "/landscape-internal-facade.list"

#2:
+ with open(path + "/Packages", "a") as packages:

these should use os.path.join()

#3:
+ with open(list_filename, "r") as sources:
+ sources_contents = sources.read()

you can use read_file() from landscape.lib.fs here (there are multiple tests with similar code)

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

Nice work, +1!

[1]

+ sources_file_path = sources_dir + "/landscape-internal-facade.list"

Please use os.path.join().

Also, wdyt of prefixing the file with an underscore to signal it's a "private" file? like "_landscape-internal-facade.list" (unfortunately using a dot as prefix won't work because libapt ignores it).

[2]

+ add_channel_apt_deb() adds a new deb URL to a file in

Please write it with epydoc syntax, "C{add_channel_apt_deb}". Same for the other occurrences.

[3]

+ self.facade.add_channel_apt_deb(
+ "http://example.com/ubuntu", "lucid", None)

It'd be nice to make the components parameter optional in add_channel_apt_deb. I understand that it's not optional in the SmartFacade, but it should be compatible.

[4]

+ If deb URLs have been added, a list of dict is returned with
+ information about the channels.

The second line is indented too much.

[5]

+ self.facade.add_channel_apt_deb(
+ "http://enabled.example.com/ubuntu", "lucid", ["main"])
+ self.facade.add_channel_apt_deb(
+ "http://disabled.example.com/ubuntu", "lucid", ["main"])
+ sources_list = SourcesList()
+ for entry in sources_list:
+ if "disabled" in entry.uri:
+ entry.set_enabled(False)
+ sources_list.save()

Doh, SourcesList() is nice and convenient, just too bad that it relies on global state! Anyway, good idea to use it, I didn't know or recall about it.

[6]

+ def reset_channels(self):
+ """Remove all the configured channels."""
+ sources_list = SourcesList()
+ for entry in sources_list:
+ entry.set_enabled(False)

The only use case for reset_channels() in the server code, canonical/landscape/scripts/hashids.py, where we update the lists of pre-canned hash->id maps. I think it's pretty safe to remove the entry entirely for that, like:

        for entry in sources_list:
            sources_list.remove(entry)

which I think would fit more with the "reset" term. If we need a "disable" behavior we could add a disable_channels() method.

Anyway, this is really minor, so your call.

review: Approve
385. By Björn Tillenius

Merge trunk.

386. By Björn Tillenius

Use os.path.join.

387. By Björn Tillenius

Use read_file().

388. By Björn Tillenius

Add append_file() and use it to get rid of with statements.

389. By Björn Tillenius

Rename .list file to make it more obvious that it's private.

390. By Björn Tillenius

Epydoc syntax.

Revision history for this message
Björn Tillenius (bjornt) wrote :

On Wed, Sep 28, 2011 at 12:56:28PM -0000, Alberto Donato wrote:
> Review: Approve
>
> Nice, +1!
>
> #1:
> + sources_file_path = sources_dir + "/landscape-internal-facade.list"
>
> #2:
> + with open(path + "/Packages", "a") as packages:
>
> these should use os.path.join()

Sure.

> #3:
> + with open(list_filename, "r") as sources:
> + sources_contents = sources.read()
>
> you can use read_file() from landscape.lib.fs here (there are multiple
> tests with similar code)

Sure. I added append_file() as well, and replaced the with statements
that appended contents to files.

--
Björn Tillenius | https://launchpad.net/~bjornt

Revision history for this message
Björn Tillenius (bjornt) wrote :

On Wed, Sep 28, 2011 at 01:00:27PM -0000, Free Ekanayaka wrote:
> Review: Approve
>
> Nice work, +1!
>
> [1]
>
> + sources_file_path = sources_dir + "/landscape-internal-facade.list"
>
> Please use os.path.join().

Sure.

> Also, wdyt of prefixing the file with an underscore to signal it's a
> "private" file? like "_landscape-internal-facade.list" (unfortunately
> using a dot as prefix won't work because libapt ignores it).

Yes, that's a good idea.

> [2]
>
> + add_channel_apt_deb() adds a new deb URL to a file in
>
> Please write it with epydoc syntax, "C{add_channel_apt_deb}". Same for
> the other occurrences.

Done.

> [3]
>
> + self.facade.add_channel_apt_deb(
> + "http://example.com/ubuntu", "lucid", None)
>
> It'd be nice to make the components parameter optional in
> add_channel_apt_deb. I understand that it's not optional in the
> SmartFacade, but it should be compatible.

Sure, I made it optional

> [4]
>
> + If deb URLs have been added, a list of dict is returned with
> + information about the channels.
>
> The second line is indented too much.

Fixed.

> [5]
>
> + self.facade.add_channel_apt_deb(
> + "http://enabled.example.com/ubuntu", "lucid", ["main"])
> + self.facade.add_channel_apt_deb(
> + "http://disabled.example.com/ubuntu", "lucid", ["main"])
> + sources_list = SourcesList()
> + for entry in sources_list:
> + if "disabled" in entry.uri:
> + entry.set_enabled(False)
> + sources_list.save()
>
> Doh, SourcesList() is nice and convenient, just too bad that it relies on global state! Anyway, good idea to use it, I didn't know or recall about it.
>
> [6]
>
> + def reset_channels(self):
> + """Remove all the configured channels."""
> + sources_list = SourcesList()
> + for entry in sources_list:
> + entry.set_enabled(False)
>
> The only use case for reset_channels() in the server code,
> canonical/landscape/scripts/hashids.py, where we update the lists of
> pre-canned hash->id maps. I think it's pretty safe to remove the entry
> entirely for that, like:
>
> for entry in sources_list:
> sources_list.remove(entry)
>
> which I think would fit more with the "reset" term. If we need a
> "disable" behavior we could add a disable_channels() method.

I agree. However, it doesn't quite work ;) I tried doing this, but for
some reason it doesn't remove the entries. So rather than spending time
on debuggin, I chose to simply disable them.

--
Björn Tillenius | https://launchpad.net/~bjornt

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'landscape/lib/fs.py'
--- landscape/lib/fs.py 2010-04-01 08:28:22 +0000
+++ landscape/lib/fs.py 2011-09-29 12:39:24 +0000
@@ -14,6 +14,19 @@
14 fd.close()14 fd.close()
1515
1616
17def append_file(path, content):
18 """Append a file with the given content.
19
20 The file is created, if it doesn't exist already.
21
22 @param path: The path to the file.
23 @param content: The content to be written in the file at the end.
24 """
25 fd = open(path, "a")
26 fd.write(content)
27 fd.close()
28
29
17def read_file(path, limit=None):30def read_file(path, limit=None):
18 """Return the content of the given file.31 """Return the content of the given file.
1932
2033
=== modified file 'landscape/lib/tests/test_fs.py'
--- landscape/lib/tests/test_fs.py 2011-07-05 05:09:11 +0000
+++ landscape/lib/tests/test_fs.py 2011-09-29 12:39:24 +0000
@@ -1,6 +1,8 @@
1import os
2
1from landscape.tests.helpers import LandscapeTest3from landscape.tests.helpers import LandscapeTest
24
3from landscape.lib.fs import read_file, touch_file5from landscape.lib.fs import append_file, read_file, touch_file
46
57
6class ReadFileTest(LandscapeTest):8class ReadFileTest(LandscapeTest):
@@ -59,3 +61,23 @@
59 touch_file(path)61 touch_file(path)
60 touch_file(path)62 touch_file(path)
61 self.assertFileContent(path, "")63 self.assertFileContent(path, "")
64
65
66class AppendFileTest(LandscapeTest):
67
68 def test_append_existing_file(self):
69 """
70 The L{append_file} function appends contents to an existing file.
71 """
72 existing_file = self.makeFile("foo bar")
73 append_file(existing_file, " baz")
74 self.assertFileContent(existing_file, "foo bar baz")
75
76 def test_append_no_file(self):
77 """
78 The L{append_file} function creates a new file if one doesn't
79 exist already.
80 """
81 new_file = os.path.join(self.makeDir(), "new_file")
82 append_file(new_file, "contents")
83 self.assertFileContent(new_file, "contents")
6284
=== modified file 'landscape/package/facade.py'
--- landscape/package/facade.py 2011-09-26 12:20:46 +0000
+++ landscape/package/facade.py 2011-09-29 12:39:24 +0000
@@ -1,3 +1,5 @@
1import os
2
1from smart.transaction import (3from smart.transaction import (
2 Transaction, PolicyInstall, PolicyUpgrade, PolicyRemove, Failed)4 Transaction, PolicyInstall, PolicyUpgrade, PolicyRemove, Failed)
3from smart.const import INSTALL, REMOVE, UPGRADE, ALWAYS, NEVER5from smart.const import INSTALL, REMOVE, UPGRADE, ALWAYS, NEVER
@@ -5,7 +7,10 @@
5import smart7import smart
68
7import apt9import apt
10import apt_pkg
11from aptsources.sourceslist import SourcesList
812
13from landscape.lib.fs import append_file
9from landscape.package.skeleton import build_skeleton14from landscape.package.skeleton import build_skeleton
1015
1116
@@ -51,6 +56,43 @@
51 def reload_channels(self):56 def reload_channels(self):
52 """Reload the channels and update the cache."""57 """Reload the channels and update the cache."""
53 self._cache.open(None)58 self._cache.open(None)
59 self._cache.update()
60 self._cache.open(None)
61
62 def add_channel_apt_deb(self, url, codename, components):
63 """Add a deb URL which points to a repository.
64
65 @param url: The base URL of the repository.
66 @param codename: The dist in the repository.
67 @param components: The components to be included.
68 """
69 sources_dir = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
70 sources_file_path = os.path.join(
71 sources_dir, "_landscape-internal-facade.list")
72 sources_line = "deb %s %s" % (url, codename)
73 if components:
74 sources_line += " %s" % " ".join(components)
75 sources_line += "\n"
76 append_file(sources_file_path, sources_line)
77
78 def get_channels(self):
79 """Return a list of channels configured.
80
81 A channel is a deb line in sources.list or sources.list.d. It's
82 represented by a dict with baseurl, distribution, components,
83 and type keys.
84 """
85 sources_list = SourcesList()
86 return [{"baseurl": entry.uri, "distribution": entry.dist,
87 "components": " ".join(entry.comps), "type": entry.type}
88 for entry in sources_list if not entry.disabled]
89
90 def reset_channels(self):
91 """Remove all the configured channels."""
92 sources_list = SourcesList()
93 for entry in sources_list:
94 entry.set_enabled(False)
95 sources_list.save()
5496
5597
56class SmartFacade(object):98class SmartFacade(object):
5799
=== modified file 'landscape/package/tests/helpers.py'
--- landscape/package/tests/helpers.py 2011-09-29 10:08:01 +0000
+++ landscape/package/tests/helpers.py 2011-09-29 12:39:24 +0000
@@ -16,6 +16,7 @@
16 # auto-create them, which causing the paths to be printed to stdout.16 # auto-create them, which causing the paths to be printed to stdout.
17 test_case.dpkg_dir = self._create_sub_dir(test_case, "var/lib/dpkg")17 test_case.dpkg_dir = self._create_sub_dir(test_case, "var/lib/dpkg")
18 self._create_sub_dir(test_case, "etc/apt")18 self._create_sub_dir(test_case, "etc/apt")
19 self._create_sub_dir(test_case, "etc/apt/sources.list.d")
19 self._create_sub_dir(test_case, "var/cache/apt/archives/partial")20 self._create_sub_dir(test_case, "var/cache/apt/archives/partial")
20 self._create_sub_dir(test_case, "var/lib/apt/lists/partial")21 self._create_sub_dir(test_case, "var/lib/apt/lists/partial")
21 test_case.dpkg_status = os.path.join(test_case.dpkg_dir, "status")22 test_case.dpkg_status = os.path.join(test_case.dpkg_dir, "status")
2223
=== modified file 'landscape/package/tests/test_facade.py'
--- landscape/package/tests/test_facade.py 2011-09-29 08:20:24 +0000
+++ landscape/package/tests/test_facade.py 2011-09-29 12:39:24 +0000
@@ -8,12 +8,15 @@
8from smart.cache import Provides8from smart.cache import Provides
9from smart.const import NEVER, ALWAYS9from smart.const import NEVER, ALWAYS
1010
11from aptsources.sourceslist import SourcesList
12
11from twisted.internet import reactor13from twisted.internet import reactor
12from twisted.internet.defer import Deferred14from twisted.internet.defer import Deferred
13from twisted.internet.utils import getProcessOutputAndValue15from twisted.internet.utils import getProcessOutputAndValue
1416
15import smart17import smart
1618
19from landscape.lib.fs import append_file, read_file
17from landscape.package.facade import (20from landscape.package.facade import (
18 TransactionError, DependencyError, ChannelError, SmartError)21 TransactionError, DependencyError, ChannelError, SmartError)
1922
@@ -30,8 +33,7 @@
3033
31 def _add_system_package(self, name):34 def _add_system_package(self, name):
32 """Add a package to the dpkg status file."""35 """Add a package to the dpkg status file."""
33 with open(self.dpkg_status, "a") as status_file:36 append_file(self.dpkg_status, textwrap.dedent("""\
34 status_file.write(textwrap.dedent("""\
35 Package: %s37 Package: %s
36 Status: install ok installed38 Status: install ok installed
37 Priority: optional39 Priority: optional
@@ -46,10 +48,22 @@
4648
47 """ % name))49 """ % name))
4850
51 def _add_package_to_deb_dir(self, path, name, version="1.0"):
52 """Add fake package information to a directory.
53
54 There will only be basic information about the package
55 available, so that get_packages() have something to return.
56 There won't be an actual package in the dir.
57 """
58 package_stanza = "Package: %(name)s\nVersion: %(version)s\n\n"
59 append_file(
60 os.path.join(path, "Packages"),
61 package_stanza % {"name": name, "version": version})
62
49 def test_no_system_packages(self):63 def test_no_system_packages(self):
50 """64 """
51 If the dpkg status file is empty, not packages are reported by65 If the dpkg status file is empty, not packages are reported by
52 get_packages().66 C{get_packages()}.
53 """67 """
54 self.facade.reload_channels()68 self.facade.reload_channels()
55 self.assertEqual([], self.facade.get_packages())69 self.assertEqual([], self.facade.get_packages())
@@ -57,7 +71,7 @@
57 def test_get_system_packages(self):71 def test_get_system_packages(self):
58 """72 """
59 If the dpkg status file contains some packages, those packages73 If the dpkg status file contains some packages, those packages
60 are reported by get_packages().74 are reported by C{get_packages()}.
61 """75 """
62 self._add_system_package("foo")76 self._add_system_package("foo")
63 self._add_system_package("bar")77 self._add_system_package("bar")
@@ -66,6 +80,103 @@
66 ["bar", "foo"],80 ["bar", "foo"],
67 sorted(package.name for package in self.facade.get_packages()))81 sorted(package.name for package in self.facade.get_packages()))
6882
83 def test_add_channel_apt_deb_without_components(self):
84 """
85 C{add_channel_apt_deb()} adds a new deb URL to a file in
86 sources.list.d.
87
88 If no components are given, nothing is written after the dist.
89 """
90 self.facade.add_channel_apt_deb(
91 "http://example.com/ubuntu", "lucid", None)
92 list_filename = (
93 self.apt_root +
94 "/etc/apt/sources.list.d/_landscape-internal-facade.list")
95 sources_contents = read_file(list_filename)
96 self.assertEqual(
97 "deb http://example.com/ubuntu lucid\n",
98 sources_contents)
99
100 def test_add_channel_apt_deb_with_components(self):
101 """
102 C{add_channel_apt_deb()} adds a new deb URL to a file in
103 sources.list.d.
104
105 If components are given, they are included after the dist.
106 """
107 self.facade.add_channel_apt_deb(
108 "http://example.com/ubuntu", "lucid", ["main", "restricted"])
109 list_filename = (
110 self.apt_root +
111 "/etc/apt/sources.list.d/_landscape-internal-facade.list")
112 sources_contents = read_file(list_filename)
113 self.assertEqual(
114 "deb http://example.com/ubuntu lucid main restricted\n",
115 sources_contents)
116
117 def test_get_channels_with_no_channels(self):
118 """
119 If no deb URLs have been added, C{get_channels()} returns an empty list.
120 """
121 self.assertEqual([], self.facade.get_channels())
122
123 def test_get_channels_with_channels(self):
124 """
125 If deb URLs have been added, a list of dict is returned with
126 information about the channels.
127 """
128 self.facade.add_channel_apt_deb(
129 "http://example.com/ubuntu", "lucid", ["main", "restricted"])
130 self.assertEqual([{"baseurl": "http://example.com/ubuntu",
131 "distribution": "lucid",
132 "components": "main restricted",
133 "type": "deb"}],
134 self.facade.get_channels())
135
136 def test_get_channels_with_disabled_channels(self):
137 """
138 C{get_channels()} doesn't return disabled deb URLs.
139 """
140 self.facade.add_channel_apt_deb(
141 "http://enabled.example.com/ubuntu", "lucid", ["main"])
142 self.facade.add_channel_apt_deb(
143 "http://disabled.example.com/ubuntu", "lucid", ["main"])
144 sources_list = SourcesList()
145 for entry in sources_list:
146 if "disabled" in entry.uri:
147 entry.set_enabled(False)
148 sources_list.save()
149 self.assertEqual([{"baseurl": "http://enabled.example.com/ubuntu",
150 "distribution": "lucid",
151 "components": "main",
152 "type": "deb"}],
153 self.facade.get_channels())
154
155 def test_reset_channels(self):
156 """
157 C{reset_channels()} disables all the configured deb URLs.
158 """
159 self.facade.add_channel_apt_deb(
160 "http://1.example.com/ubuntu", "lucid", ["main", "restricted"])
161 self.facade.add_channel_apt_deb(
162 "http://2.example.com/ubuntu", "lucid", ["main", "restricted"])
163 self.facade.reset_channels()
164 self.assertEqual([], self.facade.get_channels())
165
166 def test_reload_includes_added_channels(self):
167 """
168 When reloading the channels, C{get_packages()} returns the packages
169 in the channel.
170 """
171 deb_dir = self.makeDir()
172 self._add_package_to_deb_dir(deb_dir, "foo")
173 self._add_package_to_deb_dir(deb_dir, "bar")
174 self.facade.add_channel_apt_deb("file://%s" % deb_dir, "./", None)
175 self.facade.reload_channels()
176 self.assertEqual(
177 ["bar", "foo"],
178 sorted(package.name for package in self.facade.get_packages()))
179
69180
70class SmartFacadeTest(LandscapeTest):181class SmartFacadeTest(LandscapeTest):
71182

Subscribers

People subscribed via source and target branches

to all changes: