Gentoo Archives: gentoo-portage-dev

From: "Michał Górny" <mgorny@g.o>
To: gentoo-portage-dev@l.g.o
Cc: "Michał Górny" <mgorny@g.o>
Subject: [gentoo-portage-dev] [PATCH 4/5] rsync: Load and update keys early
Date: Thu, 01 Feb 2018 12:17:44
Message-Id: 20180201121707.8623-5-mgorny@gentoo.org
In Reply to: [gentoo-portage-dev] [PATCH] rsync: Improve gemato rsync Manifest verification logic by "Michał Górny"
1 Load and update keys early to avoid delaying failures post rsync. Any
2 failure will prevent verification from happening, and presumably most of
3 the users will prefer fixing it and trying to sync again. For that case,
4 it is better to perform the task before actual rsync to avoid
5 unnecessarily rsyncing twice.
6 ---
7 pym/portage/sync/modules/rsync/rsync.py | 416 +++++++++++++++++---------------
8 1 file changed, 215 insertions(+), 201 deletions(-)
9
10 diff --git a/pym/portage/sync/modules/rsync/rsync.py b/pym/portage/sync/modules/rsync/rsync.py
11 index b1faf1ec9..b7758c2c3 100644
12 --- a/pym/portage/sync/modules/rsync/rsync.py
13 +++ b/pym/portage/sync/modules/rsync/rsync.py
14 @@ -110,211 +110,222 @@ class RsyncSync(NewBase):
15 level=logging.WARNING, noiselevel=-1)
16 self.verify_jobs = None
17
18 - # Real local timestamp file.
19 - self.servertimestampfile = os.path.join(
20 - self.repo.location, "metadata", "timestamp.chk")
21 -
22 - content = portage.util.grabfile(self.servertimestampfile)
23 - timestamp = 0
24 - if content:
25 - try:
26 - timestamp = time.mktime(time.strptime(content[0],
27 - TIMESTAMP_FORMAT))
28 - except (OverflowError, ValueError):
29 - pass
30 - del content
31 -
32 - try:
33 - self.rsync_initial_timeout = \
34 - int(self.settings.get("PORTAGE_RSYNC_INITIAL_TIMEOUT", "15"))
35 - except ValueError:
36 - self.rsync_initial_timeout = 15
37 + openpgp_env = None
38 + if self.verify_metamanifest and gemato is not None:
39 + # Use isolated environment if key is specified,
40 + # system environment otherwise
41 + if self.repo.openpgp_key_path is not None:
42 + openpgp_env = gemato.openpgp.OpenPGPEnvironment()
43 + else:
44 + openpgp_env = gemato.openpgp.OpenPGPSystemEnvironment()
45
46 try:
47 - maxretries=int(self.settings["PORTAGE_RSYNC_RETRIES"])
48 - except SystemExit as e:
49 - raise # Needed else can't exit
50 - except:
51 - maxretries = -1 #default number of retries
52 -
53 - if syncuri.startswith("file://"):
54 - self.proto = "file"
55 - dosyncuri = syncuri[7:]
56 - unchanged, is_synced, exitcode, updatecache_flg = self._do_rsync(
57 - dosyncuri, timestamp, opts)
58 - self._process_exitcode(exitcode, dosyncuri, out, 1)
59 - return (exitcode, updatecache_flg)
60 + # Load and update the keyring early. If it fails, then verification
61 + # will not be performed and the user will have to fix it and try again,
62 + # so we may as well bail out before actual rsync happens.
63 + if openpgp_env is not None and self.repo.openpgp_key_path is not None:
64 + try:
65 + out.einfo('Using keys from %s' % (self.repo.openpgp_key_path,))
66 + with io.open(self.repo.openpgp_key_path, 'rb') as f:
67 + openpgp_env.import_key(f)
68 + out.ebegin('Refreshing keys from keyserver')
69 + openpgp_env.refresh_keys()
70 + out.eend(0)
71 + except Exception as e:
72 + writemsg_level("!!! Manifest verification impossible due to keyring problem:\n%s\n"
73 + % (e,),
74 + level=logging.ERROR, noiselevel=-1)
75 + return (1, False)
76
77 - retries=0
78 - try:
79 - self.proto, user_name, hostname, port = re.split(
80 - r"(rsync|ssh)://([^:/]+@)?(\[[:\da-fA-F]*\]|[^:/]*)(:[0-9]+)?",
81 - syncuri, maxsplit=4)[1:5]
82 - except ValueError:
83 - writemsg_level("!!! sync-uri is invalid: %s\n" % syncuri,
84 - noiselevel=-1, level=logging.ERROR)
85 - return (1, False)
86 + # Real local timestamp file.
87 + self.servertimestampfile = os.path.join(
88 + self.repo.location, "metadata", "timestamp.chk")
89
90 - self.ssh_opts = self.settings.get("PORTAGE_SSH_OPTS")
91 + content = portage.util.grabfile(self.servertimestampfile)
92 + timestamp = 0
93 + if content:
94 + try:
95 + timestamp = time.mktime(time.strptime(content[0],
96 + TIMESTAMP_FORMAT))
97 + except (OverflowError, ValueError):
98 + pass
99 + del content
100
101 - if port is None:
102 - port=""
103 - if user_name is None:
104 - user_name=""
105 - if re.match(r"^\[[:\da-fA-F]*\]$", hostname) is None:
106 - getaddrinfo_host = hostname
107 - else:
108 - # getaddrinfo needs the brackets stripped
109 - getaddrinfo_host = hostname[1:-1]
110 - updatecache_flg = False
111 - all_rsync_opts = set(self.rsync_opts)
112 - all_rsync_opts.update(self.extra_rsync_opts)
113 + try:
114 + self.rsync_initial_timeout = \
115 + int(self.settings.get("PORTAGE_RSYNC_INITIAL_TIMEOUT", "15"))
116 + except ValueError:
117 + self.rsync_initial_timeout = 15
118
119 - family = socket.AF_UNSPEC
120 - if "-4" in all_rsync_opts or "--ipv4" in all_rsync_opts:
121 - family = socket.AF_INET
122 - elif socket.has_ipv6 and \
123 - ("-6" in all_rsync_opts or "--ipv6" in all_rsync_opts):
124 - family = socket.AF_INET6
125 + try:
126 + maxretries=int(self.settings["PORTAGE_RSYNC_RETRIES"])
127 + except SystemExit as e:
128 + raise # Needed else can't exit
129 + except:
130 + maxretries = -1 #default number of retries
131 +
132 + if syncuri.startswith("file://"):
133 + self.proto = "file"
134 + dosyncuri = syncuri[7:]
135 + unchanged, is_synced, exitcode, updatecache_flg = self._do_rsync(
136 + dosyncuri, timestamp, opts)
137 + self._process_exitcode(exitcode, dosyncuri, out, 1)
138 + return (exitcode, updatecache_flg)
139 +
140 + retries=0
141 + try:
142 + self.proto, user_name, hostname, port = re.split(
143 + r"(rsync|ssh)://([^:/]+@)?(\[[:\da-fA-F]*\]|[^:/]*)(:[0-9]+)?",
144 + syncuri, maxsplit=4)[1:5]
145 + except ValueError:
146 + writemsg_level("!!! sync-uri is invalid: %s\n" % syncuri,
147 + noiselevel=-1, level=logging.ERROR)
148 + return (1, False)
149
150 - addrinfos = None
151 - uris = []
152 + self.ssh_opts = self.settings.get("PORTAGE_SSH_OPTS")
153
154 - try:
155 - addrinfos = getaddrinfo_validate(
156 - socket.getaddrinfo(getaddrinfo_host, None,
157 - family, socket.SOCK_STREAM))
158 - except socket.error as e:
159 - writemsg_level(
160 - "!!! getaddrinfo failed for '%s': %s\n"
161 - % (_unicode_decode(hostname), _unicode(e)),
162 - noiselevel=-1, level=logging.ERROR)
163 -
164 - if addrinfos:
165 -
166 - AF_INET = socket.AF_INET
167 - AF_INET6 = None
168 - if socket.has_ipv6:
169 - AF_INET6 = socket.AF_INET6
170 -
171 - ips_v4 = []
172 - ips_v6 = []
173 -
174 - for addrinfo in addrinfos:
175 - if addrinfo[0] == AF_INET:
176 - ips_v4.append("%s" % addrinfo[4][0])
177 - elif AF_INET6 is not None and addrinfo[0] == AF_INET6:
178 - # IPv6 addresses need to be enclosed in square brackets
179 - ips_v6.append("[%s]" % addrinfo[4][0])
180 -
181 - random.shuffle(ips_v4)
182 - random.shuffle(ips_v6)
183 -
184 - # Give priority to the address family that
185 - # getaddrinfo() returned first.
186 - if AF_INET6 is not None and addrinfos and \
187 - addrinfos[0][0] == AF_INET6:
188 - ips = ips_v6 + ips_v4
189 - else:
190 - ips = ips_v4 + ips_v6
191 -
192 - for ip in ips:
193 - uris.append(syncuri.replace(
194 - "//" + user_name + hostname + port + "/",
195 - "//" + user_name + ip + port + "/", 1))
196 -
197 - if not uris:
198 - # With some configurations we need to use the plain hostname
199 - # rather than try to resolve the ip addresses (bug #340817).
200 - uris.append(syncuri)
201 -
202 - # reverse, for use with pop()
203 - uris.reverse()
204 - uris_orig = uris[:]
205 -
206 - effective_maxretries = maxretries
207 - if effective_maxretries < 0:
208 - effective_maxretries = len(uris) - 1
209 -
210 - local_state_unchanged = True
211 - while (1):
212 - if uris:
213 - dosyncuri = uris.pop()
214 - elif maxretries < 0 or retries > maxretries:
215 - writemsg("!!! Exhausted addresses for %s\n"
216 - % _unicode_decode(hostname), noiselevel=-1)
217 - return (1, False)
218 - else:
219 - uris.extend(uris_orig)
220 - dosyncuri = uris.pop()
221 -
222 - if (retries==0):
223 - if "--ask" in opts:
224 - uq = UserQuery(opts)
225 - if uq.query("Do you want to sync your Portage tree " + \
226 - "with the mirror at\n" + blue(dosyncuri) + bold("?"),
227 - enter_invalid) == "No":
228 - print()
229 - print("Quitting.")
230 - print()
231 - sys.exit(128 + signal.SIGINT)
232 - self.logger(self.xterm_titles,
233 - ">>> Starting rsync with " + dosyncuri)
234 - if "--quiet" not in opts:
235 - print(">>> Starting rsync with "+dosyncuri+"...")
236 + if port is None:
237 + port=""
238 + if user_name is None:
239 + user_name=""
240 + if re.match(r"^\[[:\da-fA-F]*\]$", hostname) is None:
241 + getaddrinfo_host = hostname
242 else:
243 - self.logger(self.xterm_titles,
244 - ">>> Starting retry %d of %d with %s" % \
245 - (retries, effective_maxretries, dosyncuri))
246 - writemsg_stdout(
247 - "\n\n>>> Starting retry %d of %d with %s\n" % \
248 - (retries, effective_maxretries, dosyncuri), noiselevel=-1)
249 -
250 - if dosyncuri.startswith('ssh://'):
251 - dosyncuri = dosyncuri[6:].replace('/', ':/', 1)
252 -
253 - unchanged, is_synced, exitcode, updatecache_flg = self._do_rsync(
254 - dosyncuri, timestamp, opts)
255 - if not unchanged:
256 - local_state_unchanged = False
257 - if is_synced:
258 - break
259 -
260 - retries=retries+1
261 -
262 - if maxretries < 0 or retries <= maxretries:
263 - print(">>> Retrying...")
264 - else:
265 - # over retries
266 - # exit loop
267 - exitcode = EXCEEDED_MAX_RETRIES
268 - break
269 - self._process_exitcode(exitcode, dosyncuri, out, maxretries)
270 -
271 - # if synced successfully, verify now
272 - if exitcode == 0 and self.verify_metamanifest:
273 - if gemato is None:
274 - writemsg_level("!!! Unable to verify: gemato not found\n",
275 - level=logging.ERROR, noiselevel=-1)
276 - exitcode = 127
277 - else:
278 - # Use isolated environment if key is specified,
279 - # system environment otherwise
280 - if self.repo.openpgp_key_path is not None:
281 - openpgp_env_cls = gemato.openpgp.OpenPGPEnvironment
282 - else:
283 - openpgp_env_cls = gemato.openpgp.OpenPGPSystemEnvironment
284 -
285 - try:
286 - with openpgp_env_cls() as openpgp_env:
287 - if self.repo.openpgp_key_path is not None:
288 - out.einfo('Using keys from %s' % (self.repo.openpgp_key_path,))
289 - with io.open(self.repo.openpgp_key_path, 'rb') as f:
290 - openpgp_env.import_key(f)
291 - out.ebegin('Refreshing keys from keyserver')
292 - openpgp_env.refresh_keys()
293 - out.eend(0)
294 + # getaddrinfo needs the brackets stripped
295 + getaddrinfo_host = hostname[1:-1]
296 + updatecache_flg = False
297 + all_rsync_opts = set(self.rsync_opts)
298 + all_rsync_opts.update(self.extra_rsync_opts)
299 +
300 + family = socket.AF_UNSPEC
301 + if "-4" in all_rsync_opts or "--ipv4" in all_rsync_opts:
302 + family = socket.AF_INET
303 + elif socket.has_ipv6 and \
304 + ("-6" in all_rsync_opts or "--ipv6" in all_rsync_opts):
305 + family = socket.AF_INET6
306 +
307 + addrinfos = None
308 + uris = []
309
310 + try:
311 + addrinfos = getaddrinfo_validate(
312 + socket.getaddrinfo(getaddrinfo_host, None,
313 + family, socket.SOCK_STREAM))
314 + except socket.error as e:
315 + writemsg_level(
316 + "!!! getaddrinfo failed for '%s': %s\n"
317 + % (_unicode_decode(hostname), _unicode(e)),
318 + noiselevel=-1, level=logging.ERROR)
319 +
320 + if addrinfos:
321 +
322 + AF_INET = socket.AF_INET
323 + AF_INET6 = None
324 + if socket.has_ipv6:
325 + AF_INET6 = socket.AF_INET6
326 +
327 + ips_v4 = []
328 + ips_v6 = []
329 +
330 + for addrinfo in addrinfos:
331 + if addrinfo[0] == AF_INET:
332 + ips_v4.append("%s" % addrinfo[4][0])
333 + elif AF_INET6 is not None and addrinfo[0] == AF_INET6:
334 + # IPv6 addresses need to be enclosed in square brackets
335 + ips_v6.append("[%s]" % addrinfo[4][0])
336 +
337 + random.shuffle(ips_v4)
338 + random.shuffle(ips_v6)
339 +
340 + # Give priority to the address family that
341 + # getaddrinfo() returned first.
342 + if AF_INET6 is not None and addrinfos and \
343 + addrinfos[0][0] == AF_INET6:
344 + ips = ips_v6 + ips_v4
345 + else:
346 + ips = ips_v4 + ips_v6
347 +
348 + for ip in ips:
349 + uris.append(syncuri.replace(
350 + "//" + user_name + hostname + port + "/",
351 + "//" + user_name + ip + port + "/", 1))
352 +
353 + if not uris:
354 + # With some configurations we need to use the plain hostname
355 + # rather than try to resolve the ip addresses (bug #340817).
356 + uris.append(syncuri)
357 +
358 + # reverse, for use with pop()
359 + uris.reverse()
360 + uris_orig = uris[:]
361 +
362 + effective_maxretries = maxretries
363 + if effective_maxretries < 0:
364 + effective_maxretries = len(uris) - 1
365 +
366 + local_state_unchanged = True
367 + while (1):
368 + if uris:
369 + dosyncuri = uris.pop()
370 + elif maxretries < 0 or retries > maxretries:
371 + writemsg("!!! Exhausted addresses for %s\n"
372 + % _unicode_decode(hostname), noiselevel=-1)
373 + return (1, False)
374 + else:
375 + uris.extend(uris_orig)
376 + dosyncuri = uris.pop()
377 +
378 + if (retries==0):
379 + if "--ask" in opts:
380 + uq = UserQuery(opts)
381 + if uq.query("Do you want to sync your Portage tree " + \
382 + "with the mirror at\n" + blue(dosyncuri) + bold("?"),
383 + enter_invalid) == "No":
384 + print()
385 + print("Quitting.")
386 + print()
387 + sys.exit(128 + signal.SIGINT)
388 + self.logger(self.xterm_titles,
389 + ">>> Starting rsync with " + dosyncuri)
390 + if "--quiet" not in opts:
391 + print(">>> Starting rsync with "+dosyncuri+"...")
392 + else:
393 + self.logger(self.xterm_titles,
394 + ">>> Starting retry %d of %d with %s" % \
395 + (retries, effective_maxretries, dosyncuri))
396 + writemsg_stdout(
397 + "\n\n>>> Starting retry %d of %d with %s\n" % \
398 + (retries, effective_maxretries, dosyncuri), noiselevel=-1)
399 +
400 + if dosyncuri.startswith('ssh://'):
401 + dosyncuri = dosyncuri[6:].replace('/', ':/', 1)
402 +
403 + unchanged, is_synced, exitcode, updatecache_flg = self._do_rsync(
404 + dosyncuri, timestamp, opts)
405 + if not unchanged:
406 + local_state_unchanged = False
407 + if is_synced:
408 + break
409 +
410 + retries=retries+1
411 +
412 + if maxretries < 0 or retries <= maxretries:
413 + print(">>> Retrying...")
414 + else:
415 + # over retries
416 + # exit loop
417 + exitcode = EXCEEDED_MAX_RETRIES
418 + break
419 + self._process_exitcode(exitcode, dosyncuri, out, maxretries)
420 +
421 + # if synced successfully, verify now
422 + if exitcode == 0 and self.verify_metamanifest:
423 + if gemato is None:
424 + writemsg_level("!!! Unable to verify: gemato not found\n",
425 + level=logging.ERROR, noiselevel=-1)
426 + exitcode = 127
427 + else:
428 + try:
429 # we always verify the Manifest signature, in case
430 # we had to deal with key revocation case
431 m = gemato.recursiveloader.ManifestRecursiveLoader(
432 @@ -344,13 +355,16 @@ class RsyncSync(NewBase):
433 out.ebegin('Verifying %s' % (self.repo.location,))
434 m.assert_directory_verifies()
435 out.eend(0)
436 - except Exception as e:
437 - writemsg_level("!!! Manifest verification failed:\n%s\n"
438 - % (e,),
439 - level=logging.ERROR, noiselevel=-1)
440 - exitcode = 1
441 + except Exception as e:
442 + writemsg_level("!!! Manifest verification failed:\n%s\n"
443 + % (e,),
444 + level=logging.ERROR, noiselevel=-1)
445 + exitcode = 1
446
447 - return (exitcode, updatecache_flg)
448 + return (exitcode, updatecache_flg)
449 + finally:
450 + if openpgp_env is not None:
451 + openpgp_env.close()
452
453
454 def _process_exitcode(self, exitcode, syncuri, out, maxretries):
455 --
456 2.16.1