Merge lp:~gz/pyjuju/add_maas_constraints into lp:pyjuju

Proposed by Martin Packman
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
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+126203@code.launchpad.net

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.

https://codereview.appspot.com/6569053/

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

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_constraints/+merge/126203

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/6569053/

Affected files:
   A [revision details]
   M juju/providers/maas/maas.py
   M juju/providers/maas/provider.py
   M juju/providers/maas/tests/test_launch.py
   M juju/providers/maas/tests/test_maas.py
   M juju/providers/maas/tests/test_provider.py

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
=== modified file 'juju/providers/maas/maas.py'
--- juju/providers/maas/maas.py 2012-05-25 06:49:32 +0000
+++ juju/providers/maas/maas.py 2012-09-25 08:58:47 +0000
@@ -44,6 +44,14 @@

  class MAASClient(MAASOAuthConnection):

+ _handled_constraints = (
+ ("maas-name", "name"),
+ ("maas-tags", "tags"),
+ ("arch", "arch"),
+ ("cpu", "cpu_count"),
+ ("mem", "mem"),
+ )
+
      def __init__(self, config):
          """Initialise an API client for MAAS.

@@ -119,9 +127,10 @@
          """
          params = {"op": "acquire"}
          if constraints is not None:
- name = constraints["maas-name"]
- if name is not None:
- params["name"] = name
+ for key_from, key_to in self._handled_constraints:
+ value = constraints.get(key_from, None)
+ if value is not None:
+ params[key_to] = str(value)
          return self.post("api/1.0/nodes/", params)

      def start_node(self, resource_uri, user_data):

Index: juju/providers/maas/provider.py
=== modified file 'juju/providers/maas/provider.py'
--- juju/providers/maas/provider.py 2012-03-28 14:40:14 +0000
+++ juju/providers/maas/provider.py 2012-09-25 08:58:47 +0000
@@ -46,6 +46,10 @@

          cs.register("ubuntu-series", converter=require_precise,
visible=False)
          cs.register("maas-name")
+ cs.register_generics([])
+ # 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("maas-tags")
          returnValue(cs)

      def get_serialization_data(self):

Index: juju/providers/maas/tests/test_launch.py
=== modified file 'juju/providers/maas/tests/test_launch.py'
--- juju/providers/maas/tests/test_launch.py 2012-05-25 06:32:24 +0000
+++ juju/providers/maas/tests/test_launch.py 2012-09-25 08:59:22...

Read more...

Revision history for this message
John A Meinel (jameinel) wrote :

(Just adding a comment so that I will get any chatter on this proposal)

Revision history for this message
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="maas-tags=foo&bar"

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('maas-tags') you can just pass the a converter function with
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://codereview.appspot.com/6569053/diff/1/juju/providers/maas/maas.py
File juju/providers/maas/maas.py (right):

https://codereview.appspot.com/6569053/diff/1/juju/providers/maas/maas.py#newcode48
juju/providers/maas/maas.py:48: ("maas-name", "name"),
is maas-name still supported?

https://codereview.appspot.com/6569053/

Revision history for this message
Martin Packman (gz) wrote :

> so a user on the cli is going to do something like
>
> juju deploy xyz --constraints="maas-tags=foo&bar"
>
> 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://codereview.appspot.com/6569053/diff/1/juju/providers/maas/maas.py#newc
> ode48
> juju/providers/maas/maas.py:48: ("maas-name", "name"),
> 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.

Revision history for this message
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="maas-tags=foo&bar"
> >
> > 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/transitioning these to error, when some reasonable time period
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://codereview.appspot.com/6569053/diff/1/juju/providers/maas/maas.py#newc
> > ode48
> > juju/providers/maas/maas.py:48: ("maas-name", "name"),
> > 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://code.launchpad.net/~gz/juju/add_maas_constraints/+merge/126203
> You are subscribed to branch lp:juju.
>

Revision history for this message
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/tags?op=list and Maas will give you a list of all tags in the db (it probably gives too much information, but you will get the list of tag names out of that).

I agree that we shouldn't be caching the known list of tags.

Revision history for this message
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/destroy-environment), instead a separate provisioning agent in the environment does.

>
> If so, you can query api/1.0/tags?op=list and Maas will give you a list of all
> 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.

lp:~gz/pyjuju/add_maas_constraints updated
581. By Martin Packman

Revert to checking tag constraint directly, fetch list of valid tags from maas

Revision history for this message
Martin Packman (gz) wrote :
lp:~gz/pyjuju/add_maas_constraints updated
582. By Martin Packman

Merge trunk to resolve conflicts with removal limitation to precise

Revision history for this message
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.

https://codereview.appspot.com/6569053/

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

s/creating services/creating service units.

Revision history for this message
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!

https://codereview.appspot.com/6569053/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches

to status/vote changes: