Merge lp:~rvb/maas/expose-version-api-1.7 into lp:maas/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
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.

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Simple backport, self-approving.

review: Approve

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

Subscribers

People subscribed via source and target branches

to all changes: