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
1=== modified file 'juju/charm/repository.py'
2--- juju/charm/repository.py 2011-10-03 20:04:45 +0000
3+++ juju/charm/repository.py 2011-10-05 10:00:46 +0000
4@@ -1,9 +1,11 @@
5+import json
6 import os
7 import tempfile
8+import urllib
9 import yaml
10
11 from twisted.internet.defer import fail, inlineCallbacks, returnValue, succeed
12-from twisted.web.client import downloadPage
13+from twisted.web.client import downloadPage, getPage
14 from twisted.web.error import Error
15
16 from juju.charm.provider import get_charm_from_path
17@@ -21,6 +23,11 @@
18 pass
19
20
21+def _cache_key(charm_url):
22+ charm_url.assert_revision()
23+ return under.quote("%s.charm" % charm_url)
24+
25+
26 class LocalCharmRepository(object):
27 """Charm repository in a local directory."""
28
29@@ -50,11 +57,12 @@
30 recent one (greatest revision) will be returned.
31 """
32 assert charm_url.collection.schema == "local", "schema mismatch"
33- assert charm_url.revision is None, "find-by-revision not supported yet"
34
35 latest = None
36 for charm in self._collection(charm_url.collection):
37 if charm.metadata.name == charm_url.name:
38+ if charm.metadata.revision == charm_url.revision:
39+ return succeed(charm)
40 if (latest is None or
41 latest.metadata.revision < charm.metadata.revision):
42 latest = charm
43@@ -64,6 +72,11 @@
44
45 return succeed(latest)
46
47+ def latest(self, charm_url):
48+ d = self.find(charm_url.with_revision(None))
49+ d.addCallback(lambda c: c.metadata.revision)
50+ return d
51+
52
53 class RemoteCharmRepository(object):
54
55@@ -75,42 +88,60 @@
56 self.cache_path = cache_path
57
58 @inlineCallbacks
59- def _download(self, url):
60+ def _get_info(self, charm_url):
61+ url = "%s/charm-info?charms=%s" % (
62+ self.url_base, urllib.quote(str(charm_url)))
63+ try:
64+ data = yield getPage(url)
65+ returnValue(json.loads(data)[str(charm_url)])
66+ except Error:
67+ raise CharmNotFound(self.url_base, charm_url)
68+
69+ @inlineCallbacks
70+ def _download(self, charm_url, cache_path):
71+ url = "%s/charm/%s" % (self.url_base, urllib.quote(charm_url.path))
72 downloads = os.path.join(self.cache_path, "downloads")
73 _makedirs(downloads)
74 f = tempfile.NamedTemporaryFile(
75- prefix=under.quote(url), suffix=".charm", dir=downloads,
76+ prefix=_cache_key(charm_url), suffix=".part", dir=downloads,
77 delete=False)
78 f.close()
79- completed_path = f.name
80- downloading_path = completed_path + ".part"
81- yield downloadPage(url, downloading_path)
82- os.rename(downloading_path, completed_path)
83- returnValue(completed_path)
84-
85- def _cache(self, charm_url_base, temp_charm_path):
86- temp_charm = get_charm_from_path(temp_charm_path)
87- revision = temp_charm.metadata.revision
88- charm_key = under.quote(
89- "%s.charm" % (charm_url_base.with_revision(revision)))
90- charm_path = os.path.join(self.cache_path, charm_key)
91- _makedirs(self.cache_path)
92- os.rename(temp_charm_path, charm_path)
93- return get_charm_from_path(charm_path)
94-
95- @inlineCallbacks
96- def find(self, charm_url):
97- assert charm_url.revision is None, "find-by-revision not supported yet"
98- _, path = str(charm_url).split(":", 1)
99- url = "%s/charm/%s" % (self.url_base, path)
100+ downloading_path = f.name
101 try:
102- temp_charm_path = yield self._download(url)
103+ yield downloadPage(url, downloading_path)
104 except Error:
105 raise CharmNotFound(self.url_base, charm_url)
106- returnValue(self._cache(charm_url, temp_charm_path))
107-
108-
109-@inlineCallbacks
110+ os.rename(downloading_path, cache_path)
111+
112+ @inlineCallbacks
113+ def find(self, charm_url):
114+ info = yield self._get_info(charm_url)
115+ revision = info["revision"]
116+ if charm_url.revision is None:
117+ charm_url = charm_url.with_revision(revision)
118+ else:
119+ assert revision == charm_url.revision, "bad url revision"
120+
121+ cache_path = os.path.join(self.cache_path, _cache_key(charm_url))
122+ cached = os.path.exists(cache_path)
123+ if not cached:
124+ yield self._download(charm_url, cache_path)
125+ charm = get_charm_from_path(cache_path)
126+
127+ assert charm.metadata.revision == revision, "bad charm revision"
128+ if charm.get_sha256() != info["sha256"]:
129+ os.remove(cache_path)
130+ name = "%s (%s)" % (
131+ charm_url, "cached" if cached else "downloaded")
132+ raise CharmError(name, "SHA256 mismatch")
133+ returnValue(charm)
134+
135+ @inlineCallbacks
136+ def latest(self, charm_url):
137+ info = yield self._get_info(charm_url.with_revision(None))
138+ returnValue(info["revision"])
139+
140+
141 def resolve(vague_name, repository_path, default_series):
142 """Get a Charm and associated identifying information
143
144@@ -125,7 +156,7 @@
145 :param str default_series: the Ubuntu series to insert when `charm_name` is
146 inadequately specified.
147
148- :return: a tuple of a :class:`CharmURL` and a
149+ :return: a tuple of a :class:`juju.charm.url.CharmURL` and a
150 :class:`juju.charm.base.CharmBase` subclass, which together contain
151 both the charm's data and all information necessary to specify its
152 source.
153@@ -135,5 +166,4 @@
154 repo = LocalCharmRepository(repository_path)
155 elif url.collection.schema == "cs":
156 repo = RemoteCharmRepository("https://store.juju.ubuntu.com")
157- charm = yield repo.find(url)
158- returnValue((url, charm))
159+ return repo, url
160
161=== modified file 'juju/charm/tests/test_repository.py'
162--- juju/charm/tests/test_repository.py 2011-09-30 10:32:01 +0000
163+++ juju/charm/tests/test_repository.py 2011-10-05 10:00:46 +0000
164@@ -1,3 +1,4 @@
165+import json
166 import os
167 import inspect
168 import shutil
169@@ -12,6 +13,7 @@
170 from juju.charm.repository import (
171 LocalCharmRepository, RemoteCharmRepository, resolve)
172 from juju.charm.url import CharmURL
173+from juju.errors import CharmError
174 from juju.lib import under
175
176 from juju.charm import tests
177@@ -61,25 +63,37 @@
178 return CharmURL.parse("local:series/" + name)
179
180 @inlineCallbacks
181- def test_find_charm_by_name_which_is_unbundled(self):
182- charm = yield self.repository1.find(self.charm_url("sample"))
183- self.assertTrue(charm)
184- self.assertTrue(charm.metadata)
185-
186- def test_find_charm_ignores_unknown_files(self):
187+ def assert_not_there(self, name):
188+ url = self.charm_url(name)
189+ msg = "Charm 'local:series/%s' not found in repository %s" % (
190+ name, self.unbundled_repo_path)
191+ err = yield self.assertFailure(
192+ self.repository1.find(url), CharmNotFound)
193+ self.assertEquals(str(err), msg)
194+ err = yield self.assertFailure(
195+ self.repository1.latest(url), CharmNotFound)
196+ self.assertEquals(str(err), msg)
197+
198+ def test_find_inappropriate_url(self):
199+ url = CharmURL.parse("cs:foo/bar")
200+ err = self.assertRaises(AssertionError, self.repository1.find, url)
201+ self.assertEquals(str(err), "schema mismatch")
202+
203+ def test_completely_missing(self):
204+ return self.assert_not_there("zebra")
205+
206+ def test_unkown_files_ignored(self):
207 self.makeFile(
208 "Foobar",
209 path=os.path.join(self.repository1.path, "series", "zebra"))
210- return self.assertFailure(
211- self.repository1.find(self.charm_url("zebra")), CharmNotFound)
212+ return self.assert_not_there("zebra")
213
214- def test_find_charm_ignores_unknown_directories(self):
215+ def test_unknown_directories_ignored(self):
216 self.makeDir(
217 path=os.path.join(self.repository1.path, "series", "zebra"))
218- return self.assertFailure(
219- self.repository1.find(self.charm_url("zebra")), CharmNotFound)
220+ return self.assert_not_there("zebra")
221
222- def test_find_charm_ignores_broken_charms(self):
223+ def test_broken_charms_ignored(self):
224 charm_path = self.makeDir(
225 path=os.path.join(self.repository1.path, "series", "zebra"))
226 fh = open(os.path.join(charm_path, "metadata.yaml"), "w+")
227@@ -90,45 +104,27 @@
228 revision: 0
229 summary: hola""")
230 fh.close()
231-
232- return self.assertFailure(
233- self.repository1.find(self.charm_url("zebra")), CharmNotFound)
234-
235- @inlineCallbacks
236- def test_find_charm_by_name_which_is_bundled(self):
237- charm = yield self.repository2.find(self.charm_url("sample"))
238- self.assertTrue(charm)
239- self.assertTrue(charm.metadata)
240-
241- @inlineCallbacks
242- def test_find_charm_by_name_fails_with_bad_series(self):
243- error = yield self.assertFailure(
244- self.repository1.find(CharmURL.parse("local:missing/charm")),
245- CharmNotFound)
246- self.assertEquals(
247- str(error),
248- "Charm 'local:missing/charm' not found in repository %s"
249- % self.unbundled_repo_path)
250-
251- def test_find_charm_by_name_fails(self):
252- d = self.assertFailure(
253- self.repository1.find(self.charm_url("missing")), CharmNotFound)
254-
255- def verify(error):
256- self.assertEquals(
257- str(error),
258- "Charm 'local:series/missing' not found in repository %s"
259- % self.unbundled_repo_path)
260- d.addCallback(verify)
261- return d
262-
263- @inlineCallbacks
264- def test_find_charm_with_multiple_versions_returns_latest(self):
265- # Copy the repository out of the codebase, so that we can hack it.
266- charm = yield self.repository1.find(self.charm_url("sample"))
267- self.assertEquals(charm.metadata.revision, 2)
268-
269- # Invert the "latest" logic, to ensure it's not just a coincidence.
270+ return self.assert_not_there("zebra")
271+
272+ def assert_there(self, name, repo, revision, latest_revision=None):
273+ url = self.charm_url(name)
274+ charm = yield repo.find(url)
275+ self.assertEquals(charm.metadata.revision, revision)
276+ latest = yield repo.latest(url)
277+ self.assertEquals(latest, latest_revision or revision)
278+
279+ def test_success_unbundled(self):
280+ return self.assert_there("sample", self.repository1, 2)
281+ return self.assert_there("sample-2", self.repository1, 2)
282+
283+ def test_success_bundled(self):
284+ return self.assert_there("sample", self.repository2, 2)
285+ return self.assert_there("sample-2", self.repository2, 2)
286+
287+ @inlineCallbacks
288+ def test_no_revision_gets_latest(self):
289+ yield self.assert_there("sample", self.repository1, 2)
290+
291 file = open(os.path.join(
292 self.repository1.path, "series/old/metadata.yaml"), "rw+")
293 data = yaml.load(file.read())
294@@ -137,18 +133,8 @@
295 file.write(yaml.dump(data))
296 file.close()
297
298- charm = yield self.repository1.find(self.charm_url("sample"))
299- self.assertEquals(charm.metadata.revision, 3)
300-
301- def test_find_inappropriate_url(self):
302- url = CharmURL.parse("cs:foo/bar")
303- err = self.assertRaises(AssertionError, self.repository1.find, url)
304- self.assertEquals(str(err), "schema mismatch")
305-
306- def test_find_with_revision(self):
307- url = CharmURL.parse("local:foo/bar-1")
308- err = self.assertRaises(AssertionError, self.repository1.find, url)
309- self.assertEquals(str(err), "find-by-revision not supported yet")
310+ yield self.assert_there("sample", self.repository1, 3)
311+ yield self.assert_there("sample-2", self.repository1, 2, 3)
312
313
314 class RemoteRepositoryTest(RepositoryTestBase):
315@@ -157,142 +143,270 @@
316 super(RemoteRepositoryTest, self).setUp()
317 self.cache_path = os.path.join(
318 tempfile.mkdtemp(), "notexistyet")
319+ self.download_path = os.path.join(self.cache_path, "downloads")
320
321 def delete():
322 if os.path.exists(self.cache_path):
323 shutil.rmtree(self.cache_path)
324 self.addCleanup(delete)
325
326+ self.charm = CharmDirectory(
327+ os.path.join(self.unbundled_repo_path, "series", "dummy"))
328+ with open(self.charm.as_bundle().path, "rb") as f:
329+ self.bundle_data = f.read()
330+ self.sha256 = self.charm.as_bundle().get_sha256()
331+ self.getPage = self.mocker.replace("twisted.web.client.getPage")
332+ self.downloadPage = self.mocker.replace(
333+ "twisted.web.client.downloadPage")
334+
335 def repo(self, url_base):
336 return RemoteCharmRepository(url_base, self.cache_path)
337
338- @inlineCallbacks
339- def assert_find(self, dns_name, url_str, expect_url):
340- src_charm = CharmDirectory(
341- os.path.join(self.unbundled_repo_path, "series", "dummy"))
342- with open(src_charm.as_bundle().path, "rb") as f:
343- bundle_data = f.read()
344-
345- download_dir = os.path.join(self.cache_path, "downloads")
346-
347- downloadPage = self.mocker.replace("twisted.web.client.downloadPage")
348- downloadPage(expect_url, ANY)
349-
350- def download(_, temp_path):
351- self.assertTrue(temp_path.startswith(download_dir))
352- with open(temp_path, "wb") as f:
353- f.write(bundle_data)
354+ def cache_location(self, url_str, revision):
355+ charm_url = CharmURL.parse(url_str)
356+ cache_key = under.quote(
357+ "%s.charm" % (charm_url.with_revision(revision)))
358+ return os.path.join(self.cache_path, cache_key)
359+
360+ def charm_info(self, url_str, revision):
361+ return json.dumps({
362+ url_str: {"revision": revision, "sha256": self.sha256}})
363+
364+ def mock_charm_info(self, url, result):
365+ self.getPage(url)
366+ self.mocker.result(result)
367+
368+ def mock_download(self, url, error=None):
369+ self.downloadPage(url, ANY)
370+ if error:
371+ return self.mocker.result(fail(error))
372+
373+ def download(_, path):
374+ self.assertTrue(path.startswith(self.download_path))
375+ with open(path, "wb") as f:
376+ f.write(self.bundle_data)
377 return succeed(None)
378 self.mocker.call(download)
379- self.mocker.replay()
380-
381- repo = self.repo(dns_name)
382- charm = yield repo.find(CharmURL.parse(url_str))
383- self.assertEquals(charm.get_sha256(), src_charm.get_sha256())
384- self.assertEquals(os.listdir(download_dir), [])
385- expect_name = "%s-%s.charm" % (
386- under.quote(url_str), charm.metadata.revision)
387- self.assertEquals(
388- charm.path, os.path.join(self.cache_path, expect_name))
389-
390- def test_find_plain(self):
391- return self.assert_find(
392- "https://somewhe.re", "cs:series/name",
393- "https://somewhe.re/charm/series/name")
394-
395- def test_find_user(self):
396- return self.assert_find(
397- "https://somewhereel.se", "cs:~user/series/name",
398- "https://somewhereel.se/charm/~user/series/name")
399-
400- def test_cant_find(self):
401- downloadPage = self.mocker.replace("twisted.web.client.downloadPage")
402- downloadPage("https://anoth.er/charm/series/name", ANY)
403- self.mocker.result(fail(Error("500")))
404- self.mocker.replay()
405-
406- repo = RemoteCharmRepository("https://anoth.er")
407- d = self.assertFailure(
408- repo.find(CharmURL.parse("cs:series/name")),
409- CharmNotFound)
410-
411- def verify(error):
412- self.assertEquals(
413- str(error),
414- "Charm 'cs:series/name' not found in repository "
415- "https://anoth.er")
416- d.addCallback(verify)
417- return d
418-
419- def test_revision(self):
420- repo = RemoteCharmRepository("whatev.er")
421- d = self.assertFailure(
422- repo.find(CharmURL.parse("cs:series/name-1")),
423- AssertionError)
424-
425- def verify(error):
426- self.assertEquals(str(error), "find-by-revision not supported yet")
427- d.addCallback(verify)
428- return d
429+
430+ @inlineCallbacks
431+ def assert_find_uncached(self, dns_name, url_str, info_url, find_url):
432+ self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1)))
433+ self.mock_download(find_url)
434+ self.mocker.replay()
435+
436+ repo = self.repo(dns_name)
437+ charm = yield repo.find(CharmURL.parse(url_str))
438+ self.assertEquals(charm.get_sha256(), self.sha256)
439+ self.assertEquals(charm.path, self.cache_location(url_str, 1))
440+ self.assertEquals(os.listdir(self.download_path), [])
441+
442+ @inlineCallbacks
443+ def assert_find_cached(self, dns_name, url_str, info_url):
444+ os.makedirs(self.cache_path)
445+ cache_location = self.cache_location(url_str, 1)
446+ shutil.copy(self.charm.as_bundle().path, cache_location)
447+
448+ self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1)))
449+ self.mocker.replay()
450+
451+ repo = self.repo(dns_name)
452+ charm = yield repo.find(CharmURL.parse(url_str))
453+ self.assertEquals(charm.get_sha256(), self.sha256)
454+ self.assertEquals(charm.path, cache_location)
455+
456+ def assert_find_error(self, dns_name, url_str, err_type, message):
457+ self.mocker.replay()
458+ repo = self.repo(dns_name)
459+ d = self.assertFailure(repo.find(CharmURL.parse(url_str)), err_type)
460+
461+ def verify(error):
462+ self.assertEquals(str(error), message)
463+ d.addCallback(verify)
464+ return d
465+
466+ @inlineCallbacks
467+ def assert_latest(self, dns_name, url_str, revision):
468+ self.mocker.replay()
469+ repo = self.repo(dns_name)
470+ result = yield repo.latest(CharmURL.parse(url_str))
471+ self.assertEquals(result, revision)
472+
473+ def assert_latest_error(self, dns_name, url_str, err_type, message):
474+ self.mocker.replay()
475+ repo = self.repo(dns_name)
476+ d = self.assertFailure(repo.latest(CharmURL.parse(url_str)), err_type)
477+
478+ def verify(error):
479+ self.assertEquals(str(error), message)
480+ d.addCallback(verify)
481+ return d
482+
483+ def test_find_plain_uncached(self):
484+ return self.assert_find_uncached(
485+ "https://somewhe.re", "cs:series/name",
486+ "https://somewhe.re/charm-info?charms=cs%3Aseries/name",
487+ "https://somewhe.re/charm/series/name-1")
488+
489+ def test_find_revision_uncached(self):
490+ return self.assert_find_uncached(
491+ "https://somewhe.re", "cs:series/name-1",
492+ "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1",
493+ "https://somewhe.re/charm/series/name-1")
494+
495+ def test_find_user_uncached(self):
496+ return self.assert_find_uncached(
497+ "https://somewhereel.se", "cs:~user/srs/name",
498+ "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name",
499+ "https://somewhereel.se/charm/%7Euser/srs/name-1")
500+
501+ def test_find_plain_cached(self):
502+ return self.assert_find_cached(
503+ "https://somewhe.re", "cs:series/name",
504+ "https://somewhe.re/charm-info?charms=cs%3Aseries/name")
505+
506+ def test_find_revision_cached(self):
507+ return self.assert_find_cached(
508+ "https://somewhe.re", "cs:series/name-1",
509+ "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1")
510+
511+ def test_find_user_cached(self):
512+ return self.assert_find_cached(
513+ "https://somewhereel.se", "cs:~user/srs/name",
514+ "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name")
515+
516+ def test_find_info_error(self):
517+ self.mock_charm_info(
518+ "https://anoth.er/charm-info?charms=cs%3Aseries/name",
519+ fail(Error("500")))
520+ return self.assert_find_error(
521+ "https://anoth.er", "cs:series/name", CharmNotFound,
522+ "Charm 'cs:series/name' not found in repository https://anoth.er")
523+
524+ def test_find_info_bad_revision(self):
525+ self.mock_charm_info(
526+ "https://anoth.er/charm-info?charms=cs%3Aseries/name-99",
527+ succeed(self.charm_info("cs:series/name-99", 1)))
528+ return self.assert_find_error(
529+ "https://anoth.er", "cs:series/name-99", AssertionError,
530+ "bad url revision")
531+
532+ def test_find_download_error(self):
533+ self.mock_charm_info(
534+ "https://anoth.er/charm-info?charms=cs%3Aseries/name",
535+ succeed(json.dumps({"cs:series/name": {"revision": 123}})))
536+ self.mock_download(
537+ "https://anoth.er/charm/series/name-123", Error("999"))
538+ return self.assert_find_error(
539+ "https://anoth.er", "cs:series/name", CharmNotFound,
540+ "Charm 'cs:series/name-123' not found in repository "
541+ "https://anoth.er")
542+
543+ def test_find_charm_revision_mismatch(self):
544+ self.mock_charm_info(
545+ "https://anoth.er/charm-info?charms=cs%3Aseries/name",
546+ succeed(json.dumps({"cs:series/name": {"revision": 99}})))
547+ self.mock_download("https://anoth.er/charm/series/name-99")
548+ return self.assert_find_error(
549+ "https://anoth.er", "cs:series/name", AssertionError,
550+ "bad charm revision")
551+
552+ @inlineCallbacks
553+ def test_find_downloaded_hash_mismatch(self):
554+ cache_location = self.cache_location("cs:series/name-1", 1)
555+ self.mock_charm_info(
556+ "https://anoth.er/charm-info?charms=cs%3Aseries/name",
557+ succeed(json.dumps(
558+ {"cs:series/name": {"revision": 1, "sha256": "NO YUO"}})))
559+ self.mock_download("https://anoth.er/charm/series/name-1")
560+ yield self.assert_find_error(
561+ "https://anoth.er", "cs:series/name", CharmError,
562+ "Error processing 'cs:series/name-1 (downloaded)': SHA256 "
563+ "mismatch")
564+ self.assertFalse(os.path.exists(cache_location))
565+
566+ @inlineCallbacks
567+ def test_find_cached_hash_mismatch(self):
568+ os.makedirs(self.cache_path)
569+ cache_location = self.cache_location("cs:series/name-1", 1)
570+ shutil.copy(self.charm.as_bundle().path, cache_location)
571+
572+ self.mock_charm_info(
573+ "https://anoth.er/charm-info?charms=cs%3Aseries/name",
574+ succeed(json.dumps(
575+ {"cs:series/name": {"revision": 1, "sha256": "NO YUO"}})))
576+ yield self.assert_find_error(
577+ "https://anoth.er", "cs:series/name", CharmError,
578+ "Error processing 'cs:series/name-1 (cached)': SHA256 mismatch")
579+ self.assertFalse(os.path.exists(cache_location))
580+
581+ def test_latest_plain(self):
582+ self.mock_charm_info(
583+ "https://somewhe.re/charm-info?charms=cs%3Afoo/bar",
584+ succeed(self.charm_info("cs:foo/bar", 99)))
585+ return self.assert_latest("https://somewhe.re", "cs:foo/bar", 99)
586+
587+ def test_latest_user(self):
588+ self.mock_charm_info(
589+ "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar",
590+ succeed(self.charm_info("cs:~fee/foo/bar", 123)))
591+ return self.assert_latest(
592+ "https://somewhereel.se", "cs:~fee/foo/bar", 123)
593+
594+ def test_latest_revision(self):
595+ self.mock_charm_info(
596+ "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar",
597+ succeed(self.charm_info("cs:~fee/foo/bar", 123)))
598+ return self.assert_latest(
599+ "https://somewhereel.se", "cs:~fee/foo/bar-99", 123)
600+
601+ def test_latest_error(self):
602+ self.mock_charm_info(
603+ "https://andanoth.er/charm-info?charms=cs%3A%7Eblib/blab/blob",
604+ fail(Error("404")))
605+ return self.assert_latest_error(
606+ "https://andanoth.er", "cs:~blib/blab/blob", CharmNotFound,
607+ "Charm 'cs:~blib/blab/blob' not found in repository "
608+ "https://andanoth.er")
609
610
611 class ResolveTest(RepositoryTestBase):
612
613- @inlineCallbacks
614- def test_resolve_local_with_collection(self):
615- url, charm = yield resolve(
616- "local:series/sample", self.unbundled_repo_path, "series")
617- self.assertEquals(str(url), "local:series/sample")
618- self.assertEquals(charm.metadata.revision, 2)
619-
620- @inlineCallbacks
621- def test_resolve_local_no_collection(self):
622- url, charm = yield resolve(
623- "local:sample", self.unbundled_repo_path, "series")
624- self.assertEquals(str(url), "local:series/sample")
625- self.assertEquals(charm.metadata.revision, 2)
626-
627- @inlineCallbacks
628- def test_resolve_local_alternative_collection(self):
629- url, charm = yield resolve(
630- "local:series/sample", self.unbundled_repo_path, "otherseries")
631- self.assertEquals(str(url), "local:series/sample")
632- self.assertEquals(charm.metadata.revision, 2)
633-
634- def assert_not_found(self, name, default_series):
635- downloadPage = self.mocker.replace("twisted.web.client.downloadPage")
636- downloadPage("https://store.juju.ubuntu.com/charm/series/whatever", ANY)
637- self.mocker.result(fail(Error("404")))
638- self.mocker.replay()
639-
640- d = self.assertFailure(
641- resolve(name, None, default_series), CharmNotFound)
642-
643- def verify(error):
644- self.assertEquals(
645- str(error),
646- "Charm 'cs:series/whatever' not found in repository "
647- "https://store.juju.ubuntu.com")
648- d.addCallback(verify)
649- return d
650-
651- def test_resolve_minimal(self):
652- return self.assert_not_found("whatever", "series")
653-
654- def test_resolve_no_root(self):
655- return self.assert_not_found("series/whatever", "series")
656-
657- def test_resolve_with_root(self):
658- return self.assert_not_found("cs:series/whatever", "series")
659-
660- def test_resolve_nonsense_root(self):
661- d = self.assertFailure(
662- resolve("blah:whatever", None, "series"), CharmURLError)
663-
664- def verify(error):
665- self.assertEquals(
666- str(error),
667- "Bad charm URL 'blah:series/whatever': invalid schema (URL "
668- "inferred from 'blah:whatever')")
669- d.addCallback(verify)
670- return d
671+ def assert_resolve_local(self, vague, default, expect):
672+ repo, url = resolve(vague, "/some/path", default)
673+ self.assertEquals(str(url), expect)
674+ self.assertTrue(isinstance(repo, LocalCharmRepository))
675+ self.assertEquals(repo.path, "/some/path")
676+
677+ def test_resolve_local(self):
678+ self.assert_resolve_local(
679+ "local:series/sample", "default", "local:series/sample")
680+ self.assert_resolve_local(
681+ "local:sample", "default", "local:default/sample")
682+
683+ def assert_resolve_remote(self, vague, default, expect):
684+ repo, url = resolve(vague, None, default)
685+ self.assertEquals(str(url), expect)
686+ self.assertTrue(isinstance(repo, RemoteCharmRepository))
687+ self.assertEquals(repo.url_base, "https://store.juju.ubuntu.com")
688+
689+ def test_resolve_remote(self):
690+ self.assert_resolve_remote(
691+ "sample", "default", "cs:default/sample")
692+ self.assert_resolve_remote(
693+ "series/sample", "default", "cs:series/sample")
694+ self.assert_resolve_remote(
695+ "cs:sample", "default", "cs:default/sample")
696+ self.assert_resolve_remote(
697+ "cs:series/sample", "default", "cs:series/sample")
698+ self.assert_resolve_remote(
699+ "cs:~user/sample", "default", "cs:~user/default/sample")
700+ self.assert_resolve_remote(
701+ "cs:~user/series/sample", "default", "cs:~user/series/sample")
702+
703+ def test_resolve_nonsense(self):
704+ error = self.assertRaises(
705+ CharmURLError, resolve, "blah:whatever", None, "series")
706+ self.assertEquals(
707+ str(error),
708+ "Bad charm URL 'blah:series/whatever': invalid schema (URL "
709+ "inferred from 'blah:whatever')")
710
711=== modified file 'juju/charm/tests/test_url.py'
712--- juju/charm/tests/test_url.py 2011-09-29 03:07:26 +0000
713+++ juju/charm/tests/test_url.py 2011-10-05 10:00:46 +0000
714@@ -29,6 +29,7 @@
715 url = CharmURL.parse(string)
716 self.assert_url(url, schema, user, series, name, rev)
717 self.assertEquals(str(url), string)
718+ self.assertEquals(url.path, string.split(":", 1)[1])
719
720 def test_parse(self):
721 self.assert_parse(
722
723=== modified file 'juju/charm/url.py'
724--- juju/charm/url.py 2011-09-29 03:07:26 +0000
725+++ juju/charm/url.py 2011-10-05 10:00:46 +0000
726@@ -56,6 +56,10 @@
727 return "%s/%s" % (self.collection, self.name)
728 return "%s/%s-%s" % (self.collection, self.name, self.revision)
729
730+ @property
731+ def path(self):
732+ return str(self).split(":", 1)[1]
733+
734 def with_revision(self, revision):
735 other = copy.deepcopy(self)
736 other.revision = revision
737
738=== modified file 'juju/control/deploy.py'
739--- juju/control/deploy.py 2011-09-30 09:48:25 +0000
740+++ juju/control/deploy.py 2011-10-05 10:00:46 +0000
741@@ -81,12 +81,11 @@
742 service_name, log, config_file=None):
743 """Deploy a charm within an environment.
744
745-
746 This will publish the charm to the environment, creating
747 a service from the charm, and get it set to be launched
748 on a new machine.
749 """
750- charm_url, charm = yield resolve(
751+ repo, charm_url = resolve(
752 charm_name, repository_path, environment.default_series)
753
754 # Validate config options prior to deployment attempt
755@@ -95,6 +94,9 @@
756 if config_file:
757 service_options = parse_config_options(config_file, service_name)
758
759+ charm = yield repo.find(charm_url)
760+ charm_id = str(charm_url.with_revision(charm.metadata.revision))
761+
762 provider = environment.get_machine_provider()
763 placement_policy = provider.get_placement_policy()
764 client = yield provider.connect()
765@@ -108,7 +110,6 @@
766
767 # Publish the charm to juju
768 publisher = CharmPublisher(client, storage)
769- charm_id = str(charm_url.with_revision(charm.metadata.revision))
770 yield publisher.add_charm(charm_id, charm)
771 result = yield publisher.publish()
772
773
774=== modified file 'juju/control/upgrade_charm.py'
775--- juju/control/upgrade_charm.py 2011-09-29 03:07:26 +0000
776+++ juju/control/upgrade_charm.py 2011-10-05 10:00:46 +0000
777@@ -64,11 +64,12 @@
778
779 old_charm_url = CharmURL.parse(old_charm_id)
780 old_charm_url.assert_revision()
781- charm_url, charm = yield resolve(
782+ repo, charm_url = resolve(
783 str(old_charm_url.with_revision(None)),
784 repository_path,
785 environment.default_series)
786- new_charm_url = charm_url.with_revision(charm.metadata.revision)
787+ new_charm_url = charm_url.with_revision(
788+ (yield repo.latest(charm_url)))
789 new_charm_id = str(new_charm_url)
790
791 # Verify its newer than what's deployed
792@@ -86,6 +87,7 @@
793 # Publish the new charm
794 storage = provider.get_file_storage()
795 publisher = CharmPublisher(client, storage)
796+ charm = yield repo.find(charm_url)
797 yield publisher.add_charm(new_charm_id, charm)
798 result = yield publisher.publish()
799 charm_state = result[0]

Subscribers

People subscribed via source and target branches

to status/vote changes: