Merge lp:~blake-rouse/maas/add-version-to-ui into lp:~maas-committers/maas/trunk

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 3597
Proposed branch: lp:~blake-rouse/maas/add-version-to-ui
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 392 lines (+332/-2)
6 files modified
required-packages/base (+1/-0)
src/maasserver/context_processors.py (+6/-0)
src/maasserver/templates/maasserver/base.html (+1/-1)
src/maasserver/templates/maasserver/index.html (+1/-1)
src/maasserver/utils/tests/test_version.py (+194/-0)
src/maasserver/utils/version.py (+129/-0)
To merge this branch: bzr merge lp:~blake-rouse/maas/add-version-to-ui
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+251615@code.launchpad.net

Commit message

Show the installed version of MAAS on the WebUI.

This will show the installed version of the maas-region-controller package on the MAAS WebUI. If being ran from the developer environment it will show that it is from source and the revision number of the current branch.

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

On Tue, Mar 03, 2015 at 03:22:27PM -0000, Blake Rouse wrote:
> - <span class="version">MAAS</span> <a href="http://maas.ubuntu.com/docs1.7/changelog.html#id1">View release notes</a>|<a href="https://maas.ubuntu.com/documentation/">View documentation</a>
> + <span class="version">MAAS Version {{version}}</span> <a href="http://maas.ubuntu.com/docs1.7/changelog.html#id1">View release notes</a>|<a href="https://maas.ubuntu.com/documentation/">View documentation</a>

You may have considered this already but could we use the version string
to automatically select the right subdirectory in the URL?
--
Christian Robottom Reis | [+1] 612 888 4935 | http://launchpad.net/~kiko
Canonical VP Hyperscale | [+55 16] 9 9112 6430

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

Feel free to disagree but I feel the caching is done at the wrong level in this branch. The result is that apt will be called a lot when running from a branch.

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

Okay I made the required fixes and now have the documentation link working based on the version. More work then I wanted to do for this branch, but your welcome! :-)

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

A few comments (not a review):

- Instead of searching for a particular package name, how about
  searching for the package that owns the current file? That's more
  likely to be correct *and* it reduces the coupling between upstream
  and the package. For example:

    import inspect
    from os.path import realpath

    import apt

    def find_package_of(cache, filename):
        # This is quicker than you might think.
        for pkg in cache:
            if pkg.is_installed:
                if filename in pkg.installed_files:
                    return pkg

    cache = apt.Cache()
    source = inspect.getsourcefile(find_package_of)
    package = find_package_of(cache, realpath(source))
    if package is None:
        pass # Check for branch info.
    else:
        installed = package.installed
        # Use the *source* package name and version.
        desc = "%s %s" % (installed.source_name, installed.source_version)

- When reporting the branch version, also include the branch's location,
  or its name. This is less important to the developer, but could be
  very useful in screen-grabs, for example.

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

Those are all great suggestions but I don't want to spend anymore time on this branch. There are many more important things that need to be completed and spending more time on this branch would be a waste of the time I have available to complete more important features.

I just did this quickly to fix a annoying bug, that will help the UI.

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

> Those are all great suggestions but I don't want to spend anymore time on this branch.

All right then… I would have liked to see the caching problem fixed but it isn't critical.

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

The caching problem is fixed. It will set the MAAS_VERSION to "", causing it not to be reloaded after the first call.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'required-packages/base'
2--- required-packages/base 2015-01-20 15:56:10 +0000
3+++ required-packages/base 2015-03-03 16:09:28 +0000
4@@ -13,6 +13,7 @@
5 libjs-angularjs
6 libpq-dev
7 postgresql
8+python-apt
9 python-bson
10 python-bzrlib
11 python-convoy
12
13=== modified file 'src/maasserver/context_processors.py'
14--- src/maasserver/context_processors.py 2015-02-26 18:34:40 +0000
15+++ src/maasserver/context_processors.py 2015-03-03 16:09:28 +0000
16@@ -23,6 +23,10 @@
17 from maasserver.components import get_persistent_errors
18 from maasserver.forms import get_node_edit_form
19 from maasserver.models import Config
20+from maasserver.utils.version import (
21+ get_maas_doc_version,
22+ get_maas_version,
23+ )
24
25
26 def yui(context):
27@@ -86,4 +90,6 @@
28 'site_name': Config.objects.get_config('maas_name'),
29 },
30 'debug': settings.DEBUG,
31+ 'version': get_maas_version(),
32+ 'doc_version': get_maas_doc_version(),
33 }
34
35=== modified file 'src/maasserver/templates/maasserver/base.html'
36--- src/maasserver/templates/maasserver/base.html 2015-02-20 17:00:30 +0000
37+++ src/maasserver/templates/maasserver/base.html 2015-03-03 16:09:28 +0000
38@@ -123,7 +123,7 @@
39 <div class="legal-inner">
40 {% block footer-copyright %}
41 <p class="twelve-col">
42- <span class="version">MAAS</span> <a href="http://maas.ubuntu.com/docs1.7/changelog.html#id1">View release notes</a>|<a href="https://maas.ubuntu.com/documentation/">View documentation</a>
43+ <span class="version">MAAS Version {{version}}</span> <a href="http://maas.ubuntu.com/{{doc_version}}/changelog.html#id1">View release notes</a>|<a href="https://maas.ubuntu.com/{{doc_version}}/">View documentation</a>
44 </p>
45 <p class="twelve-col copy">&copy; 2015 Canonical Ltd. Ubuntu and Canonical are registered trademarks of Canonical Ltd.</p>
46 {% endblock %}
47
48=== modified file 'src/maasserver/templates/maasserver/index.html'
49--- src/maasserver/templates/maasserver/index.html 2015-02-20 17:00:30 +0000
50+++ src/maasserver/templates/maasserver/index.html 2015-03-03 16:09:28 +0000
51@@ -102,7 +102,7 @@
52 <div class="legal clearfix">
53 <div class="legal-inner">
54 <p class="twelve-col">
55- <span class="version">MAAS</span> <a href="http://maas.ubuntu.com/docs1.7/changelog.html#id1">View release notes</a>|<a href="https://maas.ubuntu.com/documentation/">View documentation</a>
56+ <span class="version">MAAS Version {{version}}</span> <a href="http://maas.ubuntu.com/{{doc_version}}/changelog.html#id1">View release notes</a>|<a href="https://maas.ubuntu.com/{{doc_version}}/">View documentation</a>
57 </p>
58 <p class="twelve-col copy">&copy; 2015 Canonical Ltd. Ubuntu and Canonical are registered trademarks of Canonical Ltd.</p>
59 </div>
60
61=== added file 'src/maasserver/utils/tests/test_version.py'
62--- src/maasserver/utils/tests/test_version.py 1970-01-01 00:00:00 +0000
63+++ src/maasserver/utils/tests/test_version.py 2015-03-03 16:09:28 +0000
64@@ -0,0 +1,194 @@
65+# Copyright 2015 Canonical Ltd. This software is licensed under the
66+# GNU Affero General Public License version 3 (see the file LICENSE).
67+
68+"""Test version utilities."""
69+
70+from __future__ import (
71+ absolute_import,
72+ print_function,
73+ unicode_literals,
74+ )
75+
76+str = None
77+
78+__metaclass__ = type
79+__all__ = []
80+
81+import random
82+
83+from bzrlib.errors import NotBranchError
84+from maasserver.utils import version
85+from maastesting.matchers import MockCalledOnceWith
86+from maastesting.testcase import MAASTestCase
87+from mock import (
88+ MagicMock,
89+ sentinel,
90+ )
91+from testtools.matchers import Is
92+
93+
94+class TestGetVersionFromAPT(MAASTestCase):
95+
96+ def test__returns_empty_string_if_package_not_in_cache(self):
97+ self.patch(version.apt_pkg, "Cache")
98+ self.assertEquals(
99+ "",
100+ version.get_version_from_apt(version.REGION_PACKAGE_NAME))
101+
102+ def test__returns_empty_string_if_not_current_ver_from_package(self):
103+ package = MagicMock()
104+ package.current_ver = None
105+ mock_cache = {
106+ version.REGION_PACKAGE_NAME: package,
107+ }
108+ self.patch(version.apt_pkg, "Cache").return_value = mock_cache
109+ self.assertEquals(
110+ "",
111+ version.get_version_from_apt(version.REGION_PACKAGE_NAME))
112+
113+ def test__returns_ver_str_from_package(self):
114+ package = MagicMock()
115+ package.current_ver.ver_str = sentinel.ver_str
116+ mock_cache = {
117+ version.REGION_PACKAGE_NAME: package,
118+ }
119+ self.patch(version.apt_pkg, "Cache").return_value = mock_cache
120+ self.assertIs(
121+ sentinel.ver_str,
122+ version.get_version_from_apt(version.REGION_PACKAGE_NAME))
123+
124+
125+class TestFormatVersion(MAASTestCase):
126+
127+ scenarios = [
128+ ("with ~", {
129+ "version": "1.8.0~alpha4+bzr356-0ubuntu1",
130+ "output": "1.8.0 (alpha4+bzr356)",
131+ }),
132+ ("without ~", {
133+ "version": "1.8.0+bzr356-0ubuntu1",
134+ "output": "1.8.0 (+bzr356)",
135+ }),
136+ ("without ~ or +", {
137+ "version": "1.8.0-0ubuntu1",
138+ "output": "1.8.0",
139+ }),
140+ ]
141+
142+ def test__returns_formatted_version(self):
143+ self.assertEquals(self.output, version.format_version(self.version))
144+
145+
146+class TestGetMAASRegionPackageVersion(MAASTestCase):
147+
148+ def test__returns_value_from_global(self):
149+ self.patch(version, "MAAS_VERSION", sentinel.maas_version)
150+ self.assertIs(
151+ sentinel.maas_version, version.get_maas_region_package_version())
152+
153+ def test__calls_get_version_from_apt_if_global_not_set(self):
154+ self.patch(version, "MAAS_VERSION", None)
155+ mock_apt = self.patch(version, "get_version_from_apt")
156+ version.get_maas_region_package_version()
157+ self.assertThat(
158+ mock_apt, MockCalledOnceWith(version.REGION_PACKAGE_NAME))
159+
160+ def test__calls_format_version_with_version_from_apt(self):
161+ self.patch(version, "MAAS_VERSION", None)
162+ current_ver = "1.8.0~alpha4+bzr356-0ubuntu1"
163+ mock_apt = self.patch(version, "get_version_from_apt")
164+ mock_apt.return_value = current_ver
165+ mock_format = self.patch(version, "format_version")
166+ mock_format.return_value = sentinel.format
167+ self.expectThat(
168+ version.get_maas_region_package_version(), Is(sentinel.format))
169+ self.expectThat(
170+ mock_format, MockCalledOnceWith(current_ver))
171+
172+ def test__sets_global_value(self):
173+ self.patch(version, "MAAS_VERSION", None)
174+ current_ver = "1.8.0~alpha4+bzr356-0ubuntu1"
175+ mock_apt = self.patch(version, "get_version_from_apt")
176+ mock_apt.return_value = current_ver
177+ mock_format = self.patch(version, "format_version")
178+ mock_format.return_value = sentinel.format
179+ version.get_maas_region_package_version()
180+ self.assertIs(sentinel.format, version.MAAS_VERSION)
181+
182+
183+class TestGetMAASBranch(MAASTestCase):
184+
185+ def test__returns_None_if_Branch_is_None(self):
186+ self.patch(version, "Branch", None)
187+ self.assertIsNone(version.get_maas_branch())
188+
189+ def test__calls_Branch_open_with_current_dir(self):
190+ mock_open = self.patch(version.Branch, "open")
191+ mock_open.return_value = sentinel.branch
192+ self.expectThat(version.get_maas_branch(), Is(sentinel.branch))
193+ self.expectThat(mock_open, MockCalledOnceWith("."))
194+
195+ def test__returns_None_on_NotBranchError(self):
196+ mock_open = self.patch(version.Branch, "open")
197+ mock_open.side_effect = NotBranchError("")
198+ self.assertIsNone(version.get_maas_branch())
199+
200+
201+class TestGetMAASVersion(MAASTestCase):
202+
203+ def test__returns_version_from_get_maas_region_package_version(self):
204+ mock_version = self.patch(version, "get_maas_region_package_version")
205+ mock_version.return_value = sentinel.version
206+ self.assertIs(sentinel.version, version.get_maas_version())
207+
208+ def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
209+ mock_version = self.patch(version, "get_maas_region_package_version")
210+ mock_version.return_value = ""
211+ mock_branch = self.patch(version, "get_maas_branch")
212+ mock_branch.return_value = None
213+ self.assertEquals("unknown", version.get_maas_version())
214+
215+ def test__returns_from_source_and_revno_from_branch(self):
216+ mock_version = self.patch(version, "get_maas_region_package_version")
217+ mock_version.return_value = ""
218+ revno = random.randint(1, 5000)
219+ mock_branch = self.patch(version, "get_maas_branch")
220+ mock_branch.return_value.revno.return_value = revno
221+ self.assertEquals(
222+ "from source (+bzr%s)" % revno, version.get_maas_version())
223+
224+
225+class TestGetMAASMainVersion(MAASTestCase):
226+
227+ def test__returns_main_version_from_package_version_with_space(self):
228+ mock_version = self.patch(version, "get_maas_region_package_version")
229+ mock_version.return_value = "1.8.0 (alpha4+bzr356)"
230+ self.assertEquals("1.8.0", version.get_maas_main_version())
231+
232+ def test__returns_main_version_from_package_version_without_space(self):
233+ mock_version = self.patch(version, "get_maas_region_package_version")
234+ mock_version.return_value = "1.8.0"
235+ self.assertEquals("1.8.0", version.get_maas_main_version())
236+
237+ def test__returns_empty_if_version_is_empty(self):
238+ mock_version = self.patch(version, "get_maas_region_package_version")
239+ mock_version.return_value = ""
240+ self.assertEquals("", version.get_maas_main_version())
241+
242+
243+class TestGetMAASDocVersion(MAASTestCase):
244+
245+ def test__returns_doc_version_with_greater_than_1_decimals(self):
246+ mock_version = self.patch(version, "get_maas_main_version")
247+ mock_version.return_value = "1.8.0"
248+ self.assertEquals("doc1.8", version.get_maas_doc_version())
249+
250+ def test__returns_doc_version_with_equal_to_1_decimals(self):
251+ mock_version = self.patch(version, "get_maas_main_version")
252+ mock_version.return_value = "1.8"
253+ self.assertEquals("doc1.8", version.get_maas_doc_version())
254+
255+ def test__returns_just_doc_if_version_is_empty(self):
256+ mock_version = self.patch(version, "get_maas_main_version")
257+ mock_version.return_value = ""
258+ self.assertEquals("doc", version.get_maas_doc_version())
259
260=== added file 'src/maasserver/utils/version.py'
261--- src/maasserver/utils/version.py 1970-01-01 00:00:00 +0000
262+++ src/maasserver/utils/version.py 2015-03-03 16:09:28 +0000
263@@ -0,0 +1,129 @@
264+# Copyright 2015 Canonical Ltd. This software is licensed under the
265+# GNU Affero General Public License version 3 (see the file LICENSE).
266+
267+"""Version utilities."""
268+
269+from __future__ import (
270+ absolute_import,
271+ print_function,
272+ unicode_literals,
273+ )
274+
275+str = None
276+
277+__metaclass__ = type
278+__all__ = [
279+ ]
280+
281+import apt_pkg
282+
283+
284+try:
285+ from bzrlib.branch import Branch
286+ from bzrlib.errors import NotBranchError
287+except ImportError:
288+ Branch = None
289+
290+# Initialize apt_pkg.
291+apt_pkg.init()
292+
293+# Holds the version of maas.
294+MAAS_VERSION = None
295+
296+# Name of maas package to get version from.
297+REGION_PACKAGE_NAME = "maas-region-controller"
298+
299+
300+def get_version_from_apt(package):
301+ """Return the version output from `apt_pkg.Cache` for the given package."""
302+ cache = apt_pkg.Cache()
303+ version = None
304+ if package in cache:
305+ apt_package = cache[package]
306+ version = apt_package.current_ver
307+
308+ if version is not None:
309+ return version.ver_str
310+ else:
311+ return ""
312+
313+
314+def format_version(version):
315+ """Format `version` into a better looking string for the UI."""
316+ if "~" in version:
317+ main_version, extra = version.split("~", 1)
318+ return "%s (%s)" % (main_version, extra.split("-", 1)[0])
319+ elif "+" in version:
320+ main_version, extra = version.split("+", 1)
321+ return "%s (+%s)" % (main_version, extra.split("-", 1)[0])
322+ else:
323+ return version.split("-", 1)[0]
324+
325+
326+def get_maas_region_package_version():
327+ """Return the dpkg version for `REGION_PACKAGE_NAME`.
328+
329+ Lazy loads the version. Once loaded it will not be loaded again.
330+ This is to speed up the call to this method and also to require the
331+ region to be restarted to see the new version.
332+ """
333+ global MAAS_VERSION
334+ if MAAS_VERSION is None:
335+ MAAS_VERSION = get_version_from_apt(REGION_PACKAGE_NAME)
336+ # Possible this returned an empty string, meaning its not installed
337+ # and no need to call again.
338+ if MAAS_VERSION:
339+ # It is a valid version so set the correct format.
340+ MAAS_VERSION = format_version(MAAS_VERSION)
341+ return MAAS_VERSION
342+
343+
344+def get_maas_branch():
345+ """Return the `bzrlib.branch.Branch` for this running MAAS."""
346+ if Branch is None:
347+ return None
348+ try:
349+ return Branch.open(".")
350+ except NotBranchError:
351+ return None
352+
353+
354+def get_maas_version():
355+ """Return the version string for the running MAAS region."""
356+ # MAAS is installed from package, return the version string.
357+ version = get_maas_region_package_version()
358+ if version:
359+ return version
360+ else:
361+ # Get the branch information
362+ branch = get_maas_branch()
363+ if branch is None:
364+ # Not installed not in branch, then no way to identify. This should
365+ # not happen, but just in case.
366+ return "unknown"
367+ else:
368+ return "from source (+bzr%s)" % branch.revno()
369+
370+
371+def get_maas_main_version():
372+ """Return the main version for the running MAAS region."""
373+ # MAAS is installed from package, return the version string.
374+ version = get_maas_region_package_version()
375+ if version:
376+ if " " in version:
377+ return version.split(" ")[0]
378+ else:
379+ return version
380+ else:
381+ return ""
382+
383+
384+def get_maas_doc_version():
385+ """Return the doc version for the running MAAS region."""
386+ main_version = get_maas_main_version()
387+ if not main_version:
388+ return "doc"
389+ if main_version.count('.') > 1:
390+ return "doc%s" % '.'.join(main_version.split('.')[:2])
391+ else:
392+ return "doc%s" % main_version