diff --git a/etc/iotronic/policy.json b/etc/iotronic/policy.json
index f772677..2c63c08 100644
--- a/etc/iotronic/policy.json
+++ b/etc/iotronic/policy.json
@@ -1,5 +1,2 @@
{
- "admin_api": "role:admin or role:administrator",
- "show_password": "!",
- "default": "rule:admin_api"
}
diff --git a/iotronic/api/__init__.py b/iotronic/api/__init__.py
index 46a025c..e69de29 100644
--- a/iotronic/api/__init__.py
+++ b/iotronic/api/__init__.py
@@ -1,38 +0,0 @@
-# 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 oslo_config import cfg
-
-
-API_SERVICE_OPTS = [
- cfg.StrOpt('host_ip',
- default='0.0.0.0',
- help='The IP address on which iotronic-api listens.'),
- cfg.IntOpt('port',
- default=1288,
- help='The TCP port on which iotronic-api listens.'),
- cfg.IntOpt('max_limit',
- default=1000,
- help='The maximum number of items returned in a single '
- 'response from a collection resource.'),
-]
-
-
-CONF = cfg.CONF
-
-opt_group = cfg.OptGroup(name='api',
- title='Options for the iotronic-api service')
-CONF.register_group(opt_group)
-CONF.register_opts(API_SERVICE_OPTS, opt_group)
diff --git a/iotronic/api/acl.py b/iotronic/api/acl.py
deleted file mode 100644
index 3d8c841..0000000
--- a/iotronic/api/acl.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# -*- encoding: utf-8 -*-
-#
-# Copyright © 2012 New Dream Network, LLC (DreamHost)
-#
-# 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 iotronic.api.middleware import auth_token
-
-
-def install(app, conf, public_routes):
- """Install ACL check on application.
-
- :param app: A WSGI applicatin.
- :param conf: Settings. Dict'ified and passed to keystonemiddleware
- :param public_routes: The list of the routes which will be allowed to
- access without authentication.
- :return: The same WSGI application with ACL installed.
-
- """
- return auth_token.AuthTokenMiddleware(app,
- conf=dict(conf),
- public_api_routes=public_routes)
diff --git a/iotronic/api/app.py b/iotronic/api/app.py
index 44b024c..76a7e69 100644
--- a/iotronic/api/app.py
+++ b/iotronic/api/app.py
@@ -15,22 +15,30 @@
# License for the specific language governing permissions and limitations
# under the License.
-from oslo_config import cfg
-import pecan
-from iotronic.api import acl
from iotronic.api import config
+from iotronic.api.controllers import base
from iotronic.api import hooks
from iotronic.api import middleware
+from iotronic.api.middleware import auth_token
+from oslo_config import cfg
+import oslo_middleware.cors as cors_middleware
+import pecan
+from pecan import make_app
-api_opts = [
+opts = [
cfg.StrOpt(
'auth_strategy',
default='keystone',
- help='Authentication strategy used by iotronic-api: one of "keystone" '
- 'or "noauth". "noauth" should not be used in a production '
- 'environment because all authentication will be disabled.'),
+ help=('Authentication strategy used by iotronic-api: "keystone" '
+ 'or "noauth". "noauth" should not be used in a production '
+ 'environment because all authentication will be disabled.')),
+ cfg.BoolOpt('debug_tracebacks_in_api',
+ default=False,
+ help=('Return server tracebacks in the API response for any '
+ 'error responses. WARNING: this is insecure '
+ 'and should not be used in a production environment.')),
cfg.BoolOpt(
'pecan_debug',
default=False,
@@ -39,8 +47,46 @@ api_opts = [
'and should not be used in a production environment.')),
]
+api_opts = [
+ cfg.StrOpt('host_ip',
+ default='0.0.0.0',
+ help=('The IP address on which iotronic-api listens.')),
+ cfg.PortOpt('port',
+ default=1288,
+ help=('The TCP port on which iotronic-api listens.')),
+ cfg.IntOpt('max_limit',
+ default=1000,
+ help=('The maximum number of items returned in a single '
+ 'response from a collection resource.')),
+ cfg.StrOpt('public_endpoint',
+ help=("Public URL to use when building the links to the API "
+ "resources."
+ " If None the links will be built using the request's "
+ "host URL. If the API is operating behind a proxy, you "
+ "will want to change this to represent the proxy's URL. "
+ "Defaults to None.")),
+ cfg.IntOpt('api_workers',
+ help=('Number of workers for OpenStack Iotronic API service. '
+ 'The default is equal to the number of CPUs available '
+ 'if that can be determined, else a default worker '
+ 'count of 1 is returned.')),
+ cfg.BoolOpt('enable_ssl_api',
+ default=False,
+ help=("Enable the integrated stand-alone API to service "
+ "requests via HTTPS instead of HTTP. If there is a "
+ "front-end service performing HTTPS offloading from "
+ "the service, this option should be False; note, you "
+ "will want to change public API endpoint to represent "
+ "SSL termination URL with 'public_endpoint' option.")),
+]
+
+opt_group = cfg.OptGroup(name='api',
+ title='Options for the iotronic-api service')
+
+
CONF = cfg.CONF
-CONF.register_opts(api_opts)
+CONF.register_opts(opts,)
+CONF.register_opts(api_opts, 'api')
def get_pecan_config():
@@ -49,34 +95,40 @@ def get_pecan_config():
return pecan.configuration.conf_from_file(filename)
-def setup_app(pecan_config=None, extra_hooks=None):
+def setup_app(config=None):
+
app_hooks = [hooks.ConfigHook(),
hooks.DBHook(),
- hooks.ContextHook(pecan_config.app.acl_public_routes),
+ hooks.ContextHook(config.app.acl_public_routes),
hooks.RPCHook(),
- hooks.NoExceptionTracebackHook()]
- if extra_hooks:
- app_hooks.extend(extra_hooks)
+ hooks.NoExceptionTracebackHook(),
+ hooks.PublicUrlHook()]
- if not pecan_config:
- pecan_config = get_pecan_config()
+ app_conf = dict(config.app)
- if pecan_config.app.enable_acl:
- app_hooks.append(hooks.TrustedCallHook())
-
- pecan.configuration.set_config(dict(pecan_config), overwrite=True)
-
- app = pecan.make_app(
- pecan_config.app.root,
- static_root=pecan_config.app.static_root,
- debug=CONF.pecan_debug,
- force_canonical=getattr(pecan_config.app, 'force_canonical', True),
+ app = make_app(
+ app_conf.pop('root'),
hooks=app_hooks,
+ force_canonical=getattr(config.app, 'force_canonical', True),
wrap_app=middleware.ParsableErrorMiddleware,
+ **app_conf
)
- if pecan_config.app.enable_acl:
- return acl.install(app, cfg.CONF, pecan_config.app.acl_public_routes)
+ if CONF.auth_strategy == "keystone":
+ app = auth_token.AuthTokenMiddleware(
+ app, dict(cfg.CONF),
+ public_api_routes=config.app.acl_public_routes)
+
+ # Create a CORS wrapper, and attach iotronic-specific defaults that must be
+ # included in all CORS responses.
+ app = cors_middleware.CORS(app, CONF)
+ app.set_latent(
+ allow_headers=[base.Version.max_string, base.Version.min_string,
+ base.Version.string],
+ allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
+ expose_headers=[base.Version.max_string, base.Version.min_string,
+ base.Version.string]
+ )
return app
@@ -86,7 +138,7 @@ class VersionSelectorApplication(object):
def __init__(self):
pc = get_pecan_config()
pc.app.enable_acl = (CONF.auth_strategy == 'keystone')
- self.v1 = setup_app(pecan_config=pc)
+ self.v1 = setup_app(config=pc)
def __call__(self, environ, start_response):
return self.v1(environ, start_response)
diff --git a/iotronic/api/config.py b/iotronic/api/config.py
index c1f8966..82f2222 100644
--- a/iotronic/api/config.py
+++ b/iotronic/api/config.py
@@ -25,18 +25,9 @@ app = {
'root': 'iotronic.api.controllers.root.RootController',
'modules': ['iotronic.api'],
'static_root': '%(confdir)s/public',
- 'debug': True,
- 'enable_acl': True,
+ 'debug': False,
'acl_public_routes': [
'/',
'/v1',
- '/v1/nodes/[a-z0-9\-]',
- '/v1/plugins/[a-z0-9\-]',
],
}
-
-# WSME Configurations
-# See https://wsme.readthedocs.org/en/latest/integrate.html#configuration
-wsme = {
- 'debug': False,
-}
diff --git a/iotronic/api/controllers/base.py b/iotronic/api/controllers/base.py
index c8f2137..69c0625 100644
--- a/iotronic/api/controllers/base.py
+++ b/iotronic/api/controllers/base.py
@@ -13,6 +13,7 @@
# under the License.
import datetime
+import functools
from webob import exc
import wsme
@@ -50,6 +51,7 @@ class APIBase(wtypes.Base):
setattr(self, k, wsme.Unset)
+@functools.total_ordering
class Version(object):
"""API Version object."""
@@ -83,7 +85,7 @@ class Version(object):
:param headers: webob headers
:param default_version: version to use if not specified in headers
:param latest_version: version to use if latest is requested
- :returns: a tupe of (major, minor) version numbers
+ :returns: a tuple of (major, minor) version numbers
:raises: webob.HTTPNotAcceptable
"""
version_str = headers.get(Version.string, default_version)
@@ -103,12 +105,11 @@ class Version(object):
"Invalid value for %s header") % Version.string)
return version
- def __lt__(a, b):
- if (a.major == b.major and a.minor < b.minor):
- return True
- return False
+ def __gt__(self, other):
+ return (self.major, self.minor) > (other.major, other.minor)
- def __gt__(a, b):
- if (a.major == b.major and a.minor > b.minor):
- return True
- return False
+ def __eq__(self, other):
+ return (self.major, self.minor) == (other.major, other.minor)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
diff --git a/iotronic/api/controllers/link.py b/iotronic/api/controllers/link.py
index 861da3b..0e26503 100644
--- a/iotronic/api/controllers/link.py
+++ b/iotronic/api/controllers/link.py
@@ -21,13 +21,13 @@ from iotronic.api.controllers import base
def build_url(resource, resource_args, bookmark=False, base_url=None):
if base_url is None:
- base_url = pecan.request.host_url
+ base_url = pecan.request.public_url
template = '%(url)s/%(res)s' if bookmark else '%(url)s/v1/%(res)s'
# FIXME(lucasagomes): I'm getting a 404 when doing a GET on
# a nested resource that the URL ends with a '/'.
# https://groups.google.com/forum/#!topic/pecan-dev/QfSeviLg5qs
- template += '%(args)s' if resource_args.startswith('?') else '/%(args)s'
+ # template += '%(args)s' if resource_args.startswith('?') else '/%(args)s'
return template % {'url': base_url, 'res': resource, 'args': resource_args}
@@ -49,10 +49,3 @@ class Link(base.APIBase):
href = build_url(resource, resource_args,
bookmark=bookmark, base_url=url)
return Link(href=href, rel=rel_name, type=type)
-
- @classmethod
- def sample(cls):
- sample = cls(href="http://localhost:1288/node/"
- "eaaca217-e7d8-47b4-bb41-3f99f20eed89",
- rel="bookmark")
- return sample
diff --git a/iotronic/api/controllers/root.py b/iotronic/api/controllers/root.py
index 725f453..8b8ef8c 100644
--- a/iotronic/api/controllers/root.py
+++ b/iotronic/api/controllers/root.py
@@ -14,36 +14,55 @@
# License for the specific language governing permissions and limitations
# under the License.
+from iotronic.api.controllers import base
+from iotronic.api.controllers import link
+from iotronic.api.controllers import v1
+from iotronic.api.controllers.v1 import versions
+from iotronic.api import expose
import pecan
from pecan import rest
from wsme import types as wtypes
-from iotronic.api.controllers import base
-from iotronic.api.controllers import link
-from iotronic.api.controllers import v1
-from iotronic.api import expose
+ID_VERSION1 = 'v1'
class Version(base.APIBase):
- """An API version representation."""
+ """An API version representation.
+
+ This class represents an API version, including the minimum and
+ maximum minor versions that are supported within the major version.
+ """
id = wtypes.text
- """The ID of the version, also acts as the release number"""
+ """The ID of the (major) version, also acts as the release number"""
links = [link.Link]
"""A Link that point to a specific version of the API"""
- @staticmethod
- def convert(id):
- version = Version()
- version.id = id
- version.links = [link.Link.make_link('self', pecan.request.host_url,
- id, '', bookmark=True)]
- return version
+ status = wtypes.text
+ """Status of the version.
+ One of:
+ * CURRENT - the latest version of API,
+ * SUPPORTED - supported, but not latest, version of API,
+ * DEPRECATED - supported, but deprecated, version of API.
+ """
+
+ version = wtypes.text
+ """The current, maximum supported (major.minor) version of API."""
+
+ min_version = wtypes.text
+ """Minimum supported (major.minor) version of API."""
+
+ def __init__(self, id, min_version, version, status='CURRENT'):
+ self.id = id
+ self.links = [link.Link.make_link('self', pecan.request.public_url,
+ self.id, '', bookmark=True)]
+ self.status = status
+ self.version = version
+ self.min_version = min_version
class Root(base.APIBase):
-
name = wtypes.text
"""The name of the API"""
@@ -60,19 +79,20 @@ class Root(base.APIBase):
def convert():
root = Root()
root.name = "OpenStack Iotronic API"
- root.description = ("IoTronic is an Internet of Things resource \
- management service for OpenStack clouds.")
- root.versions = [Version.convert('v1')]
- root.default_version = Version.convert('v1')
+ root.description = ("Iotronic is an OpenStack project which aims to "
+ "provision baremetal machines.")
+ root.default_version = Version(ID_VERSION1,
+ versions.MIN_VERSION_STRING,
+ versions.MAX_VERSION_STRING)
+ root.versions = [root.default_version]
return root
class RootController(rest.RestController):
-
- _versions = ['v1']
+ _versions = [ID_VERSION1]
"""All supported API versions"""
- _default_version = 'v1'
+ _default_version = ID_VERSION1
"""The default API version"""
v1 = v1.Controller()
@@ -86,11 +106,9 @@ class RootController(rest.RestController):
@pecan.expose()
def _route(self, args):
- """Overrides the default routing behavior.
-
- It redirects the request to the default version of the iotronic API
- if the version number is not specified in the url.
- """
+ # Overrides the default routing behavior.
+ # It redirects the request to the default version of the ironic API
+ # if the version number is not specified in the url.
if args[0] and args[0] not in self._versions:
args = [self._default_version] + args
diff --git a/iotronic/api/controllers/v1/__init__.py b/iotronic/api/controllers/v1/__init__.py
index 40e7917..3f92102 100644
--- a/iotronic/api/controllers/v1/__init__.py
+++ b/iotronic/api/controllers/v1/__init__.py
@@ -14,31 +14,38 @@
"""
Version 1 of the Iotronic API
+
+Specification can be found at doc/source/webapi/v1.rst
"""
-from iotronic.api.controllers import base
-from iotronic.api.controllers import link
-from iotronic.api.controllers.v1 import node
-from iotronic.api.controllers.v1 import plugin
-from iotronic.api import expose
-from iotronic.common.i18n import _
import pecan
from pecan import rest
from webob import exc
from wsme import types as wtypes
+from iotronic.api.controllers import base
+from iotronic.api.controllers import link
+from iotronic.api.controllers.v1 import plugin
+# from iotronic.api.controllers.v1 import driver
+# from iotronic.api.controllers.v1 import port
+# from iotronic.api.controllers.v1 import portgroup
+# from iotronic.api.controllers.v1 import ramdisk
+# from iotronic.api.controllers.v1 import utils
-BASE_VERSION = 1
+from iotronic.api.controllers.v1 import node
-MIN_VER_STR = '1.0'
+from iotronic.api.controllers.v1 import versions
+from iotronic.api import expose
+from iotronic.common.i18n import _
-MAX_VER_STR = '1.0'
+BASE_VERSION = versions.BASE_VERSION
-
-MIN_VER = base.Version({base.Version.string: MIN_VER_STR},
- MIN_VER_STR, MAX_VER_STR)
-MAX_VER = base.Version({base.Version.string: MAX_VER_STR},
- MIN_VER_STR, MAX_VER_STR)
+MIN_VER = base.Version(
+ {base.Version.string: versions.MIN_VERSION_STRING},
+ versions.MIN_VERSION_STRING, versions.MAX_VERSION_STRING)
+MAX_VER = base.Version(
+ {base.Version.string: versions.MAX_VERSION_STRING},
+ versions.MIN_VERSION_STRING, versions.MAX_VERSION_STRING)
class V1(base.APIBase):
@@ -53,31 +60,11 @@ class V1(base.APIBase):
nodes = [link.Link]
"""Links to the nodes resource"""
- plugins = [link.Link]
-
@staticmethod
def convert():
v1 = V1()
v1.id = "v1"
-
- v1.nodes = [link.Link.make_link('self', pecan.request.host_url,
- 'nodes', ''),
- link.Link.make_link('bookmark',
- pecan.request.host_url,
- 'nodes', '',
- bookmark=True)
- ]
-
- v1.plugins = [link.Link.make_link('self', pecan.request.host_url,
- 'plugins', ''),
- link.Link.make_link('bookmark',
- pecan.request.host_url,
- 'plugins', '',
- bookmark=True)
- ]
-
- '''
- v1.links = [link.Link.make_link('self', pecan.request.host_url,
+ v1.links = [link.Link.make_link('self', pecan.request.public_url,
'v1', '', bookmark=True),
link.Link.make_link('describedby',
'http://docs.openstack.org',
@@ -85,7 +72,23 @@ class V1(base.APIBase):
'api-spec-v1.html',
bookmark=True, type='text/html')
]
- '''
+
+ v1.plugins = [link.Link.make_link('self', pecan.request.public_url,
+ 'plugins', ''),
+ link.Link.make_link('bookmark',
+ pecan.request.public_url,
+ 'plugins', '',
+ bookmark=True)
+ ]
+
+ v1.nodes = [link.Link.make_link('self', pecan.request.public_url,
+ 'nodes', ''),
+ link.Link.make_link('bookmark',
+ pecan.request.public_url,
+ 'nodes', '',
+ bookmark=True)
+ ]
+
return v1
@@ -110,25 +113,30 @@ class Controller(rest.RestController):
raise exc.HTTPNotAcceptable(_(
"Mutually exclusive versions requested. Version %(ver)s "
"requested but not supported by this service. The supported "
- "version range is: [%(min)s,%(max)s]."
- ) % {'ver': version, 'min': MIN_VER_STR,
- 'max': MAX_VER_STR},
+ "version range is: [%(min)s, %(max)s].") %
+ {'ver': version, 'min': versions.MIN_VERSION_STRING,
+ 'max': versions.MAX_VERSION_STRING},
headers=headers)
# ensure the minor version is within the supported range
if version < MIN_VER or version > MAX_VER:
raise exc.HTTPNotAcceptable(_(
"Version %(ver)s was requested but the minor version is not "
"supported by this service. The supported version range is: "
- "[%(min)s, %(max)s].") % {'ver': version, 'min': MIN_VER_STR,
- 'max': MAX_VER_STR}, headers=headers)
+ "[%(min)s, %(max)s].") %
+ {'ver': version, 'min': versions.MIN_VERSION_STRING,
+ 'max': versions.MAX_VERSION_STRING},
+ headers=headers)
@pecan.expose()
def _route(self, args):
- v = base.Version(pecan.request.headers, MIN_VER_STR, MAX_VER_STR)
+ v = base.Version(pecan.request.headers, versions.MIN_VERSION_STRING,
+ versions.MAX_VERSION_STRING)
# Always set the min and max headers
- pecan.response.headers[base.Version.min_string] = MIN_VER_STR
- pecan.response.headers[base.Version.max_string] = MAX_VER_STR
+ pecan.response.headers[base.Version.min_string] = (
+ versions.MIN_VERSION_STRING)
+ pecan.response.headers[base.Version.max_string] = (
+ versions.MAX_VERSION_STRING)
# assert that requested version is supported
self._check_version(v, pecan.response.headers)
@@ -138,4 +146,4 @@ class Controller(rest.RestController):
return super(Controller, self)._route(args)
-__all__ = (Controller)
+__all__ = ('Controller',)
diff --git a/iotronic/api/controllers/v1/collection.py b/iotronic/api/controllers/v1/collection.py
index 747af6e..bec95bd 100644
--- a/iotronic/api/controllers/v1/collection.py
+++ b/iotronic/api/controllers/v1/collection.py
@@ -44,5 +44,5 @@ class Collection(base.APIBase):
'args': q_args, 'limit': limit,
'marker': self.collection[-1].uuid}
- return link.Link.make_link('next', pecan.request.host_url,
+ return link.Link.make_link('next', pecan.request.public_url,
resource_url, next_args).href
diff --git a/iotronic/api/controllers/v1/node.py b/iotronic/api/controllers/v1/node.py
index 6b7421f..ce7ad10 100644
--- a/iotronic/api/controllers/v1/node.py
+++ b/iotronic/api/controllers/v1/node.py
@@ -12,63 +12,39 @@
from iotronic.api.controllers import base
+from iotronic.api.controllers import link
from iotronic.api.controllers.v1 import collection
from iotronic.api.controllers.v1 import location as loc
from iotronic.api.controllers.v1 import types
from iotronic.api.controllers.v1 import utils as api_utils
from iotronic.api import expose
from iotronic.common import exception
+from iotronic.common import policy
from iotronic import objects
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
+_DEFAULT_RETURN_FIELDS = ('name', 'code', 'status', 'uuid', 'session', 'type')
+
class Node(base.APIBase):
"""API representation of a node.
"""
-
uuid = types.uuid
code = wsme.wsattr(wtypes.text)
status = wsme.wsattr(wtypes.text)
name = wsme.wsattr(wtypes.text)
type = wsme.wsattr(wtypes.text)
+ owner = types.uuid
session = wsme.wsattr(wtypes.text)
+ project = types.uuid
mobile = types.boolean
location = wsme.wsattr([loc.Location])
extra = types.jsontype
- @staticmethod
- def _convert_with_locates(node, url, expand=True, show_password=True):
-
- try:
- session = objects.SessionWP(
- {}).get_session_by_node_uuid(
- node.uuid, valid=True)
- node.session = session.session_id
- except Exception:
- pass
-
- if not expand:
- except_list = ['name', 'code', 'status', 'uuid', 'session', 'type']
- node.unset_fields_except(except_list)
- return node
-
- list_loc = objects.Location({}).list_by_node_id({}, node.id)
- node.location = loc.Location.convert_with_list(list_loc)
-
- return node
-
- @classmethod
- def convert_with_locates(cls, rpc_node, expand=True):
- node = Node(**rpc_node.as_dict())
- node.id = rpc_node.id
- return cls._convert_with_locates(node, pecan.request.host_url,
- expand,
- pecan.request.context.show_password)
-
def __init__(self, **kwargs):
self.fields = []
fields = list(objects.Node.fields)
@@ -79,6 +55,46 @@ class Node(base.APIBase):
self.fields.append(k)
setattr(self, k, kwargs.get(k, wtypes.Unset))
+ @staticmethod
+ def _convert_with_links(node, url, fields=None):
+ node_uuid = node.uuid
+ if fields is not None:
+ node.unset_fields_except(fields)
+
+ node.links = [link.Link.make_link('self', url, 'nodes',
+ node_uuid),
+ link.Link.make_link('bookmark', url, 'nodes',
+ node_uuid, bookmark=True)
+ ]
+ return node
+
+ @classmethod
+ def convert_with_links(cls, rpc_node, fields=None):
+ node = Node(**rpc_node.as_dict())
+
+ try:
+ session = objects.SessionWP.get_session_by_node_uuid(
+ pecan.request.context, node.uuid)
+ node.session = session.session_id
+ except Exception:
+ node.session = None
+
+ try:
+ list_loc = objects.Location.list_by_node_uuid(
+ pecan.request.context, node.uuid)
+ node.location = loc.Location.convert_with_list(list_loc)
+ except Exception:
+ node.location = []
+
+ # to enable as soon as a better session and location management
+ # is implemented
+ # if fields is not None:
+ # api_utils.check_for_invalid_fields(fields, node_dict)
+
+ return cls._convert_with_links(node,
+ pecan.request.public_url,
+ fields=fields)
+
class NodeCollection(collection.Collection):
"""API representation of a collection of nodes."""
@@ -90,20 +106,23 @@ class NodeCollection(collection.Collection):
self._type = 'nodes'
@staticmethod
- def convert_with_locates(nodes, limit, url=None, expand=False, **kwargs):
+ def convert_with_links(nodes, limit, url=None, fields=None, **kwargs):
collection = NodeCollection()
- collection.nodes = [
- Node.convert_with_locates(
- n, expand) for n in nodes]
+ collection.nodes = [Node.convert_with_links(n, fields=fields)
+ for n in nodes]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
class NodesController(rest.RestController):
- invalid_sort_key_list = ['properties']
+ """REST controller for Nodes."""
- def _get_nodes_collection(self, marker, limit, sort_key, sort_dir,
- expand=False, resource_url=None):
+ invalid_sort_key_list = ['extra', 'location']
+
+ def _get_nodes_collection(self, marker, limit,
+ sort_key, sort_dir,
+ project=None,
+ resource_url=None, fields=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
@@ -115,74 +134,71 @@ class NodesController(rest.RestController):
if sort_key in self.invalid_sort_key_list:
raise exception.InvalidParameterValue(
- _("The sort_key value %(key)s is an invalid field for "
- "sorting") % {'key': sort_key})
+ ("The sort_key value %(key)s is an invalid field for "
+ "sorting") % {'key': sort_key})
filters = {}
+
+ # bounding the request to a project
+ if project and pecan.request.context.is_admin:
+ filters['project_id'] = project
+ else:
+ filters['project_id'] = pecan.request.context.project_id
+
nodes = objects.Node.list(pecan.request.context, limit, marker_obj,
sort_key=sort_key, sort_dir=sort_dir,
filters=filters)
parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
- return NodeCollection.convert_with_locates(nodes, limit,
- url=resource_url,
- expand=expand,
- **parameters)
- @expose.expose(NodeCollection, types.uuid, int, wtypes.text, wtypes.text)
- def get_all(self, marker=None, limit=None, sort_key='id',
- sort_dir='asc'):
+ return NodeCollection.convert_with_links(nodes, limit,
+ url=resource_url,
+ fields=fields,
+ **parameters)
+
+ @expose.expose(Node, types.uuid_or_name, types.listtype)
+ def get_one(self, node_ident, fields=None):
+ """Retrieve information about the given node.
+
+ :param node_ident: UUID or logical name of a node.
+ :param fields: Optional, a list with a specified set of fields
+ of the resource to be returned.
+ """
+
+ cdict = pecan.request.context.to_policy_values()
+ policy.authorize('iot:node:get', cdict, cdict)
+
+ rpc_node = api_utils.get_rpc_node(node_ident)
+
+ return Node.convert_with_links(rpc_node, fields=fields)
+
+ @expose.expose(NodeCollection, types.uuid, int, wtypes.text,
+ wtypes.text, types.listtype, wtypes.text)
+ def get_all(self, marker=None,
+ limit=None, sort_key='id', sort_dir='asc',
+ fields=None, project=None):
"""Retrieve a list of nodes.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
+ This value cannot be larger than the value of max_limit
+ in the [api] section of the ironic configuration, or only
+ max_limit resources will be returned.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
+ :param project: Optional string value to get only nodes of the project.
+ :param fields: Optional, a list with a specified set of fields
+ of the resource to be returned.
"""
+ cdict = pecan.request.context.to_policy_values()
+ policy.authorize('iot:node:get', cdict, cdict)
+
+ if fields is None:
+ fields = _DEFAULT_RETURN_FIELDS
return self._get_nodes_collection(marker,
- limit, sort_key, sort_dir)
-
- @expose.expose(Node, types.uuid_or_name)
- def get(self, node_ident):
- """Retrieve information about the given node.
-
- :param node_ident: UUID or logical name of a node.
- """
- rpc_node = api_utils.get_rpc_node(node_ident)
- node = Node(**rpc_node.as_dict())
- node.id = rpc_node.id
- return Node.convert_with_locates(node)
-
- @expose.expose(None, types.uuid_or_name, status_code=204)
- def delete(self, node_ident):
- """Delete a node.
-
- :param node_ident: UUID or logical name of a node.
- """
- rpc_node = api_utils.get_rpc_node(node_ident)
- pecan.request.rpcapi.destroy_node(pecan.request.context,
- rpc_node.uuid)
-
- @expose.expose(Node, types.uuid_or_name, body=Node, status_code=200)
- def patch(self, node_ident, val_Node):
- """Update a node.
-
- :param node_ident: UUID or logical name of a node.
- :param Node: values to be changed
- :return updated_node: updated_node
- """
-
- node = api_utils.get_rpc_node(node_ident)
- val_Node = val_Node.as_dict()
- for key in val_Node:
- try:
- node[key] = val_Node[key]
- except Exception:
- pass
-
- updated_node = pecan.request.rpcapi.update_node(pecan.request.context,
- node)
- return Node.convert_with_locates(updated_node)
+ limit, sort_key, sort_dir,
+ project=project,
+ fields=fields)
@expose.expose(Node, body=Node, status_code=201)
def post(self, Node):
@@ -190,6 +206,10 @@ class NodesController(rest.RestController):
:param Node: a Node within the request body.
"""
+ context = pecan.request.context
+ cdict = context.to_policy_values()
+ policy.authorize('iot:node:create', cdict, cdict)
+
if not Node.name:
raise exception.MissingParameterValue(
("Name is not specified."))
@@ -209,10 +229,52 @@ class NodesController(rest.RestController):
new_Node = objects.Node(pecan.request.context,
**Node.as_dict())
+ new_Node.owner = pecan.request.context.user_id
+ new_Node.project = pecan.request.context.project_id
+
new_Location = objects.Location(pecan.request.context,
**Node.location[0].as_dict())
new_Node = pecan.request.rpcapi.create_node(pecan.request.context,
new_Node, new_Location)
- return Node.convert_with_locates(new_Node)
+ return Node.convert_with_links(new_Node)
+
+ @expose.expose(None, types.uuid_or_name, status_code=204)
+ def delete(self, node_ident):
+ """Delete a node.
+
+ :param node_ident: UUID or logical name of a node.
+ """
+ context = pecan.request.context
+ cdict = context.to_policy_values()
+ policy.authorize('iot:node:delete', cdict, cdict)
+
+ rpc_node = api_utils.get_rpc_node(node_ident)
+ pecan.request.rpcapi.destroy_node(pecan.request.context,
+ rpc_node.uuid)
+
+ @expose.expose(Node, types.uuid_or_name, body=Node, status_code=200)
+ def patch(self, node_ident, val_Node):
+ """Update a node.
+
+ :param node_ident: UUID or logical name of a node.
+ :param Node: values to be changed
+ :return updated_node: updated_node
+ """
+
+ context = pecan.request.context
+ cdict = context.to_policy_values()
+ policy.authorize('iot:node:update', cdict, cdict)
+
+ node = api_utils.get_rpc_node(node_ident)
+ val_Node = val_Node.as_dict()
+ for key in val_Node:
+ try:
+ node[key] = val_Node[key]
+ except Exception:
+ pass
+
+ updated_node = pecan.request.rpcapi.update_node(pecan.request.context,
+ node)
+ return Node.convert_with_links(updated_node)
diff --git a/iotronic/api/controllers/v1/plugin.py b/iotronic/api/controllers/v1/plugin.py
index 1e77524..8ebef32 100644
--- a/iotronic/api/controllers/v1/plugin.py
+++ b/iotronic/api/controllers/v1/plugin.py
@@ -12,44 +12,32 @@
from iotronic.api.controllers import base
+from iotronic.api.controllers import link
from iotronic.api.controllers.v1 import collection
from iotronic.api.controllers.v1 import types
from iotronic.api.controllers.v1 import utils as api_utils
from iotronic.api import expose
from iotronic.common import exception
+from iotronic.common import policy
from iotronic import objects
+
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
+_DEFAULT_RETURN_FIELDS = ('name', 'uuid')
+
class Plugin(base.APIBase):
"""API representation of a plugin.
"""
-
uuid = types.uuid
name = wsme.wsattr(wtypes.text)
config = wsme.wsattr(wtypes.text)
extra = types.jsontype
- @staticmethod
- def _convert(plugin, url, expand=True, show_password=True):
- if not expand:
- except_list = ['name', 'code', 'status', 'uuid', 'session', 'type']
- plugin.unset_fields_except(except_list)
- return plugin
- return plugin
-
- @classmethod
- def convert(cls, rpc_plugin, expand=True):
- plugin = Plugin(**rpc_plugin.as_dict())
- # plugin.id = rpc_plugin.id
- return cls._convert(plugin, pecan.request.host_url,
- expand,
- pecan.request.context.show_password)
-
def __init__(self, **kwargs):
self.fields = []
fields = list(objects.Plugin.fields)
@@ -60,6 +48,29 @@ class Plugin(base.APIBase):
self.fields.append(k)
setattr(self, k, kwargs.get(k, wtypes.Unset))
+ @staticmethod
+ def _convert_with_links(plugin, url, fields=None):
+ plugin_uuid = plugin.uuid
+ if fields is not None:
+ plugin.unset_fields_except(fields)
+
+ plugin.links = [link.Link.make_link('self', url, 'plugins',
+ plugin_uuid),
+ link.Link.make_link('bookmark', url, 'plugins',
+ plugin_uuid, bookmark=True)
+ ]
+ return plugin
+
+ @classmethod
+ def convert_with_links(cls, rpc_plugin, fields=None):
+ plugin = Plugin(**rpc_plugin.as_dict())
+
+ if fields is not None:
+ api_utils.check_for_invalid_fields(fields, plugin.as_dict())
+
+ return cls._convert_with_links(plugin, pecan.request.public_url,
+ fields=fields)
+
class PluginCollection(collection.Collection):
"""API representation of a collection of plugins."""
@@ -71,20 +82,23 @@ class PluginCollection(collection.Collection):
self._type = 'plugins'
@staticmethod
- def convert(plugins, limit, url=None, expand=False, **kwargs):
+ def convert_with_links(plugins, limit, url=None, fields=None, **kwargs):
collection = PluginCollection()
- collection.plugins = [
- Plugin.convert(
- n, expand) for n in plugins]
+ collection.plugins = [Plugin.convert_with_links(n, fields=fields)
+ for n in plugins]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
class PluginsController(rest.RestController):
- invalid_sort_key_list = []
+ """REST controller for Plugins."""
- def _get_plugins_collection(self, marker, limit, sort_key, sort_dir,
- expand=False, resource_url=None):
+ invalid_sort_key_list = ['extra', 'location']
+
+ def _get_plugins_collection(self, marker, limit,
+ sort_key, sort_dir,
+ resource_class=None,
+ resource_url=None, fields=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
@@ -100,39 +114,66 @@ class PluginsController(rest.RestController):
"sorting") % {'key': sort_key})
filters = {}
+
+ if resource_class is not None:
+ filters['resource_class'] = resource_class
+
plugins = objects.Plugin.list(pecan.request.context, limit, marker_obj,
sort_key=sort_key, sort_dir=sort_dir,
filters=filters)
parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
- return PluginCollection.convert(plugins, limit,
- url=resource_url,
- expand=expand,
- **parameters)
- @expose.expose(PluginCollection, types.uuid, int, wtypes.text, wtypes.text)
- def get_all(self, marker=None, limit=None, sort_key='id',
- sort_dir='asc'):
+ return PluginCollection.convert_with_links(plugins, limit,
+ url=resource_url,
+ fields=fields,
+ **parameters)
+
+ @expose.expose(Plugin, types.uuid_or_name, types.listtype)
+ def get_one(self, plugin_ident, fields=None):
+ """Retrieve information about the given plugin.
+
+ :param plugin_ident: UUID or logical name of a plugin.
+ :param fields: Optional, a list with a specified set of fields
+ of the resource to be returned.
+ """
+ cdict = pecan.request.context.to_policy_values()
+ policy.authorize('iot:plugin:get', cdict, cdict)
+
+ rpc_plugin = api_utils.get_rpc_plugin(plugin_ident)
+
+ return Plugin.convert_with_links(rpc_plugin, fields=fields)
+
+ @expose.expose(PluginCollection, types.uuid, int, wtypes.text,
+ wtypes.text, types.listtype, wtypes.text)
+ def get_all(self, marker=None,
+ limit=None, sort_key='id', sort_dir='asc',
+ fields=None, resource_class=None):
"""Retrieve a list of plugins.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
+ This value cannot be larger than the value of max_limit
+ in the [api] section of the ironic configuration, or only
+ max_limit resources will be returned.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
+ :param project: Optional string value to get only plugins
+ of the project.
+ :param resource_class: Optional string value to get only plugins with
+ that resource_class.
+ :param fields: Optional, a list with a specified set of fields
+ of the resource to be returned.
"""
+ cdict = pecan.request.context.to_policy_values()
+ policy.authorize('iot:plugin:get', cdict, cdict)
+
+ if fields is None:
+ fields = _DEFAULT_RETURN_FIELDS
return self._get_plugins_collection(marker,
- limit, sort_key, sort_dir)
-
- @expose.expose(Plugin, types.uuid_or_name)
- def get(self, plugin_ident):
- """Retrieve information about the given plugin.
-
- :param plugin_ident: UUID or logical name of a plugin.
- """
- rpc_plugin = api_utils.get_rpc_plugin(plugin_ident)
- plugin = Plugin(**rpc_plugin.as_dict())
- plugin.id = rpc_plugin.id
- return Plugin.convert(plugin)
+ limit, sort_key, sort_dir,
+ resource_class=resource_class,
+ fields=fields)
@expose.expose(Plugin, body=Plugin, status_code=201)
def post(self, Plugin):
@@ -140,6 +181,10 @@ class PluginsController(rest.RestController):
:param Plugin: a Plugin within the request body.
"""
+ context = pecan.request.context
+ cdict = context.to_policy_values()
+ policy.authorize('iot:plugin:create', cdict, cdict)
+
if not Plugin.name:
raise exception.MissingParameterValue(
("Name is not specified."))
@@ -155,7 +200,8 @@ class PluginsController(rest.RestController):
new_Plugin = pecan.request.rpcapi.create_plugin(pecan.request.context,
new_Plugin)
- return Plugin.convert(new_Plugin)
+
+ return Plugin.convert_with_links(new_Plugin)
@expose.expose(None, types.uuid_or_name, status_code=204)
def delete(self, plugin_ident):
@@ -163,10 +209,39 @@ class PluginsController(rest.RestController):
:param plugin_ident: UUID or logical name of a plugin.
"""
+ context = pecan.request.context
+ cdict = context.to_policy_values()
+ policy.authorize('iot:plugin:delete', cdict, cdict)
+
rpc_plugin = api_utils.get_rpc_plugin(plugin_ident)
pecan.request.rpcapi.destroy_plugin(pecan.request.context,
rpc_plugin.uuid)
+ @expose.expose(Plugin, types.uuid_or_name, body=Plugin, status_code=200)
+ def patch(self, plugin_ident, val_Plugin):
+ """Update a plugin.
+
+ :param plugin_ident: UUID or logical name of a plugin.
+ :param Plugin: values to be changed
+ :return updated_plugin: updated_plugin
+ """
+
+ context = pecan.request.context
+ cdict = context.to_policy_values()
+ policy.authorize('iot:plugin:update', cdict, cdict)
+
+ plugin = api_utils.get_rpc_plugin(plugin_ident)
+ val_Plugin = val_Plugin.as_dict()
+ for key in val_Plugin:
+ try:
+ plugin[key] = val_Plugin[key]
+ except Exception:
+ pass
+
+ updated_plugin = pecan.request.rpcapi.update_plugin(
+ pecan.request.context, plugin)
+ return Plugin.convert_with_links(updated_plugin)
+
@expose.expose(None, types.uuid_or_name, types.uuid_or_name,
status_code=200)
def put(self, plugin_ident, node_ident):
@@ -175,6 +250,11 @@ class PluginsController(rest.RestController):
:param plugin_ident: UUID or logical name of a plugin.
:param node_ident: UUID or logical name of a node.
"""
+
+ context = pecan.request.context
+ cdict = context.to_policy_values()
+ policy.authorize('iot:plugin:inject', cdict, cdict)
+
rpc_plugin = api_utils.get_rpc_plugin(plugin_ident)
rpc_node = api_utils.get_rpc_node(node_ident)
pecan.request.rpcapi.inject_plugin(pecan.request.context,
diff --git a/iotronic/api/controllers/v1/types.py b/iotronic/api/controllers/v1/types.py
index f435c61..30f7254 100644
--- a/iotronic/api/controllers/v1/types.py
+++ b/iotronic/api/controllers/v1/types.py
@@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import inspect
import json
from oslo_utils import strutils
@@ -23,6 +24,7 @@ import six
import wsme
from wsme import types as wtypes
+from iotronic.api.controllers.v1 import utils as v1_utils
from iotronic.common import exception
from iotronic.common.i18n import _
from iotronic.common import utils
@@ -33,11 +35,6 @@ class MacAddressType(wtypes.UserType):
basetype = wtypes.text
name = 'macaddress'
- # FIXME(lucasagomes): When used with wsexpose decorator WSME will try
- # to get the name of the type by accessing it's __name__ attribute.
- # Remove this __name__ attribute once it's fixed in WSME.
- # https://bugs.launchpad.net/wsme/+bug/1265590
- __name__ = name
@staticmethod
def validate(value):
@@ -55,16 +52,11 @@ class UuidOrNameType(wtypes.UserType):
basetype = wtypes.text
name = 'uuid_or_name'
- # FIXME(lucasagomes): When used with wsexpose decorator WSME will try
- # to get the name of the type by accessing it's __name__ attribute.
- # Remove this __name__ attribute once it's fixed in WSME.
- # https://bugs.launchpad.net/wsme/+bug/1265590
- __name__ = name
@staticmethod
def validate(value):
if not (uuidutils.is_uuid_like(value)
- or utils.is_hostname_safe(value)):
+ or v1_utils.is_valid_logical_name(value)):
raise exception.InvalidUuidOrName(name=value)
return value
@@ -80,15 +72,10 @@ class NameType(wtypes.UserType):
basetype = wtypes.text
name = 'name'
- # FIXME(lucasagomes): When used with wsexpose decorator WSME will try
- # to get the name of the type by accessing it's __name__ attribute.
- # Remove this __name__ attribute once it's fixed in WSME.
- # https://bugs.launchpad.net/wsme/+bug/1265590
- __name__ = name
@staticmethod
def validate(value):
- if not utils.is_hostname_safe(value):
+ if not v1_utils.is_valid_logical_name(value):
raise exception.InvalidName(name=value)
return value
@@ -104,11 +91,6 @@ class UuidType(wtypes.UserType):
basetype = wtypes.text
name = 'uuid'
- # FIXME(lucasagomes): When used with wsexpose decorator WSME will try
- # to get the name of the type by accessing it's __name__ attribute.
- # Remove this __name__ attribute once it's fixed in WSME.
- # https://bugs.launchpad.net/wsme/+bug/1265590
- __name__ = name
@staticmethod
def validate(value):
@@ -128,11 +110,6 @@ class BooleanType(wtypes.UserType):
basetype = wtypes.text
name = 'boolean'
- # FIXME(lucasagomes): When used with wsexpose decorator WSME will try
- # to get the name of the type by accessing it's __name__ attribute.
- # Remove this __name__ attribute once it's fixed in WSME.
- # https://bugs.launchpad.net/wsme/+bug/1265590
- __name__ = name
@staticmethod
def validate(value):
@@ -140,7 +117,7 @@ class BooleanType(wtypes.UserType):
return strutils.bool_from_string(value, strict=True)
except ValueError as e:
# raise Invalid to return 400 (BadRequest) in the API
- raise exception.Invalid(e)
+ raise exception.Invalid(six.text_type(e))
@staticmethod
def frombasetype(value):
@@ -154,11 +131,6 @@ class JsonType(wtypes.UserType):
basetype = wtypes.text
name = 'json'
- # FIXME(lucasagomes): When used with wsexpose decorator WSME will try
- # to get the name of the type by accessing it's __name__ attribute.
- # Remove this __name__ attribute once it's fixed in WSME.
- # https://bugs.launchpad.net/wsme/+bug/1265590
- __name__ = name
def __str__(self):
# These are the json serializable native types
@@ -179,11 +151,37 @@ class JsonType(wtypes.UserType):
return JsonType.validate(value)
+class ListType(wtypes.UserType):
+ """A simple list type."""
+
+ basetype = wtypes.text
+ name = 'list'
+
+ @staticmethod
+ def validate(value):
+ """Validate and convert the input to a ListType.
+
+ :param value: A comma separated string of values
+ :returns: A list of unique values, whose order is not guaranteed.
+ """
+ items = [v.strip().lower() for v in six.text_type(value).split(',')]
+ # filter() to remove empty items
+ # set() to remove duplicated items
+ return list(set(filter(None, items)))
+
+ @staticmethod
+ def frombasetype(value):
+ if value is None:
+ return None
+ return ListType.validate(value)
+
+
macaddress = MacAddressType()
uuid_or_name = UuidOrNameType()
name = NameType()
uuid = UuidType()
boolean = BooleanType()
+listtype = ListType()
# Can't call it 'json' because that's the name of the stdlib module
jsontype = JsonType()
@@ -197,6 +195,17 @@ class JsonPatchType(wtypes.Base):
mandatory=True)
value = wsme.wsattr(jsontype, default=wtypes.Unset)
+ # The class of the objects being patched. Override this in subclasses.
+ # Should probably be a subclass of iotronic.api.controllers.base.APIBase.
+ _api_base = None
+
+ # Attributes that are not required for construction, but which may not be
+ # removed if set. Override in subclasses if needed.
+ _extra_non_removable_attrs = set()
+
+ # Set of non-removable attributes, calculated lazily.
+ _non_removable_attrs = None
+
@staticmethod
def internal_attrs():
"""Returns a list of internal attributes.
@@ -207,15 +216,24 @@ class JsonPatchType(wtypes.Base):
"""
return ['/created_at', '/id', '/links', '/updated_at', '/uuid']
- @staticmethod
- def mandatory_attrs():
- """Retruns a list of mandatory attributes.
-
- Mandatory attributes can't be removed from the document. This
- method should be overwritten by derived class.
+ @classmethod
+ def non_removable_attrs(cls):
+ """Returns a set of names of attributes that may not be removed.
+ Attributes whose 'mandatory' property is True are automatically added
+ to this set. To add additional attributes to the set, override the
+ field _extra_non_removable_attrs in subclasses, with a set of the form
+ {'/foo', '/bar'}.
"""
- return []
+ if cls._non_removable_attrs is None:
+ cls._non_removable_attrs = cls._extra_non_removable_attrs.copy()
+ if cls._api_base:
+ fields = inspect.getmembers(cls._api_base,
+ lambda a: not inspect.isroutine(a))
+ for name, field in fields:
+ if getattr(field, 'mandatory', False):
+ cls._non_removable_attrs.add('/%s' % name)
+ return cls._non_removable_attrs
@staticmethod
def validate(patch):
@@ -224,16 +242,119 @@ class JsonPatchType(wtypes.Base):
msg = _("'%s' is an internal attribute and can not be updated")
raise wsme.exc.ClientSideError(msg % patch.path)
- if patch.path in patch.mandatory_attrs() and patch.op == 'remove':
+ if patch.path in patch.non_removable_attrs() and patch.op == 'remove':
msg = _("'%s' is a mandatory attribute and can not be removed")
raise wsme.exc.ClientSideError(msg % patch.path)
if patch.op != 'remove':
if patch.value is wsme.Unset:
- msg = _("'add' and 'replace' operations needs value")
+ msg = _("'add' and 'replace' operations need a value")
raise wsme.exc.ClientSideError(msg)
ret = {'path': patch.path, 'op': patch.op}
if patch.value is not wsme.Unset:
ret['value'] = patch.value
return ret
+
+
+class LocalLinkConnectionType(wtypes.UserType):
+ """A type describing local link connection."""
+
+ basetype = wtypes.DictType
+ name = 'locallinkconnection'
+
+ mandatory_fields = {'switch_id',
+ 'port_id'}
+ valid_fields = mandatory_fields.union({'switch_info'})
+
+ @staticmethod
+ def validate(value):
+ """Validate and convert the input to a LocalLinkConnectionType.
+
+ :param value: A dictionary of values to validate, switch_id is a MAC
+ address or an OpenFlow based datapath_id, switch_info is an
+ optional field.
+
+ For example::
+
+ {
+ 'switch_id': mac_or_datapath_id(),
+ 'port_id': 'Ethernet3/1',
+ 'switch_info': 'switch1'
+ }
+
+ :returns: A dictionary.
+ :raises: Invalid if some of the keys in the dictionary being validated
+ are unknown, invalid, or some required ones are missing.
+ """
+ wtypes.DictType(wtypes.text, wtypes.text).validate(value)
+
+ keys = set(value)
+
+ # This is to workaround an issue when an API object is initialized from
+ # RPC object, in which dictionary fields that are set to None become
+ # empty dictionaries
+ if not keys:
+ return value
+
+ invalid = keys - LocalLinkConnectionType.valid_fields
+ if invalid:
+ raise exception.Invalid(_('%s are invalid keys') % (invalid))
+
+ # Check all mandatory fields are present
+ missing = LocalLinkConnectionType.mandatory_fields - keys
+ if missing:
+ msg = _('Missing mandatory keys: %s') % missing
+ raise exception.Invalid(msg)
+
+ # Check switch_id is either a valid mac address or
+ # OpenFlow datapath_id and normalize it.
+ try:
+ value['switch_id'] = utils.validate_and_normalize_mac(
+ value['switch_id'])
+ except exception.InvalidMAC:
+ try:
+ value['switch_id'] = utils.validate_and_normalize_datapath_id(
+ value['switch_id'])
+ except exception.InvalidDatapathID:
+ raise exception.InvalidSwitchID(switch_id=value['switch_id'])
+
+ return value
+
+ @staticmethod
+ def frombasetype(value):
+ if value is None:
+ return None
+ return LocalLinkConnectionType.validate(value)
+
+locallinkconnectiontype = LocalLinkConnectionType()
+
+
+class VifType(JsonType):
+
+ basetype = wtypes.text
+ name = 'viftype'
+
+ mandatory_fields = {'id'}
+
+ @staticmethod
+ def validate(value):
+ super(VifType, VifType).validate(value)
+ keys = set(value)
+ # Check all mandatory fields are present
+ missing = VifType.mandatory_fields - keys
+ if missing:
+ msg = _('Missing mandatory keys: %s') % ', '.join(list(missing))
+ raise exception.Invalid(msg)
+ UuidOrNameType.validate(value['id'])
+
+ return value
+
+ @staticmethod
+ def frombasetype(value):
+ if value is None:
+ return None
+ return VifType.validate(value)
+
+
+viftype = VifType()
diff --git a/iotronic/api/controllers/v1/utils.py b/iotronic/api/controllers/v1/utils.py
index a63bb71..4d378ac 100644
--- a/iotronic/api/controllers/v1/utils.py
+++ b/iotronic/api/controllers/v1/utils.py
@@ -140,3 +140,19 @@ def is_valid_name(name):
:returns: True if the name is valid, False otherwise.
"""
return not uuidutils.is_uuid_like(name)
+
+
+def check_for_invalid_fields(fields, object_fields):
+ """Check for requested non-existent fields.
+
+ Check if the user requested non-existent fields.
+
+ :param fields: A list of fields requested by the user
+ :object_fields: A list of fields supported by the object.
+ :raises: InvalidParameterValue if invalid fields were requested.
+
+ """
+ invalid_fields = set(fields) - set(object_fields)
+ if invalid_fields:
+ raise exception.InvalidParameterValue(
+ _('Field(s) "%s" are not valid') % ', '.join(invalid_fields))
diff --git a/iotronic/api/controllers/v1/versions.py b/iotronic/api/controllers/v1/versions.py
new file mode 100644
index 0000000..4de266c
--- /dev/null
+++ b/iotronic/api/controllers/v1/versions.py
@@ -0,0 +1,25 @@
+# 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.
+
+# This is the version 1 API
+BASE_VERSION = 1
+
+MINOR_1_INITIAL_VERSION = 0
+MINOR_MAX_VERSION = 0
+
+# String representations of the minor and maximum versions
+MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION,
+ MINOR_1_INITIAL_VERSION)
+MAX_VERSION_STRING = '{}.{}'.format(BASE_VERSION,
+ MINOR_MAX_VERSION)
diff --git a/iotronic/api/hooks.py b/iotronic/api/hooks.py
index 5659721..af8742c 100644
--- a/iotronic/api/hooks.py
+++ b/iotronic/api/hooks.py
@@ -15,14 +15,18 @@
# under the License.
from oslo_config import cfg
+from oslo_log import log
from pecan import hooks
-from webob import exc
+from six.moves import http_client
from iotronic.common import context
from iotronic.common import policy
-
-
from iotronic.conductor import rpcapi
+from iotronic.db import api as dbapi
+
+LOG = log.getLogger(__name__)
+
+CHECKED_DEPRECATED_POLICY_ARGS = False
class ConfigHook(hooks.PecanHook):
@@ -36,31 +40,11 @@ class DBHook(hooks.PecanHook):
"""Attach the dbapi object to the request so controllers can get to it."""
def before(self, state):
-
- # state.request.dbapi = dbapi.get_instance()
- pass
+ state.request.dbapi = dbapi.get_instance()
class ContextHook(hooks.PecanHook):
- """Configures a request context and attaches it to the request.
-
- The following HTTP request headers are used:
-
- X-User-Id or X-User:
- Used for context.user_id.
-
- X-Tenant-Id or X-Tenant:
- Used for context.tenant.
-
- X-Auth-Token:
- Used for context.auth_token.
-
- X-Roles:
- Used for setting context.is_admin flag to either True or False.
- The flag is set to True, if X-Roles contains either an administrator
- or admin substring. Otherwise it is set to False.
-
- """
+ """Configures a request context and attaches it to the request."""
def __init__(self, public_api_routes):
self.public_api_routes = public_api_routes
@@ -69,32 +53,34 @@ class ContextHook(hooks.PecanHook):
def before(self, state):
headers = state.request.headers
- # Do not pass any token with context for noauth mode
- auth_token = (None if cfg.CONF.auth_strategy == 'noauth' else
- headers.get('X-Auth-Token'))
-
- creds = {
- 'user': headers.get('X-User') or headers.get('X-User-Id'),
- 'tenant': headers.get('X-Tenant') or headers.get('X-Tenant-Id'),
- 'domain_id': headers.get('X-User-Domain-Id'),
- 'domain_name': headers.get('X-User-Domain-Name'),
- 'auth_token': auth_token,
- 'roles': headers.get('X-Roles', '').split(','),
- }
-
- # NOTE(adam_g): We also check the previous 'admin' rule to ensure
- # compat with default juno policy.json. This double check may be
- # removed in L.
- is_admin = (policy.enforce('admin_api', creds, creds) or
- policy.enforce('admin', creds, creds))
- is_public_api = state.request.environ.get('is_public_api', False)
- show_password = policy.enforce('show_password', creds, creds)
-
- state.request.context = context.RequestContext(
- is_admin=is_admin,
+ is_public_api = state.request.environ.get(
+ 'is_public_api', False)
+ ctx = context.RequestContext.from_environ(
+ state.request.environ,
is_public_api=is_public_api,
- show_password=show_password,
- **creds)
+ project_id=headers.get('X-Project-Id'),
+ user_id=headers.get('X-User-Id'),
+ )
+
+ # Do not pass any token with context for noauth mode
+ if cfg.CONF.auth_strategy == 'noauth':
+ ctx.auth_token = None
+
+ creds = ctx.to_policy_values()
+
+ is_admin = policy.check('is_admin', creds, creds)
+ ctx.is_admin = is_admin
+
+ state.request.context = ctx
+
+ def after(self, state):
+ if state.request.context == {}:
+ # An incorrect url path will not create RequestContext
+ return
+ # NOTE(lintan): RequestContext will generate a request_id if no one
+ # passing outside, so it always contain a request_id.
+ request_id = state.request.context.request_id
+ state.response.headers['Openstack-Request-Id'] = request_id
class RPCHook(hooks.PecanHook):
@@ -104,23 +90,6 @@ class RPCHook(hooks.PecanHook):
state.request.rpcapi = rpcapi.ConductorAPI()
-class TrustedCallHook(hooks.PecanHook):
- """Verify that the user has admin rights.
-
- Checks whether the API call is performed against a public
- resource or the user has admin privileges in the appropriate
- tenant, domain or other administrative unit.
-
- """
-
- def before(self, state):
- ctx = state.request.context
- if ctx.is_public_api:
- return
- policy.enforce('admin_api', ctx.to_dict(), ctx.to_dict(),
- do_raise=True, exc=exc.HTTPForbidden)
-
-
class NoExceptionTracebackHook(hooks.PecanHook):
"""Workaround rpc.common: deserialize_remote_exception.
@@ -129,24 +98,26 @@ class NoExceptionTracebackHook(hooks.PecanHook):
concern so this hook is aimed to cut-off traceback from the error message.
"""
+
# NOTE(max_lobur): 'after' hook used instead of 'on_error' because
# 'on_error' never fired for wsme+pecan pair. wsme @wsexpose decorator
# catches and handles all the errors, so 'on_error' dedicated for unhandled
# exceptions never fired.
-
def after(self, state):
# Omit empty body. Some errors may not have body at this level yet.
if not state.response.body:
return
# Do nothing if there is no error.
- if 200 <= state.response.status_int < 400:
+ # Status codes in the range 200 (OK) to 399 (400 = BAD_REQUEST) are not
+ # an error.
+ if (http_client.OK <= state.response.status_int <
+ http_client.BAD_REQUEST):
return
json_body = state.response.json
- # Do not remove traceback when server in debug mode (except 'Server'
- # errors when 'debuginfo' will be used for traces).
- if cfg.CONF.debug and json_body.get('faultcode') != 'Server':
+ # Do not remove traceback when traceback config is set
+ if cfg.CONF.debug_tracebacks_in_api:
return
faultstring = json_body.get('faultstring')
@@ -156,6 +127,19 @@ class NoExceptionTracebackHook(hooks.PecanHook):
faultstring = faultstring.split(traceback_marker, 1)[0]
# Remove trailing newlines and spaces if any.
json_body['faultstring'] = faultstring.rstrip()
- # Replace the whole json. Cannot change original one beacause it's
+ # Replace the whole json. Cannot change original one because it's
# generated on the fly.
state.response.json = json_body
+
+
+class PublicUrlHook(hooks.PecanHook):
+ """Attach the right public_url to the request.
+
+ Attach the right public_url to the request so resources can create
+ links even when the API service is behind a proxy or SSL terminator.
+
+ """
+
+ def before(self, state):
+ state.request.public_url = (cfg.CONF.api.public_endpoint or
+ state.request.host_url)
diff --git a/iotronic/api/middleware/__init__.py b/iotronic/api/middleware/__init__.py
index 022a5ab..746a609 100644
--- a/iotronic/api/middleware/__init__.py
+++ b/iotronic/api/middleware/__init__.py
@@ -19,5 +19,5 @@ from iotronic.api.middleware import parsable_error
ParsableErrorMiddleware = parsable_error.ParsableErrorMiddleware
AuthTokenMiddleware = auth_token.AuthTokenMiddleware
-__all__ = (ParsableErrorMiddleware,
- AuthTokenMiddleware)
+__all__ = ('ParsableErrorMiddleware',
+ 'AuthTokenMiddleware')
diff --git a/iotronic/api/middleware/auth_token.py b/iotronic/api/middleware/auth_token.py
index 9c21ab1..c598d69 100644
--- a/iotronic/api/middleware/auth_token.py
+++ b/iotronic/api/middleware/auth_token.py
@@ -31,15 +31,16 @@ class AuthTokenMiddleware(auth_token.AuthProtocol):
for public routes in the API.
"""
-
- def __init__(self, app, conf, public_api_routes=[]):
+ def __init__(self, app, conf, public_api_routes=None):
+ api_routes = [] if public_api_routes is None else public_api_routes
+ self._iotronic_app = app
# TODO(mrda): Remove .xml and ensure that doesn't result in a
# 401 Authentication Required instead of 404 Not Found
route_pattern_tpl = '%s(\.json|\.xml)?$'
try:
self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
- for route_tpl in public_api_routes]
+ for route_tpl in api_routes]
except re.error as e:
msg = _('Cannot compile public API routes: %s') % e
@@ -58,6 +59,6 @@ class AuthTokenMiddleware(auth_token.AuthProtocol):
self.public_api_routes))
if env['is_public_api']:
- return self._app(env, start_response)
+ return self._iotronic_app(env, start_response)
return super(AuthTokenMiddleware, self).__call__(env, start_response)
diff --git a/iotronic/api/middleware/parsable_error.py b/iotronic/api/middleware/parsable_error.py
index e02a164..d4150fc 100644
--- a/iotronic/api/middleware/parsable_error.py
+++ b/iotronic/api/middleware/parsable_error.py
@@ -69,14 +69,15 @@ class ParsableErrorMiddleware(object):
app_iter = self.app(environ, replacement_start_response)
if (state['status_code'] // 100) not in (2, 3):
req = webob.Request(environ)
- if (req.accept.best_match(['application/json', 'application/xml'])
- == 'application/xml'):
+ if (req.accept.best_match(
+ ['application/json',
+ 'application/xml']) == 'application/xml'):
try:
# simple check xml is valid
body = [et.ElementTree.tostring(
- et.ElementTree.fromstring(''
- + '\n'.join(app_iter)
- + ''))]
+ et.ElementTree.fromstring(''
+ + '\n'.join(app_iter)
+ + ''))]
except et.ElementTree.ParseError as err:
LOG.error(_LE('Error parsing HTTP response: %s'), err)
body = ['%s' % state['status_code']
diff --git a/iotronic/common/context.py b/iotronic/common/context.py
index aaeffb3..a6b9759 100644
--- a/iotronic/common/context.py
+++ b/iotronic/common/context.py
@@ -16,37 +16,38 @@ from oslo_context import context
class RequestContext(context.RequestContext):
- """Extends security contexts from the OpenStack common library."""
+ """Extends security contexts from the oslo.context library."""
- def __init__(self, auth_token=None, domain_id=None, domain_name=None,
- user=None, tenant=None, is_admin=False, is_public_api=False,
- read_only=False, show_deleted=False, request_id=None,
- roles=None, show_password=True):
- """Stores several additional request parameters:
+ def __init__(self, is_public_api=False, user_id=None,
+ project_id=None, **kwargs):
+ """Initialize the RequestContext
- :param domain_id: The ID of the domain.
- :param domain_name: The name of the domain.
:param is_public_api: Specifies whether the request should be processed
- without authentication.
- :param roles: List of user's roles if any.
- :param show_password: Specifies whether passwords should be masked
- before sending back to API call.
-
+ without authentication.
+ :param kwargs: additional arguments passed to oslo.context.
"""
+ super(RequestContext, self).__init__(**kwargs)
self.is_public_api = is_public_api
- self.domain_id = domain_id
- self.domain_name = domain_name
- self.roles = roles or []
- self.show_password = show_password
+ self.project_id = project_id
+ self.user_id = user_id
- super(RequestContext, self).__init__(auth_token=auth_token,
- user=user, tenant=tenant,
- is_admin=is_admin,
- read_only=read_only,
- show_deleted=show_deleted,
- request_id=request_id)
+ def to_policy_values(self):
+ policy_values = super(RequestContext, self).to_policy_values()
+
+ # TODO(vdrok): remove all of these apart from is_public_api and
+ # project_name after deprecation period
+ policy_values.update({
+ 'user': self.user,
+ 'domain_id': self.user_domain,
+ 'domain_name': self.user_domain_name,
+ 'tenant': self.tenant,
+ 'project_name': self.project_name,
+ 'is_public_api': self.is_public_api,
+ })
+ return policy_values
def to_dict(self):
+ # TODO(vdrok): reuse the base class to_dict in Pike
return {'auth_token': self.auth_token,
'user': self.user,
'tenant': self.tenant,
@@ -54,14 +55,40 @@ class RequestContext(context.RequestContext):
'read_only': self.read_only,
'show_deleted': self.show_deleted,
'request_id': self.request_id,
- 'domain_id': self.domain_id,
+ 'domain_id': self.user_domain,
'roles': self.roles,
- 'domain_name': self.domain_name,
- 'show_password': self.show_password,
- 'is_public_api': self.is_public_api}
+ 'domain_name': self.user_domain_name,
+ 'is_public_api': self.is_public_api,
+ 'user_id': self.user_id,
+ 'project_id': self.project_id
+ }
@classmethod
- def from_dict(cls, values):
- values.pop('user', None)
- values.pop('tenant', None)
- return cls(**values)
+ def from_dict(cls, values, **kwargs):
+ kwargs.setdefault('is_public_api', values.get('is_public_api', False))
+ if 'domain_id' in values:
+ kwargs.setdefault('user_domain', values['domain_id'])
+ return super(RequestContext, RequestContext).from_dict(values,
+ **kwargs)
+
+ def ensure_thread_contain_context(self):
+ """Ensure threading contains context
+
+ For async/periodic tasks, the context of local thread is missing.
+ Set it with request context and this is useful to log the request_id
+ in log messages.
+
+ """
+ if context.get_current():
+ return
+ self.update_store()
+
+
+def get_admin_context():
+ """Create an administrator context."""
+
+ context = RequestContext(auth_token=None,
+ tenant=None,
+ is_admin=True,
+ overwrite=False)
+ return context
diff --git a/iotronic/common/exception.py b/iotronic/common/exception.py
index a76af14..acbf4fc 100644
--- a/iotronic/common/exception.py
+++ b/iotronic/common/exception.py
@@ -405,7 +405,7 @@ class ServiceUnavailable(IotronicException):
class Forbidden(IotronicException):
- message = _("Requested OpenStack Images API is forbidden")
+ message = _("Requested Iotronic API is forbidden")
class BadRequest(IotronicException):
diff --git a/iotronic/common/policy.py b/iotronic/common/policy.py
index 754782e..aa6c72e 100644
--- a/iotronic/common/policy.py
+++ b/iotronic/common/policy.py
@@ -13,26 +13,115 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""Policy Engine For Iotronic."""
+"""Policy Engine For Ironic."""
+
+import sys
from oslo_concurrency import lockutils
from oslo_config import cfg
+from oslo_log import log
from oslo_policy import policy
+from iotronic.common import exception
+from iotronic.common.i18n import _LW
+
_ENFORCER = None
CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+
+default_policies = [
+ # Legacy setting, don't remove. Likely to be overridden by operators who
+ # forget to update their policy.json configuration file.
+ # This gets rolled into the new "is_admin" rule below.
+ policy.RuleDefault('admin_api',
+ 'role:admin or role:administrator',
+ description='Legacy rule for cloud admin access'),
+ # is_public_api is set in the environment from AuthTokenMiddleware
+ policy.RuleDefault('public_api',
+ 'is_public_api:True',
+ description='Internal flag for public API routes'),
+
+ policy.RuleDefault('is_admin',
+ 'rule:admin_api',
+ description='Full read/write API access'),
+ policy.RuleDefault('is_admin_iot_project',
+ 'role:admin_iot_project',
+ description='Full read/write API access'),
+ policy.RuleDefault('is_manager_iot_project',
+ 'role:manager_iot_project',
+ description='Full read/write API access'),
+ policy.RuleDefault('is_user_iot',
+ 'role:user_iot',
+ description='Full read/write API access'),
+]
+
+# NOTE(deva): to follow policy-in-code spec, we define defaults for
+# the granular policies in code, rather than in policy.json.
+# All of these may be overridden by configuration, but we can
+# depend on their existence throughout the code.
+
+node_policies = [
+ policy.RuleDefault('iot:node:get',
+ 'rule:is_admin or rule:is_admin_iot_project '
+ 'or rule:is_manager_iot_project or rule:is_user_iot',
+ description='Retrieve Node records'),
+ policy.RuleDefault('iot:node:create',
+ 'rule:is_admin_iot_project',
+ description='Create Node records'),
+ policy.RuleDefault('iot:node:delete',
+ 'rule:is_admin or rule:is_admin_iot_project '
+ 'or rule:is_manager_iot_project',
+ description='Delete Node records'),
+ policy.RuleDefault('iot:node:update',
+ 'rule:is_admin or rule:is_admin_iot_project '
+ 'or rule:is_manager_iot_project',
+ description='Update Node records'),
+
+]
+
+plugin_policies = [
+ policy.RuleDefault('iot:plugin:get',
+ 'rule:is_admin or rule:is_admin_iot_project '
+ 'or rule:is_manager_iot_project or rule:is_user_iot',
+ description='Retrieve Plugin records'),
+ policy.RuleDefault('iot:plugin:create',
+ 'rule:is_admin_iot_project',
+ description='Create Plugin records'),
+ policy.RuleDefault('iot:plugin:delete',
+ 'rule:is_admin or rule:is_admin_iot_project '
+ 'or rule:is_manager_iot_project',
+ description='Delete Plugin records'),
+ policy.RuleDefault('iot:plugin:update',
+ 'rule:is_admin or rule:is_admin_iot_project '
+ 'or rule:is_manager_iot_project',
+ description='Update Plugin records'),
+ policy.RuleDefault('iot:plugin:inject',
+ 'rule:is_admin or rule:is_admin_iot_project '
+ 'or rule:is_manager_iot_project',
+ description='Inject Plugin records'),
+
+]
-@lockutils.synchronized('policy_enforcer', 'iotronic-')
+def list_policies():
+ policies = (default_policies
+ + node_policies
+ + plugin_policies
+ )
+ return policies
+
+
+@lockutils.synchronized('policy_enforcer')
def init_enforcer(policy_file=None, rules=None,
default_rule=None, use_conf=True):
"""Synchronously initializes the policy enforcer
:param policy_file: Custom policy file to use, if none is specified,
- `CONF.policy_file` will be used.
+ `CONF.oslo_policy.policy_file` will be used.
:param rules: Default dictionary / Rules to use. It will be
considered just in the first instantiation.
- :param default_rule: Default rule to use, CONF.default_rule will
+ :param default_rule: Default rule to use,
+ CONF.oslo_policy.policy_default_rule will
be used if none is specified.
:param use_conf: Whether to load rules from config file.
@@ -42,11 +131,17 @@ def init_enforcer(policy_file=None, rules=None,
if _ENFORCER:
return
+ # NOTE(deva): Register defaults for policy-in-code here so that they are
+ # loaded exactly once - when this module-global is initialized.
+ # Defining these in the relevant API modules won't work
+ # because API classes lack singletons and don't use globals.
_ENFORCER = policy.Enforcer(CONF, policy_file=policy_file,
rules=rules,
default_rule=default_rule,
use_conf=use_conf)
+ _ENFORCER.register_defaults(list_policies())
+
def get_enforcer():
"""Provides access to the single instance of Policy enforcer."""
@@ -57,12 +152,79 @@ def get_enforcer():
return _ENFORCER
+def get_oslo_policy_enforcer():
+ # This method is for use by oslopolicy CLI scripts. Those scripts need the
+ # 'output-file' and 'namespace' options, but having those in sys.argv means
+ # loading the Ironic config options will fail as those are not expected to
+ # be present. So we pass in an arg list with those stripped out.
+
+ conf_args = []
+ # Start at 1 because cfg.CONF expects the equivalent of sys.argv[1:]
+ i = 1
+ while i < len(sys.argv):
+ if sys.argv[i].strip('-') in ['namespace', 'output-file']:
+ i += 2
+ continue
+ conf_args.append(sys.argv[i])
+ i += 1
+
+ cfg.CONF(conf_args, project='ironic')
+
+ return get_enforcer()
+
+
+# NOTE(deva): We can't call these methods from within decorators because the
+# 'target' and 'creds' parameter must be fetched from the call time
+# context-local pecan.request magic variable, but decorators are compiled
+# at module-load time.
+
+
+def authorize(rule, target, creds, *args, **kwargs):
+ """A shortcut for policy.Enforcer.authorize()
+
+ Checks authorization of a rule against the target and credentials, and
+ raises an exception if the rule is not defined.
+ Always returns true if CONF.auth_strategy == noauth.
+
+ Beginning with the Newton cycle, this should be used in place of 'enforce'.
+ """
+ if CONF.auth_strategy == 'noauth':
+ return True
+ enforcer = get_enforcer()
+
+ try:
+ return enforcer.authorize(rule, target, creds, do_raise=True,
+ *args, **kwargs)
+ except policy.PolicyNotAuthorized:
+ raise exception.HTTPForbidden(resource=rule)
+
+
+def check(rule, target, creds, *args, **kwargs):
+ """A shortcut for policy.Enforcer.enforce()
+
+ Checks authorization of a rule against the target and credentials
+ and returns True or False.
+ """
+ enforcer = get_enforcer()
+ return enforcer.enforce(rule, target, creds, *args, **kwargs)
+
+
def enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs):
"""A shortcut for policy.Enforcer.enforce()
Checks authorization of a rule against the target and credentials.
+ Always returns true if CONF.auth_strategy == noauth.
"""
+ # NOTE(deva): this method is obsoleted by authorize(), but retained for
+ # backwards compatibility in case it has been used downstream.
+ # It may be removed in the Pike cycle.
+ LOG.warning(_LW(
+ "Deprecation warning: calls to ironic.common.policy.enforce() "
+ "should be replaced with authorize(). This method may be removed "
+ "in a future release."))
+ if CONF.auth_strategy == 'noauth':
+ return True
enforcer = get_enforcer()
return enforcer.enforce(rule, target, creds, do_raise=do_raise,
exc=exc, *args, **kwargs)
diff --git a/iotronic/db/api.py b/iotronic/db/api.py
index 07859e1..9422e7a 100644
--- a/iotronic/db/api.py
+++ b/iotronic/db/api.py
@@ -129,6 +129,14 @@ class Connection(object):
:returns: A node.
"""
+ @abc.abstractmethod
+ def get_node_id_by_uuid(self, node_uuid):
+ """Return a node id.
+
+ :param node_uuid: The uuid of a node.
+ # :returns: A node.id.
+ """
+
@abc.abstractmethod
def get_node_by_name(self, node_name):
"""Return a node.
@@ -205,8 +213,7 @@ class Connection(object):
"""
@abc.abstractmethod
- def get_session_by_node_uuid(self, filters=None, limit=None, marker=None,
- sort_key=None, sort_dir=None):
+ def get_session_by_node_uuid(self, node_uuid, valid):
"""Return a Wamp session of a Node
:param filters: Filters to apply. Defaults to None.
diff --git a/iotronic/db/sqlalchemy/api.py b/iotronic/db/sqlalchemy/api.py
index 95b78af..098ae4b 100644
--- a/iotronic/db/sqlalchemy/api.py
+++ b/iotronic/db/sqlalchemy/api.py
@@ -125,12 +125,9 @@ class Connection(api.Connection):
def _add_nodes_filters(self, query, filters):
if filters is None:
filters = []
-
- if 'associated' in filters:
- if filters['associated']:
- query = query.filter(models.Node.instance_uuid is not None)
- else:
- query = query.filter(models.Node.instance_uuid is None)
+ #
+ if 'project_id' in filters:
+ query = query.filter(models.Node.project == filters['project_id'])
return query
@@ -224,6 +221,13 @@ class Connection(api.Connection):
except NoResultFound:
raise exception.NodeNotFound(node=node_id)
+ def get_node_id_by_uuid(self, node_uuid):
+ query = model_query(models.Node.id).filter_by(uuid=node_uuid)
+ try:
+ return query.one()
+ except NoResultFound:
+ raise exception.NodeNotFound(node=node_uuid)
+
def get_node_by_uuid(self, node_uuid):
query = model_query(models.Node).filter_by(uuid=node_uuid)
try:
@@ -402,6 +406,7 @@ class Connection(api.Connection):
models.SessionWP).filter_by(
node_uuid=node_uuid).filter_by(
valid=valid)
+
try:
return query.one()
except NoResultFound:
diff --git a/iotronic/db/sqlalchemy/models.py b/iotronic/db/sqlalchemy/models.py
index 6f78d49..16ebf85 100644
--- a/iotronic/db/sqlalchemy/models.py
+++ b/iotronic/db/sqlalchemy/models.py
@@ -154,7 +154,8 @@ class Node(Base):
name = Column(String(255), nullable=True)
type = Column(String(255))
agent = Column(String(255), nullable=True)
- session = Column(String(255), nullable=True)
+ owner = Column(String(36))
+ project = Column(String(36))
mobile = Column(Boolean, default=False)
config = Column(JSONEncodedDict)
extra = Column(JSONEncodedDict)
diff --git a/iotronic/objects/location.py b/iotronic/objects/location.py
index c3d8729..f8f6f2a 100644
--- a/iotronic/objects/location.py
+++ b/iotronic/objects/location.py
@@ -108,6 +108,27 @@ class Location(base.IotronicObject):
sort_dir=sort_dir)
return Location._from_db_object_list(db_locations, cls, context)
+ @base.remotable_classmethod
+ def list_by_node_uuid(cls, context, node_uuid, limit=None, marker=None,
+ sort_key=None, sort_dir=None):
+ """Return a list of Location objects associated with a given node ID.
+
+ :param context: Security context.
+ :param node_id: the ID of the node.
+ :param limit: maximum number of resources to return in a single result.
+ :param marker: pagination marker for large data sets.
+ :param sort_key: column to sort results by.
+ :param sort_dir: direction to sort. "asc" or "desc".
+ :returns: a list of :class:`Location` object.
+
+ """
+ node_id = cls.dbapi.get_node_id_by_uuid(node_uuid)[0]
+ db_locations = cls.dbapi.get_locations_by_node_id(node_id, limit=limit,
+ marker=marker,
+ sort_key=sort_key,
+ sort_dir=sort_dir)
+ return Location._from_db_object_list(db_locations, cls, context)
+
@base.remotable_classmethod
def list_by_node_id(cls, context, node_id, limit=None, marker=None,
sort_key=None, sort_dir=None):
@@ -194,6 +215,7 @@ class Location(base.IotronicObject):
"""
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
for field in self.fields:
- if (hasattr(self, base.get_attrname(field)) and
- self[field] != current[field]):
+ if (hasattr(
+ self, base.get_attrname(field))
+ and self[field] != current[field]):
self[field] = current[field]
diff --git a/iotronic/objects/node.py b/iotronic/objects/node.py
index a19ee8d..9bdd0d5 100644
--- a/iotronic/objects/node.py
+++ b/iotronic/objects/node.py
@@ -36,7 +36,8 @@ class Node(base.IotronicObject):
'name': obj_utils.str_or_none,
'type': obj_utils.str_or_none,
'agent': obj_utils.str_or_none,
- 'session': obj_utils.str_or_none,
+ 'owner': obj_utils.str_or_none,
+ 'project': obj_utils.str_or_none,
'mobile': bool,
'config': obj_utils.dict_or_none,
'extra': obj_utils.dict_or_none,
diff --git a/iotronic/objects/sessionwp.py b/iotronic/objects/sessionwp.py
index 7a702b8..1ca9283 100644
--- a/iotronic/objects/sessionwp.py
+++ b/iotronic/objects/sessionwp.py
@@ -89,7 +89,7 @@ class SessionWP(base.IotronicObject):
return session
@base.remotable_classmethod
- def get_session_by_node_uuid(cls, node_uuid, valid=True, context=None):
+ def get_session_by_node_uuid(cls, context, node_uuid, valid=True):
"""Find a session based on uuid and return a :class:`SessionWP` object.
:param node_uuid: the uuid of a node.
@@ -209,6 +209,6 @@ class SessionWP(base.IotronicObject):
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
for field in self.fields:
if (hasattr(
- self, base.get_attrname(field)) and
- self[field] != current[field]):
+ self, base.get_attrname(field))
+ and self[field] != current[field]):
self[field] = current[field]
diff --git a/utils/iotronic.sql b/utils/iotronic.sql
index e86dfca..1d2003c 100644
--- a/utils/iotronic.sql
+++ b/utils/iotronic.sql
@@ -70,7 +70,8 @@ CREATE TABLE IF NOT EXISTS `iotronic`.`nodes` (
`name` VARCHAR(255) NULL DEFAULT NULL,
`type` VARCHAR(255) NOT NULL,
`agent` VARCHAR(255) NULL DEFAULT NULL,
- `session` VARCHAR(255) NULL DEFAULT NULL,
+ `owner` VARCHAR(36) NOT NULL,
+ `project` VARCHAR(36) NOT NULL,
`mobile` TINYINT(1) NOT NULL DEFAULT '0',
`config` TEXT NULL DEFAULT NULL,
`extra` TEXT NULL DEFAULT NULL,
@@ -189,5 +190,11 @@ SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
-- insert testing nodes
-INSERT INTO `nodes` VALUES ('2017-02-20 10:38:26',NULL,132,'f3961f7a-c937-4359-8848-fb64aa8eeaaa','12345','registered','node','server',NULL,NULL,0,'{}','{}'),('2017-02-20 10:38:45',NULL,133,'ba1efce9-cad9-4ae1-a5d1-d90a8d203d3b','yunyun','registered','yun22','yun',NULL,NULL,0,'{}','{}'),('2017-02-20 10:39:08',NULL,134,'65f9db36-9786-4803-b66f-51dcdb60066e','test','registered','test','server',NULL,NULL,0,'{}','{}');
-INSERT INTO `locations` VALUES ('2017-02-20 10:38:26',NULL,6,'2','1','3',132),('2017-02-20 10:38:45',NULL,7,'2','1','3',133),('2017-02-20 10:39:08',NULL,8,'2','1','3',134)
\ No newline at end of file
+INSERT INTO `nodes` VALUES
+ ('2017-02-20 10:38:26',NULL,132,'f3961f7a-c937-4359-8848-fb64aa8eeaaa','12345','registered','node','server',NULL,'eee383360cc14c44b9bf21e1e003a4f3','4adfe95d49ad41398e00ecda80257d21',0,'{}','{}'),
+ ('2017-02-20 10:38:45',NULL,133,'ba1efce9-cad9-4ae1-a5d1-d90a8d203d3b','yunyun','registered','yun22','yun',NULL,'eee383360cc14c44b9bf21e1e003a4f3','4adfe95d49ad41398e00ecda80257d21',0,'{}','{}'),
+ ('2017-02-20 10:39:08',NULL,134,'65f9db36-9786-4803-b66f-51dcdb60066e','test','registered','test','server',NULL,'eee383360cc14c44b9bf21e1e003a4f3','4adfe95d49ad41398e00ecda80257d21',0,'{}','{}');
+INSERT INTO `locations` VALUES
+ ('2017-02-20 10:38:26',NULL,6,'2','1','3',132),
+ ('2017-02-20 10:38:45',NULL,7,'2','1','3',133),
+ ('2017-02-20 10:39:08',NULL,8,'2','1','3',134)
\ No newline at end of file
diff --git a/utils/iotronic_curl_client b/utils/iotronic_curl_client
index ca2a00a..e390864 100755
--- a/utils/iotronic_curl_client
+++ b/utils/iotronic_curl_client
@@ -1,60 +1,195 @@
-#!/bin/bash
+#! /usr/bin/python
-HOST='localhost'
-PORT='1288'
-VERSION='v1'
-BASE=http://$HOST:$PORT/$VERSION
-function node_manager() {
- case "$1" in
- list) curl -sS $BASE/nodes/ | python -m json.tool
- echo "";
- ;;
- create) curl -sS -H "Content-Type: application/json" -X POST $BASE/nodes/ \
- -d '{"type":"'"$7"'","code":"'"$2"'","name":"'"$3"'","location":[{"latitude":"'"$4"'","longitude":"'"$5"'","altitude":"'"$6"'"}]}' | python -m json.tool
- echo "";
- ;;
- delete) curl -sS -X DELETE $BASE/nodes/$2 | python -m json.tool
- echo "";
- ;;
- show) curl -sS $BASE/nodes/$2 | python -m json.tool
- echo "";
- ;;
- *) echo "node list|create|delete|show"
- esac
-}
+import sys
+import requests
+import json
+import os
-function plugin_manager() {
- case "$1" in
- list) curl -sS $BASE/plugins/ | python -m json.tool
- echo "";
- ;;
- create) echo "TBI"
- echo "";
- ;;
- delete) echo "TBI"
- echo "";
- ;;
- show) curl -sS $BASE/plugins/$2 | python -m json.tool
- echo "";
- ;;
- *) echo "plugin list|create|delete|show"
- esac
-}
+token = None
+iotronic_url = "http://192.168.17.102:1288/v1"
-if [ $# -lt 1 ]
-then
-echo "USAGE: iotronic node|plugin [OPTIONS]"
-exit
-fi
+try:
+ os.environ['OS_AUTH_URL']
+except Exception:
+ print("load the rc")
+ sys.exit(1)
-case "$1" in
-node) node_manager "${@:2}";
-echo "";
-;;
-plugin) plugin_manager "${@:2}"
-echo "";
-;;
-*) echo "USAGE: iotronic node|plugin [OPTIONS]"
-esac
+url = os.environ['OS_AUTH_URL'] + "/auth/tokens"
+
+def get_token(user, project, psw):
+ payload = {"auth": {
+ "identity": {
+ "methods": ["password"],
+ "password": {
+ "user": {
+ "name": user,
+ "domain": {"id": "default"},
+ "password": psw
+ }
+ }
+ },
+ "scope": {
+ "project": {
+ "name": project,
+ "domain": {"id": "default"}
+ }
+ }
+ }
+ }
+
+ headers = {
+ 'content-type': "application/json",
+ }
+
+ response = requests.request("POST", url, data=json.dumps(payload), headers=headers)
+ token=response.headers.get('X-Subject-Token')
+ if token:
+ r=json.loads(response.text)['token']['roles']
+ roles=[str(x['name']) for x in r]
+
+ print(user + " in " + project + ' with roles: '+ " ".join(roles) )
+
+ return token
+
+
+def node_manager(argv):
+ actions = ['show', 'list', 'create', 'delete', 'update']
+ if argv[0] not in actions or len(argv) == 0:
+ print("node list|create|delete|show|update")
+ sys.exit()
+
+ global iotronic_url, token
+ if not token:
+ token = get_token(os.environ['OS_USERNAME'], os.environ['OS_PROJECT_NAME'], os.environ['OS_PASSWORD'])
+
+ # print(token)
+
+ headers = {'content-type': "application/json", 'x-auth-token': token, }
+
+ if argv[0] == 'list':
+ url = iotronic_url + "/nodes"
+ response = requests.request("GET", url, headers=headers)
+
+ elif argv[0] == 'create':
+ code = argv[1]
+ name = argv[2]
+ lat = argv[3]
+ lon = argv[4]
+ alt = argv[5]
+ typ = argv[6]
+
+ url = iotronic_url + "/nodes"
+ payload = {
+ "code": code,
+ "mobile": False,
+ "location": [
+ {
+ "latitude": lat,
+ "altitude": alt,
+ "longitude": lon
+ }
+ ],
+ "type": typ,
+ "name": name
+ }
+
+ response = requests.request("POST", url, data=json.dumps(payload), headers=headers)
+
+ elif argv[0] == 'delete':
+ node = argv[1]
+ url = iotronic_url + "/nodes/" + node
+ response = requests.request("DELETE", url, headers=headers)
+
+ elif argv[0] == 'show':
+ node = argv[1]
+ url = iotronic_url + "/nodes/" + node
+ response = requests.request("GET", url, headers=headers)
+
+ elif argv[0] == 'update':
+ node = argv[1]
+ values = {}
+ for opt in argv[2:]:
+ key, val = opt.split(':')
+ values[key] = val
+ url = iotronic_url + "/nodes/" + node
+ payload = values
+ response = requests.request("PATCH", url, data=json.dumps(payload), headers=headers)
+
+ else:
+ print ("node list|create|delete|show|update")
+ sys.exit()
+
+ try:
+ print(json.dumps(response.json(), indent=2))
+ except Exception:
+ pass
+
+
+def plugin_manager(argv):
+ actions = ['show', 'list', 'create', 'delete', 'update', 'inject']
+ if argv[0] not in actions or len(argv) == 0:
+ print("plugin list|create|delete|show|update|inject")
+ sys.exit()
+
+ global iotronic_url, token
+ if not token:
+ token = get_token(os.environ['OS_USERNAME'], os.environ['OS_PROJECT_NAME'], os.environ['OS_PASSWORD'])
+
+ headers = {'content-type': "application/json", 'x-auth-token': token, }
+
+ if argv[0] == 'list':
+ url = iotronic_url + "/plugins"
+ response = requests.request("GET", url, headers=headers)
+
+ elif argv[0] == 'create':
+ f=argv[1]
+ with open(f, 'r') as fil:
+ t = fil.read()
+ url = iotronic_url + "/plugins"
+ payload={"name": f, "config": t}
+ response = requests.request("POST", url, data=json.dumps(payload), headers=headers)
+
+ elif argv[0] == 'delete':
+ plugin = argv[1]
+ url = iotronic_url + "/plugins/" + plugin
+ response = requests.request("DELETE", url, headers=headers)
+
+ elif argv[0] == 'show':
+ plugin = argv[1]
+ url = iotronic_url + "/plugins/" + plugin
+ response = requests.request("GET", url, headers=headers)
+
+ elif argv[0] == 'update':
+ plugin = argv[1]
+ values = {}
+ for opt in argv[2:]:
+ key, val = opt.split(':')
+ values[key] = val
+ url = iotronic_url + "/plugins/" + plugin
+ payload = values
+ response = requests.request("PATCH", url, data=json.dumps(payload), headers=headers)
+
+ else:
+ print ("node list|create|delete|show|update")
+ sys.exit()
+
+ try:
+ print(json.dumps(response.json(), indent=2))
+ except Exception:
+ pass
+
+
+if __name__ == "__main__":
+ argv = sys.argv
+ if len(argv) <= 2:
+ print("USAGE: iotronic node|plugin [OPTIONS]")
+ sys.exit()
+
+ if argv[1] == 'node':
+ node_manager(argv[2:])
+ elif argv[1] == 'plugin':
+ plugin_manager(argv[2:])
+ else:
+ print("USAGE: iotronic node|plugin [OPTIONS]")
diff --git a/utils/zero b/utils/zero
new file mode 100644
index 0000000..85bfa59
--- /dev/null
+++ b/utils/zero
@@ -0,0 +1,14 @@
+from iotronic_lightningrod.plugins import Plugin
+from oslo_log import log as logging
+
+LOG = logging.getLogger(__name__)
+
+# User imports
+
+class Worker(Plugin.Plugin):
+ def __init__(self, name, is_running):
+ super(Worker, self).__init__(name, is_running)
+
+ def run(self):
+ LOG.info("Plugin process completed!")
+ #self.Done()
\ No newline at end of file
diff --git a/var/www/cgi-bin/iotronic/app.wsgi b/var/www/cgi-bin/iotronic/app.wsgi
index fc3a0f0..9f3ba50 100644
--- a/var/www/cgi-bin/iotronic/app.wsgi
+++ b/var/www/cgi-bin/iotronic/app.wsgi
@@ -13,11 +13,24 @@
# License for the specific language governing permissions and limitations
# under the License.
+import sys
+
+from oslo_config import cfg
+import oslo_i18n as i18n
+from oslo_log import log
+
from iotronic.api import app
from iotronic.common import service
-import oslo_i18n
-oslo_i18n.install('iotronic')
-service.prepare_service([])
+
+CONF = cfg.CONF
+
+i18n.install('iotronic')
+
+service.prepare_service(sys.argv)
+
+LOG = log.getLogger(__name__)
+LOG.debug("Configuration:")
+CONF.log_opt_values(LOG, log.DEBUG)
application = app.VersionSelectorApplication()