Gentoo Archives: gentoo-portage-dev

From: Mike Frysinger <vapier@g.o>
To: gentoo-portage-dev@l.g.o
Subject: [gentoo-portage-dev] [PATCH v3] xattr: centralize the various shims in one place
Date: Sat, 30 May 2015 15:14:15
Message-Id: 1432998844-15210-1-git-send-email-vapier@gentoo.org
In Reply to: [gentoo-portage-dev] [PATCH] xattr: centralize the various shims in one place by Mike Frysinger
1 Rather than each module implementing its own shim around the various
2 methods for accessing extended attributes, start a dedicated module
3 that exports a consistent API.
4 ---
5 bin/xattr-helper.py | 11 +-
6 pym/portage/tests/util/test_xattr.py | 178 ++++++++++++++++++++++++++++
7 pym/portage/util/_xattr.py | 221 +++++++++++++++++++++++++++++++++++
8 pym/portage/util/movefile.py | 100 ++++------------
9 4 files changed, 423 insertions(+), 87 deletions(-)
10 create mode 100644 pym/portage/tests/util/test_xattr.py
11 create mode 100644 pym/portage/util/_xattr.py
12
13 diff --git a/bin/xattr-helper.py b/bin/xattr-helper.py
14 index 3e9b81e..19f25f9 100755
15 --- a/bin/xattr-helper.py
16 +++ b/bin/xattr-helper.py
17 @@ -19,16 +19,7 @@ import re
18 import sys
19
20 from portage.util._argparse import ArgumentParser
21 -
22 -if hasattr(os, "getxattr"):
23 -
24 - class xattr(object):
25 - get = os.getxattr
26 - set = os.setxattr
27 - list = os.listxattr
28 -
29 -else:
30 - import xattr
31 +from portage.util._xattr import xattr
32
33
34 _UNQUOTE_RE = re.compile(br'\\[0-7]{3}')
35 diff --git a/pym/portage/tests/util/test_xattr.py b/pym/portage/tests/util/test_xattr.py
36 new file mode 100644
37 index 0000000..2e2564a
38 --- /dev/null
39 +++ b/pym/portage/tests/util/test_xattr.py
40 @@ -0,0 +1,178 @@
41 +# Copyright 2010-2015 Gentoo Foundation
42 +# Distributed under the terms of the GNU General Public License v2
43 +
44 +"""Tests for the portage.util._xattr module"""
45 +
46 +from __future__ import print_function
47 +
48 +try:
49 + # Try python-3.3 module first.
50 + # pylint: disable=no-name-in-module
51 + from unittest import mock
52 +except ImportError:
53 + try:
54 + # Try standalone module.
55 + import mock
56 + except ImportError:
57 + mock = None
58 +
59 +import subprocess
60 +
61 +import portage
62 +from portage.tests import TestCase
63 +from portage.util._xattr import (xattr as _xattr, _XattrSystemCommands,
64 + _XattrStub)
65 +
66 +
67 +orig_popen = subprocess.Popen
68 +def MockSubprocessPopen(stdin):
69 + """Helper to mock (closely) a subprocess.Popen call
70 +
71 + The module has minor tweaks in behavior when it comes to encoding and
72 + python versions, so use a real subprocess.Popen call to fake out the
73 + runtime behavior. This way we don't have to also implement different
74 + encodings as that gets ugly real fast.
75 + """
76 + # pylint: disable=protected-access
77 + proc = orig_popen(['cat'], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
78 + proc.stdin.write(portage._unicode_encode(stdin, portage._encodings['stdio']))
79 + return proc
80 +
81 +
82 +class SystemCommandsTest(TestCase):
83 + """Test _XattrSystemCommands"""
84 +
85 + OUTPUT = '\n'.join((
86 + '# file: /bin/ping',
87 + 'security.capability=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=',
88 + 'user.foo="asdf"',
89 + '',
90 + ))
91 +
92 + def _setUp(self):
93 + if mock is None:
94 + self.skipTest('need mock for testing')
95 +
96 + return _XattrSystemCommands
97 +
98 + def _testGetBasic(self):
99 + """Verify the get() behavior"""
100 + xattr = self._setUp()
101 + with mock.patch.object(subprocess, 'Popen') as call_mock:
102 + # Verify basic behavior, and namespace arg works as expected.
103 + xattr.get('/some/file', 'user.foo')
104 + xattr.get('/some/file', 'foo', namespace='user')
105 + self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1])
106 +
107 + # Verify nofollow behavior.
108 + call_mock.reset()
109 + xattr.get('/some/file', 'user.foo', nofollow=True)
110 + self.assertIn('-h', call_mock.call_args[0][0])
111 +
112 + def testGetParsing(self):
113 + """Verify get() parses output sanely"""
114 + xattr = self._setUp()
115 + with mock.patch.object(subprocess, 'Popen') as call_mock:
116 + # Verify output parsing.
117 + call_mock.return_value = MockSubprocessPopen('\n'.join([
118 + '# file: /some/file',
119 + 'user.foo="asdf"',
120 + '',
121 + ]))
122 + call_mock.reset()
123 + self.assertEqual(xattr.get('/some/file', 'user.foo'), b'"asdf"')
124 +
125 + def testGetAllBasic(self):
126 + """Verify the get_all() behavior"""
127 + xattr = self._setUp()
128 + with mock.patch.object(subprocess, 'Popen') as call_mock:
129 + # Verify basic behavior.
130 + xattr.get_all('/some/file')
131 +
132 + # Verify nofollow behavior.
133 + call_mock.reset()
134 + xattr.get_all('/some/file', nofollow=True)
135 + self.assertIn('-h', call_mock.call_args[0][0])
136 +
137 + def testGetAllParsing(self):
138 + """Verify get_all() parses output sanely"""
139 + xattr = self._setUp()
140 + with mock.patch.object(subprocess, 'Popen') as call_mock:
141 + # Verify output parsing.
142 + call_mock.return_value = MockSubprocessPopen(self.OUTPUT)
143 + exp = [
144 + (b'security.capability', b'0sAQAAAgAgAAAAAAAAAAAAAAAAAAA='),
145 + (b'user.foo', b'"asdf"'),
146 + ]
147 + self.assertEqual(exp, xattr.get_all('/some/file'))
148 +
149 + def testSetBasic(self):
150 + """Verify the set() behavior"""
151 + xattr = self._setUp()
152 + with mock.patch.object(subprocess, 'Popen') as call_mock:
153 + # Verify basic behavior, and namespace arg works as expected.
154 + xattr.set('/some/file', 'user.foo', 'bar')
155 + xattr.set('/some/file', 'foo', 'bar', namespace='user')
156 + self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1])
157 +
158 + def testListBasic(self):
159 + """Verify the list() behavior"""
160 + xattr = self._setUp()
161 + with mock.patch.object(subprocess, 'Popen') as call_mock:
162 + # Verify basic behavior.
163 + xattr.list('/some/file')
164 +
165 + # Verify nofollow behavior.
166 + call_mock.reset()
167 + xattr.list('/some/file', nofollow=True)
168 + self.assertIn('-h', call_mock.call_args[0][0])
169 +
170 + def testListParsing(self):
171 + """Verify list() parses output sanely"""
172 + xattr = self._setUp()
173 + with mock.patch.object(subprocess, 'Popen') as call_mock:
174 + # Verify output parsing.
175 + call_mock.return_value = MockSubprocessPopen(self.OUTPUT)
176 + exp = [b'security.capability', b'user.foo']
177 + self.assertEqual(exp, xattr.list('/some/file'))
178 +
179 + def testRemoveBasic(self):
180 + """Verify the remove() behavior"""
181 + xattr = self._setUp()
182 + with mock.patch.object(subprocess, 'Popen') as call_mock:
183 + # Verify basic behavior, and namespace arg works as expected.
184 + xattr.remove('/some/file', 'user.foo')
185 + xattr.remove('/some/file', 'foo', namespace='user')
186 + self.assertEqual(call_mock.call_args_list[0], call_mock.call_args_list[1])
187 +
188 + # Verify nofollow behavior.
189 + call_mock.reset()
190 + xattr.remove('/some/file', 'user.foo', nofollow=True)
191 + self.assertIn('-h', call_mock.call_args[0][0])
192 +
193 +
194 +class StubTest(TestCase):
195 + """Test _XattrStub"""
196 +
197 + def testBasic(self):
198 + """Verify the stub is stubby"""
199 + # Would be nice to verify raised errno is OperationNotSupported.
200 + self.assertRaises(OSError, _XattrStub.get, '/', '')
201 + self.assertRaises(OSError, _XattrStub.set, '/', '', '')
202 + self.assertRaises(OSError, _XattrStub.get_all, '/')
203 + self.assertRaises(OSError, _XattrStub.remove, '/', '')
204 + self.assertRaises(OSError, _XattrStub.list, '/')
205 +
206 +
207 +class StandardTest(TestCase):
208 + """Test basic xattr API"""
209 +
210 + MODULES = (_xattr, _XattrSystemCommands, _XattrStub)
211 + FUNCS = ('get', 'get_all', 'set', 'remove', 'list')
212 +
213 + def testApi(self):
214 + """Make sure the exported API matches"""
215 + for mod in self.MODULES:
216 + for f in self.FUNCS:
217 + self.assertTrue(hasattr(mod, f),
218 + '%s func missing in %s' % (f, mod))
219 diff --git a/pym/portage/util/_xattr.py b/pym/portage/util/_xattr.py
220 new file mode 100644
221 index 0000000..4436c05
222 --- /dev/null
223 +++ b/pym/portage/util/_xattr.py
224 @@ -0,0 +1,221 @@
225 +# Copyright 2010-2015 Gentoo Foundation
226 +# Distributed under the terms of the GNU General Public License v2
227 +
228 +"""Portability shim for xattr support
229 +
230 +Exported API is the xattr object with get/get_all/set/remove/list operations.
231 +
232 +See the standard xattr module for more documentation.
233 +"""
234 +
235 +from __future__ import print_function
236 +
237 +import contextlib
238 +import os
239 +import subprocess
240 +
241 +from portage.exception import OperationNotSupported
242 +
243 +
244 +class _XattrGetAll(object):
245 + """Implement get_all() using list()/get() if there is no easy bulk method"""
246 +
247 + @classmethod
248 + def get_all(cls, item, nofollow=False, namespace=None):
249 + return [(name, cls.get(item, name, nofollow=nofollow, namespace=namespace))
250 + for name in cls.list(item, nofollow=nofollow, namespace=namespace)]
251 +
252 +
253 +class _XattrSystemCommands(_XattrGetAll):
254 + """Implement things with getfattr/setfattr"""
255 +
256 + @staticmethod
257 + def _parse_output(output):
258 + for line in output.readlines():
259 + if line.startswith(b'#'):
260 + continue
261 + line = line.rstrip()
262 + if not line:
263 + continue
264 + # The lines will have the format:
265 + # user.hex=0x12345
266 + # user.base64=0sAQAAAgAgAAAAAAAAAAAAAAAAAAA=
267 + # user.string="value0"
268 + # But since we don't do interpretation on the value (we just
269 + # save & restore it), don't bother with decoding here.
270 + yield line.split(b'=', 1)
271 +
272 + @staticmethod
273 + def _call(*args, **kwargs):
274 + proc = subprocess.Popen(*args, **kwargs)
275 + if proc.stdin:
276 + proc.stdin.close()
277 + proc.wait()
278 + return proc
279 +
280 + @classmethod
281 + def get(cls, item, name, nofollow=False, namespace=None):
282 + if namespace:
283 + name = '%s.%s' % (namespace, name)
284 + cmd = ['getfattr', '--absolute-names', '-n', name, item]
285 + if nofollow:
286 + cmd += ['-h']
287 + proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
288 +
289 + value = None
290 + for _, value in cls._parse_output(proc.stdout):
291 + break
292 +
293 + proc.stdout.close()
294 + return value
295 +
296 + @classmethod
297 + def set(cls, item, name, value, _flags=0, namespace=None):
298 + if namespace:
299 + name = '%s.%s' % (namespace, name)
300 + cmd = ['setfattr', '-n', name, '-v', value, item]
301 + cls._call(cmd)
302 +
303 + @classmethod
304 + def remove(cls, item, name, nofollow=False, namespace=None):
305 + if namespace:
306 + name = '%s.%s' % (namespace, name)
307 + cmd = ['setfattr', '-x', name, item]
308 + if nofollow:
309 + cmd += ['-h']
310 + cls._call(cmd)
311 +
312 + @classmethod
313 + def list(cls, item, nofollow=False, namespace=None, _names_only=True):
314 + cmd = ['getfattr', '-d', '--absolute-names', item]
315 + if nofollow:
316 + cmd += ['-h']
317 + cmd += ['-m', ('^%s[.]' % namespace) if namespace else '']
318 + proc = cls._call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
319 +
320 + ret = []
321 + if namespace:
322 + namespace = '%s.' % namespace
323 + for name, value in cls._parse_output(proc.stdout):
324 + if namespace:
325 + if name.startswith(namespace):
326 + name = name[len(namespace):]
327 + else:
328 + continue
329 + if _names_only:
330 + ret.append(name)
331 + else:
332 + ret.append((name, value))
333 +
334 + proc.stdout.close()
335 + return ret
336 +
337 + @classmethod
338 + def get_all(cls, item, nofollow=False, namespace=None):
339 + return cls.list(item, nofollow=nofollow, namespace=namespace,
340 + _names_only=False)
341 +
342 +
343 +class _XattrStub(_XattrGetAll):
344 + """Fake object since system doesn't support xattrs"""
345 +
346 + # pylint: disable=unused-argument
347 +
348 + @staticmethod
349 + def _raise():
350 + e = OSError('stub')
351 + e.errno = OperationNotSupported.errno
352 + raise e
353 +
354 + @classmethod
355 + def get(cls, item, name, nofollow=False, namespace=None):
356 + cls._raise()
357 +
358 + @classmethod
359 + def set(cls, item, name, value, flags=0, namespace=None):
360 + cls._raise()
361 +
362 + @classmethod
363 + def remove(cls, item, name, nofollow=False, namespace=None):
364 + cls._raise()
365 +
366 + @classmethod
367 + def list(cls, item, nofollow=False, namespace=None):
368 + cls._raise()
369 +
370 +
371 +if hasattr(os, 'getxattr'):
372 + # Easy as pie -- active python supports it.
373 + class xattr(_XattrGetAll):
374 + """Python >=3.3 and GNU/Linux"""
375 +
376 + # pylint: disable=unused-argument
377 +
378 + @staticmethod
379 + def get(item, name, nofollow=False, namespace=None):
380 + return os.getxattr(item, name, follow_symlinks=not nofollow)
381 +
382 + @staticmethod
383 + def set(item, name, value, flags=0, namespace=None):
384 + return os.setxattr(item, name, value, flags=flags)
385 +
386 + @staticmethod
387 + def remove(item, name, nofollow=False, namespace=None):
388 + return os.removexattr(item, name, follow_symlinks=not nofollow)
389 +
390 + @staticmethod
391 + def list(item, nofollow=False, namespace=None):
392 + return os.listxattr(item, follow_symlinks=not nofollow)
393 +
394 +else:
395 + try:
396 + # Maybe we have the xattr module.
397 + import xattr
398 +
399 + except ImportError:
400 + try:
401 + # Maybe we have the attr package.
402 + with open(os.devnull, 'wb') as f:
403 + subprocess.call(['getfattr', '--version'], stdout=f)
404 + subprocess.call(['setfattr', '--version'], stdout=f)
405 + xattr = _XattrSystemCommands
406 +
407 + except OSError:
408 + # Stub it out completely.
409 + xattr = _XattrStub
410 +
411 +
412 +@××××××××××.contextmanager
413 +def preserve_xattrs(path, nofollow=False, namespace=None):
414 + """Context manager to save/restore extended attributes on |path|
415 +
416 + If you want to rewrite a file (possibly replacing it with a new one), but
417 + want to preserve the extended attributes, this will do the trick.
418 +
419 + # First read all the extended attributes.
420 + with save_xattrs('/some/file'):
421 + ... rewrite the file ...
422 + # Now the extended attributes are restored as needed.
423 + """
424 + kwargs = {'nofollow': nofollow,}
425 + if namespace:
426 + # Compiled xattr python module does not like it when namespace=None.
427 + kwargs['namespace'] = namespace
428 +
429 + old_attrs = dict(xattr.get_all(path, **kwargs))
430 + try:
431 + yield
432 + finally:
433 + new_attrs = dict(xattr.get_all(path, **kwargs))
434 + for name, value in new_attrs.items():
435 + if name not in old_attrs:
436 + # Clear out new ones.
437 + xattr.remove(path, name, **kwargs)
438 + elif new_attrs[name] != old_attrs[name]:
439 + # Update changed ones.
440 + xattr.set(path, name, value, **kwargs)
441 +
442 + for name, value in old_attrs.items():
443 + if name not in new_attrs:
444 + # Re-add missing ones.
445 + xattr.set(path, name, value, **kwargs)
446 diff --git a/pym/portage/util/movefile.py b/pym/portage/util/movefile.py
447 index d00f624..0cb1977 100644
448 --- a/pym/portage/util/movefile.py
449 +++ b/pym/portage/util/movefile.py
450 @@ -11,7 +11,6 @@ import os as _os
451 import shutil as _shutil
452 import stat
453 import sys
454 -import subprocess
455 import textwrap
456
457 import portage
458 @@ -23,6 +22,7 @@ from portage.exception import OperationNotSupported
459 from portage.localization import _
460 from portage.process import spawn
461 from portage.util import writemsg
462 +from portage.util._xattr import xattr
463
464 def _apply_stat(src_stat, dest):
465 _os.chown(dest, src_stat.st_uid, src_stat.st_gid)
466 @@ -68,86 +68,32 @@ class _xattr_excluder(object):
467
468 return False
469
470 -if hasattr(_os, "getxattr"):
471 - # Python >=3.3 and GNU/Linux
472 - def _copyxattr(src, dest, exclude=None):
473 -
474 - try:
475 - attrs = _os.listxattr(src)
476 - except OSError as e:
477 - if e.errno != OperationNotSupported.errno:
478 - raise
479 - attrs = ()
480 - if attrs:
481 - if exclude is not None and isinstance(attrs[0], bytes):
482 - exclude = exclude.encode(_encodings['fs'])
483 - exclude = _get_xattr_excluder(exclude)
484 -
485 - for attr in attrs:
486 - if exclude(attr):
487 - continue
488 - try:
489 - _os.setxattr(dest, attr, _os.getxattr(src, attr))
490 - raise_exception = False
491 - except OSError:
492 - raise_exception = True
493 - if raise_exception:
494 - raise OperationNotSupported(_("Filesystem containing file '%s' "
495 - "does not support extended attribute '%s'") %
496 - (_unicode_decode(dest), _unicode_decode(attr)))
497 -else:
498 +def _copyxattr(src, dest, exclude=None):
499 + """Copy the extended attributes from |src| to |dest|"""
500 try:
501 - import xattr
502 - except ImportError:
503 - xattr = None
504 - if xattr is not None:
505 - def _copyxattr(src, dest, exclude=None):
506 -
507 - try:
508 - attrs = xattr.list(src)
509 - except IOError as e:
510 - if e.errno != OperationNotSupported.errno:
511 - raise
512 - attrs = ()
513 + attrs = xattr.list(src)
514 + except (OSError, IOError) as e:
515 + if e.errno != OperationNotSupported.errno:
516 + raise
517 + attrs = ()
518
519 - if attrs:
520 - if exclude is not None and isinstance(attrs[0], bytes):
521 - exclude = exclude.encode(_encodings['fs'])
522 - exclude = _get_xattr_excluder(exclude)
523 + if attrs:
524 + if exclude is not None and isinstance(attrs[0], bytes):
525 + exclude = exclude.encode(_encodings['fs'])
526 + exclude = _get_xattr_excluder(exclude)
527
528 - for attr in attrs:
529 - if exclude(attr):
530 - continue
531 - try:
532 - xattr.set(dest, attr, xattr.get(src, attr))
533 - raise_exception = False
534 - except IOError:
535 - raise_exception = True
536 - if raise_exception:
537 - raise OperationNotSupported(_("Filesystem containing file '%s' "
538 - "does not support extended attribute '%s'") %
539 - (_unicode_decode(dest), _unicode_decode(attr)))
540 - else:
541 + for attr in attrs:
542 + if exclude(attr):
543 + continue
544 try:
545 - with open(_os.devnull, 'wb') as f:
546 - subprocess.call(["getfattr", "--version"], stdout=f)
547 - subprocess.call(["setfattr", "--version"], stdout=f)
548 - except OSError:
549 - def _copyxattr(src, dest, exclude=None):
550 - # TODO: implement exclude
551 - getfattr_process = subprocess.Popen(["getfattr", "-d", "--absolute-names", src], stdout=subprocess.PIPE)
552 - getfattr_process.wait()
553 - extended_attributes = getfattr_process.stdout.readlines()
554 - getfattr_process.stdout.close()
555 - if extended_attributes:
556 - extended_attributes[0] = b"# file: " + _unicode_encode(dest) + b"\n"
557 - setfattr_process = subprocess.Popen(["setfattr", "--restore=-"], stdin=subprocess.PIPE, stderr=subprocess.PIPE)
558 - setfattr_process.communicate(input=b"".join(extended_attributes))
559 - if setfattr_process.returncode != 0:
560 - raise OperationNotSupported("Filesystem containing file '%s' does not support extended attributes" % dest)
561 - else:
562 - def _copyxattr(src, dest, exclude=None):
563 - pass
564 + xattr.set(dest, attr, xattr.get(src, attr))
565 + raise_exception = False
566 + except (OSError, IOError):
567 + raise_exception = True
568 + if raise_exception:
569 + raise OperationNotSupported(_("Filesystem containing file '%s' "
570 + "does not support extended attribute '%s'") %
571 + (_unicode_decode(dest), _unicode_decode(attr)))
572
573 def movefile(src, dest, newmtime=None, sstat=None, mysettings=None,
574 hardlink_candidates=None, encoding=_encodings['fs']):
575 --
576 2.4.1

Replies