Merge ~mpontillo/maas:rack-version-utils into maas:master

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: f660088482f7947bda6654935deb03c382b7f5df
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~mpontillo/maas:rack-version-utils
Merge into: maas:master
Diff against target: 1137 lines (+461/-159)
17 files modified
dev/null (+0/-142)
src/maasserver/__init__.py (+3/-1)
src/maasserver/api/tests/test_version.py (+1/-1)
src/maasserver/api/version.py (+1/-1)
src/maasserver/bootresources.py (+4/-4)
src/maasserver/bootsources.py (+1/-1)
src/maasserver/context_processors.py (+1/-1)
src/maasserver/tests/test_bootresources.py (+1/-1)
src/maasserver/tests/test_bootsources.py (+1/-1)
src/maasserver/websockets/handlers/bootresource.py (+1/-1)
src/maasserver/websockets/handlers/general.py (+1/-1)
src/maasserver/websockets/handlers/tests/test_bootresource.py (+1/-1)
src/metadataserver/builtin_scripts/__init__.py (+1/-1)
src/metadataserver/builtin_scripts/tests/test_builtin_scripts.py (+1/-1)
src/provisioningserver/utils/tests/test_version.py (+293/-0)
src/provisioningserver/utils/version.py (+149/-0)
utilities/check-imports (+1/-1)
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Newell Jensen Pending
Review via email: mp+327333@code.launchpad.net

This proposal supersedes a proposal from 2017-07-12.

Commit message

Make version check utilities work for the rack.

This allows rack-only installations to determine their version.

Also moves code to determine MAAS version to the src/provisioningserver.

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote : Posted in a previous version of this proposal

Looks good. Just one small suggestion.

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Self-approving previously approved branch. (I resubmitted to land two related branches together.)

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
~mpontillo/maas:rack-version-utils updated
f660088... by Mike Pontillo

Fix imports.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/__init__.py b/src/maasserver/__init__.py
2index cd6b4d6..7d85981 100644
3--- a/src/maasserver/__init__.py
4+++ b/src/maasserver/__init__.py
5@@ -13,8 +13,10 @@ __all__ = [
6 import logging
7 from os import environ
8
9+from provisioningserver.utils import version
10
11-__version__ = '2.3.0'
12+
13+__version__ = version.DEFAULT_VERSION
14
15 default_app_config = 'maasserver.apps.MAASConfig'
16
17diff --git a/src/maasserver/api/tests/test_version.py b/src/maasserver/api/tests/test_version.py
18index b9204d2..defa1fb 100644
19--- a/src/maasserver/api/tests/test_version.py
20+++ b/src/maasserver/api/tests/test_version.py
21@@ -13,7 +13,7 @@ from django.conf import settings
22 from django.core.urlresolvers import reverse
23 from maasserver.api.version import API_CAPABILITIES_LIST
24 from maasserver.testing.api import APITestCase
25-from maasserver.utils import version as version_module
26+from provisioningserver.utils import version as version_module
27
28
29 class TestVersionAPIBasics(APITestCase.ForAnonymousAndUserAndAdmin):
30diff --git a/src/maasserver/api/version.py b/src/maasserver/api/version.py
31index 3fc557b..77bb317 100644
32--- a/src/maasserver/api/version.py
33+++ b/src/maasserver/api/version.py
34@@ -12,7 +12,7 @@ import json
35
36 from django.http import HttpResponse
37 from maasserver.api.support import AnonymousOperationsHandler
38-from maasserver.utils.version import get_maas_version_subversion
39+from provisioningserver.utils.version import get_maas_version_subversion
40
41 # MAAS capabilities. See docs/version.rst for documentation.
42 CAP_NETWORKS_MANAGEMENT = 'networks-management'
43diff --git a/src/maasserver/bootresources.py b/src/maasserver/bootresources.py
44index 996a6a0..fd7ede4 100644
45--- a/src/maasserver/bootresources.py
46+++ b/src/maasserver/bootresources.py
47@@ -77,10 +77,6 @@ from maasserver.utils.orm import (
48 with_connection,
49 )
50 from maasserver.utils.threads import deferToDatabase
51-from maasserver.utils.version import (
52- get_maas_version_tuple,
53- get_maas_version_user_agent,
54-)
55 from provisioningserver.config import is_dev_environment
56 from provisioningserver.events import EVENT_TYPES
57 from provisioningserver.import_images.download_descriptions import (
58@@ -111,6 +107,10 @@ from provisioningserver.utils.twisted import (
59 pause,
60 synchronous,
61 )
62+from provisioningserver.utils.version import (
63+ get_maas_version_tuple,
64+ get_maas_version_user_agent,
65+)
66 from simplestreams import util as sutil
67 from simplestreams.mirrors import (
68 BasicMirrorWriter,
69diff --git a/src/maasserver/bootsources.py b/src/maasserver/bootsources.py
70index 999a6d8..2cd5ab2 100644
71--- a/src/maasserver/bootsources.py
72+++ b/src/maasserver/bootsources.py
73@@ -27,7 +27,6 @@ from maasserver.models import (
74 )
75 from maasserver.utils.orm import transactional
76 from maasserver.utils.threads import deferToDatabase
77-from maasserver.utils.version import get_maas_version_user_agent
78 from provisioningserver.auth import get_maas_user_gpghome
79 from provisioningserver.config import (
80 DEFAULT_IMAGES_URL,
81@@ -44,6 +43,7 @@ from provisioningserver.utils.twisted import (
82 asynchronous,
83 FOREVER,
84 )
85+from provisioningserver.utils.version import get_maas_version_user_agent
86 from requests.exceptions import ConnectionError
87 from twisted.internet.defer import inlineCallbacks
88
89diff --git a/src/maasserver/context_processors.py b/src/maasserver/context_processors.py
90index da8e817..697ec15 100644
91--- a/src/maasserver/context_processors.py
92+++ b/src/maasserver/context_processors.py
93@@ -11,7 +11,7 @@ __all__ = [
94 from django.conf import settings
95 from maasserver.config import RegionConfiguration
96 from maasserver.models import Config
97-from maasserver.utils.version import (
98+from provisioningserver.utils.version import (
99 get_maas_doc_version,
100 get_maas_version_ui,
101 )
102diff --git a/src/maasserver/tests/test_bootresources.py b/src/maasserver/tests/test_bootresources.py
103index 135715b..b3fd049 100644
104--- a/src/maasserver/tests/test_bootresources.py
105+++ b/src/maasserver/tests/test_bootresources.py
106@@ -92,7 +92,6 @@ from maasserver.utils.orm import (
107 transactional,
108 )
109 from maasserver.utils.threads import deferToDatabase
110-from maasserver.utils.version import get_maas_version_user_agent
111 from maastesting.matchers import (
112 MockCalledOnce,
113 MockCalledOnceWith,
114@@ -115,6 +114,7 @@ from provisioningserver.utils.twisted import (
115 asynchronous,
116 DeferredValue,
117 )
118+from provisioningserver.utils.version import get_maas_version_user_agent
119 from testtools.matchers import (
120 Contains,
121 ContainsAll,
122diff --git a/src/maasserver/tests/test_bootsources.py b/src/maasserver/tests/test_bootsources.py
123index ecba7ce..d7f8ce8 100644
124--- a/src/maasserver/tests/test_bootsources.py
125+++ b/src/maasserver/tests/test_bootsources.py
126@@ -42,7 +42,6 @@ from maasserver.testing.testcase import (
127 MAASTransactionServerTestCase,
128 )
129 from maasserver.tests.test_bootresources import SimplestreamsEnvFixture
130-from maasserver.utils.version import get_maas_version_user_agent
131 from maastesting.matchers import MockCalledOnceWith
132 from provisioningserver.config import DEFAULT_IMAGES_URL
133 from provisioningserver.import_images import (
134@@ -52,6 +51,7 @@ from provisioningserver.import_images.boot_image_mapping import (
135 BootImageMapping,
136 )
137 from provisioningserver.import_images.helpers import ImageSpec
138+from provisioningserver.utils.version import get_maas_version_user_agent
139 from requests.exceptions import ConnectionError
140 from testtools.matchers import HasLength
141
142diff --git a/src/maasserver/utils/tests/test_version.py b/src/maasserver/utils/tests/test_version.py
143deleted file mode 100644
144index 92b31dd..0000000
145--- a/src/maasserver/utils/tests/test_version.py
146+++ /dev/null
147@@ -1,278 +0,0 @@
148-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
149-# GNU Affero General Public License version 3 (see the file LICENSE).
150-
151-"""Test version utilities."""
152-
153-__all__ = []
154-
155-import os.path
156-import random
157-from unittest import skipUnless
158-from unittest.mock import (
159- MagicMock,
160- sentinel,
161-)
162-
163-from maasserver import __version__ as old_version
164-from maasserver.utils import version
165-from maastesting import root
166-from maastesting.matchers import MockCalledOnceWith
167-from maastesting.testcase import MAASTestCase
168-from provisioningserver.utils import (
169- shell,
170- snappy,
171-)
172-from testtools.matchers import (
173- GreaterThan,
174- Is,
175- IsInstance,
176-)
177-
178-
179-class TestGetVersionFromAPT(MAASTestCase):
180-
181- def test__creates_cache_with_None_progress(self):
182- mock_Cache = self.patch(version.apt_pkg, "Cache")
183- version.get_version_from_apt(version.REGION_PACKAGE_NAME)
184- self.assertThat(mock_Cache, MockCalledOnceWith(None))
185-
186- def test__returns_empty_string_if_package_not_in_cache(self):
187- self.patch(version.apt_pkg, "Cache")
188- self.assertEqual(
189- "",
190- version.get_version_from_apt(version.REGION_PACKAGE_NAME))
191-
192- def test__returns_empty_string_if_not_current_ver_from_package(self):
193- package = MagicMock()
194- package.current_ver = None
195- mock_cache = {
196- version.REGION_PACKAGE_NAME: package,
197- }
198- self.patch(version.apt_pkg, "Cache").return_value = mock_cache
199- self.assertEqual(
200- "",
201- version.get_version_from_apt(version.REGION_PACKAGE_NAME))
202-
203- def test__returns_ver_str_from_package(self):
204- package = MagicMock()
205- package.current_ver.ver_str = sentinel.ver_str
206- mock_cache = {
207- version.REGION_PACKAGE_NAME: package,
208- }
209- self.patch(version.apt_pkg, "Cache").return_value = mock_cache
210- self.assertIs(
211- sentinel.ver_str,
212- version.get_version_from_apt(version.REGION_PACKAGE_NAME))
213-
214-
215-class TestGetMAASBranchVersion(MAASTestCase):
216-
217- def test__returns_None_if_this_is_not_a_branch(self):
218- self.patch(version, "__file__", "/")
219- self.assertIsNone(version.get_maas_branch_version())
220-
221- def test__returns_None_if_bzr_crashes(self):
222- call_and_check = self.patch(shell, "call_and_check")
223- call_and_check.side_effect = shell.ExternalProcessError(2, "cmd")
224- self.assertIsNone(version.get_maas_branch_version())
225-
226- def test__returns_None_if_bzr_not_found(self):
227- call_and_check = self.patch(shell, "call_and_check")
228- call_and_check.side_effect = FileNotFoundError()
229- self.assertIsNone(version.get_maas_branch_version())
230-
231- def test__returns_None_if_bzr_emits_something_thats_not_a_number(self):
232- call_and_check = self.patch(shell, "call_and_check")
233- call_and_check.return_value = b"???"
234- self.assertIsNone(version.get_maas_branch_version())
235-
236- @skipUnless(os.path.isdir(os.path.join(root, ".bzr")), "Not a branch")
237- def test__returns_revno_for_this_branch(self):
238- revno = version.get_maas_branch_version()
239- self.assertThat(revno, IsInstance(int))
240- self.assertThat(revno, GreaterThan(0))
241-
242-
243-class TestExtractVersionSubversion(MAASTestCase):
244-
245- scenarios = [
246- ("with ~", {
247- "version": "2.2.0~beta4+bzr5856-0ubuntu1",
248- "output": ("2.2.0~beta4", "bzr5856-0ubuntu1"),
249- }),
250- ("without ~", {
251- "version": "2.1.0+bzr5480-0ubuntu1",
252- "output": ("2.1.0", "bzr5480-0ubuntu1"),
253- }),
254- ("without ~ or +", {
255- "version": "2.1.0-0ubuntu1",
256- "output": ("2.1.0", "0ubuntu1"),
257- }),
258- ]
259-
260- def test__returns_version_subversion(self):
261- self.assertEqual(
262- self.output, version.extract_version_subversion(self.version))
263-
264-
265-class TestVersionTestCase(MAASTestCase):
266- """MAASTestCase that resets the cache used by utility methods."""
267-
268- def setUp(self):
269- super(TestVersionTestCase, self).setUp()
270- for attribute in vars(version).values():
271- if hasattr(attribute, "cache_clear"):
272- attribute.cache_clear()
273-
274-
275-class TestGetMAASVersion(TestVersionTestCase):
276-
277- def test__calls_get_version_from_apt(self):
278- mock_apt = self.patch(version, "get_version_from_apt")
279- mock_apt.return_value = sentinel.version
280- self.expectThat(
281- version.get_maas_version(), Is(sentinel.version))
282- self.expectThat(
283- mock_apt, MockCalledOnceWith(version.REGION_PACKAGE_NAME))
284-
285- def test__uses_snappy_get_snap_version(self):
286- self.patch(snappy, 'running_in_snap').return_value = True
287- self.patch(snappy, 'get_snap_version').return_value = sentinel.version
288- self.assertEqual(sentinel.version, version.get_maas_version())
289-
290-
291-class TestGetMAASVersionSubversion(TestVersionTestCase):
292-
293- def test__returns_package_version(self):
294- mock_apt = self.patch(version, "get_version_from_apt")
295- mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
296- self.assertEqual(
297- ("1.8.0~alpha4", "bzr356-0ubuntu1"),
298- version.get_maas_version_subversion())
299-
300- def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
301- mock_version = self.patch(version, "get_version_from_apt")
302- mock_version.return_value = ""
303- mock_branch_version = self.patch(version, "get_maas_branch_version")
304- mock_branch_version.return_value = None
305- self.assertEqual(
306- (old_version, "unknown"),
307- version.get_maas_version_subversion())
308-
309- def test__returns_from_source_and_revno_from_branch(self):
310- mock_version = self.patch(version, "get_version_from_apt")
311- mock_version.return_value = ""
312- revno = random.randint(1, 5000)
313- mock_branch_version = self.patch(version, "get_maas_branch_version")
314- mock_branch_version.return_value = revno
315- self.assertEqual(
316- ("%s from source" % old_version, "bzr%d" % revno),
317- version.get_maas_version_subversion())
318-
319-
320-class TestGetMAASVersionUI(TestVersionTestCase):
321-
322- def test__returns_package_version(self):
323- mock_apt = self.patch(version, "get_version_from_apt")
324- mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
325- self.assertEqual(
326- "1.8.0~alpha4 (bzr356-0ubuntu1)", version.get_maas_version_ui())
327-
328- def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
329- mock_version = self.patch(version, "get_version_from_apt")
330- mock_version.return_value = ""
331- mock_branch_version = self.patch(version, "get_maas_branch_version")
332- mock_branch_version.return_value = None
333- self.assertEqual(
334- "%s (unknown)" % old_version,
335- version.get_maas_version_ui())
336-
337- def test__returns_from_source_and_revno_from_branch(self):
338- mock_version = self.patch(version, "get_version_from_apt")
339- mock_version.return_value = ""
340- revno = random.randint(1, 5000)
341- mock_branch_version = self.patch(version, "get_maas_branch_version")
342- mock_branch_version.return_value = revno
343- self.assertEqual(
344- "%s from source (bzr%d)" % (old_version, revno),
345- version.get_maas_version_ui())
346-
347-
348-class TestGetMAASVersionUserAgent(TestVersionTestCase):
349-
350- def test__returns_package_version(self):
351- mock_apt = self.patch(version, "get_version_from_apt")
352- mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
353- self.assertEqual(
354- "maas/1.8.0~alpha4/bzr356-0ubuntu1",
355- version.get_maas_version_user_agent())
356-
357- def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
358- mock_version = self.patch(version, "get_version_from_apt")
359- mock_version.return_value = ""
360- mock_branch_version = self.patch(version, "get_maas_branch_version")
361- mock_branch_version.return_value = None
362- self.assertEqual(
363- "maas/%s/unknown" % old_version,
364- version.get_maas_version_user_agent())
365-
366- def test__returns_from_source_and_revno_from_branch(self):
367- mock_version = self.patch(version, "get_version_from_apt")
368- mock_version.return_value = ""
369- revno = random.randint(1, 5000)
370- mock_branch_version = self.patch(version, "get_maas_branch_version")
371- mock_branch_version.return_value = revno
372- self.assertEqual(
373- "maas/%s from source/bzr%d" % (old_version, revno),
374- version.get_maas_version_user_agent())
375-
376-
377-class TestGetMAASDocVersion(TestVersionTestCase):
378-
379- def test__returns_doc_version_with_greater_than_1_decimals(self):
380- mock_apt = self.patch(version, "get_version_from_apt")
381- mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
382- self.assertEqual("1.8", version.get_maas_doc_version())
383-
384- def test__returns_doc_version_with_equal_to_1_decimals(self):
385- mock_apt = self.patch(version, "get_version_from_apt")
386- mock_apt.return_value = "1.8~alpha4+bzr356-0ubuntu1"
387- self.assertEqual("1.8", version.get_maas_doc_version())
388-
389- def test__returns_empty_if_version_is_empty(self):
390- mock_apt = self.patch(version, "get_version_from_apt")
391- mock_apt.return_value = ""
392- self.assertEqual("", version.get_maas_doc_version())
393-
394-
395-class TestVersionMethodsCached(TestVersionTestCase):
396-
397- scenarios = [
398- ("get_maas_version", dict(method="get_maas_version")),
399- ("get_maas_version_subversion", dict(
400- method="get_maas_version_subversion")),
401- ("get_maas_version_ui", dict(method="get_maas_version_ui")),
402- ("get_maas_doc_version", dict(method="get_maas_doc_version")),
403- ]
404-
405- def test_method_is_cached(self):
406- mock_apt = self.patch(version, "get_version_from_apt")
407- mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
408- cached_method = getattr(version, self.method)
409- first_return_value = cached_method()
410- second_return_value = cached_method()
411- # The return value is not empty (full unit tests have been performed
412- # earlier).
413- self.assertNotIn(first_return_value, [b'', '', None])
414- self.assertEqual(first_return_value, second_return_value)
415- # Apt has only been called once.
416- self.expectThat(
417- mock_apt, MockCalledOnceWith(version.REGION_PACKAGE_NAME))
418-
419-
420-class TestGetMAASVersionTuple(MAASTestCase):
421-
422- def test_get_maas_version_tuple(self):
423- self.assertEquals(
424- '.'.join([str(i) for i in version.get_maas_version_tuple()]),
425- version.get_maas_version_subversion()[0])
426diff --git a/src/maasserver/utils/version.py b/src/maasserver/utils/version.py
427deleted file mode 100644
428index bdd6424..0000000
429--- a/src/maasserver/utils/version.py
430+++ /dev/null
431@@ -1,142 +0,0 @@
432-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
433-# GNU Affero General Public License version 3 (see the file LICENSE).
434-
435-"""Version utilities."""
436-
437-__all__ = [
438- "get_maas_doc_version",
439- "get_maas_version_subversion",
440- "get_maas_version_ui",
441- ]
442-
443-from functools import lru_cache
444-import re
445-
446-from maasserver import __version__ as old_version
447-from maasserver.api.logger import maaslog
448-from provisioningserver.utils import (
449- shell,
450- snappy,
451-)
452-
453-# Only import apt_pkg and initialize when not running in a snap.
454-if not snappy.running_in_snap():
455- import apt_pkg
456- apt_pkg.init()
457-
458-# Name of maas package to get version from.
459-REGION_PACKAGE_NAME = "maas-region-api"
460-
461-
462-def get_version_from_apt(package):
463- """Return the version output from `apt_pkg.Cache` for the given package or
464- an error message if the package data is not valid."""
465- try:
466- cache = apt_pkg.Cache(None)
467- except SystemError:
468- maaslog.error(
469- 'Installed version could not be determined. Ensure '
470- '/var/lib/dpkg/status is valid.')
471- return ""
472-
473- version = None
474- if package in cache:
475- apt_package = cache[package]
476- version = apt_package.current_ver
477-
478- return version.ver_str if version is not None else ""
479-
480-
481-def extract_version_subversion(version):
482- """Return a tuple (version, subversion) from the given apt version."""
483- main_version, subversion = re.split('[+|-]', version, 1)
484- return main_version, subversion
485-
486-
487-def get_maas_branch_version():
488- """Return the Bazaar revision for this running MAAS.
489-
490- :return: An integer if MAAS is running from a Bazaar working tree, else
491- `None`. The revision number is only representative of the BRANCH, not
492- the working tree.
493- """
494- try:
495- revno = shell.call_and_check(("bzr", "revno", __file__))
496- except shell.ExternalProcessError:
497- # We may not be in a Bazaar working tree, or any manner of other
498- # errors. For the purposes of this function we don't care; simply say
499- # we don't know.
500- return None
501- except FileNotFoundError:
502- # Bazaar is not installed. We don't care and simply say we don't know.
503- return None
504- else:
505- # `bzr revno` can return '???' when it can't find the working tree's
506- # current revision in the branch. Hopefully a fairly unlikely thing to
507- # happen, but we guard against it, and other ills, here.
508- try:
509- return int(revno)
510- except ValueError:
511- return None
512-
513-
514-@lru_cache(maxsize=1)
515-def get_maas_version():
516- """Return the apt or snap version for the main MAAS package."""
517- if snappy.running_in_snap():
518- return snappy.get_snap_version()
519- else:
520- return get_version_from_apt(REGION_PACKAGE_NAME)
521-
522-
523-@lru_cache(maxsize=1)
524-def get_maas_version_subversion():
525- """Return a tuple with the MAAS version and the MAAS subversion."""
526- version = get_maas_version()
527- if version:
528- return extract_version_subversion(version)
529- else:
530- # Get the branch information
531- branch_version = get_maas_branch_version()
532- if branch_version is None:
533- # Not installed not in branch, then no way to identify. This should
534- # not happen, but just in case.
535- return old_version, "unknown"
536- else:
537- return "%s from source" % old_version, "bzr%d" % branch_version
538-
539-
540-@lru_cache(maxsize=1)
541-def get_maas_version_ui():
542- """Return the version string for the running MAAS region.
543-
544- The returned string is suitable to display in the UI.
545- """
546- version, subversion = get_maas_version_subversion()
547- return "%s (%s)" % (version, subversion) if subversion else version
548-
549-
550-@lru_cache(maxsize=1)
551-def get_maas_version_user_agent():
552- """Return the version string for the running MAAS region.
553-
554- The returned string is suitable to set the user agent.
555- """
556- version, subversion = get_maas_version_subversion()
557- return "maas/%s/%s" % (version, subversion)
558-
559-
560-@lru_cache(maxsize=1)
561-def get_maas_doc_version():
562- """Return the doc version for the running MAAS region."""
563- apt_version = get_maas_version()
564- if apt_version:
565- version, _ = extract_version_subversion(apt_version)
566- return '.'.join(version.split('~')[0].split('.')[:2])
567- else:
568- return ''
569-
570-
571-def get_maas_version_tuple():
572- """Returns a tuple of the MAAS version without the svn rev."""
573- return tuple(int(x) for x in old_version.split('.'))
574diff --git a/src/maasserver/websockets/handlers/bootresource.py b/src/maasserver/websockets/handlers/bootresource.py
575index 1df2ecd..68742d9 100644
576--- a/src/maasserver/websockets/handlers/bootresource.py
577+++ b/src/maasserver/websockets/handlers/bootresource.py
578@@ -43,7 +43,6 @@ from maasserver.models import (
579 from maasserver.utils.converters import human_readable_bytes
580 from maasserver.utils.orm import transactional
581 from maasserver.utils.threads import deferToDatabase
582-from maasserver.utils.version import get_maas_version_user_agent
583 from maasserver.websockets.base import (
584 Handler,
585 HandlerError,
586@@ -65,6 +64,7 @@ from provisioningserver.utils.twisted import (
587 callOut,
588 FOREVER,
589 )
590+from provisioningserver.utils.version import get_maas_version_user_agent
591 from twisted.internet.defer import Deferred
592
593
594diff --git a/src/maasserver/websockets/handlers/general.py b/src/maasserver/websockets/handlers/general.py
595index b15300a..e5df4f0 100644
596--- a/src/maasserver/websockets/handlers/general.py
597+++ b/src/maasserver/websockets/handlers/general.py
598@@ -33,9 +33,9 @@ from maasserver.utils.osystems import (
599 list_osystem_choices,
600 list_release_choices,
601 )
602-from maasserver.utils.version import get_maas_version_ui
603 from maasserver.websockets.base import Handler
604 import petname
605+from provisioningserver.utils.version import get_maas_version_ui
606
607
608 class GeneralHandler(Handler):
609diff --git a/src/maasserver/websockets/handlers/tests/test_bootresource.py b/src/maasserver/websockets/handlers/tests/test_bootresource.py
610index 57940a7..87438ea 100644
611--- a/src/maasserver/websockets/handlers/tests/test_bootresource.py
612+++ b/src/maasserver/websockets/handlers/tests/test_bootresource.py
613@@ -32,7 +32,6 @@ from maasserver.utils.orm import (
614 get_one,
615 reload_object,
616 )
617-from maasserver.utils.version import get_maas_version_user_agent
618 from maasserver.websockets.base import (
619 HandlerError,
620 HandlerValidationError,
621@@ -54,6 +53,7 @@ from provisioningserver.import_images.testing.factory import (
622 make_image_spec,
623 set_resource,
624 )
625+from provisioningserver.utils.version import get_maas_version_user_agent
626 from testtools.matchers import (
627 ContainsAll,
628 HasLength,
629diff --git a/src/metadataserver/builtin_scripts/__init__.py b/src/metadataserver/builtin_scripts/__init__.py
630index 934a3a1..8cff96f 100644
631--- a/src/metadataserver/builtin_scripts/__init__.py
632+++ b/src/metadataserver/builtin_scripts/__init__.py
633@@ -15,10 +15,10 @@ from attr.validators import (
634 instance_of,
635 optional,
636 )
637-from maasserver.utils.version import get_maas_version
638 from metadataserver.enum import SCRIPT_TYPE
639 from metadataserver.models import Script
640 from provisioningserver.utils.fs import read_text_file
641+from provisioningserver.utils.version import get_maas_version
642 from zope.interface import (
643 Attribute,
644 implementer,
645diff --git a/src/metadataserver/builtin_scripts/tests/test_builtin_scripts.py b/src/metadataserver/builtin_scripts/tests/test_builtin_scripts.py
646index 004560b..703ba94 100644
647--- a/src/metadataserver/builtin_scripts/tests/test_builtin_scripts.py
648+++ b/src/metadataserver/builtin_scripts/tests/test_builtin_scripts.py
649@@ -11,13 +11,13 @@ from maasserver.models import VersionedTextFile
650 from maasserver.testing.factory import factory
651 from maasserver.testing.testcase import MAASServerTestCase
652 from maasserver.utils.orm import reload_object
653-from maasserver.utils.version import get_maas_version
654 from metadataserver.builtin_scripts import (
655 BUILTIN_SCRIPTS,
656 load_builtin_scripts,
657 )
658 from metadataserver.enum import SCRIPT_TYPE_CHOICES
659 from metadataserver.models import Script
660+from provisioningserver.utils.version import get_maas_version
661
662
663 class TestBuiltinScripts(MAASServerTestCase):
664diff --git a/src/provisioningserver/utils/tests/test_version.py b/src/provisioningserver/utils/tests/test_version.py
665new file mode 100644
666index 0000000..410369d
667--- /dev/null
668+++ b/src/provisioningserver/utils/tests/test_version.py
669@@ -0,0 +1,293 @@
670+# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
671+# GNU Affero General Public License version 3 (see the file LICENSE).
672+
673+"""Test version utilities."""
674+
675+__all__ = []
676+
677+
678+import os.path
679+import random
680+from unittest import skipUnless
681+from unittest.mock import (
682+ MagicMock,
683+ sentinel,
684+)
685+
686+from maastesting import root
687+from maastesting.matchers import MockCalledOnceWith
688+from maastesting.testcase import MAASTestCase
689+from provisioningserver.utils import (
690+ shell,
691+ snappy,
692+ version,
693+)
694+from provisioningserver.utils.version import DEFAULT_VERSION as old_version
695+from testtools.matchers import (
696+ GreaterThan,
697+ Is,
698+ IsInstance,
699+)
700+
701+
702+class TestGetVersionFromAPT(MAASTestCase):
703+
704+ def test__creates_cache_with_None_progress(self):
705+ mock_Cache = self.patch(version.apt_pkg, "Cache")
706+ version.get_version_from_apt(version.REGION_PACKAGE_NAME)
707+ self.assertThat(mock_Cache, MockCalledOnceWith(None))
708+
709+ def test__returns_empty_string_if_package_not_in_cache(self):
710+ self.patch(version.apt_pkg, "Cache")
711+ self.assertEqual(
712+ "",
713+ version.get_version_from_apt(version.REGION_PACKAGE_NAME))
714+
715+ def test__returns_empty_string_if_not_current_ver_from_package(self):
716+ package = MagicMock()
717+ package.current_ver = None
718+ mock_cache = {
719+ version.REGION_PACKAGE_NAME: package,
720+ }
721+ self.patch(version.apt_pkg, "Cache").return_value = mock_cache
722+ self.assertEqual(
723+ "",
724+ version.get_version_from_apt(version.REGION_PACKAGE_NAME))
725+
726+ def test__returns_ver_str_from_package(self):
727+ package = MagicMock()
728+ package.current_ver.ver_str = sentinel.ver_str
729+ mock_cache = {
730+ version.RACK_PACKAGE_NAME: package
731+ }
732+ self.patch(version.apt_pkg, "Cache").return_value = mock_cache
733+ self.assertIs(
734+ sentinel.ver_str,
735+ version.get_version_from_apt(version.RACK_PACKAGE_NAME))
736+
737+ def test__returns_ver_str_from_second_package_if_first_not_found(self):
738+ package = MagicMock()
739+ package.current_ver.ver_str = sentinel.ver_str
740+ mock_cache = {
741+ version.REGION_PACKAGE_NAME: package,
742+ }
743+ self.patch(version.apt_pkg, "Cache").return_value = mock_cache
744+ self.assertIs(
745+ sentinel.ver_str,
746+ version.get_version_from_apt(
747+ version.RACK_PACKAGE_NAME, version.REGION_PACKAGE_NAME))
748+
749+
750+class TestGetMAASBranchVersion(MAASTestCase):
751+
752+ def test__returns_None_if_this_is_not_a_branch(self):
753+ self.patch(version, "__file__", "/")
754+ self.assertIsNone(version.get_maas_branch_version())
755+
756+ def test__returns_None_if_bzr_crashes(self):
757+ call_and_check = self.patch(shell, "call_and_check")
758+ call_and_check.side_effect = shell.ExternalProcessError(2, "cmd")
759+ self.assertIsNone(version.get_maas_branch_version())
760+
761+ def test__returns_None_if_bzr_not_found(self):
762+ call_and_check = self.patch(shell, "call_and_check")
763+ call_and_check.side_effect = FileNotFoundError()
764+ self.assertIsNone(version.get_maas_branch_version())
765+
766+ def test__returns_None_if_bzr_emits_something_thats_not_a_number(self):
767+ call_and_check = self.patch(shell, "call_and_check")
768+ call_and_check.return_value = b"???"
769+ self.assertIsNone(version.get_maas_branch_version())
770+
771+ @skipUnless(os.path.isdir(os.path.join(root, ".bzr")), "Not a branch")
772+ def test__returns_revno_for_this_branch(self):
773+ revno = version.get_maas_branch_version()
774+ self.assertThat(revno, IsInstance(int))
775+ self.assertThat(revno, GreaterThan(0))
776+
777+
778+class TestExtractVersionSubversion(MAASTestCase):
779+
780+ scenarios = [
781+ ("with ~", {
782+ "version": "2.2.0~beta4+bzr5856-0ubuntu1",
783+ "output": ("2.2.0~beta4", "bzr5856-0ubuntu1"),
784+ }),
785+ ("without ~", {
786+ "version": "2.1.0+bzr5480-0ubuntu1",
787+ "output": ("2.1.0", "bzr5480-0ubuntu1"),
788+ }),
789+ ("without ~ or +", {
790+ "version": "2.1.0-0ubuntu1",
791+ "output": ("2.1.0", "0ubuntu1"),
792+ }),
793+ ]
794+
795+ def test__returns_version_subversion(self):
796+ self.assertEqual(
797+ self.output, version.extract_version_subversion(self.version))
798+
799+
800+class TestVersionTestCase(MAASTestCase):
801+ """MAASTestCase that resets the cache used by utility methods."""
802+
803+ def setUp(self):
804+ super(TestVersionTestCase, self).setUp()
805+ for attribute in vars(version).values():
806+ if hasattr(attribute, "cache_clear"):
807+ attribute.cache_clear()
808+
809+
810+class TestGetMAASVersion(TestVersionTestCase):
811+
812+ def test__calls_get_version_from_apt(self):
813+ mock_apt = self.patch(version, "get_version_from_apt")
814+ mock_apt.return_value = sentinel.version
815+ self.expectThat(
816+ version.get_maas_version(), Is(sentinel.version))
817+ self.expectThat(
818+ mock_apt, MockCalledOnceWith(
819+ version.RACK_PACKAGE_NAME, version.REGION_PACKAGE_NAME))
820+
821+ def test__uses_snappy_get_snap_version(self):
822+ self.patch(snappy, 'running_in_snap').return_value = True
823+ self.patch(snappy, 'get_snap_version').return_value = sentinel.version
824+ self.assertEqual(sentinel.version, version.get_maas_version())
825+
826+
827+class TestGetMAASVersionSubversion(TestVersionTestCase):
828+
829+ def test__returns_package_version(self):
830+ mock_apt = self.patch(version, "get_version_from_apt")
831+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
832+ self.assertEqual(
833+ ("1.8.0~alpha4", "bzr356-0ubuntu1"),
834+ version.get_maas_version_subversion())
835+
836+ def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
837+ mock_version = self.patch(version, "get_version_from_apt")
838+ mock_version.return_value = ""
839+ mock_branch_version = self.patch(version, "get_maas_branch_version")
840+ mock_branch_version.return_value = None
841+ self.assertEqual(
842+ (old_version, "unknown"),
843+ version.get_maas_version_subversion())
844+
845+ def test__returns_from_source_and_revno_from_branch(self):
846+ mock_version = self.patch(version, "get_version_from_apt")
847+ mock_version.return_value = ""
848+ revno = random.randint(1, 5000)
849+ mock_branch_version = self.patch(version, "get_maas_branch_version")
850+ mock_branch_version.return_value = revno
851+ self.assertEqual(
852+ ("%s from source" % old_version, "bzr%d" % revno),
853+ version.get_maas_version_subversion())
854+
855+
856+class TestGetMAASVersionUI(TestVersionTestCase):
857+
858+ def test__returns_package_version(self):
859+ mock_apt = self.patch(version, "get_version_from_apt")
860+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
861+ self.assertEqual(
862+ "1.8.0~alpha4 (bzr356-0ubuntu1)", version.get_maas_version_ui())
863+
864+ def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
865+ mock_version = self.patch(version, "get_version_from_apt")
866+ mock_version.return_value = ""
867+ mock_branch_version = self.patch(version, "get_maas_branch_version")
868+ mock_branch_version.return_value = None
869+ self.assertEqual(
870+ "%s (unknown)" % old_version,
871+ version.get_maas_version_ui())
872+
873+ def test__returns_from_source_and_revno_from_branch(self):
874+ mock_version = self.patch(version, "get_version_from_apt")
875+ mock_version.return_value = ""
876+ revno = random.randint(1, 5000)
877+ mock_branch_version = self.patch(version, "get_maas_branch_version")
878+ mock_branch_version.return_value = revno
879+ self.assertEqual(
880+ "%s from source (bzr%d)" % (old_version, revno),
881+ version.get_maas_version_ui())
882+
883+
884+class TestGetMAASVersionUserAgent(TestVersionTestCase):
885+
886+ def test__returns_package_version(self):
887+ mock_apt = self.patch(version, "get_version_from_apt")
888+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
889+ self.assertEqual(
890+ "maas/1.8.0~alpha4/bzr356-0ubuntu1",
891+ version.get_maas_version_user_agent())
892+
893+ def test__returns_unknown_if_version_is_empty_and_not_bzr_branch(self):
894+ mock_version = self.patch(version, "get_version_from_apt")
895+ mock_version.return_value = ""
896+ mock_branch_version = self.patch(version, "get_maas_branch_version")
897+ mock_branch_version.return_value = None
898+ self.assertEqual(
899+ "maas/%s/unknown" % old_version,
900+ version.get_maas_version_user_agent())
901+
902+ def test__returns_from_source_and_revno_from_branch(self):
903+ mock_version = self.patch(version, "get_version_from_apt")
904+ mock_version.return_value = ""
905+ revno = random.randint(1, 5000)
906+ mock_branch_version = self.patch(version, "get_maas_branch_version")
907+ mock_branch_version.return_value = revno
908+ self.assertEqual(
909+ "maas/%s from source/bzr%d" % (old_version, revno),
910+ version.get_maas_version_user_agent())
911+
912+
913+class TestGetMAASDocVersion(TestVersionTestCase):
914+
915+ def test__returns_doc_version_with_greater_than_1_decimals(self):
916+ mock_apt = self.patch(version, "get_version_from_apt")
917+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
918+ self.assertEqual("1.8", version.get_maas_doc_version())
919+
920+ def test__returns_doc_version_with_equal_to_1_decimals(self):
921+ mock_apt = self.patch(version, "get_version_from_apt")
922+ mock_apt.return_value = "1.8~alpha4+bzr356-0ubuntu1"
923+ self.assertEqual("1.8", version.get_maas_doc_version())
924+
925+ def test__returns_empty_if_version_is_empty(self):
926+ mock_apt = self.patch(version, "get_version_from_apt")
927+ mock_apt.return_value = ""
928+ self.assertEqual("", version.get_maas_doc_version())
929+
930+
931+class TestVersionMethodsCached(TestVersionTestCase):
932+
933+ scenarios = [
934+ ("get_maas_version", dict(method="get_maas_version")),
935+ ("get_maas_version_subversion", dict(
936+ method="get_maas_version_subversion")),
937+ ("get_maas_version_ui", dict(method="get_maas_version_ui")),
938+ ("get_maas_doc_version", dict(method="get_maas_doc_version")),
939+ ]
940+
941+ def test_method_is_cached(self):
942+ mock_apt = self.patch(version, "get_version_from_apt")
943+ mock_apt.return_value = "1.8.0~alpha4+bzr356-0ubuntu1"
944+ cached_method = getattr(version, self.method)
945+ first_return_value = cached_method()
946+ second_return_value = cached_method()
947+ # The return value is not empty (full unit tests have been performed
948+ # earlier).
949+ self.assertNotIn(first_return_value, [b'', '', None])
950+ self.assertEqual(first_return_value, second_return_value)
951+ # Apt has only been called once.
952+ self.expectThat(
953+ mock_apt, MockCalledOnceWith(
954+ version.RACK_PACKAGE_NAME, version.REGION_PACKAGE_NAME))
955+
956+
957+class TestGetMAASVersionTuple(MAASTestCase):
958+
959+ def test_get_maas_version_tuple(self):
960+ self.assertEquals(
961+ '.'.join([str(i) for i in version.get_maas_version_tuple()]),
962+ version.get_maas_version_subversion()[0])
963diff --git a/src/provisioningserver/utils/version.py b/src/provisioningserver/utils/version.py
964new file mode 100644
965index 0000000..f372b0d
966--- /dev/null
967+++ b/src/provisioningserver/utils/version.py
968@@ -0,0 +1,149 @@
969+# Copyright 2015-2017 Canonical Ltd. This software is licensed under the
970+# GNU Affero General Public License version 3 (see the file LICENSE).
971+
972+"""Version utilities."""
973+
974+__all__ = [
975+ "get_maas_doc_version",
976+ "get_maas_version_subversion",
977+ "get_maas_version_ui",
978+ ]
979+
980+from functools import lru_cache
981+import re
982+
983+from provisioningserver.logger import get_maas_logger
984+from provisioningserver.utils import (
985+ shell,
986+ snappy,
987+)
988+
989+
990+maaslog = get_maas_logger('version')
991+
992+DEFAULT_VERSION = "2.3.0"
993+
994+# Only import apt_pkg and initialize when not running in a snap.
995+if not snappy.running_in_snap():
996+ import apt_pkg
997+ apt_pkg.init()
998+
999+# Name of maas package to get version from.
1000+REGION_PACKAGE_NAME = "maas-region-api"
1001+RACK_PACKAGE_NAME = "maas-rack-controller"
1002+
1003+
1004+def get_version_from_apt(*packages):
1005+ """Return the version output from `apt_pkg.Cache` for the given package(s),
1006+ or log an error message if the package data is not valid."""
1007+ try:
1008+ cache = apt_pkg.Cache(None)
1009+ except SystemError:
1010+ maaslog.error(
1011+ 'Installed version could not be determined. Ensure '
1012+ '/var/lib/dpkg/status is valid.')
1013+ return ""
1014+
1015+ version = None
1016+ for package in packages:
1017+ if package in cache:
1018+ apt_package = cache[package]
1019+ version = apt_package.current_ver
1020+ break
1021+
1022+ return version.ver_str if version is not None else ""
1023+
1024+
1025+def extract_version_subversion(version):
1026+ """Return a tuple (version, subversion) from the given apt version."""
1027+ main_version, subversion = re.split('[+|-]', version, 1)
1028+ return main_version, subversion
1029+
1030+
1031+def get_maas_branch_version():
1032+ """Return the Bazaar revision for this running MAAS.
1033+
1034+ :return: An integer if MAAS is running from a Bazaar working tree, else
1035+ `None`. The revision number is only representative of the BRANCH, not
1036+ the working tree.
1037+ """
1038+ try:
1039+ revno = shell.call_and_check(("bzr", "revno", __file__))
1040+ except shell.ExternalProcessError:
1041+ # We may not be in a Bazaar working tree, or any manner of other
1042+ # errors. For the purposes of this function we don't care; simply say
1043+ # we don't know.
1044+ return None
1045+ except FileNotFoundError:
1046+ # Bazaar is not installed. We don't care and simply say we don't know.
1047+ return None
1048+ else:
1049+ # `bzr revno` can return '???' when it can't find the working tree's
1050+ # current revision in the branch. Hopefully a fairly unlikely thing to
1051+ # happen, but we guard against it, and other ills, here.
1052+ try:
1053+ return int(revno)
1054+ except ValueError:
1055+ return None
1056+
1057+
1058+@lru_cache(maxsize=1)
1059+def get_maas_version():
1060+ """Return the apt or snap version for the main MAAS package."""
1061+ if snappy.running_in_snap():
1062+ return snappy.get_snap_version()
1063+ else:
1064+ return get_version_from_apt(RACK_PACKAGE_NAME, REGION_PACKAGE_NAME)
1065+
1066+
1067+@lru_cache(maxsize=1)
1068+def get_maas_version_subversion():
1069+ """Return a tuple with the MAAS version and the MAAS subversion."""
1070+ version = get_maas_version()
1071+ if version:
1072+ return extract_version_subversion(version)
1073+ else:
1074+ # Get the branch information
1075+ branch_version = get_maas_branch_version()
1076+ if branch_version is None:
1077+ # Not installed not in branch, then no way to identify. This should
1078+ # not happen, but just in case.
1079+ return DEFAULT_VERSION, "unknown"
1080+ else:
1081+ return "%s from source" % DEFAULT_VERSION, "bzr%d" % branch_version
1082+
1083+
1084+@lru_cache(maxsize=1)
1085+def get_maas_version_ui():
1086+ """Return the version string for the running MAAS region.
1087+
1088+ The returned string is suitable to display in the UI.
1089+ """
1090+ version, subversion = get_maas_version_subversion()
1091+ return "%s (%s)" % (version, subversion) if subversion else version
1092+
1093+
1094+@lru_cache(maxsize=1)
1095+def get_maas_version_user_agent():
1096+ """Return the version string for the running MAAS region.
1097+
1098+ The returned string is suitable to set the user agent.
1099+ """
1100+ version, subversion = get_maas_version_subversion()
1101+ return "maas/%s/%s" % (version, subversion)
1102+
1103+
1104+@lru_cache(maxsize=1)
1105+def get_maas_doc_version():
1106+ """Return the doc version for the running MAAS region."""
1107+ apt_version = get_maas_version()
1108+ if apt_version:
1109+ version, _ = extract_version_subversion(apt_version)
1110+ return '.'.join(version.split('~')[0].split('.')[:2])
1111+ else:
1112+ return ''
1113+
1114+
1115+def get_maas_version_tuple():
1116+ """Returns a tuple of the MAAS version without the svn rev."""
1117+ return tuple(int(x) for x in DEFAULT_VERSION.split('.'))
1118diff --git a/utilities/check-imports b/utilities/check-imports
1119index 405ca8d..d047e70 100755
1120--- a/utilities/check-imports
1121+++ b/utilities/check-imports
1122@@ -195,6 +195,7 @@ RackControllerRule = Rule(
1123 Allow("apiclient.creds.*"),
1124 Allow("apiclient.maas_client.*"),
1125 Allow("apiclient.utils.*"),
1126+ Allow("apt_pkg"),
1127 Allow("attr|attr.**"),
1128 Allow("bson|bson.**"),
1129 Allow("crochet|crochet.**"),
1130@@ -245,7 +246,6 @@ RegionControllerRule = Rule(
1131 Allow("apiclient.creds.*"),
1132 Allow("apiclient.multipart.*"),
1133 Allow("apiclient.utils.*"),
1134- Allow("apt_pkg"),
1135 Allow("attr|attr.**"),
1136 Allow("bson"),
1137 Allow("convoy|convoy.**"),

Subscribers

People subscribed via source and target branches