Merge lp:~hazmat/juju-deployer/unit-placement into lp:juju-deployer

Proposed by Kapil Thangavelu
Status: Merged
Approved by: Adam Gandelman
Approved revision: 91
Merged at revision: 80
Proposed branch: lp:~hazmat/juju-deployer/unit-placement
Merge into: lp:juju-deployer
Diff against target: 731 lines (+436/-42)
14 files modified
configs/export.yml (+17/-0)
deployer/action/diff.py (+1/-1)
deployer/action/importer.py (+55/-23)
deployer/deployment.py (+111/-1)
deployer/env/base.py (+3/-0)
deployer/env/go.py (+68/-6)
deployer/service.py (+11/-3)
deployer/tests/test_config.py (+5/-6)
deployer/tests/test_data/stack-placement-invalid-2.yaml (+13/-0)
deployer/tests/test_data/stack-placement-invalid.yaml (+13/-0)
deployer/tests/test_data/stack-placement.yaml (+18/-0)
deployer/tests/test_deployment.py (+66/-1)
doc/config.rst (+54/-0)
setup.py (+1/-1)
To merge this branch: bzr merge lp:~hazmat/juju-deployer/unit-placement
Reviewer Review Type Date Requested Status
Adam Gandelman (community) Needs Fixing
Review via email: mp+193445@code.launchpad.net

Description of the change

Support for unit placement using deployer configuration. Also improve terminate/reset support with containers, and a fix for --diff output.

To post a comment you must log in.
Revision history for this message
Adam Gandelman (gandelman-a) wrote :

Testing this now, couple of issues so far:

If a service is configured as:

fooservice:
   to: [0]

deployer blows up:

  70, in get_unit_placement
    if ':' in unit_placement:
TypeError: argument of type 'int' is not iterable

Only running that code if unit_placement is a string seems to fix that.

Adding units fails:

2013-10-31 17:34:26 [INFO] deployer.import: Adding 2 more units to nova-compute
Traceback (most recent call last):
  File "/usr/local/bin/juju-deployer", line 9, in <module>
    load_entry_point('juju-deployer==0.2.8', 'console_scripts', 'juju-deployer')()
  File "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/cli.py", line 118, in main
    run()
  File "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/cli.py", line 209, in run
    importer.Importer(env, deployment, options).run()
  File "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/action/importer.py", line 186, in run
    self.add_units()
  File "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/action/importer.py", line 54, in add_units
    self.env.add_unit(svc.name, mspec)
  File "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/env/go.py", line 28, in add_unit
    return self.client.add_unit(service_name, machine_spec)
  File "/usr/local/lib/python2.7/dist-packages/jujuclient-0.12-py2.7.egg/jujuclient.py", line 465, in add_unit
    "Params": params})
  File "/usr/local/lib/python2.7/dist-packages/jujuclient-0.12-py2.7.egg/jujuclient.py", line 135, in _rpc
    raise EnvError(result)
jujuclient.EnvError: <Env Error - Details:
 { u'Error': u'must add at least one unit', u'RequestId': 1, u'Response': { }}

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

The second one is due to a need for 0.13 for jujuclient, i'll update the
dep spec for that and add some test for the first case.

On Thu, Oct 31, 2013 at 1:41 PM, Adam Gandelman <email address hidden> wrote:

> Review: Needs Fixing
>
> Testing this now, couple of issues so far:
>
> If a service is configured as:
>
> fooservice:
> to: [0]
>
> deployer blows up:
>
> 70, in get_unit_placement
> if ':' in unit_placement:
> TypeError: argument of type 'int' is not iterable
>
> Only running that code if unit_placement is a string seems to fix that.
>
> Adding units fails:
>
> 2013-10-31 17:34:26 [INFO] deployer.import: Adding 2 more units to
> nova-compute
> Traceback (most recent call last):
> File "/usr/local/bin/juju-deployer", line 9, in <module>
> load_entry_point('juju-deployer==0.2.8', 'console_scripts',
> 'juju-deployer')()
> File
> "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/cli.py",
> line 118, in main
> run()
> File
> "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/cli.py",
> line 209, in run
> importer.Importer(env, deployment, options).run()
> File
> "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/action/importer.py",
> line 186, in run
> self.add_units()
> File
> "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/action/importer.py",
> line 54, in add_units
> self.env.add_unit(svc.name, mspec)
> File
> "/usr/local/lib/python2.7/dist-packages/juju_deployer-0.2.8-py2.7.egg/deployer/env/go.py",
> line 28, in add_unit
> return self.client.add_unit(service_name, machine_spec)
> File
> "/usr/local/lib/python2.7/dist-packages/jujuclient-0.12-py2.7.egg/jujuclient.py",
> line 465, in add_unit
> "Params": params})
> File
> "/usr/local/lib/python2.7/dist-packages/jujuclient-0.12-py2.7.egg/jujuclient.py",
> line 135, in _rpc
> raise EnvError(result)
> jujuclient.EnvError: <Env Error - Details:
> { u'Error': u'must add at least one unit', u'RequestId': 1,
> u'Response': { }}
>
> --
>
> https://code.launchpad.net/~hazmat/juju-deployer/unit-placement/+merge/193445
> You are the owner of lp:~hazmat/juju-deployer/unit-placement.
>

89. By Kapil Thangavelu

fix for machine 0 placement

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

Fixes pushed

90. By Kapil Thangavelu

require newer version of jujuclient

91. By Kapil Thangavelu

by request do a wait for units before adding relations.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'configs/export.yml'
--- configs/export.yml 1970-01-01 00:00:00 +0000
+++ configs/export.yml 2013-11-01 15:46:48 +0000
@@ -0,0 +1,17 @@
1envExport:
2 services:
3 mysql:
4 charm: "cs:precise/mysql-27"
5 annotations:
6 "gui-x": "-251"
7 "gui-y": "-203"
8 wordpress:
9 charm: "cs:precise/wordpress-20"
10 num_units: 3
11 to: ['lxc:mysql', 'lxc:mysql']
12 annotations:
13 "gui-x": "116"
14 "gui-y": "-206"
15 relations:
16 - - "wordpress:db"
17 - "mysql:db"
018
=== modified file 'deployer/action/diff.py'
--- deployer/action/diff.py 2013-07-22 15:29:31 +0000
+++ deployer/action/diff.py 2013-11-01 15:46:48 +0000
@@ -134,7 +134,7 @@
134 if e_v != v:134 if e_v != v:
135 mod['config'] = {k: e_v}135 mod['config'] = {k: e_v}
136 if e_s['unit_count'] != d_s.get('num_units', 1):136 if e_s['unit_count'] != d_s.get('num_units', 1):
137 mod['num_units'] = e_s['num_units']137 mod['num_units'] = e_s['unit_count'] - d_s['num_units']
138 return mod138 return mod
139139
140 def run(self):140 def run(self):
141141
=== modified file 'deployer/action/importer.py'
--- deployer/action/importer.py 2013-10-02 17:50:05 +0000
+++ deployer/action/importer.py 2013-11-01 15:46:48 +0000
@@ -19,25 +19,41 @@
19 self.log.debug("Adding units...")19 self.log.debug("Adding units...")
20 # Add units to existing services that don't match count.20 # Add units to existing services that don't match count.
21 env_status = self.env.status()21 env_status = self.env.status()
22 added = set()22 reloaded = False
23
23 for svc in self.deployment.get_services():24 for svc in self.deployment.get_services():
24 delta = (svc.num_units -25 cur_units = len(env_status['services'][svc.name].get('units', ()))
25 len(env_status['services'][svc.name].get('units', ())))26 delta = (svc.num_units - cur_units)
26 if delta > 0:27
27 charm = self.deployment.get_charm_for(svc.name)28 if delta <= 0:
28 if charm.is_subordinate():
29 self.log.warning(
30 "Config specifies num units for subordinate: %s",
31 svc.name)
32 continue
33 self.log.info(
34 "Adding %d more units to %s" % (abs(delta), svc.name))
35 for u in self.env.add_units(svc.name, abs(delta)):
36 added.add(u)
37 else:
38 self.log.debug(29 self.log.debug(
39 " Service %r does not need any more units added.",30 " Service %r does not need any more units added.",
40 svc.name)31 svc.name)
32 continue
33
34 charm = self.deployment.get_charm_for(svc.name)
35 if charm.is_subordinate():
36 self.log.warning(
37 "Config specifies num units for subordinate: %s",
38 svc.name)
39 continue
40
41 self.log.info(
42 "Adding %d more units to %s" % (abs(delta), svc.name))
43 if svc.unit_placement:
44 # Reload status once after non placed services units are done.
45 if reloaded is False:
46 # Crappy workaround juju-core api inconsistency
47 time.sleep(5.1)
48 env_status = self.env.status()
49 reloaded = True
50
51 for mid in range(cur_units, svc.num_units):
52 mspec = self.deployment.get_unit_placement(
53 svc, mid, env_status)
54 self.env.add_unit(svc.name, mspec)
55 else:
56 self.env.add_units(svc.name, abs(delta))
4157
42 def get_charms(self):58 def get_charms(self):
43 # Get Charms59 # Get Charms
@@ -53,6 +69,8 @@
53 def deploy_services(self):69 def deploy_services(self):
54 self.log.info("Deploying services...")70 self.log.info("Deploying services...")
55 env_status = self.env.status()71 env_status = self.env.status()
72 reloaded = False
73
56 for svc in self.deployment.get_services():74 for svc in self.deployment.get_services():
57 if svc.name in env_status['services']:75 if svc.name in env_status['services']:
58 self.log.debug(76 self.log.debug(
@@ -62,14 +80,28 @@
62 charm = self.deployment.get_charm_for(svc.name)80 charm = self.deployment.get_charm_for(svc.name)
63 self.log.info(81 self.log.info(
64 " Deploying service %s using %s", svc.name, charm.charm_url)82 " Deploying service %s using %s", svc.name, charm.charm_url)
83
84 if svc.unit_placement:
85 # We sorted all the non placed services first, so we only
86 # need to update status once after we're done with them.
87 if not reloaded:
88 self.log.debug(
89 " Refetching status for placement deploys")
90 time.sleep(5.1)
91 env_status = self.env.status()
92 reloaded = True
93 num_units = 1
94 else:
95 num_units = svc.num_units
96
65 self.env.deploy(97 self.env.deploy(
66 svc.name,98 svc.name,
67 charm.charm_url,99 charm.charm_url,
68 self.deployment.repo_path,100 self.deployment.repo_path,
69 svc.config,101 svc.config,
70 svc.constraints,102 svc.constraints,
71 svc.num_units,103 num_units,
72 svc.force_machine)104 self.deployment.get_unit_placement(svc, 0, env_status))
73105
74 if svc.expose:106 if svc.expose:
75 self.log.info(" Exposing service %r" % svc.name)107 self.log.info(" Exposing service %r" % svc.name)
@@ -93,8 +125,8 @@
93 self.env.add_relation(end_a, end_b)125 self.env.add_relation(end_a, end_b)
94 created = True126 created = True
95 # per the original, not sure the use case.127 # per the original, not sure the use case.
96 self.log.debug(" Waiting 5s before next relation")128 #self.log.debug(" Waiting 5s before next relation")
97 time.sleep(5)129 #time.sleep(5)
98 return created130 return created
99131
100 def _rel_exists(self, status, end_a, end_b):132 def _rel_exists(self, status, end_a, end_b):
@@ -149,17 +181,17 @@
149 # to be consistent to subsequent watch api interactions, see181 # to be consistent to subsequent watch api interactions, see
150 # http://pad.lv/1203105 which will obviate this wait.182 # http://pad.lv/1203105 which will obviate this wait.
151 time.sleep(5.1)183 time.sleep(5.1)
184 self.add_units()
152185
186 self.log.debug("Waiting for units before adding relations")
153 self.wait_for_units()187 self.wait_for_units()
154 self.add_units()
155188
156 rels_created = self.add_relations()189 rels_created = self.add_relations()
157190
158 # Wait for the units to be up before waiting for rel stability.191 # Wait for the units to be up before waiting for rel stability.
159 self.log.debug("Waiting for units to be started")
160 self.wait_for_units(self.options.retry_count)
161 if rels_created:192 if rels_created:
162 self.log.debug("Waiting for relations %d", self.options.rel_wait)193 self.log.debug(
194 "Waiting for relation convergence %ds", self.options.rel_wait)
163 time.sleep(self.options.rel_wait)195 time.sleep(self.options.rel_wait)
164 self.wait_for_units(self.options.retry_count)196 self.wait_for_units(self.options.retry_count)
165197
166198
=== modified file 'deployer/deployment.py'
--- deployer/deployment.py 2013-07-24 23:10:15 +0000
+++ deployer/deployment.py 2013-11-01 15:46:48 +0000
@@ -40,8 +40,74 @@
40 return Service(name, self.data['services'][name])40 return Service(name, self.data['services'][name])
4141
42 def get_services(self):42 def get_services(self):
43 services = []
43 for name, svc_data in self.data.get('services', {}).items():44 for name, svc_data in self.data.get('services', {}).items():
44 yield Service(name, svc_data)45 services.append(Service(name, svc_data))
46 services.sort(self._placement_sort)
47 return services
48
49 @staticmethod
50 def _placement_sort(svc_a, svc_b):
51 if svc_a.unit_placement:
52 if svc_b.unit_placement:
53 return cmp(svc_a.name, svc_b.name)
54 return 1
55 if svc_b.unit_placement:
56 return -1
57 return cmp(svc_a.name, svc_b.name)
58
59 @staticmethod
60 def _format_placement(machine, container=None):
61 if container:
62 return "%s:%s" % (container, machine)
63 else:
64 return machine
65
66 def get_unit_placement(self, svc, unit_number, status):
67 unit_mapping = svc.unit_placement
68 if not unit_mapping:
69 return None
70 if len(unit_mapping) <= unit_number:
71 return None
72
73 unit_placement = placement = str(unit_mapping[unit_number])
74 container = None
75 u_idx = unit_number
76
77 if ':' in unit_placement:
78 container, placement = unit_placement.split(":")
79 if '=' in placement:
80 placement, u_idx = placement.split("=")
81
82 if placement.isdigit() and placement == "0":
83 return self._format_placement(placement, container)
84
85 with_service = status['services'].get(placement)
86 if with_service is None:
87 # Should be caught in validate relations but sanity check
88 # for concurrency.
89 self.log.error(
90 "Service %s to be deployed with non existant service %s",
91 svc.name, placement)
92 # Prefer continuing deployment with a new machine rather
93 # than an in-progress abort.
94 return None
95
96 svc_units = with_service['units']
97 if len(svc_units) <= unit_number:
98 self.log.warning(
99 "Service:%s deploy-with Service:%s, but no with unit found",
100 svc.name, placement)
101 return None
102 unit_names = svc_units.keys()
103 unit_names.sort()
104 machine = svc_units[unit_names[int(u_idx)]].get('machine')
105 if not machine:
106 self.log.warning(
107 "Service:%s deploy-with unit missing machine %s",
108 svc.name, unit_names[unit_number])
109 return None
110 return self._format_placement(machine, container)
45111
46 def get_relations(self):112 def get_relations(self):
47 if 'relations' not in self.data:113 if 'relations' not in self.data:
@@ -117,6 +183,7 @@
117 self.load_overrides(cli_overides)183 self.load_overrides(cli_overides)
118 self.resolve_config()184 self.resolve_config()
119 self.validate_relations()185 self.validate_relations()
186 self.validate_placement()
120187
121 def load_overrides(self, cli_overrides=()):188 def load_overrides(self, cli_overrides=()):
122 """Load overrides."""189 """Load overrides."""
@@ -195,6 +262,49 @@
195 ep.service, "%s <-> %s" % (e_a, e_b))262 ep.service, "%s <-> %s" % (e_a, e_b))
196 raise ErrorExit()263 raise ErrorExit()
197264
265 def validate_placement(self):
266 services = dict([(s.name, s) for s in self.get_services()])
267 for name, s in services.items():
268 unit_placement = s.unit_placement
269 if unit_placement is None:
270 continue
271 if not isinstance(unit_placement, list):
272 unit_placement = [unit_placement]
273 unit_placement = map(str, unit_placement)
274 for idx, p in enumerate(unit_placement):
275 if ':' in p:
276 container, p = p.split(':')
277 if container not in ('lxc', 'kvm'):
278 self.log.error(
279 "Invalid service:%s placement: %s",
280 name, unit_placement[idx])
281 raise ErrorExit()
282 if '=' in p:
283 p, u_idx = p.split("=")
284 if not u_idx.isdigit():
285 self.log.error(
286 "Invalid service:%s placement: %s",
287 name, unit_placement[idx])
288 raise ErrorExit()
289 if p.isdigit() and p == '0':
290 continue
291 elif p.isdigit():
292 self.log.error(
293 "Service placement to machine not supported %s to %s",
294 name, unit_placement[idx])
295 raise ErrorExit()
296 elif p in services:
297 if services[p].unit_placement:
298 self.log.error(
299 "Nested placement not supported %s -> %s -> %s" % (
300 name, p, services[p].unit_placement))
301 raise ErrorExit()
302 else:
303 self.log.error(
304 "Invalid service placement %s to %s" % (
305 name, unit_placement[idx]))
306 raise ErrorExit()
307
198 def save(self, path):308 def save(self, path):
199 with open(path, "w") as fh:309 with open(path, "w") as fh:
200 fh.write(yaml_dump(self.data))310 fh.write(yaml_dump(self.data))
201311
=== modified file 'deployer/env/base.py'
--- deployer/env/base.py 2013-10-17 03:16:24 +0000
+++ deployer/env/base.py 2013-11-01 15:46:48 +0000
@@ -152,3 +152,6 @@
152 stderr=fh)152 stderr=fh)
153 status = yaml_load(output)153 status = yaml_load(output)
154 return status154 return status
155
156 def add_unit(self, service_name, machine_spec):
157 raise NotImplementedError()
155158
=== modified file 'deployer/env/go.py'
--- deployer/env/go.py 2013-10-08 20:13:39 +0000
+++ deployer/env/go.py 2013-11-01 15:46:48 +0000
@@ -5,7 +5,11 @@
5from .base import BaseEnvironment5from .base import BaseEnvironment
6from ..utils import ErrorExit6from ..utils import ErrorExit
77
8from jujuclient import Environment as EnvironmentClient, UnitErrors, EnvError8from jujuclient import (
9 Environment as EnvironmentClient,
10 UnitErrors,
11 EnvError,
12 WatchWrapper)
913
1014
11class GoEnvironment(BaseEnvironment):15class GoEnvironment(BaseEnvironment):
@@ -20,6 +24,9 @@
20 config = self._get_env_config()24 config = self._get_env_config()
21 return config['admin-secret']25 return config['admin-secret']
2226
27 def add_unit(self, service_name, machine_spec):
28 return self.client.add_unit(service_name, machine_spec)
29
23 def add_units(self, service_name, num_units):30 def add_units(self, service_name, num_units):
24 return self.client.add_units(service_name, num_units)31 return self.client.add_units(service_name, num_units)
2532
@@ -101,17 +108,50 @@
101 if len(status['machines']) == 1:108 if len(status['machines']) == 1:
102 return109 return
103110
104 for mid in status['machines'].keys():111 # containers before machines, container hosts post wait.
105 if mid == "0":112 machines = status['machines'].keys()
106 continue113
107 self.log.debug(" Terminating machine %s", mid)114 container_hosts = set()
108 self.terminate_machine(mid)115 containers = set()
116
117 def machine_sort(x, y):
118 for ctype in ('lxc', 'kvm'):
119 for m in (x, y):
120 if ctype in m:
121 container_hosts.add(m.split('/', 1)[0])
122 containers.add(m)
123 if m == x:
124 return -1
125 if m == y:
126 return 1
127 return cmp(x, y)
128
129 machines.sort(machine_sort)
130
131 for mid in machines:
132 self._terminate_machine(mid, container_hosts)
133
134 if containers:
135 watch = self.client.get_watch(120)
136 WaitForMachineTermination(
137 watch, containers).run(self._delta_event_log)
138
139 for mid in container_hosts:
140 self._terminate_machine(mid)
109141
110 if terminate_wait:142 if terminate_wait:
111 self.log.info(" Waiting for machine termination")143 self.log.info(" Waiting for machine termination")
112 callback = watch and self._delta_event_log or None144 callback = watch and self._delta_event_log or None
113 self.client.wait_for_no_machines(None, callback)145 self.client.wait_for_no_machines(None, callback)
114146
147 def _terminate_machine(self, mid, container_hosts=()):
148 if mid == "0":
149 return
150 if mid in container_hosts:
151 return
152 self.log.debug(" Terminating machine %s", mid)
153 self.terminate_machine(mid)
154
115 def _check_timeout(self, etime):155 def _check_timeout(self, etime):
116 w_timeout = etime - time.time()156 w_timeout = etime - time.time()
117 if w_timeout < 0:157 if w_timeout < 0:
@@ -211,3 +251,25 @@
211 eps[0]['Relation']['Name'],251 eps[0]['Relation']['Name'],
212 eps[1]['ServiceName'],252 eps[1]['ServiceName'],
213 eps[1]['Relation']['Name'])253 eps[1]['Relation']['Name'])
254
255
256class WaitForMachineTermination(WatchWrapper):
257
258 def __init__(self, watch, machines):
259 super(WaitForMachineTermination, self).__init__(watch)
260 self.machines = set(machines)
261 self.known = set()
262
263 def process(self, entity_type, change, data):
264 if entity_type != 'machine':
265 return
266 if change == 'remove' and data['Id'] in self.machines:
267 self.machines.remove(data['Id'])
268 else:
269 self.known.add(data['Id'])
270
271 def complete(self):
272 for m in self.machines:
273 if m in self.known:
274 return False
275 return True
214276
=== modified file 'deployer/service.py'
--- deployer/service.py 2013-09-13 13:36:22 +0000
+++ deployer/service.py 2013-11-01 15:46:48 +0000
@@ -4,6 +4,9 @@
4 self.svc_data = svc_data4 self.svc_data = svc_data
5 self.name = name5 self.name = name
66
7 def __repr__(self):
8 return "<Service %s>" % (self.name)
9
7 @property10 @property
8 def config(self):11 def config(self):
9 return self.svc_data.get('options', None)12 return self.svc_data.get('options', None)
@@ -17,9 +20,14 @@
17 return int(self.svc_data.get('num_units', 1))20 return int(self.svc_data.get('num_units', 1))
1821
19 @property22 @property
20 def force_machine(self):23 def unit_placement(self):
21 return self.svc_data.get('to') or self.svc_data.get(24 # Separate checks to support machine 0 placement.
22 'force-machine')25 value = self.svc_data.get('to')
26 if value is None:
27 value = self.svc_data.get('force-machine')
28 if value is not None and not isinstance(value, list):
29 value = [value]
30 return value or []
2331
24 @property32 @property
25 def expose(self):33 def expose(self):
2634
=== modified file 'deployer/tests/test_config.py'
--- deployer/tests/test_config.py 2013-10-10 21:18:38 +0000
+++ deployer/tests/test_config.py 2013-11-01 15:46:48 +0000
@@ -54,7 +54,6 @@
54 config.get('my-files-frontend-dev').get_services()]54 config.get('my-files-frontend-dev').get_services()]
55 self.assertTrue(set(wordpress).issubset(set(my_app)))55 self.assertTrue(set(wordpress).issubset(set(my_app)))
5656
57
58 def test_inherits_config_overridden(self):57 def test_inherits_config_overridden(self):
59 config = ConfigStack([58 config = ConfigStack([
60 os.path.join(self.test_data_dir, "stack-default.cfg"),59 os.path.join(self.test_data_dir, "stack-default.cfg"),
@@ -66,12 +65,12 @@
66 # over-ridden65 # over-ridden
67 self.assertEquals(db.config.get('tuning-level'), 'fastest')66 self.assertEquals(db.config.get('tuning-level'), 'fastest')
6867
69
70 def test_multi_inheritance_multi_files(self):68 def test_multi_inheritance_multi_files(self):
71 config = ConfigStack([69 config = ConfigStack([
72 os.path.join(self.test_data_dir, "openstack", "openstack.cfg"),70 os.path.join(self.test_data_dir, "openstack", "openstack.cfg"),
73 os.path.join(self.test_data_dir, "openstack", "ubuntu_base.cfg"),71 os.path.join(self.test_data_dir, "openstack", "ubuntu_base.cfg"),
74 os.path.join(self.test_data_dir, "openstack", "openstack_base.cfg"),72 os.path.join(
73 self.test_data_dir, "openstack", "openstack_base.cfg"),
75 ])74 ])
76 self._test_multiple_inheritance(config)75 self._test_multiple_inheritance(config)
7776
@@ -107,7 +106,7 @@
107106
108 deployment = config.get('precise-grizzly')107 deployment = config.get('precise-grizzly')
109 services = [s.name for s in list(deployment.get_services())]108 services = [s.name for s in list(deployment.get_services())]
110 self.assertEquals(['nova-cloud-controller', 'mysql'], services)109 self.assertEquals(['mysql', 'nova-cloud-controller'], services)
111110
112 nova = deployment.get_service('nova-cloud-controller')111 nova = deployment.get_service('nova-cloud-controller')
113 self.assertEquals(nova.config['openstack-origin'],112 self.assertEquals(nova.config['openstack-origin'],
@@ -116,8 +115,8 @@
116 deployment = config.get('precise-grizzly-quantum')115 deployment = config.get('precise-grizzly-quantum')
117 services = [s.name for s in list(deployment.get_services())]116 services = [s.name for s in list(deployment.get_services())]
118 self.assertEquals(services,117 self.assertEquals(services,
119 ['quantum-gateway', 'nova-cloud-controller',118 ['mysql', 'nova-cloud-controller',
120 'mysql'])119 'quantum-gateway'])
121 nova = deployment.get_service('nova-cloud-controller')120 nova = deployment.get_service('nova-cloud-controller')
122 self.assertEquals(nova.config['network-manager'], 'Quantum')121 self.assertEquals(nova.config['network-manager'], 'Quantum')
123 self.assertEquals(nova.config['openstack-origin'],122 self.assertEquals(nova.config['openstack-origin'],
124123
=== added file 'deployer/tests/test_data/stack-placement-invalid-2.yaml'
--- deployer/tests/test_data/stack-placement-invalid-2.yaml 1970-01-01 00:00:00 +0000
+++ deployer/tests/test_data/stack-placement-invalid-2.yaml 2013-11-01 15:46:48 +0000
@@ -0,0 +1,13 @@
1stack:
2 series: precise
3 services:
4 nova-compute:
5 charm: cs:precise/nova-compute
6 units: 3
7 ceph:
8 units: 3
9 to: [nova-compute, nova-compute, nova-compute]
10 mysql:
11 to: lxc:nova-compute
12 wordpress:
13 to: lxc:foobar
014
=== added file 'deployer/tests/test_data/stack-placement-invalid.yaml'
--- deployer/tests/test_data/stack-placement-invalid.yaml 1970-01-01 00:00:00 +0000
+++ deployer/tests/test_data/stack-placement-invalid.yaml 2013-11-01 15:46:48 +0000
@@ -0,0 +1,13 @@
1stack:
2 series: precise
3 services:
4 nova-compute:
5 charm: cs:precise/nova-compute
6 units: 3
7 ceph:
8 units: 3
9 to: [nova-compute, nova-compute, nova-compute]
10 mysql:
11 to: lxc:nova-compute
12 wordpress:
13 to: lxc:mysql
014
=== added file 'deployer/tests/test_data/stack-placement.yaml'
--- deployer/tests/test_data/stack-placement.yaml 1970-01-01 00:00:00 +0000
+++ deployer/tests/test_data/stack-placement.yaml 2013-11-01 15:46:48 +0000
@@ -0,0 +1,18 @@
1stack:
2 series: precise
3 services:
4 nova-compute:
5 charm: cs:precise/nova-compute
6 units: 3
7 ceph:
8 units: 3
9 to: [nova-compute, nova-compute]
10 mysql:
11 to: 0
12 quantum:
13 units: 4
14 to: ["lxc:nova-compute", "lxc:nova-compute", "lxc:nova-compute", "lxc:nova-compute"]
15 verity:
16 to: lxc:nova-compute=2
17 semper:
18 to: nova-compute=2
019
=== modified file 'deployer/tests/test_deployment.py'
--- deployer/tests/test_deployment.py 2013-07-24 23:10:15 +0000
+++ deployer/tests/test_deployment.py 2013-11-01 15:46:48 +0000
@@ -5,7 +5,7 @@
55
6from deployer.config import ConfigStack6from deployer.config import ConfigStack
7from deployer.deployment import Deployment7from deployer.deployment import Deployment
8from deployer.utils import setup_logging8from deployer.utils import setup_logging, ErrorExit
99
10from .base import Base10from .base import Base
1111
@@ -16,6 +16,11 @@
16 self.output = setup_logging(16 self.output = setup_logging(
17 debug=True, verbose=True, stream=StringIO.StringIO())17 debug=True, verbose=True, stream=StringIO.StringIO())
1818
19 def get_named_deployment(self, file_name, stack_name):
20 return ConfigStack(
21 [os.path.join(
22 self.test_data_dir, file_name)]).get(stack_name)
23
19 def test_deployer(self):24 def test_deployer(self):
20 d = ConfigStack(25 d = ConfigStack(
21 [os.path.join(26 [os.path.join(
@@ -51,6 +56,66 @@
51 list(d.get_relations()),56 list(d.get_relations()),
52 [('blog', 'db'), ('blog', 'cache'), ('blog', 'haproxy')])57 [('blog', 'db'), ('blog', 'cache'), ('blog', 'haproxy')])
5358
59 def test_validate_placement_sorting(self):
60 d = self.get_named_deployment("stack-placement.yaml", "stack")
61 services = d.get_services()
62 self.assertEqual(services[0].name, 'nova-compute')
63 try:
64 d.validate_placement()
65 except ErrorExit:
66 self.fail("Should not fail")
67
68 def test_validate_invalid_placement_nested(self):
69 d = self.get_named_deployment("stack-placement-invalid.yaml", "stack")
70 services = d.get_services()
71 self.assertEqual(services[0].name, 'nova-compute')
72 try:
73 d.validate_placement()
74 except ErrorExit:
75 pass
76 else:
77 self.fail("Should fail")
78
79 def test_validate_invalid_placement_no_with_service(self):
80 d = self.get_named_deployment(
81 "stack-placement-invalid-2.yaml", "stack")
82 services = d.get_services()
83 self.assertEqual(services[0].name, 'nova-compute')
84 try:
85 d.validate_placement()
86 except ErrorExit:
87 pass
88 else:
89 self.fail("Should fail")
90
91 def test_get_unit_placement(self):
92 d = self.get_named_deployment("stack-placement.yaml", "stack")
93 status = {
94 'services': {
95 'nova-compute': {
96 'units': {
97 'nova-compute/2': {'machine': '1'},
98 'nova-compute/3': {'machine': '2'},
99 'nova-compute/4': {'machine': '3'}}}}}
100 svc = d.get_service('ceph')
101 self.assertEqual(d.get_unit_placement(svc, 0, status), '1')
102 self.assertEqual(d.get_unit_placement(svc, 1, status), '2')
103 self.assertEqual(d.get_unit_placement(svc, 2, status), None)
104
105 svc = d.get_service('quantum')
106 self.assertEqual(d.get_unit_placement(svc, 0, status), 'lxc:1')
107 self.assertEqual(d.get_unit_placement(svc, 2, status), 'lxc:3')
108 self.assertEqual(d.get_unit_placement(svc, 3, status), None)
109
110 svc = d.get_service('verity')
111 self.assertEqual(d.get_unit_placement(svc, 0, status), 'lxc:3')
112
113 svc = d.get_service('mysql')
114 self.assertEqual(d.get_unit_placement(svc, 0, status), '0')
115
116 svc = d.get_service('semper')
117 self.assertEqual(d.get_unit_placement(svc, 0, status), '3')
118
54 def test_multiple_relations_no_weight(self):119 def test_multiple_relations_no_weight(self):
55 data = {"relations": {"wordpress": {"consumes": ["mysql"]},120 data = {"relations": {"wordpress": {"consumes": ["mysql"]},
56 "nginx": {"consumes": ["wordpress"]}}}121 "nginx": {"consumes": ["wordpress"]}}}
57122
=== modified file 'doc/config.rst'
--- doc/config.rst 2013-05-16 03:20:42 +0000
+++ doc/config.rst 2013-11-01 15:46:48 +0000
@@ -94,3 +94,57 @@
94 constraints: mem=1694 constraints: mem=16
95 options:95 options:
96 tuning: optimized96 tuning: optimized
97
98
99Placement
100=========
101
102Flexible unit placement can be specified via deployer. Primarily this is via
103specifying service units deployments alongside those of another service.
104
105Each unit's placement must be specified individually, absence of placement for
106a unit results in juju default behavior for the given constraints.
107
108One special placement form is machine placement, which only allowed to machine 0,
109as other machine identities are ambigious for most usage scenarios.
110
111Both container and hulk-smash placement are supported. Different
112containerization and deploy with services can be mixed.
113
114The deployed-with service must have enough units to hold # # Nested
115to: specifications are not supported (ie in the below wordpress can't
116# be deployed to mysql because mysql already specifies a 'to'
117placement)
118
119Example::
120 envExport:
121 services:
122 mysql:
123 # The only machine id supported is machine 0
124 to: 0
125 wordpress:
126 units: 3
127 redis-server:
128 units: 3
129 to: [lxc:wordpress, wordpress]
130 ceph:
131 units: 4
132 to: [wordpress, wordpress, wordpress, wordpress]
133 serenade:
134 to: lxc:wordpress=2
135
136In this case the first unit of redis-server is deployed in a container
137on wordpress/0 The second unit of redis-server is deployed hulk smash
138onto wordpress/1 The third unit of redis-server gets a full machine
139allocated to itself.
140
141For ceph, we deploy hulk smash of the first 3 units, the final unit doesn't
142have a corresponding unit of wordpress and is deployed (along with a console
143warning) to a separate machine per its default constraints.
144
145The serenade service is overriding the default deploy-with unit by
146explicitly specifying a unit index for the deployment. These are not
147unit id based but rather zero based offsets into a sorted list of
148units.
149
150
97151
=== modified file 'setup.py'
--- setup.py 2013-10-11 17:01:16 +0000
+++ setup.py 2013-11-01 15:46:48 +0000
@@ -12,7 +12,7 @@
12 author="Kapil Thangavelu",12 author="Kapil Thangavelu",
13 author_email="kapil.foss@gmail.com",13 author_email="kapil.foss@gmail.com",
14 url="http://launchpad.net/juju-deployer",14 url="http://launchpad.net/juju-deployer",
15 install_requires=["jujuclient >= 0.0.7"],15 install_requires=["jujuclient >= 0.13"],
16 packages=find_packages(),16 packages=find_packages(),
17 classifiers=[17 classifiers=[
18 "Development Status :: 2 - Pre-Alpha",18 "Development Status :: 2 - Pre-Alpha",

Subscribers

People subscribed via source and target branches