Merge lp:~gandelman-a/juju-deployer/retry_cli into lp:~hazmat/juju-deployer/refactor

Proposed by Adam Gandelman
Status: Merged
Merged at revision: 111
Proposed branch: lp:~gandelman-a/juju-deployer/retry_cli
Merge into: lp:~hazmat/juju-deployer/refactor
Diff against target: 257 lines (+70/-21)
8 files modified
deployer/action/importer.py (+1/-1)
deployer/cli.py (+4/-4)
deployer/env/__init__.py (+4/-3)
deployer/env/base.py (+10/-3)
deployer/env/go.py (+2/-1)
deployer/env/py.py (+8/-7)
deployer/tests/test_utils.py (+32/-1)
deployer/utils.py (+9/-1)
To merge this branch: bzr merge lp:~gandelman-a/juju-deployer/retry_cli
Reviewer Review Type Date Requested Status
Kapil Thangavelu Pending
Review via email: mp+177724@code.launchpad.net

Description of the change

Allows for failed CLI commands to be optionally retried. We seem to hit transient provider errors with a long running MAAS environments, and the previous deployer plowed on thru via retries. Shares the same retry_count used when resolving unit errors.

To post a comment you must log in.
111. By Adam Gandelman

Fix AttributeError in original retry_count usage.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'deployer/action/importer.py'
2--- deployer/action/importer.py 2013-07-22 15:29:31 +0000
3+++ deployer/action/importer.py 2013-07-31 01:56:27 +0000
4@@ -163,5 +163,5 @@
5 if self.options.retry_count:
6 self.log.info("Looking for errors to auto-retry")
7 self.env.resolve_errors(
8- self.retry_count,
9+ self.options.retry_count,
10 self.options.timeout - time.time() - self.start_time)
11
12=== modified file 'deployer/cli.py'
13--- deployer/cli.py 2013-07-23 20:33:37 +0000
14+++ deployer/cli.py 2013-07-31 01:56:27 +0000
15@@ -89,12 +89,12 @@
16 dest="watch", action="store_true", default=False)
17 parser.add_argument(
18 '-r', "--retry", default=0, type=int, dest="retry_count",
19- help=("Resolve unit errors via retry."
20+ help=("Resolve CLI and unit errors via number of retries (default: 0)."
21 " Either standalone or in a deployment"))
22 parser.add_argument(
23 "--diff", action="store_true", default=False,
24- help=("Resolve unit errors via retry."
25- " Either standalone or in a deployment"))
26+ help=("Generate a delta between a configured deployment and a running"
27+ " environment."))
28 parser.add_argument(
29 '-w', '--relation-wait', action='store', dest='rel_wait',
30 default=60, type=int,
31@@ -127,7 +127,7 @@
32 log = logging.getLogger("deployer.cli")
33 start_time = time.time()
34
35- env = select_runtime(options.juju_env)
36+ env = select_runtime(options.juju_env, options)
37 log.debug('Using runtime %s', env.__class__.__name__)
38
39 config = ConfigStack(options.configs or [])
40
41=== modified file 'deployer/env/__init__.py'
42--- deployer/env/__init__.py 2013-07-22 15:29:31 +0000
43+++ deployer/env/__init__.py 2013-07-31 01:56:27 +0000
44@@ -4,9 +4,10 @@
45 from ..utils import _check_call
46
47
48-def select_runtime(env_name):
49+def select_runtime(name, options):
50 # pyjuju does juju --version
51 result = _check_call(["juju", "version"], None, ignoreerr=True)
52 if result is None:
53- return PyEnvironment(env_name)
54- return GoEnvironment(env_name)
55+ return PyEnvironment(name, options)
56+ return GoEnvironment(name, options)
57+
58
59=== modified file 'deployer/env/base.py'
60--- deployer/env/base.py 2013-07-22 15:29:31 +0000
61+++ deployer/env/base.py 2013-07-31 01:56:27 +0000
62@@ -15,6 +15,11 @@
63 env_config_path = path_join(jhome, 'environments.yaml')
64 return env_config_path
65
66+ def _check_call(self, *args, **kwargs):
67+ if self.options and self.options.retry_count:
68+ kwargs['max_retry'] = self.options.retry_count
69+ return _check_call(*args, **kwargs)
70+
71 def _named_env(self, params):
72 if self.name:
73 params.extend(["-e", self.name])
74@@ -80,7 +85,8 @@
75 params.extend["--force-machine=%d" % force_machine]
76
77 params.extend([charm_url, name])
78- _check_call(params, self.log, "Error deploying service %r", name)
79+ self._check_call(
80+ params, self.log, "Error deploying service %r", name)
81
82 def terminate_machine(self, mid, wait=False):
83 """Terminate a machine.
84@@ -95,7 +101,8 @@
85 params = self._named_env(["juju", "terminate-machine"])
86 params.append(mid)
87 try:
88- _check_call(params, self.log, "Error terminating machine %r" % mid)
89+ self._check_call(
90+ params, self.log, "Error terminating machine %r" % mid)
91 except ErrorExit, e:
92 if ("machine %s does not exist" % mid) in e.error.output:
93 return
94@@ -115,7 +122,7 @@
95 def get_cli_status(self):
96 params = self._named_env(["juju", "status"])
97 with open('/dev/null', 'w') as fh:
98- output = _check_call(
99+ output = self._check_call(
100 params, self.log, "Error getting status, is it bootstrapped?",
101 stderr=fh)
102 status = yaml_load(output)
103
104=== modified file 'deployer/env/go.py'
105--- deployer/env/go.py 2013-07-22 15:29:31 +0000
106+++ deployer/env/go.py 2013-07-31 01:56:27 +0000
107@@ -10,8 +10,9 @@
108
109 class GoEnvironment(BaseEnvironment):
110
111- def __init__(self, name, endpoint=None):
112+ def __init__(self, name, options=None, endpoint=None):
113 self.name = name
114+ self.options = options
115 self.api_endpoint = endpoint
116 self.client = None
117
118
119=== modified file 'deployer/env/py.py'
120--- deployer/env/py.py 2013-07-22 15:29:31 +0000
121+++ deployer/env/py.py 2013-07-31 01:56:27 +0000
122@@ -1,28 +1,29 @@
123 import time
124
125 from deployer.errors import UnitErrors
126-from deployer.utils import _check_call, ErrorExit
127+from deployer.utils import ErrorExit
128
129 from .base import BaseEnvironment
130
131
132 class PyEnvironment(BaseEnvironment):
133
134- def __init__(self, name):
135+ def __init__(self, name, options=None):
136 self.name = name
137+ self.options = options
138
139 def add_units(self, service_name, num_units):
140 params = self._named_env(["juju", "add-unit"])
141 if num_units > 1:
142 params.extend(["-n", str(num_units)])
143 params.append(service_name)
144- _check_call(
145+ self._check_call(
146 params, self.log, "Error adding units to %s", service_name)
147
148 def add_relation(self, endpoint_a, endpoint_b):
149 params = self._named_env(["juju", "add-relation"])
150 params.extend([endpoint_a, endpoint_b])
151- _check_call(
152+ self._check_call(
153 params, self.log, "Error adding relation to %s %s",
154 endpoint_a, endpoint_b)
155
156@@ -35,19 +36,19 @@
157 def _destroy_service(self, service_name):
158 params = self._named_env(["juju", "destroy-service"])
159 params.append(service_name)
160- _check_call(
161+ self._check_call(
162 params, self.log, "Error destroying service %s" % service_name)
163
164 def get_config(self, svc_name):
165 params = self._named_env(["juju", "get"])
166 params.append(svc_name)
167- return _check_call(
168+ return self._check_call(
169 params, self.log, "Error retrieving config for %r", svc_name)
170
171 def get_constraints(self, svc_name):
172 params = self._named_env(["juju", "get-constraints"])
173 params.append(svc_name)
174- return _check_call(
175+ return self._check_call(
176 params, self.log, "Error retrieving constraints for %r", svc_name)
177
178 def reset(self,
179
180=== modified file 'deployer/tests/test_utils.py'
181--- deployer/tests/test_utils.py 2013-07-22 15:29:31 +0000
182+++ deployer/tests/test_utils.py 2013-07-31 01:56:27 +0000
183@@ -1,5 +1,7 @@
184+from mock import patch, MagicMock
185+from subprocess import CalledProcessError
186 from .base import Base
187-from deployer.utils import dict_merge
188+from deployer.utils import dict_merge, _check_call, ErrorExit
189
190
191 class UtilTests(Base):
192@@ -17,3 +19,32 @@
193 {'a': 1},
194 {'relations': [['m1', 'x1'], ['m2', 'x2']]}),
195 {'a': 1, 'relations': [['m1', 'x1'], ['m2', 'x2']]})
196+
197+ @patch('subprocess.check_output')
198+ def test_check_call_fail_no_retry(self, check_output):
199+ _e = CalledProcessError(returncode=1, cmd=['fail'])
200+ check_output.side_effect = _e
201+ self.assertRaises(
202+ ErrorExit, _check_call, params=['fail'], log=MagicMock())
203+
204+ @patch('time.sleep')
205+ @patch('subprocess.check_output')
206+ def test_check_call_fail_retry(self, check_output, sleep):
207+ _e = CalledProcessError(returncode=1, cmd=['fail'])
208+ check_output.side_effect = _e
209+ self.assertRaises(
210+ ErrorExit, _check_call, params=['fail'], log=MagicMock(), max_retry=3)
211+ # 1 failure + 3 retries
212+ self.assertEquals(len(check_output.call_args_list), 4)
213+
214+ @patch('time.sleep')
215+ @patch('subprocess.check_output')
216+ def test_check_call_succeed_after_retry(self, check_output, sleep):
217+ # call succeeds after the 3rd try
218+ _e = CalledProcessError(returncode=1, cmd=['maybe_fail'])
219+ check_output.side_effect = [
220+ _e, _e, 'good', _e ]
221+ output = _check_call(params=['magybe_fail'], log=MagicMock(), max_retry=3)
222+ self.assertEquals(output, 'good')
223+ # 1 failure + 3 retries
224+ self.assertEquals(len(check_output.call_args_list), 3)
225
226=== modified file 'deployer/utils.py'
227--- deployer/utils.py 2013-07-22 15:29:31 +0000
228+++ deployer/utils.py 2013-07-31 01:56:27 +0000
229@@ -11,6 +11,7 @@
230
231 import stat
232 import subprocess
233+import time
234 import tempfile
235 import zipfile
236
237@@ -150,6 +151,8 @@
238
239
240 def _check_call(params, log, *args, **kw):
241+ max_retry = kw.get('max_retry', None)
242+ cur = kw.get('cur_try', 1)
243 try:
244 cwd = abspath(".")
245 if 'cwd' in kw:
246@@ -169,7 +172,12 @@
247 #print e.output
248 log.error(*args)
249 log.error("Command (%s) Output:\n\n %s", " ".join(params), e.output)
250- raise ErrorExit(e)
251+ if not max_retry or cur > max_retry:
252+ raise ErrorExit(e)
253+ kw['cur_try'] = cur + 1
254+ log.error("Retrying (%s of %s)" % (cur, max_retry))
255+ time.sleep(1)
256+ output = _check_call(params, log, args, **kw)
257 return output
258
259

Subscribers

People subscribed via source and target branches