/usr/lib/python2.7/dist-packages/chef/api.py is in python-chef 0.2.3-3.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | import copy
import datetime
import itertools
import logging
import os
import re
import socket
import subprocess
import threading
import urllib2
import urlparse
import weakref
import pkg_resources
from chef.auth import sign_request
from chef.exceptions import ChefServerError
from chef.rsa import Key
from chef.utils import json
from chef.utils.file import walk_backwards
api_stack = threading.local()
log = logging.getLogger('chef.api')
config_ruby_script = """
require 'chef'
Chef::Config.from_file('%s')
puts Chef::Config.configuration.to_json
""".strip()
def api_stack_value():
if not hasattr(api_stack, 'value'):
api_stack.value = []
return api_stack.value
class UnknownRubyExpression(Exception):
"""Token exception for unprocessed Ruby expressions."""
class ChefRequest(urllib2.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)
def get_method(self):
if self._method:
return self._method
return urllib2.Request.get_method(self)
class ChefAPI(object):
"""The ChefAPI object is a wrapper for a single Chef server.
.. admonition:: The API stack
PyChef maintains a stack of :class:`ChefAPI` objects to be use with
other methods if an API object isn't given explicitly. The first
ChefAPI created will become the default, though you can set a specific
default using :meth:`ChefAPI.set_default`. You can also use a ChefAPI
as a context manager to create a scoped default::
with ChefAPI('http://localhost:4000', 'client.pem', 'admin'):
n = Node('web1')
"""
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)
if not isinstance(key, Key):
key = Key(key)
self.key = key
self.client = client
self.version = version
self.headers = dict((k.lower(), v) for k, v in headers.iteritems())
self.version_parsed = pkg_resources.parse_version(self.version)
self.platform = self.parsed_url.hostname == 'api.opscode.com'
if not api_stack_value():
self.set_default()
@classmethod
def from_config_file(cls, path):
"""Load Chef API paraters from a config file. Returns None if the
config can't be used.
"""
log.debug('Trying to load from "%s"', path)
if not os.path.isfile(path) or not os.access(path, os.R_OK):
# Can't even read the config file
log.debug('Unable to read config file "%s"', path)
return
url = key_path = client_name = None
for line in open(path):
if not line.strip() or line.startswith('#'):
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
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))
continue
def _ruby_value(match):
expr = match.group(1).strip()
if expr == 'current_dir':
return os.path.dirname(path)
envmatch = cls.env_value_re.match(expr)
if envmatch:
envmatch = envmatch.group(1).strip('"').strip("'")
return os.environ.get(envmatch) or ''
log.debug('Unknown ruby expression in line "%s"', line)
raise UnknownRubyExpression
try:
value = cls.ruby_value_re.sub(_ruby_value, value)
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 and client_name and key_path):
# No URL, no chance this was valid, try running Ruby
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')
return
if not key_path:
# Try and use ./client.pem
key_path = os.path.join(os.path.dirname(path), 'client.pem')
if not os.path.isfile(key_path) or not os.access(key_path, os.R_OK):
# Can't read the client key
log.debug('Unable to read key file "%s"', key_path)
return
if not client_name:
client_name = socket.getfqdn()
return cls(url, key_path, client_name)
@staticmethod
def get_global():
"""Return the API on the top of the stack."""
while api_stack_value():
api = api_stack_value()[-1]()
if api is not None:
return api
del api_stack_value()[-1]
def set_default(self):
"""Make this the default API in the stack. Returns the old default if any."""
old = None
if api_stack_value():
old = api_stack_value().pop(0)
api_stack_value().insert(0, weakref.ref(self))
return old
def __enter__(self):
api_stack_value().append(weakref.ref(self))
return self
def __exit__(self, type, value, traceback):
del api_stack_value()[-1]
def _request(self, method, url, data, headers):
# Testing hook, subclass and override for WSGI intercept
request = ChefRequest(url, data, headers, method=method)
return urllib2.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)
request_headers = {}
request_headers.update(self.headers)
request_headers.update(dict((k.lower(), v) for k, v in headers.iteritems()))
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:
e.content = e.read()
try:
e.content = json.loads(e.content)
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())
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)
def __getitem__(self, path):
return self.api_request('GET', path)
def autoconfigure(base_path=None):
"""Try to find a knife or chef-client config file to load parameters from,
starting from either the given base path or the current working directory.
The lookup order mirrors the one from Chef, first all folders from the base
path are walked back looking for .chef/knife.rb, then ~/.chef/knife.rb,
and finally /etc/chef/client.rb.
The first file that is found and can be loaded successfully will be loaded
into a :class:`ChefAPI` object.
"""
base_path = base_path or os.getcwd()
# Scan up the tree for a knife.rb or client.rb. If that fails try looking
# in /etc/chef. The /etc/chef check will never work in Win32, but it doesn't
# hurt either.
for path in walk_backwards(base_path):
config_path = os.path.join(path, '.chef', 'knife.rb')
api = ChefAPI.from_config_file(config_path)
if api is not None:
return api
# The walk didn't work, try ~/.chef/knife.rb
config_path = os.path.expanduser(os.path.join('~', '.chef', 'knife.rb'))
api = ChefAPI.from_config_file(config_path)
if api is not None:
return api
# Nothing in the home dir, try /etc/chef/client.rb
config_path = os.path.join(os.path.sep, 'etc', 'chef', 'client.rb')
api = ChefAPI.from_config_file(config_path)
if api is not None:
return api
|