Merge lp:~racb/maas/arch-constraint-wildcard into lp:~maas-committers/maas/trunk

Proposed by Robie Basak
Status: Merged
Approved by: Robie Basak
Approved revision: no longer in the source branch.
Merged at revision: 1226
Proposed branch: lp:~racb/maas/arch-constraint-wildcard
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 188 lines (+96/-6)
4 files modified
src/maasserver/enum.py (+4/-0)
src/maasserver/models/node_constraint_filter.py (+53/-1)
src/maasserver/tests/test_api.py (+3/-3)
src/maasserver/tests/test_node_constraint_filter.py (+36/-2)
To merge this branch: bzr merge lp:~racb/maas/arch-constraint-wildcard
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Martin Packman (community) Approve
Review via email: mp+128513@code.launchpad.net

Commit message

Add automatic architecture constraint wildcarding and arm alias

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Looks good.

The generate_architecture_wildcards is very cute, I'm not totally convinced it's needed versus just making someone type that out next to the arch enum, but as you'll be the one who has to add arm flavours it's fair enough to save yourself the bother. :)

Some small notes:

+ # Replace an alias with its value if it is an alias
+ try:
+ aliased_value = primary_architecture_aliases[value]
+ except KeyError:
+ aliased_value = value

This is not an exceptional circumstance, so I'd use the following (still non-recursive) spelling instead:

    aliased_value = primary_architecture_aliases.get(value, value)

+ if aliased_value in (choice[0] for choice in ARCHITECTURE_CHOICES):

Rather than making a generator on the fly here to test membership, I'd do either:
* Add the full form as a mapping to a 1-tuple of itself in generate_architecture_wildcards and use the block below only
* Create an ARCHITECTURE_CHOICES_DICT in maasserver.enum as per other objects there and use `if aliased_value in ARCHITECTURE_CHOICES_DICT` instead.

I find test_generate_architecture_wildcards slightly hard to understand, a docstring, comment or more assertions might help.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (4.0 KiB)

Looks good, but I have some notes that I'd like you to address. Don't worry -- I usually do!

Notes follow.

.

In node_constraint_filter.py:

52 +def constrain_architecture(nodes, key, value):
53 + assert(key == 'architecture')

In python, "assert" is a statement:

    assert key == 'architecture'

This can matter because it takes a second argument, namely an error string that will be shown if the assertion fails:

    assert key == 'architecture', "This filter is for architecture only."

If you parenthesize as one might in C, things go mysteriously wrong:

    assert(key == 'architecture', "This filter is for architecture only.")

This creates a tuple of a boolean and a string, and then asserts that the tuple, not the boolean, evaluates to True. Not what you want! If you need a line break, the idiomatic way of doing it is:

    assert key == 'architecture', (
        "This filter is for architecture only.")

.

55 + # Replace an alias with its value if it is an alias
56 + try:
57 + aliased_value = primary_architecture_aliases[value]
58 + except KeyError:
59 + aliased_value = value

It seems misleading to write this such that the normal, successful, expected-to-be-fast code path is the one taking an exception. An exception conveys both to the reader and to the language implementation that something is out of the ordinary, that the intended code flow is different, and that it's probably worth sacrificing performance in favour of the non-exceptional code path even in extreme trade-offs.

So I think it would be clearer to write it thus:

    aliased_value = primary_architecture.aliases.get(value, value)

.

68 + else:
69 + raise InvalidConstraint(
70 + 'architecture', value, 'Architecture not recognised')

I would recommend "double quotes" for free-form text. This is not shell where you have to keep a constant eye on weird characters in double-quoted strings, and indeed the character most worth worrying about is the apostrophe. It'd be silly to change your quoting just because your sentence sprouted a contraction!

.

In test_api.py:

91 - def test_POST_acquire_treats_unknown_arch_as_resource_conflict(self):
92 + def test_POST_acquire_treats_unknown_arch_as_bad_request(self):
93 # Asking for an unknown arch returns an HTTP conflict

The comment still needs updating!

.

In test_node_constraint_filter.py:

124 + def test_generate_architecture_wildcards(self):
125 + single_subarch = factory.getRandomString(), factory.getRandomString()
126 + double_subarch_1 = factory.getRandomString(), factory.getRandomString()
127 + double_subarch_2 = double_subarch_1[0], factory.getRandomString()

Might I recommend factory.make_name()? Depending on the exact string you pass, factory.make_name('arch') is not going to be much longer than factory.getRandomString() but it produces a more recognizable string, which can be useful in failure output.

.

In the same test module, test_architecture is getting out of hand. There is little enough shared test setup that it doesn't seem worth the flexibility cost to bundle all these different behaviours and outcomes together into o...

Read more...

review: Approve
Revision history for this message
Robie Basak (racb) wrote :

Thanks for the reviews, Martin and Jeroen! Loads of improvements there. I've fixed all of these except for refactoring of the architecture constraint tests for which I'll submit a separate proposal, so I'll land this now.

Revision history for this message
Robie Basak (racb) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/enum.py'
2--- src/maasserver/enum.py 2012-10-02 17:53:24 +0000
3+++ src/maasserver/enum.py 2012-10-08 17:35:23 +0000
4@@ -13,6 +13,7 @@
5 __all__ = [
6 'ARCHITECTURE',
7 'ARCHITECTURE_CHOICES',
8+ 'ARCHITECTURE_CHOICES_DICT',
9 'COMPONENT',
10 'NODEGROUP_STATUS',
11 'NODEGROUP_STATUS_CHOICES',
12@@ -127,6 +128,9 @@
13 )
14
15
16+ARCHITECTURE_CHOICES_DICT = OrderedDict(ARCHITECTURE_CHOICES)
17+
18+
19 class DISTRO_SERIES:
20 """List of supported ubuntu releases."""
21 #:
22
23=== modified file 'src/maasserver/models/node_constraint_filter.py'
24--- src/maasserver/models/node_constraint_filter.py 2012-10-04 10:52:31 +0000
25+++ src/maasserver/models/node_constraint_filter.py 2012-10-08 17:35:23 +0000
26@@ -12,8 +12,13 @@
27 'constrain_nodes',
28 ]
29
30+import itertools
31 import math
32
33+from maasserver.enum import (
34+ ARCHITECTURE_CHOICES,
35+ ARCHITECTURE_CHOICES_DICT,
36+ )
37 from maasserver.exceptions import (
38 InvalidConstraint,
39 )
40@@ -51,12 +56,59 @@
41 return nodes
42
43
44+def generate_architecture_wildcards(choices=ARCHITECTURE_CHOICES):
45+ """Map 'primary' architecture names to a list of full expansions.
46+
47+ Return a dictionary keyed by the primary architecture name (the part before
48+ the '/'). The value of an entry is a frozenset of full architecture names
49+ ('primary_arch/subarch') under the keyed primary architecture.
50+
51+ """
52+
53+ sorted_arch_list = sorted(choice[0] for choice in choices)
54+
55+ def extract_primary_arch(arch):
56+ return arch.split('/')[0]
57+
58+ return {
59+ primary_arch: frozenset(subarch_generator)
60+ for primary_arch, subarch_generator in itertools.groupby(
61+ sorted_arch_list, key=extract_primary_arch
62+ )
63+ }
64+
65+
66+architecture_wildcards = generate_architecture_wildcards()
67+
68+
69+# juju uses a general "arm" architecture constraint across all of its
70+# providers. Since armhf is the cross-distro agreed Linux userspace
71+# architecture and ABI and ARM servers are expected to only use armhf,
72+# interpret "arm" to mean "armhf" in MAAS.
73+architecture_wildcards['arm'] = architecture_wildcards['armhf']
74+
75+
76+def constrain_architecture(nodes, key, value):
77+ assert key == 'architecture', "This filter is for architecture only."
78+
79+ if value in ARCHITECTURE_CHOICES_DICT:
80+ # Full 'arch/subarch' specified directly
81+ return nodes.filter(architecture=value)
82+ elif value in architecture_wildcards:
83+ # Try to expand 'arch' to all available 'arch/subarch' matches
84+ return nodes.filter(
85+ architecture__in=architecture_wildcards[value])
86+ else:
87+ raise InvalidConstraint(
88+ 'architecture', value, "Architecture not recognised.")
89+
90+
91 # this is the mapping of constraint names to how to apply the constraint
92 constraint_filters = {
93 # Currently architecture only supports exact matching. Eventually, we will
94 # probably want more logic to note that eg, amd64 can be used for an i386
95 # request
96- 'architecture': constrain_identical,
97+ 'architecture': constrain_architecture,
98 'hostname': constrain_identical,
99 'cpu_count': constrain_int_greater_or_equal,
100 'memory': constrain_int_greater_or_equal,
101
102=== modified file 'src/maasserver/tests/test_api.py'
103--- src/maasserver/tests/test_api.py 2012-10-08 11:46:58 +0000
104+++ src/maasserver/tests/test_api.py 2012-10-08 17:35:23 +0000
105@@ -1914,14 +1914,14 @@
106 response_json = json.loads(response.content)
107 self.assertEqual(node.architecture, response_json['architecture'])
108
109- def test_POST_acquire_treats_unknown_arch_as_resource_conflict(self):
110- # Asking for an unknown arch returns an HTTP conflict
111+ def test_POST_acquire_treats_unknown_arch_as_bad_request(self):
112+ # Asking for an unknown arch returns an HTTP "400 Bad Request"
113 factory.make_node(status=NODE_STATUS.READY)
114 response = self.client.post(self.get_uri('nodes/'), {
115 'op': 'acquire',
116 'arch': 'sparc',
117 })
118- self.assertEqual(httplib.CONFLICT, response.status_code)
119+ self.assertEqual(httplib.BAD_REQUEST, response.status_code)
120
121 def test_POST_acquire_allocates_node_by_cpu(self):
122 # Asking for enough cpu acquires a node with at least that.
123
124=== modified file 'src/maasserver/tests/test_node_constraint_filter.py'
125--- src/maasserver/tests/test_node_constraint_filter.py 2012-10-04 10:52:31 +0000
126+++ src/maasserver/tests/test_node_constraint_filter.py 2012-10-08 17:35:23 +0000
127@@ -15,7 +15,10 @@
128 from maasserver.enum import ARCHITECTURE
129 from maasserver.exceptions import InvalidConstraint
130 from maasserver.models import Node
131-from maasserver.models.node_constraint_filter import constrain_nodes
132+from maasserver.models.node_constraint_filter import (
133+ constrain_nodes,
134+ generate_architecture_wildcards,
135+)
136 from maasserver.testing.factory import factory
137 from maasserver.testing.testcase import TestCase
138 from maasserver.utils import ignore_unused
139@@ -27,6 +30,29 @@
140 nodes = constrain_nodes(Node.objects.all(), constraints)
141 self.assertItemsEqual(expected_nodes, nodes)
142
143+ def test_generate_architecture_wildcards(self):
144+ # Create a test architecture choice list of one architecture that only
145+ # has one available subarch (single_subarch) and two architectures that
146+ # have a matching primary architecture (double_subarch_{1,2})
147+ single_subarch = factory.make_name('arch'), factory.make_name('arch')
148+ double_subarch_1 = factory.make_name('arch'), factory.make_name('arch')
149+ double_subarch_2 = double_subarch_1[0], factory.make_name('arch')
150+ choices = (
151+ ('/'.join(single_subarch), None),
152+ ('/'.join(double_subarch_1), None),
153+ ('/'.join(double_subarch_2), None),
154+ )
155+
156+ # single_subarch should end up in the dict essentially unchanged, and
157+ # the double_subarchs should have been flattened into a single dict
158+ # element with a list of them.
159+ self.assertEquals({
160+ single_subarch[0]: frozenset([choices[0][0]]),
161+ double_subarch_1[0]: frozenset([choices[1][0], choices[2][0]]),
162+ },
163+ generate_architecture_wildcards(choices=choices)
164+ )
165+
166 def test_no_constraints(self):
167 node1 = factory.make_node()
168 node2 = factory.make_node()
169@@ -43,10 +69,18 @@
170 def test_architecture(self):
171 node1 = factory.make_node(architecture=ARCHITECTURE.i386)
172 node2 = factory.make_node(architecture=ARCHITECTURE.armhf_highbank)
173+ self.assertConstrainedNodes([node1], {'architecture': 'i386'})
174 self.assertConstrainedNodes([node1], {'architecture': 'i386/generic'})
175 self.assertConstrainedNodes(
176+ [node2], {'architecture': 'arm'})
177+ self.assertConstrainedNodes(
178+ [node2], {'architecture': 'armhf'})
179+ self.assertConstrainedNodes(
180 [node2], {'architecture': 'armhf/highbank'})
181- self.assertConstrainedNodes([], {'architecture': 'sparc'})
182+ self.assertRaises(InvalidConstraint,
183+ self.assertConstrainedNodes, [], {'architecture': 'armhf/generic'})
184+ self.assertRaises(InvalidConstraint,
185+ self.assertConstrainedNodes, [], {'architecture': 'sparc'})
186
187 def test_cpu_count(self):
188 node1 = factory.make_node(cpu_count=1)