Merge lp:~raharper/curtin/trunk.add-gpg-retry into lp:~curtin-dev/curtin/trunk

Proposed by Ryan Harper
Status: Merged
Merged at revision: 445
Proposed branch: lp:~raharper/curtin/trunk.add-gpg-retry
Merge into: lp:~curtin-dev/curtin/trunk
Diff against target: 249 lines (+172/-7)
5 files modified
curtin/commands/apt_config.py (+2/-1)
curtin/gpg.py (+4/-4)
curtin/util.py (+3/-0)
tests/unittests/test_apt_source.py (+4/-2)
tests/unittests/test_gpg.py (+159/-0)
To merge this branch: bzr merge lp:~raharper/curtin/trunk.add-gpg-retry
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser (community) Approve
Review via email: mp+316264@code.launchpad.net

Description of the change

gpg: retry when recv'ing gpg keys fail

Retry gpg recv operations in case error is transient.
Add unittests to validate curtin.gpg including retry behavior

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
446. By Ryan Harper

Don't hardcode retries value in gpg module.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Ryan, this looks good.
Some comments in line, possibly me just misunderstanding.

Revision history for this message
Ryan Harper (raharper) wrote :
Download full text (8.2 KiB)

On Thu, Feb 2, 2017 at 8:27 PM, Scott Moser <email address hidden> wrote:

> Ryan, this looks good.
> Some comments in line, possibly me just misunderstanding.
>
>
> Diff comments:
>
> >
> > === added file 'tests/unittests/test_gpg.py'
> > --- tests/unittests/test_gpg.py 1970-01-01 00:00:00 +0000
> > +++ tests/unittests/test_gpg.py 2017-02-02 22:52:32 +0000
> > @@ -0,0 +1,174 @@
> > +from unittest import TestCase
> > +from mock import call, patch
> > +import textwrap
> > +
> > +from curtin import gpg
> > +from curtin import util
> > +
> > +
> > +class TestCurtinGpg(TestCase):
> > +
> > + @patch('curtin.util.subp')
> > + def test_export_armour(self, mock_subp):
> > + key = 'DEADBEEF'
> > + expected_armour = textwrap.dedent("""
> > + -----BEGIN PGP PUBLIC KEY BLOCK-----
> > + Version: GnuPG v1
> > +
> > + deadbeef
> > + -----END PGP PUBLIC KEY BLOCK----
> > + """)
> > + mock_subp.side_effect = iter([(expected_armour, "")])
>
> what is the second side affect for ? the ""?
> I dont think subp should be called twice for this, is it ?
>

subp usually returns an (out, err) tuple; It's a habit in case we need to
do something with the error in the calling code.

>
> > +
> > + armour = gpg.export_armour(key)
> > + mock_subp.assert_called_with(["gpg", "--export", "--armour",
> key],
> > + capture=True)
> > + self.assertEqual(expected_armour, armour)
> > +
> > + @patch('curtin.util.subp')
> > + def test_export_armour_missingkey(self, mock_subp):
> > + key = 'DEADBEEF'
> > + mock_subp.side_effect = iter([util.ProcessExecutionError()])
> > +
> > + expected_armour = gpg.export_armour(key)
> > + mock_subp.assert_called_with(["gpg", "--export", "--armour",
> key],
> > + capture=True)
> > + self.assertEqual(None, expected_armour)
> > +
> > + @patch('curtin.util.subp')
> > + def test_recv_key(self, mock_subp):
> > + key = 'DEADBEEF'
> > + keyserver = 'keyserver.ubuntu.com'
> > + mock_subp.side_effect = iter([("", "")])
> > +
> > + gpg.recv_key(key, keyserver, )
>
> calling here is wierd with the following ,
>
> And again, why the 2 side_affects ? shouldn't subp just be called once?
>

It's a single side-effect with a tuple return.

the iter() is for python mock on trusty which doesn't convert the list of
side-effects into an
iterable directly.

>
> > + mock_subp.assert_called_with(["gpg", "--keyserver", keyserver,
> > + "--recv", key], capture=True,
> > + retries=None)
> > +
> > + @patch('time.sleep')
> > + @patch('curtin.util._subp')
> > + def test_recv_key_retry_raises(self, mock_under_subp, mock_sleep):
> > + key = 'DEADBEEF'
> > + keyserver = 'keyserver.ubuntu.com'
> > + retries = (1, 2, 5, 10)
> > + mock_under_subp.side_effect = iter([
> > + util.ProcessExecutionError(), # 1
> > + util.ProcessExecutionError(), # 2
> > + util.ProcessExecutionError(), # 3
> > + ...

Read more...

Revision history for this message
Scott Moser (smoser) wrote :

> > what is the second side affect for ? the ""?
> > I dont think subp should be called twice for this, is it ?
> >
>
> subp usually returns an (out, err) tuple; It's a habit in case we need to
> do something with the error in the calling code.

Yeah, i figured i was missing something. You are correct. Sorry for the noise.

> > > + gpg.recv_key(key, keyserver, )
> >
> > calling here is wierd with the following ,
> >
> > And again, why the 2 side_affects ? shouldn't subp just be called once?
> >
>
> It's a single side-effect with a tuple return.
>
> the iter() is for python mock on trusty which doesn't convert the list of
> side-effects into an
> iterable directly.

the subp comment was noise as mentioned above.
But why:
 gpg.recv_key(key, keyserver, )
and not
 gpg.recv_key(key, keyserver)

Just looks wierd.

> > you dont' use key in this test.
> >
>
> ? it's passed to gpg.recv_key) and checked in the expected_calls.

Not my best review. sorry :) Again, you are correct.

Revision history for this message
Scott Moser (smoser) wrote :

go ahead and cleanup the repetitive arrays with the * operator, and then i'm good with this.
Thanks.

review: Approve
447. By Ryan Harper

Unittest cleanup after review

- Use list expansion for repeated calls
- Remove extra comma when calling gpg.recv_key

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'curtin/commands/apt_config.py'
--- curtin/commands/apt_config.py 2016-12-01 07:12:04 +0000
+++ curtin/commands/apt_config.py 2017-02-03 16:06:45 +0000
@@ -362,7 +362,8 @@
362 if 'keyserver' in ent:362 if 'keyserver' in ent:
363 keyserver = ent['keyserver']363 keyserver = ent['keyserver']
364364
365 ent['key'] = gpg.getkeybyid(ent['keyid'], keyserver)365 ent['key'] = gpg.getkeybyid(ent['keyid'], keyserver,
366 retries=(1, 2, 5, 10))
366367
367 if 'key' in ent:368 if 'key' in ent:
368 add_apt_key_raw(ent['key'], target)369 add_apt_key_raw(ent['key'], target)
369370
=== modified file 'curtin/gpg.py'
--- curtin/gpg.py 2016-06-23 15:04:21 +0000
+++ curtin/gpg.py 2017-02-03 16:06:45 +0000
@@ -36,12 +36,12 @@
36 return armour36 return armour
3737
3838
39def recv_key(key, keyserver):39def recv_key(key, keyserver, retries=None):
40 """Receive gpg key from the specified keyserver"""40 """Receive gpg key from the specified keyserver"""
41 LOG.debug('Receive gpg key "%s"', key)41 LOG.debug('Receive gpg key "%s"', key)
42 try:42 try:
43 util.subp(["gpg", "--keyserver", keyserver, "--recv", key],43 util.subp(["gpg", "--keyserver", keyserver, "--recv", key],
44 capture=True)44 capture=True, retries=retries)
45 except util.ProcessExecutionError as error:45 except util.ProcessExecutionError as error:
46 raise ValueError(('Failed to import key "%s" '46 raise ValueError(('Failed to import key "%s" '
47 'from server "%s" - error %s') %47 'from server "%s" - error %s') %
@@ -57,12 +57,12 @@
57 LOG.warn('Failed delete key "%s": %s', key, error)57 LOG.warn('Failed delete key "%s": %s', key, error)
5858
5959
60def getkeybyid(keyid, keyserver='keyserver.ubuntu.com'):60def getkeybyid(keyid, keyserver='keyserver.ubuntu.com', retries=None):
61 """get gpg keyid from keyserver"""61 """get gpg keyid from keyserver"""
62 armour = export_armour(keyid)62 armour = export_armour(keyid)
63 if not armour:63 if not armour:
64 try:64 try:
65 recv_key(keyid, keyserver=keyserver)65 recv_key(keyid, keyserver=keyserver, retries=retries)
66 armour = export_armour(keyid)66 armour = export_armour(keyid)
67 except ValueError:67 except ValueError:
68 LOG.exception('Failed to obtain gpg key %s', keyid)68 LOG.exception('Failed to obtain gpg key %s', keyid)
6969
=== modified file 'curtin/util.py'
--- curtin/util.py 2017-02-01 03:18:21 +0000
+++ curtin/util.py 2017-02-03 16:06:45 +0000
@@ -155,6 +155,9 @@
155 retries = []155 retries = []
156 if "retries" in kwargs:156 if "retries" in kwargs:
157 retries = kwargs.pop("retries")157 retries = kwargs.pop("retries")
158 if not retries:
159 # allow retries=None
160 retries = []
158161
159 if args:162 if args:
160 cmd = args[0]163 cmd = args[0]
161164
=== modified file 'tests/unittests/test_apt_source.py'
--- tests/unittests/test_apt_source.py 2016-11-14 22:55:12 +0000
+++ tests/unittests/test_apt_source.py 2017-02-03 16:06:45 +0000
@@ -364,7 +364,8 @@
364 keycfg = cfg[self.aptlistfile]364 keycfg = cfg[self.aptlistfile]
365 mockgetkey.assert_called_with(keycfg['keyid'],365 mockgetkey.assert_called_with(keycfg['keyid'],
366 keycfg.get('keyserver',366 keycfg.get('keyserver',
367 'keyserver.ubuntu.com'))367 'keyserver.ubuntu.com'),
368 retries=(1, 2, 5, 10))
368 mockkey.assert_called_with(expectedkey, TARGET)369 mockkey.assert_called_with(expectedkey, TARGET)
369370
370 # filename should be ignored on key only371 # filename should be ignored on key only
@@ -407,7 +408,8 @@
407 self._add_apt_sources(cfg, TARGET, template_params=params,408 self._add_apt_sources(cfg, TARGET, template_params=params,
408 aa_repo_match=self.matcher)409 aa_repo_match=self.matcher)
409410
410 mockgetkey.assert_called_with('03683F77', 'test.random.com')411 mockgetkey.assert_called_with('03683F77', 'test.random.com',
412 retries=(1, 2, 5, 10))
411 mockadd.assert_called_with('fakekey', TARGET)413 mockadd.assert_called_with('fakekey', TARGET)
412414
413 # filename should be ignored on key only415 # filename should be ignored on key only
414416
=== added file 'tests/unittests/test_gpg.py'
--- tests/unittests/test_gpg.py 1970-01-01 00:00:00 +0000
+++ tests/unittests/test_gpg.py 2017-02-03 16:06:45 +0000
@@ -0,0 +1,159 @@
1from unittest import TestCase
2from mock import call, patch
3import textwrap
4
5from curtin import gpg
6from curtin import util
7
8
9class TestCurtinGpg(TestCase):
10
11 @patch('curtin.util.subp')
12 def test_export_armour(self, mock_subp):
13 key = 'DEADBEEF'
14 expected_armour = textwrap.dedent("""
15 -----BEGIN PGP PUBLIC KEY BLOCK-----
16 Version: GnuPG v1
17
18 deadbeef
19 -----END PGP PUBLIC KEY BLOCK----
20 """)
21 mock_subp.side_effect = iter([(expected_armour, "")])
22
23 armour = gpg.export_armour(key)
24 mock_subp.assert_called_with(["gpg", "--export", "--armour", key],
25 capture=True)
26 self.assertEqual(expected_armour, armour)
27
28 @patch('curtin.util.subp')
29 def test_export_armour_missingkey(self, mock_subp):
30 key = 'DEADBEEF'
31 mock_subp.side_effect = iter([util.ProcessExecutionError()])
32
33 expected_armour = gpg.export_armour(key)
34 mock_subp.assert_called_with(["gpg", "--export", "--armour", key],
35 capture=True)
36 self.assertEqual(None, expected_armour)
37
38 @patch('curtin.util.subp')
39 def test_recv_key(self, mock_subp):
40 key = 'DEADBEEF'
41 keyserver = 'keyserver.ubuntu.com'
42 mock_subp.side_effect = iter([("", "")])
43
44 gpg.recv_key(key, keyserver)
45 mock_subp.assert_called_with(["gpg", "--keyserver", keyserver,
46 "--recv", key], capture=True,
47 retries=None)
48
49 @patch('time.sleep')
50 @patch('curtin.util._subp')
51 def test_recv_key_retry_raises(self, mock_under_subp, mock_sleep):
52 key = 'DEADBEEF'
53 keyserver = 'keyserver.ubuntu.com'
54 retries = (1, 2, 5, 10)
55 nr_calls = 5
56 mock_under_subp.side_effect = iter([
57 util.ProcessExecutionError()] * nr_calls)
58
59 with self.assertRaises(ValueError):
60 gpg.recv_key(key, keyserver, retries=retries)
61
62 print("_subp calls: %s" % mock_under_subp.call_args_list)
63 print("sleep calls: %s" % mock_sleep.call_args_list)
64 expected_calls = nr_calls * [
65 call(["gpg", "--keyserver", keyserver, "--recv", key],
66 capture=True)]
67 mock_under_subp.assert_has_calls(expected_calls)
68
69 expected_calls = [call(1), call(2), call(5), call(10)]
70 mock_sleep.assert_has_calls(expected_calls)
71
72 @patch('time.sleep')
73 @patch('curtin.util._subp')
74 def test_recv_key_retry_works(self, mock_under_subp, mock_sleep):
75 key = 'DEADBEEF'
76 keyserver = 'keyserver.ubuntu.com'
77 nr_calls = 2
78 mock_under_subp.side_effect = iter([
79 util.ProcessExecutionError(), # 1
80 ("", ""),
81 ])
82
83 gpg.recv_key(key, keyserver, retries=[1])
84
85 print("_subp calls: %s" % mock_under_subp.call_args_list)
86 print("sleep calls: %s" % mock_sleep.call_args_list)
87 expected_calls = nr_calls * [
88 call(["gpg", "--keyserver", keyserver, "--recv", key],
89 capture=True)]
90 mock_under_subp.assert_has_calls(expected_calls)
91 mock_sleep.assert_has_calls([call(1)])
92
93 @patch('curtin.util.subp')
94 def test_delete_key(self, mock_subp):
95 key = 'DEADBEEF'
96 mock_subp.side_effect = iter([("", "")])
97
98 gpg.delete_key(key)
99 mock_subp.assert_called_with(["gpg", "--batch", "--yes",
100 "--delete-keys", key], capture=True)
101
102 @patch('curtin.gpg.delete_key')
103 @patch('curtin.gpg.recv_key')
104 @patch('curtin.gpg.export_armour')
105 def test_getkeybyid(self, mock_export, mock_recv, mock_del):
106 key = 'DEADBEEF'
107 keyserver = 'my.keyserver.xyz.co.uk'
108
109 mock_export.side_effect = iter([
110 None,
111 "-----BEGIN PGP PUBLIC KEY BLOCK-----",
112 ])
113
114 gpg.getkeybyid(key, keyserver=keyserver)
115
116 mock_export.assert_has_calls([call(key), call(key)])
117 mock_recv.assert_has_calls([
118 call(key, keyserver=keyserver, retries=None)])
119 mock_del.assert_has_calls([call(key)])
120
121 @patch('curtin.gpg.delete_key')
122 @patch('curtin.gpg.recv_key')
123 @patch('curtin.gpg.export_armour')
124 def test_getkeybyid_exists(self, mock_export, mock_recv, mock_del):
125 key = 'DEADBEEF'
126
127 mock_export.side_effect = iter([
128 "-----BEGIN PGP PUBLIC KEY BLOCK-----",
129 ])
130
131 gpg.getkeybyid(key)
132
133 mock_export.assert_has_calls([call(key)])
134 self.assertEqual([], mock_recv.call_args_list)
135 self.assertEqual([], mock_del.call_args_list)
136
137 @patch('curtin.gpg.delete_key')
138 @patch('curtin.gpg.recv_key')
139 @patch('curtin.gpg.export_armour')
140 def test_getkeybyid_raises(self, mock_export, mock_recv, mock_del):
141 key = 'DEADBEEF'
142 keyserver = 'my.keyserver.xyz.co.uk'
143
144 mock_export.side_effect = iter([
145 None,
146 "-----BEGIN PGP PUBLIC KEY BLOCK-----",
147 ])
148 mock_recv.side_effect = iter([
149 ValueError("Failed to import key %s from server %s" %
150 (key, keyserver)),
151 ])
152
153 with self.assertRaises(ValueError):
154 gpg.getkeybyid(key, keyserver=keyserver)
155
156 mock_export.assert_has_calls([call(key)])
157 mock_recv.assert_has_calls([
158 call(key, keyserver=keyserver, retries=None)])
159 mock_del.assert_has_calls([call(key)])

Subscribers

People subscribed via source and target branches