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
=== modified file 'juju/providers/maas/maas.py'
--- juju/providers/maas/maas.py 2012-09-20 15:11:08 +0000
+++ juju/providers/maas/maas.py 2012-09-28 11:52:20 +0000
@@ -44,6 +44,14 @@
4444
45class MAASClient(MAASOAuthConnection):45class MAASClient(MAASOAuthConnection):
4646
47 _handled_constraints = (
48 ("maas-name", "name"),
49 ("maas-tags", "tags"),
50 ("arch", "arch"),
51 ("cpu", "cpu_count"),
52 ("mem", "mem"),
53 )
54
47 def __init__(self, config):55 def __init__(self, config):
48 """Initialise an API client for MAAS.56 """Initialise an API client for MAAS.
4957
@@ -119,9 +127,10 @@
119 """127 """
120 params = {"op": "acquire"}128 params = {"op": "acquire"}
121 if constraints is not None:129 if constraints is not None:
122 name = constraints["maas-name"]130 for key_from, key_to in self._handled_constraints:
123 if name is not None:131 value = constraints.get(key_from, None)
124 params["name"] = name132 if value is not None:
133 params[key_to] = str(value)
125 return self.post("api/1.0/nodes/", params)134 return self.post("api/1.0/nodes/", params)
126135
127 def start_node(self, resource_uri, ubuntu_series, user_data):136 def start_node(self, resource_uri, ubuntu_series, user_data):
@@ -157,3 +166,11 @@
157 """166 """
158 params = {"op": "release"}167 params = {"op": "release"}
159 return self.post(resource_uri, params)168 return self.post(resource_uri, params)
169
170 def list_tags(self):
171 """Ask MAAS to return a list of all the tags defined.
172
173 :return: A Deferred whose value is the list of tags.
174 """
175 params = {"op": "list"}
176 return self.get("api/1.0/tags/", params)
160177
=== modified file 'juju/providers/maas/provider.py'
--- juju/providers/maas/provider.py 2012-09-14 20:02:17 +0000
+++ juju/providers/maas/provider.py 2012-09-28 11:52:20 +0000
@@ -17,6 +17,36 @@
17log = logging.getLogger("juju.maas")17log = logging.getLogger("juju.maas")
1818
1919
20class _TagHandler(object):
21 """Parser and validator for tags constraint expressions
22
23 Tag names are extracted and checked against the list of known tags
24 reported by the api.
25
26 Currently tag constraints consist of just comma and/or whitespace tag
27 names, all of are required for a match. Extending this to support full
28 boolean expressions would be possible, and some forward compatibility
29 is attempted.
30 """
31
32 def __init__(self, tags_info):
33 self.tag_names = [tag['name'] for tag in tags_info]
34
35 compare = staticmethod(set.issuperset)
36
37 def convert(self, tag_expression):
38 """Extract set of names in tag_expression checking they all exist"""
39 tags = set()
40 for c in (",", "&", "|", "!"):
41 tag_expression = tag_expression.replace(c, " ")
42 for word in tag_expression.strip().split():
43 tag = word.lower()
44 if tag not in self.tag_names:
45 raise ValueError("tag %r does not exist" % (tag,))
46 tags.add(tag)
47 return tags
48
49
20class MachineProvider(MachineProviderBase):50class MachineProvider(MachineProviderBase):
21 """MachineProvider for use in a MAAS environment"""51 """MachineProvider for use in a MAAS environment"""
2252
@@ -40,6 +70,15 @@
4070
41 cs.register("ubuntu-series", visible=False)71 cs.register("ubuntu-series", visible=False)
42 cs.register("maas-name")72 cs.register("maas-name")
73 cs.register_generics([])
74 # MaaS client errors on the provisioning agent are not reported by the
75 # juju cli so try validating tags when the constraint is created.
76 # Because new tags may be registered at any point, caching the list of
77 # tags is not safe and they must be refetched every time.
78 tags_info = yield self.maas_client.list_tags()
79 handler = _TagHandler(tags_info)
80 cs.register("maas-tags", converter=handler.convert,
81 comparer=handler.compare)
43 returnValue(cs)82 returnValue(cs)
4483
45 def get_serialization_data(self):84 def get_serialization_data(self):
4685
=== modified file 'juju/providers/maas/tests/test_launch.py'
--- juju/providers/maas/tests/test_launch.py 2012-09-27 04:36:51 +0000
+++ juju/providers/maas/tests/test_launch.py 2012-09-28 11:52:20 +0000
@@ -71,8 +71,7 @@
71 provider = self._get_provider(71 provider = self._get_provider(
72 FakeMAASHTTPConnectionWithNoAvailableNodes)72 FakeMAASHTTPConnectionWithNoAvailableNodes)
73 machine_data = {73 machine_data = {
74 "machine-id": "foo", "constraints": {"maas-name": None,74 "machine-id": "foo", "constraints": {"ubuntu-series": "splendid"}}
75 "ubuntu-series": "splendid"}}
76 d = provider.start_machine(machine_data)75 d = provider.start_machine(machine_data)
7776
78 # These arbitrary fake failure values come from77 # These arbitrary fake failure values come from
@@ -94,8 +93,7 @@
94 # Try to start up that node using the fake MAAS.93 # Try to start up that node using the fake MAAS.
95 provider = self._get_provider(FakeMAASHTTPConnection)94 provider = self._get_provider(FakeMAASHTTPConnection)
96 machine_data = {95 machine_data = {
97 "machine-id": machine_id, "constraints": {"maas-name": None,96 "machine-id": "foo", "constraints": {"ubuntu-series": "splendid"}}
98 "ubuntu-series": "splendid"}}
99 machine_list = yield provider.start_machine(machine_data)97 machine_list = yield provider.start_machine(machine_data)
10098
101 # Test that it returns a list containing a single MAASMachine99 # Test that it returns a list containing a single MAASMachine
@@ -148,7 +146,7 @@
148 # The following operations happen in sequence.146 # The following operations happen in sequence.
149 mocker.order()147 mocker.order()
150 # First, the node is acquired.148 # First, the node is acquired.
151 mock_client.acquire_node({"maas-name": None, "ubuntu-series": "precise"})149 mock_client.acquire_node({"ubuntu-series": "precise"})
152 mocker.result({"resource_uri": "/node/123"})150 mocker.result({"resource_uri": "/node/123"})
153 # Second, the node is started. We simulate a failure at this stage.151 # Second, the node is started. We simulate a failure at this stage.
154 mock_client.start_node("/node/123", "precise", ANY)152 mock_client.start_node("/node/123", "precise", ANY)
@@ -160,5 +158,5 @@
160 mocker.replay()158 mocker.replay()
161 return self.assertFailure(159 return self.assertFailure(
162 MAASLaunchMachine(mock_provider,160 MAASLaunchMachine(mock_provider,
163 {"maas-name": None, "ubuntu-series": "precise"}).run("fred"),161 {"ubuntu-series": "precise"}).run("fred"),
164 ZeroDivisionError)162 ZeroDivisionError)
165163
=== modified file 'juju/providers/maas/tests/test_maas.py'
--- juju/providers/maas/tests/test_maas.py 2012-09-14 20:02:17 +0000
+++ juju/providers/maas/tests/test_maas.py 2012-09-28 11:52:20 +0000
@@ -337,3 +337,35 @@
337 self.assertIs(None, guinness)337 self.assertIs(None, guinness)
338 name = client.post.params_used.get("name")338 name = client.post.params_used.get("name")
339 self.assertEqual("zaphod", name)339 self.assertEqual("zaphod", name)
340
341 def test_acquire_node_handles_arch_constraint(self):
342 client = self.set_up_client_with_fake()
343 constraints = {"arch": "i386"}
344 client.acquire_node(constraints)
345
346 arch = client.post.params_used.get("arch")
347 self.assertEqual("i386", arch)
348
349 def test_acquire_node_handles_cpu_constraint(self):
350 client = self.set_up_client_with_fake()
351 constraints = {"cpu": 2}
352 client.acquire_node(constraints)
353
354 cpu_count = client.post.params_used.get("cpu_count")
355 self.assertEqual("2", cpu_count)
356
357 def test_acquire_node_handles_mem_constraint(self):
358 client = self.set_up_client_with_fake()
359 constraints = {"mem": 2048}
360 client.acquire_node(constraints)
361
362 mem = client.post.params_used.get("mem")
363 self.assertEqual("2048", mem)
364
365 def test_acquire_node_handles_arbitrary_tag_query(self):
366 client = self.set_up_client_with_fake()
367 constraints = {"maas-tags": "red&!white|blue"}
368 client.acquire_node(constraints)
369
370 tags = client.post.params_used.get("tags")
371 self.assertEqual("red&!white|blue", tags)
340372
=== modified file 'juju/providers/maas/tests/test_provider.py'
--- juju/providers/maas/tests/test_provider.py 2012-09-27 04:36:51 +0000
+++ juju/providers/maas/tests/test_provider.py 2012-09-28 11:52:20 +0000
@@ -3,9 +3,9 @@
33
4"""Test cases for juju.providers.maas.provider"""4"""Test cases for juju.providers.maas.provider"""
55
6from twisted.internet.defer import inlineCallbacks6from twisted.internet.defer import inlineCallbacks, succeed
77
8from juju.errors import MachinesNotFound, ProviderError8from juju.errors import ConstraintError, MachinesNotFound, ProviderError
9from juju.lib.mocker import ANY9from juju.lib.mocker import ANY
10from juju.providers.maas import MachineProvider10from juju.providers.maas import MachineProvider
11from juju.providers.maas.maas import MAASClient11from juju.providers.maas.maas import MAASClient
@@ -141,15 +141,24 @@
141141
142 @inlineCallbacks142 @inlineCallbacks
143 def test_constraints(self):143 def test_constraints(self):
144 self.setup_connection(MAASClient, FakeMAASHTTPConnection)
144 provider = MachineProvider("blah", CONFIG)145 provider = MachineProvider("blah", CONFIG)
145 cs = yield provider.get_constraint_set()146 cs = yield provider.get_constraint_set()
146 self.assertEquals(cs.parse([]), {147 self.assertEquals(cs.parse([]), {
147 "provider-type": "maas",148 "provider-type": "maas",
148 "ubuntu-series": None,149 "ubuntu-series": None,
150 "arch": "amd64",
151 "cpu": 1,
152 "mem": 512,
153 "maas-tags": None,
149 "maas-name": None})154 "maas-name": None})
150 self.assertEquals(cs.parse(["maas-name=totoro"]), {155 self.assertEquals(cs.parse(["maas-name=totoro"]), {
151 "provider-type": "maas",156 "provider-type": "maas",
152 "ubuntu-series": None,157 "ubuntu-series": None,
158 "arch": "amd64",
159 "cpu": 1,
160 "mem": 512,
161 "maas-tags": None,
153 "maas-name": "totoro"})162 "maas-name": "totoro"})
154163
155 bill = cs.parse(["maas-name=bill"]).with_series("precise")164 bill = cs.parse(["maas-name=bill"]).with_series("precise")
@@ -164,3 +173,23 @@
164 self.assertFalse(nil.can_satisfy(ben))173 self.assertFalse(nil.can_satisfy(ben))
165 self.assertFalse(ben.can_satisfy(bill))174 self.assertFalse(ben.can_satisfy(bill))
166 self.assertFalse(bill.can_satisfy(ben))175 self.assertFalse(bill.can_satisfy(ben))
176
177 @inlineCallbacks
178 def test_constraints_on_tags(self):
179 mock_client = self.mocker.patch(MAASClient(CONFIG))
180 mock_client.list_tags()
181 self.mocker.result(succeed([
182 {'name': "furry", 'definition': "HAS fur", 'comment': ""},
183 {'name': "clawed", 'definition': "HAS claws", 'comment': ""},
184 ]))
185 self.mocker.replay()
186 provider = MachineProvider("maasiv", CONFIG)
187 provider.maas_client = mock_client
188 cs = yield provider.get_constraint_set()
189 bear = cs.parse(["maas-tags=clawed, furry"])
190 bear.can_satisfy(bear)
191 err = self.assertRaises(ConstraintError,
192 cs.parse, ["maas-tags=furry, bouncy"])
193 self.assertEqual("Bad 'maas-tags' constraint 'furry, bouncy': "
194 "tag 'bouncy' does not exist",
195 str(err))
167196
=== modified file 'juju/providers/maas/tests/testing.py'
--- juju/providers/maas/tests/testing.py 2012-05-25 06:49:32 +0000
+++ juju/providers/maas/tests/testing.py 2012-09-28 11:52:20 +0000
@@ -114,6 +114,9 @@
114 # List some nodes.114 # List some nodes.
115 elif "nodes/?op=list_allocated&id=" in self.url:115 elif "nodes/?op=list_allocated&id=" in self.url:
116 return self.list_some_nodes()116 return self.list_some_nodes()
117 # List tags.
118 elif self.url.endswith("/tags/?op=list"):
119 return self.list_tags()
117 # Not recognized.120 # Not recognized.
118 else:121 else:
119 raise AssertionError("Unknown API method called")122 raise AssertionError("Unknown API method called")
@@ -161,6 +164,9 @@
161 def release_node(self):164 def release_node(self):
162 return defer.succeed(json.dumps(NODE_JSON[0]))165 return defer.succeed(json.dumps(NODE_JSON[0]))
163166
167 def list_tags(self):
168 return defer.succeed("[]")
169
164170
165class FakeMAASHTTPConnectionWithNoAvailableNodes(FakeMAASHTTPConnection):171class FakeMAASHTTPConnectionWithNoAvailableNodes(FakeMAASHTTPConnection):
166 """Special version of L{FakeMAASHTTPConnection} that fakes that no nodes172 """Special version of L{FakeMAASHTTPConnection} that fakes that no nodes

Subscribers

People subscribed via source and target branches

to status/vote changes: