Merge lp:~fwereade/pyjuju/check-latest-formulas into lp:pyjuju

Proposed by William Reade
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 386
Merged at revision: 387
Proposed branch: lp:~fwereade/pyjuju/check-latest-formulas
Merge into: lp:pyjuju
Prerequisite: lp:~fwereade/pyjuju/use-remote-formulas
Diff against target: 799 lines (+377/-225)
6 files modified
juju/charm/repository.py (+63/-33)
juju/charm/tests/test_repository.py (+301/-187)
juju/charm/tests/test_url.py (+1/-0)
juju/charm/url.py (+4/-0)
juju/control/deploy.py (+4/-3)
juju/control/upgrade_charm.py (+4/-2)
To merge this branch: bzr merge lp:~fwereade/pyjuju/check-latest-formulas
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+77357@code.launchpad.net

Description of the change

Use the /latest?charms=blah,blah API to detect latest revisions in RemoteCharmRepository; added analogous .latest to LocalFormulaRepository as well.

To post a comment you must log in.
Revision history for this message
William Reade (fwereade) wrote :

Based on request for a cache dir in https://code.launchpad.net/~fwereade/juju/use-remote-formulas/+merge/77323, this branch now actually uses the cache. It assumes that a charm store implementation will guarantee that a charm-url-with-revision uniquely identifies a given charm, but I think that's a reasonable thing to demand ;-).

Revision history for this message
William Reade (fwereade) wrote :

Technically, I suppose, I'm demanding that a charm-url-without-revision plus a revision will uniquely identify a given charm, but if the two things turn out to be different I think we have bigger problems.

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[1]

+ def latest(self, charm_url):
+ d = self.find(charm_url)
+ d.addCallback(attrgetter("metadata.revision"))
+ return d

Eventually the method signature should be closer to the API in
the spec, since we don't want to do a query for every single
charm we have to find out the revision for.

I'm fine to move this forward as an initial implementation, though.

[2]

+ d.addCallback(attrgetter("metadata.revision"))

This seems clearer, avoids the import, and is shorter:

    d.addCallback(lambda c: c.metadata.revision)

[3]

+ url = "%s/%s?charms=%s" % (self.url_base, query, charm_url)

charm_url has to be url-escaped here.

[4]

+ def find(self, charm_url):
+ revision = yield self.latest(charm_url)
+ charm_url = charm_url.with_revision(revision)
+ charm = yield self._get_charm(charm_url)

Why is this hardcoding getting the latest formula at all times?

If charm_url has a revision, there's no reason not to respect it
and proceed exactly the same way, I think.

Also, the original version of this function worked with a single
roundtrip to the server. This one does three of them (one for
the revision, one for the charm, and another one for the sha256).
I think we should tweak the server API so we can get both the
revision and the sha256 at the same time. Will think a bit about
this and post back later today.

review: Needs Fixing
Revision history for this message
William Reade (fwereade) wrote :

Thanks :).

> [1]
>
> + def latest(self, charm_url):
> + d = self.find(charm_url)
> + d.addCallback(attrgetter("metadata.revision"))
> + return d
>
> Eventually the method signature should be closer to the API in
> the spec, since we don't want to do a query for every single
> charm we have to find out the revision for.
>
> I'm fine to move this forward as an initial implementation, though.

Agree, but am trying to avoid speculative generality in the plastic bits.

> [2]
>
> + d.addCallback(attrgetter("metadata.revision"))
>
> This seems clearer, avoids the import, and is shorter:
>
> d.addCallback(lambda c: c.metadata.revision)

Awww, ok.

> [3]
>
> + url = "%s/%s?charms=%s" % (self.url_base, query, charm_url)
>
> charm_url has to be url-escaped here.

* fwereade hangs head in shame

> [4]
>
> + def find(self, charm_url):
> + revision = yield self.latest(charm_url)
> + charm_url = charm_url.with_revision(revision)
> + charm = yield self._get_charm(charm_url)
>
> Why is this hardcoding getting the latest formula at all times?
>
> If charm_url has a revision, there's no reason not to respect it
> and proceed exactly the same way, I think.

Sounds good, I'll fix it.

> Also, the original version of this function worked with a single
> roundtrip to the server. This one does three of them (one for
> the revision, one for the charm, and another one for the sha256).
> I think we should tweak the server API so we can get both the
> revision and the sha256 at the same time. Will think a bit about
> this and post back later today.

A combined method in the server API would be great, we'd just need one roundtrip on cache hits and two on misses. I'll go ahead assuming one exists and make sure we agree what it should be called before next MP :).

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[4]

So, here is an idea to sort out the multiple-request problem: let's
replace the /latest entry by one called /charm-info that is called
the same way (/charm-info?charms=<urls>), but returns a JSON object
like this instead:

{charm_url: {"sha256": s, "revision": n}, ...}

How does that sound?

Revision history for this message
William Reade (fwereade) wrote :

> [4]
>
> So, here is an idea to sort out the multiple-request problem: let's
> replace the /latest entry by one called /charm-info that is called
> the same way (/charm-info?charms=<urls>), but returns a JSON object
> like this instead:
>
> {charm_url: {"sha256": s, "revision": n}, ...}
>
> How does that sound?

Perfect.

386. By William Reade

merge trunk

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

This is looking great. LGTM, considering just a couple of minors:

[5]

+ try:
+ data = yield getPage(url)
+ returnValue(json.loads(data)[str(charm_url)])
+ except (Error, KeyError, ValueError, TypeError):
+ raise CharmNotFound(self.url_base, charm_url)

Hmmm.. do we really want TypeError and ValueError in this list?
My gut feeling when looking at this is that they would be bugs,
but maybe you have something else in mind.

[6]

+ assert charm.metadata.revision == revision, "bad charm revision"
+ assert charm.get_sha256() == info["sha256"], "bad bundle hash"

The first seems like a good fit for an assertion, but the bottom one
is a plausible runtime error that deserves a proper exception and
a nice error message to help the user.

review: Approve
Revision history for this message
William Reade (fwereade) wrote :

[5]

They're intended to guard against bugs in the charm store, really, but I take your point. Hmm. It would certainly be clearer if I assumed that CS will either give me what I asked for, or Error out; can't guard against everything, after all, and the above is indeed potentially masking local bugs. Yep, I'm convinced.

[6]

Sounds good. I'll make sure I delete the bad cache entry as well, so there's some chance of a retry actually working.

387. By William Reade

address review points

388. By William Reade

merge trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'juju/charm/repository.py'
--- juju/charm/repository.py 2011-10-03 20:04:45 +0000
+++ juju/charm/repository.py 2011-10-05 10:00:46 +0000
@@ -1,9 +1,11 @@
1import json
1import os2import os
2import tempfile3import tempfile
4import urllib
3import yaml5import yaml
46
5from twisted.internet.defer import fail, inlineCallbacks, returnValue, succeed7from twisted.internet.defer import fail, inlineCallbacks, returnValue, succeed
6from twisted.web.client import downloadPage8from twisted.web.client import downloadPage, getPage
7from twisted.web.error import Error9from twisted.web.error import Error
810
9from juju.charm.provider import get_charm_from_path11from juju.charm.provider import get_charm_from_path
@@ -21,6 +23,11 @@
21 pass23 pass
2224
2325
26def _cache_key(charm_url):
27 charm_url.assert_revision()
28 return under.quote("%s.charm" % charm_url)
29
30
24class LocalCharmRepository(object):31class LocalCharmRepository(object):
25 """Charm repository in a local directory."""32 """Charm repository in a local directory."""
2633
@@ -50,11 +57,12 @@
50 recent one (greatest revision) will be returned.57 recent one (greatest revision) will be returned.
51 """58 """
52 assert charm_url.collection.schema == "local", "schema mismatch"59 assert charm_url.collection.schema == "local", "schema mismatch"
53 assert charm_url.revision is None, "find-by-revision not supported yet"
5460
55 latest = None61 latest = None
56 for charm in self._collection(charm_url.collection):62 for charm in self._collection(charm_url.collection):
57 if charm.metadata.name == charm_url.name:63 if charm.metadata.name == charm_url.name:
64 if charm.metadata.revision == charm_url.revision:
65 return succeed(charm)
58 if (latest is None or66 if (latest is None or
59 latest.metadata.revision < charm.metadata.revision):67 latest.metadata.revision < charm.metadata.revision):
60 latest = charm68 latest = charm
@@ -64,6 +72,11 @@
6472
65 return succeed(latest)73 return succeed(latest)
6674
75 def latest(self, charm_url):
76 d = self.find(charm_url.with_revision(None))
77 d.addCallback(lambda c: c.metadata.revision)
78 return d
79
6780
68class RemoteCharmRepository(object):81class RemoteCharmRepository(object):
6982
@@ -75,42 +88,60 @@
75 self.cache_path = cache_path88 self.cache_path = cache_path
7689
77 @inlineCallbacks90 @inlineCallbacks
78 def _download(self, url):91 def _get_info(self, charm_url):
92 url = "%s/charm-info?charms=%s" % (
93 self.url_base, urllib.quote(str(charm_url)))
94 try:
95 data = yield getPage(url)
96 returnValue(json.loads(data)[str(charm_url)])
97 except Error:
98 raise CharmNotFound(self.url_base, charm_url)
99
100 @inlineCallbacks
101 def _download(self, charm_url, cache_path):
102 url = "%s/charm/%s" % (self.url_base, urllib.quote(charm_url.path))
79 downloads = os.path.join(self.cache_path, "downloads")103 downloads = os.path.join(self.cache_path, "downloads")
80 _makedirs(downloads)104 _makedirs(downloads)
81 f = tempfile.NamedTemporaryFile(105 f = tempfile.NamedTemporaryFile(
82 prefix=under.quote(url), suffix=".charm", dir=downloads,106 prefix=_cache_key(charm_url), suffix=".part", dir=downloads,
83 delete=False)107 delete=False)
84 f.close()108 f.close()
85 completed_path = f.name109 downloading_path = f.name
86 downloading_path = completed_path + ".part"
87 yield downloadPage(url, downloading_path)
88 os.rename(downloading_path, completed_path)
89 returnValue(completed_path)
90
91 def _cache(self, charm_url_base, temp_charm_path):
92 temp_charm = get_charm_from_path(temp_charm_path)
93 revision = temp_charm.metadata.revision
94 charm_key = under.quote(
95 "%s.charm" % (charm_url_base.with_revision(revision)))
96 charm_path = os.path.join(self.cache_path, charm_key)
97 _makedirs(self.cache_path)
98 os.rename(temp_charm_path, charm_path)
99 return get_charm_from_path(charm_path)
100
101 @inlineCallbacks
102 def find(self, charm_url):
103 assert charm_url.revision is None, "find-by-revision not supported yet"
104 _, path = str(charm_url).split(":", 1)
105 url = "%s/charm/%s" % (self.url_base, path)
106 try:110 try:
107 temp_charm_path = yield self._download(url)111 yield downloadPage(url, downloading_path)
108 except Error:112 except Error:
109 raise CharmNotFound(self.url_base, charm_url)113 raise CharmNotFound(self.url_base, charm_url)
110 returnValue(self._cache(charm_url, temp_charm_path))114 os.rename(downloading_path, cache_path)
111115
112116 @inlineCallbacks
113@inlineCallbacks117 def find(self, charm_url):
118 info = yield self._get_info(charm_url)
119 revision = info["revision"]
120 if charm_url.revision is None:
121 charm_url = charm_url.with_revision(revision)
122 else:
123 assert revision == charm_url.revision, "bad url revision"
124
125 cache_path = os.path.join(self.cache_path, _cache_key(charm_url))
126 cached = os.path.exists(cache_path)
127 if not cached:
128 yield self._download(charm_url, cache_path)
129 charm = get_charm_from_path(cache_path)
130
131 assert charm.metadata.revision == revision, "bad charm revision"
132 if charm.get_sha256() != info["sha256"]:
133 os.remove(cache_path)
134 name = "%s (%s)" % (
135 charm_url, "cached" if cached else "downloaded")
136 raise CharmError(name, "SHA256 mismatch")
137 returnValue(charm)
138
139 @inlineCallbacks
140 def latest(self, charm_url):
141 info = yield self._get_info(charm_url.with_revision(None))
142 returnValue(info["revision"])
143
144
114def resolve(vague_name, repository_path, default_series):145def resolve(vague_name, repository_path, default_series):
115 """Get a Charm and associated identifying information146 """Get a Charm and associated identifying information
116147
@@ -125,7 +156,7 @@
125 :param str default_series: the Ubuntu series to insert when `charm_name` is156 :param str default_series: the Ubuntu series to insert when `charm_name` is
126 inadequately specified.157 inadequately specified.
127158
128 :return: a tuple of a :class:`CharmURL` and a159 :return: a tuple of a :class:`juju.charm.url.CharmURL` and a
129 :class:`juju.charm.base.CharmBase` subclass, which together contain160 :class:`juju.charm.base.CharmBase` subclass, which together contain
130 both the charm's data and all information necessary to specify its161 both the charm's data and all information necessary to specify its
131 source.162 source.
@@ -135,5 +166,4 @@
135 repo = LocalCharmRepository(repository_path)166 repo = LocalCharmRepository(repository_path)
136 elif url.collection.schema == "cs":167 elif url.collection.schema == "cs":
137 repo = RemoteCharmRepository("https://store.juju.ubuntu.com")168 repo = RemoteCharmRepository("https://store.juju.ubuntu.com")
138 charm = yield repo.find(url)169 return repo, url
139 returnValue((url, charm))
140170
=== modified file 'juju/charm/tests/test_repository.py'
--- juju/charm/tests/test_repository.py 2011-09-30 10:32:01 +0000
+++ juju/charm/tests/test_repository.py 2011-10-05 10:00:46 +0000
@@ -1,3 +1,4 @@
1import json
1import os2import os
2import inspect3import inspect
3import shutil4import shutil
@@ -12,6 +13,7 @@
12from juju.charm.repository import (13from juju.charm.repository import (
13 LocalCharmRepository, RemoteCharmRepository, resolve)14 LocalCharmRepository, RemoteCharmRepository, resolve)
14from juju.charm.url import CharmURL15from juju.charm.url import CharmURL
16from juju.errors import CharmError
15from juju.lib import under17from juju.lib import under
1618
17from juju.charm import tests19from juju.charm import tests
@@ -61,25 +63,37 @@
61 return CharmURL.parse("local:series/" + name)63 return CharmURL.parse("local:series/" + name)
6264
63 @inlineCallbacks65 @inlineCallbacks
64 def test_find_charm_by_name_which_is_unbundled(self):66 def assert_not_there(self, name):
65 charm = yield self.repository1.find(self.charm_url("sample"))67 url = self.charm_url(name)
66 self.assertTrue(charm)68 msg = "Charm 'local:series/%s' not found in repository %s" % (
67 self.assertTrue(charm.metadata)69 name, self.unbundled_repo_path)
6870 err = yield self.assertFailure(
69 def test_find_charm_ignores_unknown_files(self):71 self.repository1.find(url), CharmNotFound)
72 self.assertEquals(str(err), msg)
73 err = yield self.assertFailure(
74 self.repository1.latest(url), CharmNotFound)
75 self.assertEquals(str(err), msg)
76
77 def test_find_inappropriate_url(self):
78 url = CharmURL.parse("cs:foo/bar")
79 err = self.assertRaises(AssertionError, self.repository1.find, url)
80 self.assertEquals(str(err), "schema mismatch")
81
82 def test_completely_missing(self):
83 return self.assert_not_there("zebra")
84
85 def test_unkown_files_ignored(self):
70 self.makeFile(86 self.makeFile(
71 "Foobar",87 "Foobar",
72 path=os.path.join(self.repository1.path, "series", "zebra"))88 path=os.path.join(self.repository1.path, "series", "zebra"))
73 return self.assertFailure(89 return self.assert_not_there("zebra")
74 self.repository1.find(self.charm_url("zebra")), CharmNotFound)
7590
76 def test_find_charm_ignores_unknown_directories(self):91 def test_unknown_directories_ignored(self):
77 self.makeDir(92 self.makeDir(
78 path=os.path.join(self.repository1.path, "series", "zebra"))93 path=os.path.join(self.repository1.path, "series", "zebra"))
79 return self.assertFailure(94 return self.assert_not_there("zebra")
80 self.repository1.find(self.charm_url("zebra")), CharmNotFound)
8195
82 def test_find_charm_ignores_broken_charms(self):96 def test_broken_charms_ignored(self):
83 charm_path = self.makeDir(97 charm_path = self.makeDir(
84 path=os.path.join(self.repository1.path, "series", "zebra"))98 path=os.path.join(self.repository1.path, "series", "zebra"))
85 fh = open(os.path.join(charm_path, "metadata.yaml"), "w+")99 fh = open(os.path.join(charm_path, "metadata.yaml"), "w+")
@@ -90,45 +104,27 @@
90revision: 0104revision: 0
91summary: hola""")105summary: hola""")
92 fh.close()106 fh.close()
93107 return self.assert_not_there("zebra")
94 return self.assertFailure(108
95 self.repository1.find(self.charm_url("zebra")), CharmNotFound)109 def assert_there(self, name, repo, revision, latest_revision=None):
96110 url = self.charm_url(name)
97 @inlineCallbacks111 charm = yield repo.find(url)
98 def test_find_charm_by_name_which_is_bundled(self):112 self.assertEquals(charm.metadata.revision, revision)
99 charm = yield self.repository2.find(self.charm_url("sample"))113 latest = yield repo.latest(url)
100 self.assertTrue(charm)114 self.assertEquals(latest, latest_revision or revision)
101 self.assertTrue(charm.metadata)115
102116 def test_success_unbundled(self):
103 @inlineCallbacks117 return self.assert_there("sample", self.repository1, 2)
104 def test_find_charm_by_name_fails_with_bad_series(self):118 return self.assert_there("sample-2", self.repository1, 2)
105 error = yield self.assertFailure(119
106 self.repository1.find(CharmURL.parse("local:missing/charm")),120 def test_success_bundled(self):
107 CharmNotFound)121 return self.assert_there("sample", self.repository2, 2)
108 self.assertEquals(122 return self.assert_there("sample-2", self.repository2, 2)
109 str(error),123
110 "Charm 'local:missing/charm' not found in repository %s"124 @inlineCallbacks
111 % self.unbundled_repo_path)125 def test_no_revision_gets_latest(self):
112126 yield self.assert_there("sample", self.repository1, 2)
113 def test_find_charm_by_name_fails(self):127
114 d = self.assertFailure(
115 self.repository1.find(self.charm_url("missing")), CharmNotFound)
116
117 def verify(error):
118 self.assertEquals(
119 str(error),
120 "Charm 'local:series/missing' not found in repository %s"
121 % self.unbundled_repo_path)
122 d.addCallback(verify)
123 return d
124
125 @inlineCallbacks
126 def test_find_charm_with_multiple_versions_returns_latest(self):
127 # Copy the repository out of the codebase, so that we can hack it.
128 charm = yield self.repository1.find(self.charm_url("sample"))
129 self.assertEquals(charm.metadata.revision, 2)
130
131 # Invert the "latest" logic, to ensure it's not just a coincidence.
132 file = open(os.path.join(128 file = open(os.path.join(
133 self.repository1.path, "series/old/metadata.yaml"), "rw+")129 self.repository1.path, "series/old/metadata.yaml"), "rw+")
134 data = yaml.load(file.read())130 data = yaml.load(file.read())
@@ -137,18 +133,8 @@
137 file.write(yaml.dump(data))133 file.write(yaml.dump(data))
138 file.close()134 file.close()
139135
140 charm = yield self.repository1.find(self.charm_url("sample"))136 yield self.assert_there("sample", self.repository1, 3)
141 self.assertEquals(charm.metadata.revision, 3)137 yield self.assert_there("sample-2", self.repository1, 2, 3)
142
143 def test_find_inappropriate_url(self):
144 url = CharmURL.parse("cs:foo/bar")
145 err = self.assertRaises(AssertionError, self.repository1.find, url)
146 self.assertEquals(str(err), "schema mismatch")
147
148 def test_find_with_revision(self):
149 url = CharmURL.parse("local:foo/bar-1")
150 err = self.assertRaises(AssertionError, self.repository1.find, url)
151 self.assertEquals(str(err), "find-by-revision not supported yet")
152138
153139
154class RemoteRepositoryTest(RepositoryTestBase):140class RemoteRepositoryTest(RepositoryTestBase):
@@ -157,142 +143,270 @@
157 super(RemoteRepositoryTest, self).setUp()143 super(RemoteRepositoryTest, self).setUp()
158 self.cache_path = os.path.join(144 self.cache_path = os.path.join(
159 tempfile.mkdtemp(), "notexistyet")145 tempfile.mkdtemp(), "notexistyet")
146 self.download_path = os.path.join(self.cache_path, "downloads")
160147
161 def delete():148 def delete():
162 if os.path.exists(self.cache_path):149 if os.path.exists(self.cache_path):
163 shutil.rmtree(self.cache_path)150 shutil.rmtree(self.cache_path)
164 self.addCleanup(delete)151 self.addCleanup(delete)
165152
153 self.charm = CharmDirectory(
154 os.path.join(self.unbundled_repo_path, "series", "dummy"))
155 with open(self.charm.as_bundle().path, "rb") as f:
156 self.bundle_data = f.read()
157 self.sha256 = self.charm.as_bundle().get_sha256()
158 self.getPage = self.mocker.replace("twisted.web.client.getPage")
159 self.downloadPage = self.mocker.replace(
160 "twisted.web.client.downloadPage")
161
166 def repo(self, url_base):162 def repo(self, url_base):
167 return RemoteCharmRepository(url_base, self.cache_path)163 return RemoteCharmRepository(url_base, self.cache_path)
168164
169 @inlineCallbacks165 def cache_location(self, url_str, revision):
170 def assert_find(self, dns_name, url_str, expect_url):166 charm_url = CharmURL.parse(url_str)
171 src_charm = CharmDirectory(167 cache_key = under.quote(
172 os.path.join(self.unbundled_repo_path, "series", "dummy"))168 "%s.charm" % (charm_url.with_revision(revision)))
173 with open(src_charm.as_bundle().path, "rb") as f:169 return os.path.join(self.cache_path, cache_key)
174 bundle_data = f.read()170
175171 def charm_info(self, url_str, revision):
176 download_dir = os.path.join(self.cache_path, "downloads")172 return json.dumps({
177173 url_str: {"revision": revision, "sha256": self.sha256}})
178 downloadPage = self.mocker.replace("twisted.web.client.downloadPage")174
179 downloadPage(expect_url, ANY)175 def mock_charm_info(self, url, result):
180176 self.getPage(url)
181 def download(_, temp_path):177 self.mocker.result(result)
182 self.assertTrue(temp_path.startswith(download_dir))178
183 with open(temp_path, "wb") as f:179 def mock_download(self, url, error=None):
184 f.write(bundle_data)180 self.downloadPage(url, ANY)
181 if error:
182 return self.mocker.result(fail(error))
183
184 def download(_, path):
185 self.assertTrue(path.startswith(self.download_path))
186 with open(path, "wb") as f:
187 f.write(self.bundle_data)
185 return succeed(None)188 return succeed(None)
186 self.mocker.call(download)189 self.mocker.call(download)
187 self.mocker.replay()190
188191 @inlineCallbacks
189 repo = self.repo(dns_name)192 def assert_find_uncached(self, dns_name, url_str, info_url, find_url):
190 charm = yield repo.find(CharmURL.parse(url_str))193 self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1)))
191 self.assertEquals(charm.get_sha256(), src_charm.get_sha256())194 self.mock_download(find_url)
192 self.assertEquals(os.listdir(download_dir), [])195 self.mocker.replay()
193 expect_name = "%s-%s.charm" % (196
194 under.quote(url_str), charm.metadata.revision)197 repo = self.repo(dns_name)
195 self.assertEquals(198 charm = yield repo.find(CharmURL.parse(url_str))
196 charm.path, os.path.join(self.cache_path, expect_name))199 self.assertEquals(charm.get_sha256(), self.sha256)
197200 self.assertEquals(charm.path, self.cache_location(url_str, 1))
198 def test_find_plain(self):201 self.assertEquals(os.listdir(self.download_path), [])
199 return self.assert_find(202
200 "https://somewhe.re", "cs:series/name",203 @inlineCallbacks
201 "https://somewhe.re/charm/series/name")204 def assert_find_cached(self, dns_name, url_str, info_url):
202205 os.makedirs(self.cache_path)
203 def test_find_user(self):206 cache_location = self.cache_location(url_str, 1)
204 return self.assert_find(207 shutil.copy(self.charm.as_bundle().path, cache_location)
205 "https://somewhereel.se", "cs:~user/series/name",208
206 "https://somewhereel.se/charm/~user/series/name")209 self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1)))
207210 self.mocker.replay()
208 def test_cant_find(self):211
209 downloadPage = self.mocker.replace("twisted.web.client.downloadPage")212 repo = self.repo(dns_name)
210 downloadPage("https://anoth.er/charm/series/name", ANY)213 charm = yield repo.find(CharmURL.parse(url_str))
211 self.mocker.result(fail(Error("500")))214 self.assertEquals(charm.get_sha256(), self.sha256)
212 self.mocker.replay()215 self.assertEquals(charm.path, cache_location)
213216
214 repo = RemoteCharmRepository("https://anoth.er")217 def assert_find_error(self, dns_name, url_str, err_type, message):
215 d = self.assertFailure(218 self.mocker.replay()
216 repo.find(CharmURL.parse("cs:series/name")),219 repo = self.repo(dns_name)
217 CharmNotFound)220 d = self.assertFailure(repo.find(CharmURL.parse(url_str)), err_type)
218221
219 def verify(error):222 def verify(error):
220 self.assertEquals(223 self.assertEquals(str(error), message)
221 str(error),224 d.addCallback(verify)
222 "Charm 'cs:series/name' not found in repository "225 return d
223 "https://anoth.er")226
224 d.addCallback(verify)227 @inlineCallbacks
225 return d228 def assert_latest(self, dns_name, url_str, revision):
226229 self.mocker.replay()
227 def test_revision(self):230 repo = self.repo(dns_name)
228 repo = RemoteCharmRepository("whatev.er")231 result = yield repo.latest(CharmURL.parse(url_str))
229 d = self.assertFailure(232 self.assertEquals(result, revision)
230 repo.find(CharmURL.parse("cs:series/name-1")),233
231 AssertionError)234 def assert_latest_error(self, dns_name, url_str, err_type, message):
232235 self.mocker.replay()
233 def verify(error):236 repo = self.repo(dns_name)
234 self.assertEquals(str(error), "find-by-revision not supported yet")237 d = self.assertFailure(repo.latest(CharmURL.parse(url_str)), err_type)
235 d.addCallback(verify)238
236 return d239 def verify(error):
240 self.assertEquals(str(error), message)
241 d.addCallback(verify)
242 return d
243
244 def test_find_plain_uncached(self):
245 return self.assert_find_uncached(
246 "https://somewhe.re", "cs:series/name",
247 "https://somewhe.re/charm-info?charms=cs%3Aseries/name",
248 "https://somewhe.re/charm/series/name-1")
249
250 def test_find_revision_uncached(self):
251 return self.assert_find_uncached(
252 "https://somewhe.re", "cs:series/name-1",
253 "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1",
254 "https://somewhe.re/charm/series/name-1")
255
256 def test_find_user_uncached(self):
257 return self.assert_find_uncached(
258 "https://somewhereel.se", "cs:~user/srs/name",
259 "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name",
260 "https://somewhereel.se/charm/%7Euser/srs/name-1")
261
262 def test_find_plain_cached(self):
263 return self.assert_find_cached(
264 "https://somewhe.re", "cs:series/name",
265 "https://somewhe.re/charm-info?charms=cs%3Aseries/name")
266
267 def test_find_revision_cached(self):
268 return self.assert_find_cached(
269 "https://somewhe.re", "cs:series/name-1",
270 "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1")
271
272 def test_find_user_cached(self):
273 return self.assert_find_cached(
274 "https://somewhereel.se", "cs:~user/srs/name",
275 "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name")
276
277 def test_find_info_error(self):
278 self.mock_charm_info(
279 "https://anoth.er/charm-info?charms=cs%3Aseries/name",
280 fail(Error("500")))
281 return self.assert_find_error(
282 "https://anoth.er", "cs:series/name", CharmNotFound,
283 "Charm 'cs:series/name' not found in repository https://anoth.er")
284
285 def test_find_info_bad_revision(self):
286 self.mock_charm_info(
287 "https://anoth.er/charm-info?charms=cs%3Aseries/name-99",
288 succeed(self.charm_info("cs:series/name-99", 1)))
289 return self.assert_find_error(
290 "https://anoth.er", "cs:series/name-99", AssertionError,
291 "bad url revision")
292
293 def test_find_download_error(self):
294 self.mock_charm_info(
295 "https://anoth.er/charm-info?charms=cs%3Aseries/name",
296 succeed(json.dumps({"cs:series/name": {"revision": 123}})))
297 self.mock_download(
298 "https://anoth.er/charm/series/name-123", Error("999"))
299 return self.assert_find_error(
300 "https://anoth.er", "cs:series/name", CharmNotFound,
301 "Charm 'cs:series/name-123' not found in repository "
302 "https://anoth.er")
303
304 def test_find_charm_revision_mismatch(self):
305 self.mock_charm_info(
306 "https://anoth.er/charm-info?charms=cs%3Aseries/name",
307 succeed(json.dumps({"cs:series/name": {"revision": 99}})))
308 self.mock_download("https://anoth.er/charm/series/name-99")
309 return self.assert_find_error(
310 "https://anoth.er", "cs:series/name", AssertionError,
311 "bad charm revision")
312
313 @inlineCallbacks
314 def test_find_downloaded_hash_mismatch(self):
315 cache_location = self.cache_location("cs:series/name-1", 1)
316 self.mock_charm_info(
317 "https://anoth.er/charm-info?charms=cs%3Aseries/name",
318 succeed(json.dumps(
319 {"cs:series/name": {"revision": 1, "sha256": "NO YUO"}})))
320 self.mock_download("https://anoth.er/charm/series/name-1")
321 yield self.assert_find_error(
322 "https://anoth.er", "cs:series/name", CharmError,
323 "Error processing 'cs:series/name-1 (downloaded)': SHA256 "
324 "mismatch")
325 self.assertFalse(os.path.exists(cache_location))
326
327 @inlineCallbacks
328 def test_find_cached_hash_mismatch(self):
329 os.makedirs(self.cache_path)
330 cache_location = self.cache_location("cs:series/name-1", 1)
331 shutil.copy(self.charm.as_bundle().path, cache_location)
332
333 self.mock_charm_info(
334 "https://anoth.er/charm-info?charms=cs%3Aseries/name",
335 succeed(json.dumps(
336 {"cs:series/name": {"revision": 1, "sha256": "NO YUO"}})))
337 yield self.assert_find_error(
338 "https://anoth.er", "cs:series/name", CharmError,
339 "Error processing 'cs:series/name-1 (cached)': SHA256 mismatch")
340 self.assertFalse(os.path.exists(cache_location))
341
342 def test_latest_plain(self):
343 self.mock_charm_info(
344 "https://somewhe.re/charm-info?charms=cs%3Afoo/bar",
345 succeed(self.charm_info("cs:foo/bar", 99)))
346 return self.assert_latest("https://somewhe.re", "cs:foo/bar", 99)
347
348 def test_latest_user(self):
349 self.mock_charm_info(
350 "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar",
351 succeed(self.charm_info("cs:~fee/foo/bar", 123)))
352 return self.assert_latest(
353 "https://somewhereel.se", "cs:~fee/foo/bar", 123)
354
355 def test_latest_revision(self):
356 self.mock_charm_info(
357 "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar",
358 succeed(self.charm_info("cs:~fee/foo/bar", 123)))
359 return self.assert_latest(
360 "https://somewhereel.se", "cs:~fee/foo/bar-99", 123)
361
362 def test_latest_error(self):
363 self.mock_charm_info(
364 "https://andanoth.er/charm-info?charms=cs%3A%7Eblib/blab/blob",
365 fail(Error("404")))
366 return self.assert_latest_error(
367 "https://andanoth.er", "cs:~blib/blab/blob", CharmNotFound,
368 "Charm 'cs:~blib/blab/blob' not found in repository "
369 "https://andanoth.er")
237370
238371
239class ResolveTest(RepositoryTestBase):372class ResolveTest(RepositoryTestBase):
240373
241 @inlineCallbacks374 def assert_resolve_local(self, vague, default, expect):
242 def test_resolve_local_with_collection(self):375 repo, url = resolve(vague, "/some/path", default)
243 url, charm = yield resolve(376 self.assertEquals(str(url), expect)
244 "local:series/sample", self.unbundled_repo_path, "series")377 self.assertTrue(isinstance(repo, LocalCharmRepository))
245 self.assertEquals(str(url), "local:series/sample")378 self.assertEquals(repo.path, "/some/path")
246 self.assertEquals(charm.metadata.revision, 2)379
247380 def test_resolve_local(self):
248 @inlineCallbacks381 self.assert_resolve_local(
249 def test_resolve_local_no_collection(self):382 "local:series/sample", "default", "local:series/sample")
250 url, charm = yield resolve(383 self.assert_resolve_local(
251 "local:sample", self.unbundled_repo_path, "series")384 "local:sample", "default", "local:default/sample")
252 self.assertEquals(str(url), "local:series/sample")385
253 self.assertEquals(charm.metadata.revision, 2)386 def assert_resolve_remote(self, vague, default, expect):
254387 repo, url = resolve(vague, None, default)
255 @inlineCallbacks388 self.assertEquals(str(url), expect)
256 def test_resolve_local_alternative_collection(self):389 self.assertTrue(isinstance(repo, RemoteCharmRepository))
257 url, charm = yield resolve(390 self.assertEquals(repo.url_base, "https://store.juju.ubuntu.com")
258 "local:series/sample", self.unbundled_repo_path, "otherseries")391
259 self.assertEquals(str(url), "local:series/sample")392 def test_resolve_remote(self):
260 self.assertEquals(charm.metadata.revision, 2)393 self.assert_resolve_remote(
261394 "sample", "default", "cs:default/sample")
262 def assert_not_found(self, name, default_series):395 self.assert_resolve_remote(
263 downloadPage = self.mocker.replace("twisted.web.client.downloadPage")396 "series/sample", "default", "cs:series/sample")
264 downloadPage("https://store.juju.ubuntu.com/charm/series/whatever", ANY)397 self.assert_resolve_remote(
265 self.mocker.result(fail(Error("404")))398 "cs:sample", "default", "cs:default/sample")
266 self.mocker.replay()399 self.assert_resolve_remote(
267400 "cs:series/sample", "default", "cs:series/sample")
268 d = self.assertFailure(401 self.assert_resolve_remote(
269 resolve(name, None, default_series), CharmNotFound)402 "cs:~user/sample", "default", "cs:~user/default/sample")
270403 self.assert_resolve_remote(
271 def verify(error):404 "cs:~user/series/sample", "default", "cs:~user/series/sample")
272 self.assertEquals(405
273 str(error),406 def test_resolve_nonsense(self):
274 "Charm 'cs:series/whatever' not found in repository "407 error = self.assertRaises(
275 "https://store.juju.ubuntu.com")408 CharmURLError, resolve, "blah:whatever", None, "series")
276 d.addCallback(verify)409 self.assertEquals(
277 return d410 str(error),
278411 "Bad charm URL 'blah:series/whatever': invalid schema (URL "
279 def test_resolve_minimal(self):412 "inferred from 'blah:whatever')")
280 return self.assert_not_found("whatever", "series")
281
282 def test_resolve_no_root(self):
283 return self.assert_not_found("series/whatever", "series")
284
285 def test_resolve_with_root(self):
286 return self.assert_not_found("cs:series/whatever", "series")
287
288 def test_resolve_nonsense_root(self):
289 d = self.assertFailure(
290 resolve("blah:whatever", None, "series"), CharmURLError)
291
292 def verify(error):
293 self.assertEquals(
294 str(error),
295 "Bad charm URL 'blah:series/whatever': invalid schema (URL "
296 "inferred from 'blah:whatever')")
297 d.addCallback(verify)
298 return d
299413
=== modified file 'juju/charm/tests/test_url.py'
--- juju/charm/tests/test_url.py 2011-09-29 03:07:26 +0000
+++ juju/charm/tests/test_url.py 2011-10-05 10:00:46 +0000
@@ -29,6 +29,7 @@
29 url = CharmURL.parse(string)29 url = CharmURL.parse(string)
30 self.assert_url(url, schema, user, series, name, rev)30 self.assert_url(url, schema, user, series, name, rev)
31 self.assertEquals(str(url), string)31 self.assertEquals(str(url), string)
32 self.assertEquals(url.path, string.split(":", 1)[1])
3233
33 def test_parse(self):34 def test_parse(self):
34 self.assert_parse(35 self.assert_parse(
3536
=== modified file 'juju/charm/url.py'
--- juju/charm/url.py 2011-09-29 03:07:26 +0000
+++ juju/charm/url.py 2011-10-05 10:00:46 +0000
@@ -56,6 +56,10 @@
56 return "%s/%s" % (self.collection, self.name)56 return "%s/%s" % (self.collection, self.name)
57 return "%s/%s-%s" % (self.collection, self.name, self.revision)57 return "%s/%s-%s" % (self.collection, self.name, self.revision)
5858
59 @property
60 def path(self):
61 return str(self).split(":", 1)[1]
62
59 def with_revision(self, revision):63 def with_revision(self, revision):
60 other = copy.deepcopy(self)64 other = copy.deepcopy(self)
61 other.revision = revision65 other.revision = revision
6266
=== modified file 'juju/control/deploy.py'
--- juju/control/deploy.py 2011-09-30 09:48:25 +0000
+++ juju/control/deploy.py 2011-10-05 10:00:46 +0000
@@ -81,12 +81,11 @@
81 service_name, log, config_file=None):81 service_name, log, config_file=None):
82 """Deploy a charm within an environment.82 """Deploy a charm within an environment.
8383
84
85 This will publish the charm to the environment, creating84 This will publish the charm to the environment, creating
86 a service from the charm, and get it set to be launched85 a service from the charm, and get it set to be launched
87 on a new machine.86 on a new machine.
88 """87 """
89 charm_url, charm = yield resolve(88 repo, charm_url = resolve(
90 charm_name, repository_path, environment.default_series)89 charm_name, repository_path, environment.default_series)
9190
92 # Validate config options prior to deployment attempt91 # Validate config options prior to deployment attempt
@@ -95,6 +94,9 @@
95 if config_file:94 if config_file:
96 service_options = parse_config_options(config_file, service_name)95 service_options = parse_config_options(config_file, service_name)
9796
97 charm = yield repo.find(charm_url)
98 charm_id = str(charm_url.with_revision(charm.metadata.revision))
99
98 provider = environment.get_machine_provider()100 provider = environment.get_machine_provider()
99 placement_policy = provider.get_placement_policy()101 placement_policy = provider.get_placement_policy()
100 client = yield provider.connect()102 client = yield provider.connect()
@@ -108,7 +110,6 @@
108110
109 # Publish the charm to juju111 # Publish the charm to juju
110 publisher = CharmPublisher(client, storage)112 publisher = CharmPublisher(client, storage)
111 charm_id = str(charm_url.with_revision(charm.metadata.revision))
112 yield publisher.add_charm(charm_id, charm)113 yield publisher.add_charm(charm_id, charm)
113 result = yield publisher.publish()114 result = yield publisher.publish()
114115
115116
=== modified file 'juju/control/upgrade_charm.py'
--- juju/control/upgrade_charm.py 2011-09-29 03:07:26 +0000
+++ juju/control/upgrade_charm.py 2011-10-05 10:00:46 +0000
@@ -64,11 +64,12 @@
6464
65 old_charm_url = CharmURL.parse(old_charm_id)65 old_charm_url = CharmURL.parse(old_charm_id)
66 old_charm_url.assert_revision()66 old_charm_url.assert_revision()
67 charm_url, charm = yield resolve(67 repo, charm_url = resolve(
68 str(old_charm_url.with_revision(None)),68 str(old_charm_url.with_revision(None)),
69 repository_path,69 repository_path,
70 environment.default_series)70 environment.default_series)
71 new_charm_url = charm_url.with_revision(charm.metadata.revision)71 new_charm_url = charm_url.with_revision(
72 (yield repo.latest(charm_url)))
72 new_charm_id = str(new_charm_url)73 new_charm_id = str(new_charm_url)
7374
74 # Verify its newer than what's deployed75 # Verify its newer than what's deployed
@@ -86,6 +87,7 @@
86 # Publish the new charm87 # Publish the new charm
87 storage = provider.get_file_storage()88 storage = provider.get_file_storage()
88 publisher = CharmPublisher(client, storage)89 publisher = CharmPublisher(client, storage)
90 charm = yield repo.find(charm_url)
89 yield publisher.add_charm(new_charm_id, charm)91 yield publisher.add_charm(new_charm_id, charm)
90 result = yield publisher.publish()92 result = yield publisher.publish()
91 charm_state = result[0]93 charm_state = result[0]

Subscribers

People subscribed via source and target branches

to status/vote changes: