Merge lp:~gz/pyjuju/add_maas_constraints into lp:pyjuju
- add_maas_constraints
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Kapil Thangavelu | ||||
Approved revision: | 582 | ||||
Merged at revision: | 585 | ||||
Proposed branch: | lp:~gz/pyjuju/add_maas_constraints | ||||
Merge into: | lp:pyjuju | ||||
Diff against target: |
270 lines (+132/-11) 6 files modified
juju/providers/maas/maas.py (+20/-3) juju/providers/maas/provider.py (+39/-0) juju/providers/maas/tests/test_launch.py (+4/-6) juju/providers/maas/tests/test_maas.py (+32/-0) juju/providers/maas/tests/test_provider.py (+31/-2) juju/providers/maas/tests/testing.py (+6/-0) |
||||
To merge this branch: | bzr merge lp:~gz/pyjuju/add_maas_constraints | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+126203@code.launchpad.net |
Commit message
Description of the change
Add generic and tag constraints to maas provider
Passes through standard constraints 'arch', 'cpu', and 'mem', also
specific 'maas-tags' which can be used to indicate other properties.
Launching a new machine still involves calling acquire in the MaaS api
which returns only one node rather than a list of candidates to select
from, so the constraints are still not used on the Juju side for now.
Martin Packman (gz) wrote : | # |
John A Meinel (jameinel) wrote : | # |
(Just adding a comment so that I will get any chatter on this proposal)
Kapil Thangavelu (hazmat) wrote : | # |
from irc
Sep 25 05:27:05 <jam> mgz: well I know hazmat asked us to make it less
specific, so the provider should query Maas for a list of acceptable
tags.
Sep 25 05:27:10 <mgz> could just pick nodes with a superset of the
tags specified, or could do full boolean logic
Sep 25 05:27:18 <jam> less generic?
Sep 25 05:27:44 <mgz> I think hazmat will be fine with maas returning
good errors for non-existant tags or a bad tag string
Sep 25 05:27:49 <jam> The argument is that deploying is async, so by
the time the provider responds, it is too late to inform the user of a
typo/etc.
Sep 25 05:28:35 <mgz> right, so provided we catch before acquire
actually happens that a tag is bad, and return a 4XX straight away
without going down to the cluster, I think that's fine
Sep 25 05:30:49 <mgz> ...I probably want to add some tests for that
error case
so a user on the cli is going to do something like
juju deploy xyz --constraints=
except bar doesn't exist, and they'll never see an error, just an
instance which stays forever in pending state, and errors in the
provisioning agent log.
given that this is effectively free form vocabulary of values, it would
be nice to see a validation of the constraint value to prevent this.
constraint validation facilities are a bit weak but effectively on
register(
the vocabulary introspected from the maas server. the logical operators
can be stripped and the remainder differenced against the vocabulary to
detect unknown values, raising a constrainterror on invalid values.
https:/
File juju/providers/
https:/
juju/providers/
is maas-name still supported?
Martin Packman (gz) wrote : | # |
> so a user on the cli is going to do something like
>
> juju deploy xyz --constraints=
>
> except bar doesn't exist, and they'll never see an error, just an
> instance which stays forever in pending state, and errors in the
> provisioning agent log.
Thanks for clarifying, that is a painful scenario. So, this is already an issue with providers that permit constraints that can't be fully validated at the juju cli level before passing on to the provisioning agent? For instance, supplying 'ec2-zone' that is a simble ascii character, but does not exist in the region? Is there a bug filed already for better error reporting in cases like this where the provisioning agent fails to deploy something? Just retrying forever isn't really ideal.
> given that this is effectively free form vocabulary of values, it would
> be nice to see a validation of the constraint value to prevent this.
Okay, I'll go back to the earlier code I had which checked tags were valid at constraint creation. It's a little painful because juju can't sensibly cache the list of tags, as the scenario where someone creates a new tag using the maas cli client then wants to juju deploy a service constrained on that tag immediately seems quite likely. As convert callbacks aren't deferred, it's not possible to recheck for a newly added tag on the fly.
> https:/
> ode48
> juju/providers/
> is maas-name still supported?
I see no reason to drop it as part of this branch, and the underlying code in MaaS is still present for now. It's simply not useful if you want to use other constraints.
Kapil Thangavelu (hazmat) wrote : | # |
On Thursday, September 27, 2012, Martin Packman wrote:
> > so a user on the cli is going to do something like
> >
> > juju deploy xyz --constraints=
> >
> > except bar doesn't exist, and they'll never see an error, just an
> > instance which stays forever in pending state, and errors in the
> > provisioning agent log.
>
> Thanks for clarifying, that is a painful scenario. So, this is already an
> issue with providers that permit constraints that can't be fully validated
> at the juju cli level before passing on to the provisioning agent? For
> instance, supplying 'ec2-zone' that is a simble ascii character, but does
> not exist in the region? Is there a bug filed already for better error
> reporting in cases like this where the provisioning agent fails to deploy
> something? Just retrying forever isn't really ideal.
It's definitely an issue, the other providers export a fairly well known
set of symbols, there is some limited validation of bits like zone, which
actually does correspond to an ec2 entity albeit in abbreviated form, and
the symbol itself is well symbolic at the aws level.
A further notion is tracking state creation, and properly
reporting/
has passed.
> > given that this is effectively free form vocabulary of values, it would
> > be nice to see a validation of the constraint value to prevent this.
>
> Okay, I'll go back to the earlier code I had which checked tags were valid
> at constraint creation. It's a little painful because juju can't sensibly
> cache the list of tags, as the scenario where someone creates a new tag
> using the maas cli client then wants to juju deploy a service constrained
> on that tag immediately seems quite likely. As convert callbacks aren't
> deferred, it's not possible to recheck for a newly added tag on the fly.
>
>
I'll need to address some of that in the security work, re serialization of
constraints, to bypass non privileged agents using the constraints
definition, unfortunately constraints got at the env level, get looked up
from machine states, and unit states, as it's effectively an ordered
multidict lookup for those entities.
> >
> https:/
> > ode48
> > juju/providers/
> > is maas-name still supported?
>
> I see no reason to drop it as part of this branch, and the underlying code
> in MaaS is still present for now. It's simply not useful if you want to use
> other constraints.
It's worse than not useful IMO, its proven actively detrimental, but your
right re keeping, hopefully with real constraints people can stop using it.
> --
> https:/
> You are subscribed to branch lp:juju.
>
John A Meinel (jameinel) wrote : | # |
So at this point on the Maas side, we raise a InvalidConstraint error if you ask for a tag that doesn't exist in the DB. (Which gets turned into a proper BAD_REQUEST error with a helpful error message.)
Is that enough for this to work nicely? (Or is the actual request so async that it still isn't in time.)
If so, you can query api/1.0/
I agree that we shouldn't be caching the known list of tags.
Kapil Thangavelu (hazmat) wrote : | # |
> So at this point on the Maas side, we raise a InvalidConstraint error if you
> ask for a tag that doesn't exist in the DB. (Which gets turned into a proper
> BAD_REQUEST error with a helpful error message.)
>
> Is that enough for this to work nicely? (Or is the actual request so async
> that it still isn't in time.)
it does not, the scenario in my initial reply still holds true. the user commands don't execute calls to providers for normal usage (exceptions bootstrap/
>
> If so, you can query api/1.0/
> tags in the db (it probably gives too much information, but you will get the
> list of tag names out of that).
>
sounds good, what's the return value of an individual tag in that result look like?
> I agree that we shouldn't be caching the known list of tags.
looking back my reply about this was a little unclear previously, but agreed no need to cache.
- 581. By Martin Packman
-
Revert to checking tag constraint directly, fetch list of valid tags from maas
Martin Packman (gz) wrote : | # |
Please take a look.
- 582. By Martin Packman
-
Merge trunk to resolve conflicts with removal limitation to precise
Kapil Thangavelu (hazmat) wrote : | # |
looks nice, thanks.
One concern for which i'd request you'd file a new bug for future ref,
is that given the valid tag set could change at any time, a previously
valid set could become invalid. That's deferrable for now, at the moment
it will just yield a problem creating services.
Kapil Thangavelu (hazmat) wrote : | # |
s/creating services/creating service units.
Martin Packman (gz) wrote : | # |
On 2012/10/01 14:39:59, hazmat wrote:
> looks nice, thanks.
> One concern for which i'd request you'd file a new bug for future ref,
is that
> given the valid tag set could change at any time, a previously valid
set could
> become invalid. That's deferrable for now, at the moment it will just
yield a
> problem creating services.
I've filed bug 1059753 which I hope captures this issue, feel free to
edit the summary if I have any terminology or details confused. Thanks!
Preview Diff
1 | === modified file 'juju/providers/maas/maas.py' |
2 | --- juju/providers/maas/maas.py 2012-09-20 15:11:08 +0000 |
3 | +++ juju/providers/maas/maas.py 2012-09-28 11:52:20 +0000 |
4 | @@ -44,6 +44,14 @@ |
5 | |
6 | class MAASClient(MAASOAuthConnection): |
7 | |
8 | + _handled_constraints = ( |
9 | + ("maas-name", "name"), |
10 | + ("maas-tags", "tags"), |
11 | + ("arch", "arch"), |
12 | + ("cpu", "cpu_count"), |
13 | + ("mem", "mem"), |
14 | + ) |
15 | + |
16 | def __init__(self, config): |
17 | """Initialise an API client for MAAS. |
18 | |
19 | @@ -119,9 +127,10 @@ |
20 | """ |
21 | params = {"op": "acquire"} |
22 | if constraints is not None: |
23 | - name = constraints["maas-name"] |
24 | - if name is not None: |
25 | - params["name"] = name |
26 | + for key_from, key_to in self._handled_constraints: |
27 | + value = constraints.get(key_from, None) |
28 | + if value is not None: |
29 | + params[key_to] = str(value) |
30 | return self.post("api/1.0/nodes/", params) |
31 | |
32 | def start_node(self, resource_uri, ubuntu_series, user_data): |
33 | @@ -157,3 +166,11 @@ |
34 | """ |
35 | params = {"op": "release"} |
36 | return self.post(resource_uri, params) |
37 | + |
38 | + def list_tags(self): |
39 | + """Ask MAAS to return a list of all the tags defined. |
40 | + |
41 | + :return: A Deferred whose value is the list of tags. |
42 | + """ |
43 | + params = {"op": "list"} |
44 | + return self.get("api/1.0/tags/", params) |
45 | |
46 | === modified file 'juju/providers/maas/provider.py' |
47 | --- juju/providers/maas/provider.py 2012-09-14 20:02:17 +0000 |
48 | +++ juju/providers/maas/provider.py 2012-09-28 11:52:20 +0000 |
49 | @@ -17,6 +17,36 @@ |
50 | log = logging.getLogger("juju.maas") |
51 | |
52 | |
53 | +class _TagHandler(object): |
54 | + """Parser and validator for tags constraint expressions |
55 | + |
56 | + Tag names are extracted and checked against the list of known tags |
57 | + reported by the api. |
58 | + |
59 | + Currently tag constraints consist of just comma and/or whitespace tag |
60 | + names, all of are required for a match. Extending this to support full |
61 | + boolean expressions would be possible, and some forward compatibility |
62 | + is attempted. |
63 | + """ |
64 | + |
65 | + def __init__(self, tags_info): |
66 | + self.tag_names = [tag['name'] for tag in tags_info] |
67 | + |
68 | + compare = staticmethod(set.issuperset) |
69 | + |
70 | + def convert(self, tag_expression): |
71 | + """Extract set of names in tag_expression checking they all exist""" |
72 | + tags = set() |
73 | + for c in (",", "&", "|", "!"): |
74 | + tag_expression = tag_expression.replace(c, " ") |
75 | + for word in tag_expression.strip().split(): |
76 | + tag = word.lower() |
77 | + if tag not in self.tag_names: |
78 | + raise ValueError("tag %r does not exist" % (tag,)) |
79 | + tags.add(tag) |
80 | + return tags |
81 | + |
82 | + |
83 | class MachineProvider(MachineProviderBase): |
84 | """MachineProvider for use in a MAAS environment""" |
85 | |
86 | @@ -40,6 +70,15 @@ |
87 | |
88 | cs.register("ubuntu-series", visible=False) |
89 | cs.register("maas-name") |
90 | + cs.register_generics([]) |
91 | + # MaaS client errors on the provisioning agent are not reported by the |
92 | + # juju cli so try validating tags when the constraint is created. |
93 | + # Because new tags may be registered at any point, caching the list of |
94 | + # tags is not safe and they must be refetched every time. |
95 | + tags_info = yield self.maas_client.list_tags() |
96 | + handler = _TagHandler(tags_info) |
97 | + cs.register("maas-tags", converter=handler.convert, |
98 | + comparer=handler.compare) |
99 | returnValue(cs) |
100 | |
101 | def get_serialization_data(self): |
102 | |
103 | === modified file 'juju/providers/maas/tests/test_launch.py' |
104 | --- juju/providers/maas/tests/test_launch.py 2012-09-27 04:36:51 +0000 |
105 | +++ juju/providers/maas/tests/test_launch.py 2012-09-28 11:52:20 +0000 |
106 | @@ -71,8 +71,7 @@ |
107 | provider = self._get_provider( |
108 | FakeMAASHTTPConnectionWithNoAvailableNodes) |
109 | machine_data = { |
110 | - "machine-id": "foo", "constraints": {"maas-name": None, |
111 | - "ubuntu-series": "splendid"}} |
112 | + "machine-id": "foo", "constraints": {"ubuntu-series": "splendid"}} |
113 | d = provider.start_machine(machine_data) |
114 | |
115 | # These arbitrary fake failure values come from |
116 | @@ -94,8 +93,7 @@ |
117 | # Try to start up that node using the fake MAAS. |
118 | provider = self._get_provider(FakeMAASHTTPConnection) |
119 | machine_data = { |
120 | - "machine-id": machine_id, "constraints": {"maas-name": None, |
121 | - "ubuntu-series": "splendid"}} |
122 | + "machine-id": "foo", "constraints": {"ubuntu-series": "splendid"}} |
123 | machine_list = yield provider.start_machine(machine_data) |
124 | |
125 | # Test that it returns a list containing a single MAASMachine |
126 | @@ -148,7 +146,7 @@ |
127 | # The following operations happen in sequence. |
128 | mocker.order() |
129 | # First, the node is acquired. |
130 | - mock_client.acquire_node({"maas-name": None, "ubuntu-series": "precise"}) |
131 | + mock_client.acquire_node({"ubuntu-series": "precise"}) |
132 | mocker.result({"resource_uri": "/node/123"}) |
133 | # Second, the node is started. We simulate a failure at this stage. |
134 | mock_client.start_node("/node/123", "precise", ANY) |
135 | @@ -160,5 +158,5 @@ |
136 | mocker.replay() |
137 | return self.assertFailure( |
138 | MAASLaunchMachine(mock_provider, |
139 | - {"maas-name": None, "ubuntu-series": "precise"}).run("fred"), |
140 | + {"ubuntu-series": "precise"}).run("fred"), |
141 | ZeroDivisionError) |
142 | |
143 | === modified file 'juju/providers/maas/tests/test_maas.py' |
144 | --- juju/providers/maas/tests/test_maas.py 2012-09-14 20:02:17 +0000 |
145 | +++ juju/providers/maas/tests/test_maas.py 2012-09-28 11:52:20 +0000 |
146 | @@ -337,3 +337,35 @@ |
147 | self.assertIs(None, guinness) |
148 | name = client.post.params_used.get("name") |
149 | self.assertEqual("zaphod", name) |
150 | + |
151 | + def test_acquire_node_handles_arch_constraint(self): |
152 | + client = self.set_up_client_with_fake() |
153 | + constraints = {"arch": "i386"} |
154 | + client.acquire_node(constraints) |
155 | + |
156 | + arch = client.post.params_used.get("arch") |
157 | + self.assertEqual("i386", arch) |
158 | + |
159 | + def test_acquire_node_handles_cpu_constraint(self): |
160 | + client = self.set_up_client_with_fake() |
161 | + constraints = {"cpu": 2} |
162 | + client.acquire_node(constraints) |
163 | + |
164 | + cpu_count = client.post.params_used.get("cpu_count") |
165 | + self.assertEqual("2", cpu_count) |
166 | + |
167 | + def test_acquire_node_handles_mem_constraint(self): |
168 | + client = self.set_up_client_with_fake() |
169 | + constraints = {"mem": 2048} |
170 | + client.acquire_node(constraints) |
171 | + |
172 | + mem = client.post.params_used.get("mem") |
173 | + self.assertEqual("2048", mem) |
174 | + |
175 | + def test_acquire_node_handles_arbitrary_tag_query(self): |
176 | + client = self.set_up_client_with_fake() |
177 | + constraints = {"maas-tags": "red&!white|blue"} |
178 | + client.acquire_node(constraints) |
179 | + |
180 | + tags = client.post.params_used.get("tags") |
181 | + self.assertEqual("red&!white|blue", tags) |
182 | |
183 | === modified file 'juju/providers/maas/tests/test_provider.py' |
184 | --- juju/providers/maas/tests/test_provider.py 2012-09-27 04:36:51 +0000 |
185 | +++ juju/providers/maas/tests/test_provider.py 2012-09-28 11:52:20 +0000 |
186 | @@ -3,9 +3,9 @@ |
187 | |
188 | """Test cases for juju.providers.maas.provider""" |
189 | |
190 | -from twisted.internet.defer import inlineCallbacks |
191 | +from twisted.internet.defer import inlineCallbacks, succeed |
192 | |
193 | -from juju.errors import MachinesNotFound, ProviderError |
194 | +from juju.errors import ConstraintError, MachinesNotFound, ProviderError |
195 | from juju.lib.mocker import ANY |
196 | from juju.providers.maas import MachineProvider |
197 | from juju.providers.maas.maas import MAASClient |
198 | @@ -141,15 +141,24 @@ |
199 | |
200 | @inlineCallbacks |
201 | def test_constraints(self): |
202 | + self.setup_connection(MAASClient, FakeMAASHTTPConnection) |
203 | provider = MachineProvider("blah", CONFIG) |
204 | cs = yield provider.get_constraint_set() |
205 | self.assertEquals(cs.parse([]), { |
206 | "provider-type": "maas", |
207 | "ubuntu-series": None, |
208 | + "arch": "amd64", |
209 | + "cpu": 1, |
210 | + "mem": 512, |
211 | + "maas-tags": None, |
212 | "maas-name": None}) |
213 | self.assertEquals(cs.parse(["maas-name=totoro"]), { |
214 | "provider-type": "maas", |
215 | "ubuntu-series": None, |
216 | + "arch": "amd64", |
217 | + "cpu": 1, |
218 | + "mem": 512, |
219 | + "maas-tags": None, |
220 | "maas-name": "totoro"}) |
221 | |
222 | bill = cs.parse(["maas-name=bill"]).with_series("precise") |
223 | @@ -164,3 +173,23 @@ |
224 | self.assertFalse(nil.can_satisfy(ben)) |
225 | self.assertFalse(ben.can_satisfy(bill)) |
226 | self.assertFalse(bill.can_satisfy(ben)) |
227 | + |
228 | + @inlineCallbacks |
229 | + def test_constraints_on_tags(self): |
230 | + mock_client = self.mocker.patch(MAASClient(CONFIG)) |
231 | + mock_client.list_tags() |
232 | + self.mocker.result(succeed([ |
233 | + {'name': "furry", 'definition': "HAS fur", 'comment': ""}, |
234 | + {'name': "clawed", 'definition': "HAS claws", 'comment': ""}, |
235 | + ])) |
236 | + self.mocker.replay() |
237 | + provider = MachineProvider("maasiv", CONFIG) |
238 | + provider.maas_client = mock_client |
239 | + cs = yield provider.get_constraint_set() |
240 | + bear = cs.parse(["maas-tags=clawed, furry"]) |
241 | + bear.can_satisfy(bear) |
242 | + err = self.assertRaises(ConstraintError, |
243 | + cs.parse, ["maas-tags=furry, bouncy"]) |
244 | + self.assertEqual("Bad 'maas-tags' constraint 'furry, bouncy': " |
245 | + "tag 'bouncy' does not exist", |
246 | + str(err)) |
247 | |
248 | === modified file 'juju/providers/maas/tests/testing.py' |
249 | --- juju/providers/maas/tests/testing.py 2012-05-25 06:49:32 +0000 |
250 | +++ juju/providers/maas/tests/testing.py 2012-09-28 11:52:20 +0000 |
251 | @@ -114,6 +114,9 @@ |
252 | # List some nodes. |
253 | elif "nodes/?op=list_allocated&id=" in self.url: |
254 | return self.list_some_nodes() |
255 | + # List tags. |
256 | + elif self.url.endswith("/tags/?op=list"): |
257 | + return self.list_tags() |
258 | # Not recognized. |
259 | else: |
260 | raise AssertionError("Unknown API method called") |
261 | @@ -161,6 +164,9 @@ |
262 | def release_node(self): |
263 | return defer.succeed(json.dumps(NODE_JSON[0])) |
264 | |
265 | + def list_tags(self): |
266 | + return defer.succeed("[]") |
267 | + |
268 | |
269 | class FakeMAASHTTPConnectionWithNoAvailableNodes(FakeMAASHTTPConnection): |
270 | """Special version of L{FakeMAASHTTPConnection} that fakes that no nodes |
Reviewers: mp+126203_ code.launchpad. net,
Message:
Please take a look.
Description:
Add generic and tag constraints to maas provider
Passes through standard constraints 'arch', 'cpu', and 'mem', also
specific 'maas-tags' which can be used to indicate other properties.
Launching a new machine still involves calling acquire in the MaaS api
which returns only one node rather than a list of candidates to select
from, so the constraints are still not used on the Juju side for now.
https:/ /code.launchpad .net/~gz/ juju/add_ maas_constraint s/+merge/ 126203
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/6569053/
Affected files: maas/maas. py maas/provider. py maas/tests/ test_launch. py maas/tests/ test_maas. py maas/tests/ test_provider. py
A [revision details]
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
M juju/providers/
Index: [revision details]
=== added file '[revision details]'
--- [revision details] 2012-01-01 00:00:00 +0000
+++ [revision details] 2012-01-01 00:00:00 +0000
@@ -0,0 +1,2 @@
+Old revision: <email address hidden>
+New revision: <email address hidden>
Index: juju/providers/ maas/maas. py /maas/maas. py' maas/maas. py 2012-05-25 06:49:32 +0000 maas/maas. py 2012-09-25 08:58:47 +0000
=== modified file 'juju/providers
--- juju/providers/
+++ juju/providers/
@@ -44,6 +44,14 @@
class MAASClient( MAASOAuthConnec tion):
+ _handled_ constraints = (
"""Initialis e an API client for MAAS.
+ ("maas-name", "name"),
+ ("maas-tags", "tags"),
+ ("arch", "arch"),
+ ("cpu", "cpu_count"),
+ ("mem", "mem"),
+ )
+
def __init__(self, config):
@@ -119,9 +127,10 @@ "maas-name" ] constraints: get(key_ from, None) "api/1. 0/nodes/ ", params)
"""
params = {"op": "acquire"}
if constraints is not None:
- name = constraints[
- if name is not None:
- params["name"] = name
+ for key_from, key_to in self._handled_
+ value = constraints.
+ if value is not None:
+ params[key_to] = str(value)
return self.post(
def start_node(self, resource_uri, user_data):
Index: juju/providers/ maas/provider. py /maas/provider. py' maas/provider. py 2012-03-28 14:40:14 +0000 maas/provider. py 2012-09-25 08:58:47 +0000
=== modified file 'juju/providers
--- juju/providers/
+++ juju/providers/
@@ -46,6 +46,10 @@
visible=False)
+ cs.register_
+ # All the logic for resolving tags lives in MaaS for now so Juju
+ # can just treat the maas-tags constraint as an opaque string.
+ cs.register(
def get_serializati on_data( self):
Index: juju/providers/ maas/tests/ test_launch. py /maas/tests/ test_launch. py' maas/tests/ test_launch. py 2012-05-25 06:32:24 +0000 maas/tests/ test_launch. py 2012-09-25 08:59:22...
=== modified file 'juju/providers
--- juju/providers/
+++ juju/providers/