summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDevananda van der Veen <devananda.vdv@gmail.com>2013-05-06 20:07:49 -0700
committerDevananda van der Veen <devananda.vdv@gmail.com>2013-05-13 00:57:31 -0700
commit8ac57c720c873f9c64e1ac94c8748fce48bbca54 (patch)
treef47ab7824ccf365ee99e0289eadf0819874d7a02
parent73f81e029f02a050f4869c7d13ca3a2913ed8355 (diff)
downloadironic-8ac57c720c873f9c64e1ac94c8748fce48bbca54.tar.gz
Implement initial draft of a Pecan-based API.
-rw-r--r--.gitignore6
-rw-r--r--ironic/api/__init__.py37
-rw-r--r--ironic/api/acl.py56
-rw-r--r--ironic/api/app.py81
-rw-r--r--ironic/api/config.py43
-rw-r--r--ironic/api/controllers/__init__.py16
-rw-r--r--ironic/api/controllers/root.py31
-rw-r--r--ironic/api/controllers/v1.py165
-rw-r--r--ironic/api/hooks.py45
-rw-r--r--ironic/api/model/__init__.py21
-rw-r--r--ironic/api/templates/index.html12
-rw-r--r--ironic/cmd/api.py54
-rw-r--r--ironic/cmd/manager.py46
-rw-r--r--ironic/common/policy.py16
-rw-r--r--ironic/common/service.py92
-rw-r--r--ironic/db/__init__.py6
-rw-r--r--ironic/db/api.py13
-rw-r--r--ironic/db/sqlalchemy/api.py6
-rw-r--r--ironic/doc/api/v1.rst28
-rw-r--r--ironic/openstack/common/rpc/__init__.py8
-rw-r--r--openstack-common.conf2
-rw-r--r--requirements.txt4
-rw-r--r--setup.cfg4
-rw-r--r--tools/install_venv_common.py22
-rw-r--r--tools/pip-requires5
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
diff --git a/setup.cfg b/setup.cfg
index 0fae6463f..c63639310 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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