new API dev, new policy, keystone integration
Change-Id: Id90353f5d31e848f16f36fd44935459cd8b27bb7
This commit is contained in:
parent
f5a222f2e0
commit
3964168d1e
@ -1,5 +1,2 @@
|
||||
{
|
||||
"admin_api": "role:admin or role:administrator",
|
||||
"show_password": "!",
|
||||
"default": "rule:admin_api"
|
||||
}
|
||||
|
@ -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)
|
@ -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)
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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',)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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))
|
||||
|
25
iotronic/api/controllers/v1/versions.py
Normal file
25
iotronic/api/controllers/v1/versions.py
Normal file
@ -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)
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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('<error_message>'
|
||||
+ '\n'.join(app_iter)
|
||||
+ '</error_message>'))]
|
||||
et.ElementTree.fromstring('<error_message>'
|
||||
+ '\n'.join(app_iter)
|
||||
+ '</error_message>'))]
|
||||
except et.ElementTree.ParseError as err:
|
||||
LOG.error(_LE('Error parsing HTTP response: %s'), err)
|
||||
body = ['<error_message>%s' % state['status_code']
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
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)
|
@ -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]")
|
||||
|
14
utils/zero
Normal file
14
utils/zero
Normal file
@ -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()
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user