Merge lp:~makyo/juju-deployer/machines-and-placement into lp:juju-deployer

Proposed by Madison Scott-Clary
Status: Merged
Merged at revision: 144
Proposed branch: lp:~makyo/juju-deployer/machines-and-placement
Merge into: lp:juju-deployer
Diff against target: 1612 lines (+1109/-73)
24 files modified
README (+5/-1)
deployer/action/importer.py (+74/-4)
deployer/config.py (+5/-3)
deployer/deployment.py (+48/-6)
deployer/env/base.py (+1/-1)
deployer/env/go.py (+27/-3)
deployer/service.py (+249/-33)
deployer/tests/base.py (+8/-2)
deployer/tests/test_config.py (+2/-2)
deployer/tests/test_data/v4/container-new.yaml (+43/-0)
deployer/tests/test_data/v4/container.yaml (+43/-0)
deployer/tests/test_data/v4/fill_placement.yaml (+17/-0)
deployer/tests/test_data/v4/hulk-smash-nounits-nomachines.yaml (+38/-0)
deployer/tests/test_data/v4/hulk-smash-nounits.yaml (+43/-0)
deployer/tests/test_data/v4/hulk-smash.yaml (+43/-0)
deployer/tests/test_data/v4/placement-invalid-subordinate.yaml (+13/-0)
deployer/tests/test_data/v4/placement.yaml (+45/-0)
deployer/tests/test_data/v4/series.yaml (+21/-0)
deployer/tests/test_data/v4/simple.yaml (+1/-0)
deployer/tests/test_data/v4/validate.yaml (+16/-0)
deployer/tests/test_deployment.py (+302/-14)
deployer/tests/test_goenv.py (+27/-4)
deployer/tests/test_importer.py (+19/-0)
deployer/utils.py (+19/-0)
To merge this branch: bzr merge lp:~makyo/juju-deployer/machines-and-placement
Reviewer Review Type Date Requested Status
David Britton (community) Approve
🤖 Landscape Builder (community) Approve
Review via email: mp+251857@code.launchpad.net

Description of the change

Add v4 support for machine spec per the bundles spec.

QA:
Ensure that each of the bundles in deployer/tests/test_data/v4/ deploy via:
  PYTHONPATH=. python deployer/cli.py -c deployer/tests/test_data/v4/<bundle>.yaml

All tests should pass.

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

QA instructions with PYTHONPATH won't work if juju-deployer has previously been pip installed.

Still doing QA.

Revision history for this message
Francesco Banconi (frankban) wrote :

Hi Madison, this is an impressive branch.
Thank you for sorting out the intricacies of the new units placement logic.
Did not review the tests yet, but I have some suggestions and questions below: please let me know what do you think.
I am not sure about repeated placement parsing logic across the modules in this branch (see below).
I'll take another and deeper look after your replies.
Thank you!

Revision history for this message
David Britton (dpb) wrote :

Hi -- Other than examining the code, where can I see differences between the v4 and v3 spec? Could a README change be introduced with perhaps an example v4 file with annotated differences? Also, rather than duplicating the entire v3 class, could things be refactored to be shared, and then tested together? Like a common set of validations, and then just the v4 additions (same for 'get()').

I have a number of comments inline, mostly around test coverage. I found one coding error just by inspection, making me a bit worried about the lack of unit tests for the change. Relying on just the more functional style of tests (of parsing a complete bundle) for covering this new code seems like a risky approach.

Some of the more complex functions (ex: validate()) could also use from refactoring and breaking apart as they are multiple pages in length (with clear duplication in the v4 addition).

review: Needs Fixing
Revision history for this message
Madison Scott-Clary (makyo) wrote :

Checkpoint for pairing.

Revision history for this message
Brad Crittenden (bac) wrote :

Looks good Madison with a few comments.

Revision history for this message
Madison Scott-Clary (makyo) wrote :

For live environment testing, assuming an LXC env named local,

TEST_ENDPOINT=localhost JUJU_ENV=local nosetests --verbosity=2 deployer.tests.test_goenv:LiveEnvironmentTest

Revision history for this message
Madison Scott-Clary (makyo) wrote :

> For live environment testing, assuming an LXC env named local,
>
> TEST_ENDPOINT=localhost JUJU_ENV=local nosetests --verbosity=2
> deployer.tests.test_goenv:LiveEnvironmentTest

Also, ensure that that happens on a single line. LP broke it up

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

What happens when i use a v4 spec with multiple machines, remove a unit that was placed to one of those machines, and then try to run it again? the code while it looks nice seems to have some gaps around consideration of running it multiple times, which sort of defines an ideal production usage (run again to apply delta from vcs).

Revision history for this message
Francesco Banconi (frankban) wrote :

Thanks for the fixes Madison, nicely done!
Also thanks for improving the pre-existing code docstrings and test coverage.
I have some minor questions and suggestions, please see below.

In the case Kapil described, the current behavior seems to be that we get the last unit placements.
In theory, we should really diff and retrieve the missing placement.
For the time being, a safe tradeoff could be to always create new machines for missing units if cur_units > 0.

Revision history for this message
Madison Scott-Clary (makyo) wrote :

> What happens when i use a v4 spec with multiple machines, remove a unit that
> was placed to one of those machines, and then try to run it again? the code
> while it looks nice seems to have some gaps around consideration of running it
> multiple times, which sort of defines an ideal production usage (run again to
> apply delta from vcs).

We talked about this with the team this morning (and there was additional discussion online last night), noting that this is something that the new bundle specification does not currently have in place. As part of this branch, I will change the logic that is used to to decide what a v4 bundle is (that is, only one with a machine spec), and all other bundles will fall back to the v3 format, which can be run multiple times.

Revision history for this message
Madison Scott-Clary (makyo) :
Revision history for this message
David Britton (dpb) wrote :

Thanks Makyo -- I have added a number of diff comments. Getting much better, thanks for the improvements and additional tests, though it could still use more. I will keep this as needs fixing, and I'll also test it out with a couple v3 bundles live while I wait to hear your response.

I'd still also like to see an addtion to the README pointing to or explaining the v4 spec, and an annotated file that highlights differences.

Thanks!

review: Needs Fixing
Revision history for this message
Madison Scott-Clary (makyo) wrote :

Committing most fixes now; tests will be in the next commit.

153. By Madison Scott-Clary

Merge trunk; update tests for subordinate placements.

154. By Madison Scott-Clary

Add invalid subordinate placement v4 bundle.

155. By Madison Scott-Clary

Add series to machine spec.

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Thanks, test coverage much better, follow-on branch will address concerns about attempts being idempotent. Deployed both v3 and v4 bundles with placement.

review: Approve
Revision history for this message
David Britton (dpb) wrote :

Thanks, test coverage much better, follow-on branch will address concerns about attempts being idempotent. Deployed both v3 and v4 bundles with placement.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2013-07-24 12:46:56 +0000
3+++ README 2015-03-24 17:39:12 +0000
4@@ -21,7 +21,7 @@
5 Stack Definitions
6 -----------------
7
8-High level view::
9+High level view of v3 stacks::
10
11 blog:
12 series: precise
13@@ -57,6 +57,10 @@
14 We've got two deployment stacks here, blog, and blog-prod. The blog stack defines
15 a simple wordpress deploy with mysql and two relations. In this case its
16
17+Version 4 bundles are currently under development. The development document for
18+these types of bundles is available `here
19+<https://docs.google.com/a/canonical.com/document/d/1SF8hTBi6oVbki8V__beNij6wnQU-5cm6PZsy5gf0j_Y>`_.
20+
21
22 Development
23 -----------
24
25=== modified file 'deployer/action/importer.py'
26--- deployer/action/importer.py 2014-10-01 10:18:36 +0000
27+++ deployer/action/importer.py 2015-03-24 17:39:12 +0000
28@@ -15,6 +15,7 @@
29 self.options = options
30 self.env = env
31 self.deployment = deployment
32+ self.machines = {}
33
34 def add_units(self):
35 self.log.debug("Adding units...")
36@@ -55,6 +56,50 @@
37 else:
38 self.env.add_units(svc.name, abs(delta))
39
40+ def create_machines(self):
41+ """Create machines as specified in the machine spec in the bundle.
42+
43+ A machine spec consists of a named machine (the name is, by convention,
44+ an integer) with an optional series, optional constraints and optional
45+ annotations:
46+
47+ 0:
48+ series: "precise"
49+ constraints: "mem=4G arch=i386"
50+ annotations:
51+ foo: bar
52+ 1:
53+ constraints: "mem=4G"
54+
55+ This method first attempts to create any machines in the 'machines'
56+ section of the bundle specification with the given constraints and
57+ annotations. Then, if there are any machines requested for containers
58+ in the style of <container type>:new, it creates those machines and
59+ adds them to the machines map."""
60+ machines = self.deployment.get_machines()
61+ if machines:
62+ self.log.info("Creating machines...")
63+ for machine_name, spec in machines.items():
64+ self.machines[machine_name] = self.env.add_machine(
65+ series=spec.get('series',
66+ self.deployment.data['series']),
67+ constraints=spec.get('constraints'))
68+ annotations = spec.get('annotations')
69+ if annotations:
70+ self.env.set_annotation(
71+ self.machines[machine_name],
72+ annotations,
73+ entity_type='machine')
74+ # In the case of <container type>:new, we need to create a machine
75+ # before creating the container on which the service will be
76+ # deployed. This is stored in the machines map which will be used
77+ # in the service placement.
78+ for service in self.deployment.get_services():
79+ placement = self.deployment.get_unit_placement(service, None)
80+ for container_host in placement.get_new_machines_for_containers():
81+ self.machines[container_host] = self.env.add_machine()
82+ self.deployment.set_machines(machines)
83+
84 def get_charms(self):
85 # Get Charms
86 self.log.debug("Getting charms...")
87@@ -66,7 +111,14 @@
88 # validate them.
89 self.deployment.resolve(self.options.overrides or ())
90
91- def deploy_services(self):
92+ def deploy_services(self, add_units=True):
93+ """Deploy the services specified in the deployment.
94+
95+ add_units: whether or not to add units to the service as it is
96+ deployed; newer versions of bundles may have machines specified
97+ in a machine spec, and units will be placed accordingly if this
98+ flag is false.
99+ """
100 self.log.info("Deploying services...")
101 self.log.debug(self.env)
102 env_status = self.env.status()
103@@ -84,17 +136,31 @@
104
105 if svc.unit_placement:
106 # We sorted all the non placed services first, so we only
107- # need to update status once after we're done with them.
108+ # need to update status once after we're done with them, in
109+ # the instance of v3 bundles; in the more complex case of v4
110+ # bundles, we'll need to refresh each time.
111 if not reloaded:
112 self.log.debug(
113 " Refetching status for placement deploys")
114 time.sleep(5.1)
115 env_status = self.env.status()
116- reloaded = True
117+ # In the instance of version 3 deployments, we will not
118+ # need to fetch the status more than once. In version 4
119+ # bundles, however, we will need to fetch the status each
120+ # time in order to allow for the machine specification to
121+ # be taken into account.
122+ if self.deployment.version == 3:
123+ reloaded = True
124 num_units = 1
125 else:
126 num_units = svc.num_units
127
128+ # Unset num_units if we are not adding units. This should be done
129+ # after the above work to ensure that the status is still retrieved
130+ # as necessary.
131+ if not add_units:
132+ num_units = None
133+
134 placement = self.deployment.get_unit_placement(svc, env_status)
135
136 if charm.is_subordinate():
137@@ -193,7 +259,11 @@
138 self.env.bootstrap()
139 self.env.connect()
140
141- self.deploy_services()
142+ if self.deployment.version > 3:
143+ self.create_machines()
144+
145+ # We can shortcut and add the units during deployment for v3 bundles.
146+ self.deploy_services(add_units=(self.deployment.version == 3))
147
148 # Workaround api issue in juju-core, where any action takes 5s
149 # to be consistent to subsequent watch api interactions, see
150
151=== modified file 'deployer/config.py'
152--- deployer/config.py 2015-02-24 15:49:28 +0000
153+++ deployer/config.py 2015-03-24 17:39:12 +0000
154@@ -52,7 +52,8 @@
155
156 # Check if this is a v4 bundle.
157 if 'services' in yaml_result:
158- self.version = 4
159+ if 'machines' in yaml_result:
160+ self.version = 4
161 yaml_result = {config_file: yaml_result}
162
163 self.yaml[config_file] = yaml_result
164@@ -68,13 +69,14 @@
165 key, ", ".join(self.keys()))
166 raise ErrorExit()
167 deploy_data = self.data[key]
168- if self.version != 4:
169+ if self.version < 4:
170 deploy_data = self._resolve_inherited(deploy_data)
171 if self.cli_series:
172 deploy_data['series'] = self.cli_series
173 return Deployment(
174 key, deploy_data, self.include_dirs,
175- repo_path=os.environ.get("JUJU_REPOSITORY", ""))
176+ repo_path=os.environ.get("JUJU_REPOSITORY", ""),
177+ version=self.version)
178
179 def load(self):
180 data = {}
181
182=== modified file 'deployer/deployment.py'
183--- deployer/deployment.py 2015-02-06 21:43:24 +0000
184+++ deployer/deployment.py 2015-03-24 17:39:12 +0000
185@@ -7,20 +7,22 @@
186
187 from .charm import Charm
188 from .feedback import Feedback
189-from .service import Service, ServiceUnitPlacement
190+from .service import Service, ServiceUnitPlacementV3, ServiceUnitPlacementV4
191 from .relation import Endpoint
192-from .utils import path_join, yaml_dump, ErrorExit, resolve_include
193+from .utils import path_join, yaml_dump, ErrorExit, resolve_include, x_in_y
194
195
196 class Deployment(object):
197
198 log = logging.getLogger("deployer.deploy")
199
200- def __init__(self, name, data, include_dirs, repo_path=""):
201+ def __init__(self, name, data, include_dirs, repo_path="", version=3):
202 self.name = name
203 self.data = data
204 self.include_dirs = include_dirs
205 self.repo_path = repo_path
206+ self.version = version
207+ self.machines = {}
208
209 @property
210 def series(self):
211@@ -44,17 +46,53 @@
212 services = []
213 for name, svc_data in self.data.get('services', {}).items():
214 services.append(Service(name, svc_data))
215- services.sort(self._placement_sort)
216+ if self.version == 3:
217+ # Sort unplaced units first, then sort by name for placed units.
218+ services.sort(key=lambda svc: (bool(svc.unit_placement), svc.name))
219+ else:
220+ services.sort(self._machines_placement_sort)
221 return services
222
223+ def set_machines(self, machines):
224+ """Set a dict of machines, mapping from the names in the machine spec
225+ to the machine names in the environment status.
226+ """
227+ self.machines = machines
228+
229+ def get_machines(self):
230+ """Return a dict mapping machine names to machine options.
231+
232+ An empty dict is returned if no machines are defined in the
233+ bundle YAML.
234+ """
235+ machines = {}
236+ for key, machine in self.data.get('machines', {}).items():
237+ machines[str(key)] = machine
238+ return machines
239+
240 def get_service_names(self):
241 """Return a sequence of service names for this deployment."""
242 return self.data.get('services', {}).keys()
243
244 @staticmethod
245- def _placement_sort(svc_a, svc_b):
246+ def _machines_placement_sort(svc_a, svc_b):
247+ """Sort machines with machine placement in mind.
248+
249+ If svc_a is colocated alongside svc_b, svc_b needs to be deployed
250+ first, so that it exists by the time svc_a is deployed, and vice
251+ versa; this sorts first based on this fact, then secondly based on
252+ whether or not the service has a unit placement, and then finally
253+ based on the name of the service.
254+ """
255 if svc_a.unit_placement:
256 if svc_b.unit_placement:
257+ # Check for colocation. This naively assumes that there is no
258+ # circularity in placements.
259+ if x_in_y(svc_b, svc_a):
260+ return -1
261+ if x_in_y(svc_a, svc_b):
262+ return 1
263+ # If no colocation exists, simply compare names.
264 return cmp(svc_a.name, svc_b.name)
265 return 1
266 if svc_b.unit_placement:
267@@ -64,7 +102,11 @@
268 def get_unit_placement(self, svc, status):
269 if isinstance(svc, (str, unicode)):
270 svc = self.get_service(svc)
271- return ServiceUnitPlacement(svc, self, status)
272+ if self.version == 3:
273+ return ServiceUnitPlacementV3(svc, self, status)
274+ else:
275+ return ServiceUnitPlacementV4(svc, self, status,
276+ machines_map=self.machines)
277
278 def get_relations(self):
279 if 'relations' not in self.data:
280
281=== modified file 'deployer/env/base.py'
282--- deployer/env/base.py 2015-03-12 21:37:23 +0000
283+++ deployer/env/base.py 2015-03-24 17:39:12 +0000
284@@ -158,5 +158,5 @@
285 def add_unit(self, service_name, machine_spec):
286 raise NotImplementedError()
287
288- def set_annotation(self, entity, annotations):
289+ def set_annotation(self, entity, annotations, entity_type='service'):
290 raise NotImplementedError()
291
292=== modified file 'deployer/env/go.py'
293--- deployer/env/go.py 2014-09-16 17:04:46 +0000
294+++ deployer/env/go.py 2015-03-24 17:39:12 +0000
295@@ -1,7 +1,10 @@
296 import time
297
298 from .base import BaseEnvironment
299-from ..utils import ErrorExit
300+from ..utils import (
301+ ErrorExit,
302+ parse_constraints,
303+)
304
305 from jujuclient import (
306 EnvError,
307@@ -24,6 +27,20 @@
308 self.api_endpoint = endpoint
309 self.client = None
310
311+ def add_machine(self, series="",constraints={}):
312+ """Add a top level machine to the Juju environment.
313+
314+ Use the given series and constraints.
315+ Return the machine identifier (e.g. "1").
316+
317+ series: a string such as 'precise' or 'trusty'.
318+ constraints: a map of constraints (such as mem, arch, etc.) which
319+ can be parsed by utils.parse_constraints
320+ """
321+ return self.client.add_machine(
322+ series=series,
323+ constraints=parse_constraints(constraints))['Machine']
324+
325 def add_unit(self, service_name, machine_spec):
326 return self.client.add_unit(service_name, machine_spec)
327
328@@ -196,8 +213,15 @@
329 else:
330 return
331
332- def set_annotation(self, svc_name, annotation):
333- return self.client.set_annotation(svc_name, 'service', annotation)
334+ def set_annotation(self, entity_name, annotation, entity_type='service'):
335+ """Set an annotation on an entity.
336+
337+ entity_name: the name of the entity (machine, service, etc.) to
338+ annotate.
339+ annotation: a dict of key/value pairs to set on the entity.
340+ entity_type: the type of entity (machine, service, etc.) to annotate.
341+ """
342+ return self.client.set_annotation(entity_name, entity_type, annotation)
343
344 def status(self):
345 return self.client.get_stat()
346
347=== modified file 'deployer/service.py'
348--- deployer/service.py 2015-03-13 17:41:18 +0000
349+++ deployer/service.py 2015-03-24 17:39:12 +0000
350@@ -1,3 +1,5 @@
351+import itertools
352+
353 from feedback import Feedback
354
355
356@@ -63,6 +65,57 @@
357 else:
358 return machine
359
360+ def colocate(self, status, placement, u_idx, container, svc):
361+ """Colocate one service with an existing service either within a
362+ container alongside that service or hulk-smashed onto the same unit.
363+
364+ status: the environment status.
365+ placement: the placement directive of the unit to be colocated.
366+ u_idx: the unit index of the unit to be colocated.
367+ container: a string containing the container type, or None.
368+ svc: the service object for this placement.
369+ """
370+ with_service = status['services'].get(placement)
371+ if with_service is None:
372+ # Should be caught in validate relations but sanity check
373+ # for concurrency.
374+ self.deployment.log.error(
375+ "Service %s to be deployed with non-existent service %s",
376+ svc.name, placement)
377+ # Prefer continuing deployment with a new machine rather
378+ # than an in-progress abort.
379+ return None
380+
381+ svc_units = with_service['units']
382+ if int(u_idx) >= len(svc_units):
383+ self.deployment.log.warning(
384+ "Service:%s, Deploy-with-service:%s, Requested-unit-index=%s, "
385+ "Cannot solve, falling back to default placement",
386+ svc.name, placement, u_idx)
387+ return None
388+ unit_names = svc_units.keys()
389+ unit_names.sort()
390+ machine = svc_units[unit_names[int(u_idx)]].get('machine')
391+ if not machine:
392+ self.deployment.log.warning(
393+ "Service:%s deploy-with unit missing machine %s",
394+ svc.name, unit_names[int(u_idx)])
395+ return None
396+ return self._format_placement(machine, container)
397+
398+
399+class ServiceUnitPlacementV3(ServiceUnitPlacement):
400+
401+ def _parse_placement(self, unit_placement):
402+ placement = unit_placement
403+ container = None
404+ u_idx = None
405+ if ':' in unit_placement:
406+ container, placement = unit_placement.split(":")
407+ if '=' in placement:
408+ placement, u_idx = placement.split("=")
409+ return container, placement, u_idx
410+
411 def validate(self):
412 feedback = Feedback()
413
414@@ -77,14 +130,13 @@
415 services = dict([(s.name, s) for s in self.deployment.get_services()])
416
417 for idx, p in enumerate(unit_placement):
418- if ':' in p:
419- container, p = p.split(':')
420+ container, p, u_idx = self._parse_placement(p)
421+ if container:
422 if container not in ('lxc', 'kvm'):
423 feedback.error(
424- "Invalid service:%s placement: %s" % (
425- self.service.name, unit_placement[idx]))
426- if '=' in p:
427- p, u_idx = p.split("=")
428+ "Invalid container type:%s service: %s placement: %s" \
429+ % (container, self.service.name, unit_placement[idx]))
430+ if u_idx:
431 if p in ('maas', 'zone'):
432 continue
433 if not u_idx.isdigit():
434@@ -117,6 +169,10 @@
435 return feedback
436
437 def get(self, unit_number):
438+ """Get the placement directive for a given unit.
439+
440+ unit_number: the number of the unit to deploy
441+ """
442 status = self.status
443 svc = self.service
444
445@@ -143,30 +199,190 @@
446 elif placement == "zone":
447 return "zone=%s" % u_idx
448
449- with_service = status['services'].get(placement)
450- if with_service is None:
451- # Should be caught in validate relations but sanity check
452- # for concurrency.
453- self.deployment.log.error(
454- "Service %s to be deployed with non existant service %s",
455- svc.name, placement)
456- # Prefer continuing deployment with a new machine rather
457- # than an in-progress abort.
458- return None
459-
460- svc_units = with_service['units']
461- if int(u_idx) >= len(svc_units):
462- self.deployment.log.warning(
463- "Service:%s, Deploy-with-service:%s, Requested-unit-index=%s, "
464- "Cannot solve, falling back to default placement",
465- svc.name, placement, u_idx)
466- return None
467- unit_names = svc_units.keys()
468- unit_names.sort()
469- machine = svc_units[unit_names[int(u_idx)]].get('machine')
470- if not machine:
471- self.deployment.log.warning(
472- "Service:%s deploy-with unit missing machine %s",
473- svc.name, unit_names[unit_number])
474- return None
475- return self._format_placement(machine, container)
476+ return self.colocate(status, placement, u_idx, container, svc)
477+
478+
479+class ServiceUnitPlacementV4(ServiceUnitPlacement):
480+
481+ def __init__(self, service, deployment, status, arbitrary_machines=False,
482+ machines_map=None):
483+ super(ServiceUnitPlacementV4, self).__init__(
484+ service, deployment, status, arbitrary_machines=arbitrary_machines)
485+
486+ # Arbitrary machines will not be allowed in v4 bundles.
487+ self.arbitrary_machines = False
488+
489+ self.machines_map = machines_map
490+
491+ # Ensure that placement spec is filled according to the bundle
492+ # specification.
493+ self._fill_placement()
494+
495+ def _fill_placement(self):
496+ """Fill the placement spec with necessary data.
497+
498+ From the spec:
499+ A unit placement may be specified with a service name only, in which
500+ case its unit number is assumed to be one more than the unit number of
501+ the previous unit in the list with the same service, or zero if there
502+ were none.
503+
504+ If there are less elements in To than NumUnits, the last element is
505+ replicated to fill it. If there are no elements (or To is omitted),
506+ "new" is replicated.
507+ """
508+ unit_mapping = self.service.unit_placement
509+ unit_count = self.service.num_units
510+ if not unit_mapping:
511+ self.service.svc_data['to'] = ['new'] * unit_count
512+ return
513+
514+ self.service.svc_data['to'] = (
515+ unit_mapping +
516+ list(itertools.repeat(unit_mapping[-1], unit_count - len(unit_mapping)))
517+ )
518+ unit_mapping = self.service.unit_placement
519+
520+ colocate_counts = {}
521+ for idx, mapping in enumerate(unit_mapping):
522+ service = mapping
523+ if ':' in mapping:
524+ service = mapping.split(':')[1]
525+ if service in self.deployment.data['services']:
526+ unit_number = colocate_counts.setdefault(service, 0)
527+ unit_mapping[idx] = "{}/{}".format(mapping, unit_number)
528+ colocate_counts[service] += 1
529+ self.service.svc_data['to'] = unit_mapping
530+
531+ def _parse_placement(self, placement):
532+ """Parse a unit placement statement.
533+
534+ In version 4 bundles, unit placement statements take the form of
535+
536+ (<containertype>:)?(<unit>|<machine>|new)
537+
538+ This splits the placement into a container, a placement, and a unit
539+ number. Both container and unit number are optional and can be None.
540+ """
541+ container = unit_number = None
542+ if ':' in placement:
543+ container, placement = placement.split(':')
544+ if '/' in placement:
545+ placement, unit_number = placement.split('/')
546+ return container, placement, unit_number
547+
548+ def validate(self):
549+ """Validate the placement of a service and all of its units.
550+
551+ If a service has a 'to' block specified, the list of machines, units,
552+ containers, and/or services must be internally consistent, consistent
553+ with other services in the deployment, and consistent with any machines
554+ specified in the 'machines' block of the deployment.
555+
556+ A feedback object is returned, potentially with errors and warnings
557+ inside it.
558+ """
559+ feedback = Feedback()
560+
561+ unit_placement = self.service.unit_placement
562+ if unit_placement is None:
563+ return feedback
564+
565+ if not isinstance(unit_placement, (list, tuple)):
566+ unit_placement = [unit_placement]
567+ unit_placement = map(str, unit_placement)
568+
569+ services = dict([(s.name, s) for s in self.deployment.get_services()])
570+ machines = self.deployment.get_machines()
571+ container = None
572+ unit_number = None
573+
574+ for i, placement in enumerate(unit_placement):
575+ container, placement, unit_number = self._parse_placement(placement)
576+
577+ if container and container not in ('lxc', 'kvm'):
578+ feedback.error(
579+ "Invalid container type: %s service: %s placement: %s" \
580+ % (container, self.service.name, unit_placement[i]))
581+ # XXX Nesting containers not supported yet.
582+ # Makyo - 2015-03-01
583+ if container is not None and not (placement.isdigit()
584+ or placement == 'new'):
585+ feedback.error(
586+ "Invalid target for container: %s" % (
587+ unit_placement[i]))
588+ # Specify an existing machine (or, if the number is in the
589+ # list of machine specs, one of those).
590+ if placement.isdigit():
591+ if placement in machines:
592+ continue
593+ else:
594+ feedback.error(
595+ ("Service placement to machine "
596+ "not supported %s to %s") % (
597+ self.service.name, unit_placement[i]))
598+ # Specify a machine from the machine spec.
599+ elif placement in self.deployment.get_machines():
600+ continue
601+ # Specify a service for colocation.
602+ elif placement in services:
603+ # Specify a particular unit for colocation.
604+ if unit_number is not None and \
605+ unit_number > services[placement].num_units:
606+ feedback.error(
607+ "Service unit does not exist, %s to %s/%s" % (
608+ self.service.name, placement, unit_number))
609+ elif self.deployment.get_charm_for(placement).is_subordinate():
610+ feedback.error(
611+ "Cannot place to a subordinate service: %s -> %s" % (
612+ self.service.name, placement))
613+ # Create a new machine or container.
614+ elif placement == 'new':
615+ continue
616+ else:
617+ feedback.error(
618+ "Invalid service placement %s to %s" % (
619+ self.service.name, unit_placement[i]))
620+ return feedback
621+
622+ def get_new_machines_for_containers(self):
623+ """Return a list of containers in the service's unit placement that
624+ have been requested to be put on new machines."""
625+ new_machines = []
626+ unit = itertools.count()
627+ for placement in self.service.unit_placement:
628+ if ':new' in placement:
629+ # Generate a name for this machine to be used in the
630+ # machines_map used later; as a quick path forward, simply use
631+ # the unit's name.
632+ new_machines.append('{}/{}'.format(self.service.name, unit.next()))
633+ return new_machines
634+
635+ def get(self, unit_number):
636+ """Get the placement directive for a given unit.
637+
638+ unit_number: the number of the unit to deploy
639+ """
640+ status = self.status
641+ svc = self.service
642+
643+ unit_mapping = svc.unit_placement
644+ unit_placement = placement = str(unit_mapping[unit_number])
645+ container = None
646+ u_idx = unit_number
647+
648+ # Shortcut for new machines.
649+ if placement == 'new':
650+ return None
651+
652+ container, placement, unit_number = self._parse_placement(unit_placement)
653+
654+ if placement in self.machines_map:
655+ return self._format_placement(self.machines_map[placement], container)
656+
657+ # Handle <container_type>:new
658+ if placement == 'new':
659+ return self._format_placement(
660+ self.machines_map['%s/%d' % (self.service.name, u_idx)], container)
661+
662+ return self.colocate(status, placement, u_idx, container, svc)
663
664=== modified file 'deployer/tests/base.py'
665--- deployer/tests/base.py 2015-03-17 17:38:26 +0000
666+++ deployer/tests/base.py 2015-03-24 17:39:12 +0000
667@@ -30,13 +30,19 @@
668 def tearDownClass(cls):
669 shutil.rmtree(os.environ["JUJU_HOME"])
670
671- def get_named_deployment(self, file_name, stack_name):
672- """ Get deployment from test_data file.
673+ def get_named_deployment_v3(self, file_name, stack_name):
674+ """ Get v3 deployment from a test_data file.
675 """
676 return ConfigStack(
677 [os.path.join(
678 self.test_data_dir, file_name)]).get(stack_name)
679
680+ def get_deployment_v4(self, file_name):
681+ """Get v4 deployment from a test_data file.
682+ """
683+ f = os.path.join(self.test_data_dir, 'v4', file_name)
684+ return ConfigStack([f]).get(f)
685+
686 def capture_logging(self, name="", level=logging.INFO,
687 log_file=None, formatter=None):
688 if log_file is None:
689
690=== modified file 'deployer/tests/test_config.py'
691--- deployer/tests/test_config.py 2015-02-26 17:33:21 +0000
692+++ deployer/tests/test_config.py 2015-03-24 17:39:12 +0000
693@@ -44,11 +44,11 @@
694
695 def test_config_v4(self):
696 config = ConfigStack([
697- os.path.join(self.test_data_dir, 'blog_v4.yaml')])
698+ os.path.join(self.test_data_dir, 'v4', 'simple.yaml')])
699 config.load()
700 self.assertEqual(
701 config.keys(),
702- [os.path.join(self.test_data_dir, 'blog_v4.yaml')])
703+ [os.path.join(self.test_data_dir, 'v4', 'simple.yaml')])
704 with mock.patch('deployer.config.ConfigStack._resolve_inherited') \
705 as mock_resolve:
706 deployment = config.get(config.keys()[0])
707
708=== added directory 'deployer/tests/test_data/v4'
709=== added file 'deployer/tests/test_data/v4/container-new.yaml'
710--- deployer/tests/test_data/v4/container-new.yaml 1970-01-01 00:00:00 +0000
711+++ deployer/tests/test_data/v4/container-new.yaml 2015-03-24 17:39:12 +0000
712@@ -0,0 +1,43 @@
713+services:
714+ mediawiki:
715+ charm: cs:precise/mediawiki-10
716+ num_units: 1
717+ options:
718+ debug: false
719+ name: Please set name of wiki
720+ skin: vector
721+ annotations:
722+ gui-x: "609"
723+ gui-y: "-15"
724+ to:
725+ - "1"
726+ mysql:
727+ charm: cs:precise/mysql-28
728+ num_units: 1
729+ options:
730+ binlog-format: MIXED
731+ block-size: 5
732+ dataset-size: 80%
733+ flavor: distro
734+ ha-bindiface: eth0
735+ ha-mcastport: 5411
736+ max-connections: -1
737+ preferred-storage-engine: InnoDB
738+ query-cache-size: -1
739+ query-cache-type: "OFF"
740+ rbd-name: mysql1
741+ tuning-level: safest
742+ vip_cidr: 24
743+ vip_iface: eth0
744+ annotations:
745+ gui-x: "610"
746+ gui-y: "255"
747+ to:
748+ - "lxc:new"
749+series: precise
750+relations:
751+- - mediawiki:db
752+ - mysql:db
753+machines:
754+ 1:
755+ constraints: 'mem=512M'
756
757=== added file 'deployer/tests/test_data/v4/container.yaml'
758--- deployer/tests/test_data/v4/container.yaml 1970-01-01 00:00:00 +0000
759+++ deployer/tests/test_data/v4/container.yaml 2015-03-24 17:39:12 +0000
760@@ -0,0 +1,43 @@
761+services:
762+ mediawiki:
763+ charm: cs:precise/mediawiki-10
764+ num_units: 1
765+ options:
766+ debug: false
767+ name: Please set name of wiki
768+ skin: vector
769+ annotations:
770+ gui-x: "609"
771+ gui-y: "-15"
772+ to:
773+ - "1"
774+ mysql:
775+ charm: cs:precise/mysql-28
776+ num_units: 1
777+ options:
778+ binlog-format: MIXED
779+ block-size: 5
780+ dataset-size: 80%
781+ flavor: distro
782+ ha-bindiface: eth0
783+ ha-mcastport: 5411
784+ max-connections: -1
785+ preferred-storage-engine: InnoDB
786+ query-cache-size: -1
787+ query-cache-type: "OFF"
788+ rbd-name: mysql1
789+ tuning-level: safest
790+ vip_cidr: 24
791+ vip_iface: eth0
792+ annotations:
793+ gui-x: "610"
794+ gui-y: "255"
795+ to:
796+ - "lxc:1"
797+series: precise
798+relations:
799+- - mediawiki:db
800+ - mysql:db
801+machines:
802+ 1:
803+ constraints: "mem=512M"
804
805=== added file 'deployer/tests/test_data/v4/fill_placement.yaml'
806--- deployer/tests/test_data/v4/fill_placement.yaml 1970-01-01 00:00:00 +0000
807+++ deployer/tests/test_data/v4/fill_placement.yaml 2015-03-24 17:39:12 +0000
808@@ -0,0 +1,17 @@
809+services:
810+ mediawiki1:
811+ charm: cs:precise/mediawiki-10
812+ num_units: 2
813+ mediawiki2:
814+ charm: cs:precise/mediawiki-10
815+ num_units: 2
816+ to:
817+ - 0
818+ mediawiki3:
819+ charm: cs:precise/mediawiki-10
820+ num_units: 2
821+ to:
822+ - mediawiki1
823+ - mediawiki1
824+machines: {}
825+series: precise
826
827=== added file 'deployer/tests/test_data/v4/hulk-smash-nounits-nomachines.yaml'
828--- deployer/tests/test_data/v4/hulk-smash-nounits-nomachines.yaml 1970-01-01 00:00:00 +0000
829+++ deployer/tests/test_data/v4/hulk-smash-nounits-nomachines.yaml 2015-03-24 17:39:12 +0000
830@@ -0,0 +1,38 @@
831+services:
832+ mediawiki:
833+ charm: cs:precise/mediawiki-10
834+ num_units: 1
835+ options:
836+ debug: false
837+ name: Please set name of wiki
838+ skin: vector
839+ annotations:
840+ gui-x: "609"
841+ gui-y: "-15"
842+ mysql:
843+ charm: cs:precise/mysql-28
844+ num_units: 1
845+ options:
846+ binlog-format: MIXED
847+ block-size: 5
848+ dataset-size: 80%
849+ flavor: distro
850+ ha-bindiface: eth0
851+ ha-mcastport: 5411
852+ max-connections: -1
853+ preferred-storage-engine: InnoDB
854+ query-cache-size: -1
855+ query-cache-type: "OFF"
856+ rbd-name: mysql1
857+ tuning-level: safest
858+ vip_cidr: 24
859+ vip_iface: eth0
860+ annotations:
861+ gui-x: "610"
862+ gui-y: "255"
863+ to:
864+ - "mediawiki"
865+series: precise
866+relations:
867+- - mediawiki:db
868+ - mysql:db
869
870=== added file 'deployer/tests/test_data/v4/hulk-smash-nounits.yaml'
871--- deployer/tests/test_data/v4/hulk-smash-nounits.yaml 1970-01-01 00:00:00 +0000
872+++ deployer/tests/test_data/v4/hulk-smash-nounits.yaml 2015-03-24 17:39:12 +0000
873@@ -0,0 +1,43 @@
874+services:
875+ mediawiki:
876+ charm: cs:precise/mediawiki-10
877+ num_units: 1
878+ options:
879+ debug: false
880+ name: Please set name of wiki
881+ skin: vector
882+ annotations:
883+ gui-x: "609"
884+ gui-y: "-15"
885+ to:
886+ - "1"
887+ mysql:
888+ charm: cs:precise/mysql-28
889+ num_units: 1
890+ options:
891+ binlog-format: MIXED
892+ block-size: 5
893+ dataset-size: 80%
894+ flavor: distro
895+ ha-bindiface: eth0
896+ ha-mcastport: 5411
897+ max-connections: -1
898+ preferred-storage-engine: InnoDB
899+ query-cache-size: -1
900+ query-cache-type: "OFF"
901+ rbd-name: mysql1
902+ tuning-level: safest
903+ vip_cidr: 24
904+ vip_iface: eth0
905+ annotations:
906+ gui-x: "610"
907+ gui-y: "255"
908+ to:
909+ - "mediawiki"
910+series: precise
911+relations:
912+- - mediawiki:db
913+ - mysql:db
914+machines:
915+ 1:
916+ constraints: 'mem=512M'
917
918=== added file 'deployer/tests/test_data/v4/hulk-smash.yaml'
919--- deployer/tests/test_data/v4/hulk-smash.yaml 1970-01-01 00:00:00 +0000
920+++ deployer/tests/test_data/v4/hulk-smash.yaml 2015-03-24 17:39:12 +0000
921@@ -0,0 +1,43 @@
922+services:
923+ mediawiki:
924+ charm: cs:precise/mediawiki-10
925+ num_units: 1
926+ options:
927+ debug: false
928+ name: Please set name of wiki
929+ skin: vector
930+ annotations:
931+ gui-x: "609"
932+ gui-y: "-15"
933+ to:
934+ - "1"
935+ mysql:
936+ charm: cs:precise/mysql-28
937+ num_units: 1
938+ options:
939+ binlog-format: MIXED
940+ block-size: 5
941+ dataset-size: 80%
942+ flavor: distro
943+ ha-bindiface: eth0
944+ ha-mcastport: 5411
945+ max-connections: -1
946+ preferred-storage-engine: InnoDB
947+ query-cache-size: -1
948+ query-cache-type: "OFF"
949+ rbd-name: mysql1
950+ tuning-level: safest
951+ vip_cidr: 24
952+ vip_iface: eth0
953+ annotations:
954+ gui-x: "610"
955+ gui-y: "255"
956+ to:
957+ - "mediawiki/0"
958+series: precise
959+relations:
960+- - mediawiki:db
961+ - mysql:db
962+machines:
963+ 1:
964+ constraints: 'mem=512M'
965
966=== added file 'deployer/tests/test_data/v4/placement-invalid-subordinate.yaml'
967--- deployer/tests/test_data/v4/placement-invalid-subordinate.yaml 1970-01-01 00:00:00 +0000
968+++ deployer/tests/test_data/v4/placement-invalid-subordinate.yaml 2015-03-24 17:39:12 +0000
969@@ -0,0 +1,13 @@
970+series: precise
971+services:
972+ nova-compute:
973+ charm: cs:precise/nova-compute
974+ to:
975+ - nrpe
976+ ceph:
977+ charm: cs:precise/ceph
978+ to:
979+ - lxc:nrpe/0
980+ nrpe:
981+ charm: cs:precise/nrpe
982+ units: 2
983
984=== added file 'deployer/tests/test_data/v4/placement.yaml'
985--- deployer/tests/test_data/v4/placement.yaml 1970-01-01 00:00:00 +0000
986+++ deployer/tests/test_data/v4/placement.yaml 2015-03-24 17:39:12 +0000
987@@ -0,0 +1,45 @@
988+services:
989+ mediawiki:
990+ charm: cs:precise/mediawiki-10
991+ num_units: 1
992+ options:
993+ debug: false
994+ name: Please set name of wiki
995+ skin: vector
996+ annotations:
997+ gui-x: "609"
998+ gui-y: "-15"
999+ to:
1000+ - "1"
1001+ mysql:
1002+ charm: cs:precise/mysql-28
1003+ num_units: 1
1004+ options:
1005+ binlog-format: MIXED
1006+ block-size: 5
1007+ dataset-size: 80%
1008+ flavor: distro
1009+ ha-bindiface: eth0
1010+ ha-mcastport: 5411
1011+ max-connections: -1
1012+ preferred-storage-engine: InnoDB
1013+ query-cache-size: -1
1014+ query-cache-type: "OFF"
1015+ rbd-name: mysql1
1016+ tuning-level: safest
1017+ vip_cidr: 24
1018+ vip_iface: eth0
1019+ annotations:
1020+ gui-x: "610"
1021+ gui-y: "255"
1022+ to:
1023+ - "2"
1024+series: precise
1025+relations:
1026+- - mediawiki:db
1027+ - mysql:db
1028+machines:
1029+ 1:
1030+ constraints: 'mem=512M'
1031+ 2:
1032+ constraints: 'mem=512M'
1033
1034=== added file 'deployer/tests/test_data/v4/series.yaml'
1035--- deployer/tests/test_data/v4/series.yaml 1970-01-01 00:00:00 +0000
1036+++ deployer/tests/test_data/v4/series.yaml 2015-03-24 17:39:12 +0000
1037@@ -0,0 +1,21 @@
1038+services:
1039+ mediawiki:
1040+ charm: cs:precise/mediawiki-10
1041+ num_units: 1
1042+ annotations:
1043+ gui-x: "609"
1044+ gui-y: "-15"
1045+ to:
1046+ - "1"
1047+ mysql:
1048+ charm: cs:trusty/mysql-1
1049+ num_units: 1
1050+ to:
1051+ - "2"
1052+series: trusty
1053+machines:
1054+ 1:
1055+ series: precise
1056+ constraints: 'mem=512M'
1057+ 2:
1058+ constraints: 'mem=512M'
1059
1060=== renamed file 'deployer/tests/test_data/blog_v4.yaml' => 'deployer/tests/test_data/v4/simple.yaml'
1061--- deployer/tests/test_data/blog_v4.yaml 2015-02-20 19:18:58 +0000
1062+++ deployer/tests/test_data/v4/simple.yaml 2015-03-24 17:39:12 +0000
1063@@ -30,6 +30,7 @@
1064 annotations:
1065 gui-x: "610"
1066 gui-y: "255"
1067+machines: {}
1068 series: precise
1069 relations:
1070 - - mediawiki:db
1071
1072=== added file 'deployer/tests/test_data/v4/validate.yaml'
1073--- deployer/tests/test_data/v4/validate.yaml 1970-01-01 00:00:00 +0000
1074+++ deployer/tests/test_data/v4/validate.yaml 2015-03-24 17:39:12 +0000
1075@@ -0,0 +1,16 @@
1076+services:
1077+ mysql:
1078+ charm: 'cs:precise/mysql-1'
1079+ num_units: 5
1080+ to:
1081+ - 'asdf:0'
1082+ - 'lxc:asdf'
1083+ - '1'
1084+ - 'wordpress/3'
1085+ - 'asdf'
1086+ wordpress:
1087+ charm: 'cs:precise/wordpress-1'
1088+ num_units: 1
1089+machines:
1090+ 3:
1091+ constraints: ''
1092
1093=== modified file 'deployer/tests/test_deployment.py'
1094--- deployer/tests/test_deployment.py 2015-03-17 16:24:27 +0000
1095+++ deployer/tests/test_deployment.py 2015-03-24 17:39:12 +0000
1096@@ -8,14 +8,34 @@
1097 from .base import Base, skip_if_offline
1098
1099
1100+class FauxService(object):
1101+ """A fake service with a unit_placement attribute, used for testing
1102+ the sort functionality.
1103+ """
1104+
1105+ def __init__(self, name=None, unit_placement=None):
1106+ self.name = name
1107+ self.unit_placement = unit_placement
1108+
1109+
1110 class DeploymentTest(Base):
1111
1112 def setUp(self):
1113 self.output = setup_logging(
1114 debug=True, verbose=True, stream=StringIO.StringIO())
1115
1116- def get_named_deployment_and_fetch(self, file_name, stack_name):
1117- deployment = self.get_named_deployment(file_name, stack_name)
1118+ def get_named_deployment_and_fetch_v3(self, file_name, stack_name):
1119+ deployment = self.get_named_deployment_v3(file_name, stack_name)
1120+ # Fetch charms in order to allow proper late binding config and
1121+ # placement validation.
1122+ repo_path = self.mkdir()
1123+ os.mkdir(os.path.join(repo_path, "precise"))
1124+ deployment.repo_path = repo_path
1125+ deployment.fetch_charms()
1126+ return deployment
1127+
1128+ def get_deployment_and_fetch_v4(self, file_name):
1129+ deployment = self.get_deployment_v4(file_name)
1130 # Fetch charms in order to allow proper late binding config and
1131 # placement validation.
1132 repo_path = self.mkdir()
1133@@ -26,7 +46,7 @@
1134
1135 @skip_if_offline
1136 def test_deployer(self):
1137- d = self.get_named_deployment_and_fetch('blog.yaml', 'wordpress-prod')
1138+ d = self.get_named_deployment_and_fetch_v3('blog.yaml', 'wordpress-prod')
1139 services = d.get_services()
1140 self.assertTrue([s for s in services if s.name == "newrelic"])
1141
1142@@ -53,7 +73,7 @@
1143 [('blog', 'db'), ('blog', 'cache'), ('blog', 'haproxy')])
1144
1145 def test_maas_name_and_zone_placement(self):
1146- d = self.get_named_deployment("stack-placement-maas.yml", "stack")
1147+ d = self.get_named_deployment_v3("stack-placement-maas.yml", "stack")
1148 d.validate_placement()
1149 placement = d.get_unit_placement('ceph', {})
1150 self.assertEqual(placement.get(0), "arnolt")
1151@@ -62,7 +82,7 @@
1152
1153 @skip_if_offline
1154 def test_validate_placement_sorting(self):
1155- d = self.get_named_deployment_and_fetch("stack-placement.yaml", "stack")
1156+ d = self.get_named_deployment_and_fetch_v3("stack-placement.yaml", "stack")
1157 services = d.get_services()
1158 self.assertEqual(services[0].name, 'nova-compute')
1159 try:
1160@@ -70,9 +90,76 @@
1161 except ErrorExit:
1162 self.fail("Should not fail")
1163
1164+ def test_machines_placement_sort(self):
1165+ d = Deployment('test', None, None)
1166+ self.assertEqual(
1167+ d._machines_placement_sort(
1168+ FauxService(unit_placement=1),
1169+ FauxService()
1170+ ), 1)
1171+ self.assertEqual(
1172+ d._machines_placement_sort(
1173+ FauxService(),
1174+ FauxService(unit_placement=1)
1175+ ), -1)
1176+ self.assertEqual(
1177+ d._machines_placement_sort(
1178+ FauxService(name="x", unit_placement=['asdf']),
1179+ FauxService(name="y", unit_placement=['lxc:x/1'])
1180+ ), 1)
1181+ self.assertEqual(
1182+ d._machines_placement_sort(
1183+ FauxService(name="y", unit_placement=['lxc:x/1']),
1184+ FauxService(name="x", unit_placement=['asdf'])
1185+ ), -1)
1186+ self.assertEqual(
1187+ d._machines_placement_sort(
1188+ FauxService(name="x", unit_placement=['asdf']),
1189+ FauxService(name="y", unit_placement=['hjkl'])
1190+ ), -1)
1191+ self.assertEqual(
1192+ d._machines_placement_sort(
1193+ FauxService(name="x"),
1194+ FauxService(name="y")
1195+ ), -1)
1196+
1197+ def test_colocate(self):
1198+ status = {
1199+ 'services': {
1200+ 'foo': {
1201+ 'units': {
1202+ '1': {
1203+ 'machine': 1
1204+ },
1205+ '2': {}
1206+ }
1207+ }
1208+ }
1209+ }
1210+ d = self.get_named_deployment_v3("stack-placement.yaml", "stack")
1211+ p = d.get_unit_placement('ceph', status)
1212+ svc = FauxService(name='bar')
1213+
1214+ self.assertEqual(p.colocate(status, 'asdf', '1', '', svc),
1215+ None)
1216+ self.assertIn('Service bar to be deployed with non-existent service '
1217+ 'asdf',
1218+ self.output.getvalue())
1219+ self.assertEqual(p.colocate(status, 'foo', '2', '', svc),
1220+ None)
1221+ self.assertIn('Service:bar, Deploy-with-service:foo, '
1222+ 'Requested-unit-index=2, Cannot solve, '
1223+ 'falling back to default placement',
1224+ self.output.getvalue())
1225+ self.assertEqual(p.colocate(status, 'foo', '1', '', svc),
1226+ None)
1227+ self.assertIn('Service:bar deploy-with unit missing machine 2',
1228+ self.output.getvalue())
1229+ self.assertEqual(p.colocate(status, 'foo', '0', '', svc), 1)
1230+
1231 @skip_if_offline
1232 def test_validate_invalid_placement_nested(self):
1233- d = self.get_named_deployment_and_fetch("stack-placement-invalid.yaml", "stack")
1234+ d = self.get_named_deployment_and_fetch_v3("stack-placement-invalid.yaml", "stack")
1235 services = d.get_services()
1236 self.assertEqual(services[0].name, 'nova-compute')
1237 try:
1238@@ -84,7 +171,7 @@
1239
1240 @skip_if_offline
1241 def test_validate_invalid_placement_no_with_service(self):
1242- d = self.get_named_deployment_and_fetch(
1243+ d = self.get_named_deployment_and_fetch_v3(
1244 "stack-placement-invalid-2.yaml", "stack")
1245 services = d.get_services()
1246 self.assertEqual(services[0].name, 'nova-compute')
1247@@ -96,9 +183,9 @@
1248 self.fail("Should fail")
1249
1250 @skip_if_offline
1251- def test_validate_invalid_placement_subordinate(self):
1252+ def test_validate_invalid_placement_subordinate_v3(self):
1253 # Placement validation fails if a subordinate charm is provided.
1254- deployment = self.get_named_deployment_and_fetch(
1255+ deployment = self.get_named_deployment_and_fetch_v3(
1256 'stack-placement-invalid-subordinate.yaml', 'stack')
1257 with self.assertRaises(ErrorExit):
1258 deployment.validate_placement()
1259@@ -108,9 +195,21 @@
1260 self.assertIn(
1261 'Cannot place to a subordinate service: nova-compute -> nrpe\n',
1262 output)
1263+
1264+ @skip_if_offline
1265+ def test_validate_invalid_placement_subordinate_v4(self):
1266+ # Placement validation fails if a subordinate charm is provided.
1267+ deployment = self.get_deployment_and_fetch_v4(
1268+ 'placement-invalid-subordinate.yaml')
1269+ with self.assertRaises(ErrorExit):
1270+ deployment.validate_placement()
1271+ output = self.output.getvalue()
1272+ self.assertIn(
1273+ 'Cannot place to a subordinate service: nova-compute -> nrpe\n',
1274+ output)
1275
1276- def test_get_unit_placement(self):
1277- d = self.get_named_deployment("stack-placement.yaml", "stack")
1278+ def test_get_unit_placement_v3(self):
1279+ d = self.get_named_deployment_v3("stack-placement.yaml", "stack")
1280 status = {
1281 'services': {
1282 'nova-compute': {
1283@@ -144,6 +243,195 @@
1284 self.assertEqual(placement.get(3), 'lxc:1')
1285 self.assertEqual(placement.get(4), 'lxc:3')
1286
1287+ def test_fill_placement_v4(self):
1288+ d = self.get_deployment_v4('fill_placement.yaml')
1289+ self.assertEqual(
1290+ d.get_unit_placement('mediawiki1', 0).service.svc_data['to'],
1291+ ['new', 'new'])
1292+ self.assertEqual(
1293+ d.get_unit_placement('mediawiki2', 0).service.svc_data['to'],
1294+ ['0', '0'])
1295+ self.assertEqual(
1296+ d.get_unit_placement('mediawiki3', 0).service.svc_data['to'],
1297+ ['mediawiki1/0', 'mediawiki1/1'])
1298+
1299+ def test_parse_placement_v4(self):
1300+ # Short-cut to winding up with a valid placement.
1301+ d = self.get_deployment_v4('simple.yaml')
1302+ placement = d.get_unit_placement('mysql', {})
1303+
1304+ c, p, u = placement._parse_placement('mysql')
1305+ self.assertEqual(c, None)
1306+ self.assertEqual(p, 'mysql')
1307+ self.assertEqual(u, None)
1308+
1309+ c, p, u = placement._parse_placement('mysql/1')
1310+ self.assertEqual(c, None)
1311+ self.assertEqual(p, 'mysql')
1312+ self.assertEqual(u, '1')
1313+
1314+ c, p, u = placement._parse_placement('lxc:mysql')
1315+ self.assertEqual(c, 'lxc')
1316+ self.assertEqual(p, 'mysql')
1317+ self.assertEqual(u, None)
1318+
1319+ def test_validate_v4(self):
1320+ d = self.get_deployment_v4('validate.yaml')
1321+ placement = d.get_unit_placement('mysql', {})
1322+ feedback = placement.validate()
1323+ self.assertEqual(feedback.get_errors(), [
1324+ 'Invalid container type: asdf service: mysql placement: asdf:0',
1325+ 'Service placement to machine not supported mysql to asdf:0',
1326+ 'Invalid target for container: lxc:asdf',
1327+ 'Invalid service placement mysql to lxc:asdf',
1328+ 'Service placement to machine not supported mysql to 1',
1329+ 'Service unit does not exist, mysql to wordpress/3',
1330+ 'Invalid service placement mysql to asdf'])
1331+
1332+ def test_get_unit_placement_v4_simple(self):
1333+ d = self.get_deployment_v4('simple.yaml')
1334+ placement = d.get_unit_placement('mysql', {})
1335+ self.assertEqual(placement.get(0), None)
1336+
1337+ placement = d.get_unit_placement('mediawiki', {})
1338+ self.assertEqual(placement.get(0), None)
1339+
1340+ def test_get_unit_placement_v4_placement(self):
1341+ d = self.get_deployment_v4('placement.yaml')
1342+ machines = {
1343+ '1': 1,
1344+ '2': 2,
1345+ }
1346+
1347+ d.set_machines(machines)
1348+
1349+ placement = d.get_unit_placement('mysql', {})
1350+ d.set_machines(machines)
1351+ self.assertEqual(placement.get(0), 2)
1352+
1353+ placement = d.get_unit_placement('mediawiki', {})
1354+ self.assertEqual(placement.get(0), 1)
1355+
1356+ def test_get_unit_placement_v4_hulk_smash(self):
1357+ d = self.get_deployment_v4('hulk-smash.yaml')
1358+ machines = {
1359+ '1': 1,
1360+ }
1361+ status = {
1362+ 'services': {
1363+ 'mediawiki': {
1364+ 'units': {
1365+ 'mediawiki/1': {'machine': 1}
1366+ }
1367+ }
1368+ }
1369+ }
1370+
1371+ d.set_machines(machines)
1372+
1373+ placement = d.get_unit_placement('mysql', status)
1374+ self.assertEqual(placement.get(0), 1)
1375+
1376+ placement = d.get_unit_placement('mediawiki', status)
1377+ self.assertEqual(placement.get(0), 1)
1378+
1379+ def test_get_unit_placement_v4_hulk_smash_nounits(self):
1380+ d = self.get_deployment_v4('hulk-smash-nounits.yaml')
1381+ machines = {
1382+ '1': 1,
1383+ }
1384+ status = {
1385+ 'services': {
1386+ 'mediawiki': {
1387+ 'units': {
1388+ 'mediawiki/1': {'machine': 1}
1389+ }
1390+ }
1391+ }
1392+ }
1393+
1394+ d.set_machines(machines)
1395+
1396+ placement = d.get_unit_placement('mysql', status)
1397+ self.assertEqual(placement.get(0), 1)
1398+
1399+ placement = d.get_unit_placement('mediawiki', status)
1400+ self.assertEqual(placement.get(0), 1)
1401+
1402+ def test_get_unit_placement_v4_hulk_smash_nounits_nomachines(self):
1403+ d = self.get_deployment_v4('hulk-smash-nounits-nomachines.yaml')
1404+ machines = {
1405+ '1': 1,
1406+ }
1407+ status = {
1408+ 'services': {
1409+ 'mediawiki': {
1410+ 'units': {
1411+ 'mediawiki/1': {'machine': 1}
1412+ }
1413+ }
1414+ }
1415+ }
1416+
1417+ d.set_machines(machines)
1418+
1419+ placement = d.get_unit_placement('mysql', status)
1420+ self.assertEqual(placement.get(0), 1)
1421+
1422+ # Since we don't have a placement, even with the status, this should
1423+ # still be None.
1424+ placement = d.get_unit_placement('mediawiki', status)
1425+ self.assertEqual(placement.get(0), None)
1426+
1427+ def test_get_unit_placement_v4_container(self):
1428+ d = self.get_deployment_v4('container.yaml')
1429+ machines = {
1430+ '1': 1,
1431+ }
1432+ status = {
1433+ 'services': {
1434+ 'mediawiki': {
1435+ 'units': {
1436+ 'mediawiki/1': {'machine': 1},
1437+ }
1438+ }
1439+ }
1440+ }
1441+
1442+ d.set_machines(machines)
1443+
1444+ placement = d.get_unit_placement('mysql', status)
1445+ self.assertEqual(placement.get(0), 'lxc:1')
1446+
1447+ placement = d.get_unit_placement('mediawiki', status)
1448+ self.assertEqual(placement.get(0), 1)
1449+
1450+ def test_get_unit_placement_v4_container_new(self):
1451+ d = self.get_deployment_v4('container-new.yaml')
1452+ machines = {
1453+ '1': 1,
1454+ 'mysql/0': 2
1455+ }
1456+ status = {
1457+ 'services': {
1458+ 'mediawiki': {
1459+ 'units': {
1460+ 'mediawiki/1': {'machine': 1}
1461+ }
1462+ }
1463+ }
1464+ }
1465+
1466+ d.set_machines(machines)
1467+
1468+ placement = d.get_unit_placement('mysql', status)
1469+ self.assertEqual(placement.get_new_machines_for_containers(),
1470+ ['mysql/0'])
1471+ self.assertEqual(placement.get(0), 'lxc:2')
1472+
1473+ placement = d.get_unit_placement('mediawiki', status)
1474+ self.assertEqual(placement.get(0), 1)
1475+
1476 def test_multiple_relations_no_weight(self):
1477 data = {"relations": {"wordpress": {"consumes": ["mysql"]},
1478 "nginx": {"consumes": ["wordpress"]}}}
1479@@ -176,7 +464,7 @@
1480
1481 def test_getting_service_names(self):
1482 # It is possible to retrieve the service names.
1483- deployment = self.get_named_deployment("stack-placement.yaml", "stack")
1484+ deployment = self.get_named_deployment_v3("stack-placement.yaml", "stack")
1485 service_names = deployment.get_service_names()
1486 expected_service_names = [
1487 'ceph', 'mysql', 'nova-compute', 'quantum', 'semper', 'verity', 'lxc-service']
1488@@ -184,14 +472,14 @@
1489
1490 def test_resolve_config_handles_empty_options(self):
1491 """resolve_config should handle options being "empty" lp:1361883"""
1492- deployment = self.get_named_deployment("negative.cfg", "negative")
1493+ deployment = self.get_named_deployment_v3("negative.cfg", "negative")
1494 self.assertEqual(
1495 deployment.data["services"]["foo"]["options"], {})
1496 deployment.resolve_config()
1497
1498 def test_resolve_config_handles_none_options(self):
1499 """resolve_config should handle options being "none" lp:1361883"""
1500- deployment = self.get_named_deployment("negative.yaml", "negative")
1501+ deployment = self.get_named_deployment_v3("negative.yaml", "negative")
1502 self.assertEqual(
1503 deployment.data["services"]["foo"]["options"], None)
1504 deployment.resolve_config()
1505
1506=== modified file 'deployer/tests/test_goenv.py'
1507--- deployer/tests/test_goenv.py 2014-09-03 13:17:33 +0000
1508+++ deployer/tests/test_goenv.py 2015-03-24 17:39:12 +0000
1509@@ -22,9 +22,10 @@
1510 self.env = GoEnvironment(
1511 os.environ.get("JUJU_ENV"), endpoint=self.endpoint)
1512 self.env.connect()
1513- self.assertFalse(self.env.status().get('services'))
1514+ status = self.env.status()
1515+ self.assertFalse(status.get('services'))
1516 # Destroy everything.. consistent baseline
1517- self.env.reset(terminate_machines=True, terminate_delay=240)
1518+ self.env.reset(terminate_machines=len(status['machines'].keys()) > 1, terminate_delay=240)
1519
1520 def tearDown(self):
1521 self.env.reset(terminate_machines=True, terminate_delay=240)
1522@@ -37,7 +38,7 @@
1523 self.env.add_relation("test-db", "test-blog")
1524 self.env.add_units('test-blog', 1)
1525
1526- # Sleep cause juju core watches are eventually consistent (5s window)
1527+ # Sleep because juju core watches are eventually consistent (5s window)
1528 # and status rpc is broken (http://pad.lv/1203105)
1529 time.sleep(6)
1530 self.env.wait_for_units(timeout=800)
1531@@ -49,4 +50,26 @@
1532 services)
1533 for s in services:
1534 for k, u in status['services'][s]['units'].items():
1535- self.assertEqual(u['agent-state'], "started")
1536+ self.assertIn(u['agent-state'], ("allocating", "started"))
1537+
1538+ def test_add_machine(self):
1539+ machine_name = self.env.add_machine()
1540+
1541+ # Sleep because juju core watches are eventually consistent (5s window)
1542+ # and status rpc is broken (http://pad.lv/1203105)
1543+ time.sleep(6)
1544+ status = self.env.status()
1545+
1546+ self.assertIn(machine_name, status['machines'])
1547+
1548+ def test_set_annotation(self):
1549+ machine_name = self.env.add_machine()
1550+ self.env.set_annotation(machine_name, {'foo': 'bar'}, entity_type='machine')
1551+
1552+ # Sleep because juju core watches are eventually consistent (5s window)
1553+ # and status rpc is broken (http://pad.lv/1203105)
1554+ time.sleep(6)
1555+ status = self.env.status()
1556+
1557+ self.assertIn('foo', self.env.client.get_annotation(
1558+ machine_name, 'machine')['Annotations'])
1559
1560=== modified file 'deployer/tests/test_importer.py'
1561--- deployer/tests/test_importer.py 2015-03-17 10:40:34 +0000
1562+++ deployer/tests/test_importer.py 2015-03-24 17:39:12 +0000
1563@@ -75,3 +75,22 @@
1564 importer.run()
1565
1566 self.assertFalse(env.add_relation.called, False)
1567+
1568+ @skip_if_offline
1569+ @mock.patch('deployer.action.importer.time')
1570+ def test_importer_add_machine_series(self, mock_time):
1571+ self.options.configs = [
1572+ os.path.join(self.test_data_dir, 'v4', 'series.yaml')]
1573+ stack = ConfigStack(self.options.configs)
1574+ deploy = stack.get(self.options.configs[0])
1575+ env = mock.MagicMock()
1576+ importer = Importer(env, deploy, self.options)
1577+ importer.run()
1578+
1579+ self.assertEqual(env.add_machine.call_count, 2)
1580+ self.assertEqual(
1581+ env.add_machine.call_args_list[0][1],
1582+ {'series': 'precise', 'constraints': 'mem=512M'})
1583+ self.assertEqual(
1584+ env.add_machine.call_args_list[1][1],
1585+ {'series': 'trusty', 'constraints': 'mem=512M'})
1586
1587=== modified file 'deployer/utils.py'
1588--- deployer/utils.py 2014-09-30 19:26:26 +0000
1589+++ deployer/utils.py 2015-03-24 17:39:12 +0000
1590@@ -373,3 +373,22 @@
1591 if not 'default' in conf:
1592 raise ValueError("No Environment specified")
1593 return conf['default']
1594+
1595+def x_in_y(x, y):
1596+ """Check to see if the second argument is named in the first
1597+ argument's unit placement spec.
1598+
1599+ Both arguments provided are services with unit placement directives.
1600+ If the first service appears in the second service's unit placement,
1601+ either colocated on a default unit, colocated with a specific unit,
1602+ or containerized alongside that service, then True is returned, False
1603+ otherwise.
1604+ """
1605+ for placement in y.unit_placement:
1606+ if ':' in placement:
1607+ _, placement = placement.split(':')
1608+ if '/' in placement:
1609+ placement, _ = placement.split('/')
1610+ if x.name == placement:
1611+ return True
1612+ return False

Subscribers

People subscribed via source and target branches