Merge lp:~rvb/maas/expose-version-api into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 3794
Proposed branch: lp:~rvb/maas/expose-version-api
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 496 lines (+197/-164)
5 files modified
src/maasserver/api/tests/test_version.py (+7/-0)
src/maasserver/api/version.py (+7/-0)
src/maasserver/context_processors.py (+2/-2)
src/maasserver/utils/tests/test_version.py (+125/-107)
src/maasserver/utils/version.py (+56/-55)
To merge this branch: bzr merge lp:~rvb/maas/expose-version-api
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+255357@code.launchpad.net

Commit message

Expose the version and the subversion of the running MAAS on the API.

Description of the change

Note that I'm doing a bit more caching in version.py than would be expected: I want to cache things at the low level so that calls to display the version in the UI and calls to return the version in the API call the API backend only once but I also want to cache the result of the (tiny) processing done by get_maas_version_ui() to avoid doing the string interpolation for every UI page requested.

To post a comment you must log in.
Revision history for this message
Christian Reis (kiko) wrote :

Is there any chance this could be backported to 1.7?

Revision history for this message
Blake Rouse (blake-rouse) wrote :

No complaints. Only think the simple_cache decorator would be better in its own file.

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :

Thanks for the review!

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (86.4 KiB)

The attempt to merge lp:~rvb/maas/expose-version-api into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Get:2 http://security.ubuntu.com trusty-security Release [63.5 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:3 http://security.ubuntu.com trusty-security/main Sources [76.5 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Get:5 http://security.ubuntu.com trusty-security/universe Sources [19.5 kB]
Get:6 http://security.ubuntu.com trusty-security/main amd64 Packages [251 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:7 http://nova.clouds.archive.ubuntu.com trusty-updates Release [63.5 kB]
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [92.0 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [190 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [108 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [490 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [263 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 1,621 kB in 3s (526 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm pep8 postgresql pyflakes python-apt python-bson python-bzrlib python-convoy python-coverage python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-iscpy python-jinja2 python-jsonschema python-lockfile python-lxml python-mock python-netaddr python-netifaces python-nose python-oau...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (83.7 KiB)

The attempt to merge lp:~rvb/maas/expose-version-api into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Hit http://security.ubuntu.com trusty-security Release
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm pep8 postgresql pyflakes python-apt python-bson python-bzrlib python-convoy python-coverage python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-iscpy python-jinja2 python-jsonschema python-lockfile python-lxml python-mock python-netaddr python-netifaces python-nose python-oauth python-openssl python-paramiko python-pexpect python-pip python-pocket-lint python-psycopg2 python-pyinotify python-pyparsing python-seamicroclient python-simplejson pyt...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (82.8 KiB)

The attempt to merge lp:~rvb/maas/expose-version-api into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Get:2 http://security.ubuntu.com trusty-security Release [63.5 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [63.5 kB]
Get:5 http://security.ubuntu.com trusty-security/main Sources [76.4 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:6 http://security.ubuntu.com trusty-security/universe Sources [19.5 kB]
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [252 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:8 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [190 kB]
Get:9 http://security.ubuntu.com trusty-security/universe amd64 Packages [92.0 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [108 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [490 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [263 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 1,621 kB in 3s (524 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm pep8 postgresql pyflakes python-apt python-bson python-bzrlib python-convoy python-coverage python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-iscpy python-jinja2 python-jsonschema python-lockfile python-lxml python-mock python-netaddr python-netifaces python-nose python-oau...

Revision history for this message
MAAS Lander (maas-lander) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Revision history for this message
Gavin Panella (allenap) wrote :

Not a review, but a few comments (and one in the diff):

* Is "subversion" the best term. People often talk about major and minor
  versions.

* The distutils.version module might be of interest.

* One of the explicit reasons for the capabilities views was to
  *discourage* use of version numbers when testing for features. Is this
  therefore a regression? What's the use case?

Revision history for this message
Raphaël Badin (rvb) wrote :

Hey Gavin

> Not a review, but a few comments (and one in the diff):

> * Is "subversion" the best term. People often talk about major and minor
> versions.

AFAIK, major and minor are used for something different: "1.8.0" → major: 1, minor: 8. Here we really want version/subversion I think.

> * One of the explicit reasons for the capabilities views was to
> *discourage* use of version numbers when testing for features. Is this
> therefore a regression? What's the use case?

The capabilities are the thing other tools like Juju should use to check for functionality but some times it's useful (mostly when debugging) to be able to know exactly what the version is in order to, say, figure out if a particular bug fix is present.

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:11:40 +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:11:40 +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=== modified file 'src/maasserver/context_processors.py'
67--- src/maasserver/context_processors.py 2015-04-02 14:18:18 +0000
68+++ src/maasserver/context_processors.py 2015-04-08 15:11:40 +0000
69@@ -23,7 +23,7 @@
70 from maasserver.models import Config
71 from maasserver.utils.version import (
72 get_maas_doc_version,
73- get_maas_version,
74+ get_maas_version_ui,
75 )
76
77
78@@ -88,6 +88,6 @@
79 'site_name': Config.objects.get_config('maas_name'),
80 },
81 'debug': settings.DEBUG,
82- 'version': get_maas_version(),
83+ 'version': get_maas_version_ui(),
84 'doc_version': get_maas_doc_version(),
85 }
86
87=== modified file 'src/maasserver/utils/tests/test_version.py'
88--- src/maasserver/utils/tests/test_version.py 2015-03-30 19:38:57 +0000
89+++ src/maasserver/utils/tests/test_version.py 2015-04-08 15:11:40 +0000
90@@ -63,64 +63,6 @@
91 version.get_version_from_apt(version.REGION_PACKAGE_NAME))
92
93
94-class TestFormatVersion(MAASTestCase):
95-
96- scenarios = [
97- ("with ~", {
98- "version": "1.8.0~alpha4+bzr356-0ubuntu1",
99- "output": "1.8.0 (alpha4+bzr356)",
100- }),
101- ("without ~", {
102- "version": "1.8.0+bzr356-0ubuntu1",
103- "output": "1.8.0 (+bzr356)",
104- }),
105- ("without ~ or +", {
106- "version": "1.8.0-0ubuntu1",
107- "output": "1.8.0",
108- }),
109- ]
110-
111- def test__returns_formatted_version(self):
112- self.assertEquals(self.output, version.format_version(self.version))
113-
114-
115-class TestGetMAASRegionPackageVersion(MAASTestCase):
116-
117- def test__returns_value_from_global(self):
118- self.patch(version, "MAAS_VERSION", sentinel.maas_version)
119- self.assertIs(
120- sentinel.maas_version, version.get_maas_region_package_version())
121-
122- def test__calls_get_version_from_apt_if_global_not_set(self):
123- self.patch(version, "MAAS_VERSION", None)
124- mock_apt = self.patch(version, "get_version_from_apt")
125- version.get_maas_region_package_version()
126- self.assertThat(
127- mock_apt, MockCalledOnceWith(version.REGION_PACKAGE_NAME))
128-
129- def test__calls_format_version_with_version_from_apt(self):
130- self.patch(version, "MAAS_VERSION", None)
131- current_ver = "1.8.0~alpha4+bzr356-0ubuntu1"
132- mock_apt = self.patch(version, "get_version_from_apt")
133- mock_apt.return_value = current_ver
134- mock_format = self.patch(version, "format_version")
135- mock_format.return_value = sentinel.format
136- self.expectThat(
137- version.get_maas_region_package_version(), Is(sentinel.format))
138- self.expectThat(
139- mock_format, MockCalledOnceWith(current_ver))
140-
141- def test__sets_global_value(self):
142- self.patch(version, "MAAS_VERSION", None)
143- current_ver = "1.8.0~alpha4+bzr356-0ubuntu1"
144- mock_apt = self.patch(version, "get_version_from_apt")
145- mock_apt.return_value = current_ver
146- mock_format = self.patch(version, "format_version")
147- mock_format.return_value = sentinel.format
148- version.get_maas_region_package_version()
149- self.assertIs(sentinel.format, version.MAAS_VERSION)
150-
151-
152 class TestGetMAASBranch(MAASTestCase):
153
154 def test__returns_None_if_Branch_is_None(self):
155@@ -139,61 +81,137 @@
156 self.assertIsNone(version.get_maas_branch())
157
158
159-class TestGetMAASVersion(MAASTestCase):
160-
161- def test__returns_version_from_get_maas_region_package_version(self):
162- mock_version = self.patch(version, "get_maas_region_package_version")
163- mock_version.return_value = sentinel.version
164- self.assertIs(sentinel.version, version.get_maas_version())
165-
166- def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
167- mock_version = self.patch(version, "get_maas_region_package_version")
168- mock_version.return_value = ""
169- mock_branch = self.patch(version, "get_maas_branch")
170- mock_branch.return_value = None
171- self.assertEquals("unknown", version.get_maas_version())
172-
173- def test__returns_from_source_and_revno_from_branch(self):
174- mock_version = self.patch(version, "get_maas_region_package_version")
175- mock_version.return_value = ""
176- revno = random.randint(1, 5000)
177- mock_branch = self.patch(version, "get_maas_branch")
178- mock_branch.return_value.revno.return_value = revno
179- self.assertEquals(
180- "from source (+bzr%s)" % revno, version.get_maas_version())
181-
182-
183-class TestGetMAASMainVersion(MAASTestCase):
184-
185- def test__returns_main_version_from_package_version_with_space(self):
186- mock_version = self.patch(version, "get_maas_region_package_version")
187- mock_version.return_value = "1.8.0 (alpha4+bzr356)"
188- self.assertEquals("1.8.0", version.get_maas_main_version())
189-
190- def test__returns_main_version_from_package_version_without_space(self):
191- mock_version = self.patch(version, "get_maas_region_package_version")
192- mock_version.return_value = "1.8.0"
193- self.assertEquals("1.8.0", version.get_maas_main_version())
194-
195- def test__returns_empty_if_version_is_empty(self):
196- mock_version = self.patch(version, "get_maas_region_package_version")
197- mock_version.return_value = ""
198- self.assertEquals("", version.get_maas_main_version())
199-
200-
201-class TestGetMAASDocVersion(MAASTestCase):
202+class TestExtractVersionSubversion(MAASTestCase):
203+
204+ scenarios = [
205+ ("with ~", {
206+ "version": "1.8.0~alpha4+bzr356-0ubuntu1",
207+ "output": ("1.8.0", "alpha4+bzr356"),
208+ }),
209+ ("without ~", {
210+ "version": "1.8.0+bzr356-0ubuntu1",
211+ "output": ("1.8.0", "+bzr356"),
212+ }),
213+ ("without ~ or +", {
214+ "version": "1.8.0-0ubuntu1",
215+ "output": ("1.8.0", ""),
216+ }),
217+ ]
218+
219+ def test__returns_version_subversion(self):
220+ self.assertEquals(
221+ self.output, version.extract_version_subversion(self.version))
222+
223+
224+class TestVersionTestCase(MAASTestCase):
225+ """MAASTestCase that resets the cache used by utility methods."""
226+
227+ def setUp(self):
228+ super(TestVersionTestCase, self).setUp()
229+ self.patch(version, "_cache", {})
230+
231+
232+class TestGetMAASPackageVersion(TestVersionTestCase):
233+
234+ def test__calls_get_version_from_apt(self):
235+ mock_apt = self.patch(version, "get_version_from_apt")
236+ mock_apt.return_value = sentinel.version
237+ self.expectThat(
238+ version.get_maas_package_version(), Is(sentinel.version))
239+ self.expectThat(
240+ mock_apt, MockCalledOnceWith(version.REGION_PACKAGE_NAME))
241+
242+
243+class TestGetMAASVersionSubversion(TestVersionTestCase):
244+
245+ def test__returns_package_version(self):
246+ mock_apt = self.patch(version, "get_version_from_apt")
247+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
248+ self.assertEquals(
249+ ("1.8.0", "alpha4+bzr356"), version.get_maas_version_subversion())
250+
251+ def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
252+ mock_version = self.patch(version, "get_version_from_apt")
253+ mock_version.return_value = ""
254+ mock_branch = self.patch(version, "get_maas_branch")
255+ mock_branch.return_value = None
256+ self.assertEquals(
257+ ("unknown", ""), version.get_maas_version_subversion())
258+
259+ def test__returns_from_source_and_revno_from_branch(self):
260+ mock_version = self.patch(version, "get_version_from_apt")
261+ mock_version.return_value = ""
262+ revno = random.randint(1, 5000)
263+ mock_branch = self.patch(version, "get_maas_branch")
264+ mock_branch.return_value.revno.return_value = revno
265+ self.assertEquals(
266+ ("from source (+bzr%s)" % revno, ""),
267+ version.get_maas_version_subversion())
268+
269+
270+class TestGetMAASVersionUI(TestVersionTestCase):
271+
272+ def test__returns_package_version(self):
273+ mock_apt = self.patch(version, "get_version_from_apt")
274+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
275+ self.assertEquals(
276+ "1.8.0 (alpha4+bzr356)", version.get_maas_version_ui())
277+
278+ def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
279+ mock_version = self.patch(version, "get_version_from_apt")
280+ mock_version.return_value = ""
281+ mock_branch = self.patch(version, "get_maas_branch")
282+ mock_branch.return_value = None
283+ self.assertEquals("unknown", version.get_maas_version_ui())
284+
285+ def test__returns_from_source_and_revno_from_branch(self):
286+ mock_version = self.patch(version, "get_version_from_apt")
287+ mock_version.return_value = ""
288+ revno = random.randint(1, 5000)
289+ mock_branch = self.patch(version, "get_maas_branch")
290+ mock_branch.return_value.revno.return_value = revno
291+ self.assertEquals(
292+ "from source (+bzr%s)" % revno, version.get_maas_version_ui())
293+
294+
295+class TestGetMAASDocVersion(TestVersionTestCase):
296
297 def test__returns_doc_version_with_greater_than_1_decimals(self):
298- mock_version = self.patch(version, "get_maas_main_version")
299- mock_version.return_value = "1.8.0"
300+ mock_apt = self.patch(version, "get_version_from_apt")
301+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
302 self.assertEquals("docs1.8", version.get_maas_doc_version())
303
304 def test__returns_doc_version_with_equal_to_1_decimals(self):
305- mock_version = self.patch(version, "get_maas_main_version")
306- mock_version.return_value = "1.8"
307+ mock_apt = self.patch(version, "get_version_from_apt")
308+ mock_apt.return_value = "1.8~alpha4+bzr356-0ubuntu1"
309 self.assertEquals("docs1.8", version.get_maas_doc_version())
310
311 def test__returns_just_doc_if_version_is_empty(self):
312- mock_version = self.patch(version, "get_maas_main_version")
313- mock_version.return_value = ""
314+ mock_apt = self.patch(version, "get_version_from_apt")
315+ mock_apt.return_value = ""
316 self.assertEquals("docs", version.get_maas_doc_version())
317+
318+
319+class TestVersionMethodsCached(TestVersionTestCase):
320+
321+ scenarios = [
322+ ("get_maas_package_version", dict(method="get_maas_package_version")),
323+ ("get_maas_version_subversion", dict(
324+ method="get_maas_version_subversion")),
325+ ("get_maas_version_ui", dict(method="get_maas_version_ui")),
326+ ("get_maas_doc_version", dict(method="get_maas_doc_version")),
327+ ]
328+
329+ def test_method_is_cached(self):
330+ mock_apt = self.patch(version, "get_version_from_apt")
331+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
332+ cached_method = getattr(version, self.method)
333+ first_return_value = cached_method()
334+ second_return_value = cached_method()
335+ # The return value is not empty (full unit tests have been performed
336+ # earlier).
337+ self.assertNotIn(first_return_value, [b'', u'', None])
338+ self.assertEqual(first_return_value, second_return_value)
339+ # Apt has only been called once.
340+ self.expectThat(
341+ mock_apt, MockCalledOnceWith(version.REGION_PACKAGE_NAME))
342
343=== modified file 'src/maasserver/utils/version.py'
344--- src/maasserver/utils/version.py 2015-03-30 17:54:35 +0000
345+++ src/maasserver/utils/version.py 2015-04-08 15:11:40 +0000
346@@ -14,7 +14,8 @@
347 __metaclass__ = type
348 __all__ = [
349 "get_maas_doc_version",
350- "get_maas_version",
351+ "get_maas_version_subversion",
352+ "get_maas_version_ui",
353 ]
354
355 import apt_pkg
356@@ -29,9 +30,6 @@
357 # Initialize apt_pkg.
358 apt_pkg.init()
359
360-# Holds the version of maas.
361-MAAS_VERSION = None
362-
363 # Name of maas package to get version from.
364 REGION_PACKAGE_NAME = "maas-region-controller-min"
365
366@@ -50,34 +48,16 @@
367 return ""
368
369
370-def format_version(version):
371- """Format `version` into a better looking string for the UI."""
372+def extract_version_subversion(version):
373+ """Return a tuple (version, subversion) from the given apt version."""
374 if "~" in version:
375 main_version, extra = version.split("~", 1)
376- return "%s (%s)" % (main_version, extra.split("-", 1)[0])
377+ return main_version, extra.split("-", 1)[0]
378 elif "+" in version:
379 main_version, extra = version.split("+", 1)
380- return "%s (+%s)" % (main_version, extra.split("-", 1)[0])
381+ return main_version, "+" + extra.split("-", 1)[0]
382 else:
383- return version.split("-", 1)[0]
384-
385-
386-def get_maas_region_package_version():
387- """Return the dpkg version for `REGION_PACKAGE_NAME`.
388-
389- Lazy loads the version. Once loaded it will not be loaded again.
390- This is to speed up the call to this method and also to require the
391- region to be restarted to see the new version.
392- """
393- global MAAS_VERSION
394- if MAAS_VERSION is None:
395- MAAS_VERSION = get_version_from_apt(REGION_PACKAGE_NAME)
396- # Possibly this returned an empty string, meaning it's not installed
397- # and no need to call again.
398- if MAAS_VERSION:
399- # It is a valid version so set the correct format.
400- MAAS_VERSION = format_version(MAAS_VERSION)
401- return MAAS_VERSION
402+ return version.split("-", 1)[0], ''
403
404
405 def get_maas_branch():
406@@ -90,41 +70,62 @@
407 return None
408
409
410-def get_maas_version():
411- """Return the version string for the running MAAS region."""
412- # MAAS is installed from package, return the version string.
413- version = get_maas_region_package_version()
414- if version:
415- return version
416+_cache = {}
417+
418+
419+# A very simply memoize function: when we switch to Django 1.7 we should use
420+# Django's lru_cache method.
421+def simple_cache(fun):
422+ def wrapped(*args, **kwargs):
423+ key = hash(repr(fun) + repr(args) + repr(kwargs))
424+ if not key in _cache:
425+ _cache[key] = fun(*args, **kwargs)
426+ return _cache[key]
427+
428+ wrapped.__doc__ = "%s %s" % (fun.__doc__, "(cached)")
429+ return wrapped
430+
431+
432+@simple_cache
433+def get_maas_package_version():
434+ """Return the apt version for the main MAAS package."""
435+ return get_version_from_apt(REGION_PACKAGE_NAME)
436+
437+
438+@simple_cache
439+def get_maas_version_subversion():
440+ """Return a tuple with the MAAS version and the MAAS subversion."""
441+ apt_version = get_maas_package_version()
442+ if apt_version:
443+ return extract_version_subversion(apt_version)
444 else:
445 # Get the branch information
446 branch = get_maas_branch()
447 if branch is None:
448 # Not installed not in branch, then no way to identify. This should
449 # not happen, but just in case.
450- return "unknown"
451- else:
452- return "from source (+bzr%s)" % branch.revno()
453-
454-
455-def get_maas_main_version():
456- """Return the main version for the running MAAS region."""
457- # MAAS is installed from package, return the version string.
458- version = get_maas_region_package_version()
459- if version:
460- if " " in version:
461- return version.split(" ")[0]
462- else:
463- return version
464- else:
465- return ""
466-
467-
468+ return "unknown", ''
469+ else:
470+ return "from source (+bzr%s)" % branch.revno(), ''
471+
472+
473+@simple_cache
474+def get_maas_version_ui():
475+ """Return the version string for the running MAAS region.
476+
477+ The returned string is suitable to display in the UI.
478+ """
479+ version, subversion = get_maas_version_subversion()
480+ return "%s (%s)" % (version, subversion) if subversion else version
481+
482+
483+@simple_cache
484 def get_maas_doc_version():
485 """Return the doc version for the running MAAS region."""
486- main_version = get_maas_main_version()
487 doc_prefix = 'docs'
488- if not main_version:
489+ apt_version = get_maas_package_version()
490+ if apt_version:
491+ version, _ = extract_version_subversion(apt_version)
492+ return doc_prefix + '.'.join(version.split('.')[:2])
493+ else:
494 return doc_prefix
495- else:
496- return doc_prefix + '.'.join(main_version.split('.')[:2])