Merge lp:~tvansteenburgh/charmworldlib/bundle-support into lp:charmworldlib/0.3
- bundle-support
- Merge into 0.3
Proposed by
Tim Van Steenburgh
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 31 | ||||
Proposed branch: | lp:~tvansteenburgh/charmworldlib/bundle-support | ||||
Merge into: | lp:charmworldlib/0.3 | ||||
Diff against target: |
953 lines (+761/-63) 7 files modified
charmworldlib/api.py (+21/-0) charmworldlib/bundle.py (+112/-4) charmworldlib/charm.py (+12/-21) tests/test_api.py (+7/-0) tests/test_bundle.py (+493/-32) tests/test_bundles.py (+108/-0) tests/test_charms.py (+8/-6) |
||||
To merge this branch: | bzr merge lp:~tvansteenburgh/charmworldlib/bundle-support | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Marco Ceppi (community) | Approve | ||
charmers | Pending | ||
Review via email: mp+219745@code.launchpad.net |
Commit message
Description of the change
Added Bundle support
To post a comment you must log in.
- 31. By Marco Ceppi
-
[tvansteenburgh] Add Bundle support
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charmworldlib/api.py' |
2 | --- charmworldlib/api.py 2014-02-04 12:44:47 +0000 |
3 | +++ charmworldlib/api.py 2014-05-15 18:32:26 +0000 |
4 | @@ -38,6 +38,19 @@ |
5 | |
6 | return r |
7 | |
8 | + def search(self, endpoint, doctype, criteria=None, limit=None): |
9 | + if type(criteria) is str: |
10 | + criteria = {'text': criteria} |
11 | + else: |
12 | + criteria = criteria or {} |
13 | + |
14 | + criteria['text'] = self._doctype_filter(criteria.get('text'), doctype) |
15 | + |
16 | + if limit and type(limit) is int: |
17 | + criteria['limit'] = limit |
18 | + |
19 | + return self.get(endpoint, criteria)['result'] or [] |
20 | + |
21 | def _earl(self): |
22 | if self.port: |
23 | return '%s://%s:%s' % (self.protocol, self.server, self.port) |
24 | @@ -49,3 +62,11 @@ |
25 | endpoint = '/%s' % endpoint |
26 | |
27 | return '%s/api/%s%s' % (self._earl(), self.version, endpoint) |
28 | + |
29 | + def _doctype_filter(self, text, doctype): |
30 | + text = (text or '').strip() |
31 | + if not text: |
32 | + return doctype |
33 | + if not text.startswith(doctype + ':'): |
34 | + return '{}:{}'.format(doctype, text) |
35 | + return text |
36 | |
37 | === modified file 'charmworldlib/bundle.py' |
38 | --- charmworldlib/bundle.py 2014-02-28 13:22:33 +0000 |
39 | +++ charmworldlib/bundle.py 2014-05-15 18:32:26 +0000 |
40 | @@ -1,10 +1,18 @@ |
41 | +import json |
42 | import yaml |
43 | |
44 | from . import api |
45 | +from .charm import Charm |
46 | + |
47 | + |
48 | +class BundleNotFound(Exception): |
49 | + pass |
50 | |
51 | |
52 | class Bundles(api.API): |
53 | - _base_endpoint = 'bundle' |
54 | + _base_endpoint = {1: 'bundle', 2: 'bundle', 3: 'bundle'} |
55 | + _base_search_endpoint = {1: 'bundles', 2: 'search', 3: 'search'} |
56 | + _doctype = 'bundle' |
57 | |
58 | def proof(self, deployer_contents): |
59 | if not self.version >= 3: |
60 | @@ -13,10 +21,110 @@ |
61 | if type(deployer_contents) is not dict: |
62 | raise Exception('Invalid deployer_contents') |
63 | |
64 | - return self.post('%s/proof' % self._base_endpoint, |
65 | + return self.post('%s/proof' % self._base_endpoint[self.version], |
66 | {'deployer_file': yaml.dump(deployer_contents)}) |
67 | |
68 | + def bundle(self, basket_name, name, revision=None, owner=None): |
69 | + data = self.get(self.bundle_endpoint( |
70 | + basket_name, name, revision, owner)) |
71 | + |
72 | + if 'result' not in data: |
73 | + return None |
74 | + |
75 | + bundle_raw = data['result'][0] |
76 | + return Bundle.from_bundledata(bundle_raw) |
77 | + |
78 | + def bundle_endpoint(self, basket_name, name, revision=None, owner=None): |
79 | + if owner and not owner.startswith('~'): |
80 | + owner = "~%s" % owner |
81 | + |
82 | + endpoint = self._base_endpoint[self.version] |
83 | + |
84 | + if owner: |
85 | + endpoint = "%s/%s" % (endpoint, owner) |
86 | + |
87 | + endpoint = "%s/%s" % (endpoint, basket_name) |
88 | + |
89 | + if isinstance(revision, int) and revision >= 0: |
90 | + endpoint = "%s/%s" % (endpoint, revision) |
91 | + |
92 | + endpoint = "%s/%s" % (endpoint, name) |
93 | + |
94 | + return endpoint |
95 | + |
96 | + def search(self, criteria=None, limit=None): |
97 | + result = super(Bundles, self).search( |
98 | + self._base_search_endpoint[self.version], |
99 | + self._doctype, |
100 | + criteria=criteria, |
101 | + limit=limit) |
102 | + return [Bundle.from_bundledata(bundle) for bundle in result] |
103 | + |
104 | + def approved(self): |
105 | + return self.search(criteria={'type': 'approved'}) |
106 | + |
107 | |
108 | class Bundle(object): |
109 | - def __init__(self): |
110 | - pass |
111 | + @classmethod |
112 | + def from_bundledata(cls, bundle_data): |
113 | + bundle = cls() |
114 | + bundle._parse(bundle_data) |
115 | + |
116 | + return bundle |
117 | + |
118 | + def __init__(self, bundle_id=None): |
119 | + self.charms = {} |
120 | + self._raw = {} |
121 | + self._api = api.API() |
122 | + |
123 | + if bundle_id: |
124 | + self._fetch(bundle_id) |
125 | + |
126 | + def __getattr__(self, key): |
127 | + return self._raw[key] |
128 | + |
129 | + def _fetch(self, bundle_id): |
130 | + b = Bundles() |
131 | + owner, basket_name, revision, name = self._parse_id(bundle_id) |
132 | + try: |
133 | + data = self._api.get(b.bundle_endpoint(basket_name, name, |
134 | + revision=revision, |
135 | + owner=owner)) |
136 | + self._parse(data) |
137 | + except Exception as e: |
138 | + raise BundleNotFound('API request failed: %s' % str(e)) |
139 | + |
140 | + def _parse(self, bundle_data): |
141 | + self._raw = bundle_data |
142 | + |
143 | + for charm_name, charm_data in bundle_data['charm_metadata'].items(): |
144 | + self.charms[charm_name] = Charm.from_charmdata({ |
145 | + 'charm': charm_data}) |
146 | + |
147 | + def _parse_id(self, bundle_id): |
148 | + owner, revision = None, None |
149 | + |
150 | + parts = bundle_id.split('/') |
151 | + |
152 | + if len(parts) == 4: |
153 | + owner, basket_name, revision, name = parts |
154 | + elif len(parts) == 3: |
155 | + if bundle_id.startswith('~'): |
156 | + owner, basket_name, name = parts |
157 | + else: |
158 | + basket_name, revision, name = parts |
159 | + elif len(parts) == 2: |
160 | + basket_name, name = parts |
161 | + else: |
162 | + raise ValueError('Invalid bundle id: {}'.format(bundle_id)) |
163 | + |
164 | + if revision: |
165 | + revision = int(revision) |
166 | + |
167 | + return owner, basket_name, revision, name |
168 | + |
169 | + def __str__(self): |
170 | + return json.dumps(self._raw, indent=2) |
171 | + |
172 | + def __repr__(self): |
173 | + return '<Bundle %s>' % self.id |
174 | |
175 | === modified file 'charmworldlib/charm.py' |
176 | --- charmworldlib/charm.py 2014-02-04 16:24:43 +0000 |
177 | +++ charmworldlib/charm.py 2014-05-15 18:32:26 +0000 |
178 | @@ -1,4 +1,3 @@ |
179 | - |
180 | import re |
181 | import json |
182 | from . import api |
183 | @@ -40,6 +39,7 @@ |
184 | class Charms(api.API): |
185 | _base_endpoint = {1: 'charm', 2: 'charm', 3: 'charm'} |
186 | _base_search_endpoint = {1: 'charms', 2: 'charms', 3: 'search'} |
187 | + _doctype = 'charm' |
188 | |
189 | def requires(self, interfaces=[], limit=None): |
190 | return self.interfaces(requires=interfaces) |
191 | @@ -73,26 +73,6 @@ |
192 | charm_raw = data['result'][0] |
193 | return Charm.from_charmdata(charm_raw) |
194 | |
195 | - def approved(self): |
196 | - return self.search({'type': 'approved'}) |
197 | - |
198 | - def search(self, criteria={}, limit=None): |
199 | - results = [] |
200 | - if type(criteria) is str: |
201 | - criteria = {'text': criteria} |
202 | - if limit and type(limit) is int: |
203 | - criteria['limit'] = limit |
204 | - |
205 | - data = self.get(self._base_search_endpoint[self.version], criteria) |
206 | - |
207 | - if not data['result']: |
208 | - return [] |
209 | - |
210 | - for charm in data['result']: |
211 | - results.append(Charm.from_charmdata(charm)) |
212 | - |
213 | - return results |
214 | - |
215 | def charm_endpoint(self, name, series='precise', revision=None, |
216 | owner=None): |
217 | if owner and not owner.startswith('~'): |
218 | @@ -110,6 +90,17 @@ |
219 | |
220 | return endpoint |
221 | |
222 | + def search(self, criteria=None, limit=None): |
223 | + result = super(Charms, self).search( |
224 | + self._base_search_endpoint[self.version], |
225 | + self._doctype, |
226 | + criteria=criteria, |
227 | + limit=limit) |
228 | + return [Charm.from_charmdata(charm) for charm in result] |
229 | + |
230 | + def approved(self): |
231 | + return self.search(criteria={'type': 'approved'}) |
232 | + |
233 | |
234 | class Charm(object): |
235 | @classmethod |
236 | |
237 | === modified file 'tests/test_api.py' |
238 | --- tests/test_api.py 2014-01-20 17:43:03 +0000 |
239 | +++ tests/test_api.py 2014-05-15 18:32:26 +0000 |
240 | @@ -78,3 +78,10 @@ |
241 | def test_post_params(self, mfetch): |
242 | self.a.post('end/of/the/world', {'isit'}) |
243 | mfetch.assert_called_with('end/of/the/world', {'isit'}, 'post') |
244 | + |
245 | + def test_doctype_filter(self): |
246 | + f = self.a._doctype_filter |
247 | + self.assertEqual(f(None, 'charm'), 'charm') |
248 | + self.assertEqual(f(' ', 'charm'), 'charm') |
249 | + self.assertEqual(f('searchterm', 'charm'), 'charm:searchterm') |
250 | + self.assertEqual(f(' charm:searchterm', 'charm'), 'charm:searchterm') |
251 | |
252 | === modified file 'tests/test_bundle.py' |
253 | --- tests/test_bundle.py 2014-02-28 13:22:33 +0000 |
254 | +++ tests/test_bundle.py 2014-05-15 18:32:26 +0000 |
255 | @@ -1,38 +1,499 @@ |
256 | -"""Unit test for API""" |
257 | +"""Unit test for Bundle""" |
258 | |
259 | +import json |
260 | +from mock import patch |
261 | import unittest |
262 | |
263 | -from mock import patch |
264 | -from charmworldlib.bundle import ( |
265 | - Bundle, |
266 | - Bundles, |
267 | -) |
268 | - |
269 | - |
270 | -class BundlesTest(unittest.TestCase): |
271 | - @classmethod |
272 | - def setUpClass(cls): |
273 | - cls.b = Bundles() |
274 | - |
275 | - @patch('charmworldlib.bundle.api.API.post') |
276 | - def test_bundles_proof(self, mpost): |
277 | - mpost.return_value = {'deploy'} |
278 | - self.b.proof({'deployment': {}}) |
279 | - mpost.assert_called_with('bundle/proof', {'deployer_file': |
280 | - 'deployment: {}\n'}) |
281 | - |
282 | - def test_bundles_proof_ver(self): |
283 | - b = Bundles(version=2) |
284 | - self.assertRaises(ValueError, b.proof, 'garbage') |
285 | - |
286 | - def test_bundles_proof_invalid(self): |
287 | - self.assertRaises(Exception, self.b.proof, 'garbage') |
288 | +from charmworldlib.bundle import Bundle, BundleNotFound |
289 | + |
290 | +BUNDLE_DATA = json.loads(r''' |
291 | +{ |
292 | + "basket_name": "mediawiki", |
293 | + "basket_revision": 6, |
294 | + "branch_deleted": false, |
295 | + "branch_spec": "~charmers/charms/bundles/mediawiki/bundle", |
296 | + "changes": [ |
297 | + { |
298 | + "authors": [ |
299 | + "Jorge O. Castro <jorge@ubuntu.com>" |
300 | + ], |
301 | + "committer": "Jorge O. Castro <jorge@ubuntu.com>", |
302 | + "created": 1383229159.809, |
303 | + "message": "Initial commit\n", |
304 | + "revno": 1 |
305 | + } |
306 | + ], |
307 | + "charm_metadata": { |
308 | + "mediawiki": { |
309 | + "annotations": { |
310 | + "gui-x": 609, |
311 | + "gui-y": -15 |
312 | + }, |
313 | + "categories": [ |
314 | + "applications" |
315 | + ], |
316 | + "code_source": { |
317 | + "bugs_link": "https://bugs.launchpad.net/charms/+source/mediawiki", |
318 | + "last_log": "merging lp:~dave-cheney/charms/precise/mediawiki/trunk as per https://code.launchpad.net/~dave-cheney/charms/precise/mediawiki/trunk/+merge/182803", |
319 | + "location": "lp:~charmers/charms/precise/mediawiki/trunk", |
320 | + "revision": "72", |
321 | + "revisions": [ |
322 | + { |
323 | + "authors": [ |
324 | + { |
325 | + "email": "clint@ubuntu.com", |
326 | + "name": "Clint Byrum" |
327 | + } |
328 | + ], |
329 | + "date": "2012-06-28T00:02:47Z", |
330 | + "message": "removing old broken munin bits", |
331 | + "revno": 63 |
332 | + } |
333 | + ], |
334 | + "type": "bzr" |
335 | + }, |
336 | + "date_created": "2012-04-16T18:29:51Z", |
337 | + "description": "MediaWiki is a wiki engine (a program for creating a collaboratively\nedited website). It is designed to handle heavy websites containing\nlibrary-like document collections, and supports user uploads of\nimages/sounds, multilingual content, TOC autogeneration, ISBN links,\netc.\n", |
338 | + "distro_series": "precise", |
339 | + "downloads": 3928, |
340 | + "downloads_in_past_30_days": 484, |
341 | + "files": [ |
342 | + "hooks/slave-relation-departed", |
343 | + "hooks/combine-dbservers", |
344 | + "hooks/cache-relation-changed", |
345 | + "hooks/website-relation-joined", |
346 | + "revision", |
347 | + "icon.svg", |
348 | + "hooks/upgrade-charm", |
349 | + "hooks/stop", |
350 | + "README.md", |
351 | + "hooks/db-relation-changed", |
352 | + "hooks/db-relation-departed", |
353 | + "hooks/install", |
354 | + "metadata.yaml", |
355 | + "hooks/config-changed", |
356 | + "hooks/slave-relation-changed", |
357 | + "config.yaml", |
358 | + "hooks/slave-relation-broken" |
359 | + ], |
360 | + "id": "precise/mediawiki-10", |
361 | + "is_approved": true, |
362 | + "is_subordinate": false, |
363 | + "maintainer": { |
364 | + "email": "clint@ubuntu.com", |
365 | + "name": "Clint Byrum" |
366 | + }, |
367 | + "maintainers": [ |
368 | + { |
369 | + "email": "clint@ubuntu.com", |
370 | + "name": "Clint Byrum" |
371 | + } |
372 | + ], |
373 | + "name": "mediawiki", |
374 | + "options": { |
375 | + "admins": { |
376 | + "description": "Admin users to create, user:pass", |
377 | + "type": "string" |
378 | + }, |
379 | + "debug": { |
380 | + "default": false, |
381 | + "description": "turn on debugging features of mediawiki", |
382 | + "type": "boolean" |
383 | + }, |
384 | + "logo": { |
385 | + "description": "URL to fetch logo from", |
386 | + "type": "string" |
387 | + }, |
388 | + "name": { |
389 | + "default": "Please set name of wiki", |
390 | + "description": "The name, or Title of the Wiki", |
391 | + "type": "string" |
392 | + }, |
393 | + "skin": { |
394 | + "default": "vector", |
395 | + "description": "skin for the Wiki", |
396 | + "type": "string" |
397 | + } |
398 | + }, |
399 | + "owner": "charmers", |
400 | + "rating_denominator": 0, |
401 | + "rating_numerator": 0, |
402 | + "relations": { |
403 | + "provides": { |
404 | + "website": { |
405 | + "interface": "http" |
406 | + } |
407 | + }, |
408 | + "requires": { |
409 | + "cache": { |
410 | + "interface": "memcache" |
411 | + }, |
412 | + "db": { |
413 | + "interface": "mysql" |
414 | + }, |
415 | + "slave": { |
416 | + "interface": "mysql" |
417 | + } |
418 | + } |
419 | + }, |
420 | + "revision": 90, |
421 | + "summary": "Website engine for collaborative work", |
422 | + "tested_providers": { |
423 | + "ec2": "SUCCESS", |
424 | + "openstack": "SUCCESS" |
425 | + }, |
426 | + "url": "cs:precise/mediawiki-10" |
427 | + }, |
428 | + "mysql": { |
429 | + "annotations": { |
430 | + "gui-x": 610, |
431 | + "gui-y": 255 |
432 | + }, |
433 | + "categories": [ |
434 | + "databases" |
435 | + ], |
436 | + "code_source": { |
437 | + "bugs_link": "https://bugs.launchpad.net/charms/+source/mysql", |
438 | + "last_log": "Updated README", |
439 | + "location": "lp:~charmers/charms/precise/mysql/trunk", |
440 | + "revision": "105", |
441 | + "revisions": [ |
442 | + { |
443 | + "authors": [ |
444 | + { |
445 | + "email": "marco@ceppi.net", |
446 | + "name": "Marco Ceppi" |
447 | + } |
448 | + ], |
449 | + "date": "2013-04-25T18:19:45Z", |
450 | + "message": "Added icon.svg", |
451 | + "revno": 96 |
452 | + } |
453 | + ], |
454 | + "type": "bzr" |
455 | + }, |
456 | + "date_created": "2012-04-16T18:30:00Z", |
457 | + "description": "MySQL is a fast, stable and true multi-user, multi-threaded SQL database\nserver. SQL (Structured Query Language) is the most popular database query\nlanguage in the world. The main goals of MySQL are speed, robustness and\nease of use.\n", |
458 | + "distro_series": "precise", |
459 | + "downloads": 21895, |
460 | + "downloads_in_past_30_days": 2006, |
461 | + "files": [ |
462 | + "hooks/munin-relation-joined", |
463 | + "hooks/monitors.common.bash", |
464 | + "hooks/db-relation-joined", |
465 | + "hooks/shared-db-relation-changed", |
466 | + "hooks/master-relation-departed", |
467 | + "hooks/monitors-relation-departed", |
468 | + "hooks/master-relation-broken", |
469 | + "hooks/lib/cluster_utils.py", |
470 | + "hooks/shared_db_relations.py", |
471 | + "hooks/slave-relation-broken", |
472 | + "hooks/lib/utils.py", |
473 | + "hooks/ha-relation-changed", |
474 | + "hooks/munin-relation-changed", |
475 | + "hooks/common.py", |
476 | + "hooks/start", |
477 | + "hooks/config-changed", |
478 | + "hooks/db-relation-broken", |
479 | + "hooks/slave-relation-changed", |
480 | + "hooks/shared-db-relation-joined", |
481 | + "hooks/ha_relations.py", |
482 | + "hooks/cluster-relation-changed", |
483 | + "hooks/slave-relation-departed", |
484 | + "hooks/lib/ceph_utils.py", |
485 | + "hooks/ceph-relation-changed", |
486 | + "metadata.yaml", |
487 | + "hooks/ha-relation-joined", |
488 | + "hooks/stop", |
489 | + "hooks/db-admin-relation-joined", |
490 | + "config.yaml", |
491 | + "hooks/monitors-relation-joined", |
492 | + "icon.svg", |
493 | + "hooks/upgrade-charm", |
494 | + "README.md", |
495 | + "hooks/ceph-relation-joined", |
496 | + "hooks/master-relation-changed", |
497 | + "hooks/lib/__init__.py", |
498 | + "hooks/slave-relation-joined", |
499 | + "hooks/install", |
500 | + "hooks/local-monitors-relation-joined", |
501 | + "revision", |
502 | + "hooks/monitors-relation-broken" |
503 | + ], |
504 | + "id": "precise/mysql-28", |
505 | + "is_approved": true, |
506 | + "is_subordinate": false, |
507 | + "maintainer": { |
508 | + "email": "marco@ceppi.net", |
509 | + "name": "Marco Ceppi" |
510 | + }, |
511 | + "maintainers": [ |
512 | + { |
513 | + "email": "marco@ceppi.net", |
514 | + "name": "Marco Ceppi" |
515 | + } |
516 | + ], |
517 | + "name": "mysql", |
518 | + "options": { |
519 | + "binlog-format": { |
520 | + "default": "MIXED", |
521 | + "description": "If binlogging is enabled, this is the format that will be used. Ignored when tuning-level == fast.", |
522 | + "type": "string" |
523 | + }, |
524 | + "block-size": { |
525 | + "default": 5, |
526 | + "description": "Default block storage size to create when setting up MySQL block storage.\nThis value should be specified in GB (e.g. 100 not 100GB).\n", |
527 | + "type": "int" |
528 | + }, |
529 | + "dataset-size": { |
530 | + "default": "80%", |
531 | + "description": "How much data do you want to keep in memory in the DB. This will be used to tune settings in the database server appropriately. Any more specific settings will override these defaults though. This currently sets innodb_buffer_pool_size or key_cache_size depending on the setting in preferred-storage-engine. If query-cache-type is set to 'ON' or 'DEMAND' 20% of this is given to query-cache-size. Suffix this value with 'K','M','G', or 'T' to get the relevant kilo/mega/etc. bytes. If suffixed with %, one will get that percentage of RAM devoted to dataset and (if enabled) query cache.", |
532 | + "type": "string" |
533 | + }, |
534 | + "flavor": { |
535 | + "default": "distro", |
536 | + "description": "Possible values are 'distro' or 'percona'", |
537 | + "type": "string" |
538 | + }, |
539 | + "ha-bindiface": { |
540 | + "default": "eth0", |
541 | + "description": "Default network interface on which HA cluster will bind to communication\nwith the other members of the HA Cluster.\n", |
542 | + "type": "string" |
543 | + }, |
544 | + "ha-mcastport": { |
545 | + "default": 5411, |
546 | + "description": "Default multicast port number that will be used to communicate between\nHA Cluster nodes.\n", |
547 | + "type": "int" |
548 | + }, |
549 | + "max-connections": { |
550 | + "default": -1, |
551 | + "description": "Maximum connections to allow. -1 means use the server's compiled in default.", |
552 | + "type": "int" |
553 | + }, |
554 | + "preferred-storage-engine": { |
555 | + "default": "InnoDB", |
556 | + "description": "Tune the server for usage of this storage engine. Other possible value is MyISAM. Comma separated will cause settings to split resources evenly among given engines.", |
557 | + "type": "string" |
558 | + }, |
559 | + "query-cache-size": { |
560 | + "default": -1, |
561 | + "description": "Override the computed version from dataset-size. Still works if query-cache-type is \"OFF\" since sessions can override the cache type setting on their own.", |
562 | + "type": "int" |
563 | + }, |
564 | + "query-cache-type": { |
565 | + "default": "OFF", |
566 | + "description": "Query cache is usually a good idea, but can hurt concurrency. Valid values are \"OFF\", \"ON\", or \"DEMAND\". http://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_query_cache_type", |
567 | + "type": "string" |
568 | + }, |
569 | + "rbd-name": { |
570 | + "default": "mysql1", |
571 | + "description": "The name that will be used to create the Ceph's RBD image with. If the\nimage name exists in Ceph, it will be re-used and the data will be\noverwritten.\n", |
572 | + "type": "string" |
573 | + }, |
574 | + "tuning-level": { |
575 | + "default": "safest", |
576 | + "description": "Valid values are 'safest', 'fast', and 'unsafe'. If set to safest, all settings are tuned to have maximum safety at the cost of performance. Fast will turn off most controls, but may lose data on crashes. unsafe will turn off all protections.", |
577 | + "type": "string" |
578 | + }, |
579 | + "vip": { |
580 | + "description": "Virtual IP to use to front mysql in ha configuration", |
581 | + "type": "string" |
582 | + }, |
583 | + "vip_cidr": { |
584 | + "default": 24, |
585 | + "description": "Netmask that will be used for the Virtual IP", |
586 | + "type": "int" |
587 | + }, |
588 | + "vip_iface": { |
589 | + "default": "eth0", |
590 | + "description": "Network Interface where to place the Virtual IP", |
591 | + "type": "string" |
592 | + } |
593 | + }, |
594 | + "owner": "charmers", |
595 | + "rating_denominator": 0, |
596 | + "rating_numerator": 0, |
597 | + "relations": { |
598 | + "provides": { |
599 | + "db": { |
600 | + "interface": "mysql" |
601 | + }, |
602 | + "db-admin": { |
603 | + "interface": "mysql-root" |
604 | + }, |
605 | + "local-monitors": { |
606 | + "interface": "local-monitors", |
607 | + "scope": "container" |
608 | + }, |
609 | + "master": { |
610 | + "interface": "mysql-oneway-replication" |
611 | + }, |
612 | + "monitors": { |
613 | + "interface": "monitors" |
614 | + }, |
615 | + "munin": { |
616 | + "interface": "munin-node" |
617 | + }, |
618 | + "shared-db": { |
619 | + "interface": "mysql-shared" |
620 | + } |
621 | + }, |
622 | + "requires": { |
623 | + "ceph": { |
624 | + "interface": "ceph-client" |
625 | + }, |
626 | + "ha": { |
627 | + "interface": "hacluster", |
628 | + "scope": "container" |
629 | + }, |
630 | + "slave": { |
631 | + "interface": "mysql-oneway-replication" |
632 | + } |
633 | + } |
634 | + }, |
635 | + "revision": 309, |
636 | + "summary": "MySQL is a fast, stable and true multi-user, multi-threaded SQL database", |
637 | + "tested_providers": {}, |
638 | + "url": "cs:precise/mysql-28" |
639 | + } |
640 | + }, |
641 | + "data": { |
642 | + "relations": [ |
643 | + [ |
644 | + "mediawiki:db", |
645 | + "mysql:db" |
646 | + ] |
647 | + ], |
648 | + "series": "precise", |
649 | + "services": { |
650 | + "mediawiki": { |
651 | + "annotations": { |
652 | + "gui-x": 609, |
653 | + "gui-y": -15 |
654 | + }, |
655 | + "charm": "cs:precise/mediawiki-10", |
656 | + "num_units": 1, |
657 | + "options": { |
658 | + "debug": false, |
659 | + "name": "Please set name of wiki", |
660 | + "skin": "vector" |
661 | + } |
662 | + }, |
663 | + "mysql": { |
664 | + "annotations": { |
665 | + "gui-x": 610, |
666 | + "gui-y": 255 |
667 | + }, |
668 | + "charm": "cs:precise/mysql-28", |
669 | + "num_units": 1, |
670 | + "options": { |
671 | + "binlog-format": "MIXED", |
672 | + "block-size": 5, |
673 | + "dataset-size": "80%", |
674 | + "flavor": "distro", |
675 | + "ha-bindiface": "eth0", |
676 | + "ha-mcastport": 5411, |
677 | + "max-connections": -1, |
678 | + "preferred-storage-engine": "InnoDB", |
679 | + "query-cache-size": -1, |
680 | + "query-cache-type": "OFF", |
681 | + "rbd-name": "mysql1", |
682 | + "tuning-level": "safest", |
683 | + "vip_cidr": 24, |
684 | + "vip_iface": "eth0" |
685 | + } |
686 | + } |
687 | + } |
688 | + }, |
689 | + "deployer_file_url": "http://manage.jujucharms.com/bundle/%7Echarmers/mediawiki/6/single/json", |
690 | + "description": "", |
691 | + "downloads": 58, |
692 | + "downloads_in_past_30_days": 4, |
693 | + "files": [ |
694 | + "README.md", |
695 | + "bundles.yaml" |
696 | + ], |
697 | + "first_change": { |
698 | + "authors": [ |
699 | + "Jorge O. Castro <jorge@ubuntu.com>" |
700 | + ], |
701 | + "committer": "Jorge O. Castro <jorge@ubuntu.com>", |
702 | + "created": 1383229159.809, |
703 | + "message": "Initial commit\n", |
704 | + "revno": 1 |
705 | + }, |
706 | + "id": "~charmers/mediawiki/6/single", |
707 | + "last_change": { |
708 | + "authors": [ |
709 | + "Jorge O. Castro <jorge@ubuntu.com>" |
710 | + ], |
711 | + "committer": "Jorge O. Castro <jorge@ubuntu.com>", |
712 | + "created": 1394724884.008, |
713 | + "message": "Combine single and scalable bundles into one.\n", |
714 | + "revno": 6 |
715 | + }, |
716 | + "name": "single", |
717 | + "owner": "charmers", |
718 | + "permanent_url": "bundle:~charmers/mediawiki/6/single", |
719 | + "promulgated": true, |
720 | + "title": "" |
721 | +}''') |
722 | |
723 | |
724 | class BundleTest(unittest.TestCase): |
725 | - @classmethod |
726 | - def setUpClass(cls): |
727 | - cls.b = Bundle() |
728 | - |
729 | - def test_bundle(self): |
730 | - pass |
731 | + def test_from_bundledata(self): |
732 | + b = Bundle.from_bundledata(BUNDLE_DATA) |
733 | + self.assertIsInstance(b, Bundle) |
734 | + self.assertEqual(b._raw, BUNDLE_DATA) |
735 | + |
736 | + @patch('charmworldlib.bundle.api.API.get') |
737 | + def test_init(self, get): |
738 | + get.return_value = BUNDLE_DATA |
739 | + self.assertEqual( |
740 | + Bundle('~me/mediawiki/0/single')._raw, |
741 | + Bundle.from_bundledata(BUNDLE_DATA)._raw) |
742 | + |
743 | + def test_getattr(self): |
744 | + b = Bundle.from_bundledata(BUNDLE_DATA) |
745 | + for k, v in b._raw.items(): |
746 | + self.assertEqual(v, getattr(b, k)) |
747 | + |
748 | + @patch('charmworldlib.bundle.api.API.get') |
749 | + def test_bundle_fetch_fail(self, mget): |
750 | + msg = 'Request failed with: 500' |
751 | + mget.side_effect = Exception(msg) |
752 | + self.assertRaises(BundleNotFound, Bundle, bundle_id='mediawiki/single') |
753 | + |
754 | + def test_parse(self): |
755 | + from charmworldlib.charm import Charm |
756 | + b = Bundle.from_bundledata(BUNDLE_DATA) |
757 | + self.assertEqual(len(b.charms), 2) |
758 | + self.assertTrue('mediawiki' in b.charms) |
759 | + self.assertTrue('mysql' in b.charms) |
760 | + for charm in b.charms.values(): |
761 | + self.assertIsInstance(charm, Charm) |
762 | + |
763 | + def test_parse_id(self): |
764 | + b = Bundle() |
765 | + |
766 | + self.assertEqual( |
767 | + b._parse_id('~me/mediawiki/0/single'), |
768 | + ('~me', 'mediawiki', 0, 'single')) |
769 | + self.assertEqual( |
770 | + b._parse_id('~me/mediawiki/single'), |
771 | + ('~me', 'mediawiki', None, 'single')) |
772 | + self.assertEqual( |
773 | + b._parse_id('mediawiki/0/single'), |
774 | + (None, 'mediawiki', 0, 'single')) |
775 | + self.assertEqual( |
776 | + b._parse_id('mediawiki/single'), |
777 | + (None, 'mediawiki', None, 'single')) |
778 | + self.assertRaises(ValueError, b._parse_id, 'mediawiki') |
779 | + |
780 | + def test_str(self): |
781 | + b = Bundle.from_bundledata(BUNDLE_DATA) |
782 | + self.assertEqual(str(b), json.dumps(BUNDLE_DATA, indent=2)) |
783 | + |
784 | + def test_repr(self): |
785 | + b = Bundle.from_bundledata(BUNDLE_DATA) |
786 | + self.assertEqual(repr(b), '<Bundle ~charmers/mediawiki/6/single>') |
787 | |
788 | === added file 'tests/test_bundles.py' |
789 | --- tests/test_bundles.py 1970-01-01 00:00:00 +0000 |
790 | +++ tests/test_bundles.py 2014-05-15 18:32:26 +0000 |
791 | @@ -0,0 +1,108 @@ |
792 | +"""Unit test for Bundles""" |
793 | + |
794 | +import unittest |
795 | + |
796 | +from mock import patch |
797 | +from charmworldlib.bundle import Bundles |
798 | + |
799 | + |
800 | +class BundlesTest(unittest.TestCase): |
801 | + @classmethod |
802 | + def setUpClass(cls): |
803 | + cls.b = Bundles() |
804 | + |
805 | + @patch('charmworldlib.bundle.api.API.post') |
806 | + def test_bundles_proof(self, mpost): |
807 | + mpost.return_value = {'deploy'} |
808 | + self.b.proof({'deployment': {}}) |
809 | + mpost.assert_called_with('bundle/proof', {'deployer_file': |
810 | + 'deployment: {}\n'}) |
811 | + |
812 | + def test_bundles_proof_ver(self): |
813 | + b = Bundles(version=2) |
814 | + self.assertRaises(ValueError, b.proof, 'garbage') |
815 | + |
816 | + def test_bundles_proof_invalid(self): |
817 | + self.assertRaises(Exception, self.b.proof, 'garbage') |
818 | + |
819 | + @patch('charmworldlib.bundle.Bundle') |
820 | + @patch('charmworldlib.bundle.Bundles.get') |
821 | + def test_bundles_search(self, mget, mBundle): |
822 | + cdata = {'bundle': {'id': 'oneiric/bundle-0'}} |
823 | + mget.return_value = {'result': [cdata]} |
824 | + self.assertEqual([mBundle.from_bundledata()], self.b.search()) |
825 | + mget.assert_called_with('search', {'text': 'bundle'}) |
826 | + mBundle.from_bundledata.assert_called_with(cdata) |
827 | + |
828 | + @patch('charmworldlib.bundle.Bundles.get') |
829 | + def test_bundles_search_string(self, mget): |
830 | + mget.return_value = {'result': None} |
831 | + self.assertEqual([], self.b.search('blurb', 2)) |
832 | + mget.assert_called_with('search', {'text': 'bundle:blurb', 'limit': 2}) |
833 | + |
834 | + @patch('charmworldlib.bundle.Bundles.get') |
835 | + def test_bundles_search_params(self, mget): |
836 | + mget.return_value = {'result': None} |
837 | + self.assertEqual([], self.b.search({'approved': True}, 1)) |
838 | + mget.assert_called_with( |
839 | + 'search', {'approved': True, 'limit': 1, 'text': 'bundle'}) |
840 | + |
841 | + @patch('charmworldlib.bundle.Bundles.get') |
842 | + def test_bundles_search_no_results(self, mget): |
843 | + mget.return_value = {'result': None} |
844 | + self.assertEqual([], self.b.search('no-match')) |
845 | + mget.assert_called_with('search', {'text': 'bundle:no-match'}) |
846 | + |
847 | + @patch('charmworldlib.bundle.Bundle') |
848 | + @patch('charmworldlib.bundle.Bundles.get') |
849 | + def test_bundles_search_versions(self, mget, mBundle): |
850 | + self.b.version = 2 |
851 | + cdata = {'bundle': {'id': 'mediawiki/0/single'}} |
852 | + mget.return_value = {'result': [cdata]} |
853 | + self.assertEqual([mBundle.from_bundledata()], self.b.search()) |
854 | + mget.assert_called_with('search', {'text': 'bundle'}) |
855 | + mBundle.from_bundledata.assert_called_with(cdata) |
856 | + |
857 | + @patch('charmworldlib.bundle.Bundles.get') |
858 | + def test_bundles_approved(self, mget): |
859 | + mget.return_value = {'result': None} |
860 | + self.assertEqual([], self.b.approved()) |
861 | + mget.assert_called_with( |
862 | + 'search', {'type': 'approved', 'text': 'bundle'}) |
863 | + |
864 | + @patch('charmworldlib.bundle.Bundle.from_bundledata') |
865 | + @patch('charmworldlib.bundle.Bundles.get') |
866 | + def test_bundles_bundle(self, mget, mBundle): |
867 | + cdata = {'bundle': {'id': 'mediawiki/0/single'}} |
868 | + mget.return_value = {'result': [cdata]} |
869 | + self.assertEqual(mBundle(), self.b.bundle('mediawiki', 'single', |
870 | + revision=0)) |
871 | + mget.assert_called_with('bundle/mediawiki/0/single') |
872 | + mBundle.assert_called_with(cdata) |
873 | + |
874 | + @patch('charmworldlib.bundle.Bundle.from_bundledata') |
875 | + @patch('charmworldlib.bundle.Bundles.get') |
876 | + def test_bundles_bundle_full(self, mget, mBundle): |
877 | + cdata = {'bundle': {'id': '~me/mediawiki/0/single'}} |
878 | + mget.return_value = {'result': [cdata]} |
879 | + self.assertEqual(mBundle(), self.b.bundle('mediawiki', 'single', |
880 | + revision=0, owner='~me')) |
881 | + mget.assert_called_with('bundle/~me/mediawiki/0/single') |
882 | + mBundle.assert_called_with(cdata) |
883 | + |
884 | + @patch('charmworldlib.bundle.Bundles.get') |
885 | + def test_bundles_bundle_404(self, mget): |
886 | + mget.return_value = {'none': None} |
887 | + self.assertEqual(None, self.b.bundle('mediawiki', 'single', |
888 | + revision=0, owner='~me')) |
889 | + mget.assert_called_with('bundle/~me/mediawiki/0/single') |
890 | + |
891 | + @patch('charmworldlib.bundle.Bundle.from_bundledata') |
892 | + @patch('charmworldlib.bundle.Bundles.get') |
893 | + def test_bundles_bundle_full_owner(self, mget, mBundle): |
894 | + cdata = {'bundle': {'id': '~me/mediawiki/0/single'}} |
895 | + mget.return_value = {'result': [cdata]} |
896 | + self.assertEqual(mBundle(), self.b.bundle('mediawiki', 'single', |
897 | + revision=0, owner='me')) |
898 | + mget.assert_called_with('bundle/~me/mediawiki/0/single') |
899 | + mBundle.assert_called_with(cdata) |
900 | |
901 | === modified file 'tests/test_charms.py' |
902 | --- tests/test_charms.py 2014-01-24 17:15:21 +0000 |
903 | +++ tests/test_charms.py 2014-05-15 18:32:26 +0000 |
904 | @@ -17,26 +17,27 @@ |
905 | cdata = {'charm': {'id': 'oneiric/charm-0'}} |
906 | mget.return_value = {'result': [cdata]} |
907 | self.assertEqual([mCharm.from_charmdata()], self.c.search()) |
908 | - mget.assert_called_with('search', {}) |
909 | + mget.assert_called_with('search', {'text': 'charm'}) |
910 | mCharm.from_charmdata.assert_called_with(cdata) |
911 | |
912 | @patch('charmworldlib.charm.Charms.get') |
913 | def test_charms_search_string(self, mget): |
914 | mget.return_value = {'result': None} |
915 | self.assertEqual([], self.c.search('blurb', 2)) |
916 | - mget.assert_called_with('search', {'text': 'blurb', 'limit': 2}) |
917 | + mget.assert_called_with('search', {'text': 'charm:blurb', 'limit': 2}) |
918 | |
919 | @patch('charmworldlib.charm.Charms.get') |
920 | def test_charms_search_params(self, mget): |
921 | mget.return_value = {'result': None} |
922 | self.assertEqual([], self.c.search({'approved': True}, 1)) |
923 | - mget.assert_called_with('search', {'approved': True, 'limit': 1}) |
924 | + mget.assert_called_with( |
925 | + 'search', {'approved': True, 'limit': 1, 'text': 'charm'}) |
926 | |
927 | @patch('charmworldlib.charm.Charms.get') |
928 | def test_charms_search_no_results(self, mget): |
929 | mget.return_value = {'result': None} |
930 | self.assertEqual([], self.c.search('no-match')) |
931 | - mget.assert_called_with('search', {'text': 'no-match'}) |
932 | + mget.assert_called_with('search', {'text': 'charm:no-match'}) |
933 | |
934 | @patch('charmworldlib.charm.Charm') |
935 | @patch('charmworldlib.charm.Charms.get') |
936 | @@ -45,14 +46,15 @@ |
937 | cdata = {'charm': {'id': 'oneiric/charm-0'}} |
938 | mget.return_value = {'result': [cdata]} |
939 | self.assertEqual([mCharm.from_charmdata()], self.c.search()) |
940 | - mget.assert_called_with('charms', {}) |
941 | + mget.assert_called_with('charms', {'text': 'charm'}) |
942 | mCharm.from_charmdata.assert_called_with(cdata) |
943 | |
944 | @patch('charmworldlib.charm.Charms.get') |
945 | def test_charms_approved(self, mget): |
946 | mget.return_value = {'result': None} |
947 | self.assertEqual([], self.c.approved()) |
948 | - mget.assert_called_with('search', {'type': 'approved'}) |
949 | + mget.assert_called_with( |
950 | + 'search', {'type': 'approved', 'text': 'charm'}) |
951 | |
952 | @patch('charmworldlib.charm.Charm.from_charmdata') |
953 | @patch('charmworldlib.charm.Charms.get') |
Yes. LGTM!