diff options
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | ironic/api/__init__.py | 37 | ||||
-rw-r--r-- | ironic/api/acl.py | 56 | ||||
-rw-r--r-- | ironic/api/app.py | 81 | ||||
-rw-r--r-- | ironic/api/config.py | 43 | ||||
-rw-r--r-- | ironic/api/controllers/__init__.py | 16 | ||||
-rw-r--r-- | ironic/api/controllers/root.py | 31 | ||||
-rw-r--r-- | ironic/api/controllers/v1.py | 165 | ||||
-rw-r--r-- | ironic/api/hooks.py | 45 | ||||
-rw-r--r-- | ironic/api/model/__init__.py | 21 | ||||
-rw-r--r-- | ironic/api/templates/index.html | 12 | ||||
-rw-r--r-- | ironic/cmd/api.py | 54 | ||||
-rw-r--r-- | ironic/cmd/manager.py | 46 | ||||
-rw-r--r-- | ironic/common/policy.py | 16 | ||||
-rw-r--r-- | ironic/common/service.py | 92 | ||||
-rw-r--r-- | ironic/db/__init__.py | 6 | ||||
-rw-r--r-- | ironic/db/api.py | 13 | ||||
-rw-r--r-- | ironic/db/sqlalchemy/api.py | 6 | ||||
-rw-r--r-- | ironic/doc/api/v1.rst | 28 | ||||
-rw-r--r-- | ironic/openstack/common/rpc/__init__.py | 8 | ||||
-rw-r--r-- | openstack-common.conf | 2 | ||||
-rw-r--r-- | requirements.txt | 4 | ||||
-rw-r--r-- | setup.cfg | 4 | ||||
-rw-r--r-- | tools/install_venv_common.py | 22 | ||||
-rw-r--r-- | tools/pip-requires | 5 |
25 files changed, 780 insertions, 39 deletions
diff --git a/.gitignore b/.gitignore index 1fe4a33f0..ad8160261 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ dist build eggs parts -bin var sdist develop-eggs @@ -24,12 +23,9 @@ develop-eggs *.DS_Store .testrepository .tox +.venv .*.swp .coverage cover AUTHORS ChangeLog - -.testrepository/ -.tox -.venv diff --git a/ironic/api/__init__.py b/ironic/api/__init__.py new file mode 100644 index 000000000..1d6743d2e --- /dev/null +++ b/ironic/api/__init__.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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 flask.helpers +from oslo.config import cfg + +from ironic.openstack.common import jsonutils + +flask.helpers.json = jsonutils + +API_SERVICE_OPTS = [ + cfg.StrOpt('ironic_api_bind_ip', + default='0.0.0.0', + help='IP for the Ironic API server to bind to', + ), + cfg.IntOpt('ironic_api_port', + default=6385, + help='The port for the Ironic API server', + ), + ] + +CONF = cfg.CONF +CONF.register_opts(API_SERVICE_OPTS) diff --git a/ironic/api/acl.py b/ironic/api/acl.py new file mode 100644 index 000000000..eab697e69 --- /dev/null +++ b/ironic/api/acl.py @@ -0,0 +1,56 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann <doug.hellmann@dreamhost.com> +# +# 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. + +"""Access Control Lists (ACL's) control access the API server.""" + +from keystoneclient.middleware import auth_token +from oslo.config import cfg +from pecan import hooks +from webob import exc + +from ironic.common import policy + + +OPT_GROUP_NAME = 'keystone_authtoken' + + +def register_opts(conf): + """Register keystoneclient middleware options + """ + conf.register_opts(auth_token.opts, + group=OPT_GROUP_NAME) + auth_token.CONF = conf + + +register_opts(cfg.CONF) + + +def install(app, conf): + """Install ACL check on application.""" + return auth_token.AuthProtocol(app, + conf=dict(conf.get(OPT_GROUP_NAME))) + + +class AdminAuthHook(hooks.PecanHook): + """Verify that the user has admin rights + """ + + def before(self, state): + headers = state.request.headers + if not policy.check_is_admin(headers.get('X-Roles', "").split(",")): + raise exc.HTTPUnauthorized() diff --git a/ironic/api/app.py b/ironic/api/app.py new file mode 100644 index 000000000..12f5b8d1b --- /dev/null +++ b/ironic/api/app.py @@ -0,0 +1,81 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# 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 oslo.config import cfg +import pecan + +from ironic.api import acl +from ironic.api import config +from ironic.api import hooks + +auth_opts = [ + cfg.StrOpt('auth_strategy', + default='noauth', + help='Method to use for auth: noauth or keystone.'), + ] + +CONF = cfg.CONF +CONF.register_opts(auth_opts) + + +def get_pecan_config(): + # Set up the pecan configuration + filename = config.__file__.replace('.pyc', '.py') + return pecan.configuration.conf_from_file(filename) + + +def setup_app(pecan_config=None, extra_hooks=None): + # FIXME: Replace DBHook with a hooks.TransactionHook + app_hooks = [hooks.ConfigHook()] +# hooks.DBHook()] + if extra_hooks: + app_hooks.extend(extra_hooks) + + if not pecan_config: + pecan_config = get_pecan_config() + + if pecan_config.app.enable_acl: + app_hooks.append(acl.AdminAuthHook()) + + pecan.configuration.set_config(dict(pecan_config), overwrite=True) + + app = pecan.make_app( + pecan_config.app.root, + static_root=pecan_config.app.static_root, + template_path=pecan_config.app.template_path, + logging=getattr(pecan_config, 'logging', {}), + debug=getattr(pecan_config.app, 'debug', False), + force_canonical=getattr(pecan_config.app, 'force_canonical', True), + hooks=app_hooks, + ) +# wrap_app=middleware.ParsableErrorMiddleware, + + if pecan_config.app.enable_acl: + return acl.install(app, cfg.CONF) + + return app + + +class VersionSelectorApplication(object): + def __init__(self): + pc = get_pecan_config() + pc.app.debug = CONF.debug + pc.app.enable_acl = (CONF.auth_strategy == 'keystone') + self.v1 = setup_app(pecan_config=pc) + + def __call__(self, environ, start_response): + return self.v1(environ, start_response) diff --git a/ironic/api/config.py b/ironic/api/config.py new file mode 100644 index 000000000..d9278fd7f --- /dev/null +++ b/ironic/api/config.py @@ -0,0 +1,43 @@ +# Server Specific Configurations +server = { + 'port': '6382', + 'host': '0.0.0.0' +} + +# Pecan Application Configurations +app = { + 'root': 'ironic.api.controllers.root.RootController', + 'modules': ['ironic.api'], + 'static_root': '%(confdir)s/public', + 'template_path': '%(confdir)s/ironic/api/templates', + 'debug': False, + 'enable_acl': False, +} + +logging = { + 'loggers': { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'ironic': {'level': 'DEBUG', 'handlers': ['console']}, + 'wsme': {'level': 'DEBUG', 'handlers': ['console']} + }, + 'handlers': { + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'simple' + } + }, + 'formatters': { + 'simple': { + 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' + '[%(threadName)s] %(message)s') + } + }, +} + +# Custom Configurations must be in Python dictionary format:: +# +# foo = {'bar':'baz'} +# +# All configurations are accessible at:: +# pecan.conf diff --git a/ironic/api/controllers/__init__.py b/ironic/api/controllers/__init__.py new file mode 100644 index 000000000..56425d0fc --- /dev/null +++ b/ironic/api/controllers/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. diff --git a/ironic/api/controllers/root.py b/ironic/api/controllers/root.py new file mode 100644 index 000000000..8df1f4fac --- /dev/null +++ b/ironic/api/controllers/root.py @@ -0,0 +1,31 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann <doug.hellmann@dreamhost.com> +# +# 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 pecan + +from ironic.api.controllers import v1 + + +class RootController(object): + + v1 = v1.Controller() + + @pecan.expose(generic=True) + def index(self): + # FIXME: GET / should return more than just '' + return '' diff --git a/ironic/api/controllers/v1.py b/ironic/api/controllers/v1.py new file mode 100644 index 000000000..b7f83c905 --- /dev/null +++ b/ironic/api/controllers/v1.py @@ -0,0 +1,165 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +""" +Version 1 of the Ironic API + +Should maintain feature parity with Nova Baremetal Extension. +Specification in ironic/doc/api/v1.rst +""" + + +import pecan +from pecan import rest + +import wsme +import wsmeext.pecan as wsme_pecan +from wsme import types as wtypes + +from ironic import db + + +class Base(wtypes.Base): + # TODO: all the db bindings + + @classmethod + def from_db_model(cls, m): + return cls(**(m.as_dict())) + + @classmethod + def from_db_and_links(cls, m, links): + return cls(links=links, **(m.as_dict())) + + def as_dict(self, db_model): + valid_keys = inspect.getargspec(db_model.__init__)[0] + if 'self' in valid_keys: + valid_keys.remove('self') + + return dict((k, getattr(self, k)) + for k in valid_keys + if hasattr(self, k) and + getattr(self, k) != wsme.Unset) + + +class Interface(Base): + """A representation of a network interface for a baremetal node""" + + node_id = int + address = wtypes.text + + def __init__(self, node_id=None, address=None): + self.node_id = node_id + self.address = address + + @classmethod + def sample(cls): + return cls(node_id=1, + address='52:54:00:cf:2d:31', + ) + + +class InterfacesController(rest.RestController): + """REST controller for Interfaces""" + + @wsme_pecan.wsexpose(Interface, unicode) + def post(self, iface): + """Ceate a new interface.""" + return Interface.sample() + + @wsme_pecan.wsexpose() + def get_all(self): + """Retrieve a list of all interfaces.""" + ifaces = [Interface.sample()] + return [(i.node_id, i.address) for i in ifaces] + + @wsme_pecan.wsexpose(Interface, unicode) + def get_one(self, address): + """Retrieve information about the given interface.""" + one = Interface.sample() + one.address = address + return one + + @wsme_pecan.wsexpose() + def delete(self, iface_id): + """Delete an interface""" + pass + + @wsme_pecan.wsexpose() + def put(self, iface_id): + """Update an interface""" + pass + + +class Node(Base): + """A representation of a bare metal node""" + + uuid = wtypes.text + cpus = int + memory_mb = int + + def __init__(self, uuid=None, cpus=None, memory_mb=None): + self.uuid = uuid + self.cpus = cpus + self.memory_mb = memory_mb + + @classmethod + def sample(cls): + return cls(uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', + cpus=2, + memory_mb=1024, + ) + + +class NodesController(rest.RestController): + """REST controller for Nodes""" + + @wsme_pecan.wsexpose(Node, unicode) + def post(self, node): + """Ceate a new node.""" + return Node.sample() + + @wsme_pecan.wsexpose() + def get_all(self): + """Retrieve a list of all nodes.""" + nodes = [Node.sample()] + return [n.uuid for n in nodes] + + @wsme_pecan.wsexpose(Node, unicode) + def get_one(self, node_id): + """Retrieve information about the given node.""" + one = Node.sample() + one.uuid = node_id + return one + + @wsme_pecan.wsexpose() + def delete(self, node_id): + """Delete a node""" + pass + + @wsme_pecan.wsexpose() + def put(self, node_id): + """Update a node""" + pass + + +class Controller(object): + """Version 1 API controller root.""" + + # TODO: _default and index + + nodes = NodesController() + interfaces = InterfacesController() diff --git a/ironic/api/hooks.py b/ironic/api/hooks.py new file mode 100644 index 000000000..6b066571e --- /dev/null +++ b/ironic/api/hooks.py @@ -0,0 +1,45 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann <doug.hellmann@dreamhost.com> +# +# 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 oslo.config import cfg +from pecan import hooks + +from ironic import db + + +class ConfigHook(hooks.PecanHook): + """Attach the configuration object to the request + so controllers can get to it. + """ + + def before(self, state): + state.request.cfg = cfg.CONF + + +class DBHook(hooks.PecanHook): + + def before(self, state): + # FIXME + storage_engine = storage.get_engine(state.request.cfg) + state.request.storage_engine = storage_engine + state.request.storage_conn = storage_engine.get_connection( + state.request.cfg) + + # def after(self, state): + # print 'method:', state.request.method + # print 'response:', state.response.status diff --git a/ironic/api/model/__init__.py b/ironic/api/model/__init__.py new file mode 100644 index 000000000..9579e31ea --- /dev/null +++ b/ironic/api/model/__init__.py @@ -0,0 +1,21 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# 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. + +from pecan import conf + +def init_model(): + pass diff --git a/ironic/api/templates/index.html b/ironic/api/templates/index.html new file mode 100644 index 000000000..48a5dae5b --- /dev/null +++ b/ironic/api/templates/index.html @@ -0,0 +1,12 @@ +<%def name="title()"> + Ironic API v1 +</%def> + +<header> +</header> + +<div id="content"> + + <p> TODO </p> + +</div> diff --git a/ironic/cmd/api.py b/ironic/cmd/api.py new file mode 100644 index 000000000..79e8cac34 --- /dev/null +++ b/ironic/cmd/api.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# 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. + +""" +The Ironic Service API +""" + +import sys + +from oslo.config import cfg +from wsgiref import simple_server + +from ironic.api import app +from ironic.common.service import prepare_service +from ironic.openstack.common import service +from ironic.openstack.common.rpc import service as rpc_service + +CONF = cfg.CONF + + +def main(): + # Pase config file and command line options, then start logging + prepare_service(sys.argv) + + # Build and start the WSGI app + host = CONF.ironic_api_bind_ip + port = CONF.ironic_api_port + wsgi = simple_server.make_server( + host, port, + app.VersionSelectorApplication()) + + print "Serving on http://%s:%s" % (host, port) + + try: + wsgi.serve_forever() + except KeyboardInterrupt: + pass diff --git a/ironic/cmd/manager.py b/ironic/cmd/manager.py new file mode 100644 index 000000000..cb53750ee --- /dev/null +++ b/ironic/cmd/manager.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# 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. + +""" +The Ironic Management Service +""" + +import sys + +from oslo.config import cfg +from wsgiref import simple_server + +from ironic.manager import manager +from ironic.common.service import prepare_service +from ironic.openstack.common import service +from ironic.openstack.common.rpc import service as rpc_service + +CONF = cfg.CONF + + +def main(): + # Pase config file and command line options, then start logging + prepare_service(sys.argv) + + mgr = manager.AgentManager() + topic = 'ironic.manager' + ironic = rcp_service.Service(CONF.host, topic, mgr) + launcher = service.launch(ironic) + launcher.wait() diff --git a/ironic/common/policy.py b/ironic/common/policy.py index ff0fdb0bf..0f3a141dc 100644 --- a/ironic/common/policy.py +++ b/ironic/common/policy.py @@ -92,7 +92,7 @@ def enforce(context, action, target, do_raise=True): """ init() - credentials = context.to_dict() + credentials = ironic_context.to_dict() # Add the exception arguments if asked to do a raise extra = {} @@ -102,17 +102,19 @@ def enforce(context, action, target, do_raise=True): return policy.check(action, target, credentials, **extra) -def check_is_admin(context): +def check_is_admin(roles): """Whether or not roles contains 'admin' role according to policy setting. """ init() - #the target is user-self - credentials = context.to_dict() - target = credentials - - return policy.check('context_is_admin', target, credentials) + if isinstance(roles, RequestContext): + # the target is user-self + credentials = roles.to_dict() + target = credentials + return policy.check('context_is_admin', target, credentials) + else: + return policy.check('context_is_admin', {}, {'roles': roles}) @policy.register('is_admin') diff --git a/ironic/common/service.py b/ironic/common/service.py new file mode 100644 index 000000000..44478f1af --- /dev/null +++ b/ironic/common/service.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 eNovance <licensing@enovance.com> +# +# Author: Julien Danjou <julien@danjou.info> +# +# 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 os +import socket + +from oslo.config import cfg + +from ironic.openstack.common import context +from ironic.openstack.common import log +from ironic.openstack.common import rpc +from ironic.openstack.common.rpc import service as rpc_service + + +cfg.CONF.register_opts([ + cfg.IntOpt('periodic_interval', + default=600, + help='seconds between running periodic tasks'), + cfg.StrOpt('host', + default=socket.getfqdn(), + help='Name of this node. This can be an opaque identifier. ' + 'It is not necessarily a hostname, FQDN, or IP address. ' + 'However, the node name must be valid within ' + 'an AMQP key, and if using ZeroMQ, a valid ' + 'hostname, FQDN, or IP address'), +]) + +CLI_OPTIONS = [ + cfg.StrOpt('os-username', + default=os.environ.get('OS_USERNAME', 'ironic'), + help='Username to use for openstack service access'), + cfg.StrOpt('os-password', + default=os.environ.get('OS_PASSWORD', 'admin'), + help='Password to use for openstack service access'), + cfg.StrOpt('os-tenant-id', + default=os.environ.get('OS_TENANT_ID', ''), + help='Tenant ID to use for openstack service access'), + cfg.StrOpt('os-tenant-name', + default=os.environ.get('OS_TENANT_NAME', 'admin'), + help='Tenant name to use for openstack service access'), + cfg.StrOpt('os-auth-url', + default=os.environ.get('OS_AUTH_URL', + 'http://localhost:5000/v2.0'), + help='Auth URL to use for openstack service access'), +] +cfg.CONF.register_cli_opts(CLI_OPTIONS) + + +class PeriodicService(rpc_service.Service): + + def start(self): + super(PeriodicService, self).start() + admin_context = context.RequestContext('admin', 'admin', is_admin=True) + self.tg.add_timer(cfg.CONF.periodic_interval, + self.manager.periodic_tasks, + context=admin_context) + + +def _sanitize_cmd_line(argv): + """Remove non-nova CLI options from argv.""" + cli_opt_names = ['--%s' % o.name for o in CLI_OPTIONS] + return [a for a in argv if a in cli_opt_names] + + +def prepare_service(argv=[]): + rpc.set_defaults(control_exchange='ironic') + cfg.set_defaults(log.log_opts, + default_log_levels=['amqplib=WARN', + 'qpid.messaging=INFO', + 'sqlalchemy=WARN', + 'keystoneclient=INFO', + 'stevedore=INFO', + 'eventlet.wsgi.server=WARN' + ]) + cfg.CONF(argv[1:], project='ironic') + log.setup('ironic') diff --git a/ironic/db/__init__.py b/ironic/db/__init__.py index 93994b1a6..d3a0cc399 100644 --- a/ironic/db/__init__.py +++ b/ironic/db/__init__.py @@ -1,6 +1,8 @@ -# Copyright (c) 2012 NTT DOCOMO, INC. +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. # All Rights Reserved. -# flake8: noqa +# # 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 diff --git a/ironic/db/api.py b/ironic/db/api.py index 120f0a71c..e4fc198e4 100644 --- a/ironic/db/api.py +++ b/ironic/db/api.py @@ -45,19 +45,12 @@ these objects be simple dictionaries. from oslo.config import cfg from ironic.common import utils +from ironic.openstack.common.db import api as db_api -db_opts = [ - cfg.StrOpt('db_backend', - default='sqlalchemy', - help='The backend to use for the ironic database'), - ] -CONF = cfg.CONF -CONF.register_opts(db_opts) +_BACKEND_MAPPING = {'sqlalchemy': 'ironic.db.sqlalchemy.api'} -IMPL = utils.LazyPluggable( - 'db_backend', - sqlalchemy='ironic.db.sqlalchemy.api') +IMPL = db_api.DBAPI(backend_mapping=_BACKEND_MAPPING) def bm_node_get_all(context, service_host=None): diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py index 34936720e..27dc20986 100644 --- a/ironic/db/sqlalchemy/api.py +++ b/ironic/db/sqlalchemy/api.py @@ -20,6 +20,7 @@ """Implementation of SQLAlchemy backend.""" +import sys import uuid from sqlalchemy.sql.expression import asc @@ -40,6 +41,11 @@ get_engine = db_session.get_engine get_session = db_session.get_session +def get_backend(): + """The backend is this module itself.""" + return sys.modules[__name__] + + def model_query(context, model, *args, **kwargs): """Query helper that accounts for context's `read_deleted` field. diff --git a/ironic/doc/api/v1.rst b/ironic/doc/api/v1.rst new file mode 100644 index 000000000..a33ca31cd --- /dev/null +++ b/ironic/doc/api/v1.rst @@ -0,0 +1,28 @@ +GET / - information about this API + +GET /node - list all nodes +GET /node/<id> - get node info +POST /node - insert a new node +DELETE /node/<id> - delete a node (and any associated interfaces) + +GET /iface - list all interfaces +GET /iface/<id> - information about the interface +GET /iface/node/<id> - list interfaces for this node +POST /iface - insert a new interface +DELETE /iface/<id> - delete an interface + +GET /node/image/<id> - get deployment driver info +PUT /node/image/<id> - set deployment driver info +GET /node/image/pxe/<id> - get PXE info +PUT /node/image/pxe/<id> - set PXE info + +GET /node/power/<id> - get power driver info +PUT /node/power/<id> - set power driver info +GET /node/power/ipmi/<id> - get IPMI info (sanitised pw) +PUT /node/power/ipmi/<id> - set IPMI info + +GET /node/power/state/<id> - get the power state +PUT /node/power/state/<id> - set the power state + +GET /find/node - search for node based on query string +GET /find/iface - search for iface based on query string diff --git a/ironic/openstack/common/rpc/__init__.py b/ironic/openstack/common/rpc/__init__.py index 2695f0729..adea0a596 100644 --- a/ironic/openstack/common/rpc/__init__.py +++ b/ironic/openstack/common/rpc/__init__.py @@ -297,5 +297,11 @@ def _get_impl(): """Delay import of rpc_backend until configuration is loaded.""" global _RPCIMPL if _RPCIMPL is None: - _RPCIMPL = importutils.import_module(CONF.rpc_backend) + try: + _RPCIMPL = importutils.import_module(CONF.rpc_backend) + except ImportError: + # For backwards compatibility with older nova config. + impl = CONF.rpc_backend.replace('nova.rpc', + 'nova.openstack.common.rpc') + _RPCIMPL = importutils.import_module(impl) return _RPCIMPL diff --git a/openstack-common.conf b/openstack-common.conf index b6bedf74d..729f6de06 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -1,6 +1,4 @@ [DEFAULT] - -# The list of modules to copy from oslo-incubator.git module=cliutils module=context module=db diff --git a/requirements.txt b/requirements.txt index 2b313584b..30e70e288 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ pyasn1 Babel>=0.9.6 iso8601>=0.1.4 httplib2 +setuptools_git>=0.4 python-cinderclient>=1.0.1 python-quantumclient>=2.2.0,<3.0.0 python-glanceclient>=0.5.0,<2 @@ -29,3 +30,6 @@ python-keystoneclient>=0.2.0 stevedore>=0.7 websockify<0.4 oslo.config>=1.1.0 +Flask==0.9 +pecan>=0.2.0 +wsme>=0.5b1 @@ -28,8 +28,8 @@ packages = [entry_points] console_scripts = - ironic-baremetal-deploy-helper = ironic.cmd.baremetal_deploy_helper:main - ironic-baremetal-manage = ironic.cmd.baremetal_manage:main + ironic-api = ironic.cmd.api:main + ironic-manager = ironic.cmd.manager:main [build_sphinx] all_files = 1 diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py index 0401a958f..914fcf17e 100644 --- a/tools/install_venv_common.py +++ b/tools/install_venv_common.py @@ -24,6 +24,8 @@ 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 @@ -42,7 +44,7 @@ class InstallVenv(object): self.project = project def die(self, message, *args): - print >> sys.stderr, message % args + print(message % args, file=sys.stderr) sys.exit(1) def check_python_version(self): @@ -89,20 +91,20 @@ class InstallVenv(object): virtual environment. """ if not os.path.isdir(self.venv): - print 'Creating 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.' - print 'Installing pip in venv...', + print('done.') + print('Installing pip in venv...', end=' ') if not self.run_command(['tools/with_venv.sh', 'easy_install', 'pip>1.0']).strip(): self.die("Failed to install pip.") - print 'done.' + print('done.') else: - print "venv already exists..." + print("venv already exists...") pass def pip_install(self, *args): @@ -111,7 +113,7 @@ class InstallVenv(object): redirect_output=False) def install_dependencies(self): - print 'Installing dependencies with pip (this can take a while)...' + print('Installing dependencies with pip (this can take a while)...') # First things first, make sure our venv has the latest pip and # distribute. @@ -153,12 +155,12 @@ class Distro(InstallVenv): return if self.check_cmd('easy_install'): - print 'Installing virtualenv via easy_install...', + print('Installing virtualenv via easy_install...', end=' ') if self.run_command(['easy_install', 'virtualenv']): - print 'Succeeded' + print('Succeeded') return else: - print 'Failed' + print('Failed') self.die('ERROR: virtualenv not found.\n\n%s development' ' requires virtualenv, please install it using your' diff --git a/tools/pip-requires b/tools/pip-requires index 05a103ee9..30e70e288 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,3 +1,5 @@ +d2to1>=0.2.10,<0.3 +pbr>=0.5,<0.6 SQLAlchemy>=0.7.8,<0.7.99 Cheetah>=2.4.4 amqplib>=0.6.1 @@ -28,3 +30,6 @@ python-keystoneclient>=0.2.0 stevedore>=0.7 websockify<0.4 oslo.config>=1.1.0 +Flask==0.9 +pecan>=0.2.0 +wsme>=0.5b1 |