Merge lp:~makyo/juju-deployer/machines-and-placement into lp:juju-deployer
- machines-and-placement
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
David Britton (community) | Approve | ||
🤖 Landscape Builder (community) | Approve | ||
Review via email: mp+251857@code.launchpad.net |
Commit message
Description of the change
Add v4 support for machine spec per the bundles spec.
QA:
Ensure that each of the bundles in deployer/
PYTHONPATH=. python deployer/cli.py -c deployer/
All tests should pass.
Brad Crittenden (bac) wrote : | # |
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!
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).
Madison Scott-Clary (makyo) wrote : | # |
Checkpoint for pairing.
Brad Crittenden (bac) wrote : | # |
Looks good Madison with a few comments.
Madison Scott-Clary (makyo) wrote : | # |
For live environment testing, assuming an LXC env named local,
TEST_ENDPOINT=
Madison Scott-Clary (makyo) wrote : | # |
> For live environment testing, assuming an LXC env named local,
>
> TEST_ENDPOINT=
> deployer.
Also, ensure that that happens on a single line. LP broke it up
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).
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.
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.
Madison Scott-Clary (makyo) : | # |
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!
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.
🤖 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.
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.
Preview Diff
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 |
QA instructions with PYTHONPATH won't work if juju-deployer has previously been pip installed.
Still doing QA.