Skip to content
This repository was archived by the owner on May 9, 2020. It is now read-only.

Commit c4a89a0

Browse files
Roma KoshelMarat Komarov
authored andcommitted
Added Python 3 support
PyChef now supports both Python 2/3. New dependency `six` for 2/3 compatibility
1 parent b2b209f commit c4a89a0

File tree

17 files changed

+203
-104
lines changed

17 files changed

+203
-104
lines changed

chef/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright (c) 2010 Noah Kantrowitz <noah@coderanger.net>
22

3-
__version__ = (0, 2, 2, 'dev')
3+
__version__ = (0, 2, 3, 'dev')
44

55
from chef.api import ChefAPI, autoconfigure
66
from chef.client import Client

chef/api.py

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import copy
1+
import six
2+
23
import datetime
3-
import itertools
44
import logging
55
import os
66
import re
77
import socket
88
import subprocess
99
import threading
10-
import urllib2
11-
import urlparse
10+
import six.moves.urllib.request
11+
import six.moves.urllib.error
12+
import six.moves.urllib.parse
13+
1214
import weakref
1315

1416
import pkg_resources
@@ -28,30 +30,35 @@
2830
puts Chef::Config.configuration.to_json
2931
""".strip()
3032

33+
3134
def api_stack_value():
3235
if not hasattr(api_stack, 'value'):
3336
api_stack.value = []
3437
return api_stack.value
3538

3639

3740
class UnknownRubyExpression(Exception):
41+
3842
"""Token exception for unprocessed Ruby expressions."""
3943

4044

41-
class ChefRequest(urllib2.Request):
45+
class ChefRequest(six.moves.urllib.request.Request):
46+
4247
"""Workaround for using PUT/DELETE with urllib2."""
48+
4349
def __init__(self, *args, **kwargs):
4450
self._method = kwargs.pop('method', None)
4551
# Request is an old-style class, no super() allowed.
46-
urllib2.Request.__init__(self, *args, **kwargs)
52+
six.moves.urllib.request.Request.__init__(self, *args, **kwargs)
4753

4854
def get_method(self):
4955
if self._method:
5056
return self._method
51-
return urllib2.Request.get_method(self)
57+
return six.moves.urllib.request.Request.get_method(self)
5258

5359

5460
class ChefAPI(object):
61+
5562
"""The ChefAPI object is a wrapper for a single Chef server.
5663
5764
.. admonition:: The API stack
@@ -68,16 +75,19 @@ class ChefAPI(object):
6875

6976
ruby_value_re = re.compile(r'#\{([^}]+)\}')
7077
env_value_re = re.compile(r'ENV\[(.+)\]')
78+
ruby_string_re = re.compile(r'^\s*(["\'])(.*?)\1\s*$')
7179

7280
def __init__(self, url, key, client, version='0.10.8', headers={}):
7381
self.url = url.rstrip('/')
74-
self.parsed_url = urlparse.urlparse(self.url)
82+
self.parsed_url = six.moves.urllib.parse.urlparse(self.url)
7583
if not isinstance(key, Key):
7684
key = Key(key)
85+
if not key.key:
86+
raise ValueError("ChefAPI attribute 'key' was invalid.")
7787
self.key = key
7888
self.client = client
7989
self.version = version
80-
self.headers = dict((k.lower(), v) for k, v in headers.iteritems())
90+
self.headers = dict((k.lower(), v) for k, v in headers.items())
8191
self.version_parsed = pkg_resources.parse_version(self.version)
8292
self.platform = self.parsed_url.hostname == 'api.opscode.com'
8393
if not api_stack_value():
@@ -96,12 +106,19 @@ def from_config_file(cls, path):
96106
url = key_path = client_name = None
97107
for line in open(path):
98108
if not line.strip() or line.startswith('#'):
99-
continue # Skip blanks and comments
109+
continue # Skip blanks and comments
100110
parts = line.split(None, 1)
101111
if len(parts) != 2:
102-
continue # Not a simple key/value, we can't parse it anyway
112+
continue # Not a simple key/value, we can't parse it anyway
103113
key, value = parts
104-
value = value.strip().strip('"\'')
114+
md = cls.ruby_string_re.search(value)
115+
if md:
116+
value = md.group(2)
117+
else:
118+
# Not a string, don't even try
119+
log.debug('Value for %s does not look like a string: %s' % (key, value))
120+
continue
121+
105122
def _ruby_value(match):
106123
expr = match.group(1).strip()
107124
if expr == 'current_dir':
@@ -117,25 +134,32 @@ def _ruby_value(match):
117134
except UnknownRubyExpression:
118135
continue
119136
if key == 'chef_server_url':
137+
log.debug('Found URL: %r', value)
120138
url = value
121139
elif key == 'node_name':
140+
log.debug('Found client name: %r', value)
122141
client_name = value
123142
elif key == 'client_key':
143+
log.debug('Found key path: %r', value)
124144
key_path = value
125145
if not os.path.isabs(key_path):
126146
# Relative paths are relative to the config file
127147
key_path = os.path.abspath(os.path.join(os.path.dirname(path), key_path))
128-
if not url:
148+
if not (url and client_name and key_path):
129149
# No URL, no chance this was valid, try running Ruby
130-
log.debug('No Chef server URL found, trying Ruby parse')
150+
log.debug('No Chef server config found, trying Ruby parse')
151+
url = key_path = client_name = None
131152
proc = subprocess.Popen('ruby', stdin=subprocess.PIPE, stdout=subprocess.PIPE)
132153
script = config_ruby_script % path.replace('\\', '\\\\').replace("'", "\\'")
133154
out, err = proc.communicate(script)
134155
if proc.returncode == 0 and out.strip():
135156
data = json.loads(out)
157+
log.debug('Ruby parse succeeded with %r', data)
136158
url = data.get('chef_server_url')
137159
client_name = data.get('node_name')
138160
key_path = data.get('client_key')
161+
else:
162+
log.debug('Ruby parse failed with exit code %s: %s', proc.returncode, out.strip())
139163
if not url:
140164
# Still no URL, can't use this config
141165
log.debug('Still no Chef server URL found')
@@ -177,39 +201,42 @@ def __exit__(self, type, value, traceback):
177201

178202
def _request(self, method, url, data, headers):
179203
# Testing hook, subclass and override for WSGI intercept
204+
if six.PY3 and data:
205+
data = data.encode()
180206
request = ChefRequest(url, data, headers, method=method)
181-
return urllib2.urlopen(request).read()
207+
return six.moves.urllib.request.urlopen(request).read()
182208

183209
def request(self, method, path, headers={}, data=None):
184210
auth_headers = sign_request(key=self.key, http_method=method,
185-
path=self.parsed_url.path+path.split('?', 1)[0], body=data,
186-
host=self.parsed_url.netloc, timestamp=datetime.datetime.utcnow(),
187-
user_id=self.client)
211+
path=self.parsed_url.path + path.split('?', 1)[0], body=data,
212+
host=self.parsed_url.netloc, timestamp=datetime.datetime.utcnow(),
213+
user_id=self.client)
188214
request_headers = {}
189215
request_headers.update(self.headers)
190-
request_headers.update(dict((k.lower(), v) for k, v in headers.iteritems()))
216+
request_headers.update(dict((k.lower(), v) for k, v in headers.items()))
191217
request_headers['x-chef-version'] = self.version
192218
request_headers.update(auth_headers)
193219
try:
194-
response = self._request(method, self.url+path, data, dict((k.capitalize(), v) for k, v in request_headers.iteritems()))
195-
except urllib2.HTTPError, e:
220+
response = self._request(method, self.url + path, data, dict(
221+
(k.capitalize(), v) for k, v in request_headers.items()))
222+
except six.moves.urllib.error.HTTPError as e:
196223
e.content = e.read()
197224
try:
198-
e.content = json.loads(e.content)
225+
e.content = json.loads(e.content.decode())
199226
raise ChefServerError.from_error(e.content['error'], code=e.code)
200227
except ValueError:
201228
pass
202229
raise e
203230
return response
204231

205-
def api_request(self, method, path, headers={}, data=None):
206-
headers = dict((k.lower(), v) for k, v in headers.iteritems())
232+
def api_request(self, method, path, headers={}, data=None):
233+
headers = dict((k.lower(), v) for k, v in headers.items())
207234
headers['accept'] = 'application/json'
208235
if data is not None:
209236
headers['content-type'] = 'application/json'
210237
data = json.dumps(data)
211238
response = self.request(method, path, headers, data)
212-
return json.loads(response)
239+
return json.loads(response.decode())
213240

214241
def __getitem__(self, path):
215242
return self.api_request('GET', path)

chef/auth.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,29 @@
33
import hashlib
44
import re
55

6+
67
def _ruby_b64encode(value):
78
"""The Ruby function Base64.encode64 automatically breaks things up
89
into 60-character chunks.
910
"""
1011
b64 = base64.b64encode(value)
11-
for i in xrange(0, len(b64), 60):
12-
yield b64[i:i+60]
12+
for i in range(0, len(b64), 60):
13+
yield b64[i:i + 60].decode('utf-8')
14+
1315

1416
def ruby_b64encode(value):
1517
return '\n'.join(_ruby_b64encode(value))
1618

19+
1720
def sha1_base64(value):
1821
"""An implementation of Mixlib::Authentication::Digester."""
19-
return ruby_b64encode(hashlib.sha1(value).digest())
22+
return ruby_b64encode(hashlib.sha1(value.encode('utf-8')).digest())
23+
2024

2125
class UTC(datetime.tzinfo):
26+
2227
"""UTC timezone stub."""
23-
28+
2429
ZERO = datetime.timedelta(0)
2530

2631
def utcoffset(self, dt):
@@ -34,18 +39,22 @@ def dst(self, dt):
3439

3540
utc = UTC()
3641

42+
3743
def canonical_time(timestamp):
3844
if timestamp.tzinfo is not None:
3945
timestamp = timestamp.astimezone(utc).replace(tzinfo=None)
4046
return timestamp.replace(microsecond=0).isoformat() + 'Z'
4147

4248
canonical_path_regex = re.compile(r'/+')
49+
50+
4351
def canonical_path(path):
4452
path = canonical_path_regex.sub('/', path)
4553
if len(path) > 1:
4654
path = path.rstrip('/')
4755
return path
4856

57+
4958
def canonical_request(http_method, path, hashed_body, timestamp, user_id):
5059
# Canonicalize request parameters
5160
http_method = http_method.upper()
@@ -59,22 +68,23 @@ def canonical_request(http_method, path, hashed_body, timestamp, user_id):
5968
'X-Ops-Timestamp:%(timestamp)s\n'
6069
'X-Ops-UserId:%(user_id)s' % vars())
6170

71+
6272
def sign_request(key, http_method, path, body, host, timestamp, user_id):
6373
"""Generate the needed headers for the Opscode authentication protocol."""
6474
timestamp = canonical_time(timestamp)
6575
hashed_body = sha1_base64(body or '')
66-
76+
6777
# Simple headers
6878
headers = {
6979
'x-ops-sign': 'version=1.0',
7080
'x-ops-userid': user_id,
7181
'x-ops-timestamp': timestamp,
7282
'x-ops-content-hash': hashed_body,
7383
}
74-
84+
7585
# Create RSA signature
7686
req = canonical_request(http_method, path, hashed_body, timestamp, user_id)
7787
sig = _ruby_b64encode(key.private_encrypt(req))
7888
for i, line in enumerate(sig):
79-
headers['x-ops-authorization-%s'%(i+1)] = line
89+
headers['x-ops-authorization-%s' % (i + 1)] = line
8090
return headers

chef/base.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import six
12
import collections
23

34
import pkg_resources
@@ -34,10 +35,8 @@ def __init__(cls, name, bases, d):
3435
cls.api_version_parsed = pkg_resources.parse_version(cls.api_version)
3536

3637

37-
class ChefObject(object):
38+
class ChefObject(six.with_metaclass(ChefObjectMeta, object)):
3839
"""A base class for Chef API objects."""
39-
40-
__metaclass__ = ChefObjectMeta
4140
types = {}
4241

4342
url = ''
@@ -63,7 +62,7 @@ def __init__(self, name, api=None, skip_load=False):
6362
self._populate(data)
6463

6564
def _populate(self, data):
66-
for name, cls in self.__class__.attributes.iteritems():
65+
for name, cls in self.__class__.attributes.items():
6766
if name in data:
6867
value = cls(data[name])
6968
else:
@@ -83,7 +82,7 @@ def list(cls, api=None):
8382
"""
8483
api = api or ChefAPI.get_global()
8584
cls._check_api_version(api)
86-
names = [name for name, url in api[cls.url].iteritems()]
85+
names = [name for name, url in api[cls.url].items()]
8786
return ChefQuery(cls, names, api)
8887

8988
@classmethod
@@ -94,7 +93,7 @@ def create(cls, name, api=None, **kwargs):
9493
api = api or ChefAPI.get_global()
9594
cls._check_api_version(api)
9695
obj = cls(name, api, skip_load=True)
97-
for key, value in kwargs.iteritems():
96+
for key, value in kwargs.items():
9897
setattr(obj, key, value)
9998
api.api_request('POST', cls.url, data=obj)
10099
return obj
@@ -106,7 +105,7 @@ def save(self, api=None):
106105
api = api or self.api
107106
try:
108107
api.api_request('PUT', self.url, data=self)
109-
except ChefServerNotFoundError, e:
108+
except ChefServerNotFoundError as e:
110109
# If you get a 404 during a save, just create it instead
111110
# This mirrors the logic in the Chef code
112111
api.api_request('POST', self.__class__.url, data=self)
@@ -122,7 +121,7 @@ def to_dict(self):
122121
'json_class': 'Chef::'+self.__class__.__name__,
123122
'chef_type': self.__class__.__name__.lower(),
124123
}
125-
for attr in self.__class__.attributes.iterkeys():
124+
for attr in self.__class__.attributes.keys():
126125
d[attr] = getattr(self, attr)
127126
return d
128127

@@ -138,4 +137,4 @@ def _check_api_version(cls, api):
138137
# use for creating Chef objects without an API connection (just for
139138
# serialization perhaps).
140139
if api and cls.api_version_parsed > api.version_parsed:
141-
raise ChefAPIVersionError, "Class %s is not compatible with API version %s" % (cls.__name__, api.version)
140+
raise ChefAPIVersionError("Class %s is not compatible with API version %s" % (cls.__name__, api.version))

0 commit comments

Comments
 (0)