Merge lp:~hazmat/juju-deployer/refactor-placement-and-validate-feedback into lp:juju-deployer

Proposed by Kapil Thangavelu
Status: Merged
Merged at revision: 91
Proposed branch: lp:~hazmat/juju-deployer/refactor-placement-and-validate-feedback
Merge into: lp:juju-deployer
Diff against target: 485 lines (+214/-129)
6 files modified
deployer/action/importer.py (+5/-4)
deployer/deployment.py (+38/-102)
deployer/feedback.py (+35/-0)
deployer/service.py (+110/-0)
deployer/tests/base.py (+8/-0)
deployer/tests/test_deployment.py (+18/-23)
To merge this branch: bzr merge lp:~hazmat/juju-deployer/refactor-placement-and-validate-feedback
Reviewer Review Type Date Requested Status
Francesco Banconi (community) Approve
Review via email: mp+195903@code.launchpad.net

Description of the change

refactor of unit placement and deployment validation/feedback.

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

The following summarizes what discussed on IRC.

Currently the guiserver starts the deployer in a separate process using a concurrent.futures.ProcessPoolExecutor. The executor calls the validate and import_bundle functions in deployer.guiserver. This means that, as the branch is now, the log messages will appear in the guiserver logs, and that's good. But what we'd also need is something that the future returned by the executor can receive, i.e.
1) a picklable return value or 2) an picklable exception (that is what we do now).
Long story short, it would be nice to have that kind of feedback implemented as an exception itself, or returned by the functions we call (validate/import_bundle).

Some background:
From the guiserver perspective, in case of errors, we need some error info to sent back to the user, and this can be stored in future.result() or future.exception() (right now the latter is used, i.e. validate() and import_bundle() just return None or raise an exception_.
Currently sometimes the code path started by those functions just raises exceptions without messages. In those cases, we don't have feedback to send back to the guiserver client (usually the GUI).

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

just to be clear the branch atm is still backwards compatible with the previous gui (and other) usages, validate and others still raise ErrorExit, just that it now provides a place to cleanly override the handling of feedback handling on deployment._handle_feedback while retaining the defaults (log and error) and that it collects whole sets of errors instead of erroring on the first during validation.

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

This branch looks good Kapil, thank you!
I'll just add some minors/comments below.

105 + def get_unit_placement(self, svc, status):
106 + if isinstance(svc, (str, unicode)):

This can also be written as isinstance(svc, basestring) <shrug>.

229 + def _handle_feedback(self, feedback):

So, this can be a plan:
after this branch is merged, I'll start working on another
one that modifies the guiserver module in the following ways:
- _validate raises an error if if any units is in error (the deployer will
  refuse to proceed anyway, and there is no reason for the
  guiserver to wait for that);
- import_bundle instantiates a Deployment subclass overriding
  _handle_feedback so that an error is raised if the feedback
  has errors.
How does it sound?

336 + if p.isdigit() and p == '0':
337 + continue

This could just be "if p == '0'" <shrug>.

372 + if placement.isdigit() and placement == "0":
373 + return self._format_placement(placement, container)

The same here.

382 + # Prefer continuing deployment with a new machine rather
383 + # than an in-progress abort.
384 + return None

Nice.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'deployer/action/importer.py'
--- deployer/action/importer.py 2013-11-12 04:54:58 +0000
+++ deployer/action/importer.py 2013-11-20 05:09:11 +0000
@@ -48,10 +48,9 @@
48 env_status = self.env.status()48 env_status = self.env.status()
49 reloaded = True49 reloaded = True
5050
51 placement = self.deployment.get_unit_placement(svc, env_status)
51 for mid in range(cur_units, svc.num_units):52 for mid in range(cur_units, svc.num_units):
52 mspec = self.deployment.get_unit_placement(53 self.env.add_unit(svc.name, placement.get(mid))
53 svc, mid, env_status)
54 self.env.add_unit(svc.name, mspec)
55 else:54 else:
56 self.env.add_units(svc.name, abs(delta))55 self.env.add_units(svc.name, abs(delta))
5756
@@ -94,6 +93,8 @@
94 else:93 else:
95 num_units = svc.num_units94 num_units = svc.num_units
9695
96 placement = self.deployment.get_unit_placement(svc, env_status)
97
97 self.env.deploy(98 self.env.deploy(
98 svc.name,99 svc.name,
99 charm.charm_url,100 charm.charm_url,
@@ -101,7 +102,7 @@
101 svc.config,102 svc.config,
102 svc.constraints,103 svc.constraints,
103 num_units,104 num_units,
104 self.deployment.get_unit_placement(svc, 0, env_status))105 placement.get(0))
105106
106 if svc.annotations:107 if svc.annotations:
107 self.log.debug(" Setting annotations")108 self.log.debug(" Setting annotations")
108109
=== modified file 'deployer/deployment.py'
--- deployer/deployment.py 2013-10-31 19:19:36 +0000
+++ deployer/deployment.py 2013-11-20 05:09:11 +0000
@@ -6,7 +6,8 @@
6import yaml6import yaml
77
8from .charm import Charm8from .charm import Charm
9from .service import Service9from .feedback import Feedback
10from .service import Service, ServiceUnitPlacement
10from .relation import Endpoint11from .relation import Endpoint
11from .utils import path_join, yaml_dump, ErrorExit, resolve_include12from .utils import path_join, yaml_dump, ErrorExit, resolve_include
1213
@@ -56,58 +57,10 @@
56 return -157 return -1
57 return cmp(svc_a.name, svc_b.name)58 return cmp(svc_a.name, svc_b.name)
5859
59 @staticmethod60 def get_unit_placement(self, svc, status):
60 def _format_placement(machine, container=None):61 if isinstance(svc, (str, unicode)):
61 if container:62 svc = self.get_service(svc)
62 return "%s:%s" % (container, machine)63 return ServiceUnitPlacement(svc, self, status)
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)
11164
112 def get_relations(self):65 def get_relations(self):
113 if 'relations' not in self.data:66 if 'relations' not in self.data:
@@ -213,6 +166,7 @@
213 self.log.debug("Resolving configuration")166 self.log.debug("Resolving configuration")
214 # XXX TODO, rename resolve, validate relations167 # XXX TODO, rename resolve, validate relations
215 # against defined services168 # against defined services
169 feedback = Feedback()
216 for svc_name, svc_data in self.data.get('services', {}).items():170 for svc_name, svc_data in self.data.get('services', {}).items():
217 if not 'options' in svc_data:171 if not 'options' in svc_data:
218 continue172 continue
@@ -222,16 +176,21 @@
222176
223 for k, v in svc_data['options'].items():177 for k, v in svc_data['options'].items():
224 if not k in config:178 if not k in config:
225 self.log.error(179 feedback.error(
226 "Invalid config charm %s %s=%s", charm.name, k, v)180 "Invalid config charm %s %s=%s" % (charm.name, k, v))
227 raise ErrorExit()181 continue
228 iv = self._resolve_include(svc_name, k, v)182 iv = self._resolve_include(svc_name, k, v)
183 if isinstance(iv, Feedback):
184 feedback.extend(iv)
185 continue
229 if iv is not None:186 if iv is not None:
230 v = iv187 v = iv
231 options[k] = v188 options[k] = v
232 svc_data['options'] = options189 svc_data['options'] = options
190 self._handle_feedback(feedback)
233191
234 def _resolve_include(self, svc_name, k, v):192 def _resolve_include(self, svc_name, k, v):
193 feedback = Feedback()
235 for include_type in ["file", "base64"]:194 for include_type in ["file", "base64"]:
236 if (not isinstance(v, basestring)195 if (not isinstance(v, basestring)
237 or not v.startswith(196 or not v.startswith(
@@ -240,70 +199,47 @@
240 include, fname = v.split("://", 1)199 include, fname = v.split("://", 1)
241 ip = resolve_include(fname, self.include_dirs)200 ip = resolve_include(fname, self.include_dirs)
242 if ip is None:201 if ip is None:
243 self.log.warning(202 feedback.error(
244 "Invalid config %s.%s include not found %s",203 "Invalid config %s.%s include not found %s" % (
245 svc_name, k, v)204 svc_name, k, v))
246 continue205 continue
247 with open(ip) as fh:206 with open(ip) as fh:
248 v = fh.read()207 v = fh.read()
249 if include_type == "base64":208 if include_type == "base64":
250 v = b64encode(v)209 v = b64encode(v)
251 return v210 return v
211 if feedback:
212 return feedback
252213
253 def validate_relations(self):214 def validate_relations(self):
254 # Could extend to do interface matching against charms.215 # Could extend to do interface matching against charms.
255 services = dict([(s.name, s) for s in self.get_services()])216 services = dict([(s.name, s) for s in self.get_services()])
217 feedback = Feedback()
256 for e_a, e_b in self.get_relations():218 for e_a, e_b in self.get_relations():
257 for ep in [Endpoint(e_a), Endpoint(e_b)]:219 for ep in [Endpoint(e_a), Endpoint(e_b)]:
258 if not ep.service in services:220 if not ep.service in services:
259 self.log.error(221 feedback.error(
260 ("Invalid relation in config,"222 ("Invalid relation in config,"
261 " service %s not found, rel %s"),223 " service %s not found, rel %s") % (
262 ep.service, "%s <-> %s" % (e_a, e_b))224 ep.service, "%s <-> %s" % (e_a, e_b)))
263 raise ErrorExit()225 continue
226 self._handle_feedback(feedback)
264227
265 def validate_placement(self):228 def validate_placement(self):
266 services = dict([(s.name, s) for s in self.get_services()])229 services = dict([(s.name, s) for s in self.get_services()])
230 feedback = Feedback()
267 for name, s in services.items():231 for name, s in services.items():
268 unit_placement = s.unit_placement232 placement = self.get_unit_placement(s, {})
269 if unit_placement is None:233 feedback.extend(placement.validate())
270 continue234 self._handle_feedback(feedback)
271 if not isinstance(unit_placement, list):235
272 unit_placement = [unit_placement]236 def _handle_feedback(self, feedback):
273 unit_placement = map(str, unit_placement)237 for e in feedback.get_errors():
274 for idx, p in enumerate(unit_placement):238 self.log.error(e)
275 if ':' in p:239 for w in feedback.get_warnings():
276 container, p = p.split(':')240 self.log.warning(w)
277 if container not in ('lxc', 'kvm'):241 if feedback.has_errors:
278 self.log.error(242 raise ErrorExit()
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()
307243
308 def save(self, path):244 def save(self, path):
309 with open(path, "w") as fh:245 with open(path, "w") as fh:
310246
=== added file 'deployer/feedback.py'
--- deployer/feedback.py 1970-01-01 00:00:00 +0000
+++ deployer/feedback.py 2013-11-20 05:09:11 +0000
@@ -0,0 +1,35 @@
1
2
3WARN = 3
4ERROR = 7
5
6
7class Feedback(object):
8
9 def __init__(self):
10 self.messages = []
11 self.has_errors = False
12
13 def error(self, msg):
14 self.messages.append((ERROR, msg))
15 self.has_errors = True
16
17 def warn(self, msg):
18 self.messages.append((WARN, msg))
19
20 def __iter__(self):
21 return iter(self.messages)
22
23 def __nonzero__(self):
24 return bool(self.messages)
25
26 def get_errors(self):
27 return [m for (m_kind, m) in self.messages if m_kind == ERROR]
28
29 def get_warnings(self):
30 return [m for (m_kind, m) in self.messages if m_kind == WARN]
31
32 def extend(self, other):
33 self.messages.extend(other.messages)
34 if not self.has_errors and other.has_errors:
35 self.has_errors = True
036
=== modified file 'deployer/service.py'
--- deployer/service.py 2013-11-20 02:28:01 +0000
+++ deployer/service.py 2013-11-20 05:09:11 +0000
@@ -1,3 +1,6 @@
1from feedback import Feedback
2
3
1class Service(object):4class Service(object):
25
3 def __init__(self, name, svc_data):6 def __init__(self, name, svc_data):
@@ -43,3 +46,110 @@
43 @property46 @property
44 def expose(self):47 def expose(self):
45 return self.svc_data.get('expose', False)48 return self.svc_data.get('expose', False)
49
50
51class ServiceUnitPlacement(object):
52
53 def __init__(self, service, deployment, status):
54 self.service = service
55 self.deployment = deployment
56 self.status = status
57
58 @staticmethod
59 def _format_placement(machine, container=None):
60 if container:
61 return "%s:%s" % (container, machine)
62 else:
63 return machine
64
65 def validate(self):
66 feedback = Feedback()
67
68 unit_placement = self.service.unit_placement
69 if unit_placement is None:
70 return feedback
71
72 if not isinstance(unit_placement, list):
73 unit_placement = [unit_placement]
74 unit_placement = map(str, unit_placement)
75
76 services = dict([(s.name, s) for s in self.deployment.get_services()])
77
78 for idx, p in enumerate(unit_placement):
79 if ':' in p:
80 container, p = p.split(':')
81 if container not in ('lxc', 'kvm'):
82 feedback.error(
83 "Invalid service:%s placement: %s" % (
84 self.service.name, unit_placement[idx]))
85 if '=' in p:
86 p, u_idx = p.split("=")
87 if not u_idx.isdigit():
88 feedback.error(
89 "Invalid service:%s placement: %s",
90 self.service.name, unit_placement[idx])
91 if p.isdigit() and p == '0':
92 continue
93 elif p.isdigit():
94 feedback.error(
95 "Service placement to machine not supported %s to %s",
96 self.service.name, unit_placement[idx])
97 elif p in services:
98 if services[p].unit_placement:
99 feedback.error(
100 "Nested placement not supported %s -> %s -> %s" % (
101 self.service.name, p, services[p].unit_placement))
102 else:
103 feedback.error(
104 "Invalid service placement %s to %s" % (
105 self.service.name, unit_placement[idx]))
106 return feedback
107
108 def get(self, unit_number):
109 status = self.status
110 svc = self.service
111
112 unit_mapping = svc.unit_placement
113 if not unit_mapping:
114 return None
115 if len(unit_mapping) <= unit_number:
116 return None
117
118 unit_placement = placement = str(unit_mapping[unit_number])
119 container = None
120 u_idx = unit_number
121
122 if ':' in unit_placement:
123 container, placement = unit_placement.split(":")
124 if '=' in placement:
125 placement, u_idx = placement.split("=")
126
127 if placement.isdigit() and placement == "0":
128 return self._format_placement(placement, container)
129
130 with_service = status['services'].get(placement)
131 if with_service is None:
132 # Should be caught in validate relations but sanity check
133 # for concurrency.
134 self.deployment.log.error(
135 "Service %s to be deployed with non existant service %s",
136 svc.name, placement)
137 # Prefer continuing deployment with a new machine rather
138 # than an in-progress abort.
139 return None
140
141 svc_units = with_service['units']
142 if len(svc_units) <= unit_number:
143 self.deployment.log.warning(
144 "Service:%s deploy-with Service:%s, but no with unit found",
145 svc.name, placement)
146 return None
147 unit_names = svc_units.keys()
148 unit_names.sort()
149 machine = svc_units[unit_names[int(u_idx)]].get('machine')
150 if not machine:
151 self.deployment.log.warning(
152 "Service:%s deploy-with unit missing machine %s",
153 svc.name, unit_names[unit_number])
154 return None
155 return self._format_placement(machine, container)
46156
=== modified file 'deployer/tests/base.py'
--- deployer/tests/base.py 2013-07-22 18:26:06 +0000
+++ deployer/tests/base.py 2013-11-20 05:09:11 +0000
@@ -7,6 +7,7 @@
7import tempfile7import tempfile
88
9import deployer9import deployer
10from deployer.config import ConfigStack
1011
1112
12class Base(unittest.TestCase):13class Base(unittest.TestCase):
@@ -14,6 +15,13 @@
14 test_data_dir = os.path.join(15 test_data_dir = os.path.join(
15 os.path.dirname(inspect.getabsfile(deployer)), "tests", "test_data")16 os.path.dirname(inspect.getabsfile(deployer)), "tests", "test_data")
1617
18 def get_named_deployment(self, file_name, stack_name):
19 """ Get deployment from test_data file.
20 """
21 return ConfigStack(
22 [os.path.join(
23 self.test_data_dir, file_name)]).get(stack_name)
24
17 def capture_logging(self, name="", level=logging.INFO,25 def capture_logging(self, name="", level=logging.INFO,
18 log_file=None, formatter=None):26 log_file=None, formatter=None):
19 if log_file is None:27 if log_file is None:
2028
=== modified file 'deployer/tests/test_deployment.py'
--- deployer/tests/test_deployment.py 2013-10-31 19:19:36 +0000
+++ deployer/tests/test_deployment.py 2013-11-20 05:09:11 +0000
@@ -16,11 +16,6 @@
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
24 def test_deployer(self):19 def test_deployer(self):
25 d = ConfigStack(20 d = ConfigStack(
26 [os.path.join(21 [os.path.join(
@@ -97,24 +92,24 @@
97 'nova-compute/2': {'machine': '1'},92 'nova-compute/2': {'machine': '1'},
98 'nova-compute/3': {'machine': '2'},93 'nova-compute/3': {'machine': '2'},
99 'nova-compute/4': {'machine': '3'}}}}}94 'nova-compute/4': {'machine': '3'}}}}}
100 svc = d.get_service('ceph')95 placement = d.get_unit_placement('ceph', status)
101 self.assertEqual(d.get_unit_placement(svc, 0, status), '1')96 self.assertEqual(placement.get(0), '1')
102 self.assertEqual(d.get_unit_placement(svc, 1, status), '2')97 self.assertEqual(placement.get(1), '2')
103 self.assertEqual(d.get_unit_placement(svc, 2, status), None)98 self.assertEqual(placement.get(2), None)
10499
105 svc = d.get_service('quantum')100 placement = d.get_unit_placement('quantum', status)
106 self.assertEqual(d.get_unit_placement(svc, 0, status), 'lxc:1')101 self.assertEqual(placement.get(0), 'lxc:1')
107 self.assertEqual(d.get_unit_placement(svc, 2, status), 'lxc:3')102 self.assertEqual(placement.get(2), 'lxc:3')
108 self.assertEqual(d.get_unit_placement(svc, 3, status), None)103 self.assertEqual(placement.get(3), None)
109104
110 svc = d.get_service('verity')105 placement = d.get_unit_placement('verity', status)
111 self.assertEqual(d.get_unit_placement(svc, 0, status), 'lxc:3')106 self.assertEqual(placement.get(0), 'lxc:3')
112107
113 svc = d.get_service('mysql')108 placement = d.get_unit_placement('mysql', status)
114 self.assertEqual(d.get_unit_placement(svc, 0, status), '0')109 self.assertEqual(placement.get(0), '0')
115110
116 svc = d.get_service('semper')111 placement = d.get_unit_placement('semper', status)
117 self.assertEqual(d.get_unit_placement(svc, 0, status), '3')112 self.assertEqual(placement.get(0), '3')
118113
119 def test_multiple_relations_no_weight(self):114 def test_multiple_relations_no_weight(self):
120 data = {"relations": {"wordpress": {"consumes": ["mysql"]},115 data = {"relations": {"wordpress": {"consumes": ["mysql"]},

Subscribers

People subscribed via source and target branches