Gentoo Archives: gentoo-portage-dev

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

Replies

Subject Author
Re: [gentoo-portage-dev] [PATCH v2] xattr: centralize the various shims in one place Arfrever Frehtes Taifersar Arahesis <arfrever.fta@×××××.com>