From 4f21af955aa469b74e390a7cca205d7eec4e7bbc Mon Sep 17 00:00:00 2001 From: Steve Milner Date: Nov 18 2016 15:31:03 +0000 Subject: Multiple fixes for package and module. - Added noarch to the list to build. - Fixed provides (see rhbz#1374240) - Disabled the new auth module (see https://github.com/jplana/python-etcd/issues/210) --- diff --git a/python-etcd-0.4.3-Removed-the-new-auth-module.patch b/python-etcd-0.4.3-Removed-the-new-auth-module.patch new file mode 100644 index 0000000..4ff2223 --- /dev/null +++ b/python-etcd-0.4.3-Removed-the-new-auth-module.patch @@ -0,0 +1,448 @@ +From 063f3ed388c96959f284f6e2b9e08a0328481197 Mon Sep 17 00:00:00 2001 +From: Steve Milner +Date: Fri, 18 Nov 2016 10:12:58 -0500 +Subject: [PATCH] Removed the new auth module. + +The new auth module doesn't support etcd 2.3+ yet. Since it is a net new +module in this version it has been disabled until an update becomes +available. + +See: https://github.com/jplana/python-etcd/issues/210 +--- + src/etcd/auth.py | 255 -------------------------------------------- + src/etcd/tests/test_auth.py | 161 ---------------------------- + 2 files changed, 416 deletions(-) + delete mode 100644 src/etcd/auth.py + delete mode 100644 src/etcd/tests/test_auth.py + +diff --git a/src/etcd/auth.py b/src/etcd/auth.py +deleted file mode 100644 +index 796772d..0000000 +--- a/src/etcd/auth.py ++++ /dev/null +@@ -1,255 +0,0 @@ +-import json +- +-import logging +-import etcd +- +-_log = logging.getLogger(__name__) +- +- +-class EtcdAuthBase(object): +- entity = 'example' +- +- def __init__(self, client, name): +- self.client = client +- self.name = name +- self.uri = "{}/auth/{}s/{}".format(self.client.version_prefix, +- self.entity, self.name) +- +- @property +- def names(self): +- key = "{}s".format(self.entity) +- uri = "{}/auth/{}".format(self.client.version_prefix, key) +- response = self.client.api_execute(uri, self.client._MGET) +- return json.loads(response.data.decode('utf-8'))[key] +- +- def read(self): +- try: +- response = self.client.api_execute(self.uri, self.client._MGET) +- except etcd.EtcdInsufficientPermissions as e: +- _log.error("Any action on the authorization requires the root role") +- raise +- except etcd.EtcdKeyNotFound: +- _log.info("%s '%s' not found", self.entity, self.name) +- raise +- except Exception as e: +- _log.error("Failed to fetch %s in %s%s: %r", +- self.entity, self.client._base_uri, +- self.client.version_prefix, e) +- raise etcd.EtcdException( +- "Could not fetch {} '{}'".format(self.entity, self.name)) +- +- self._from_net(response.data) +- +- def write(self): +- try: +- r = self.__class__(self.client, self.name) +- r.read() +- except etcd.EtcdKeyNotFound: +- r = None +- try: +- for payload in self._to_net(r): +- response = self.client.api_execute_json(self.uri, +- self.client._MPUT, +- params=payload) +- # This will fail if the response is an error +- self._from_net(response.data) +- except etcd.EtcdInsufficientPermissions as e: +- _log.error("Any action on the authorization requires the root role") +- raise +- except Exception as e: +- _log.error("Failed to write %s '%s'", self.entity, self.name) +- # TODO: fine-grained exception handling +- raise etcd.EtcdException( +- "Could not write {} '{}': {}".format(self.entity, +- self.name, e)) +- +- def delete(self): +- try: +- _ = self.client.api_execute(self.uri, self.client._MDELETE) +- except etcd.EtcdInsufficientPermissions as e: +- _log.error("Any action on the authorization requires the root role") +- raise +- except etcd.EtcdKeyNotFound: +- _log.info("%s '%s' not found", self.entity, self.name) +- raise +- except Exception as e: +- _log.error("Failed to delete %s in %s%s: %r", +- self.entity, self._base_uri, self.version_prefix, e) +- raise etcd.EtcdException( +- "Could not delete {} '{}'".format(self.entity, self.name)) +- +- def _from_net(self, data): +- raise NotImplementedError() +- +- def _to_net(self, old=None): +- raise NotImplementedError() +- +- @classmethod +- def new(cls, client, data): +- c = cls(client, data[cls.entity]) +- c._from_net(data) +- return c +- +- +-class EtcdUser(EtcdAuthBase): +- """Class to manage in a orm-like way etcd users""" +- entity = 'user' +- +- def __init__(self, client, name): +- super(EtcdUser, self).__init__(client, name) +- self._roles = set() +- self._password = None +- +- def _from_net(self, data): +- d = json.loads(data.decode('utf-8')) +- self.roles = d.get('roles', []) +- self.name = d.get('user') +- +- def _to_net(self, prevobj=None): +- if prevobj is None: +- retval = [{"user": self.name, "password": self._password, +- "roles": list(self.roles)}] +- else: +- retval = [] +- if self._password: +- retval.append({"user": self.name, "password": self._password}) +- to_grant = list(self.roles - prevobj.roles) +- to_revoke = list(prevobj.roles - self.roles) +- if to_grant: +- retval.append({"user": self.name, "grant": to_grant}) +- if to_revoke: +- retval.append({"user": self.name, "revoke": to_revoke}) +- # Let's blank the password now +- # Even if the user can't be written we don't want it to leak anymore. +- self._password = None +- return retval +- +- @property +- def roles(self): +- return self._roles +- +- @roles.setter +- def roles(self, val): +- self._roles = set(val) +- +- @property +- def password(self): +- """Empty property for password.""" +- return None +- +- @password.setter +- def password(self, new_password): +- """Change user's password.""" +- self._password = new_password +- +- def __str__(self): +- return json.dumps(self._to_net()[0]) +- +- +- +-class EtcdRole(EtcdAuthBase): +- entity = 'role' +- +- def __init__(self, client, name): +- super(EtcdRole, self).__init__(client, name) +- self._read_paths = set() +- self._write_paths = set() +- +- def _from_net(self, data): +- d = json.loads(data.decode('utf-8')) +- self.name = d.get('role') +- +- try: +- kv = d["permissions"]["kv"] +- except: +- self._read_paths = set() +- self._write_paths = set() +- return +- +- self._read_paths = set(kv.get('read', [])) +- self._write_paths = set(kv.get('write', [])) +- +- def _to_net(self, prevobj=None): +- retval = [] +- if prevobj is None: +- retval.append({ +- "role": self.name, +- "permissions": +- { +- "kv": +- { +- "read": list(self._read_paths), +- "write": list(self._write_paths) +- } +- } +- }) +- else: +- to_grant = { +- 'read': list(self._read_paths - prevobj._read_paths), +- 'write': list(self._write_paths - prevobj._write_paths) +- } +- to_revoke = { +- 'read': list(prevobj._read_paths - self._read_paths), +- 'write': list(prevobj._write_paths - self._write_paths) +- } +- if [path for sublist in to_revoke.values() for path in sublist]: +- retval.append({'role': self.name, 'revoke': {'kv': to_revoke}}) +- if [path for sublist in to_grant.values() for path in sublist]: +- retval.append({'role': self.name, 'grant': {'kv': to_grant}}) +- return retval +- +- def grant(self, path, permission): +- if permission.upper().find('R') >= 0: +- self._read_paths.add(path) +- if permission.upper().find('W') >= 0: +- self._write_paths.add(path) +- +- def revoke(self, path, permission): +- if permission.upper().find('R') >= 0 and \ +- path in self._read_paths: +- self._read_paths.remove(path) +- if permission.upper().find('W') >= 0 and \ +- path in self._write_paths: +- self._write_paths.remove(path) +- +- @property +- def acls(self): +- perms = {} +- try: +- for path in self._read_paths: +- perms[path] = 'R' +- for path in self._write_paths: +- if path in perms: +- perms[path] += 'W' +- else: +- perms[path] = 'W' +- except: +- pass +- return perms +- +- @acls.setter +- def acls(self, acls): +- self._read_paths = set() +- self._write_paths = set() +- for path, permission in acls.items(): +- self.grant(path, permission) +- +- def __str__(self): +- return json.dumps({"role": self.name, 'acls': self.acls}) +- +- +-class Auth(object): +- def __init__(self, client): +- self.client = client +- self.uri = "{}/auth/enable".format(self.client.version_prefix) +- +- @property +- def active(self): +- resp = self.client.api_execute(self.uri, self.client._MGET) +- return json.loads(resp.data.decode('utf-8'))['enabled'] +- +- @active.setter +- def active(self, value): +- if value != self.active: +- method = value and self.client._MPUT or self.client._MDELETE +- self.client.api_execute(self.uri, method) +diff --git a/src/etcd/tests/test_auth.py b/src/etcd/tests/test_auth.py +deleted file mode 100644 +index fc6ce70..0000000 +--- a/src/etcd/tests/test_auth.py ++++ /dev/null +@@ -1,161 +0,0 @@ +-from etcd.tests.integration.test_simple import EtcdIntegrationTest +-from etcd import auth +-import etcd +- +- +-class TestEtcdAuthBase(EtcdIntegrationTest): +- cl_size = 1 +- +- def setUp(self): +- # Sets up the root user, toggles auth +- u = auth.EtcdUser(self.client, 'root') +- u.password = 'testpass' +- u.write() +- self.client = etcd.Client(port=6001, username='root', +- password='testpass') +- self.unauth_client = etcd.Client(port=6001) +- a = auth.Auth(self.client) +- a.active = True +- +- def tearDown(self): +- u = auth.EtcdUser(self.client, 'test_user') +- r = auth.EtcdRole(self.client, 'test_role') +- try: +- u.delete() +- except: +- pass +- try: +- r.delete() +- except: +- pass +- a = auth.Auth(self.client) +- a.active = False +- +- +-class EtcdUserTest(TestEtcdAuthBase): +- def test_names(self): +- u = auth.EtcdUser(self.client, 'test_user') +- self.assertEquals(u.names, ['root']) +- +- def test_read(self): +- u = auth.EtcdUser(self.client, 'root') +- # Reading an existing user succeeds +- try: +- u.read() +- except Exception: +- self.fail("reading the root user raised an exception") +- +- # roles for said user are fetched +- self.assertEquals(u.roles, set(['root'])) +- +- # The user is correctly rendered out +- self.assertEquals(u._to_net(), [{'user': 'root', 'password': None, +- 'roles': ['root']}]) +- +- # An inexistent user raises the appropriate exception +- u = auth.EtcdUser(self.client, 'user.does.not.exist') +- self.assertRaises(etcd.EtcdKeyNotFound, u.read) +- +- # Reading with an unauthenticated client raises an exception +- u = auth.EtcdUser(self.unauth_client, 'root') +- self.assertRaises(etcd.EtcdInsufficientPermissions, u.read) +- +- # Generic errors are caught +- c = etcd.Client(port=9999) +- u = auth.EtcdUser(c, 'root') +- self.assertRaises(etcd.EtcdException, u.read) +- +- def test_write_and_delete(self): +- # Create an user +- u = auth.EtcdUser(self.client, 'test_user') +- u.roles.add('guest') +- u.roles.add('root') +- # directly from my suitcase +- u.password = '123456' +- try: +- u.write() +- except: +- self.fail("creating a user doesn't work") +- # Password gets wiped +- self.assertEquals(u.password, None) +- u.read() +- # Verify we can log in as this user and access the auth (it has the +- # root role) +- cl = etcd.Client(port=6001, username='test_user', +- password='123456') +- ul = auth.EtcdUser(cl, 'root') +- try: +- ul.read() +- except etcd.EtcdInsufficientPermissions: +- self.fail("Reading auth with the new user is not possible") +- +- self.assertEquals(u.name, "test_user") +- self.assertEquals(u.roles, set(['guest', 'root'])) +- # set roles as a list, it works! +- u.roles = ['guest', 'test_group'] +- try: +- u.write() +- except: +- self.fail("updating a user you previously created fails") +- u.read() +- self.assertIn('test_group', u.roles) +- +- # Unauthorized access is properly handled +- ua = auth.EtcdUser(self.unauth_client, 'test_user') +- self.assertRaises(etcd.EtcdInsufficientPermissions, ua.write) +- +- # now let's test deletion +- du = auth.EtcdUser(self.client, 'user.does.not.exist') +- self.assertRaises(etcd.EtcdKeyNotFound, du.delete) +- +- # Delete test_user +- u.delete() +- self.assertRaises(etcd.EtcdKeyNotFound, u.read) +- # Permissions are properly handled +- self.assertRaises(etcd.EtcdInsufficientPermissions, ua.delete) +- +- +-class EtcdRoleTest(TestEtcdAuthBase): +- def test_names(self): +- r = auth.EtcdRole(self.client, 'guest') +- self.assertListEqual(r.names, [u'guest', u'root']) +- +- def test_read(self): +- r = auth.EtcdRole(self.client, 'guest') +- try: +- r.read() +- except: +- self.fail('Reading an existing role failed') +- +- self.assertEquals(r.acls, {'*': 'RW'}) +- # We can actually skip most other read tests as they are common +- # with EtcdUser +- +- def test_write_and_delete(self): +- r = auth.EtcdRole(self.client, 'test_role') +- r.acls = {'*': 'R', '/test/*': 'RW'} +- try: +- r.write() +- except: +- self.fail("Writing a simple groups should not fail") +- +- r1 = auth.EtcdRole(self.client, 'test_role') +- r1.read() +- self.assertEquals(r1.acls, r.acls) +- r.revoke('/test/*', 'W') +- r.write() +- r1.read() +- self.assertEquals(r1.acls, {'*': 'R', '/test/*': 'R'}) +- r.grant('/pub/*', 'RW') +- r.write() +- r1.read() +- self.assertEquals(r1.acls['/pub/*'], 'RW') +- # All other exceptions are tested by the user tests +- r1.name = None +- self.assertRaises(etcd.EtcdException, r1.write) +- # ditto for delete +- try: +- r.delete() +- except: +- self.fail("A normal delete should not fail") +- self.assertRaises(etcd.EtcdKeyNotFound, r.read) +-- +2.7.4 + diff --git a/python-etcd-0.4.3-auth-test-fail-workaround.patch b/python-etcd-0.4.3-auth-test-fail-workaround.patch deleted file mode 100644 index 608f37a..0000000 --- a/python-etcd-0.4.3-auth-test-fail-workaround.patch +++ /dev/null @@ -1,25 +0,0 @@ -commit 927fdb160e9b197ee24ce05e8289f5d2def526dc -Author: Matthew Barnes -Date: Sun Mar 6 20:23:58 2016 -0500 - - Work around EtcdRoleTest failure in test_read() - -diff --git a/src/etcd/tests/test_auth.py b/src/etcd/tests/test_auth.py -index fc6ce70..14475f9 100644 ---- a/src/etcd/tests/test_auth.py -+++ b/src/etcd/tests/test_auth.py -@@ -127,7 +127,13 @@ class EtcdRoleTest(TestEtcdAuthBase): - except: - self.fail('Reading an existing role failed') - -- self.assertEquals(r.acls, {'*': 'RW'}) -+ # XXX The ACL path result changed from '*' to '/*' at some point -+ # between etcd-2.2.2 and 2.2.5. They're equivalent so allow -+ # for both. -+ if '/*' in r.acls: -+ self.assertEquals(r.acls, {'/*': 'RW'}) -+ else: -+ self.assertEquals(r.acls, {'*': 'RW'}) - # We can actually skip most other read tests as they are common - # with EtcdUser - diff --git a/python-etcd.spec b/python-etcd.spec index 71fe09c..28df6bf 100644 --- a/python-etcd.spec +++ b/python-etcd.spec @@ -1,4 +1,5 @@ -%global srcname python-etcd +%global modname etcd +%global srcname python-%{modname} Name: %{srcname} Version: 0.4.3 @@ -36,7 +37,7 @@ BuildRequires: python3-pyOpenSSL # Needed for tests BuildRequires: etcd -Patch1: python-etcd-0.4.3-auth-test-fail-workaround.patch +Patch1: python-etcd-0.4.3-Removed-the-new-auth-module.patch %description Client library for interacting with an etcd service, providing Python @@ -48,7 +49,7 @@ election. Summary: %summary Requires: etcd Requires: python-dns -%{?python_provide:%python_provide python2-%{srcname}} +%{?python_provide:%python_provide python2-%{modname}} %description -n python2-%{srcname} Client library for interacting with an etcd service, providing Python @@ -60,7 +61,7 @@ election. Summary: %summary Requires: etcd Requires: python3-dns -%{?python_provide:%python_provide python3-%{srcname}} +%{?python_provide:%python_provide python3-%{modname}} %description -n python3-%{srcname} Client library for interacting with an etcd service, providing Python @@ -100,6 +101,8 @@ election. %changelog * Wed Nov 16 2016 Steve Milner - 0.4.3-4 - Added noarch to the list to build. +- Fixed provides (see rhbz#1374240) +- Disabled the new auth module (see https://github.com/jplana/python-etcd/issues/210) * Wed Nov 09 2016 Matthew Barnes - 0.4.3-3 - etcd now excludes ppc64; follow suit.