Merge lp:~blake-rouse/maas/boot-images-rpc-v2-1.7 into lp:maas/1.7

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: no longer in the source branch.
Merged at revision: 3380
Proposed branch: lp:~blake-rouse/maas/boot-images-rpc-v2-1.7
Merge into: lp:maas/1.7
Diff against target: 615 lines (+162/-172)
7 files modified
src/maasserver/clusterrpc/boot_images.py (+43/-35)
src/maasserver/clusterrpc/tests/test_boot_images.py (+75/-1)
src/maasserver/utils/version.py (+0/-131)
src/provisioningserver/rpc/cluster.py (+24/-0)
src/provisioningserver/rpc/clusterservice.py (+9/-0)
src/provisioningserver/rpc/tests/test_clusterservice.py (+9/-4)
src/provisioningserver/rpc/tests/test_docs.py (+2/-1)
To merge this branch: bzr merge lp:~blake-rouse/maas/boot-images-rpc-v2-1.7
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+266772@code.launchpad.net

Commit message

Add ListBootImagesV2 RPC command. Fallback to using ListBootImages RPC when the ListBootImagesV2 is not handled on the cluster.

The signature of the ListBootImages RPC call was changed to support more data in the response. This change will cause connected clusters that are not updated the same time as the region to stop communicating and error. This allows both methods to exist on the cluster where V2 will be called first then fallback to the older call in error.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Self-approving backport.

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

The attempt to merge lp:~blake-rouse/maas/boot-images-rpc-v2-1.7 into lp:maas/1.7 failed. Below is the output from the failed tests.

Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://security.ubuntu.com trusty-security Release
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:1 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:2 http://nova.clouds.archive.ubuntu.com trusty-updates Release [63.5 kB]
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [227 kB]
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [130 kB]
Get:5 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [599 kB]
Get:6 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [301 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,322 kB in 3s (413 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools gjs ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bson python-bzrlib python-celery python-convoy 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-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse 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-seamicroclient python-simplejson py...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/clusterrpc/boot_images.py'
2--- src/maasserver/clusterrpc/boot_images.py 2014-09-30 18:13:21 +0000
3+++ src/maasserver/clusterrpc/boot_images.py 2015-08-03 19:11:39 +0000
4@@ -21,6 +21,7 @@
5 ]
6
7 from functools import partial
8+from itertools import imap
9
10 from maasserver.rpc import (
11 getAllClients,
12@@ -30,8 +31,10 @@
13 from provisioningserver.rpc.cluster import (
14 IsImportBootImagesRunning,
15 ListBootImages,
16+ ListBootImagesV2,
17 )
18 from provisioningserver.utils.twisted import synchronous
19+from twisted.protocols.amp import UnhandledCommand
20 from twisted.python.failure import Failure
21
22
23@@ -89,46 +92,51 @@
24 30 seconds.
25 """
26 client = getClientFor(nodegroup.uuid, timeout=1)
27- call = client(ListBootImages)
28- return call.wait(30).get("images")
29+ try:
30+ call = client(ListBootImagesV2)
31+ return call.wait(30).get("images")
32+ except UnhandledCommand:
33+ call = client(ListBootImages)
34+ return call.wait(30).get("images")
35+
36+
37+@synchronous
38+def _get_available_boot_images():
39+ """Obtain boot images available on connected clusters."""
40+ listimages_v1 = lambda client: partial(client, ListBootImages)
41+ listimages_v2 = lambda client: partial(client, ListBootImagesV2)
42+ clients_v2 = getAllClients()
43+ responses_v2 = async.gather(imap(listimages_v2, clients_v2))
44+ clients_v1 = []
45+ for i, response in enumerate(responses_v2):
46+ if (isinstance(response, Failure) and
47+ response.check(UnhandledCommand) is not None):
48+ clients_v1.append(clients_v2[i])
49+ elif not isinstance(response, Failure):
50+ # Convert each image to a frozenset of its items.
51+ yield frozenset(
52+ frozenset(image.viewitems())
53+ for image in response["images"]
54+ )
55+ responses_v1 = async.gather(imap(listimages_v1, clients_v1))
56+ for response in suppress_failures(responses_v1):
57+ # Convert each image to a frozenset of its items.
58+ yield frozenset(
59+ frozenset(image.viewitems())
60+ for image in response["images"]
61+ )
62
63
64 @synchronous
65 def get_available_boot_images():
66 """Obtain boot images that are available on all clusters."""
67- responses = async.gather(
68- partial(client, ListBootImages)
69- for client in getAllClients())
70- responses = [
71- response["images"]
72- for response in suppress_failures(responses)
73- ]
74- if len(responses) == 0:
75- return []
76-
77- # Create the initial set of images from the first response. This will be
78- # used to perform the intersection of all the other responses.
79- images = responses.pop()
80- images = {
81- frozenset(image.items())
82- for image in images
83- }
84-
85- # Intersect all of the remaining responses to get only the images that
86- # exist on all clusters.
87- for response in responses:
88- response_images = {
89- frozenset(image.items())
90- for image in response
91- }
92- images = images & response_images
93-
94- # Return only boot images on all cluster, in the same format as
95- # get_boot_images.
96- return [
97- dict(image)
98- for image in list(images)
99- ]
100+ image_sets = list(_get_available_boot_images())
101+ if len(image_sets) > 0:
102+ images = frozenset.intersection(*image_sets)
103+ else:
104+ images = frozenset()
105+ # Return using the same format as get_boot_images.
106+ return list(dict(image) for image in images)
107
108
109 @synchronous
110
111=== modified file 'src/maasserver/clusterrpc/tests/test_boot_images.py'
112--- src/maasserver/clusterrpc/tests/test_boot_images.py 2014-12-16 19:29:45 +0000
113+++ src/maasserver/clusterrpc/tests/test_boot_images.py 2015-08-03 19:11:39 +0000
114@@ -16,6 +16,7 @@
115
116 import os
117
118+from maasserver.clusterrpc import boot_images as boot_images_module
119 from maasserver.clusterrpc.boot_images import (
120 get_available_boot_images,
121 get_boot_images,
122@@ -29,9 +30,24 @@
123 NODEGROUP_STATUS,
124 )
125 from maasserver.rpc import getAllClients
126-from maasserver.rpc.testing.fixtures import RunningClusterRPCFixture
127+from maasserver.rpc.testing.fixtures import (
128+ MockLiveRegionToClusterRPCFixture,
129+ RunningClusterRPCFixture,
130+ )
131+from maasserver.testing.eventloop import (
132+ RegionEventLoopFixture,
133+ RunningEventLoopFixture,
134+ )
135 from maasserver.testing.factory import factory
136 from maasserver.testing.testcase import MAASServerTestCase
137+from maastesting.matchers import (
138+ MockCalledOnceWith,
139+ MockCallsMatch,
140+ )
141+from mock import (
142+ call,
143+ MagicMock,
144+ )
145 from provisioningserver.boot.tests import test_tftppath
146 from provisioningserver.boot.tftppath import (
147 compose_image_path,
148@@ -41,11 +57,16 @@
149 boot_images,
150 clusterservice,
151 )
152+from provisioningserver.rpc.cluster import (
153+ ListBootImages,
154+ ListBootImagesV2,
155+ )
156 from provisioningserver.testing.boot_images import (
157 make_boot_image_storage_params,
158 make_image,
159 )
160 from twisted.internet.defer import succeed
161+from twisted.protocols.amp import UnhandledCommand
162
163
164 def make_image_dir(image_params, tftproot):
165@@ -164,6 +185,28 @@
166 ],
167 get_boot_images(nodegroup))
168
169+ def test_calls_ListBootImagesV2_before_ListBootImages(self):
170+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
171+ mock_client = MagicMock()
172+ self.patch_autospec(
173+ boot_images_module, "getClientFor").return_value = mock_client
174+ get_boot_images(nodegroup)
175+ self.assertThat(mock_client, MockCalledOnceWith(ListBootImagesV2))
176+
177+ def test_calls_ListBootImages_if_raised_UnhandledCommand(self):
178+ nodegroup = factory.make_NodeGroup(status=NODEGROUP_STATUS.ACCEPTED)
179+ mock_client = MagicMock()
180+ self.patch_autospec(
181+ boot_images_module, "getClientFor").return_value = mock_client
182+ mock_client.return_value.wait.side_effect = [
183+ UnhandledCommand(),
184+ {"images": []},
185+ ]
186+ get_boot_images(nodegroup)
187+ self.assertThat(mock_client, MockCallsMatch(
188+ call(ListBootImagesV2),
189+ call(ListBootImages)))
190+
191
192 class TestGetAvailableBootImages(MAASServerTestCase):
193 """Tests for `get_available_boot_images`."""
194@@ -239,6 +282,37 @@
195 images,
196 get_available_boot_images())
197
198+ def test_fallback_to_ListBootImages_on_old_clusters(self):
199+ nodegroup_1 = factory.make_NodeGroup()
200+ nodegroup_1.accept()
201+ nodegroup_2 = factory.make_NodeGroup()
202+ nodegroup_2.accept()
203+ nodegroup_3 = factory.make_NodeGroup()
204+ nodegroup_3.accept()
205+
206+ images = [make_rpc_boot_image() for _ in range(3)]
207+
208+ # Limit the region's event loop to only the "rpc" service.
209+ self.useFixture(RegionEventLoopFixture("rpc"))
210+ # Now start the region's event loop.
211+ self.useFixture(RunningEventLoopFixture())
212+ # This fixture allows us to simulate mock clusters.
213+ rpc = self.useFixture(MockLiveRegionToClusterRPCFixture())
214+
215+ # This simulates an older cluster, one without ListBootImagesV2.
216+ cluster_1 = rpc.makeCluster(nodegroup_1, ListBootImages)
217+ cluster_1.ListBootImages.return_value = succeed({'images': images})
218+
219+ # This simulates a newer cluster, one with ListBootImagesV2.
220+ cluster_2 = rpc.makeCluster(nodegroup_2, ListBootImagesV2)
221+ cluster_2.ListBootImagesV2.return_value = succeed({'images': images})
222+
223+ # This simulates a broken cluster.
224+ cluster_3 = rpc.makeCluster(nodegroup_3, ListBootImagesV2)
225+ cluster_3.ListBootImagesV2.side_effect = ZeroDivisionError
226+
227+ self.assertItemsEqual(images, get_available_boot_images())
228+
229 def test_returns_empty_list_when_all_clusters_fail(self):
230 factory.make_NodeGroup().accept()
231 factory.make_NodeGroup().accept()
232
233=== added file 'src/maasserver/utils/version.py'
234--- src/maasserver/utils/version.py 1970-01-01 00:00:00 +0000
235+++ src/maasserver/utils/version.py 2015-08-03 19:11:39 +0000
236@@ -0,0 +1,131 @@
237+# Copyright 2015 Canonical Ltd. This software is licensed under the
238+# GNU Affero General Public License version 3 (see the file LICENSE).
239+
240+"""Version utilities."""
241+
242+from __future__ import (
243+ absolute_import,
244+ print_function,
245+ unicode_literals,
246+ )
247+
248+str = None
249+
250+__metaclass__ = type
251+__all__ = [
252+ "get_maas_doc_version",
253+ "get_maas_version_subversion",
254+ "get_maas_version_ui",
255+ ]
256+
257+import apt_pkg
258+
259+
260+try:
261+ from bzrlib.branch import Branch
262+ from bzrlib.errors import NotBranchError
263+except ImportError:
264+ Branch = None
265+
266+# Initialize apt_pkg.
267+apt_pkg.init()
268+
269+# Name of maas package to get version from.
270+REGION_PACKAGE_NAME = "maas-region-controller-min"
271+
272+
273+def get_version_from_apt(package):
274+ """Return the version output from `apt_pkg.Cache` for the given package."""
275+ cache = apt_pkg.Cache(None)
276+ version = None
277+ if package in cache:
278+ apt_package = cache[package]
279+ version = apt_package.current_ver
280+
281+ if version is not None:
282+ return version.ver_str
283+ else:
284+ return ""
285+
286+
287+def extract_version_subversion(version):
288+ """Return a tuple (version, subversion) from the given apt version."""
289+ if "~" in version:
290+ main_version, extra = version.split("~", 1)
291+ return main_version, extra.split("-", 1)[0]
292+ elif "+" in version:
293+ main_version, extra = version.split("+", 1)
294+ return main_version, "+" + extra.split("-", 1)[0]
295+ else:
296+ return version.split("-", 1)[0], ''
297+
298+
299+def get_maas_branch():
300+ """Return the `bzrlib.branch.Branch` for this running MAAS."""
301+ if Branch is None:
302+ return None
303+ try:
304+ return Branch.open(".")
305+ except NotBranchError:
306+ return None
307+
308+
309+_cache = {}
310+
311+
312+# A very simply memoize function: when we switch to Django 1.7 we should use
313+# Django's lru_cache method.
314+def simple_cache(fun):
315+ def wrapped(*args, **kwargs):
316+ key = hash(repr(fun) + repr(args) + repr(kwargs))
317+ if key not in _cache:
318+ _cache[key] = fun(*args, **kwargs)
319+ return _cache[key]
320+
321+ wrapped.__doc__ = "%s %s" % (fun.__doc__, "(cached)")
322+ return wrapped
323+
324+
325+@simple_cache
326+def get_maas_package_version():
327+ """Return the apt version for the main MAAS package."""
328+ return get_version_from_apt(REGION_PACKAGE_NAME)
329+
330+
331+@simple_cache
332+def get_maas_version_subversion():
333+ """Return a tuple with the MAAS version and the MAAS subversion."""
334+ apt_version = get_maas_package_version()
335+ if apt_version:
336+ return extract_version_subversion(apt_version)
337+ else:
338+ # Get the branch information
339+ branch = get_maas_branch()
340+ if branch is None:
341+ # Not installed not in branch, then no way to identify. This should
342+ # not happen, but just in case.
343+ return "unknown", ''
344+ else:
345+ return "from source (+bzr%s)" % branch.revno(), ''
346+
347+
348+@simple_cache
349+def get_maas_version_ui():
350+ """Return the version string for the running MAAS region.
351+
352+ The returned string is suitable to display in the UI.
353+ """
354+ version, subversion = get_maas_version_subversion()
355+ return "%s (%s)" % (version, subversion) if subversion else version
356+
357+
358+@simple_cache
359+def get_maas_doc_version():
360+ """Return the doc version for the running MAAS region."""
361+ doc_prefix = 'docs'
362+ apt_version = get_maas_package_version()
363+ if apt_version:
364+ version, _ = extract_version_subversion(apt_version)
365+ return doc_prefix + '.'.join(version.split('.')[:2])
366+ else:
367+ return doc_prefix
368
369=== removed file 'src/maasserver/utils/version.py'
370--- src/maasserver/utils/version.py 2015-04-08 15:32:33 +0000
371+++ src/maasserver/utils/version.py 1970-01-01 00:00:00 +0000
372@@ -1,131 +0,0 @@
373-# Copyright 2015 Canonical Ltd. This software is licensed under the
374-# GNU Affero General Public License version 3 (see the file LICENSE).
375-
376-"""Version utilities."""
377-
378-from __future__ import (
379- absolute_import,
380- print_function,
381- unicode_literals,
382- )
383-
384-str = None
385-
386-__metaclass__ = type
387-__all__ = [
388- "get_maas_doc_version",
389- "get_maas_version_subversion",
390- "get_maas_version_ui",
391- ]
392-
393-import apt_pkg
394-
395-
396-try:
397- from bzrlib.branch import Branch
398- from bzrlib.errors import NotBranchError
399-except ImportError:
400- Branch = None
401-
402-# Initialize apt_pkg.
403-apt_pkg.init()
404-
405-# Name of maas package to get version from.
406-REGION_PACKAGE_NAME = "maas-region-controller-min"
407-
408-
409-def get_version_from_apt(package):
410- """Return the version output from `apt_pkg.Cache` for the given package."""
411- cache = apt_pkg.Cache(None)
412- version = None
413- if package in cache:
414- apt_package = cache[package]
415- version = apt_package.current_ver
416-
417- if version is not None:
418- return version.ver_str
419- else:
420- return ""
421-
422-
423-def extract_version_subversion(version):
424- """Return a tuple (version, subversion) from the given apt version."""
425- if "~" in version:
426- main_version, extra = version.split("~", 1)
427- return main_version, extra.split("-", 1)[0]
428- elif "+" in version:
429- main_version, extra = version.split("+", 1)
430- return main_version, "+" + extra.split("-", 1)[0]
431- else:
432- return version.split("-", 1)[0], ''
433-
434-
435-def get_maas_branch():
436- """Return the `bzrlib.branch.Branch` for this running MAAS."""
437- if Branch is None:
438- return None
439- try:
440- return Branch.open(".")
441- except NotBranchError:
442- return None
443-
444-
445-_cache = {}
446-
447-
448-# A very simply memoize function: when we switch to Django 1.7 we should use
449-# Django's lru_cache method.
450-def simple_cache(fun):
451- def wrapped(*args, **kwargs):
452- key = hash(repr(fun) + repr(args) + repr(kwargs))
453- if not key in _cache:
454- _cache[key] = fun(*args, **kwargs)
455- return _cache[key]
456-
457- wrapped.__doc__ = "%s %s" % (fun.__doc__, "(cached)")
458- return wrapped
459-
460-
461-@simple_cache
462-def get_maas_package_version():
463- """Return the apt version for the main MAAS package."""
464- return get_version_from_apt(REGION_PACKAGE_NAME)
465-
466-
467-@simple_cache
468-def get_maas_version_subversion():
469- """Return a tuple with the MAAS version and the MAAS subversion."""
470- apt_version = get_maas_package_version()
471- if apt_version:
472- return extract_version_subversion(apt_version)
473- else:
474- # Get the branch information
475- branch = get_maas_branch()
476- if branch is None:
477- # Not installed not in branch, then no way to identify. This should
478- # not happen, but just in case.
479- return "unknown", ''
480- else:
481- return "from source (+bzr%s)" % branch.revno(), ''
482-
483-
484-@simple_cache
485-def get_maas_version_ui():
486- """Return the version string for the running MAAS region.
487-
488- The returned string is suitable to display in the UI.
489- """
490- version, subversion = get_maas_version_subversion()
491- return "%s (%s)" % (version, subversion) if subversion else version
492-
493-
494-@simple_cache
495-def get_maas_doc_version():
496- """Return the doc version for the running MAAS region."""
497- doc_prefix = 'docs'
498- apt_version = get_maas_package_version()
499- if apt_version:
500- version, _ = extract_version_subversion(apt_version)
501- return doc_prefix + '.'.join(version.split('.')[:2])
502- else:
503- return doc_prefix
504
505=== modified file 'src/provisioningserver/rpc/cluster.py'
506--- src/provisioningserver/rpc/cluster.py 2015-07-31 20:46:29 +0000
507+++ src/provisioningserver/rpc/cluster.py 2015-08-03 19:11:39 +0000
508@@ -59,6 +59,30 @@
509
510 arguments = []
511 response = [
512+ (b"images", amp.AmpList(
513+ [(b"osystem", amp.Unicode()),
514+ (b"architecture", amp.Unicode()),
515+ (b"subarchitecture", amp.Unicode()),
516+ (b"release", amp.Unicode()),
517+ (b"label", amp.Unicode()),
518+ (b"purpose", amp.Unicode()),
519+ (b"xinstall_type", amp.Unicode()),
520+ (b"xinstall_path", amp.Unicode())]))
521+ ]
522+ errors = []
523+
524+
525+class ListBootImagesV2(amp.Command):
526+ """List the boot images available on this cluster controller.
527+
528+ This command compresses the images list to allow more images in the
529+ response and to remove the amp.TooLong error.
530+
531+ :since: 1.7.6
532+ """
533+
534+ arguments = []
535+ response = [
536 (b"images", CompressedAmpList(
537 [(b"osystem", amp.Unicode()),
538 (b"architecture", amp.Unicode()),
539
540=== modified file 'src/provisioningserver/rpc/clusterservice.py'
541--- src/provisioningserver/rpc/clusterservice.py 2014-12-18 13:53:48 +0000
542+++ src/provisioningserver/rpc/clusterservice.py 2015-08-03 19:11:39 +0000
543@@ -149,6 +149,15 @@
544 """
545 return {"images": list_boot_images()}
546
547+ @cluster.ListBootImagesV2.responder
548+ def list_boot_images_v2(self):
549+ """list_boot_images_v2()
550+
551+ Implementation of
552+ :py:class:`~provisioningserver.rpc.cluster.ListBootImagesV2`.
553+ """
554+ return {"images": list_boot_images()}
555+
556 @cluster.ImportBootImages.responder
557 def import_boot_images(self, sources, http_proxy=None, https_proxy=None):
558 """import_boot_images()
559
560=== modified file 'src/provisioningserver/rpc/tests/test_clusterservice.py'
561--- src/provisioningserver/rpc/tests/test_clusterservice.py 2015-03-02 16:12:40 +0000
562+++ src/provisioningserver/rpc/tests/test_clusterservice.py 2015-08-03 19:11:39 +0000
563@@ -217,14 +217,19 @@
564 return d.addCallback(check)
565
566
567-class TestClusterProtocol_ListBootImages(MAASTestCase):
568+class TestClusterProtocol_ListBootImages_and_ListBootImagesV2(MAASTestCase):
569
570 run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
571
572+ scenarios = (
573+ ("ListBootImages", {"rpc_call": cluster.ListBootImages}),
574+ ("ListBootImagesV2", {"rpc_call": cluster.ListBootImagesV2}),
575+ )
576+
577 def test_list_boot_images_is_registered(self):
578 protocol = Cluster()
579 responder = protocol.locateResponder(
580- cluster.ListBootImages.commandName)
581+ self.rpc_call.commandName)
582 self.assertIsNotNone(responder)
583
584 @inlineCallbacks
585@@ -233,7 +238,7 @@
586 list_boot_images = self.patch(tftppath, "list_boot_images")
587 list_boot_images.return_value = []
588
589- response = yield call_responder(Cluster(), cluster.ListBootImages, {})
590+ response = yield call_responder(Cluster(), self.rpc_call, {})
591
592 self.assertEqual({"images": []}, response)
593
594@@ -281,7 +286,7 @@
595 expected_image['xinstall_path'] = ''
596 expected_image['xinstall_type'] = ''
597
598- response = yield call_responder(Cluster(), cluster.ListBootImages, {})
599+ response = yield call_responder(Cluster(), self.rpc_call, {})
600
601 self.assertThat(response, KeysEqual("images"))
602 self.assertItemsEqual(expected_images, response["images"])
603
604=== modified file 'src/provisioningserver/rpc/tests/test_docs.py'
605--- src/provisioningserver/rpc/tests/test_docs.py 2014-06-25 11:26:56 +0000
606+++ src/provisioningserver/rpc/tests/test_docs.py 2015-08-03 19:11:39 +0000
607@@ -70,6 +70,7 @@
608 self.since_clause_missing_message, Contains(":since:"))
609 since_clause_contains_version = Annotate(
610 self.since_clause_version_not_recognised, MatchesRegex(
611- ".*^:since: *[1-9][.][0-9]+$", re.DOTALL | re.MULTILINE))
612+ ".*^:since: *[1-9][.][0-9]+([.][0-9]+)?$",
613+ re.DOTALL | re.MULTILINE))
614 self.assertThat(getdoc(self.command), MatchesAll(
615 contains_since_clause, since_clause_contains_version))

Subscribers

People subscribed via source and target branches

to all changes: