From b2b209fd996decb762eb8c188fe15075761a0a32 Mon Sep 17 00:00:00 2001 From: Sebastian Stadil Date: Tue, 14 May 2013 23:58:17 -0600 Subject: [PATCH 1/4] Added Apache License No license found, adding one. --- LICENSE | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..64370a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,203 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +--- License for python-keystoneclient versions prior to 2.1 --- + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of this project nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From c4a89a01d2bdd22a8272d996413ce6749d800dc8 Mon Sep 17 00:00:00 2001 From: Roma Koshel Date: Tue, 30 Jul 2013 13:36:10 -0700 Subject: [PATCH 2/4] Added Python 3 support PyChef now supports both Python 2/3. New dependency `six` for 2/3 compatibility --- chef/__init__.py | 2 +- chef/api.py | 77 +++++++++++++++++++++++++------------ chef/auth.py | 24 ++++++++---- chef/base.py | 17 ++++---- chef/data_bag.py | 15 +++----- chef/exceptions.py | 2 +- chef/fabric.py | 57 +++++++++++++++++++++------ chef/node.py | 2 +- chef/rsa.py | 75 ++++++++++++++++++++++-------------- chef/search.py | 8 ++-- chef/tests/__init__.py | 6 +-- chef/tests/test_api.py | 6 +++ chef/tests/test_data_bag.py | 2 +- chef/tests/test_fabric.py | 4 +- chef/utils/json.py | 3 +- docs/fabric.rst | 4 ++ setup.py | 3 +- 17 files changed, 203 insertions(+), 104 deletions(-) diff --git a/chef/__init__.py b/chef/__init__.py index fccae67..2bd45e6 100644 --- a/chef/__init__.py +++ b/chef/__init__.py @@ -1,6 +1,6 @@ # Copyright (c) 2010 Noah Kantrowitz -__version__ = (0, 2, 2, 'dev') +__version__ = (0, 2, 3, 'dev') from chef.api import ChefAPI, autoconfigure from chef.client import Client diff --git a/chef/api.py b/chef/api.py index 6d73bd8..a462e4b 100644 --- a/chef/api.py +++ b/chef/api.py @@ -1,14 +1,16 @@ -import copy +import six + import datetime -import itertools import logging import os import re import socket import subprocess import threading -import urllib2 -import urlparse +import six.moves.urllib.request +import six.moves.urllib.error +import six.moves.urllib.parse + import weakref import pkg_resources @@ -28,6 +30,7 @@ puts Chef::Config.configuration.to_json """.strip() + def api_stack_value(): if not hasattr(api_stack, 'value'): api_stack.value = [] @@ -35,23 +38,27 @@ def api_stack_value(): class UnknownRubyExpression(Exception): + """Token exception for unprocessed Ruby expressions.""" -class ChefRequest(urllib2.Request): +class ChefRequest(six.moves.urllib.request.Request): + """Workaround for using PUT/DELETE with urllib2.""" + def __init__(self, *args, **kwargs): self._method = kwargs.pop('method', None) # Request is an old-style class, no super() allowed. - urllib2.Request.__init__(self, *args, **kwargs) + six.moves.urllib.request.Request.__init__(self, *args, **kwargs) def get_method(self): if self._method: return self._method - return urllib2.Request.get_method(self) + return six.moves.urllib.request.Request.get_method(self) class ChefAPI(object): + """The ChefAPI object is a wrapper for a single Chef server. .. admonition:: The API stack @@ -68,16 +75,19 @@ class ChefAPI(object): ruby_value_re = re.compile(r'#\{([^}]+)\}') env_value_re = re.compile(r'ENV\[(.+)\]') + ruby_string_re = re.compile(r'^\s*(["\'])(.*?)\1\s*$') def __init__(self, url, key, client, version='0.10.8', headers={}): self.url = url.rstrip('/') - self.parsed_url = urlparse.urlparse(self.url) + self.parsed_url = six.moves.urllib.parse.urlparse(self.url) if not isinstance(key, Key): key = Key(key) + if not key.key: + raise ValueError("ChefAPI attribute 'key' was invalid.") self.key = key self.client = client self.version = version - self.headers = dict((k.lower(), v) for k, v in headers.iteritems()) + self.headers = dict((k.lower(), v) for k, v in headers.items()) self.version_parsed = pkg_resources.parse_version(self.version) self.platform = self.parsed_url.hostname == 'api.opscode.com' if not api_stack_value(): @@ -96,12 +106,19 @@ def from_config_file(cls, path): url = key_path = client_name = None for line in open(path): if not line.strip() or line.startswith('#'): - continue # Skip blanks and comments + continue # Skip blanks and comments parts = line.split(None, 1) if len(parts) != 2: - continue # Not a simple key/value, we can't parse it anyway + continue # Not a simple key/value, we can't parse it anyway key, value = parts - value = value.strip().strip('"\'') + md = cls.ruby_string_re.search(value) + if md: + value = md.group(2) + else: + # Not a string, don't even try + log.debug('Value for %s does not look like a string: %s' % (key, value)) + continue + def _ruby_value(match): expr = match.group(1).strip() if expr == 'current_dir': @@ -117,25 +134,32 @@ def _ruby_value(match): except UnknownRubyExpression: continue if key == 'chef_server_url': + log.debug('Found URL: %r', value) url = value elif key == 'node_name': + log.debug('Found client name: %r', value) client_name = value elif key == 'client_key': + log.debug('Found key path: %r', value) key_path = value if not os.path.isabs(key_path): # Relative paths are relative to the config file key_path = os.path.abspath(os.path.join(os.path.dirname(path), key_path)) - if not url: + if not (url and client_name and key_path): # No URL, no chance this was valid, try running Ruby - log.debug('No Chef server URL found, trying Ruby parse') + log.debug('No Chef server config found, trying Ruby parse') + url = key_path = client_name = None proc = subprocess.Popen('ruby', stdin=subprocess.PIPE, stdout=subprocess.PIPE) script = config_ruby_script % path.replace('\\', '\\\\').replace("'", "\\'") out, err = proc.communicate(script) if proc.returncode == 0 and out.strip(): data = json.loads(out) + log.debug('Ruby parse succeeded with %r', data) url = data.get('chef_server_url') client_name = data.get('node_name') key_path = data.get('client_key') + else: + log.debug('Ruby parse failed with exit code %s: %s', proc.returncode, out.strip()) if not url: # Still no URL, can't use this config log.debug('Still no Chef server URL found') @@ -177,39 +201,42 @@ def __exit__(self, type, value, traceback): def _request(self, method, url, data, headers): # Testing hook, subclass and override for WSGI intercept + if six.PY3 and data: + data = data.encode() request = ChefRequest(url, data, headers, method=method) - return urllib2.urlopen(request).read() + return six.moves.urllib.request.urlopen(request).read() def request(self, method, path, headers={}, data=None): auth_headers = sign_request(key=self.key, http_method=method, - path=self.parsed_url.path+path.split('?', 1)[0], body=data, - host=self.parsed_url.netloc, timestamp=datetime.datetime.utcnow(), - user_id=self.client) + path=self.parsed_url.path + path.split('?', 1)[0], body=data, + host=self.parsed_url.netloc, timestamp=datetime.datetime.utcnow(), + user_id=self.client) request_headers = {} request_headers.update(self.headers) - request_headers.update(dict((k.lower(), v) for k, v in headers.iteritems())) + request_headers.update(dict((k.lower(), v) for k, v in headers.items())) request_headers['x-chef-version'] = self.version request_headers.update(auth_headers) try: - response = self._request(method, self.url+path, data, dict((k.capitalize(), v) for k, v in request_headers.iteritems())) - except urllib2.HTTPError, e: + response = self._request(method, self.url + path, data, dict( + (k.capitalize(), v) for k, v in request_headers.items())) + except six.moves.urllib.error.HTTPError as e: e.content = e.read() try: - e.content = json.loads(e.content) + e.content = json.loads(e.content.decode()) raise ChefServerError.from_error(e.content['error'], code=e.code) except ValueError: pass raise e return response - def api_request(self, method, path, headers={}, data=None): - headers = dict((k.lower(), v) for k, v in headers.iteritems()) + def api_request(self, method, path, headers={}, data=None): + headers = dict((k.lower(), v) for k, v in headers.items()) headers['accept'] = 'application/json' if data is not None: headers['content-type'] = 'application/json' data = json.dumps(data) response = self.request(method, path, headers, data) - return json.loads(response) + return json.loads(response.decode()) def __getitem__(self, path): return self.api_request('GET', path) diff --git a/chef/auth.py b/chef/auth.py index 494c604..e09f556 100644 --- a/chef/auth.py +++ b/chef/auth.py @@ -3,24 +3,29 @@ import hashlib import re + def _ruby_b64encode(value): """The Ruby function Base64.encode64 automatically breaks things up into 60-character chunks. """ b64 = base64.b64encode(value) - for i in xrange(0, len(b64), 60): - yield b64[i:i+60] + for i in range(0, len(b64), 60): + yield b64[i:i + 60].decode('utf-8') + def ruby_b64encode(value): return '\n'.join(_ruby_b64encode(value)) + def sha1_base64(value): """An implementation of Mixlib::Authentication::Digester.""" - return ruby_b64encode(hashlib.sha1(value).digest()) + return ruby_b64encode(hashlib.sha1(value.encode('utf-8')).digest()) + class UTC(datetime.tzinfo): + """UTC timezone stub.""" - + ZERO = datetime.timedelta(0) def utcoffset(self, dt): @@ -34,18 +39,22 @@ def dst(self, dt): utc = UTC() + def canonical_time(timestamp): if timestamp.tzinfo is not None: timestamp = timestamp.astimezone(utc).replace(tzinfo=None) return timestamp.replace(microsecond=0).isoformat() + 'Z' canonical_path_regex = re.compile(r'/+') + + def canonical_path(path): path = canonical_path_regex.sub('/', path) if len(path) > 1: path = path.rstrip('/') return path + def canonical_request(http_method, path, hashed_body, timestamp, user_id): # Canonicalize request parameters http_method = http_method.upper() @@ -59,11 +68,12 @@ def canonical_request(http_method, path, hashed_body, timestamp, user_id): 'X-Ops-Timestamp:%(timestamp)s\n' 'X-Ops-UserId:%(user_id)s' % vars()) + def sign_request(key, http_method, path, body, host, timestamp, user_id): """Generate the needed headers for the Opscode authentication protocol.""" timestamp = canonical_time(timestamp) hashed_body = sha1_base64(body or '') - + # Simple headers headers = { 'x-ops-sign': 'version=1.0', @@ -71,10 +81,10 @@ def sign_request(key, http_method, path, body, host, timestamp, user_id): 'x-ops-timestamp': timestamp, 'x-ops-content-hash': hashed_body, } - + # Create RSA signature req = canonical_request(http_method, path, hashed_body, timestamp, user_id) sig = _ruby_b64encode(key.private_encrypt(req)) for i, line in enumerate(sig): - headers['x-ops-authorization-%s'%(i+1)] = line + headers['x-ops-authorization-%s' % (i + 1)] = line return headers diff --git a/chef/base.py b/chef/base.py index 300b5b8..be16407 100644 --- a/chef/base.py +++ b/chef/base.py @@ -1,3 +1,4 @@ +import six import collections import pkg_resources @@ -34,10 +35,8 @@ def __init__(cls, name, bases, d): cls.api_version_parsed = pkg_resources.parse_version(cls.api_version) -class ChefObject(object): +class ChefObject(six.with_metaclass(ChefObjectMeta, object)): """A base class for Chef API objects.""" - - __metaclass__ = ChefObjectMeta types = {} url = '' @@ -63,7 +62,7 @@ def __init__(self, name, api=None, skip_load=False): self._populate(data) def _populate(self, data): - for name, cls in self.__class__.attributes.iteritems(): + for name, cls in self.__class__.attributes.items(): if name in data: value = cls(data[name]) else: @@ -83,7 +82,7 @@ def list(cls, api=None): """ api = api or ChefAPI.get_global() cls._check_api_version(api) - names = [name for name, url in api[cls.url].iteritems()] + names = [name for name, url in api[cls.url].items()] return ChefQuery(cls, names, api) @classmethod @@ -94,7 +93,7 @@ def create(cls, name, api=None, **kwargs): api = api or ChefAPI.get_global() cls._check_api_version(api) obj = cls(name, api, skip_load=True) - for key, value in kwargs.iteritems(): + for key, value in kwargs.items(): setattr(obj, key, value) api.api_request('POST', cls.url, data=obj) return obj @@ -106,7 +105,7 @@ def save(self, api=None): api = api or self.api try: api.api_request('PUT', self.url, data=self) - except ChefServerNotFoundError, e: + except ChefServerNotFoundError as e: # If you get a 404 during a save, just create it instead # This mirrors the logic in the Chef code api.api_request('POST', self.__class__.url, data=self) @@ -122,7 +121,7 @@ def to_dict(self): 'json_class': 'Chef::'+self.__class__.__name__, 'chef_type': self.__class__.__name__.lower(), } - for attr in self.__class__.attributes.iterkeys(): + for attr in self.__class__.attributes.keys(): d[attr] = getattr(self, attr) return d @@ -138,4 +137,4 @@ def _check_api_version(cls, api): # use for creating Chef objects without an API connection (just for # serialization perhaps). if api and cls.api_version_parsed > api.version_parsed: - raise ChefAPIVersionError, "Class %s is not compatible with API version %s" % (cls.__name__, api.version) + raise ChefAPIVersionError("Class %s is not compatible with API version %s" % (cls.__name__, api.version)) diff --git a/chef/data_bag.py b/chef/data_bag.py index f96dcd0..e19c07e 100644 --- a/chef/data_bag.py +++ b/chef/data_bag.py @@ -1,3 +1,4 @@ +import six import abc import collections @@ -9,7 +10,7 @@ class DataBagMeta(ChefObjectMeta, abc.ABCMeta): """A metaclass to allow DataBag to use multiple inheritance.""" -class DataBag(ChefObject, ChefQuery): +class DataBag(six.with_metaclass(DataBagMeta, ChefObject, ChefQuery)): """A Chef data bag object. Data bag items are available via the mapping API. Evaluation works in the @@ -22,25 +23,21 @@ class DataBag(ChefObject, ChefQuery): print item['qa_version'] """ - __metaclass__ = DataBagMeta - url = '/data' def _populate(self, data): - self.names = data.keys() + self.names = list(data.keys()) def obj_class(self, name, api): return DataBagItem(self, name, api=api) -class DataBagItem(ChefObject, collections.MutableMapping): +class DataBagItem(six.with_metaclass(DataBagMeta, ChefObject, collections.MutableMapping)): """A Chef data bag item object. Data bag items act as normal dicts and can contain arbitrary data. """ - __metaclass__ = DataBagMeta - url = '/data' attributes = { 'raw_data': dict, @@ -99,7 +96,7 @@ def create(cls, bag, name, api=None, **kwargs): keyword arguments.""" api = api or ChefAPI.get_global() obj = cls(bag, name, api, skip_load=True) - for key, value in kwargs.iteritems(): + for key, value in kwargs.items(): obj[key] = value obj['id'] = name api.api_request('POST', cls.url+'/'+str(bag), data=obj.raw_data) @@ -117,5 +114,5 @@ def save(self, api=None): self['id'] = self.name try: api.api_request('PUT', self.url, data=self.raw_data) - except ChefServerNotFoundError, e: + except ChefServerNotFoundError as e: api.api_request('POST', self.__class__.url+'/'+str(self._bag), data=self.raw_data) diff --git a/chef/exceptions.py b/chef/exceptions.py index 9b7de67..63a4987 100644 --- a/chef/exceptions.py +++ b/chef/exceptions.py @@ -11,7 +11,7 @@ class ChefServerError(ChefError): def __init__(self, message, code=None): self.raw_message = message if isinstance(message, list): - message = u', '.join(m for m in message if m) + message = ', '.join(m for m in message if m) super(ChefError, self).__init__(message) self.code = code diff --git a/chef/fabric.py b/chef/fabric.py index f769f09..2b50c92 100644 --- a/chef/fabric.py +++ b/chef/fabric.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import + import functools @@ -6,10 +6,11 @@ from chef.environment import Environment from chef.exceptions import ChefError, ChefAPIVersionError from chef.search import Search +import collections try: - from fabric.api import env, task, roles -except ImportError, e: + from fabric.api import env, task, roles, output +except ImportError as e: env = {} task = lambda *args, **kwargs: lambda fn: fn roles = task @@ -38,7 +39,7 @@ def __init__(self, query, api, hostname_attr, environment=None): self.query = query self.api = api self.hostname_attr = hostname_attr - if isinstance(self.hostname_attr, basestring): + if isinstance(self.hostname_attr, str): self.hostname_attr = (self.hostname_attr,) self.environment = environment @@ -51,15 +52,19 @@ def __call__(self): query += ' AND chef_environment:%s' % environment for row in Search('node', query, api=self.api): if row: - if callable(self.hostname_attr): - yield self.hostname_attr(row.object) + if isinstance(self.hostname_attr, collections.Callable): + val = self.hostname_attr(row.object) + if val: + yield val else: for attr in self.hostname_attr: try: - yield row.object.attributes.get_dotted(attr) - break + val = row.object.attributes.get_dotted(attr) + if val: # Don't ever give out '' or None, since it will error anyway + yield val + break except KeyError: - continue + pass # Move on to the next else: raise ChefError('Cannot find a usable hostname attribute for node %s', row.object) @@ -82,7 +87,7 @@ def mytask(): node that holds the hostname or IP to connect to, an array of such keys to check in order (the first which exists will be used), or a callable which takes a :class:`~chef.Node` and returns the hostname or IP to connect to. - + To refer to a nested attribute, separate the levels with ``'.'`` e.g. ``'ec2.public_hostname'`` ``environment`` is the Chef :class:`~chef.Environment` name in which to @@ -147,6 +152,21 @@ def chef_environment(name, api=None): def chef_query(query, api=None, hostname_attr=DEFAULT_HOSTNAME_ATTR, environment=_default_environment): + """A decorator to use an arbitrary Chef search query to find nodes to execute on. + + This is used like Fabric's ``roles()`` decorator, but accepts a Chef search query. + + Example:: + + from chef.fabric import chef_query + + @chef_query('roles:web AND tags:active') + @task + def deploy(): + pass + + .. versionadded:: 0.2.1 + """ api = _api(api) if api.version_parsed < Environment.api_version_parsed and environment is not None: raise ChefAPIVersionError('Environment support requires Chef API 0.10 or greater') @@ -156,8 +176,23 @@ def chef_query(query, api=None, hostname_attr=DEFAULT_HOSTNAME_ATTR, environment def chef_tags(*tags, **kwargs): + """A decorator to use Chef node tags to find nodes to execute on. + + This is used like Fabric's ``roles()`` decorator, but accepts a list of tags. + + Example:: + + from chef.fabric import chef_tags + + @chef_tags('active', 'migrator') + @task + def migrate(): + pass + + .. versionadded:: 0.2.1 + """ # Allow passing a single iterable - if len(tags) == 1 and not isinstance(tags[0], basestring): + if len(tags) == 1 and not isinstance(tags[0], str): tags = tags[0] query = ' AND '.join('tags:%s'%tag.strip() for tag in tags) return chef_query(query, **kwargs) diff --git a/chef/node.py b/chef/node.py index 619d4bf..a2cb588 100644 --- a/chef/node.py +++ b/chef/node.py @@ -27,7 +27,7 @@ def __init__(self, search_path=[], path=None, write=None): def __iter__(self): keys = set() for d in self.search_path: - keys |= set(d.iterkeys()) + keys |= set(d.keys()) return iter(keys) def __len__(self): diff --git a/chef/rsa.py b/chef/rsa.py index a92c1cc..65a67a3 100644 --- a/chef/rsa.py +++ b/chef/rsa.py @@ -1,3 +1,4 @@ +import six import sys from ctypes import * @@ -8,28 +9,30 @@ else: _eay = CDLL('libcrypto.so') -#unsigned long ERR_get_error(void); +# unsigned long ERR_get_error(void); ERR_get_error = _eay.ERR_get_error ERR_get_error.argtypes = [] ERR_get_error.restype = c_ulong -#void ERR_error_string_n(unsigned long e, char *buf, size_t len); +# void ERR_error_string_n(unsigned long e, char *buf, size_t len); ERR_error_string_n = _eay.ERR_error_string_n ERR_error_string_n.argtypes = [c_ulong, c_char_p, c_size_t] ERR_error_string_n.restype = None + class SSLError(Exception): + """An error in OpenSSL.""" def __init__(self, message, *args): - message = message%args + message = message % args err = ERR_get_error() if err: message += ':' while err: buf = create_string_buffer(120) ERR_error_string_n(err, buf, 120) - message += '\n%s'%string_at(buf, 119) + message += '\n%s' % string_at(buf, 119) err = ERR_get_error() super(SSLError, self).__init__(message) @@ -49,21 +52,25 @@ def __init__(self, message, *args): BIO_s_mem.argtypes = [] BIO_s_mem.restype = c_void_p -#long BIO_ctrl(BIO *bp,int cmd,long larg,void *parg); +# long BIO_ctrl(BIO *bp,int cmd,long larg,void *parg); BIO_ctrl = _eay.BIO_ctrl BIO_ctrl.argtypes = [c_void_p, c_int, c_long, c_void_p] BIO_ctrl.restype = c_long -#define BIO_CTRL_RESET 1 /* opt - rewind/zero etc */ +# define BIO_CTRL_RESET 1 /* opt - rewind/zero etc */ BIO_CTRL_RESET = 1 -##define BIO_CTRL_INFO 3 /* opt - extra tit-bits */ +# define BIO_CTRL_INFO 3 /* opt - extra tit-bits */ BIO_CTRL_INFO = 3 -#define BIO_reset(b) (int)BIO_ctrl(b,BIO_CTRL_RESET,0,NULL) +# define BIO_reset(b) (int)BIO_ctrl(b,BIO_CTRL_RESET,0,NULL) + + def BIO_reset(b): return BIO_ctrl(b, BIO_CTRL_RESET, 0, None) -##define BIO_get_mem_data(b,pp) BIO_ctrl(b,BIO_CTRL_INFO,0,(char *)pp) +# define BIO_get_mem_data(b,pp) BIO_ctrl(b,BIO_CTRL_INFO,0,(char *)pp) + + def BIO_get_mem_data(b, pp): return BIO_ctrl(b, BIO_CTRL_INFO, 0, pp) @@ -71,42 +78,44 @@ def BIO_get_mem_data(b, pp): BIO_free = _eay.BIO_free BIO_free.argtypes = [c_void_p] BIO_free.restype = c_int + + def BIO_free_errcheck(result, func, arguments): if result == 0: raise SSLError('Unable to free BIO') BIO_free.errcheck = BIO_free_errcheck -#RSA *PEM_read_bio_RSAPrivateKey(BIO *bp, RSA **x, +# RSA *PEM_read_bio_RSAPrivateKey(BIO *bp, RSA **x, # pem_password_cb *cb, void *u); PEM_read_bio_RSAPrivateKey = _eay.PEM_read_bio_RSAPrivateKey PEM_read_bio_RSAPrivateKey.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] PEM_read_bio_RSAPrivateKey.restype = c_void_p -#RSA *PEM_read_bio_RSAPublicKey(BIO *bp, RSA **x, +# RSA *PEM_read_bio_RSAPublicKey(BIO *bp, RSA **x, # pem_password_cb *cb, void *u); PEM_read_bio_RSAPublicKey = _eay.PEM_read_bio_RSAPublicKey PEM_read_bio_RSAPublicKey.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] PEM_read_bio_RSAPublicKey.restype = c_void_p -#int PEM_write_bio_RSAPrivateKey(BIO *bp, RSA *x, const EVP_CIPHER *enc, +# int PEM_write_bio_RSAPrivateKey(BIO *bp, RSA *x, const EVP_CIPHER *enc, # unsigned char *kstr, int klen, # pem_password_cb *cb, void *u); PEM_write_bio_RSAPrivateKey = _eay.PEM_write_bio_RSAPrivateKey PEM_write_bio_RSAPrivateKey.argtypes = [c_void_p, c_void_p, c_void_p, c_char_p, c_int, c_void_p, c_void_p] PEM_write_bio_RSAPrivateKey.restype = c_int -#int PEM_write_bio_RSAPublicKey(BIO *bp, RSA *x); +# int PEM_write_bio_RSAPublicKey(BIO *bp, RSA *x); PEM_write_bio_RSAPublicKey = _eay.PEM_write_bio_RSAPublicKey PEM_write_bio_RSAPublicKey.argtypes = [c_void_p, c_void_p] PEM_write_bio_RSAPublicKey.restype = c_int -#int RSA_private_encrypt(int flen, unsigned char *from, +# int RSA_private_encrypt(int flen, unsigned char *from, # unsigned char *to, RSA *rsa,int padding); RSA_private_encrypt = _eay.RSA_private_encrypt RSA_private_encrypt.argtypes = [c_int, c_void_p, c_void_p, c_void_p, c_int] RSA_private_encrypt.restype = c_int -#int RSA_public_decrypt(int flen, unsigned char *from, +# int RSA_public_decrypt(int flen, unsigned char *from, # unsigned char *to, RSA *rsa, int padding); RSA_public_decrypt = _eay.RSA_public_decrypt RSA_public_decrypt.argtypes = [c_int, c_void_p, c_void_p, c_void_p, c_int] @@ -120,20 +129,22 @@ def BIO_free_errcheck(result, func, arguments): RSA_size.argtypes = [c_void_p] RSA_size.restype = c_int -#RSA *RSA_generate_key(int num, unsigned long e, +# RSA *RSA_generate_key(int num, unsigned long e, # void (*callback)(int,int,void *), void *cb_arg); RSA_generate_key = _eay.RSA_generate_key RSA_generate_key.argtypes = [c_int, c_ulong, c_void_p, c_void_p] RSA_generate_key.restype = c_void_p -##define RSA_F4 0x10001L +# define RSA_F4 0x10001L RSA_F4 = 0x10001 # void RSA_free(RSA *rsa); RSA_free = _eay.RSA_free RSA_free.argtypes = [c_void_p] + class Key(object): + """An OpenSSL RSA key.""" def __init__(self, fp=None): @@ -141,23 +152,22 @@ def __init__(self, fp=None): self.public = False if not fp: return - if isinstance(fp, basestring): - if fp.startswith('-----'): - # PEM formatted text - self.raw = fp - else: - self.raw = open(fp, 'rb').read() + if isinstance(fp, six.binary_type) and fp.startswith('-----'.encode()): + # PEM formatted text + self.raw = fp + elif isinstance(fp, six.string_types): + self.raw = open(fp, 'rb').read() else: self.raw = fp.read() self._load_key() def _load_key(self): - if '\0' in self.raw: + if b'\0' in self.raw: # Raw string has embedded nulls, treat it as binary data buf = create_string_buffer(self.raw, len(self.raw)) else: buf = create_string_buffer(self.raw) - + bio = BIO_new_mem_buf(buf, len(buf)) try: self.key = PEM_read_bio_RSAPrivateKey(bio, 0, 0, 0) @@ -179,7 +189,10 @@ def generate(cls, size=1024, exp=RSA_F4): def private_encrypt(self, value, padding=RSA_PKCS1_PADDING): if self.public: raise SSLError('private method cannot be used on a public key') - buf = create_string_buffer(value, len(value)) + if six.PY3 and not isinstance(value, bytes): + buf = create_string_buffer(value.encode(), len(value)) + else: + buf = create_string_buffer(value, len(value)) size = RSA_size(self.key) output = create_string_buffer(size) ret = RSA_private_encrypt(len(buf), buf, output, self.key, padding) @@ -188,13 +201,19 @@ def private_encrypt(self, value, padding=RSA_PKCS1_PADDING): return output.raw[:ret] def public_decrypt(self, value, padding=RSA_PKCS1_PADDING): - buf = create_string_buffer(value, len(value)) + if six.PY3 and not isinstance(value, bytes): + buf = create_string_buffer(value.encode(), len(value)) + else: + buf = create_string_buffer(value, len(value)) size = RSA_size(self.key) output = create_string_buffer(size) ret = RSA_public_decrypt(len(buf), buf, output, self.key, padding) if ret <= 0: raise SSLError('Unable to decrypt data') - return output.raw[:ret] + if six.PY3 and isinstance(output.raw, bytes): + return output.raw[:ret].decode() + else: + return output.raw[:ret] def private_export(self): if self.public: diff --git a/chef/search.py b/chef/search.py index 1fa96a1..979f23c 100644 --- a/chef/search.py +++ b/chef/search.py @@ -1,6 +1,6 @@ import collections import copy -import urllib +import six.moves.urllib.parse from chef.api import ChefAPI from chef.base import ChefQuery, ChefObject @@ -51,7 +51,7 @@ def __init__(self, index, q='*:*', rows=1000, start=0, api=None): self.name = index self.api = api or ChefAPI.get_global() self._args = dict(q=q, rows=rows, start=start) - self.url = self.__class__.url + '/' + self.name + '?' + urllib.urlencode(self._args) + self.url = self.__class__.url + '/' + self.name + '?' + six.moves.urllib.parse.urlencode(self._args) @property def data(self): @@ -86,7 +86,7 @@ def __getitem__(self, value): if value.step is not None and value.step != 1: raise ValueError('Cannot use a step other than 1') return self.start(self._args['start']+value.start).rows(value.stop-value.start) - if isinstance(value, basestring): + if isinstance(value, str): return self[self.index(value)] row_value = self.data['rows'][value] # Check for null rows, just in case @@ -112,5 +112,5 @@ def __call__(self, query): @classmethod def list(cls, api=None): api = api or ChefAPI.get_global() - names = [name for name, url in api[cls.url].iteritems()] + names = [name for name, url in api[cls.url].items()] return ChefQuery(cls, names, api) diff --git a/chef/tests/__init__.py b/chef/tests/__init__.py index 71a3097..285a7db 100644 --- a/chef/tests/__init__.py +++ b/chef/tests/__init__.py @@ -50,12 +50,12 @@ def tearDown(self): for obj in self.objects: try: obj.delete() - except ChefError, e: - print e + except ChefError as e: + print(e) # Continue running def register(self, obj): self.objects.append(obj) def random(self, length=8, alphabet='0123456789abcdef'): - return ''.join(random.choice(alphabet) for _ in xrange(length)) + return ''.join(random.choice(alphabet) for _ in range(length)) diff --git a/chef/tests/test_api.py b/chef/tests/test_api.py index ffcb81a..53ed9c4 100644 --- a/chef/tests/test_api.py +++ b/chef/tests/test_api.py @@ -26,3 +26,9 @@ def test_env_variables(self): self.assertEqual(api.client, 'foobar') finally: del os.environ['_PYCHEF_TEST_'] + + def test_bad_key_raises(self): + invalids = [None, ''] + for item in invalids: + self.assertRaises( + ValueError, ChefAPI, 'foobar', item, 'user') diff --git a/chef/tests/test_data_bag.py b/chef/tests/test_data_bag.py index 1a50d29..6d46bd3 100644 --- a/chef/tests/test_data_bag.py +++ b/chef/tests/test_data_bag.py @@ -10,7 +10,7 @@ def test_list(self): def test_keys(self): bag = DataBag('test_1') - self.assertItemsEqual(bag.keys(), ['item_1', 'item_2']) + self.assertItemsEqual(list(bag.keys()), ['item_1', 'item_2']) self.assertItemsEqual(iter(bag), ['item_1', 'item_2']) def test_item(self): diff --git a/chef/tests/test_fabric.py b/chef/tests/test_fabric.py index 4508c6c..88a5ae4 100644 --- a/chef/tests/test_fabric.py +++ b/chef/tests/test_fabric.py @@ -18,9 +18,9 @@ def search_mock(index, q='*:*', *args, **kwargs): search_mock_inst.data = data return search_mock_inst MockSearch.side_effect = search_mock - print MockSearch('role').data + print(MockSearch('role').data) @mockSearch({('role', '*:*'): {1:2}}) def test_roledef2(self, MockSearch): - print MockSearch('role').data + print(MockSearch('role').data) diff --git a/chef/utils/json.py b/chef/utils/json.py index 1661042..c3be239 100644 --- a/chef/utils/json.py +++ b/chef/utils/json.py @@ -1,12 +1,13 @@ from __future__ import absolute_import import types +import collections try: import json except ImportError: import simplejson as json def maybe_call(x): - if callable(x): + if isinstance(x, collections.Callable): return x() return x diff --git a/docs/fabric.rst b/docs/fabric.rst index c6bc3e4..e790a27 100644 --- a/docs/fabric.rst +++ b/docs/fabric.rst @@ -7,3 +7,7 @@ Fabric Integration .. autofunction:: chef.fabric.chef_roledefs .. autofunction:: chef.fabric.chef_environment + +.. autofunction:: chef.fabric.chef_query + +.. autofunction:: chef.fabric.chef_tags diff --git a/setup.py b/setup.py index 66fe0de..9a774f2 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name = 'PyChef', - version = '0.2.2', + version = '0.2.4-dev', packages = find_packages(), author = 'Noah Kantrowitz', author_email = 'noah@coderanger.net', @@ -29,6 +29,7 @@ 'Programming Language :: Python', ], zip_safe = False, + install_requires = ['six>=1.9.0'], tests_require = ['unittest2', 'mock'], test_suite = 'unittest2.collector', ) From 1962698c970660110c4ff3ab70255ec631580a90 Mon Sep 17 00:00:00 2001 From: Marat Komarov Date: Wed, 12 Aug 2015 17:56:11 +0300 Subject: [PATCH 3/4] Added 3.3, 3.4 into travis.yml --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 4a12ed2..c4ca47c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,7 @@ language: python python: - 2.6 - 2.7 + - 3.3 + - 3.4 install: pip install versiontools --use-mirrors script: python setup.py test From e093c265859d2992806f4313ea0235729fde1417 Mon Sep 17 00:00:00 2001 From: Roma Koshel Date: Fri, 14 Aug 2015 12:05:42 +0300 Subject: [PATCH 4/4] Corrected PR remarks --- chef/api.py | 28 +++++++++---------------- chef/auth.py | 19 +++++------------ chef/base.py | 8 ++++---- chef/data_bag.py | 4 ++-- chef/fabric.py | 10 ++++----- chef/node.py | 3 ++- chef/rsa.py | 46 +++++++++++++++++------------------------- chef/search.py | 5 +++-- chef/tests/__init__.py | 3 ++- chef/utils/json.py | 3 +-- 10 files changed, 51 insertions(+), 78 deletions(-) diff --git a/chef/api.py b/chef/api.py index a462e4b..b79ffd8 100644 --- a/chef/api.py +++ b/chef/api.py @@ -1,5 +1,4 @@ import six - import datetime import logging import os @@ -10,7 +9,6 @@ import six.moves.urllib.request import six.moves.urllib.error import six.moves.urllib.parse - import weakref import pkg_resources @@ -30,7 +28,6 @@ puts Chef::Config.configuration.to_json """.strip() - def api_stack_value(): if not hasattr(api_stack, 'value'): api_stack.value = [] @@ -38,14 +35,11 @@ def api_stack_value(): class UnknownRubyExpression(Exception): - """Token exception for unprocessed Ruby expressions.""" class ChefRequest(six.moves.urllib.request.Request): - """Workaround for using PUT/DELETE with urllib2.""" - def __init__(self, *args, **kwargs): self._method = kwargs.pop('method', None) # Request is an old-style class, no super() allowed. @@ -58,7 +52,6 @@ def get_method(self): class ChefAPI(object): - """The ChefAPI object is a wrapper for a single Chef server. .. admonition:: The API stack @@ -87,7 +80,7 @@ def __init__(self, url, key, client, version='0.10.8', headers={}): self.key = key self.client = client self.version = version - self.headers = dict((k.lower(), v) for k, v in headers.items()) + self.headers = dict((k.lower(), v) for k, v in six.iteritems(headers)) self.version_parsed = pkg_resources.parse_version(self.version) self.platform = self.parsed_url.hostname == 'api.opscode.com' if not api_stack_value(): @@ -106,19 +99,18 @@ def from_config_file(cls, path): url = key_path = client_name = None for line in open(path): if not line.strip() or line.startswith('#'): - continue # Skip blanks and comments + continue # Skip blanks and comments parts = line.split(None, 1) if len(parts) != 2: - continue # Not a simple key/value, we can't parse it anyway + continue # Not a simple key/value, we can't parse it anyway key, value = parts md = cls.ruby_string_re.search(value) if md: value = md.group(2) else: # Not a string, don't even try - log.debug('Value for %s does not look like a string: %s' % (key, value)) + log.debug('Value for {0} does not look like a string: {1}'.format(key, value)) continue - def _ruby_value(match): expr = match.group(1).strip() if expr == 'current_dir': @@ -208,17 +200,17 @@ def _request(self, method, url, data, headers): def request(self, method, path, headers={}, data=None): auth_headers = sign_request(key=self.key, http_method=method, - path=self.parsed_url.path + path.split('?', 1)[0], body=data, - host=self.parsed_url.netloc, timestamp=datetime.datetime.utcnow(), - user_id=self.client) + path=self.parsed_url.path+path.split('?', 1)[0], body=data, + host=self.parsed_url.netloc, timestamp=datetime.datetime.utcnow(), + user_id=self.client) request_headers = {} request_headers.update(self.headers) - request_headers.update(dict((k.lower(), v) for k, v in headers.items())) + request_headers.update(dict((k.lower(), v) for k, v in six.iteritems(headers))) request_headers['x-chef-version'] = self.version request_headers.update(auth_headers) try: response = self._request(method, self.url + path, data, dict( - (k.capitalize(), v) for k, v in request_headers.items())) + (k.capitalize(), v) for k, v in six.iteritems(request_headers))) except six.moves.urllib.error.HTTPError as e: e.content = e.read() try: @@ -230,7 +222,7 @@ def request(self, method, path, headers={}, data=None): return response def api_request(self, method, path, headers={}, data=None): - headers = dict((k.lower(), v) for k, v in headers.items()) + headers = dict((k.lower(), v) for k, v in six.iteritems(headers)) headers['accept'] = 'application/json' if data is not None: headers['content-type'] = 'application/json' diff --git a/chef/auth.py b/chef/auth.py index e09f556..1f1d582 100644 --- a/chef/auth.py +++ b/chef/auth.py @@ -1,29 +1,25 @@ +import six.moves import base64 import datetime import hashlib import re - def _ruby_b64encode(value): """The Ruby function Base64.encode64 automatically breaks things up into 60-character chunks. """ b64 = base64.b64encode(value) - for i in range(0, len(b64), 60): - yield b64[i:i + 60].decode('utf-8') - + for i in six.moves.range(0, len(b64), 60): + yield b64[i:i + 60].decode() def ruby_b64encode(value): return '\n'.join(_ruby_b64encode(value)) - def sha1_base64(value): """An implementation of Mixlib::Authentication::Digester.""" - return ruby_b64encode(hashlib.sha1(value.encode('utf-8')).digest()) - + return ruby_b64encode(hashlib.sha1(value.encode()).digest()) class UTC(datetime.tzinfo): - """UTC timezone stub.""" ZERO = datetime.timedelta(0) @@ -39,22 +35,18 @@ def dst(self, dt): utc = UTC() - def canonical_time(timestamp): if timestamp.tzinfo is not None: timestamp = timestamp.astimezone(utc).replace(tzinfo=None) return timestamp.replace(microsecond=0).isoformat() + 'Z' canonical_path_regex = re.compile(r'/+') - - def canonical_path(path): path = canonical_path_regex.sub('/', path) if len(path) > 1: path = path.rstrip('/') return path - def canonical_request(http_method, path, hashed_body, timestamp, user_id): # Canonicalize request parameters http_method = http_method.upper() @@ -68,7 +60,6 @@ def canonical_request(http_method, path, hashed_body, timestamp, user_id): 'X-Ops-Timestamp:%(timestamp)s\n' 'X-Ops-UserId:%(user_id)s' % vars()) - def sign_request(key, http_method, path, body, host, timestamp, user_id): """Generate the needed headers for the Opscode authentication protocol.""" timestamp = canonical_time(timestamp) @@ -86,5 +77,5 @@ def sign_request(key, http_method, path, body, host, timestamp, user_id): req = canonical_request(http_method, path, hashed_body, timestamp, user_id) sig = _ruby_b64encode(key.private_encrypt(req)) for i, line in enumerate(sig): - headers['x-ops-authorization-%s' % (i + 1)] = line + headers['x-ops-authorization-%s'%(i+1)] = line return headers diff --git a/chef/base.py b/chef/base.py index be16407..bc6d4a6 100644 --- a/chef/base.py +++ b/chef/base.py @@ -62,7 +62,7 @@ def __init__(self, name, api=None, skip_load=False): self._populate(data) def _populate(self, data): - for name, cls in self.__class__.attributes.items(): + for name, cls in six.iteritems(self.__class__.attributes): if name in data: value = cls(data[name]) else: @@ -82,7 +82,7 @@ def list(cls, api=None): """ api = api or ChefAPI.get_global() cls._check_api_version(api) - names = [name for name, url in api[cls.url].items()] + names = [name for name, url in six.iteritems(api[cls.url])] return ChefQuery(cls, names, api) @classmethod @@ -93,7 +93,7 @@ def create(cls, name, api=None, **kwargs): api = api or ChefAPI.get_global() cls._check_api_version(api) obj = cls(name, api, skip_load=True) - for key, value in kwargs.items(): + for key, value in six.iteritems(kwargs): setattr(obj, key, value) api.api_request('POST', cls.url, data=obj) return obj @@ -121,7 +121,7 @@ def to_dict(self): 'json_class': 'Chef::'+self.__class__.__name__, 'chef_type': self.__class__.__name__.lower(), } - for attr in self.__class__.attributes.keys(): + for attr in six.iterkeys(self.__class__.attributes): d[attr] = getattr(self, attr) return d diff --git a/chef/data_bag.py b/chef/data_bag.py index e19c07e..0503640 100644 --- a/chef/data_bag.py +++ b/chef/data_bag.py @@ -19,7 +19,7 @@ class DataBag(six.with_metaclass(DataBagMeta, ChefObject, ChefQuery)): bag = DataBag('versions') item = bag['web'] - for name, item in bag.iteritems(): + for name, item in six.iteritems(bag): print item['qa_version'] """ @@ -96,7 +96,7 @@ def create(cls, bag, name, api=None, **kwargs): keyword arguments.""" api = api or ChefAPI.get_global() obj = cls(bag, name, api, skip_load=True) - for key, value in kwargs.items(): + for key, value in six.iteritems(kwargs): obj[key] = value obj['id'] = name api.api_request('POST', cls.url+'/'+str(bag), data=obj.raw_data) diff --git a/chef/fabric.py b/chef/fabric.py index 2b50c92..8d49e25 100644 --- a/chef/fabric.py +++ b/chef/fabric.py @@ -1,12 +1,10 @@ - - +import six import functools from chef.api import ChefAPI, autoconfigure from chef.environment import Environment from chef.exceptions import ChefError, ChefAPIVersionError from chef.search import Search -import collections try: from fabric.api import env, task, roles, output @@ -39,7 +37,7 @@ def __init__(self, query, api, hostname_attr, environment=None): self.query = query self.api = api self.hostname_attr = hostname_attr - if isinstance(self.hostname_attr, str): + if isinstance(self.hostname_attr, six.string_types): self.hostname_attr = (self.hostname_attr,) self.environment = environment @@ -52,7 +50,7 @@ def __call__(self): query += ' AND chef_environment:%s' % environment for row in Search('node', query, api=self.api): if row: - if isinstance(self.hostname_attr, collections.Callable): + if callable(self.hostname_attr): val = self.hostname_attr(row.object) if val: yield val @@ -192,7 +190,7 @@ def migrate(): .. versionadded:: 0.2.1 """ # Allow passing a single iterable - if len(tags) == 1 and not isinstance(tags[0], str): + if len(tags) == 1 and not isinstance(tags[0], six.string_types): tags = tags[0] query = ' AND '.join('tags:%s'%tag.strip() for tag in tags) return chef_query(query, **kwargs) diff --git a/chef/node.py b/chef/node.py index a2cb588..ea7666b 100644 --- a/chef/node.py +++ b/chef/node.py @@ -1,3 +1,4 @@ +import six import collections from chef.base import ChefObject @@ -27,7 +28,7 @@ def __init__(self, search_path=[], path=None, write=None): def __iter__(self): keys = set() for d in self.search_path: - keys |= set(d.keys()) + keys |= set(six.iterkeys(d)) return iter(keys) def __len__(self): diff --git a/chef/rsa.py b/chef/rsa.py index 65a67a3..32ad3c0 100644 --- a/chef/rsa.py +++ b/chef/rsa.py @@ -9,30 +9,28 @@ else: _eay = CDLL('libcrypto.so') -# unsigned long ERR_get_error(void); +#unsigned long ERR_get_error(void); ERR_get_error = _eay.ERR_get_error ERR_get_error.argtypes = [] ERR_get_error.restype = c_ulong -# void ERR_error_string_n(unsigned long e, char *buf, size_t len); +#void ERR_error_string_n(unsigned long e, char *buf, size_t len); ERR_error_string_n = _eay.ERR_error_string_n ERR_error_string_n.argtypes = [c_ulong, c_char_p, c_size_t] ERR_error_string_n.restype = None - class SSLError(Exception): - """An error in OpenSSL.""" def __init__(self, message, *args): - message = message % args + message = message%args err = ERR_get_error() if err: message += ':' while err: buf = create_string_buffer(120) ERR_error_string_n(err, buf, 120) - message += '\n%s' % string_at(buf, 119) + message += '\n%s'%string_at(buf, 119) err = ERR_get_error() super(SSLError, self).__init__(message) @@ -52,25 +50,21 @@ def __init__(self, message, *args): BIO_s_mem.argtypes = [] BIO_s_mem.restype = c_void_p -# long BIO_ctrl(BIO *bp,int cmd,long larg,void *parg); +#long BIO_ctrl(BIO *bp,int cmd,long larg,void *parg); BIO_ctrl = _eay.BIO_ctrl BIO_ctrl.argtypes = [c_void_p, c_int, c_long, c_void_p] BIO_ctrl.restype = c_long -# define BIO_CTRL_RESET 1 /* opt - rewind/zero etc */ +#define BIO_CTRL_RESET 1 /* opt - rewind/zero etc */ BIO_CTRL_RESET = 1 -# define BIO_CTRL_INFO 3 /* opt - extra tit-bits */ +##define BIO_CTRL_INFO 3 /* opt - extra tit-bits */ BIO_CTRL_INFO = 3 -# define BIO_reset(b) (int)BIO_ctrl(b,BIO_CTRL_RESET,0,NULL) - - +#define BIO_reset(b) (int)BIO_ctrl(b,BIO_CTRL_RESET,0,NULL) def BIO_reset(b): return BIO_ctrl(b, BIO_CTRL_RESET, 0, None) -# define BIO_get_mem_data(b,pp) BIO_ctrl(b,BIO_CTRL_INFO,0,(char *)pp) - - +##define BIO_get_mem_data(b,pp) BIO_ctrl(b,BIO_CTRL_INFO,0,(char *)pp) def BIO_get_mem_data(b, pp): return BIO_ctrl(b, BIO_CTRL_INFO, 0, pp) @@ -78,44 +72,42 @@ def BIO_get_mem_data(b, pp): BIO_free = _eay.BIO_free BIO_free.argtypes = [c_void_p] BIO_free.restype = c_int - - def BIO_free_errcheck(result, func, arguments): if result == 0: raise SSLError('Unable to free BIO') BIO_free.errcheck = BIO_free_errcheck -# RSA *PEM_read_bio_RSAPrivateKey(BIO *bp, RSA **x, +#RSA *PEM_read_bio_RSAPrivateKey(BIO *bp, RSA **x, # pem_password_cb *cb, void *u); PEM_read_bio_RSAPrivateKey = _eay.PEM_read_bio_RSAPrivateKey PEM_read_bio_RSAPrivateKey.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] PEM_read_bio_RSAPrivateKey.restype = c_void_p -# RSA *PEM_read_bio_RSAPublicKey(BIO *bp, RSA **x, +#RSA *PEM_read_bio_RSAPublicKey(BIO *bp, RSA **x, # pem_password_cb *cb, void *u); PEM_read_bio_RSAPublicKey = _eay.PEM_read_bio_RSAPublicKey PEM_read_bio_RSAPublicKey.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] PEM_read_bio_RSAPublicKey.restype = c_void_p -# int PEM_write_bio_RSAPrivateKey(BIO *bp, RSA *x, const EVP_CIPHER *enc, +#int PEM_write_bio_RSAPrivateKey(BIO *bp, RSA *x, const EVP_CIPHER *enc, # unsigned char *kstr, int klen, # pem_password_cb *cb, void *u); PEM_write_bio_RSAPrivateKey = _eay.PEM_write_bio_RSAPrivateKey PEM_write_bio_RSAPrivateKey.argtypes = [c_void_p, c_void_p, c_void_p, c_char_p, c_int, c_void_p, c_void_p] PEM_write_bio_RSAPrivateKey.restype = c_int -# int PEM_write_bio_RSAPublicKey(BIO *bp, RSA *x); +#int PEM_write_bio_RSAPublicKey(BIO *bp, RSA *x); PEM_write_bio_RSAPublicKey = _eay.PEM_write_bio_RSAPublicKey PEM_write_bio_RSAPublicKey.argtypes = [c_void_p, c_void_p] PEM_write_bio_RSAPublicKey.restype = c_int -# int RSA_private_encrypt(int flen, unsigned char *from, +#int RSA_private_encrypt(int flen, unsigned char *from, # unsigned char *to, RSA *rsa,int padding); RSA_private_encrypt = _eay.RSA_private_encrypt RSA_private_encrypt.argtypes = [c_int, c_void_p, c_void_p, c_void_p, c_int] RSA_private_encrypt.restype = c_int -# int RSA_public_decrypt(int flen, unsigned char *from, +#int RSA_public_decrypt(int flen, unsigned char *from, # unsigned char *to, RSA *rsa, int padding); RSA_public_decrypt = _eay.RSA_public_decrypt RSA_public_decrypt.argtypes = [c_int, c_void_p, c_void_p, c_void_p, c_int] @@ -129,22 +121,20 @@ def BIO_free_errcheck(result, func, arguments): RSA_size.argtypes = [c_void_p] RSA_size.restype = c_int -# RSA *RSA_generate_key(int num, unsigned long e, +#RSA *RSA_generate_key(int num, unsigned long e, # void (*callback)(int,int,void *), void *cb_arg); RSA_generate_key = _eay.RSA_generate_key RSA_generate_key.argtypes = [c_int, c_ulong, c_void_p, c_void_p] RSA_generate_key.restype = c_void_p -# define RSA_F4 0x10001L +##define RSA_F4 0x10001L RSA_F4 = 0x10001 # void RSA_free(RSA *rsa); RSA_free = _eay.RSA_free RSA_free.argtypes = [c_void_p] - class Key(object): - """An OpenSSL RSA key.""" def __init__(self, fp=None): @@ -152,7 +142,7 @@ def __init__(self, fp=None): self.public = False if not fp: return - if isinstance(fp, six.binary_type) and fp.startswith('-----'.encode()): + if isinstance(fp, six.binary_type) and fp.startswith(b'-----'): # PEM formatted text self.raw = fp elif isinstance(fp, six.string_types): diff --git a/chef/search.py b/chef/search.py index 979f23c..ee1d940 100644 --- a/chef/search.py +++ b/chef/search.py @@ -1,3 +1,4 @@ +import six import collections import copy import six.moves.urllib.parse @@ -86,7 +87,7 @@ def __getitem__(self, value): if value.step is not None and value.step != 1: raise ValueError('Cannot use a step other than 1') return self.start(self._args['start']+value.start).rows(value.stop-value.start) - if isinstance(value, str): + if isinstance(value, six.string_types): return self[self.index(value)] row_value = self.data['rows'][value] # Check for null rows, just in case @@ -112,5 +113,5 @@ def __call__(self, query): @classmethod def list(cls, api=None): api = api or ChefAPI.get_global() - names = [name for name, url in api[cls.url].items()] + names = [name for name, url in six.iteritems(api[cls.url])] return ChefQuery(cls, names, api) diff --git a/chef/tests/__init__.py b/chef/tests/__init__.py index 285a7db..f5fddab 100644 --- a/chef/tests/__init__.py +++ b/chef/tests/__init__.py @@ -1,3 +1,4 @@ +import six.moves import os import random from functools import wraps @@ -58,4 +59,4 @@ def register(self, obj): self.objects.append(obj) def random(self, length=8, alphabet='0123456789abcdef'): - return ''.join(random.choice(alphabet) for _ in range(length)) + return ''.join(random.choice(alphabet) for _ in six.moves.range(length)) diff --git a/chef/utils/json.py b/chef/utils/json.py index c3be239..1661042 100644 --- a/chef/utils/json.py +++ b/chef/utils/json.py @@ -1,13 +1,12 @@ from __future__ import absolute_import import types -import collections try: import json except ImportError: import simplejson as json def maybe_call(x): - if isinstance(x, collections.Callable): + if callable(x): return x() return x