Merge lp:~chad.smith/charm-helpers/retry-add-apt-repository into lp:charm-helpers

Proposed by Chad Smith
Status: Merged
Merged at revision: 705
Proposed branch: lp:~chad.smith/charm-helpers/retry-add-apt-repository
Merge into: lp:charm-helpers
Diff against target: 230 lines (+91/-39)
2 files modified
charmhelpers/fetch/ubuntu.py (+51/-31)
tests/fetch/test_fetch.py (+40/-8)
To merge this branch: bzr merge lp:~chad.smith/charm-helpers/retry-add-apt-repository
Reviewer Review Type Date Requested Status
Eric Snow (community) Approve
David Britton (community) Approve
Review via email: mp+318951@code.launchpad.net

Description of the change

Add 3 retries and logging messages to add-apt-repository attempts.

This branch involves some slight refactoring of _run_apt_command so that the common code _retry_command() can be reused by both _run_apt_command and add_source.

Note this branch also includes the local environment now in add-apt-repository calls which should also help in environments with proxy settings present.

To post a comment you must log in.
708. By Chad Smith

comment fix

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

Small nits inline. Code tests OK, lints OK. Thanks for the contribution. +1 with the small fixes addressed!

review: Approve
709. By Chad Smith

- rename -APT_NO_LOCK_RETRY_COUNT -> CMD_RETRY_COUNT
- Retry add-apt-repository CMD_RETRY_COUNT (30) times instead of 3
- update unit tests

710. By Chad Smith

lints

Revision history for this message
Eric Snow (ericsnowcurrently) :
review: Needs Fixing
Revision history for this message
Chad Smith (chad.smith) :
711. By Chad Smith

address review comments, sensible defaults, drop fatal from from _run_with_retries to simplify logic

Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Eric Snow (ericsnowcurrently) wrote :

Thanks! That's exactly how I was thinking it should look. :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmhelpers/fetch/ubuntu.py'
--- charmhelpers/fetch/ubuntu.py 2016-11-14 16:28:24 +0000
+++ charmhelpers/fetch/ubuntu.py 2017-03-03 20:38:43 +0000
@@ -116,8 +116,8 @@
116}116}
117117
118APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.118APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
119APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.119CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
120APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.120CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times.
121121
122122
123def filter_installed_packages(packages):123def filter_installed_packages(packages):
@@ -249,7 +249,8 @@
249 source.startswith('http') or249 source.startswith('http') or
250 source.startswith('deb ') or250 source.startswith('deb ') or
251 source.startswith('cloud-archive:')):251 source.startswith('cloud-archive:')):
252 subprocess.check_call(['add-apt-repository', '--yes', source])252 cmd = ['add-apt-repository', '--yes', source]
253 _run_with_retries(cmd)
253 elif source.startswith('cloud:'):254 elif source.startswith('cloud:'):
254 install(filter_installed_packages(['ubuntu-cloud-keyring']),255 install(filter_installed_packages(['ubuntu-cloud-keyring']),
255 fatal=True)256 fatal=True)
@@ -286,41 +287,60 @@
286 key])287 key])
287288
288289
290def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
291 retry_message="", cmd_env=None):
292 """Run a command and retry until success or max_retries is reached.
293
294 :param: cmd: str: The apt command to run.
295 :param: max_retries: int: The number of retries to attempt on a fatal
296 command. Defaults to CMD_RETRY_COUNT.
297 :param: retry_exitcodes: tuple: Optional additional exit codes to retry.
298 Defaults to retry on exit code 1.
299 :param: retry_message: str: Optional log prefix emitted during retries.
300 :param: cmd_env: dict: Environment variables to add to the command run.
301 """
302
303 env = os.environ.copy()
304 if cmd_env:
305 env.update(cmd_env)
306
307 if not retry_message:
308 retry_message = "Failed executing '{}'".format(" ".join(cmd))
309 retry_message += ". Will retry in {} seconds".format(CMD_RETRY_DELAY)
310
311 retry_count = 0
312 result = None
313
314 retry_results = (None,) + retry_exitcodes
315 while result in retry_results:
316 try:
317 result = subprocess.check_call(cmd, env=env)
318 except subprocess.CalledProcessError as e:
319 retry_count = retry_count + 1
320 if retry_count > max_retries:
321 raise
322 result = e.returncode
323 log(retry_message)
324 time.sleep(CMD_RETRY_DELAY)
325
326
289def _run_apt_command(cmd, fatal=False):327def _run_apt_command(cmd, fatal=False):
290 """Run an APT command.328 """Run an apt command with optional retries.
291329
292 Checks the output and retries if the fatal flag is set
293 to True.
294
295 :param: cmd: str: The apt command to run.
296 :param: fatal: bool: Whether the command's output should be checked and330 :param: fatal: bool: Whether the command's output should be checked and
297 retried.331 retried.
298 """332 """
299 env = os.environ.copy()333 # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment.
300334 cmd_env = {
301 if 'DEBIAN_FRONTEND' not in env:335 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')}
302 env['DEBIAN_FRONTEND'] = 'noninteractive'
303336
304 if fatal:337 if fatal:
305 retry_count = 0338 _run_with_retries(
306 result = None339 cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,),
307340 retry_message="Couldn't acquire DPKG lock")
308 # If the command is considered "fatal", we need to retry if the apt
309 # lock was not acquired.
310
311 while result is None or result == APT_NO_LOCK:
312 try:
313 result = subprocess.check_call(cmd, env=env)
314 except subprocess.CalledProcessError as e:
315 retry_count = retry_count + 1
316 if retry_count > APT_NO_LOCK_RETRY_COUNT:
317 raise
318 result = e.returncode
319 log("Couldn't acquire DPKG lock. Will retry in {} seconds."
320 "".format(APT_NO_LOCK_RETRY_DELAY))
321 time.sleep(APT_NO_LOCK_RETRY_DELAY)
322
323 else:341 else:
342 env = os.environ.copy()
343 env.update(cmd_env)
324 subprocess.call(cmd, env=env)344 subprocess.call(cmd, env=env)
325345
326346
327347
=== modified file 'tests/fetch/test_fetch.py'
--- tests/fetch/test_fetch.py 2016-09-20 17:07:51 +0000
+++ tests/fetch/test_fetch.py 2017-03-03 20:38:43 +0000
@@ -192,7 +192,35 @@
192 source = "ppa:test-ppa"192 source = "ppa:test-ppa"
193 fetch.add_source(source=source)193 fetch.add_source(source=source)
194 check_call.assert_called_with(194 check_call.assert_called_with(
195 ['add-apt-repository', '--yes', source])195 ['add-apt-repository', '--yes', source], env=getenv())
196
197 @patch("charmhelpers.fetch.ubuntu.log")
198 @patch.object(osplatform, 'get_platform')
199 @patch('subprocess.check_call')
200 @patch('time.sleep')
201 def test_add_source_ppa_retries_30_times(self, sleep, check_call,
202 platform, log):
203 platform.return_value = 'ubuntu'
204 imp.reload(fetch)
205
206 self.call_count = 0
207
208 def side_effect(*args, **kwargs):
209 """Raise an 3 times, then return 0 """
210 self.call_count += 1
211 if self.call_count <= fetch.ubuntu.CMD_RETRY_COUNT:
212 raise subprocess.CalledProcessError(
213 returncode=1, cmd="some add-apt-repository command")
214 else:
215 return 0
216 check_call.side_effect = side_effect
217
218 source = "ppa:test-ppa"
219 fetch.add_source(source=source)
220 check_call.assert_called_with(
221 ['add-apt-repository', '--yes', source], env=getenv())
222 sleep.assert_called_with(10)
223 self.assertTrue(fetch.ubuntu.CMD_RETRY_COUNT, sleep.call_count)
196224
197 @patch('charmhelpers.fetch.ubuntu.log')225 @patch('charmhelpers.fetch.ubuntu.log')
198 @patch.object(osplatform, 'get_platform')226 @patch.object(osplatform, 'get_platform')
@@ -204,7 +232,7 @@
204 source = "http://archive.ubuntu.com/ubuntu raring-backports main"232 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
205 fetch.add_source(source=source)233 fetch.add_source(source=source)
206 check_call.assert_called_with(234 check_call.assert_called_with(
207 ['add-apt-repository', '--yes', source])235 ['add-apt-repository', '--yes', source], env=getenv())
208236
209 @patch('charmhelpers.fetch.centos.log')237 @patch('charmhelpers.fetch.centos.log')
210 @patch.object(osplatform, 'get_platform')238 @patch.object(osplatform, 'get_platform')
@@ -233,7 +261,7 @@
233 source = "https://example.com"261 source = "https://example.com"
234 fetch.add_source(source=source)262 fetch.add_source(source=source)
235 check_call.assert_called_with(263 check_call.assert_called_with(
236 ['add-apt-repository', '--yes', source])264 ['add-apt-repository', '--yes', source], env=getenv())
237265
238 @patch('charmhelpers.fetch.ubuntu.log')266 @patch('charmhelpers.fetch.ubuntu.log')
239 @patch.object(osplatform, 'get_platform')267 @patch.object(osplatform, 'get_platform')
@@ -261,7 +289,7 @@
261 source = "deb http://archive.ubuntu.com/ubuntu raring-backports main"289 source = "deb http://archive.ubuntu.com/ubuntu raring-backports main"
262 fetch.add_source(source=source)290 fetch.add_source(source=source)
263 check_call.assert_called_with(291 check_call.assert_called_with(
264 ['add-apt-repository', '--yes', source])292 ['add-apt-repository', '--yes', source], env=getenv())
265293
266 @patch.object(osplatform, 'get_platform')294 @patch.object(osplatform, 'get_platform')
267 @patch.object(fetch.ubuntu, 'filter_installed_packages')295 @patch.object(fetch.ubuntu, 'filter_installed_packages')
@@ -357,9 +385,10 @@
357385
358 source = "http://archive.ubuntu.com/ubuntu raring-backports main"386 source = "http://archive.ubuntu.com/ubuntu raring-backports main"
359 key_id = "akey"387 key_id = "akey"
388 check_call.return_value = 0 # Successful exit code
360 fetch.add_source(source=source, key=key_id)389 fetch.add_source(source=source, key=key_id)
361 check_call.assert_has_calls([390 check_call.assert_has_calls([
362 call(['add-apt-repository', '--yes', source]),391 call(['add-apt-repository', '--yes', source], env=getenv()),
363 call(['apt-key', 'adv', '--keyserver',392 call(['apt-key', 'adv', '--keyserver',
364 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])393 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])
365 ])394 ])
@@ -390,6 +419,7 @@
390 @patch('subprocess.check_call')419 @patch('subprocess.check_call')
391 def test_add_source_https_and_key_id_ubuntu(self, check_call,420 def test_add_source_https_and_key_id_ubuntu(self, check_call,
392 platform, log):421 platform, log):
422 check_call.return_value = 0 # Success from both calls
393 platform.return_value = 'ubuntu'423 platform.return_value = 'ubuntu'
394 imp.reload(fetch)424 imp.reload(fetch)
395425
@@ -397,7 +427,7 @@
397 key_id = "GPGPGP"427 key_id = "GPGPGP"
398 fetch.add_source(source=source, key=key_id)428 fetch.add_source(source=source, key=key_id)
399 check_call.assert_has_calls([429 check_call.assert_has_calls([
400 call(['add-apt-repository', '--yes', source]),430 call(['add-apt-repository', '--yes', source], env=getenv()),
401 call(['apt-key', 'adv', '--keyserver',431 call(['apt-key', 'adv', '--keyserver',
402 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])432 'hkp://keyserver.ubuntu.com:80', '--recv', key_id])
403 ])433 ])
@@ -442,16 +472,18 @@
442 received_args = []472 received_args = []
443 received_key = StringIO()473 received_key = StringIO()
444474
445 def _check_call(arg, stdin=None):475 def _check_call(arg, stdin=None, env=None):
446 '''side_effect to store the stdin passed to check_call process.'''476 '''side_effect to store the stdin passed to check_call process.'''
447 if stdin is not None:477 if stdin is not None:
448 received_args.extend(arg)478 received_args.extend(arg)
449 received_key.write(stdin.read())479 received_key.write(stdin.read())
480 return 0 # Successful return code checked by add-apt-repository
450481
451 with patch('subprocess.check_call',482 with patch('subprocess.check_call',
452 side_effect=_check_call) as check_call:483 side_effect=_check_call) as check_call:
453 fetch.add_source(source=source, key=key)484 fetch.add_source(source=source, key=key)
454 check_call.assert_any_call(['add-apt-repository', '--yes', source])485 check_call.assert_any_call(
486 ['add-apt-repository', '--yes', source], env=getenv())
455 self.assertEqual(['apt-key', 'add', '-'], received_args)487 self.assertEqual(['apt-key', 'add', '-'], received_args)
456 self.assertEqual(key, received_key.getvalue())488 self.assertEqual(key, received_key.getvalue())
457489

Subscribers

People subscribed via source and target branches