python-ironicclient-0.1.2/0000775000175400017540000000000012303005362016643 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/python_ironicclient.egg-info/0000775000175400017540000000000012303005362024420 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/python_ironicclient.egg-info/dependency_links.txt0000664000175400017540000000000112303005362030466 0ustar jenkinsjenkins00000000000000 python-ironicclient-0.1.2/python_ironicclient.egg-info/not-zip-safe0000664000175400017540000000000112303005362026646 0ustar jenkinsjenkins00000000000000 python-ironicclient-0.1.2/python_ironicclient.egg-info/PKG-INFO0000664000175400017540000000173512303005362025523 0ustar jenkinsjenkins00000000000000Metadata-Version: 1.1 Name: python-ironicclient Version: 0.1.2 Summary: OpenStack Bare Metal Provisioning API Client Library Home-page: http://www.openstack.org/ Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: UNKNOWN Description: Python bindings for the Ironic API ================================== A python and command line client library for Ironic. Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 python-ironicclient-0.1.2/python_ironicclient.egg-info/entry_points.txt0000664000175400017540000000006412303005362027716 0ustar jenkinsjenkins00000000000000[console_scripts] ironic = ironicclient.shell:main python-ironicclient-0.1.2/python_ironicclient.egg-info/top_level.txt0000664000175400017540000000001512303005362027146 0ustar jenkinsjenkins00000000000000ironicclient python-ironicclient-0.1.2/python_ironicclient.egg-info/requires.txt0000664000175400017540000000016012303005362027015 0ustar jenkinsjenkins00000000000000pbr>=0.5.21,<1.0 anyjson>=0.3.3 httplib2 lxml>=2.3 PrettyTable>=0.7,<0.8 python-keystoneclient>=0.4.2 six>=1.4.1python-ironicclient-0.1.2/python_ironicclient.egg-info/SOURCES.txt0000664000175400017540000000416612303005362026313 0ustar jenkinsjenkins00000000000000.testr.conf AUTHORS ChangeLog LICENSE MANIFEST.in README.rst openstack-common.conf requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/source/conf.py ironicclient/__init__.py ironicclient/client.py ironicclient/exc.py ironicclient/shell.py ironicclient/common/__init__.py ironicclient/common/base.py ironicclient/common/http.py ironicclient/common/utils.py ironicclient/openstack/__init__.py ironicclient/openstack/common/__init__.py ironicclient/openstack/common/gettextutils.py ironicclient/openstack/common/importutils.py ironicclient/openstack/common/strutils.py ironicclient/openstack/common/apiclient/__init__.py ironicclient/openstack/common/apiclient/auth.py ironicclient/openstack/common/apiclient/base.py ironicclient/openstack/common/apiclient/client.py ironicclient/openstack/common/apiclient/exceptions.py ironicclient/openstack/common/apiclient/fake_client.py ironicclient/openstack/common/py3kcompat/__init__.py ironicclient/openstack/common/py3kcompat/urlutils.py ironicclient/tests/__init__.py ironicclient/tests/test_client.py ironicclient/tests/test_http.py ironicclient/tests/test_shell.py ironicclient/tests/test_utils.py ironicclient/tests/utils.py ironicclient/tests/v1/__init__.py ironicclient/tests/v1/test_chassis.py ironicclient/tests/v1/test_chassis_shell.py ironicclient/tests/v1/test_driver.py ironicclient/tests/v1/test_node.py ironicclient/tests/v1/test_node_shell.py ironicclient/tests/v1/test_port.py ironicclient/tests/v1/test_port_shell.py ironicclient/v1/__init__.py ironicclient/v1/chassis.py ironicclient/v1/chassis_shell.py ironicclient/v1/client.py ironicclient/v1/driver.py ironicclient/v1/driver_shell.py ironicclient/v1/node.py ironicclient/v1/node_shell.py ironicclient/v1/port.py ironicclient/v1/port_shell.py ironicclient/v1/shell.py python_ironicclient.egg-info/PKG-INFO python_ironicclient.egg-info/SOURCES.txt python_ironicclient.egg-info/dependency_links.txt python_ironicclient.egg-info/entry_points.txt python_ironicclient.egg-info/not-zip-safe python_ironicclient.egg-info/requires.txt python_ironicclient.egg-info/top_level.txt tools/__init__.py tools/install_venv_common.py tools/with_venv.shpython-ironicclient-0.1.2/ironicclient/0000775000175400017540000000000012303005362021325 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/common/0000775000175400017540000000000012303005362022615 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/common/utils.py0000664000175400017540000001570712303005215024336 0ustar jenkinsjenkins00000000000000# Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from __future__ import print_function import argparse import os import sys import textwrap import uuid import prettytable import six from ironicclient import exc from ironicclient.openstack.common import importutils class HelpFormatter(argparse.HelpFormatter): def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) super(HelpFormatter, self).start_section(heading) def define_command(subparsers, command, callback, cmd_mapper): '''Define a command in the subparsers collection. :param subparsers: subparsers collection where the command will go :param command: command name :param callback: function that will be used to process the command ''' desc = callback.__doc__ or '' help = desc.strip().split('\n')[0] arguments = getattr(callback, 'arguments', []) subparser = subparsers.add_parser(command, help=help, description=desc, add_help=False, formatter_class=HelpFormatter) subparser.add_argument('-h', '--help', action='help', help=argparse.SUPPRESS) cmd_mapper[command] = subparser for (args, kwargs) in arguments: subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) def define_commands_from_module(subparsers, command_module, cmd_mapper): '''Find all methods beginning with 'do_' in a module, and add them as commands into a subparsers collection. ''' for method_name in (a for a in dir(command_module) if a.startswith('do_')): # Commands should be hypen-separated instead of underscores. command = method_name[3:].replace('_', '-') callback = getattr(command_module, method_name) define_command(subparsers, command, callback, cmd_mapper) # Decorator for cli-args def arg(*args, **kwargs): def _decorator(func): # Because of the sematics of decorator composition if we just append # to the options list positional options will appear to be backwards. func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) return func return _decorator def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) def print_list(objs, fields, field_labels, formatters={}, sortby=0, outfile=None): pt = prettytable.PrettyTable([f for f in field_labels], caching=False, print_empty=False) pt.align = 'l' for o in objs: row = [] for field in fields: if field in formatters: row.append(formatters[field](o)) else: data = getattr(o, field, '') row.append(data) pt.add_row(row) print(pt.get_string(sortby=field_labels[sortby]), file=outfile) def print_dict(d, dict_property="Property", wrap=0, outfile=None): pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False, print_empty=False) pt.align = 'l' for k, v in sorted(six.iteritems(d), key=lambda x: x[0]): # convert dict to str to check length if isinstance(v, dict): v = six.text_type(v) if wrap > 0: v = textwrap.fill(six.text_type(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, six.string_types) and r'\n' in v: lines = v.strip().split(r'\n') col1 = k for line in lines: pt.add_row([col1, line]) col1 = '' else: pt.add_row([k, v]) print(pt.get_string(), file=outfile) def find_resource(manager, name_or_id): """Helper for the _find_* methods.""" # first try to get entity as integer id try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id)) except exc.NotFound: pass # now try to get entity as uuid try: uuid.UUID(str(name_or_id)) return manager.get(name_or_id) except (ValueError, exc.NotFound): pass # finally try to find entity by name try: return manager.find(name=name_or_id) except exc.NotFound: msg = (_("No %(class)s with a name or ID of '%(nameid)s' exists") % {'class': manager.resource_class.__name__.lower(), 'nameid': name_or_id}) raise exc.CommandError(msg) def env(*vars, **kwargs): """Search for the first defined of possibly many env vars Returns the first environment variable defined in vars, or returns the default defined in kwargs. """ for v in vars: value = os.environ.get(v, None) if value: return value return kwargs.get('default', '') def import_versioned_module(version, submodule=None): module = 'ironicclient.v%s' % version if submodule: module = '.'.join((module, submodule)) return importutils.import_module(module) def args_array_to_dict(kwargs, key_to_convert): values_to_convert = kwargs.get(key_to_convert) if values_to_convert: try: kwargs[key_to_convert] = dict(v.split("=", 1) for v in values_to_convert) except ValueError: raise exc.CommandError( _('%(key)s must be a list of KEY=VALUE not "%(values)s"') % {'key': key_to_convert, 'values': values_to_convert}) return kwargs def args_array_to_patch(op, attributes): patch = [] for attr in attributes: # Sanitize if not attr.startswith('/'): attr = '/' + attr if op in ['add', 'replace']: try: path, value = attr.split("=", 1) patch.append({'op': op, 'path': path, 'value': value}) except ValueError: raise exc.CommandError(_('Attributes must be a list of ' 'PATH=VALUE not "%s"') % attr) elif op == "remove": # For remove only the key is needed patch.append({'op': op, 'path': attr}) else: raise exc.CommandError(_('Unknown PATCH operation: %s') % op) return patch def exit(msg=''): if msg: print(msg, file=sys.stderr) sys.exit(1) python-ironicclient-0.1.2/ironicclient/common/base.py0000664000175400017540000000467512303005215024112 0ustar jenkinsjenkins00000000000000# Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Base utilities to build API operation managers and objects on top of. """ import copy from ironicclient.openstack.common.apiclient import base def getid(obj): """Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ try: return obj.id except AttributeError: return obj class Manager(object): """Managers interact with a particular type of API and provide CRUD operations for them. """ resource_class = None def __init__(self, api): self.api = api def _create(self, url, body): resp, body = self.api.json_request('POST', url, body=body) if body: return self.resource_class(self, body) def _list(self, url, response_key=None, obj_class=None, body=None): resp, body = self.api.json_request('GET', url) if obj_class is None: obj_class = self.resource_class if response_key: try: data = body[response_key] except KeyError: return [] else: data = body if not isinstance(data, list): data = [data] return [obj_class(self, res, loaded=True) for res in data if res] def _update(self, url, body, method='PATCH', response_key=None): resp, body = self.api.json_request(method, url, body=body) # PATCH/PUT requests may not return a body if body: return self.resource_class(self, body) def _delete(self, url): self.api.raw_request('DELETE', url) class Resource(base.Resource): """A resource represents a particular instance of an object (tenant, user, etc). This is pretty much just a bag for attributes. """ def to_dict(self): return copy.deepcopy(self._info) python-ironicclient-0.1.2/ironicclient/common/http.py0000664000175400017540000002403512303005215024147 0ustar jenkinsjenkins00000000000000# Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import json import logging import os import socket import ssl import six from ironicclient import exc from ironicclient.openstack.common.py3kcompat import urlutils LOG = logging.getLogger(__name__) USER_AGENT = 'python-ironicclient' CHUNKSIZE = 1024 * 64 # 64kB class HTTPClient(object): def __init__(self, endpoint, **kwargs): self.endpoint = endpoint self.auth_token = kwargs.get('token') self.connection_params = self.get_connection_params(endpoint, **kwargs) @staticmethod def get_connection_params(endpoint, **kwargs): parts = urlutils.urlparse(endpoint) _args = (parts.hostname, parts.port, parts.path) _kwargs = {'timeout': (float(kwargs.get('timeout')) if kwargs.get('timeout') else 600)} if parts.scheme == 'https': _class = VerifiedHTTPSConnection _kwargs['ca_file'] = kwargs.get('ca_file', None) _kwargs['cert_file'] = kwargs.get('cert_file', None) _kwargs['key_file'] = kwargs.get('key_file', None) _kwargs['insecure'] = kwargs.get('insecure', False) elif parts.scheme == 'http': _class = six.moves.http_client.HTTPConnection else: msg = 'Unsupported scheme: %s' % parts.scheme raise exc.InvalidEndpoint(msg) return (_class, _args, _kwargs) def get_connection(self): _class = self.connection_params[0] try: return _class(*self.connection_params[1][0:2], **self.connection_params[2]) except six.moves.http_client.InvalidURL: raise exc.InvalidEndpoint() def log_curl_request(self, method, url, kwargs): curl = ['curl -i -X %s' % method] for (key, value) in kwargs['headers'].items(): header = '-H \'%s: %s\'' % (key, value) curl.append(header) conn_params_fmt = [ ('key_file', '--key %s'), ('cert_file', '--cert %s'), ('ca_file', '--cacert %s'), ] for (key, fmt) in conn_params_fmt: value = self.connection_params[2].get(key) if value: curl.append(fmt % value) if self.connection_params[2].get('insecure'): curl.append('-k') if 'body' in kwargs: curl.append('-d \'%s\'' % kwargs['body']) curl.append('%s%s' % (self.endpoint, url)) LOG.debug(' '.join(curl)) @staticmethod def log_http_response(resp, body=None): status = (resp.version / 10.0, resp.status, resp.reason) dump = ['\nHTTP/%.1f %s %s' % status] dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()]) dump.append('') if body: dump.extend([body, '']) LOG.debug('\n'.join(dump)) def _make_connection_url(self, url): (_class, _args, _kwargs) = self.connection_params base_url = _args[2] return '%s/%s' % (base_url.rstrip('/'), url.lstrip('/')) def _extract_error_json(self, body): error_json = {} try: body_json = json.loads(body) if 'error_message' in body_json: raw_msg = body_json['error_message'] error_json = json.loads(raw_msg) except ValueError: return {} return error_json def _http_request(self, url, method, **kwargs): """Send an http request with the specified characteristics. Wrapper around httplib.HTTP(S)Connection.request to handle tasks such as setting headers and error handling. """ # Copy the kwargs so we can reuse the original in case of redirects kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) kwargs['headers'].setdefault('User-Agent', USER_AGENT) if self.auth_token: kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) self.log_curl_request(method, url, kwargs) conn = self.get_connection() try: conn_url = self._make_connection_url(url) conn.request(method, conn_url, **kwargs) resp = conn.getresponse() except socket.gaierror as e: message = ("Error finding address for %(url)s: %(e)s" % dict(url=url, e=e)) raise exc.InvalidEndpoint(message=message) except (socket.error, socket.timeout) as e: endpoint = self.endpoint message = ("Error communicating with %(endpoint)s %(e)s" % dict(endpoint=endpoint, e=e)) raise exc.CommunicationError(message=message) body_iter = ResponseBodyIterator(resp) # Read body into string if it isn't obviously image data body_str = None if resp.getheader('content-type', None) != 'application/octet-stream': body_str = ''.join([chunk for chunk in body_iter]) self.log_http_response(resp, body_str) body_iter = six.StringIO(body_str) else: self.log_http_response(resp) if 400 <= resp.status < 600: LOG.warn("Request returned failure status.") error_json = self._extract_error_json(body_str) raise exc.from_response(resp, error_json.get('faultstring'), error_json.get('debuginfo')) elif resp.status in (301, 302, 305): # Redirected. Reissue the request to the new location. return self._http_request(resp['location'], method, **kwargs) elif resp.status == 300: raise exc.from_response(resp) return resp, body_iter def json_request(self, method, url, **kwargs): kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('Content-Type', 'application/json') kwargs['headers'].setdefault('Accept', 'application/json') if 'body' in kwargs: kwargs['body'] = json.dumps(kwargs['body']) resp, body_iter = self._http_request(url, method, **kwargs) content_type = resp.getheader('content-type', None) if resp.status == 204 or resp.status == 205 or content_type is None: return resp, list() if 'application/json' in content_type: body = ''.join([chunk for chunk in body_iter]) try: body = json.loads(body) except ValueError: LOG.error('Could not decode response body as JSON') else: body = None return resp, body def raw_request(self, method, url, **kwargs): kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('Content-Type', 'application/octet-stream') return self._http_request(url, method, **kwargs) class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection): """httplib-compatibile connection using client-side SSL authentication :see http://code.activestate.com/recipes/ 577548-https-httplib-client-connection-with-certificate-v/ """ def __init__(self, host, port, key_file=None, cert_file=None, ca_file=None, timeout=None, insecure=False): six.moves.http_client.HTTPSConnection.__init__(self, host, port, key_file=key_file, cert_file=cert_file) self.key_file = key_file self.cert_file = cert_file if ca_file is not None: self.ca_file = ca_file else: self.ca_file = self.get_system_ca_file() self.timeout = timeout self.insecure = insecure def connect(self): """Connect to a host on a given (SSL) port. If ca_file is pointing somewhere, use it to check Server Certificate. Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to ssl.wrap_socket(), which forces SSL to check server certificate against our client certificate. """ sock = socket.create_connection((self.host, self.port), self.timeout) if self._tunnel_host: self.sock = sock self._tunnel() if self.insecure is True: kwargs = {'cert_reqs': ssl.CERT_NONE} else: kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.ca_file} if self.cert_file: kwargs['certfile'] = self.cert_file if self.key_file: kwargs['keyfile'] = self.key_file self.sock = ssl.wrap_socket(sock, **kwargs) @staticmethod def get_system_ca_file(): """Return path to system default CA file.""" # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, # Suse, FreeBSD/OpenBSD ca_path = ['/etc/ssl/certs/ca-certificates.crt', '/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/ca-bundle.pem', '/etc/ssl/cert.pem'] for ca in ca_path: if os.path.exists(ca): return ca return None class ResponseBodyIterator(object): """A class that acts as an iterator over an HTTP response.""" def __init__(self, resp): self.resp = resp def __iter__(self): while True: yield self.next() def next(self): chunk = self.resp.read(CHUNKSIZE) if chunk: return chunk else: raise StopIteration() python-ironicclient-0.1.2/ironicclient/common/__init__.py0000664000175400017540000000000012303005215024711 0ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/tests/0000775000175400017540000000000012303005362022467 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/tests/test_shell.py0000664000175400017540000000576112303005215025215 0ustar jenkinsjenkins00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re import sys import fixtures import httplib2 import six from testtools import matchers from ironicclient import exc from ironicclient import shell as ironic_shell from ironicclient.tests import utils FAKE_ENV = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', 'OS_AUTH_URL': 'http://no.where'} class ShellTest(utils.BaseTestCase): re_options = re.DOTALL | re.MULTILINE # Patch os.environ to avoid required auth info. def make_env(self, exclude=None): env = dict((k, v) for k, v in FAKE_ENV.items() if k != exclude) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) def setUp(self): super(ShellTest, self).setUp() def shell(self, argstr): orig = sys.stdout try: sys.stdout = six.StringIO() _shell = ironic_shell.IronicShell() _shell.main(argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() self.assertEqual(exc_value.code, 0) finally: out = sys.stdout.getvalue() sys.stdout.close() sys.stdout = orig return out def test_help_unknown_command(self): self.assertRaises(exc.CommandError, self.shell, 'help foofoo') def test_debug(self): httplib2.debuglevel = 0 self.shell('--debug help') self.assertEqual(httplib2.debuglevel, 1) def test_help(self): required = [ '.*?^usage: ironic', '.*?^See "ironic help COMMAND" ' 'for help on a specific command', ] for argstr in ['--help', 'help']: help_text = self.shell(argstr) for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, self.re_options)) def test_help_on_subcommand(self): required = [ '.*?^usage: ironic chassis-show', ".*?^Show a chassis", ] argstrings = [ 'help chassis-show', ] for argstr in argstrings: help_text = self.shell(argstr) for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, self.re_options)) def test_auth_param(self): self.make_env(exclude='OS_USERNAME') self.test_help() python-ironicclient-0.1.2/ironicclient/tests/test_client.py0000664000175400017540000000351112303005215025353 0ustar jenkinsjenkins00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import fixtures from ironicclient.client import get_client from ironicclient.tests import utils def fake_get_ksclient(**kwargs): return utils.FakeKeystone('KSCLIENT_AUTH_TOKEN') class ClientTest(utils.BaseTestCase): def test_get_client_no_auth_token(self): self.useFixture(fixtures.MonkeyPatch( 'ironicclient.client._get_ksclient', fake_get_ksclient)) kwargs = { 'os_tenant_name': 'TENANT_NAME', 'os_username': 'USERNAME', 'os_password': 'PASSWORD', 'os_auth_url': 'http://localhost:35357/v2.0', 'os_auth_token': '', } client = get_client('1', **kwargs) self.assertEqual(client.http_client.auth_token, 'KSCLIENT_AUTH_TOKEN') def test_get_client_with_auth_token(self): self.useFixture(fixtures.MonkeyPatch( 'ironicclient.client._get_ksclient', fake_get_ksclient)) kwargs = { 'os_tenant_name': 'TENANT_NAME', 'os_username': 'USERNAME', 'os_password': 'PASSWORD', 'os_auth_url': 'http://localhost:35357/v2.0', 'os_auth_token': 'USER_AUTH_TOKEN', } client = get_client('1', **kwargs) self.assertEqual(client.http_client.auth_token, 'USER_AUTH_TOKEN') python-ironicclient-0.1.2/ironicclient/tests/utils.py0000664000175400017540000000541112303005215024177 0ustar jenkinsjenkins00000000000000# Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import fixtures import six import testtools from ironicclient.common import http class BaseTestCase(testtools.TestCase): def setUp(self): super(BaseTestCase, self).setUp() self.useFixture(fixtures.FakeLogger()) class FakeAPI(object): def __init__(self, responses): self.responses = responses self.calls = [] def _request(self, method, url, headers=None, body=None): call = (method, url, headers or {}, body) self.calls.append(call) return self.responses[url][method] def raw_request(self, *args, **kwargs): response = self._request(*args, **kwargs) body_iter = http.ResponseBodyIterator(six.StringIO(response[1])) return FakeResponse(response[0]), body_iter def json_request(self, *args, **kwargs): response = self._request(*args, **kwargs) return FakeResponse(response[0]), response[1] class FakeConnection(object): def __init__(self, response=None): self._response = response self._last_request = None def request(self, method, conn_url, **kwargs): self._last_request = (method, conn_url, kwargs) def setresponse(self, response): self._response = response def getresponse(self): return self._response class FakeResponse(object): def __init__(self, headers, body=None, version=None, status=None, reason=None): """:param headers: dict representing HTTP response headers :param body: file-like object """ self.headers = headers self.body = body self.version = version self.status = status self.reason = reason def getheaders(self): return copy.deepcopy(self.headers).items() def getheader(self, key, default): return self.headers.get(key, default) def read(self, amt): return self.body.read(amt) class FakeServiceCatalog(): def url_for(self, endpoint_type, service_type): return 'http://localhost:6385/v1/f14b41234' class FakeKeystone(): service_catalog = FakeServiceCatalog() def __init__(self, auth_token): self.auth_token = auth_token python-ironicclient-0.1.2/ironicclient/tests/test_http.py0000664000175400017540000001054612303005215025062 0ustar jenkinsjenkins00000000000000# Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import six from ironicclient.common import http from ironicclient import exc from ironicclient.tests import utils class HttpClientTest(utils.BaseTestCase): def test_url_generation_trailing_slash_in_base(self): client = http.HTTPClient('http://localhost/') url = client._make_connection_url('/v1/resources') self.assertEqual(url, '/v1/resources') def test_url_generation_without_trailing_slash_in_base(self): client = http.HTTPClient('http://localhost') url = client._make_connection_url('/v1/resources') self.assertEqual(url, '/v1/resources') def test_url_generation_prefix_slash_in_path(self): client = http.HTTPClient('http://localhost/') url = client._make_connection_url('/v1/resources') self.assertEqual(url, '/v1/resources') def test_url_generation_without_prefix_slash_in_path(self): client = http.HTTPClient('http://localhost') url = client._make_connection_url('v1/resources') self.assertEqual(url, '/v1/resources') @staticmethod def _get_error_body(faultstring=None, debuginfo=None): error_body = { 'faultstring': faultstring, 'debuginfo': debuginfo } raw_error_body = json.dumps(error_body) body = {'error_message': raw_error_body} raw_body = json.dumps(body) return raw_body def test_server_exception_empty_body(self): error_body = self._get_error_body() fake_resp = utils.FakeResponse({'content-type': 'application/json'}, six.StringIO(error_body), version=1, status=500) client = http.HTTPClient('http://localhost/') client.get_connection = \ lambda *a, **kw: utils.FakeConnection(fake_resp) error = self.assertRaises(exc.HTTPInternalServerError, client.json_request, 'GET', '/v1/resources') self.assertEqual('HTTPInternalServerError (HTTP 500)', str(error)) def test_server_exception_msg_only(self): error_msg = 'test error msg' error_body = self._get_error_body(error_msg) fake_resp = utils.FakeResponse({'content-type': 'application/json'}, six.StringIO(error_body), version=1, status=500) client = http.HTTPClient('http://localhost/') client.get_connection = \ lambda *a, **kw: utils.FakeConnection(fake_resp) error = self.assertRaises(exc.HTTPInternalServerError, client.json_request, 'GET', '/v1/resources') self.assertEqual(error_msg, str(error)) def test_server_exception_msg_and_traceback(self): error_msg = 'another test error' error_trace = "\"Traceback (most recent call last):\\n\\n " \ "File \\\"/usr/local/lib/python2.7/..." error_body = self._get_error_body(error_msg, error_trace) fake_resp = utils.FakeResponse({'content-type': 'application/json'}, six.StringIO(error_body), version=1, status=500) client = http.HTTPClient('http://localhost/') client.get_connection = \ lambda *a, **kw: utils.FakeConnection(fake_resp) error = self.assertRaises(exc.HTTPInternalServerError, client.json_request, 'GET', '/v1/resources') self.assertEqual(error_msg + "\n" + error_trace, str(error)) python-ironicclient-0.1.2/ironicclient/tests/test_utils.py0000664000175400017540000000603412303005215025240 0ustar jenkinsjenkins00000000000000# Copyright 2013 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import six from ironicclient.common import utils from ironicclient import exc from ironicclient.tests import utils as test_utils class UtilsTest(test_utils.BaseTestCase): def test_prettytable(self): output_dict = six.StringIO() utils.print_dict({'K': 'k', 'Key': 'Value'}, outfile=output_dict) self.assertEqual(output_dict.getvalue(), '''\ +----------+-------+ | Property | Value | +----------+-------+ | K | k | | Key | Value | +----------+-------+ ''') def test_args_array_to_dict(self): my_args = { 'matching_metadata': ['metadata.key=metadata_value'], 'other': 'value' } cleaned_dict = utils.args_array_to_dict(my_args, "matching_metadata") self.assertEqual(cleaned_dict, { 'matching_metadata': {'metadata.key': 'metadata_value'}, 'other': 'value' }) def test_args_array_to_patch(self): my_args = { 'attributes': ['foo=bar', '/extra/bar=baz'], 'op': 'add', } patch = utils.args_array_to_patch(my_args['op'], my_args['attributes']) self.assertEqual(patch, [{'op': 'add', 'value': 'bar', 'path': '/foo'}, {'op': 'add', 'value': 'baz', 'path': '/extra/bar'}]) def test_args_array_to_patch_format_error(self): my_args = { 'attributes': ['foobar'], 'op': 'add', } self.assertRaises(exc.CommandError, utils.args_array_to_patch, my_args['op'], my_args['attributes']) def test_args_array_to_patch_remove(self): my_args = { 'attributes': ['/foo', 'extra/bar'], 'op': 'remove', } patch = utils.args_array_to_patch(my_args['op'], my_args['attributes']) self.assertEqual(patch, [{'op': 'remove', 'path': '/foo'}, {'op': 'remove', 'path': '/extra/bar'}]) def test_print_dict_unicode(self): unicode_str = u'\u2026' output_file = six.StringIO() utils.print_dict({'K': 'k', 'Key': unicode_str}, outfile=output_file) self.assertIn(unicode_str, output_file.getvalue()) python-ironicclient-0.1.2/ironicclient/tests/__init__.py0000664000175400017540000000000012303005215024563 0ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/tests/v1/0000775000175400017540000000000012303005362023015 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/tests/v1/test_driver.py0000664000175400017540000000264112303005215025721 0ustar jenkinsjenkins00000000000000# coding: utf-8 # # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from testtools import matchers from ironicclient.tests import utils import ironicclient.v1.driver DRIVER = {'name': 'fake', 'hosts': ['fake-host1', 'fake-host2']} fake_responses = { '/v1/drivers': { 'GET': ( {}, {'drivers': [DRIVER]}, ), }, } class DriverManagerTest(testtools.TestCase): def setUp(self): super(DriverManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = ironicclient.v1.driver.DriverManager(self.api) def test_driver_list(self): drivers = self.mgr.list() expect = [ ('GET', '/v1/drivers', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertThat(drivers, matchers.HasLength(1)) python-ironicclient-0.1.2/ironicclient/tests/v1/test_chassis_shell.py0000664000175400017540000000235112303005215027250 0ustar jenkinsjenkins00000000000000# -*- encoding: utf-8 -*- # # Copyright 2013 IBM Corp # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import mock from ironicclient.common import utils as common_utils from ironicclient.tests import utils import ironicclient.v1.chassis_shell as c_shell class ChassisShellTest(utils.BaseTestCase): def test_chassis_show(self): actual = {} fake_print_dict = lambda data, *args, **kwargs: actual.update(data) with mock.patch.object(common_utils, 'print_dict', fake_print_dict): chassis = object() c_shell._print_chassis_show(chassis) exp = ['created_at', 'description', 'extra', 'updated_at', 'uuid'] act = actual.keys() self.assertEqual(sorted(exp), sorted(act)) python-ironicclient-0.1.2/ironicclient/tests/v1/test_chassis.py0000664000175400017540000000777412303005216026100 0ustar jenkinsjenkins00000000000000# -*- encoding: utf-8 -*- #!/usr/bin/env python # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import testtools from ironicclient.tests import utils import ironicclient.v1.chassis CHASSIS = {'id': 42, 'uuid': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 'extra': {}, 'description': 'data-center-1-chassis'} NODE = {'id': 123, 'uuid': '66666666-7777-8888-9999-000000000000', 'chassis_id': 42, 'driver': 'fake', 'driver_info': {'user': 'foo', 'password': 'bar'}, 'properties': {'num_cpu': 4}, 'extra': {}} CREATE_CHASSIS = copy.deepcopy(CHASSIS) del CREATE_CHASSIS['id'] del CREATE_CHASSIS['uuid'] UPDATED_CHASSIS = copy.deepcopy(CHASSIS) NEW_DESCR = 'new-description' UPDATED_CHASSIS['description'] = NEW_DESCR fake_responses = { '/v1/chassis': { 'GET': ( {}, {"chassis": [CHASSIS]}, ), 'POST': ( {}, CREATE_CHASSIS, ), }, '/v1/chassis/%s' % CHASSIS['uuid']: { 'GET': ( {}, CHASSIS, ), 'DELETE': ( {}, None, ), 'PATCH': ( {}, UPDATED_CHASSIS, ), }, '/v1/chassis/%s/nodes' % CHASSIS['uuid']: { 'GET': ( {}, {"nodes": [NODE]}, ), }, } class ChassisManagerTest(testtools.TestCase): def setUp(self): super(ChassisManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = ironicclient.v1.chassis.ChassisManager(self.api) def test_chassis_list(self): chassis = self.mgr.list() expect = [ ('GET', '/v1/chassis', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(1, len(chassis)) def test_chassis_show(self): chassis = self.mgr.get(CHASSIS['uuid']) expect = [ ('GET', '/v1/chassis/%s' % CHASSIS['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(CHASSIS['uuid'], chassis.uuid) self.assertEqual(CHASSIS['description'], chassis.description) def test_create(self): chassis = self.mgr.create(**CREATE_CHASSIS) expect = [ ('POST', '/v1/chassis', {}, CREATE_CHASSIS), ] self.assertEqual(expect, self.api.calls) self.assertTrue(chassis) def test_delete(self): chassis = self.mgr.delete(chassis_id=CHASSIS['uuid']) expect = [ ('DELETE', '/v1/chassis/%s' % CHASSIS['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertTrue(chassis is None) def test_update(self): patch = {'op': 'replace', 'value': NEW_DESCR, 'path': '/description'} chassis = self.mgr.update(chassis_id=CHASSIS['uuid'], patch=patch) expect = [ ('PATCH', '/v1/chassis/%s' % CHASSIS['uuid'], {}, patch), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_DESCR, chassis.description) def test_chassis_node_list(self): nodes = self.mgr.list_nodes(CHASSIS['uuid']) expect = [ ('GET', '/v1/chassis/%s/nodes' % CHASSIS['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(1, len(nodes)) self.assertEqual(NODE['uuid'], nodes[0].uuid) python-ironicclient-0.1.2/ironicclient/tests/v1/test_port_shell.py0000664000175400017540000000235712303005215026605 0ustar jenkinsjenkins00000000000000# -*- encoding: utf-8 -*- # # Copyright 2013 IBM Corp # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import mock from ironicclient.common import utils as common_utils from ironicclient.tests import utils import ironicclient.v1.port_shell as p_shell class PortShellTest(utils.BaseTestCase): def test_port_show(self): actual = {} fake_print_dict = lambda data, *args, **kwargs: actual.update(data) with mock.patch.object(common_utils, 'print_dict', fake_print_dict): port = object() p_shell._print_port_show(port) exp = ['address', 'created_at', 'extra', 'node_uuid', 'updated_at', 'uuid'] act = actual.keys() self.assertEqual(sorted(exp), sorted(act)) python-ironicclient-0.1.2/ironicclient/tests/v1/test_port.py0000664000175400017540000000636512303005216025422 0ustar jenkinsjenkins00000000000000# -*- encoding: utf-8 -*- #!/usr/bin/env python # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import testtools from ironicclient.tests import utils import ironicclient.v1.port PORT = {'id': 987, 'uuid': '11111111-2222-3333-4444-555555555555', 'node_uuid': '55555555-4444-3333-2222-111111111111', 'address': 'AA:BB:CC:DD:EE:FF', 'extra': {}} CREATE_PORT = copy.deepcopy(PORT) del CREATE_PORT['id'] del CREATE_PORT['uuid'] UPDATED_PORT = copy.deepcopy(PORT) NEW_ADDR = 'AA:AA:AA:AA:AA:AA' UPDATED_PORT['address'] = NEW_ADDR fake_responses = { '/v1/ports': { 'GET': ( {}, {"ports": [PORT]}, ), 'POST': ( {}, CREATE_PORT, ), }, '/v1/ports/%s' % PORT['uuid']: { 'GET': ( {}, PORT, ), 'DELETE': ( {}, None, ), 'PATCH': ( {}, UPDATED_PORT, ), }, } class PortManagerTest(testtools.TestCase): def setUp(self): super(PortManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = ironicclient.v1.port.PortManager(self.api) def test_ports_list(self): ports = self.mgr.list() expect = [ ('GET', '/v1/ports', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(1, len(ports)) def test_ports_show(self): port = self.mgr.get(PORT['uuid']) expect = [ ('GET', '/v1/ports/%s' % PORT['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(PORT['uuid'], port.uuid) self.assertEqual(PORT['address'], port.address) self.assertEqual(PORT['node_uuid'], port.node_uuid) def test_create(self): port = self.mgr.create(**CREATE_PORT) expect = [ ('POST', '/v1/ports', {}, CREATE_PORT), ] self.assertEqual(expect, self.api.calls) self.assertTrue(port) def test_delete(self): port = self.mgr.delete(port_id=PORT['uuid']) expect = [ ('DELETE', '/v1/ports/%s' % PORT['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertTrue(port is None) def test_update(self): patch = {'op': 'replace', 'value': NEW_ADDR, 'path': '/address'} port = self.mgr.update(port_id=PORT['uuid'], patch=patch) expect = [ ('PATCH', '/v1/ports/%s' % PORT['uuid'], {}, patch), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_ADDR, port.address) python-ironicclient-0.1.2/ironicclient/tests/v1/test_node_shell.py0000664000175400017540000000315712303005216026546 0ustar jenkinsjenkins00000000000000# -*- encoding: utf-8 -*- # # Copyright 2013 IBM Corp # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import mock from ironicclient.common import utils as common_utils from ironicclient.tests import utils import ironicclient.v1.node_shell as n_shell class NodeShellTest(utils.BaseTestCase): def test_node_show(self): actual = {} fake_print_dict = lambda data, *args, **kwargs: actual.update(data) with mock.patch.object(common_utils, 'print_dict', fake_print_dict): node = object() n_shell._print_node_show(node) exp = ['chassis_uuid', 'created_at', 'driver', 'driver_info', 'extra', 'instance_uuid', 'last_error', 'maintenance', 'power_state', 'properties', 'provision_state', 'reservation', 'target_power_state', 'target_provision_state', 'updated_at', 'uuid'] act = actual.keys() self.assertEqual(sorted(exp), sorted(act)) python-ironicclient-0.1.2/ironicclient/tests/v1/test_node.py0000664000175400017540000002107612303005216025357 0ustar jenkinsjenkins00000000000000# -*- encoding: utf-8 -*- #!/usr/bin/env python # # Copyright 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import testtools from ironicclient.tests import utils import ironicclient.v1.node from testtools.matchers import HasLength NODE1 = {'id': 123, 'uuid': '66666666-7777-8888-9999-000000000000', 'chassis_uuid': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc', 'driver': 'fake', 'driver_info': {'user': 'foo', 'password': 'bar'}, 'properties': {'num_cpu': 4}, 'extra': {}} NODE2 = {'id': 456, 'uuid': '66666666-7777-8888-9999-111111111111', 'instance_uuid': '66666666-7777-8888-9999-222222222222', 'chassis_uuid': 'aaaaaaaa-1111-bbbb-2222-cccccccccccc', 'driver': 'fake too', 'driver_info': {'user': 'foo', 'password': 'bar'}, 'properties': {'num_cpu': 4}, 'extra': {}} PORT = {'id': 456, 'uuid': '11111111-2222-3333-4444-555555555555', 'node_id': 123, 'address': 'AA:AA:AA:AA:AA:AA', 'extra': {}} POWER_STATE = {'power_state': 'power off', 'target_power_state': 'power on'} DRIVER_IFACES = {'deploy': {'result': True}, 'power': {'result': False, 'reason': 'Invalid IPMI username'}, 'console': {'result': None, 'reason': 'not supported'}, 'rescue': {'result': None, 'reason': 'not supported'}} NODE_STATES = {"last_error": None, "power_state": "power on", "provision_state": "active", "target_power_state": None, "target_provision_state": None} CREATE_NODE = copy.deepcopy(NODE1) del CREATE_NODE['id'] del CREATE_NODE['uuid'] UPDATED_NODE = copy.deepcopy(NODE1) NEW_DRIVER = 'new-driver' UPDATED_NODE['driver'] = NEW_DRIVER fake_responses = { '/v1/nodes': { 'GET': ( {}, {"nodes": [NODE1, NODE2]}, ), 'POST': ( {}, CREATE_NODE, ), }, '/v1/nodes/?associated=False': { 'GET': ( {}, {"nodes": [NODE1]}, ) }, '/v1/nodes/?associated=True': { 'GET': ( {}, {"nodes": [NODE2]}, ) }, '/v1/nodes/?instance_uuid=%s' % NODE2['instance_uuid']: { 'GET': ( {}, {"nodes": [NODE2]}, ) }, '/v1/nodes/%s' % NODE1['uuid']: { 'GET': ( {}, NODE1, ), 'DELETE': ( {}, None, ), 'PATCH': ( {}, UPDATED_NODE, ), }, '/v1/nodes/%s' % NODE2['uuid']: { 'GET': ( {}, NODE2, ), }, '/v1/nodes/%s/ports' % NODE1['uuid']: { 'GET': ( {}, {"ports": [PORT]}, ), }, '/v1/nodes/%s/states/power' % NODE1['uuid']: { 'PUT': ( {}, POWER_STATE, ), }, '/v1/nodes/%s/validate' % NODE1['uuid']: { 'GET': ( {}, DRIVER_IFACES, ), }, '/v1/nodes/%s/states/provision' % NODE1['uuid']: { 'PUT': ( {}, None, ), }, '/v1/nodes/%s/states' % NODE1['uuid']: { 'GET': ( {}, NODE_STATES, ), }, } class NodeManagerTest(testtools.TestCase): def setUp(self): super(NodeManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = ironicclient.v1.node.NodeManager(self.api) def test_node_list(self): nodes = self.mgr.list() expect = [ ('GET', '/v1/nodes', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(2, len(nodes)) def test_node_list_associated(self): nodes = self.mgr.list(associated=True) expect = [ ('GET', '/v1/nodes/?associated=True', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid')) def test_node_list_unassociated(self): nodes = self.mgr.list(associated=False) expect = [ ('GET', '/v1/nodes/?associated=False', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) def test_node_show(self): node = self.mgr.get(NODE1['uuid']) expect = [ ('GET', '/v1/nodes/%s' % NODE1['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NODE1['uuid'], node.uuid) def test_node_show_by_instance(self): node = self.mgr.get_by_instance_uuid(NODE2['instance_uuid']) expect = [ ('GET', '/v1/nodes/?instance_uuid=%s' % NODE2['instance_uuid'], {}, None), ('GET', '/v1/nodes/%s' % NODE2['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NODE2['uuid'], node.uuid) def test_create(self): node = self.mgr.create(**CREATE_NODE) expect = [ ('POST', '/v1/nodes', {}, CREATE_NODE), ] self.assertEqual(expect, self.api.calls) self.assertTrue(node) def test_delete(self): node = self.mgr.delete(node_id=NODE1['uuid']) expect = [ ('DELETE', '/v1/nodes/%s' % NODE1['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertTrue(node is None) def test_update(self): patch = {'op': 'replace', 'value': NEW_DRIVER, 'path': '/driver'} node = self.mgr.update(node_id=NODE1['uuid'], patch=patch) expect = [ ('PATCH', '/v1/nodes/%s' % NODE1['uuid'], {}, patch), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_DRIVER, node.driver) def test_node_port_list(self): ports = self.mgr.list_ports(NODE1['uuid']) expect = [ ('GET', '/v1/nodes/%s/ports' % NODE1['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(1, len(ports)) self.assertEqual(PORT['uuid'], ports[0].uuid) self.assertEqual(PORT['address'], ports[0].address) def test_node_set_power_state(self): power_state = self.mgr.set_power_state(NODE1['uuid'], "on") body = {'target': 'power on'} expect = [ ('PUT', '/v1/nodes/%s/states/power' % NODE1['uuid'], {}, body), ] self.assertEqual(expect, self.api.calls) self.assertEqual('power on', power_state.target_power_state) def test_node_validate(self): ifaces = self.mgr.validate(NODE1['uuid']) expect = [ ('GET', '/v1/nodes/%s/validate' % NODE1['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(DRIVER_IFACES['power'], ifaces.power) self.assertEqual(DRIVER_IFACES['deploy'], ifaces.deploy) self.assertEqual(DRIVER_IFACES['rescue'], ifaces.rescue) self.assertEqual(DRIVER_IFACES['console'], ifaces.console) def test_node_set_provision_state(self): target_state = 'active' self.mgr.set_provision_state(NODE1['uuid'], target_state) body = {'target': target_state} expect = [ ('PUT', '/v1/nodes/%s/states/provision' % NODE1['uuid'], {}, body), ] self.assertEqual(expect, self.api.calls) def test_node_states(self): states = self.mgr.states(NODE1['uuid']) expect = [ ('GET', '/v1/nodes/%s/states' % NODE1['uuid'], {}, None), ] self.assertEqual(expect, self.api.calls) expected_fields = ['last_error', 'power_state', 'provision_state', 'target_power_state', 'target_provision_state'] self.assertEqual(sorted(expected_fields), sorted(states.to_dict().keys())) python-ironicclient-0.1.2/ironicclient/tests/v1/__init__.py0000664000175400017540000000000012303005215025111 0ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/shell.py0000664000175400017540000002505712303005215023014 0ustar jenkinsjenkins00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Command-line interface to the OpenStack Bare Metal Provisioning """ from __future__ import print_function import argparse import logging import sys import httplib2 import ironicclient from ironicclient import client as iroclient from ironicclient.common import utils from ironicclient import exc from ironicclient.openstack.common import gettextutils gettextutils.install('ironicclient') class IronicShell(object): def get_base_parser(self): parser = argparse.ArgumentParser( prog='ironic', description=__doc__.strip(), epilog='See "ironic help COMMAND" ' 'for help on a specific command.', add_help=False, formatter_class=HelpFormatter, ) # Global arguments parser.add_argument('-h', '--help', action='store_true', help=argparse.SUPPRESS, ) parser.add_argument('--version', action='version', version=ironicclient.__version__) parser.add_argument('--debug', default=bool(utils.env('IRONICCLIENT_DEBUG')), action='store_true', help='Defaults to env[IRONICCLIENT_DEBUG]') parser.add_argument('-v', '--verbose', default=False, action="store_true", help="Print more verbose output") parser.add_argument('-k', '--insecure', default=False, action='store_true', help="Explicitly allow ironicclient to " "perform \"insecure\" SSL (https) requests. " "The server's certificate will " "not be verified against any certificate " "authorities. This option should be used with " "caution") parser.add_argument('--cert-file', help='Path of certificate file to use in SSL ' 'connection. This file can optionally be prepended' ' with the private key') parser.add_argument('--key-file', help='Path of client key to use in SSL connection.' ' This option is not necessary if your key is ' 'prepended to your cert file') parser.add_argument('--ca-file', help='Path of CA SSL certificate(s) used to verify' ' the remote server certificate. Without this ' 'option ironic looks for the default system ' 'CA certificates') parser.add_argument('--timeout', default=600, help='Number of seconds to wait for a response') parser.add_argument('--os-username', default=utils.env('OS_USERNAME'), help='Defaults to env[OS_USERNAME]') parser.add_argument('--os_username', help=argparse.SUPPRESS) parser.add_argument('--os-password', default=utils.env('OS_PASSWORD'), help='Defaults to env[OS_PASSWORD]') parser.add_argument('--os_password', help=argparse.SUPPRESS) parser.add_argument('--os-tenant-id', default=utils.env('OS_TENANT_ID'), help='Defaults to env[OS_TENANT_ID]') parser.add_argument('--os_tenant_id', help=argparse.SUPPRESS) parser.add_argument('--os-tenant-name', default=utils.env('OS_TENANT_NAME'), help='Defaults to env[OS_TENANT_NAME]') parser.add_argument('--os_tenant_name', help=argparse.SUPPRESS) parser.add_argument('--os-auth-url', default=utils.env('OS_AUTH_URL'), help='Defaults to env[OS_AUTH_URL]') parser.add_argument('--os_auth_url', help=argparse.SUPPRESS) parser.add_argument('--os-region-name', default=utils.env('OS_REGION_NAME'), help='Defaults to env[OS_REGION_NAME]') parser.add_argument('--os_region_name', help=argparse.SUPPRESS) parser.add_argument('--os-auth-token', default=utils.env('OS_AUTH_TOKEN'), help='Defaults to env[OS_AUTH_TOKEN]') parser.add_argument('--os_auth_token', help=argparse.SUPPRESS) parser.add_argument('--ironic-url', default=utils.env('IRONIC_URL'), help='Defaults to env[IRONIC_URL]') parser.add_argument('--ironic_url', help=argparse.SUPPRESS) parser.add_argument('--ironic-api-version', default=utils.env( 'IRONIC_API_VERSION', default='1'), help='Defaults to env[IRONIC_API_VERSION] ' 'or 1') parser.add_argument('--ironic_api_version', help=argparse.SUPPRESS) parser.add_argument('--os-service-type', default=utils.env('OS_SERVICE_TYPE'), help='Defaults to env[OS_SERVICE_TYPE]') parser.add_argument('--os_service_type', help=argparse.SUPPRESS) parser.add_argument('--os-endpoint-type', default=utils.env('OS_ENDPOINT_TYPE'), help='Defaults to env[OS_ENDPOINT_TYPE]') parser.add_argument('--os_endpoint_type', help=argparse.SUPPRESS) return parser def get_subcommand_parser(self, version): parser = self.get_base_parser() self.subcommands = {} subparsers = parser.add_subparsers(metavar='') submodule = utils.import_versioned_module(version, 'shell') submodule.enhance_parser(parser, subparsers, self.subcommands) utils.define_commands_from_module(subparsers, self, self.subcommands) return parser def _setup_debugging(self, debug): if debug: logging.basicConfig( format="%(levelname)s (%(module)s:%(lineno)d) %(message)s", level=logging.DEBUG) httplib2.debuglevel = 1 else: logging.basicConfig( format="%(levelname)s %(message)s", level=logging.CRITICAL) def main(self, argv): # Parse args once to find version parser = self.get_base_parser() (options, args) = parser.parse_known_args(argv) self._setup_debugging(options.debug) # build available subcommands based on version api_version = options.ironic_api_version subcommand_parser = self.get_subcommand_parser(api_version) self.parser = subcommand_parser # Handle top-level --help/-h before attempting to parse # a command off the command line if options.help or not argv: self.do_help(options) return 0 # Parse args again and call whatever callback was selected args = subcommand_parser.parse_args(argv) # Short-circuit and deal with help command right away. if args.func == self.do_help: self.do_help(args) return 0 if not (args.os_auth_token and args.ironic_url): if not args.os_username: raise exc.CommandError(_("You must provide a username via " "either --os-username or via " "env[OS_USERNAME]")) if not args.os_password: raise exc.CommandError(_("You must provide a password via " "either --os-password or via " "env[OS_PASSWORD]")) if not (args.os_tenant_id or args.os_tenant_name): raise exc.CommandError(_("You must provide a tenant_id via " "either --os-tenant-id or via " "env[OS_TENANT_ID]")) if not args.os_auth_url: raise exc.CommandError(_("You must provide an auth url via " "either --os-auth-url or via " "env[OS_AUTH_URL]")) client = iroclient.get_client(api_version, **(args.__dict__)) try: args.func(client, args) except exc.Unauthorized: raise exc.CommandError(_("Invalid OpenStack Identity credentials")) @utils.arg('command', metavar='', nargs='?', help='Display help for ') def do_help(self, args): """Display help about this program or one of its subcommands.""" if getattr(args, 'command', None): if args.command in self.subcommands: self.subcommands[args.command].print_help() else: raise exc.CommandError(_("'%s' is not a valid subcommand") % args.command) else: self.parser.print_help() class HelpFormatter(argparse.HelpFormatter): def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) super(HelpFormatter, self).start_section(heading) def main(): try: IronicShell().main(sys.argv[1:]) except Exception as e: print(str(e), file=sys.stderr) sys.exit(1) if __name__ == "__main__": main() python-ironicclient-0.1.2/ironicclient/openstack/0000775000175400017540000000000012303005362023314 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/openstack/common/0000775000175400017540000000000012303005362024604 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/openstack/common/apiclient/0000775000175400017540000000000012303005362026554 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/openstack/common/apiclient/fake_client.py0000664000175400017540000001337512303005215031400 0ustar jenkinsjenkins00000000000000# Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ A fake server that "responds" to API methods with pre-canned responses. All of these responses come from the spec, so if for some reason the spec's wrong the tests might raise AssertionError. I've indicated in comments the places where actual behavior differs from the spec. """ # W0102: Dangerous default value %s as argument # pylint: disable=W0102 import json import requests import six from ironicclient.openstack.common.apiclient import client from ironicclient.openstack.common.py3kcompat import urlutils def assert_has_keys(dct, required=[], optional=[]): for k in required: try: assert k in dct except AssertionError: extra_keys = set(dct.keys()).difference(set(required + optional)) raise AssertionError("found unexpected keys: %s" % list(extra_keys)) class TestResponse(requests.Response): """Wrap requests.Response and provide a convenient initialization. """ def __init__(self, data): super(TestResponse, self).__init__() self._content_consumed = True if isinstance(data, dict): self.status_code = data.get('status_code', 200) # Fake the text attribute to streamline Response creation text = data.get('text', "") if isinstance(text, (dict, list)): self._content = json.dumps(text) default_headers = { "Content-Type": "application/json", } else: self._content = text default_headers = {} if six.PY3 and isinstance(self._content, six.string_types): self._content = self._content.encode('utf-8', 'strict') self.headers = data.get('headers') or default_headers else: self.status_code = data def __eq__(self, other): return (self.status_code == other.status_code and self.headers == other.headers and self._content == other._content) class FakeHTTPClient(client.HTTPClient): def __init__(self, *args, **kwargs): self.callstack = [] self.fixtures = kwargs.pop("fixtures", None) or {} if not args and not "auth_plugin" in kwargs: args = (None, ) super(FakeHTTPClient, self).__init__(*args, **kwargs) def assert_called(self, method, url, body=None, pos=-1): """Assert than an API method was just called. """ expected = (method, url) called = self.callstack[pos][0:2] assert self.callstack, \ "Expected %s %s but no calls were made." % expected assert expected == called, 'Expected %s %s; got %s %s' % \ (expected + called) if body is not None: if self.callstack[pos][3] != body: raise AssertionError('%r != %r' % (self.callstack[pos][3], body)) def assert_called_anytime(self, method, url, body=None): """Assert than an API method was called anytime in the test. """ expected = (method, url) assert self.callstack, \ "Expected %s %s but no calls were made." % expected found = False entry = None for entry in self.callstack: if expected == entry[0:2]: found = True break assert found, 'Expected %s %s; got %s' % \ (method, url, self.callstack) if body is not None: assert entry[3] == body, "%s != %s" % (entry[3], body) self.callstack = [] def clear_callstack(self): self.callstack = [] def authenticate(self): pass def client_request(self, client, method, url, **kwargs): # Check that certain things are called correctly if method in ["GET", "DELETE"]: assert "json" not in kwargs # Note the call self.callstack.append( (method, url, kwargs.get("headers") or {}, kwargs.get("json") or kwargs.get("data"))) try: fixture = self.fixtures[url][method] except KeyError: pass else: return TestResponse({"headers": fixture[0], "text": fixture[1]}) # Call the method args = urlutils.parse_qsl(urlutils.urlparse(url)[4]) kwargs.update(args) munged_url = url.rsplit('?', 1)[0] munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') munged_url = munged_url.replace('-', '_') callback = "%s_%s" % (method.lower(), munged_url) if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' 'expected fakes method name: %s' % (method, url, callback)) resp = getattr(self, callback)(**kwargs) if len(resp) == 3: status, headers, body = resp else: status, body = resp headers = {} return TestResponse({ "status_code": status, "text": body, "headers": headers, }) python-ironicclient-0.1.2/ironicclient/openstack/common/apiclient/base.py0000664000175400017540000003713112303005215030042 0ustar jenkinsjenkins00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack Foundation # Copyright 2012 Grid Dynamics # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Base utilities to build API operation managers and objects on top of. """ # E1102: %s is not callable # pylint: disable=E1102 import abc import six from ironicclient.openstack.common.apiclient import exceptions from ironicclient.openstack.common.py3kcompat import urlutils from ironicclient.openstack.common import strutils def getid(obj): """Return id if argument is a Resource. Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ try: if obj.uuid: return obj.uuid except AttributeError: pass try: return obj.id except AttributeError: return obj # TODO(aababilov): call run_hooks() in HookableMixin's child classes class HookableMixin(object): """Mixin so classes can register and run hooks.""" _hooks_map = {} @classmethod def add_hook(cls, hook_type, hook_func): """Add a new hook of specified type. :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' :param hook_func: hook function """ if hook_type not in cls._hooks_map: cls._hooks_map[hook_type] = [] cls._hooks_map[hook_type].append(hook_func) @classmethod def run_hooks(cls, hook_type, *args, **kwargs): """Run all hooks of specified type. :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' :param **args: args to be passed to every hook function :param **kwargs: kwargs to be passed to every hook function """ hook_funcs = cls._hooks_map.get(hook_type) or [] for hook_func in hook_funcs: hook_func(*args, **kwargs) class BaseManager(HookableMixin): """Basic manager type providing common operations. Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ resource_class = None def __init__(self, client): """Initializes BaseManager with `client`. :param client: instance of BaseClient descendant for HTTP requests """ super(BaseManager, self).__init__() self.client = client def _list(self, url, response_key, obj_class=None, json=None): """List the collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, e.g., 'servers' :param obj_class: class for constructing the returned objects (self.resource_class will be used by default) :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) """ if json: body = self.client.post(url, json=json).json() else: body = self.client.get(url).json() if obj_class is None: obj_class = self.resource_class data = body[response_key] # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... try: data = data['values'] except (KeyError, TypeError): pass return [obj_class(self, res, loaded=True) for res in data if res] def _get(self, url, response_key): """Get an object from collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, e.g., 'server' """ body = self.client.get(url).json() return self.resource_class(self, body[response_key], loaded=True) def _head(self, url): """Retrieve request headers for an object. :param url: a partial URL, e.g., '/servers' """ resp = self.client.head(url) return resp.status_code == 204 def _post(self, url, json, response_key, return_raw=False): """Create an object. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers' :param return_raw: flag to force returning raw JSON instead of Python object of self.resource_class """ body = self.client.post(url, json=json).json() if return_raw: return body[response_key] return self.resource_class(self, body[response_key]) def _put(self, url, json=None, response_key=None): """Update an object with PUT method. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers' """ resp = self.client.put(url, json=json) # PUT requests may not return a body if resp.content: body = resp.json() if response_key is not None: return self.resource_class(self, body[response_key]) else: return self.resource_class(self, body) def _patch(self, url, json=None, response_key=None): """Update an object with PATCH method. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers' """ body = self.client.patch(url, json=json).json() if response_key is not None: return self.resource_class(self, body[response_key]) else: return self.resource_class(self, body) def _delete(self, url): """Delete an object. :param url: a partial URL, e.g., '/servers/my-server' """ return self.client.delete(url) @six.add_metaclass(abc.ABCMeta) class ManagerWithFind(BaseManager): """Manager with additional `find()`/`findall()` methods.""" @abc.abstractmethod def list(self): pass def find(self, **kwargs): """Find a single item with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch() else: return matches[0] def findall(self, **kwargs): """Find all items with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ found = [] searches = kwargs.items() for obj in self.list(): try: if all(getattr(obj, attr) == value for (attr, value) in searches): found.append(obj) except AttributeError: continue return found class CrudManager(BaseManager): """Base manager class for manipulating entities. Children of this class are expected to define a `collection_key` and `key`. - `collection_key`: Usually a plural noun by convention (e.g. `entities`); used to refer collections in both URL's (e.g. `/v3/entities`) and JSON objects containing a list of member resources (e.g. `{'entities': [{}, {}, {}]}`). - `key`: Usually a singular noun by convention (e.g. `entity`); used to refer to an individual member of the collection. """ collection_key = None key = None def build_url(self, base_url=None, **kwargs): """Builds a resource URL for the given kwargs. Given an example collection where `collection_key = 'entities'` and `key = 'entity'`, the following URL's could be generated. By default, the URL will represent a collection of entities, e.g.:: /entities If kwargs contains an `entity_id`, then the URL will represent a specific member, e.g.:: /entities/{entity_id} :param base_url: if provided, the generated URL will be appended to it """ url = base_url if base_url is not None else '' url += '/%s' % self.collection_key # do we have a specific entity? entity_id = kwargs.get('%s_id' % self.key) if entity_id is not None: url += '/%s' % entity_id return url def _filter_kwargs(self, kwargs): """Drop null values and handle ids.""" for key, ref in six.iteritems(kwargs.copy()): if ref is None: kwargs.pop(key) else: if isinstance(ref, Resource): kwargs.pop(key) kwargs['%s_id' % key] = getid(ref) return kwargs def create(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._post( self.build_url(**kwargs), {self.key: kwargs}, self.key) def get(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._get( self.build_url(**kwargs), self.key) def head(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._head(self.build_url(**kwargs)) def list(self, base_url=None, **kwargs): """List the collection. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) return self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '', }, self.collection_key) def put(self, base_url=None, **kwargs): """Update an element. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) return self._put(self.build_url(base_url=base_url, **kwargs)) def update(self, **kwargs): kwargs = self._filter_kwargs(kwargs) params = kwargs.copy() params.pop('%s_id' % self.key) return self._patch( self.build_url(**kwargs), {self.key: params}, self.key) def delete(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._delete( self.build_url(**kwargs)) def find(self, base_url=None, **kwargs): """Find a single item with attributes matching ``**kwargs``. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) rl = self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '', }, self.collection_key) num = len(rl) if num == 0: msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) raise exceptions.NotFound(404, msg) elif num > 1: raise exceptions.NoUniqueMatch else: return rl[0] class Extension(HookableMixin): """Extension descriptor.""" SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') manager_class = None def __init__(self, name, module): super(Extension, self).__init__() self.name = name self.module = module self._parse_extension_module() def _parse_extension_module(self): self.manager_class = None for attr_name, attr_value in self.module.__dict__.items(): if attr_name in self.SUPPORTED_HOOKS: self.add_hook(attr_name, attr_value) else: try: if issubclass(attr_value, BaseManager): self.manager_class = attr_value except TypeError: pass def __repr__(self): return "" % self.name class Resource(object): """Base class for OpenStack resources (tenant, user, etc.). This is pretty much just a bag for attributes. """ HUMAN_ID = False NAME_ATTR = 'name' def __init__(self, manager, info, loaded=False): """Populate and bind to a manager. :param manager: BaseManager object :param info: dictionary representing resource attributes :param loaded: prevent lazy-loading if set to True """ self.manager = manager self._info = info self._add_details(info) self._loaded = loaded def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) @property def human_id(self): """Human-readable ID which can be used for bash completion. """ if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: return strutils.to_slug(getattr(self, self.NAME_ATTR)) return None def _add_details(self, info): for (k, v) in six.iteritems(info): try: setattr(self, k, v) self._info[k] = v except AttributeError: # In this case we already defined the attribute on the class pass def __getattr__(self, k): if k not in self.__dict__: #NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) raise AttributeError(k) else: return self.__dict__[k] def get(self): # set_loaded() first ... so if we have to bail, we know we tried. self.set_loaded(True) if not hasattr(self.manager, 'get'): return new = self.manager.get(self.id) if new: self._add_details(new._info) def __eq__(self, other): if not isinstance(other, Resource): return NotImplemented # two resources of different types are not equal if not isinstance(other, self.__class__): return False if hasattr(self, 'id') and hasattr(other, 'id'): return self.id == other.id return self._info == other._info def is_loaded(self): return self._loaded def set_loaded(self, val): self._loaded = val python-ironicclient-0.1.2/ironicclient/openstack/common/apiclient/exceptions.py0000664000175400017540000002721112303005215031307 0ustar jenkinsjenkins00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Nebula, Inc. # Copyright 2013 Alessio Ababilov # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Exception definitions. """ import inspect import sys import six class ClientException(Exception): """The base exception class for all exceptions this library raises. """ pass class MissingArgs(ClientException): """Supplied arguments are not sufficient for calling a function.""" def __init__(self, missing): self.missing = missing msg = "Missing argument(s): %s" % ", ".join(missing) super(MissingArgs, self).__init__(msg) class ValidationError(ClientException): """Error in validation on API client side.""" pass class UnsupportedVersion(ClientException): """User is trying to use an unsupported version of the API.""" pass class CommandError(ClientException): """Error in CLI tool.""" pass class AuthorizationFailure(ClientException): """Cannot authorize API client.""" pass class ConnectionRefused(ClientException): """Cannot connect to API service.""" pass class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): super(AuthPluginOptionsMissing, self).__init__( "Authentication failed. Missing options: %s" % ", ".join(opt_names)) self.opt_names = opt_names class AuthSystemNotFound(AuthorizationFailure): """User has specified a AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( "AuthSystemNotFound: %s" % repr(auth_system)) self.auth_system = auth_system class NoUniqueMatch(ClientException): """Multiple entities found instead of one.""" pass class EndpointException(ClientException): """Something is rotten in Service Catalog.""" pass class EndpointNotFound(EndpointException): """Could not find requested endpoint in Service Catalog.""" pass class AmbiguousEndpoints(EndpointException): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): super(AmbiguousEndpoints, self).__init__( "AmbiguousEndpoints: %s" % repr(endpoints)) self.endpoints = endpoints class HttpError(ClientException): """The base exception class for all HTTP exceptions. """ http_status = 0 message = "HTTP Error" def __init__(self, message=None, details=None, response=None, request_id=None, url=None, method=None, http_status=None): self.http_status = http_status or self.http_status self.message = message or self.message self.details = details self.request_id = request_id self.response = response self.url = url self.method = method formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) if request_id: formatted_string += " (Request-ID: %s)" % request_id super(HttpError, self).__init__(formatted_string) class HTTPClientError(HttpError): """Client-side HTTP error. Exception for cases in which the client seems to have erred. """ message = "HTTP Client Error" class HttpServerError(HttpError): """Server-side HTTP error. Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ message = "HTTP Server Error" class BadRequest(HTTPClientError): """HTTP 400 - Bad Request. The request cannot be fulfilled due to bad syntax. """ http_status = 400 message = "Bad Request" class Unauthorized(HTTPClientError): """HTTP 401 - Unauthorized. Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. """ http_status = 401 message = "Unauthorized" class PaymentRequired(HTTPClientError): """HTTP 402 - Payment Required. Reserved for future use. """ http_status = 402 message = "Payment Required" class Forbidden(HTTPClientError): """HTTP 403 - Forbidden. The request was a valid request, but the server is refusing to respond to it. """ http_status = 403 message = "Forbidden" class NotFound(HTTPClientError): """HTTP 404 - Not Found. The requested resource could not be found but may be available again in the future. """ http_status = 404 message = "Not Found" class MethodNotAllowed(HTTPClientError): """HTTP 405 - Method Not Allowed. A request was made of a resource using a request method not supported by that resource. """ http_status = 405 message = "Method Not Allowed" class NotAcceptable(HTTPClientError): """HTTP 406 - Not Acceptable. The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. """ http_status = 406 message = "Not Acceptable" class ProxyAuthenticationRequired(HTTPClientError): """HTTP 407 - Proxy Authentication Required. The client must first authenticate itself with the proxy. """ http_status = 407 message = "Proxy Authentication Required" class RequestTimeout(HTTPClientError): """HTTP 408 - Request Timeout. The server timed out waiting for the request. """ http_status = 408 message = "Request Timeout" class Conflict(HTTPClientError): """HTTP 409 - Conflict. Indicates that the request could not be processed because of conflict in the request, such as an edit conflict. """ http_status = 409 message = "Conflict" class Gone(HTTPClientError): """HTTP 410 - Gone. Indicates that the resource requested is no longer available and will not be available again. """ http_status = 410 message = "Gone" class LengthRequired(HTTPClientError): """HTTP 411 - Length Required. The request did not specify the length of its content, which is required by the requested resource. """ http_status = 411 message = "Length Required" class PreconditionFailed(HTTPClientError): """HTTP 412 - Precondition Failed. The server does not meet one of the preconditions that the requester put on the request. """ http_status = 412 message = "Precondition Failed" class RequestEntityTooLarge(HTTPClientError): """HTTP 413 - Request Entity Too Large. The request is larger than the server is willing or able to process. """ http_status = 413 message = "Request Entity Too Large" def __init__(self, *args, **kwargs): try: self.retry_after = int(kwargs.pop('retry_after')) except (KeyError, ValueError): self.retry_after = 0 super(RequestEntityTooLarge, self).__init__(*args, **kwargs) class RequestUriTooLong(HTTPClientError): """HTTP 414 - Request-URI Too Long. The URI provided was too long for the server to process. """ http_status = 414 message = "Request-URI Too Long" class UnsupportedMediaType(HTTPClientError): """HTTP 415 - Unsupported Media Type. The request entity has a media type which the server or resource does not support. """ http_status = 415 message = "Unsupported Media Type" class RequestedRangeNotSatisfiable(HTTPClientError): """HTTP 416 - Requested Range Not Satisfiable. The client has asked for a portion of the file, but the server cannot supply that portion. """ http_status = 416 message = "Requested Range Not Satisfiable" class ExpectationFailed(HTTPClientError): """HTTP 417 - Expectation Failed. The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 message = "Expectation Failed" class UnprocessableEntity(HTTPClientError): """HTTP 422 - Unprocessable Entity. The request was well-formed but was unable to be followed due to semantic errors. """ http_status = 422 message = "Unprocessable Entity" class InternalServerError(HttpServerError): """HTTP 500 - Internal Server Error. A generic error message, given when no more specific message is suitable. """ http_status = 500 message = "Internal Server Error" # NotImplemented is a python keyword. class HttpNotImplemented(HttpServerError): """HTTP 501 - Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request. """ http_status = 501 message = "Not Implemented" class BadGateway(HttpServerError): """HTTP 502 - Bad Gateway. The server was acting as a gateway or proxy and received an invalid response from the upstream server. """ http_status = 502 message = "Bad Gateway" class ServiceUnavailable(HttpServerError): """HTTP 503 - Service Unavailable. The server is currently unavailable. """ http_status = 503 message = "Service Unavailable" class GatewayTimeout(HttpServerError): """HTTP 504 - Gateway Timeout. The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. """ http_status = 504 message = "Gateway Timeout" class HttpVersionNotSupported(HttpServerError): """HTTP 505 - HttpVersion Not Supported. The server does not support the HTTP protocol version used in the request. """ http_status = 505 message = "HTTP Version Not Supported" # _code_map contains all the classes that have http_status attribute. _code_map = dict( (getattr(obj, 'http_status', None), obj) for name, obj in six.iteritems(vars(sys.modules[__name__])) if inspect.isclass(obj) and getattr(obj, 'http_status', False) ) def from_response(response, method, url): """Returns an instance of :class:`HttpError` or subclass based on response. :param response: instance of `requests.Response` class :param method: HTTP method used for request :param url: URL used for request """ kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, "request_id": response.headers.get("x-compute-request-id"), } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] content_type = response.headers.get("Content-Type", "") if content_type.startswith("application/json"): try: body = response.json() except ValueError: pass else: if hasattr(body, "keys"): error = body[body.keys()[0]] kwargs["message"] = error.get("message", None) kwargs["details"] = error.get("details", None) elif content_type.startswith("text/"): kwargs["details"] = response.text try: cls = _code_map[response.status_code] except KeyError: if 500 <= response.status_code < 600: cls = HttpServerError elif 400 <= response.status_code < 500: cls = HTTPClientError else: cls = HttpError return cls(**kwargs) python-ironicclient-0.1.2/ironicclient/openstack/common/apiclient/client.py0000664000175400017540000003067412303005215030413 0ustar jenkinsjenkins00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack Foundation # Copyright 2011 Piston Cloud Computing, Inc. # Copyright 2013 Alessio Ababilov # Copyright 2013 Grid Dynamics # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ OpenStack Client interface. Handles the REST calls and responses. """ # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 import logging import time try: import simplejson as json except ImportError: import json import requests from ironicclient.openstack.common.apiclient import exceptions from ironicclient.openstack.common import importutils _logger = logging.getLogger(__name__) class HTTPClient(object): """This client handles sending HTTP requests to OpenStack servers. Features: - share authentication information between several clients to different services (e.g., for compute and image clients); - reissue authentication request for expired tokens; - encode/decode JSON bodies; - raise exceptions on HTTP errors; - pluggable authentication; - store authentication information in a keyring; - store time spent for requests; - register clients for particular services, so one can use `http_client.identity` or `http_client.compute`; - log requests and responses in a format that is easy to copy-and-paste into terminal and send the same request with curl. """ user_agent = "ironicclient.openstack.common.apiclient" def __init__(self, auth_plugin, region_name=None, endpoint_type="publicURL", original_ip=None, verify=True, cert=None, timeout=None, timings=False, keyring_saver=None, debug=False, user_agent=None, http=None): self.auth_plugin = auth_plugin self.endpoint_type = endpoint_type self.region_name = region_name self.original_ip = original_ip self.timeout = timeout self.verify = verify self.cert = cert self.keyring_saver = keyring_saver self.debug = debug self.user_agent = user_agent or self.user_agent self.times = [] # [("item", starttime, endtime), ...] self.timings = timings # requests within the same session can reuse TCP connections from pool self.http = http or requests.Session() self.cached_token = None def _http_log_req(self, method, url, kwargs): if not self.debug: return string_parts = [ "curl -i", "-X '%s'" % method, "'%s'" % url, ] for element in kwargs['headers']: header = "-H '%s: %s'" % (element, kwargs['headers'][element]) string_parts.append(header) _logger.debug("REQ: %s" % " ".join(string_parts)) if 'data' in kwargs: _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) def _http_log_resp(self, resp): if not self.debug: return _logger.debug( "RESP: [%s] %s\n", resp.status_code, resp.headers) if resp._content_consumed: _logger.debug( "RESP BODY: %s\n", resp.text) def serialize(self, kwargs): if kwargs.get('json') is not None: kwargs['headers']['Content-Type'] = 'application/json' kwargs['data'] = json.dumps(kwargs['json']) try: del kwargs['json'] except KeyError: pass def get_timings(self): return self.times def reset_timings(self): self.times = [] def request(self, method, url, **kwargs): """Send an http request with the specified characteristics. Wrapper around `requests.Session.request` to handle tasks such as setting headers, JSON encoding/decoding, and error handling. :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to ' requests.Session.request (such as `headers`) or `json` that will be encoded as JSON and used as `data` argument """ kwargs.setdefault("headers", kwargs.get("headers", {})) kwargs["headers"]["User-Agent"] = self.user_agent if self.original_ip: kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( self.original_ip, self.user_agent) if self.timeout is not None: kwargs.setdefault("timeout", self.timeout) kwargs.setdefault("verify", self.verify) if self.cert is not None: kwargs.setdefault("cert", self.cert) self.serialize(kwargs) self._http_log_req(method, url, kwargs) if self.timings: start_time = time.time() resp = self.http.request(method, url, **kwargs) if self.timings: self.times.append(("%s %s" % (method, url), start_time, time.time())) self._http_log_resp(resp) if resp.status_code >= 400: _logger.debug( "Request returned failure status: %s", resp.status_code) raise exceptions.from_response(resp, method, url) return resp @staticmethod def concat_url(endpoint, url): """Concatenate endpoint and final URL. E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to "http://keystone/v2.0/tokens". :param endpoint: the base URL :param url: the final URL """ return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) def client_request(self, client, method, url, **kwargs): """Send an http request using `client`'s endpoint and specified `url`. If request was rejected as unauthorized (possibly because the token is expired), issue one authorization attempt and send the request once again. :param client: instance of BaseClient descendant :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to ' `HTTPClient.request` """ filter_args = { "endpoint_type": client.endpoint_type or self.endpoint_type, "service_type": client.service_type, } token, endpoint = (self.cached_token, client.cached_endpoint) just_authenticated = False if not (token and endpoint): try: token, endpoint = self.auth_plugin.token_and_endpoint( **filter_args) except exceptions.EndpointException: pass if not (token and endpoint): self.authenticate() just_authenticated = True token, endpoint = self.auth_plugin.token_and_endpoint( **filter_args) if not (token and endpoint): raise exceptions.AuthorizationFailure( "Cannot find endpoint or token for request") old_token_endpoint = (token, endpoint) kwargs.setdefault("headers", {})["X-Auth-Token"] = token self.cached_token = token client.cached_endpoint = endpoint # Perform the request once. If we get Unauthorized, then it # might be because the auth token expired, so try to # re-authenticate and try again. If it still fails, bail. try: return self.request( method, self.concat_url(endpoint, url), **kwargs) except exceptions.Unauthorized as unauth_ex: if just_authenticated: raise self.cached_token = None client.cached_endpoint = None self.authenticate() try: token, endpoint = self.auth_plugin.token_and_endpoint( **filter_args) except exceptions.EndpointException: raise unauth_ex if (not (token and endpoint) or old_token_endpoint == (token, endpoint)): raise unauth_ex self.cached_token = token client.cached_endpoint = endpoint kwargs["headers"]["X-Auth-Token"] = token return self.request( method, self.concat_url(endpoint, url), **kwargs) def add_client(self, base_client_instance): """Add a new instance of :class:`BaseClient` descendant. `self` will store a reference to `base_client_instance`. Example: >>> def test_clients(): ... from keystoneclient.auth import keystone ... from openstack.common.apiclient import client ... auth = keystone.KeystoneAuthPlugin( ... username="user", password="pass", tenant_name="tenant", ... auth_url="http://auth:5000/v2.0") ... openstack_client = client.HTTPClient(auth) ... # create nova client ... from novaclient.v1_1 import client ... client.Client(openstack_client) ... # create keystone client ... from keystoneclient.v2_0 import client ... client.Client(openstack_client) ... # use them ... openstack_client.identity.tenants.list() ... openstack_client.compute.servers.list() """ service_type = base_client_instance.service_type if service_type and not hasattr(self, service_type): setattr(self, service_type, base_client_instance) def authenticate(self): self.auth_plugin.authenticate(self) # Store the authentication results in the keyring for later requests if self.keyring_saver: self.keyring_saver.save(self) class BaseClient(object): """Top-level object to access the OpenStack API. This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` will handle a bunch of issues such as authentication. """ service_type = None endpoint_type = None # "publicURL" will be used cached_endpoint = None def __init__(self, http_client, extensions=None): self.http_client = http_client http_client.add_client(self) # Add in any extensions... if extensions: for extension in extensions: if extension.manager_class: setattr(self, extension.name, extension.manager_class(self)) def client_request(self, method, url, **kwargs): return self.http_client.client_request( self, method, url, **kwargs) def head(self, url, **kwargs): return self.client_request("HEAD", url, **kwargs) def get(self, url, **kwargs): return self.client_request("GET", url, **kwargs) def post(self, url, **kwargs): return self.client_request("POST", url, **kwargs) def put(self, url, **kwargs): return self.client_request("PUT", url, **kwargs) def delete(self, url, **kwargs): return self.client_request("DELETE", url, **kwargs) def patch(self, url, **kwargs): return self.client_request("PATCH", url, **kwargs) @staticmethod def get_class(api_name, version, version_map): """Returns the client class for the requested API version :param api_name: the name of the API, e.g. 'compute', 'image', etc :param version: the requested API version :param version_map: a dict of client classes keyed by version :rtype: a client class for the requested API version """ try: client_path = version_map[str(version)] except (KeyError, ValueError): msg = "Invalid %s client version '%s'. must be one of: %s" % ( (api_name, version, ', '.join(version_map.keys()))) raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) python-ironicclient-0.1.2/ironicclient/openstack/common/apiclient/auth.py0000664000175400017540000001566412303005215030100 0ustar jenkinsjenkins00000000000000# Copyright 2013 OpenStack Foundation # Copyright 2013 Spanish National Research Council. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 import abc import argparse import logging import os import six from stevedore import extension from ironicclient.openstack.common.apiclient import exceptions logger = logging.getLogger(__name__) _discovered_plugins = {} def discover_auth_systems(): """Discover the available auth-systems. This won't take into account the old style auth-systems. """ global _discovered_plugins _discovered_plugins = {} def add_plugin(ext): _discovered_plugins[ext.name] = ext.plugin ep_namespace = "ironicclient.openstack.common.apiclient.auth" mgr = extension.ExtensionManager(ep_namespace) mgr.map(add_plugin) def load_auth_system_opts(parser): """Load options needed by the available auth-systems into a parser. This function will try to populate the parser with options from the available plugins. """ group = parser.add_argument_group("Common auth options") BaseAuthPlugin.add_common_opts(group) for name, auth_plugin in six.iteritems(_discovered_plugins): group = parser.add_argument_group( "Auth-system '%s' options" % name, conflict_handler="resolve") auth_plugin.add_opts(group) def load_plugin(auth_system): try: plugin_class = _discovered_plugins[auth_system] except KeyError: raise exceptions.AuthSystemNotFound(auth_system) return plugin_class(auth_system=auth_system) def load_plugin_from_args(args): """Load required plugin and populate it with options. Try to guess auth system if it is not specified. Systems are tried in alphabetical order. :type args: argparse.Namespace :raises: AuthorizationFailure """ auth_system = args.os_auth_system if auth_system: plugin = load_plugin(auth_system) plugin.parse_opts(args) plugin.sufficient_options() return plugin for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)): plugin_class = _discovered_plugins[plugin_auth_system] plugin = plugin_class() plugin.parse_opts(args) try: plugin.sufficient_options() except exceptions.AuthPluginOptionsMissing: continue return plugin raise exceptions.AuthPluginOptionsMissing(["auth_system"]) @six.add_metaclass(abc.ABCMeta) class BaseAuthPlugin(object): """Base class for authentication plugins. An authentication plugin needs to override at least the authenticate method to be a valid plugin. """ auth_system = None opt_names = [] common_opt_names = [ "auth_system", "username", "password", "tenant_name", "token", "auth_url", ] def __init__(self, auth_system=None, **kwargs): self.auth_system = auth_system or self.auth_system self.opts = dict((name, kwargs.get(name)) for name in self.opt_names) @staticmethod def _parser_add_opt(parser, opt): """Add an option to parser in two variants. :param opt: option name (with underscores) """ dashed_opt = opt.replace("_", "-") env_var = "OS_%s" % opt.upper() arg_default = os.environ.get(env_var, "") arg_help = "Defaults to env[%s]." % env_var parser.add_argument( "--os-%s" % dashed_opt, metavar="<%s>" % dashed_opt, default=arg_default, help=arg_help) parser.add_argument( "--os_%s" % opt, metavar="<%s>" % dashed_opt, help=argparse.SUPPRESS) @classmethod def add_opts(cls, parser): """Populate the parser with the options for this plugin. """ for opt in cls.opt_names: # use `BaseAuthPlugin.common_opt_names` since it is never # changed in child classes if opt not in BaseAuthPlugin.common_opt_names: cls._parser_add_opt(parser, opt) @classmethod def add_common_opts(cls, parser): """Add options that are common for several plugins. """ for opt in cls.common_opt_names: cls._parser_add_opt(parser, opt) @staticmethod def get_opt(opt_name, args): """Return option name and value. :param opt_name: name of the option, e.g., "username" :param args: parsed arguments """ return (opt_name, getattr(args, "os_%s" % opt_name, None)) def parse_opts(self, args): """Parse the actual auth-system options if any. This method is expected to populate the attribute `self.opts` with a dict containing the options and values needed to make authentication. """ self.opts.update(dict(self.get_opt(opt_name, args) for opt_name in self.opt_names)) def authenticate(self, http_client): """Authenticate using plugin defined method. The method usually analyses `self.opts` and performs a request to authentication server. :param http_client: client object that needs authentication :type http_client: HTTPClient :raises: AuthorizationFailure """ self.sufficient_options() self._do_authenticate(http_client) @abc.abstractmethod def _do_authenticate(self, http_client): """Protected method for authentication. """ def sufficient_options(self): """Check if all required options are present. :raises: AuthPluginOptionsMissing """ missing = [opt for opt in self.opt_names if not self.opts.get(opt)] if missing: raise exceptions.AuthPluginOptionsMissing(missing) @abc.abstractmethod def token_and_endpoint(self, endpoint_type, service_type): """Return token and endpoint. :param service_type: Service type of the endpoint :type service_type: string :param endpoint_type: Type of endpoint. Possible values: public or publicURL, internal or internalURL, admin or adminURL :type endpoint_type: string :returns: tuple of token and endpoint strings :raises: EndpointException """ python-ironicclient-0.1.2/ironicclient/openstack/common/apiclient/__init__.py0000664000175400017540000000000012303005215030650 0ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/openstack/common/importutils.py0000664000175400017540000000413412303005215027550 0ustar jenkinsjenkins00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Import related utilities and helper functions. """ import sys import traceback def import_class(import_str): """Returns a class from a string including module and class""" mod_str, _sep, class_str = import_str.rpartition('.') try: __import__(mod_str) return getattr(sys.modules[mod_str], class_str) except (ValueError, AttributeError): raise ImportError('Class %s cannot be found (%s)' % (class_str, traceback.format_exception(*sys.exc_info()))) def import_object(import_str, *args, **kwargs): """Import a class and return an instance of it.""" return import_class(import_str)(*args, **kwargs) def import_object_ns(name_space, import_str, *args, **kwargs): """ Import a class and return an instance of it, first by trying to find the class in a default namespace, then failing back to a full path if not found in the default namespace. """ import_value = "%s.%s" % (name_space, import_str) try: return import_class(import_value)(*args, **kwargs) except ImportError: return import_class(import_str)(*args, **kwargs) def import_module(import_str): """Import a module.""" __import__(import_str) return sys.modules[import_str] def try_import(import_str, default=None): """Try to import a module and if it fails return default.""" try: return import_module(import_str) except ImportError: return default python-ironicclient-0.1.2/ironicclient/openstack/common/strutils.py0000664000175400017540000001660012303005215027047 0ustar jenkinsjenkins00000000000000# Copyright 2011 OpenStack Foundation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ System-level utilities and helper functions. """ import re import sys import unicodedata import six from ironicclient.openstack.common.gettextutils import _ # Used for looking up extensions of text # to their 'multiplied' byte amount BYTE_MULTIPLIERS = { '': 1, 't': 1024 ** 4, 'g': 1024 ** 3, 'm': 1024 ** 2, 'k': 1024, } BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)') TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") def int_from_bool_as_string(subject): """Interpret a string as a boolean and return either 1 or 0. Any string value in: ('True', 'true', 'On', 'on', '1') is interpreted as a boolean True. Useful for JSON-decoded stuff and config file parsing """ return bool_from_string(subject) and 1 or 0 def bool_from_string(subject, strict=False, default=False): """Interpret a string as a boolean. A case-insensitive match is performed such that strings matching 't', 'true', 'on', 'y', 'yes', or '1' are considered True and, when `strict=False`, anything else returns the value specified by 'default'. Useful for JSON-decoded stuff and config file parsing. If `strict=True`, unrecognized values, including None, will raise a ValueError which is useful when parsing values passed in from an API call. Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. """ if not isinstance(subject, six.string_types): subject = str(subject) lowered = subject.strip().lower() if lowered in TRUE_STRINGS: return True elif lowered in FALSE_STRINGS: return False elif strict: acceptable = ', '.join( "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS)) msg = _("Unrecognized value '%(val)s', acceptable values are:" " %(acceptable)s") % {'val': subject, 'acceptable': acceptable} raise ValueError(msg) else: return default def safe_decode(text, incoming=None, errors='strict'): """Decodes incoming str using `incoming` if they're not already unicode. :param incoming: Text's current encoding :param errors: Errors handling policy. See here for valid values http://docs.python.org/2/library/codecs.html :returns: text or a unicode `incoming` encoded representation of it. :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): raise TypeError("%s can't be decoded" % type(text)) if isinstance(text, six.text_type): return text if not incoming: incoming = (sys.stdin.encoding or sys.getdefaultencoding()) try: return text.decode(incoming, errors) except UnicodeDecodeError: # Note(flaper87) If we get here, it means that # sys.stdin.encoding / sys.getdefaultencoding # didn't return a suitable encoding to decode # text. This happens mostly when global LANG # var is not set correctly and there's no # default encoding. In this case, most likely # python will use ASCII or ANSI encoders as # default encodings but they won't be capable # of decoding non-ASCII characters. # # Also, UTF-8 is being used since it's an ASCII # extension. return text.decode('utf-8', errors) def safe_encode(text, incoming=None, encoding='utf-8', errors='strict'): """Encodes incoming str/unicode using `encoding`. If incoming is not specified, text is expected to be encoded with current python's default encoding. (`sys.getdefaultencoding`) :param incoming: Text's current encoding :param encoding: Expected encoding for text (Default UTF-8) :param errors: Errors handling policy. See here for valid values http://docs.python.org/2/library/codecs.html :returns: text or a bytestring `encoding` encoded representation of it. :raises TypeError: If text is not an instance of str """ if not isinstance(text, six.string_types): raise TypeError("%s can't be encoded" % type(text)) if not incoming: incoming = (sys.stdin.encoding or sys.getdefaultencoding()) if isinstance(text, six.text_type): if six.PY3: return text.encode(encoding, errors).decode(incoming) else: return text.encode(encoding, errors) elif text and encoding != incoming: # Decode text before encoding it with `encoding` text = safe_decode(text, incoming, errors) if six.PY3: return text.encode(encoding, errors).decode(incoming) else: return text.encode(encoding, errors) return text def to_bytes(text, default=0): """Converts a string into an integer of bytes. Looks at the last characters of the text to determine what conversion is needed to turn the input text into a byte number. Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive) :param text: String input for bytes size conversion. :param default: Default return value when text is blank. """ match = BYTE_REGEX.search(text) if match: magnitude = int(match.group(1)) mult_key_org = match.group(2) if not mult_key_org: return magnitude elif text: msg = _('Invalid string format: %s') % text raise TypeError(msg) else: return default mult_key = mult_key_org.lower().replace('b', '', 1) multiplier = BYTE_MULTIPLIERS.get(mult_key) if multiplier is None: msg = _('Unknown byte multiplier: %s') % mult_key_org raise TypeError(msg) return magnitude * multiplier def to_slug(value, incoming=None, errors="strict"): """Normalize string. Convert to lowercase, remove non-word characters, and convert spaces to hyphens. Inspired by Django's `slugify` filter. :param value: Text to slugify :param incoming: Text's current encoding :param errors: Errors handling policy. See here for valid values http://docs.python.org/2/library/codecs.html :returns: slugified unicode representation of `value` :raises TypeError: If text is not an instance of str """ value = safe_decode(value, incoming, errors) # NOTE(aababilov): no need to use safe_(encode|decode) here: # encodings are always "ascii", error handling is always "ignore" # and types are always known (first: unicode; second: str) value = unicodedata.normalize("NFKD", value).encode( "ascii", "ignore").decode("ascii") value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() return SLUGIFY_HYPHENATE_RE.sub("-", value) python-ironicclient-0.1.2/ironicclient/openstack/common/py3kcompat/0000775000175400017540000000000012303005362026676 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/openstack/common/py3kcompat/urlutils.py0000664000175400017540000000344412303005215031135 0ustar jenkinsjenkins00000000000000# # Copyright 2013 Canonical Ltd. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # """ Python2/Python3 compatibility layer for OpenStack """ import six if six.PY3: # python3 import urllib.error import urllib.parse import urllib.request urlencode = urllib.parse.urlencode urljoin = urllib.parse.urljoin quote = urllib.parse.quote parse_qsl = urllib.parse.parse_qsl unquote = urllib.parse.unquote unquote_plus = urllib.parse.unquote_plus urlparse = urllib.parse.urlparse urlsplit = urllib.parse.urlsplit urlunsplit = urllib.parse.urlunsplit SplitResult = urllib.parse.SplitResult urlopen = urllib.request.urlopen URLError = urllib.error.URLError pathname2url = urllib.request.pathname2url else: # python2 import urllib import urllib2 import urlparse urlencode = urllib.urlencode quote = urllib.quote unquote = urllib.unquote unquote_plus = urllib.unquote_plus parse = urlparse parse_qsl = parse.parse_qsl urljoin = parse.urljoin urlparse = parse.urlparse urlsplit = parse.urlsplit urlunsplit = parse.urlunsplit SplitResult = parse.SplitResult urlopen = urllib2.urlopen URLError = urllib2.URLError pathname2url = urllib.pathname2url python-ironicclient-0.1.2/ironicclient/openstack/common/py3kcompat/__init__.py0000664000175400017540000000117112303005215031004 0ustar jenkinsjenkins00000000000000# # Copyright 2013 Canonical Ltd. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # python-ironicclient-0.1.2/ironicclient/openstack/common/gettextutils.py0000664000175400017540000003760412303005215027732 0ustar jenkinsjenkins00000000000000# Copyright 2012 Red Hat, Inc. # Copyright 2013 IBM Corp. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ gettext for openstack-common modules. Usual usage in an openstack.common module: from ironicclient.openstack.common.gettextutils import _ """ import copy import gettext import locale from logging import handlers import os import re from babel import localedata import six _localedir = os.environ.get('ironicclient'.upper() + '_LOCALEDIR') _t = gettext.translation('ironicclient', localedir=_localedir, fallback=True) _AVAILABLE_LANGUAGES = {} USE_LAZY = False def enable_lazy(): """Convenience function for configuring _() to use lazy gettext Call this at the start of execution to enable the gettextutils._ function to use lazy gettext functionality. This is useful if your project is importing _ directly instead of using the gettextutils.install() way of importing the _ function. """ global USE_LAZY USE_LAZY = True def _(msg): if USE_LAZY: return Message(msg, domain='ironicclient') else: if six.PY3: return _t.gettext(msg) return _t.ugettext(msg) def install(domain, lazy=False): """Install a _() function using the given translation domain. Given a translation domain, install a _() function using gettext's install() function. The main difference from gettext.install() is that we allow overriding the default localedir (e.g. /usr/share/locale) using a translation-domain-specific environment variable (e.g. NOVA_LOCALEDIR). :param domain: the translation domain :param lazy: indicates whether or not to install the lazy _() function. The lazy _() introduces a way to do deferred translation of messages by installing a _ that builds Message objects, instead of strings, which can then be lazily translated into any available locale. """ if lazy: # NOTE(mrodden): Lazy gettext functionality. # # The following introduces a deferred way to do translations on # messages in OpenStack. We override the standard _() function # and % (format string) operation to build Message objects that can # later be translated when we have more information. def _lazy_gettext(msg): """Create and return a Message object. Lazy gettext function for a given domain, it is a factory method for a project/module to get a lazy gettext function for its own translation domain (i.e. nova, glance, cinder, etc.) Message encapsulates a string so that we can translate it later when needed. """ return Message(msg, domain=domain) from six import moves moves.builtins.__dict__['_'] = _lazy_gettext else: localedir = '%s_LOCALEDIR' % domain.upper() if six.PY3: gettext.install(domain, localedir=os.environ.get(localedir)) else: gettext.install(domain, localedir=os.environ.get(localedir), unicode=True) class Message(six.text_type): """A Message object is a unicode object that can be translated. Translation of Message is done explicitly using the translate() method. For all non-translation intents and purposes, a Message is simply unicode, and can be treated as such. """ def __new__(cls, msgid, msgtext=None, params=None, domain='ironicclient', *args): """Create a new Message object. In order for translation to work gettext requires a message ID, this msgid will be used as the base unicode text. It is also possible for the msgid and the base unicode text to be different by passing the msgtext parameter. """ # If the base msgtext is not given, we use the default translation # of the msgid (which is in English) just in case the system locale is # not English, so that the base text will be in that locale by default. if not msgtext: msgtext = Message._translate_msgid(msgid, domain) # We want to initialize the parent unicode with the actual object that # would have been plain unicode if 'Message' was not enabled. msg = super(Message, cls).__new__(cls, msgtext) msg.msgid = msgid msg.domain = domain msg.params = params return msg def translate(self, desired_locale=None): """Translate this message to the desired locale. :param desired_locale: The desired locale to translate the message to, if no locale is provided the message will be translated to the system's default locale. :returns: the translated message in unicode """ translated_message = Message._translate_msgid(self.msgid, self.domain, desired_locale) if self.params is None: # No need for more translation return translated_message # This Message object may have been formatted with one or more # Message objects as substitution arguments, given either as a single # argument, part of a tuple, or as one or more values in a dictionary. # When translating this Message we need to translate those Messages too translated_params = _translate_args(self.params, desired_locale) translated_message = translated_message % translated_params return translated_message @staticmethod def _translate_msgid(msgid, domain, desired_locale=None): if not desired_locale: system_locale = locale.getdefaultlocale() # If the system locale is not available to the runtime use English if not system_locale[0]: desired_locale = 'en_US' else: desired_locale = system_locale[0] locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR') lang = gettext.translation(domain, localedir=locale_dir, languages=[desired_locale], fallback=True) if six.PY3: translator = lang.gettext else: translator = lang.ugettext translated_message = translator(msgid) return translated_message def __mod__(self, other): # When we mod a Message we want the actual operation to be performed # by the parent class (i.e. unicode()), the only thing we do here is # save the original msgid and the parameters in case of a translation unicode_mod = super(Message, self).__mod__(other) modded = Message(self.msgid, msgtext=unicode_mod, params=self._sanitize_mod_params(other), domain=self.domain) return modded def _sanitize_mod_params(self, other): """Sanitize the object being modded with this Message. - Add support for modding 'None' so translation supports it - Trim the modded object, which can be a large dictionary, to only those keys that would actually be used in a translation - Snapshot the object being modded, in case the message is translated, it will be used as it was when the Message was created """ if other is None: params = (other,) elif isinstance(other, dict): params = self._trim_dictionary_parameters(other) else: params = self._copy_param(other) return params def _trim_dictionary_parameters(self, dict_param): """Return a dict that only has matching entries in the msgid.""" # NOTE(luisg): Here we trim down the dictionary passed as parameters # to avoid carrying a lot of unnecessary weight around in the message # object, for example if someone passes in Message() % locals() but # only some params are used, and additionally we prevent errors for # non-deepcopyable objects by unicoding() them. # Look for %(param) keys in msgid; # Skip %% and deal with the case where % is first character on the line keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid) # If we don't find any %(param) keys but have a %s if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid): # Apparently the full dictionary is the parameter params = self._copy_param(dict_param) else: params = {} for key in keys: params[key] = self._copy_param(dict_param[key]) return params def _copy_param(self, param): try: return copy.deepcopy(param) except TypeError: # Fallback to casting to unicode this will handle the # python code-like objects that can't be deep-copied return six.text_type(param) def __add__(self, other): msg = _('Message objects do not support addition.') raise TypeError(msg) def __radd__(self, other): return self.__add__(other) def __str__(self): # NOTE(luisg): Logging in python 2.6 tries to str() log records, # and it expects specifically a UnicodeError in order to proceed. msg = _('Message objects do not support str() because they may ' 'contain non-ascii characters. ' 'Please use unicode() or translate() instead.') raise UnicodeError(msg) def get_available_languages(domain): """Lists the available languages for the given translation domain. :param domain: the domain to get languages for """ if domain in _AVAILABLE_LANGUAGES: return copy.copy(_AVAILABLE_LANGUAGES[domain]) localedir = '%s_LOCALEDIR' % domain.upper() find = lambda x: gettext.find(domain, localedir=os.environ.get(localedir), languages=[x]) # NOTE(mrodden): en_US should always be available (and first in case # order matters) since our in-line message strings are en_US language_list = ['en_US'] # NOTE(luisg): Babel <1.0 used a function called list(), which was # renamed to locale_identifiers() in >=1.0, the requirements master list # requires >=0.9.6, uncapped, so defensively work with both. We can remove # this check when the master list updates to >=1.0, and update all projects list_identifiers = (getattr(localedata, 'list', None) or getattr(localedata, 'locale_identifiers')) locale_identifiers = list_identifiers() for i in locale_identifiers: if find(i) is not None: language_list.append(i) _AVAILABLE_LANGUAGES[domain] = language_list return copy.copy(language_list) def translate(obj, desired_locale=None): """Gets the translated unicode representation of the given object. If the object is not translatable it is returned as-is. If the locale is None the object is translated to the system locale. :param obj: the object to translate :param desired_locale: the locale to translate the message to, if None the default system locale will be used :returns: the translated object in unicode, or the original object if it could not be translated """ message = obj if not isinstance(message, Message): # If the object to translate is not already translatable, # let's first get its unicode representation message = six.text_type(obj) if isinstance(message, Message): # Even after unicoding() we still need to check if we are # running with translatable unicode before translating return message.translate(desired_locale) return obj def _translate_args(args, desired_locale=None): """Translates all the translatable elements of the given arguments object. This method is used for translating the translatable values in method arguments which include values of tuples or dictionaries. If the object is not a tuple or a dictionary the object itself is translated if it is translatable. If the locale is None the object is translated to the system locale. :param args: the args to translate :param desired_locale: the locale to translate the args to, if None the default system locale will be used :returns: a new args object with the translated contents of the original """ if isinstance(args, tuple): return tuple(translate(v, desired_locale) for v in args) if isinstance(args, dict): translated_dict = {} for (k, v) in six.iteritems(args): translated_v = translate(v, desired_locale) translated_dict[k] = translated_v return translated_dict return translate(args, desired_locale) class TranslationHandler(handlers.MemoryHandler): """Handler that translates records before logging them. The TranslationHandler takes a locale and a target logging.Handler object to forward LogRecord objects to after translating them. This handler depends on Message objects being logged, instead of regular strings. The handler can be configured declaratively in the logging.conf as follows: [handlers] keys = translatedlog, translator [handler_translatedlog] class = handlers.WatchedFileHandler args = ('/var/log/api-localized.log',) formatter = context [handler_translator] class = openstack.common.log.TranslationHandler target = translatedlog args = ('zh_CN',) If the specified locale is not available in the system, the handler will log in the default locale. """ def __init__(self, locale=None, target=None): """Initialize a TranslationHandler :param locale: locale to use for translating messages :param target: logging.Handler object to forward LogRecord objects to after translation """ # NOTE(luisg): In order to allow this handler to be a wrapper for # other handlers, such as a FileHandler, and still be able to # configure it using logging.conf, this handler has to extend # MemoryHandler because only the MemoryHandlers' logging.conf # parsing is implemented such that it accepts a target handler. handlers.MemoryHandler.__init__(self, capacity=0, target=target) self.locale = locale def setFormatter(self, fmt): self.target.setFormatter(fmt) def emit(self, record): # We save the message from the original record to restore it # after translation, so other handlers are not affected by this original_msg = record.msg original_args = record.args try: self._translate_and_log_record(record) finally: record.msg = original_msg record.args = original_args def _translate_and_log_record(self, record): record.msg = translate(record.msg, self.locale) # In addition to translating the message, we also need to translate # arguments that were passed to the log method that were not part # of the main message e.g., log.info(_('Some message %s'), this_one)) record.args = _translate_args(record.args, self.locale) self.target.emit(record) python-ironicclient-0.1.2/ironicclient/openstack/common/__init__.py0000664000175400017540000000116012303005215026710 0ustar jenkinsjenkins00000000000000# Copyright 2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. python-ironicclient-0.1.2/ironicclient/openstack/__init__.py0000664000175400017540000000116012303005215025420 0ustar jenkinsjenkins00000000000000# Copyright 2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. python-ironicclient-0.1.2/ironicclient/exc.py0000664000175400017540000000717512303005215022465 0ustar jenkinsjenkins00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys class BaseException(Exception): """An error occurred.""" def __init__(self, message=None): self.message = message def __str__(self): return self.message or self.__class__.__doc__ class CommandError(BaseException): """Invalid usage of CLI.""" class InvalidEndpoint(BaseException): """The provided endpoint is invalid.""" class CommunicationError(BaseException): """Unable to communicate with server.""" class ClientException(Exception): """DEPRECATED.""" class HTTPException(ClientException): """Base exception for all HTTP-derived exceptions.""" code = 'N/A' def __init__(self, details=None, server_traceback=None): self.details = details self.server_traceback = server_traceback def __str__(self): msg = "%s (HTTP %s)" % (self.__class__.__name__, self.code) if self.details: msg = self.details if self.server_traceback: msg += "\n%s" % self.server_traceback return msg class HTTPMultipleChoices(HTTPException): code = 300 def __str__(self): self.details = ("Requested version of OpenStack Images API is not" "available.") return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code, self.details) class BadRequest(HTTPException): """DEPRECATED.""" code = 400 class HTTPBadRequest(BadRequest): pass class Unauthorized(HTTPException): """DEPRECATED.""" code = 401 class HTTPUnauthorized(Unauthorized): pass class Forbidden(HTTPException): """DEPRECATED.""" code = 403 class HTTPForbidden(Forbidden): pass class NotFound(HTTPException): """DEPRECATED.""" code = 404 class HTTPNotFound(NotFound): pass class HTTPMethodNotAllowed(HTTPException): code = 405 class Conflict(HTTPException): """DEPRECATED.""" code = 409 class HTTPConflict(Conflict): pass class OverLimit(HTTPException): """DEPRECATED.""" code = 413 class HTTPOverLimit(OverLimit): pass class HTTPInternalServerError(HTTPException): code = 500 class HTTPNotImplemented(HTTPException): code = 501 class HTTPBadGateway(HTTPException): code = 502 class ServiceUnavailable(HTTPException): """DEPRECATED.""" code = 503 class HTTPServiceUnavailable(ServiceUnavailable): pass #NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception # classes _code_map = {} for obj_name in dir(sys.modules[__name__]): if obj_name.startswith('HTTP'): obj = getattr(sys.modules[__name__], obj_name) _code_map[obj.code] = obj def from_response(response, message=None, traceback=None): """Return an instance of an HTTPException based on httplib response.""" cls = _code_map.get(response.status, HTTPException) return cls(message, traceback) class NoTokenLookupException(Exception): """DEPRECATED.""" pass class EndpointNotFound(Exception): """DEPRECATED.""" pass class InvalidAttribute(ClientException): pass python-ironicclient-0.1.2/ironicclient/client.py0000664000175400017540000000757212303005215023165 0ustar jenkinsjenkins00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneclient.v2_0 import client as ksclient from ironicclient.common import utils def _get_ksclient(**kwargs): """Get an endpoint and auth token from Keystone. :param kwargs: keyword args containing credentials: * username: name of user * password: user's password * auth_url: endpoint to authenticate against * insecure: allow insecure SSL (no cert verification) * tenant_{name|id}: name or ID of tenant """ return ksclient.Client(username=kwargs.get('username'), password=kwargs.get('password'), tenant_id=kwargs.get('tenant_id'), tenant_name=kwargs.get('tenant_name'), auth_url=kwargs.get('auth_url'), insecure=kwargs.get('insecure')) def _get_endpoint(client, **kwargs): """Get an endpoint using the provided keystone client.""" return client.service_catalog.url_for( service_type=kwargs.get('service_type') or 'baremetal', endpoint_type=kwargs.get('endpoint_type') or 'publicURL') def get_client(api_version, **kwargs): """Get an authtenticated client, based on the credentials in the keyword args. :param api_version: the API version to use ('1' or '2') :param kwargs: keyword args containing credentials, either: * os_auth_token: pre-existing token to re-use * ironic_url: ironic API endpoint or: * os_username: name of user * os_password: user's password * os_auth_url: endpoint to authenticate against * insecure: allow insecure SSL (no cert verification) * os_tenant_{name|id}: name or ID of tenant """ if kwargs.get('os_auth_token') and kwargs.get('ironic_url'): token = kwargs.get('os_auth_token') endpoint = kwargs.get('ironic_url') elif (kwargs.get('os_username') and kwargs.get('os_password') and kwargs.get('os_auth_url') and (kwargs.get('os_tenant_id') or kwargs.get('os_tenant_name'))): ks_kwargs = { 'username': kwargs.get('os_username'), 'password': kwargs.get('os_password'), 'tenant_id': kwargs.get('os_tenant_id'), 'tenant_name': kwargs.get('os_tenant_name'), 'auth_url': kwargs.get('os_auth_url'), 'service_type': kwargs.get('os_service_type'), 'endpoint_type': kwargs.get('os_endpoint_type'), 'insecure': kwargs.get('insecure'), } _ksclient = _get_ksclient(**ks_kwargs) token = (kwargs.get('os_auth_token') if kwargs.get('os_auth_token') else _ksclient.auth_token) endpoint = kwargs.get('ironic_url') or \ _get_endpoint(_ksclient, **ks_kwargs) cli_kwargs = { 'token': token, 'insecure': kwargs.get('insecure'), 'timeout': kwargs.get('timeout'), 'ca_file': kwargs.get('ca_file'), 'cert_file': kwargs.get('cert_file'), 'key_file': kwargs.get('key_file'), } return Client(api_version, endpoint, **cli_kwargs) def Client(version, *args, **kwargs): module = utils.import_versioned_module(version, 'client') client_class = getattr(module, 'Client') return client_class(*args, **kwargs) python-ironicclient-0.1.2/ironicclient/__init__.py0000664000175400017540000000132412303005215023433 0ustar jenkinsjenkins00000000000000# Copyright 2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version __version__ = pbr.version.VersionInfo('python-ironicclient').version_string() python-ironicclient-0.1.2/ironicclient/v1/0000775000175400017540000000000012303005362021653 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/ironicclient/v1/node.py0000664000175400017540000000634312303005215023155 0ustar jenkinsjenkins00000000000000# -*- encoding: utf-8 -*- # # Copyright 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import base from ironicclient import exc CREATION_ATTRIBUTES = ['chassis_uuid', 'driver', 'driver_info', 'extra', 'node_id', 'properties'] class Node(base.Resource): def __repr__(self): return "" % self._info class NodeManager(base.Manager): resource_class = Node @staticmethod def _path(id=None): return '/v1/nodes/%s' % id if id else '/v1/nodes' def list(self, associated=None): if associated is None: return self._list(self._path(), "nodes") else: path = '?associated=%s' % str(bool(associated)) return self._list(self._path(path), "nodes") def list_ports(self, node_id): path = "%s/ports" % node_id return self._list(self._path(path), "ports") def get(self, node_id): try: return self._list(self._path(node_id))[0] except IndexError: return None def get_by_instance_uuid(self, instance_uuid): path = "?instance_uuid=%s" % instance_uuid nodes = self._list(self._path(path), 'nodes') # get all the details of the node assuming that # filtering by instance_uuid returns a collection # of one node if successful. if len(nodes) > 0: uuid = getattr(nodes[0], 'uuid') return self.get(uuid) else: raise exc.HTTPNotFound() def create(self, **kwargs): new = {} for (key, value) in kwargs.items(): if key in CREATION_ATTRIBUTES: new[key] = value else: raise exc.InvalidAttribute() return self._create(self._path(), new) def delete(self, node_id): return self._delete(self._path(node_id)) def update(self, node_id, patch): return self._update(self._path(node_id), patch) def set_power_state(self, node_id, state): path = "%s/states/power" % node_id if state in ['on', 'off']: state = "power %s" % state if state in ['reboot']: state = "rebooting" target = {'target': state} return self._update(self._path(path), target, method='PUT') def validate(self, node_uuid): path = "%s/validate" % node_uuid return self.get(path) def set_provision_state(self, node_uuid, state): path = "%s/states/provision" % node_uuid target = {'target': state} return self._update(self._path(path), target, method='PUT') def states(self, node_uuid): path = "%s/states" % node_uuid return self.get(path) python-ironicclient-0.1.2/ironicclient/v1/driver.py0000664000175400017540000000162712303005215023523 0ustar jenkinsjenkins00000000000000# coding: utf-8 # # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import base class Driver(base.Resource): def __repr__(self): return "" % self._info class DriverManager(base.Manager): resource_class = Driver def list(self): return self._list('/v1/drivers', "drivers") python-ironicclient-0.1.2/ironicclient/v1/shell.py0000664000175400017540000000250212303005215023330 0ustar jenkinsjenkins00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import utils from ironicclient.v1 import chassis_shell from ironicclient.v1 import driver_shell from ironicclient.v1 import node_shell from ironicclient.v1 import port_shell COMMAND_MODULES = [ chassis_shell, node_shell, port_shell, driver_shell, ] def enhance_parser(parser, subparsers, cmd_mapper): '''Take a basic (nonversioned) parser and enhance it with commands and options specific for this version of API. :param parser: top level parser :param subparsers: top level parser's subparsers collection where subcommands will go ''' for command_module in COMMAND_MODULES: utils.define_commands_from_module(subparsers, command_module, cmd_mapper) python-ironicclient-0.1.2/ironicclient/v1/port.py0000664000175400017540000000322712303005215023212 0ustar jenkinsjenkins00000000000000# -*- encoding: utf-8 -*- # # Copyright © 2013 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import base from ironicclient import exc CREATION_ATTRIBUTES = ['address', 'extra', 'node_uuid'] class Port(base.Resource): def __repr__(self): return "" % self._info class PortManager(base.Manager): resource_class = Port @staticmethod def _path(id=None): return '/v1/ports/%s' % id if id else '/v1/ports' def list(self): return self._list(self._path(), "ports") def get(self, port_id): try: return self._list(self._path(port_id))[0] except IndexError: return None def create(self, **kwargs): new = {} for (key, value) in kwargs.items(): if key in CREATION_ATTRIBUTES: new[key] = value else: raise exc.InvalidAttribute() return self._create(self._path(), new) def delete(self, port_id): return self._delete(self._path(port_id)) def update(self, port_id, patch): return self._update(self._path(port_id), patch) python-ironicclient-0.1.2/ironicclient/v1/port_shell.py0000664000175400017540000000612412303005215024400 0ustar jenkinsjenkins00000000000000#!/usr/bin/env python # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import utils def _print_port_show(port): fields = ['address', 'created_at', 'extra', 'node_uuid', 'updated_at', 'uuid'] data = dict([(f, getattr(port, f, '')) for f in fields]) utils.print_dict(data, wrap=72) @utils.arg('port', metavar='', help="UUID of port") def do_port_show(cc, args): """Show a port.""" port = cc.port.get(args.port) _print_port_show(port) def do_port_list(cc, args): """List ports.""" port = cc.port.list() field_labels = ['UUID', 'Address'] fields = ['uuid', 'address'] utils.print_list(port, fields, field_labels, sortby=1) @utils.arg('-a', '--address', metavar='
', required=True, help='MAC Address for this port') @utils.arg('-n', '--node_uuid', metavar='', required=True, help='UUID of the node that this port belongs to') @utils.arg('-e', '--extra', metavar="", action='append', help="Record arbitrary key/value metadata. " "Can be specified multiple times") def do_port_create(cc, args): """Create a new port.""" field_list = ['address', 'extra', 'node_uuid'] fields = dict((k, v) for (k, v) in vars(args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'extra') port = cc.port.create(**fields) field_list.append('uuid') data = dict([(f, getattr(port, f, '')) for f in field_list]) utils.print_dict(data, wrap=72) @utils.arg('port', metavar='', nargs='+', help="UUID of port") def do_port_delete(cc, args): """Delete a port.""" for p in args.port: cc.port.delete(p) print ('Deleted port %s' % p) @utils.arg('port', metavar='', help="UUID of port") @utils.arg('op', metavar='', choices=['add', 'replace', 'remove'], help="Operations: 'add', 'replace' or 'remove'") @utils.arg('attributes', metavar='', nargs='+', action='append', default=[], help="Attributes to add/replace or remove " "(only PATH is necessary on remove)") def do_port_update(cc, args): """Update a port.""" patch = utils.args_array_to_patch(args.op, args.attributes[0]) port = cc.port.update(args.port, patch) _print_port_show(port) python-ironicclient-0.1.2/ironicclient/v1/node_shell.py0000664000175400017540000001355012303005216024343 0ustar jenkinsjenkins00000000000000#!/usr/bin/env python # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import utils def _print_node_show(node): fields = ['chassis_uuid', 'created_at', 'driver', 'driver_info', 'extra', 'instance_uuid', 'last_error', 'maintenance', 'power_state', 'properties', 'provision_state', 'reservation', 'target_power_state', 'target_provision_state', 'updated_at', 'uuid'] data = dict([(f, getattr(node, f, '')) for f in fields]) utils.print_dict(data, wrap=72) @utils.arg('node', metavar='', help="ID, UUID or instance UUID of node") @utils.arg('--instance', dest='instance_uuid', action='store_true', default=False, help='The id is an instance UUID') def do_node_show(cc, args): """Show a node.""" if args.instance_uuid: node = cc.node.get_by_instance_uuid(args.node) else: node = cc.node.get(args.node) _print_node_show(node) @utils.arg('--associated', metavar='', choices=['true', 'True', 'false', 'False'], help="List nodes by instance association: 'true' or 'false'") def do_node_list(cc, args): """List nodes.""" if args.associated is None: nodes = cc.node.list() else: associated = args.associated.lower() == 'true' nodes = cc.node.list(associated) field_labels = ['UUID', 'Instance UUID', 'Power State', 'Provisioning State'] fields = ['uuid', 'instance_uuid', 'power_state', 'provision_state'] utils.print_list(nodes, fields, field_labels, sortby=1) @utils.arg('-c', '--chassis_uuid', metavar='', help='UUID of the chassis that this node belongs to') @utils.arg('-d', '--driver', metavar='', help='Driver used to control the node [REQUIRED]') @utils.arg('-i', '--driver_info', metavar='', action='append', help='Key/value pairs used by the driver. ' 'Can be specified multiple times') @utils.arg('-p', '--properties', metavar='', action='append', help='Key/value pairs describing the physical characteristics ' 'of the node. This is exported to Nova and used by the ' 'scheduler. Can be specified multiple times') @utils.arg('-e', '--extra', metavar='', action='append', help="Record arbitrary key/value metadata. " "Can be specified multiple times") def do_node_create(cc, args): """Create a new node.""" field_list = ['chassis_uuid', 'driver', 'driver_info', 'properties', 'extra'] fields = dict((k, v) for (k, v) in vars(args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'driver_info') fields = utils.args_array_to_dict(fields, 'extra') fields = utils.args_array_to_dict(fields, 'properties') node = cc.node.create(**fields) field_list.append('uuid') data = dict([(f, getattr(node, f, '')) for f in field_list]) utils.print_dict(data, wrap=72) @utils.arg('node', metavar='', nargs='+', help="UUID of node") def do_node_delete(cc, args): """Delete a node.""" for n in args.node: cc.node.delete(n) print(_('Deleted node %s') % n) @utils.arg('node', metavar='', help="UUID of node") @utils.arg('op', metavar='', choices=['add', 'replace', 'remove'], help="Operations: 'add', 'replace' or 'remove'") @utils.arg('attributes', metavar='', nargs='+', action='append', default=[], help="Attributes to add/replace or remove " "(only PATH is necessary on remove)") def do_node_update(cc, args): """Update a node.""" patch = utils.args_array_to_patch(args.op, args.attributes[0]) node = cc.node.update(args.node, patch) _print_node_show(node) @utils.arg('node', metavar='', help="UUID of node") def do_node_port_list(cc, args): """List the ports contained in the node.""" ports = cc.node.list_ports(args.node) field_labels = ['UUID', 'Address'] fields = ['uuid', 'address'] utils.print_list(ports, fields, field_labels, sortby=1) @utils.arg('node', metavar='', help="UUID of node") @utils.arg('power_state', metavar='', choices=['on', 'off', 'reboot'], help="Supported states: 'on' or 'off' or 'reboot'") def do_node_set_power_state(cc, args): """Power the node on or off.""" cc.node.set_power_state(args.node, args.power_state) @utils.arg('node', metavar='', help="UUID of node") def do_node_validate(cc, args): """Validate the node driver interfaces.""" ifaces = cc.node.validate(args.node) obj_list = [] for key, value in ifaces.to_dict().iteritems(): data = {'interface': key} data.update(value) obj_list.append(type('iface', (object,), data)) field_labels = ['Interface', 'Result', 'Reason'] fields = ['interface', 'result', 'reason'] utils.print_list(obj_list, fields, field_labels) python-ironicclient-0.1.2/ironicclient/v1/chassis.py0000664000175400017540000000346612303005215023670 0ustar jenkinsjenkins00000000000000# -*- encoding: utf-8 -*- # # Copyright © 2013 Red Hat, Inc # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import base from ironicclient import exc CREATION_ATTRIBUTES = ['description', 'extra'] class Chassis(base.Resource): def __repr__(self): return "" % self._info class ChassisManager(base.Manager): resource_class = Chassis @staticmethod def _path(id=None): return '/v1/chassis/%s' % id if id else '/v1/chassis' def list(self): return self._list(self._path(), "chassis") def list_nodes(self, chassis_id): path = "%s/nodes" % chassis_id return self._list(self._path(path), "nodes") def get(self, chassis_id): try: return self._list(self._path(chassis_id))[0] except IndexError: return None def create(self, **kwargs): new = {} for (key, value) in kwargs.items(): if key in CREATION_ATTRIBUTES: new[key] = value else: raise exc.InvalidAttribute() return self._create(self._path(), new) def delete(self, chassis_id): return self._delete(self._path(chassis_id)) def update(self, chassis_id, patch): return self._update(self._path(chassis_id), patch) python-ironicclient-0.1.2/ironicclient/v1/driver_shell.py0000664000175400017540000000204212303005215024702 0ustar jenkinsjenkins00000000000000# coding: utf-8 # # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import utils def do_driver_list(cc, args): """List drivers.""" drivers = cc.driver.list() # NOTE(lucasagomes): Separate each host by a comma. # It's easier to read. for d in drivers: d.hosts = ', '.join(d.hosts) field_labels = ['Supported driver(s)', 'Active host(s)'] fields = ['name', 'hosts'] utils.print_list(drivers, fields, field_labels) python-ironicclient-0.1.2/ironicclient/v1/client.py0000664000175400017540000000304412303005215023501 0ustar jenkinsjenkins00000000000000# Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import http from ironicclient.v1 import chassis from ironicclient.v1 import driver from ironicclient.v1 import node from ironicclient.v1 import port class Client(object): """Client for the Ironic v1 API. :param string endpoint: A user-supplied endpoint URL for the ironic service. :param function token: Provides token for authentication. :param integer timeout: Allows customization of the timeout for client http requests. (optional) """ def __init__(self, *args, **kwargs): """Initialize a new client for the Ironic v1 API.""" self.http_client = http.HTTPClient(*args, **kwargs) self.chassis = chassis.ChassisManager(self.http_client) self.node = node.NodeManager(self.http_client) self.port = port.PortManager(self.http_client) self.driver = driver.DriverManager(self.http_client) python-ironicclient-0.1.2/ironicclient/v1/chassis_shell.py0000664000175400017540000000671612303005215025060 0ustar jenkinsjenkins00000000000000#!/usr/bin/env python # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from ironicclient.common import utils def _print_chassis_show(chassis): fields = ['uuid', 'description', 'created_at', 'updated_at', 'extra'] data = dict([(f, getattr(chassis, f, '')) for f in fields]) utils.print_dict(data, wrap=72) @utils.arg('chassis', metavar='', help="UUID of chassis") def do_chassis_show(cc, args): """Show a chassis.""" chassis = cc.chassis.get(args.chassis) _print_chassis_show(chassis) def do_chassis_list(cc, args): """List chassis.""" chassis = cc.chassis.list() field_labels = ['UUID', 'Description'] fields = ['uuid', 'description'] utils.print_list(chassis, fields, field_labels, sortby=1) @utils.arg('-d', '--description', metavar='', help='Free text description of the chassis') @utils.arg('-e', '--extra', metavar="", action='append', help="Record arbitrary key/value metadata. " "Can be specified multiple times") def do_chassis_create(cc, args): """Create a new chassis.""" field_list = ['description', 'extra'] fields = dict((k, v) for (k, v) in vars(args).items() if k in field_list and not (v is None)) fields = utils.args_array_to_dict(fields, 'extra') chassis = cc.chassis.create(**fields) field_list.append('uuid') data = dict([(f, getattr(chassis, f, '')) for f in field_list]) utils.print_dict(data, wrap=72) @utils.arg('chassis', metavar='', nargs='+', help="UUID of chassis") def do_chassis_delete(cc, args): """Delete a chassis.""" for c in args.chassis: cc.chassis.delete(c) print('Deleted chassis %s' % c) @utils.arg('chassis', metavar='', help="UUID of chassis") @utils.arg('op', metavar='', choices=['add', 'replace', 'remove'], help="Operations: 'add', 'replace' or 'remove'") @utils.arg('attributes', metavar='', nargs='+', action='append', default=[], help="Attributes to add/replace or remove " "(only PATH is necessary on remove)") def do_chassis_update(cc, args): """Update a chassis.""" patch = utils.args_array_to_patch(args.op, args.attributes[0]) chassis = cc.chassis.update(args.chassis, patch) _print_chassis_show(chassis) @utils.arg('chassis', metavar='', help="UUID of chassis") def do_chassis_node_list(cc, args): """List the nodes contained in the chassis.""" nodes = cc.chassis.list_nodes(args.chassis) field_labels = ['UUID', 'Instance UUID', 'Power State', 'Provisioning State'] fields = ['uuid', 'instance_uuid', 'power_state', 'provision_state'] utils.print_list(nodes, fields, field_labels, sortby=1) python-ironicclient-0.1.2/ironicclient/v1/__init__.py0000664000175400017540000000121312303005215023756 0ustar jenkinsjenkins00000000000000#!/usr/bin/env python # Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. python-ironicclient-0.1.2/test-requirements.txt0000664000175400017540000000023412303005215023100 0ustar jenkinsjenkins00000000000000hacking>=0.8.0,<0.9 coverage>=3.6 discover fixtures>=0.3.14 mock>=1.0 Babel>=1.3 python-subunit sphinx>=1.1.2,<1.2 testrepository>=0.0.17 testtools>=0.9.32 python-ironicclient-0.1.2/ChangeLog0000664000175400017540000000646012303005362020423 0ustar jenkinsjenkins00000000000000CHANGES ======= 0.1.2 ----- * Fix the test parameter order * Return 'maintenance' from node-show command * Remove vim header * Node power state is not printed after set * Return node_uuid from a port-show cmd * Fix Iterface misspelling from node-validate cmd * Remove tox locale overrides * Add node.states() to the client library * Fix node-create help requiring chassis uuid * Updated from global requirements * Remove unused method 'string_to_bool' from utils 0.1.1 ----- * Reuse Resource from oslo * Sync apiclient and strutils from Oslo * Run unittest with python 3.3 * Drop python2.5 suport in ironicclient.common.http * Improve node-validate command output * Add set_provision_state to the client libs * ironic client should display chassis_uuid value * Update openstack-common.conf list, sync with oslo (0d8f18b) 0.1.0 ----- * Rename nodes//state to nodes//states * Remove unused oslo-incubator modules * Updated from global requirements * driver-list command to show the list of conductors 0.0.1 ----- * Enable rebooting in the client lib and cli * Move from inheritance HTTPClient in Ironic client * Move six dependency to requirements.txt * Let CLI print exception traceback from 'debuginfo' * Add missing i18n support * Add node-validate to cli * Replace chassis_id with chassis_uuid on Nodes * Enable created_at/updated_at for port-show/chassis-show * Include 'chassis_id' on node-show * Enable created_at/updated_at for port-show/chassis-show * Remove in-place try/except blocks from shell commands * Replace node_id to node_uuid on Ports * Support building wheels (PEP-427) * Shows 'last_error' property for a node * Comply with new hacking release * Change id->uuid in ironic cli help messages * Updated from global requirements * Modifies CLI to show nodes by instance uuid * Remove Python 2.4 all() implementation * Deal with unicode strings * Custom output file on the print_*() functions * Add driver-list * Fix cmd usage msg for ironic port-create * Reorder imports * Rename variables called 'fixtures' * Comply with new hacking requirements * Transform print statement to print function * Add node-set-power-state to cli * Fixes Auth Token being sent as lambda function * Updated from global requirements * Sort the dict iterms * Replace basestring with six.string_types * Use six.iteritems() for dict * Import urlutils module from oslo * Import httplib from six.moves * Transform print statement to print function * Import StringIO from six * Removes mox from ironicclient * Add chassis-node-list * Add node-port-list * Updated from global requirements * Allows multiple deletions * Add short command line option * Improve help strings * Remove duplicated do_node_show * Add missing chassis_id arg to node-create * Improve error feedback * Updated from global requirements * Implement *-update commands to resrouces * Change service_type to baremetal * Split v1/shell.py into different files * Implement Node commands * Add port-create and port-delete * Stop HTTPClient from calling self.auth_token() * Add chassis-create and chassis-delete * Add port-show and port-list * Implement chassis-list + more tests * Add tests for chassis * Add basic tests * Manager._list should always return a list * Implement chassis-show * Basic project structure * remove client suffix from command line client * Add initial files * Added .gitreview python-ironicclient-0.1.2/doc/0000775000175400017540000000000012303005362017410 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/doc/source/0000775000175400017540000000000012303005362020710 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/doc/source/conf.py0000664000175400017540000000413412303005215022206 0ustar jenkinsjenkins00000000000000# -*- coding: utf-8 -*- # # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', 'oslo.sphinx' ] # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'python-ironicclient' copyright = u'OpenStack Foundation' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['ironicclient.'] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme_path = ["."] html_theme = '_theme' html_static_path = ['_static'] # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ( 'index', '%s.tex' % project, u'%s Documentation' % project, u'OpenStack LLC', 'manual' ), ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} python-ironicclient-0.1.2/setup.cfg0000664000175400017540000000171612303005362020471 0ustar jenkinsjenkins00000000000000[metadata] name = python-ironicclient summary = OpenStack Bare Metal Provisioning API Client Library description-file = README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org home-page = http://www.openstack.org/ classifier = Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 2.6 Programming Language :: Python :: 3 Programming Language :: Python :: 3.3 [files] packages = ironicclient [entry_points] console_scripts = ironic = ironicclient.shell:main [pbr] autodoc_index_modules = True [build_sphinx] all_files = 1 build-dir = doc/build source-dir = doc/source [wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 python-ironicclient-0.1.2/setup.py0000664000175400017540000000141512303005215020353 0ustar jenkinsjenkins00000000000000#!/usr/bin/env python # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT import setuptools setuptools.setup( setup_requires=['pbr'], pbr=True) python-ironicclient-0.1.2/tools/0000775000175400017540000000000012303005362020003 5ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/tools/with_venv.sh0000775000175400017540000000033212303005215022346 0ustar jenkinsjenkins00000000000000#!/bin/bash tools_path=${tools_path:-$(dirname $0)} venv_path=${venv_path:-${tools_path}} venv_dir=${venv_name:-/../.venv} TOOLS=${tools_path} VENV=${venv:-${venv_path}/${venv_dir}} source ${VENV}/bin/activate && "$@" python-ironicclient-0.1.2/tools/install_venv_common.py0000664000175400017540000001615512303005215024436 0ustar jenkinsjenkins00000000000000# Copyright 2013 OpenStack Foundation # Copyright 2013 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Provides methods needed by installation script for OpenStack development virtual environments. Since this script is used to bootstrap a virtualenv from the system's Python environment, it should be kept strictly compatible with Python 2.6. Synced in from openstack-common """ from __future__ import print_function import optparse import os import subprocess import sys class InstallVenv(object): def __init__(self, root, venv, requirements, test_requirements, py_version, project): self.root = root self.venv = venv self.requirements = requirements self.test_requirements = test_requirements self.py_version = py_version self.project = project def die(self, message, *args): print(message % args, file=sys.stderr) sys.exit(1) def check_python_version(self): if sys.version_info < (2, 6): self.die("Need Python Version >= 2.6") def run_command_with_code(self, cmd, redirect_output=True, check_exit_code=True): """Runs a command in an out-of-process shell. Returns the output of that command. Working directory is self.root. """ if redirect_output: stdout = subprocess.PIPE else: stdout = None proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) output = proc.communicate()[0] if check_exit_code and proc.returncode != 0: self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) return (output, proc.returncode) def run_command(self, cmd, redirect_output=True, check_exit_code=True): return self.run_command_with_code(cmd, redirect_output, check_exit_code)[0] def get_distro(self): if (os.path.exists('/etc/fedora-release') or os.path.exists('/etc/redhat-release')): return Fedora( self.root, self.venv, self.requirements, self.test_requirements, self.py_version, self.project) else: return Distro( self.root, self.venv, self.requirements, self.test_requirements, self.py_version, self.project) def check_dependencies(self): self.get_distro().install_virtualenv() def create_virtualenv(self, no_site_packages=True): """Creates the virtual environment and installs PIP. Creates the virtual environment and installs PIP only into the virtual environment. """ if not os.path.isdir(self.venv): print('Creating venv...', end=' ') if no_site_packages: self.run_command(['virtualenv', '-q', '--no-site-packages', self.venv]) else: self.run_command(['virtualenv', '-q', self.venv]) print('done.') else: print("venv already exists...") pass def pip_install(self, *args): self.run_command(['tools/with_venv.sh', 'pip', 'install', '--upgrade'] + list(args), redirect_output=False) def install_dependencies(self): print('Installing dependencies with pip (this can take a while)...') # First things first, make sure our venv has the latest pip and # setuptools. self.pip_install('pip>=1.3') self.pip_install('setuptools') self.pip_install('-r', self.requirements) self.pip_install('-r', self.test_requirements) def post_process(self): self.get_distro().post_process() def parse_args(self, argv): """Parses command-line arguments.""" parser = optparse.OptionParser() parser.add_option('-n', '--no-site-packages', action='store_true', help="Do not inherit packages from global Python " "install") return parser.parse_args(argv[1:])[0] class Distro(InstallVenv): def check_cmd(self, cmd): return bool(self.run_command(['which', cmd], check_exit_code=False).strip()) def install_virtualenv(self): if self.check_cmd('virtualenv'): return if self.check_cmd('easy_install'): print('Installing virtualenv via easy_install...', end=' ') if self.run_command(['easy_install', 'virtualenv']): print('Succeeded') return else: print('Failed') self.die('ERROR: virtualenv not found.\n\n%s development' ' requires virtualenv, please install it using your' ' favorite package management tool' % self.project) def post_process(self): """Any distribution-specific post-processing gets done here. In particular, this is useful for applying patches to code inside the venv. """ pass class Fedora(Distro): """This covers all Fedora-based distributions. Includes: Fedora, RHEL, CentOS, Scientific Linux """ def check_pkg(self, pkg): return self.run_command_with_code(['rpm', '-q', pkg], check_exit_code=False)[1] == 0 def apply_patch(self, originalfile, patchfile): self.run_command(['patch', '-N', originalfile, patchfile], check_exit_code=False) def install_virtualenv(self): if self.check_cmd('virtualenv'): return if not self.check_pkg('python-virtualenv'): self.die("Please install 'python-virtualenv'.") super(Fedora, self).install_virtualenv() def post_process(self): """Workaround for a bug in eventlet. This currently affects RHEL6.1, but the fix can safely be applied to all RHEL and Fedora distributions. This can be removed when the fix is applied upstream. Nova: https://bugs.launchpad.net/nova/+bug/884915 Upstream: https://bitbucket.org/eventlet/eventlet/issue/89 RHEL: https://bugzilla.redhat.com/958868 """ # Install "patch" program if it's not there if not self.check_pkg('patch'): self.die("Please install 'patch'.") # Apply the eventlet patch self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, 'site-packages', 'eventlet/green/subprocess.py'), 'contrib/redhat-eventlet.patch') python-ironicclient-0.1.2/tools/__init__.py0000664000175400017540000000000012303005215022077 0ustar jenkinsjenkins00000000000000python-ironicclient-0.1.2/README.rst0000664000175400017540000000017512303005215020332 0ustar jenkinsjenkins00000000000000Python bindings for the Ironic API ================================== A python and command line client library for Ironic. python-ironicclient-0.1.2/openstack-common.conf0000664000175400017540000000034212303005215022763 0ustar jenkinsjenkins00000000000000[DEFAULT] # The list of modules to copy from openstack-common module=gettextutils module=importutils module=py3kcompat module=apiclient module=strutils # The base module to hold the copy of openstack.common base=ironicclient python-ironicclient-0.1.2/PKG-INFO0000664000175400017540000000173512303005362017746 0ustar jenkinsjenkins00000000000000Metadata-Version: 1.1 Name: python-ironicclient Version: 0.1.2 Summary: OpenStack Bare Metal Provisioning API Client Library Home-page: http://www.openstack.org/ Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: UNKNOWN Description: Python bindings for the Ironic API ================================== A python and command line client library for Ironic. Platform: UNKNOWN Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 python-ironicclient-0.1.2/.testr.conf0000664000175400017540000000032412303005215020725 0ustar jenkinsjenkins00000000000000[DEFAULT] test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 ${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list python-ironicclient-0.1.2/LICENSE0000664000175400017540000002363712303005215017660 0ustar jenkinsjenkins00000000000000 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. python-ironicclient-0.1.2/requirements.txt0000664000175400017540000000017212303005215022124 0ustar jenkinsjenkins00000000000000pbr>=0.5.21,<1.0 anyjson>=0.3.3 argparse httplib2 lxml>=2.3 PrettyTable>=0.7,<0.8 python-keystoneclient>=0.4.2 six>=1.4.1 python-ironicclient-0.1.2/MANIFEST.in0000664000175400017540000000013612303005215020376 0ustar jenkinsjenkins00000000000000include AUTHORS include ChangeLog exclude .gitignore exclude .gitreview global-exclude *.pyc python-ironicclient-0.1.2/AUTHORS0000664000175400017540000000000112303005362017702 0ustar jenkinsjenkins00000000000000 python-ironicclient-0.1.2/tox.ini0000664000175400017540000000127712303005215020162 0ustar jenkinsjenkins00000000000000[tox] minversion = 1.6 envlist = py26,py27,py33,pep8 skipsdist = True [testenv] setenv = VIRTUAL_ENV={envdir} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = python setup.py testr --slowest --testr-args='{posargs}' [tox:jenkins] downloadcache = ~/cache/pip [testenv:pep8] commands = flake8 {posargs} [testenv:cover] setenv = VIRTUAL_ENV={envdir} commands = python tools/patch_tox_venv.py python setup.py testr --coverage {posargs} [testenv:venv] commands = {posargs} [flake8] ignore = E12 builtins = _ exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools [hacking] import_exceptions = testtools.matchers