Merge lp:~rvb/maas/expose-version-api-1.7 into lp:maas/1.7
- expose-version-api-1.7
- Merge into 1.7
Proposed by
Raphaël Badin
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Raphaël Badin | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 3360 | ||||
Proposed branch: | lp:~rvb/maas/expose-version-api-1.7 | ||||
Merge into: | lp:maas/1.7 | ||||
Diff against target: |
422 lines (+362/-0) 4 files modified
src/maasserver/api/tests/test_version.py (+7/-0) src/maasserver/api/version.py (+7/-0) src/maasserver/utils/tests/test_version.py (+217/-0) src/maasserver/utils/version.py (+131/-0) |
||||
To merge this branch: | bzr merge lp:~rvb/maas/expose-version-api-1.7 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Raphaël Badin (community) | Approve | ||
Review via email: mp+255529@code.launchpad.net |
Commit message
Backport 3794: Expose the version and the subversion of the running MAAS on the API.
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/maasserver/api/tests/test_version.py' |
2 | --- src/maasserver/api/tests/test_version.py 2014-08-18 13:23:21 +0000 |
3 | +++ src/maasserver/api/tests/test_version.py 2015-04-08 15:34:24 +0000 |
4 | @@ -21,6 +21,7 @@ |
5 | from django.core.urlresolvers import reverse |
6 | from maasserver.api.version import API_CAPABILITIES_LIST |
7 | from maasserver.testing.testcase import MAASServerTestCase |
8 | +from maasserver.utils import version as version_module |
9 | |
10 | |
11 | class TestFindingResources(MAASServerTestCase): |
12 | @@ -31,6 +32,10 @@ |
13 | '/api/1.0/version/', reverse('version')) |
14 | |
15 | def test_GET_returns_details(self): |
16 | + mock_apt = self.patch(version_module, "get_version_from_apt") |
17 | + mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1" |
18 | + self.patch(version_module, "_cache", {}) |
19 | + |
20 | response = self.client.get(reverse('version')) |
21 | self.assertEqual(httplib.OK, response.status_code) |
22 | |
23 | @@ -38,5 +43,7 @@ |
24 | self.assertEqual( |
25 | { |
26 | 'capabilities': API_CAPABILITIES_LIST, |
27 | + 'version': '1.8.0', |
28 | + 'subversion': 'alpha4+bzr356', |
29 | }, |
30 | parsed_result) |
31 | |
32 | === modified file 'src/maasserver/api/version.py' |
33 | --- src/maasserver/api/version.py 2014-09-17 02:17:35 +0000 |
34 | +++ src/maasserver/api/version.py 2015-04-08 15:34:24 +0000 |
35 | @@ -20,6 +20,7 @@ |
36 | |
37 | from django.http import HttpResponse |
38 | from maasserver.api.support import AnonymousOperationsHandler |
39 | +from maasserver.utils.version import get_maas_version_subversion |
40 | |
41 | # MAAS capabilities. See docs/capabilities.rst for documentation. |
42 | CAP_NETWORKS_MANAGEMENT = 'networks-management' |
43 | @@ -39,6 +40,8 @@ |
44 | This returns a JSON dictionary with information about this |
45 | MAAS instance. |
46 | { |
47 | + 'version': '1.8.0', |
48 | + 'subversion': 'alpha10+bzr3750', |
49 | 'capabilities': ['capability1', 'capability2', ...] |
50 | } |
51 | """ |
52 | @@ -46,8 +49,12 @@ |
53 | create = update = delete = None |
54 | |
55 | def read(self, request): |
56 | + version, subversion = get_maas_version_subversion() |
57 | version_info = { |
58 | 'capabilities': API_CAPABILITIES_LIST, |
59 | + 'version': version, |
60 | + 'subversion': subversion, |
61 | + |
62 | } |
63 | return HttpResponse( |
64 | version_info, mimetype='application/json; charset=utf-8', |
65 | |
66 | === added file 'src/maasserver/utils/tests/test_version.py' |
67 | --- src/maasserver/utils/tests/test_version.py 1970-01-01 00:00:00 +0000 |
68 | +++ src/maasserver/utils/tests/test_version.py 2015-04-08 15:34:24 +0000 |
69 | @@ -0,0 +1,217 @@ |
70 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
71 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
72 | + |
73 | +"""Test version utilities.""" |
74 | + |
75 | +from __future__ import ( |
76 | + absolute_import, |
77 | + print_function, |
78 | + unicode_literals, |
79 | + ) |
80 | + |
81 | +str = None |
82 | + |
83 | +__metaclass__ = type |
84 | +__all__ = [] |
85 | + |
86 | +import random |
87 | + |
88 | +from bzrlib.errors import NotBranchError |
89 | +from maasserver.utils import version |
90 | +from maastesting.matchers import MockCalledOnceWith |
91 | +from maastesting.testcase import MAASTestCase |
92 | +from mock import ( |
93 | + MagicMock, |
94 | + sentinel, |
95 | + ) |
96 | +from testtools.matchers import Is |
97 | + |
98 | + |
99 | +class TestGetVersionFromAPT(MAASTestCase): |
100 | + |
101 | + def test__creates_cache_with_None_progress(self): |
102 | + mock_Cache = self.patch(version.apt_pkg, "Cache") |
103 | + version.get_version_from_apt(version.REGION_PACKAGE_NAME) |
104 | + self.assertThat(mock_Cache, MockCalledOnceWith(None)) |
105 | + |
106 | + def test__returns_empty_string_if_package_not_in_cache(self): |
107 | + self.patch(version.apt_pkg, "Cache") |
108 | + self.assertEquals( |
109 | + "", |
110 | + version.get_version_from_apt(version.REGION_PACKAGE_NAME)) |
111 | + |
112 | + def test__returns_empty_string_if_not_current_ver_from_package(self): |
113 | + package = MagicMock() |
114 | + package.current_ver = None |
115 | + mock_cache = { |
116 | + version.REGION_PACKAGE_NAME: package, |
117 | + } |
118 | + self.patch(version.apt_pkg, "Cache").return_value = mock_cache |
119 | + self.assertEquals( |
120 | + "", |
121 | + version.get_version_from_apt(version.REGION_PACKAGE_NAME)) |
122 | + |
123 | + def test__returns_ver_str_from_package(self): |
124 | + package = MagicMock() |
125 | + package.current_ver.ver_str = sentinel.ver_str |
126 | + mock_cache = { |
127 | + version.REGION_PACKAGE_NAME: package, |
128 | + } |
129 | + self.patch(version.apt_pkg, "Cache").return_value = mock_cache |
130 | + self.assertIs( |
131 | + sentinel.ver_str, |
132 | + version.get_version_from_apt(version.REGION_PACKAGE_NAME)) |
133 | + |
134 | + |
135 | +class TestGetMAASBranch(MAASTestCase): |
136 | + |
137 | + def test__returns_None_if_Branch_is_None(self): |
138 | + self.patch(version, "Branch", None) |
139 | + self.assertIsNone(version.get_maas_branch()) |
140 | + |
141 | + def test__calls_Branch_open_with_current_dir(self): |
142 | + mock_open = self.patch(version.Branch, "open") |
143 | + mock_open.return_value = sentinel.branch |
144 | + self.expectThat(version.get_maas_branch(), Is(sentinel.branch)) |
145 | + self.expectThat(mock_open, MockCalledOnceWith(".")) |
146 | + |
147 | + def test__returns_None_on_NotBranchError(self): |
148 | + mock_open = self.patch(version.Branch, "open") |
149 | + mock_open.side_effect = NotBranchError("") |
150 | + self.assertIsNone(version.get_maas_branch()) |
151 | + |
152 | + |
153 | +class TestExtractVersionSubversion(MAASTestCase): |
154 | + |
155 | + scenarios = [ |
156 | + ("with ~", { |
157 | + "version": "1.8.0~alpha4+bzr356-0ubuntu1", |
158 | + "output": ("1.8.0", "alpha4+bzr356"), |
159 | + }), |
160 | + ("without ~", { |
161 | + "version": "1.8.0+bzr356-0ubuntu1", |
162 | + "output": ("1.8.0", "+bzr356"), |
163 | + }), |
164 | + ("without ~ or +", { |
165 | + "version": "1.8.0-0ubuntu1", |
166 | + "output": ("1.8.0", ""), |
167 | + }), |
168 | + ] |
169 | + |
170 | + def test__returns_version_subversion(self): |
171 | + self.assertEquals( |
172 | + self.output, version.extract_version_subversion(self.version)) |
173 | + |
174 | + |
175 | +class TestVersionTestCase(MAASTestCase): |
176 | + """MAASTestCase that resets the cache used by utility methods.""" |
177 | + |
178 | + def setUp(self): |
179 | + super(TestVersionTestCase, self).setUp() |
180 | + self.patch(version, "_cache", {}) |
181 | + |
182 | + |
183 | +class TestGetMAASPackageVersion(TestVersionTestCase): |
184 | + |
185 | + def test__calls_get_version_from_apt(self): |
186 | + mock_apt = self.patch(version, "get_version_from_apt") |
187 | + mock_apt.return_value = sentinel.version |
188 | + self.expectThat( |
189 | + version.get_maas_package_version(), Is(sentinel.version)) |
190 | + self.expectThat( |
191 | + mock_apt, MockCalledOnceWith(version.REGION_PACKAGE_NAME)) |
192 | + |
193 | + |
194 | +class TestGetMAASVersionSubversion(TestVersionTestCase): |
195 | + |
196 | + def test__returns_package_version(self): |
197 | + mock_apt = self.patch(version, "get_version_from_apt") |
198 | + mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1" |
199 | + self.assertEquals( |
200 | + ("1.8.0", "alpha4+bzr356"), version.get_maas_version_subversion()) |
201 | + |
202 | + def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self): |
203 | + mock_version = self.patch(version, "get_version_from_apt") |
204 | + mock_version.return_value = "" |
205 | + mock_branch = self.patch(version, "get_maas_branch") |
206 | + mock_branch.return_value = None |
207 | + self.assertEquals( |
208 | + ("unknown", ""), version.get_maas_version_subversion()) |
209 | + |
210 | + def test__returns_from_source_and_revno_from_branch(self): |
211 | + mock_version = self.patch(version, "get_version_from_apt") |
212 | + mock_version.return_value = "" |
213 | + revno = random.randint(1, 5000) |
214 | + mock_branch = self.patch(version, "get_maas_branch") |
215 | + mock_branch.return_value.revno.return_value = revno |
216 | + self.assertEquals( |
217 | + ("from source (+bzr%s)" % revno, ""), |
218 | + version.get_maas_version_subversion()) |
219 | + |
220 | + |
221 | +class TestGetMAASVersionUI(TestVersionTestCase): |
222 | + |
223 | + def test__returns_package_version(self): |
224 | + mock_apt = self.patch(version, "get_version_from_apt") |
225 | + mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1" |
226 | + self.assertEquals( |
227 | + "1.8.0 (alpha4+bzr356)", version.get_maas_version_ui()) |
228 | + |
229 | + def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self): |
230 | + mock_version = self.patch(version, "get_version_from_apt") |
231 | + mock_version.return_value = "" |
232 | + mock_branch = self.patch(version, "get_maas_branch") |
233 | + mock_branch.return_value = None |
234 | + self.assertEquals("unknown", version.get_maas_version_ui()) |
235 | + |
236 | + def test__returns_from_source_and_revno_from_branch(self): |
237 | + mock_version = self.patch(version, "get_version_from_apt") |
238 | + mock_version.return_value = "" |
239 | + revno = random.randint(1, 5000) |
240 | + mock_branch = self.patch(version, "get_maas_branch") |
241 | + mock_branch.return_value.revno.return_value = revno |
242 | + self.assertEquals( |
243 | + "from source (+bzr%s)" % revno, version.get_maas_version_ui()) |
244 | + |
245 | + |
246 | +class TestGetMAASDocVersion(TestVersionTestCase): |
247 | + |
248 | + def test__returns_doc_version_with_greater_than_1_decimals(self): |
249 | + mock_apt = self.patch(version, "get_version_from_apt") |
250 | + mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1" |
251 | + self.assertEquals("docs1.8", version.get_maas_doc_version()) |
252 | + |
253 | + def test__returns_doc_version_with_equal_to_1_decimals(self): |
254 | + mock_apt = self.patch(version, "get_version_from_apt") |
255 | + mock_apt.return_value = "1.8~alpha4+bzr356-0ubuntu1" |
256 | + self.assertEquals("docs1.8", version.get_maas_doc_version()) |
257 | + |
258 | + def test__returns_just_doc_if_version_is_empty(self): |
259 | + mock_apt = self.patch(version, "get_version_from_apt") |
260 | + mock_apt.return_value = "" |
261 | + self.assertEquals("docs", version.get_maas_doc_version()) |
262 | + |
263 | + |
264 | +class TestVersionMethodsCached(TestVersionTestCase): |
265 | + |
266 | + scenarios = [ |
267 | + ("get_maas_package_version", dict(method="get_maas_package_version")), |
268 | + ("get_maas_version_subversion", dict( |
269 | + method="get_maas_version_subversion")), |
270 | + ("get_maas_version_ui", dict(method="get_maas_version_ui")), |
271 | + ("get_maas_doc_version", dict(method="get_maas_doc_version")), |
272 | + ] |
273 | + |
274 | + def test_method_is_cached(self): |
275 | + mock_apt = self.patch(version, "get_version_from_apt") |
276 | + mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1" |
277 | + cached_method = getattr(version, self.method) |
278 | + first_return_value = cached_method() |
279 | + second_return_value = cached_method() |
280 | + # The return value is not empty (full unit tests have been performed |
281 | + # earlier). |
282 | + self.assertNotIn(first_return_value, [b'', u'', None]) |
283 | + self.assertEqual(first_return_value, second_return_value) |
284 | + # Apt has only been called once. |
285 | + self.expectThat( |
286 | + mock_apt, MockCalledOnceWith(version.REGION_PACKAGE_NAME)) |
287 | |
288 | === added file 'src/maasserver/utils/version.py' |
289 | --- src/maasserver/utils/version.py 1970-01-01 00:00:00 +0000 |
290 | +++ src/maasserver/utils/version.py 2015-04-08 15:34:24 +0000 |
291 | @@ -0,0 +1,131 @@ |
292 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
293 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
294 | + |
295 | +"""Version utilities.""" |
296 | + |
297 | +from __future__ import ( |
298 | + absolute_import, |
299 | + print_function, |
300 | + unicode_literals, |
301 | + ) |
302 | + |
303 | +str = None |
304 | + |
305 | +__metaclass__ = type |
306 | +__all__ = [ |
307 | + "get_maas_doc_version", |
308 | + "get_maas_version_subversion", |
309 | + "get_maas_version_ui", |
310 | + ] |
311 | + |
312 | +import apt_pkg |
313 | + |
314 | + |
315 | +try: |
316 | + from bzrlib.branch import Branch |
317 | + from bzrlib.errors import NotBranchError |
318 | +except ImportError: |
319 | + Branch = None |
320 | + |
321 | +# Initialize apt_pkg. |
322 | +apt_pkg.init() |
323 | + |
324 | +# Name of maas package to get version from. |
325 | +REGION_PACKAGE_NAME = "maas-region-controller-min" |
326 | + |
327 | + |
328 | +def get_version_from_apt(package): |
329 | + """Return the version output from `apt_pkg.Cache` for the given package.""" |
330 | + cache = apt_pkg.Cache(None) |
331 | + version = None |
332 | + if package in cache: |
333 | + apt_package = cache[package] |
334 | + version = apt_package.current_ver |
335 | + |
336 | + if version is not None: |
337 | + return version.ver_str |
338 | + else: |
339 | + return "" |
340 | + |
341 | + |
342 | +def extract_version_subversion(version): |
343 | + """Return a tuple (version, subversion) from the given apt version.""" |
344 | + if "~" in version: |
345 | + main_version, extra = version.split("~", 1) |
346 | + return main_version, extra.split("-", 1)[0] |
347 | + elif "+" in version: |
348 | + main_version, extra = version.split("+", 1) |
349 | + return main_version, "+" + extra.split("-", 1)[0] |
350 | + else: |
351 | + return version.split("-", 1)[0], '' |
352 | + |
353 | + |
354 | +def get_maas_branch(): |
355 | + """Return the `bzrlib.branch.Branch` for this running MAAS.""" |
356 | + if Branch is None: |
357 | + return None |
358 | + try: |
359 | + return Branch.open(".") |
360 | + except NotBranchError: |
361 | + return None |
362 | + |
363 | + |
364 | +_cache = {} |
365 | + |
366 | + |
367 | +# A very simply memoize function: when we switch to Django 1.7 we should use |
368 | +# Django's lru_cache method. |
369 | +def simple_cache(fun): |
370 | + def wrapped(*args, **kwargs): |
371 | + key = hash(repr(fun) + repr(args) + repr(kwargs)) |
372 | + if not key in _cache: |
373 | + _cache[key] = fun(*args, **kwargs) |
374 | + return _cache[key] |
375 | + |
376 | + wrapped.__doc__ = "%s %s" % (fun.__doc__, "(cached)") |
377 | + return wrapped |
378 | + |
379 | + |
380 | +@simple_cache |
381 | +def get_maas_package_version(): |
382 | + """Return the apt version for the main MAAS package.""" |
383 | + return get_version_from_apt(REGION_PACKAGE_NAME) |
384 | + |
385 | + |
386 | +@simple_cache |
387 | +def get_maas_version_subversion(): |
388 | + """Return a tuple with the MAAS version and the MAAS subversion.""" |
389 | + apt_version = get_maas_package_version() |
390 | + if apt_version: |
391 | + return extract_version_subversion(apt_version) |
392 | + else: |
393 | + # Get the branch information |
394 | + branch = get_maas_branch() |
395 | + if branch is None: |
396 | + # Not installed not in branch, then no way to identify. This should |
397 | + # not happen, but just in case. |
398 | + return "unknown", '' |
399 | + else: |
400 | + return "from source (+bzr%s)" % branch.revno(), '' |
401 | + |
402 | + |
403 | +@simple_cache |
404 | +def get_maas_version_ui(): |
405 | + """Return the version string for the running MAAS region. |
406 | + |
407 | + The returned string is suitable to display in the UI. |
408 | + """ |
409 | + version, subversion = get_maas_version_subversion() |
410 | + return "%s (%s)" % (version, subversion) if subversion else version |
411 | + |
412 | + |
413 | +@simple_cache |
414 | +def get_maas_doc_version(): |
415 | + """Return the doc version for the running MAAS region.""" |
416 | + doc_prefix = 'docs' |
417 | + apt_version = get_maas_package_version() |
418 | + if apt_version: |
419 | + version, _ = extract_version_subversion(apt_version) |
420 | + return doc_prefix + '.'.join(version.split('.')[:2]) |
421 | + else: |
422 | + return doc_prefix |
Simple backport, self-approving.