new API dev, new policy, keystone integration

Change-Id: Id90353f5d31e848f16f36fd44935459cd8b27bb7
This commit is contained in:
Fabio Verboso 2017-03-03 18:22:09 +01:00
parent f5a222f2e0
commit 3964168d1e
32 changed files with 1246 additions and 574 deletions

View File

@ -1,5 +1,2 @@
{
"admin_api": "role:admin or role:administrator",
"show_password": "!",
"default": "rule:admin_api"
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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,
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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',)

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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()

View File

@ -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))

View 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)

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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']

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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.

View File

@ -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:

View File

@ -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)

View File

@ -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]

View File

@ -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,

View File

@ -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]

View File

@ -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)

View File

@ -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
View 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()

View File

@ -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()