From 5a373e7941328679483ea344fe93ce4a7d8fbe13 Mon Sep 17 00:00:00 2001 From: Fabio Verboso Date: Thu, 6 Dec 2018 12:03:34 +0100 Subject: [PATCH] WebServices and New Wamp Agent routing Wamp RPCs are forwarding using the correct server and topic Introduced the new WebServices core feature with its REST APIs and models. Designate and Nginx are required for this new feature. Change-Id: Ia172654fbaf5502e3b9325a862a4e986fa27ee35 --- etc/iotronic/iotronic.conf | 18 +- iotronic/api/controllers/v1/__init__.py | 57 +++- iotronic/api/controllers/v1/board.py | 174 +++++++++- .../api/controllers/v1/enabledwebservice.py | 162 ++++++++++ iotronic/api/controllers/v1/utils.py | 27 ++ iotronic/api/controllers/v1/webservice.py | 277 ++++++++++++++++ iotronic/common/designate.py | 92 ++++++ iotronic/common/exception.py | 21 ++ iotronic/common/neutron.py | 2 +- iotronic/common/policy.py | 27 ++ iotronic/conductor/endpoints.py | 304 +++++++++++++++++- iotronic/conductor/rpcapi.py | 44 +++ iotronic/db/api.py | 83 +++++ .../versions/f28f3f4494b3_add_webservices.py | 61 ++++ iotronic/db/sqlalchemy/api.py | 199 +++++++++++- iotronic/db/sqlalchemy/models.py | 36 ++- iotronic/objects/__init__.py | 8 +- iotronic/objects/enabledwebservice.py | 186 +++++++++++ iotronic/objects/webservice.py | 190 +++++++++++ iotronic/wamp/agent.py | 46 +-- iotronic/wamp/functions.py | 14 + iotronic/wamp/proxies/__init__.py | 0 iotronic/wamp/proxies/nginx.py | 97 ++++++ iotronic/wamp/proxies/proxy.py | 34 ++ requirements.txt | 1 + 25 files changed, 2080 insertions(+), 80 deletions(-) create mode 100644 iotronic/api/controllers/v1/enabledwebservice.py create mode 100644 iotronic/api/controllers/v1/webservice.py create mode 100644 iotronic/common/designate.py create mode 100644 iotronic/db/sqlalchemy/alembic/versions/f28f3f4494b3_add_webservices.py create mode 100644 iotronic/objects/enabledwebservice.py create mode 100644 iotronic/objects/webservice.py create mode 100644 iotronic/wamp/proxies/__init__.py create mode 100644 iotronic/wamp/proxies/nginx.py create mode 100644 iotronic/wamp/proxies/proxy.py diff --git a/etc/iotronic/iotronic.conf b/etc/iotronic/iotronic.conf index 4012ffe..7dd9b46 100644 --- a/etc/iotronic/iotronic.conf +++ b/etc/iotronic/iotronic.conf @@ -2,7 +2,8 @@ transport_url=rabbit://:@:5672/ debug=True -verbose=False +proxy=nginx +# dns_zone=openstack.iotronic # Authentication strategy used by iotronic-api: one of @@ -40,6 +41,7 @@ password = [neutron] +auth_url = http://:35357 url = http://:9696 auth_strategy = password project_domain_name = default @@ -50,7 +52,21 @@ username = neutron password = retries = 3 project_domain_id= default + + +[designate] auth_url = http://:35357 +url = http://:9001 +auth_strategy = password +project_domain_name = default +user_domain_name = default +region_name = RegionOne +project_name = service +username = designate +password = +retries = 3 +project_domain_id= default + [cors] # Indicate whether this resource may be shared with the domain diff --git a/iotronic/api/controllers/v1/__init__.py b/iotronic/api/controllers/v1/__init__.py index c911eb3..76b5ce2 100644 --- a/iotronic/api/controllers/v1/__init__.py +++ b/iotronic/api/controllers/v1/__init__.py @@ -25,16 +25,13 @@ from wsme import types as wtypes from iotronic.api.controllers import base from iotronic.api.controllers import link +from iotronic.api.controllers.v1 import board +from iotronic.api.controllers.v1 import enabledwebservice from iotronic.api.controllers.v1 import fleet from iotronic.api.controllers.v1 import plugin from iotronic.api.controllers.v1 import port from iotronic.api.controllers.v1 import service -# from iotronic.api.controllers.v1 import driver -# from iotronic.api.controllers.v1 import portgroup -# from iotronic.api.controllers.v1 import ramdisk -# from iotronic.api.controllers.v1 import utils - -from iotronic.api.controllers.v1 import board +from iotronic.api.controllers.v1 import webservice from iotronic.api.controllers.v1 import versions from iotronic.api import expose @@ -63,16 +60,22 @@ class V1(base.APIBase): """Links to the boards resource""" plugins = [link.Link] - """Links to the boards resource""" + """Links to the plugins resource""" services = [link.Link] - """Links to the boards resource""" + """Links to the services resource""" + + enabledservices = [link.Link] + """Links to the services resource""" ports = [link.Link] - """Links to the boards resource""" + """Links to the ports resource""" fleet = [link.Link] - """Links to the boards resource""" + """Links to the fleets resource""" + + webservices = [link.Link] + """Links to the webservices resource""" @staticmethod def convert(): @@ -111,6 +114,15 @@ class V1(base.APIBase): bookmark=True) ] + v1.enabledwebservices = [ + link.Link.make_link('self', pecan.request.public_url, + 'enabledwebservices', ''), + link.Link.make_link('bookmark', + pecan.request.public_url, + 'enabledwebservices', '', + bookmark=True) + ] + v1.ports = [link.Link.make_link('self', pecan.request.public_url, 'ports', ''), link.Link.make_link('bookmark', @@ -125,6 +137,14 @@ class V1(base.APIBase): bookmark=True) ] + v1.webservices = [link.Link.make_link('self', pecan.request.public_url, + 'webservices', ''), + link.Link.make_link('bookmark', + pecan.request.public_url, + 'webservices', '', + bookmark=True) + ] + return v1 @@ -134,8 +154,10 @@ class Controller(rest.RestController): boards = board.BoardsController() plugins = plugin.PluginsController() services = service.ServicesController() + enabledwebservices = enabledwebservice.EnabledWebservicesController() ports = port.PortsController() fleets = fleet.FleetsController() + webservices = webservice.WebservicesController() @expose.expose(V1) def get(self): @@ -153,19 +175,20 @@ class Controller(rest.RestController): "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': versions.MIN_VERSION_STRING, - 'max': versions.MAX_VERSION_STRING - }, headers=headers) + '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': versions.MIN_VERSION_STRING, - 'max': versions.MAX_VERSION_STRING - }, headers=headers) + 'ver': version, + 'min': versions.MIN_VERSION_STRING, + 'max': versions.MAX_VERSION_STRING + }, headers=headers) @pecan.expose() def _route(self, args): diff --git a/iotronic/api/controllers/v1/board.py b/iotronic/api/controllers/v1/board.py index 273e2be..0473b27 100644 --- a/iotronic/api/controllers/v1/board.py +++ b/iotronic/api/controllers/v1/board.py @@ -14,9 +14,12 @@ from iotronic.api.controllers import base from iotronic.api.controllers import link from iotronic.api.controllers.v1 import collection +from iotronic.api.controllers.v1.enabledwebservice import EnabledWebservice 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.controllers.v1.webservice import Webservice +from iotronic.api.controllers.v1.webservice import WebserviceCollection from iotronic.api import expose from iotronic.common import exception from iotronic.common import policy @@ -27,11 +30,13 @@ import wsme from wsme import types as wtypes from oslo_log import log as logging -LOG = logging.getLogger(__name__) +LOG = logging.getLogger(__name__) _DEFAULT_RETURN_FIELDS = ('name', 'code', 'status', 'uuid', 'session', 'type', 'fleet') +_DEFAULT_WEBSERVICE_RETURN_FIELDS = ('name', 'uuid', 'port', 'board_uuid', + 'extra') class Board(base.APIBase): @@ -124,7 +129,6 @@ class BoardCollection(collection.Collection): class Port(base.APIBase): - board_uuid = types.uuid uuid = types.uuid VIF_name = wtypes.text @@ -146,7 +150,6 @@ class Port(base.APIBase): class PortCollection(collection.Collection): - """API representation of a collection of ports.""" ports = [Port] @@ -373,7 +376,6 @@ class BoardPluginsController(rest.RestController): class BoardServicesController(rest.RestController): - _custom_actions = { 'action': ['POST'], 'restore': ['GET'] @@ -452,6 +454,167 @@ class BoardServicesController(rest.RestController): return self._get_services_on_board_collection(rpc_board.uuid) +class BoardWebservicesController(rest.RestController): + _custom_actions = { + 'enable': ['POST'], + 'disable': ['DELETE'] + } + + invalid_sort_key_list = ['extra', ] + + def __init__(self, board_ident): + self.board_ident = board_ident + + def _get_webservices_collection(self, board, marker, limit, + sort_key, sort_dir, + fields=None): + + limit = api_utils.validate_limit(limit) + sort_dir = api_utils.validate_sort_dir(sort_dir) + + marker_obj = None + if marker: + marker_obj = objects.Webservice.get_by_uuid(pecan.request.context, + marker) + + 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}) + + filters = {} + filters['board_uuid'] = board + webservices = objects.Webservice.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 WebserviceCollection.convert_with_links(webservices, limit, + fields=fields, + **parameters) + + @expose.expose(WebserviceCollection, types.uuid, int, wtypes.text, + wtypes.text, types.listtype, types.boolean, types.boolean) + def get_all(self, marker=None, + limit=None, sort_key='id', sort_dir='asc', + fields=None): + """Retrieve a list of webservices. + + :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 with_public: Optional boolean to get also public pluings. + :param all_webservices: Optional boolean to get all the pluings. + Only for the admin + :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:webservice:get', cdict, cdict) + + rpc_board = api_utils.get_rpc_board(self.board_ident) + + filters = {} + filters['board_uuid'] = rpc_board.uuid + + if fields is None: + fields = _DEFAULT_WEBSERVICE_RETURN_FIELDS + return self._get_webservices_collection(rpc_board.uuid, marker, + limit, sort_key, sort_dir, + fields=fields) + + @expose.expose(Webservice, body=Webservice, status_code=201) + def put(self, Webservice): + """Create a new Webservice. + + :param Webservice: a Webservice within the request body. + """ + context = pecan.request.context + cdict = context.to_policy_values() + policy.authorize('iot:webservice:create', cdict, cdict) + + if not Webservice.name: + raise exception.MissingParameterValue( + ("Name is not specified.")) + + if Webservice.name: + if not api_utils.is_valid_name(Webservice.name): + msg = ("Cannot create webservice with invalid name %(name)s") + raise wsme.exc.ClientSideError(msg % {'name': Webservice.name}, + status_code=400) + + rpc_board = api_utils.get_rpc_board(self.board_ident) + + new_Webservice = objects.Webservice(pecan.request.context, + **Webservice.as_dict()) + + new_Webservice.board_uuid = rpc_board.uuid + new_Webservice = pecan.request.rpcapi.create_webservice( + pecan.request.context, + new_Webservice) + + return Webservice.convert_with_links(new_Webservice) + + class EnabledWebserverData(base.APIBase): + dns = wtypes.text + zone = wtypes.text + email = wtypes.text + + @expose.expose(EnabledWebservice, body=EnabledWebserverData, + status_code=201) + def enable(self, EnabledWebserverData): + """Create a new Webservice. + + :param Webservice: a Webservice within the request body. + """ + # context = pecan.request.context + # cdict = context.to_policy_values() + # policy.authorize('iot:webservice:create', cdict, cdict) + + if not EnabledWebserverData.dns: + raise exception.MissingParameterValue( + ("dns is not specified.")) + if not EnabledWebserverData.zone: + raise exception.MissingParameterValue( + ("zone is not specified.")) + if not EnabledWebserverData.email: + raise exception.MissingParameterValue( + ("email is not specified.")) + + rpc_board = api_utils.get_rpc_board(self.board_ident) + + new_EnWebservice = pecan.request.rpcapi.enable_webservice( + pecan.request.context, + EnabledWebserverData.dns, + EnabledWebserverData.zone, + EnabledWebserverData.email, + rpc_board.uuid) + + return EnabledWebservice.convert_with_links(new_EnWebservice) + + @expose.expose(None, status_code=204) + def disable(self): + """Disable webservices in a board. + + :param board_ident: UUID or logical name of a board. + """ + # context = pecan.request.context + # cdict = context.to_policy_values() + # policy.authorize('iot:board:delete', cdict, cdict) + + rpc_board = api_utils.get_rpc_board(self.board_ident) + pecan.request.rpcapi.disable_webservice(pecan.request.context, + rpc_board.uuid) + + class BoardPortsController(rest.RestController): def __init__(self, board_ident): @@ -486,7 +649,7 @@ class BoardPortsController(rest.RestController): rpc_board.check_if_online() - result = pecan.request.rpcapi.\ + result = pecan.request.rpcapi. \ create_port_on_board(pecan.request.context, rpc_board.uuid, Network.network, Network.subnet, Network.security_groups) @@ -539,6 +702,7 @@ class BoardsController(rest.RestController): 'plugins': BoardPluginsController, 'services': BoardServicesController, 'ports': BoardPortsController, + 'webservices': BoardWebservicesController, } invalid_sort_key_list = ['extra', 'location'] diff --git a/iotronic/api/controllers/v1/enabledwebservice.py b/iotronic/api/controllers/v1/enabledwebservice.py new file mode 100644 index 0000000..cee4d46 --- /dev/null +++ b/iotronic/api/controllers/v1/enabledwebservice.py @@ -0,0 +1,162 @@ +# 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 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 = ('board_uuid', 'http_port', 'https_port', + 'dns', 'zone', 'extra') + + +class EnabledWebservice(base.APIBase): + """API representation of a enabled_webservice. + + """ + http_port = wsme.types.IntegerType() + https_port = wsme.types.IntegerType() + board_uuid = types.uuid + dns = wsme.wsattr(wtypes.text) + zone = wsme.wsattr(wtypes.text) + extra = types.jsontype + + links = wsme.wsattr([link.Link], readonly=True) + + def __init__(self, **kwargs): + self.fields = [] + fields = list(objects.EnabledWebservice.fields) + for k in fields: + # Skip fields we do not expose. + if not hasattr(self, k): + continue + self.fields.append(k) + setattr(self, k, kwargs.get(k, wtypes.Unset)) + + @staticmethod + def _convert(enabled_webservice, fields=None): + if fields is not None: + enabled_webservice.unset_fields_except(fields) + + return enabled_webservice + + @classmethod + def convert_with_links(cls, rpc_enabled_webservice, fields=None): + enabled_webservice = EnabledWebservice( + **rpc_enabled_webservice.as_dict()) + if fields is not None: + api_utils.check_for_invalid_fields(fields, + enabled_webservice.as_dict()) + + return cls._convert(enabled_webservice, + fields=fields) + + +class EnabledWebserviceCollection(collection.Collection): + """API representation of a collection of EnabledWebservices.""" + + EnabledWebservices = [EnabledWebservice] + """A list containing EnabledWebservices objects""" + + def __init__(self, **kwargs): + self._type = 'EnabledWebservices' + + @staticmethod + def convert_with_links(EnabledWebservices, limit, url=None, fields=None, + **kwargs): + collection = EnabledWebserviceCollection() + collection.EnabledWebservices = [ + EnabledWebservice.convert_with_links(n, fields=fields) + for n in EnabledWebservices] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +class EnabledWebservicesController(rest.RestController): + """REST controller for EnabledWebservices.""" + + invalid_sort_key_list = ['extra', ] + + def _get_EnabledWebservices_collection(self, marker, limit, + sort_key, sort_dir, project_id, + fields=None): + + limit = api_utils.validate_limit(limit) + sort_dir = api_utils.validate_sort_dir(sort_dir) + + marker_obj = None + if marker: + marker_obj = objects.EnabledWebservice.get_by_id( + pecan.request.context, + marker) + + 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}) + + filters = {} + filters['project_id'] = project_id + + EnabledWebservices = objects.EnabledWebservice.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 EnabledWebserviceCollection.convert_with_links( + EnabledWebservices, limit, + fields=fields, + **parameters) + + @expose.expose(EnabledWebserviceCollection, types.uuid, int, wtypes.text, + wtypes.text, types.listtype) + def get_all(self, marker=None, + limit=None, sort_key='id', sort_dir='asc', + fields=None): + """Retrieve a list of EnabledWebservices. + + :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 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:enabledwebservice:get', cdict, cdict) + project_id = pecan.request.context.project_id + + if fields is None: + fields = _DEFAULT_RETURN_FIELDS + return self._get_EnabledWebservices_collection(marker, + limit, + sort_key, sort_dir, + project_id=project_id, + fields=fields) diff --git a/iotronic/api/controllers/v1/utils.py b/iotronic/api/controllers/v1/utils.py index ecd75a6..07bc96c 100644 --- a/iotronic/api/controllers/v1/utils.py +++ b/iotronic/api/controllers/v1/utils.py @@ -147,6 +147,33 @@ def get_rpc_service(service_ident): raise exception.ServiceNotFound(service=service_ident) +def get_rpc_webservice(webservice_ident): + """Get the RPC webservice from the webservice uuid or logical name. + + :param webservice_ident: the UUID or logical name of a webservice. + + :returns: The RPC Webservice. + :raises: InvalidUuidOrName if the name or uuid provided is not valid. + :raises: WebserviceNotFound if the webservice is not found. + """ + # Check to see if the webservice_ident is a valid UUID. If it is, treat it + # as a UUID. + if uuidutils.is_uuid_like(webservice_ident): + return objects.Webservice.get_by_uuid(pecan.request.context, + webservice_ident) + + # # We can refer to webservices by their name, if the client supports it + # # if allow_webservice_logical_names(): + # # if utils.is_hostname_safe(webservice_ident): + # else: + # return objects.Webservice.get_by_name(pecan.request.context, + # webservice_ident) + + raise exception.InvalidUuidOrName(name=webservice_ident) + + raise exception.WebserviceNotFound(webservice=webservice_ident) + + def get_rpc_port(port_ident): """Get the RPC port from the port uuid. diff --git a/iotronic/api/controllers/v1/webservice.py b/iotronic/api/controllers/v1/webservice.py new file mode 100644 index 0000000..161c6cb --- /dev/null +++ b/iotronic/api/controllers/v1/webservice.py @@ -0,0 +1,277 @@ +# 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 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', 'port', 'board_uuid', 'extra') + + +class Webservice(base.APIBase): + """API representation of a webservice. + + """ + uuid = types.uuid + name = wsme.wsattr(wtypes.text) + port = wsme.types.IntegerType() + board_uuid = types.uuid + secure = types.boolean + extra = types.jsontype + + links = wsme.wsattr([link.Link], readonly=True) + + def __init__(self, **kwargs): + self.fields = [] + fields = list(objects.Webservice.fields) + for k in fields: + # Skip fields we do not expose. + if not hasattr(self, k): + continue + self.fields.append(k) + setattr(self, k, kwargs.get(k, wtypes.Unset)) + + @staticmethod + def _convert_with_links(webservice, url, fields=None): + webservice_uuid = webservice.uuid + if fields is not None: + webservice.unset_fields_except(fields) + + webservice.links = [link.Link.make_link('self', url, 'webservices', + webservice_uuid), + link.Link.make_link('bookmark', url, 'webservices', + webservice_uuid, bookmark=True) + ] + return webservice + + @classmethod + def convert_with_links(cls, rpc_webservice, fields=None): + webservice = Webservice(**rpc_webservice.as_dict()) + if fields is not None: + api_utils.check_for_invalid_fields(fields, webservice.as_dict()) + + return cls._convert_with_links(webservice, pecan.request.public_url, + fields=fields) + + +class WebserviceCollection(collection.Collection): + """API representation of a collection of webservices.""" + + webservices = [Webservice] + """A list containing webservices objects""" + + def __init__(self, **kwargs): + self._type = 'webservices' + + @staticmethod + def convert_with_links(webservices, limit, url=None, fields=None, + **kwargs): + collection = WebserviceCollection() + collection.webservices = [Webservice.convert_with_links(n, + fields=fields) + for n in webservices] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +class WebservicesController(rest.RestController): + """REST controller for Webservices.""" + + # _subcontroller_map = { + # 'boards': WebserviceBoardsController, + # } + + invalid_sort_key_list = ['extra', ] + + _custom_actions = { + 'detail': ['GET'], + } + + # @pecan.expose() + # def _lookup(self, ident, *remainder): + # try: + # ident = types.uuid_or_name.validate(ident) + # except exception.InvalidUuidOrName as e: + # pecan.abort('400', e.args[0]) + # if not remainder: + # return + # + # subcontroller = self._subcontroller_map.get(remainder[0]) + # if subcontroller: + # return subcontroller(webservice_ident=ident), remainder[1:] + + def _get_webservices_collection(self, marker, limit, + sort_key, sort_dir, + project_id, + fields=None): + + limit = api_utils.validate_limit(limit) + sort_dir = api_utils.validate_sort_dir(sort_dir) + + marker_obj = None + if marker: + marker_obj = objects.Webservice.get_by_uuid(pecan.request.context, + marker) + + 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}) + + filters = {} + filters['project_id'] = project_id + + webservices = objects.Webservice.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 WebserviceCollection.convert_with_links(webservices, limit, + fields=fields, + **parameters) + + @expose.expose(Webservice, types.uuid_or_name, types.listtype) + def get_one(self, webservice_ident, fields=None): + """Retrieve information about the given webservice. + + :param webservice_ident: UUID or logical name of a webservice. + :param fields: Optional, a list with a specified set of fields + of the resource to be returned. + """ + + rpc_webservice = api_utils.get_rpc_webservice(webservice_ident) + cdict = pecan.request.context.to_policy_values() + policy.authorize('iot:webservice:get_one', cdict, cdict) + + return Webservice.convert_with_links(rpc_webservice, fields=fields) + + @expose.expose(WebserviceCollection, types.uuid, int, wtypes.text, + wtypes.text, types.listtype, types.boolean, types.boolean) + def get_all(self, marker=None, + limit=None, sort_key='id', sort_dir='asc', + fields=None): + """Retrieve a list of webservices. + + :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 with_public: Optional boolean to get also public pluings. + :param all_webservices: Optional boolean to get all the pluings. + Only for the admin + :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:webservice:get', cdict, cdict) + + project_id = pecan.request.context.project_id + + if fields is None: + fields = _DEFAULT_RETURN_FIELDS + return self._get_webservices_collection(marker, + limit, sort_key, sort_dir, + project_id=project_id, + fields=fields) + + @expose.expose(None, types.uuid_or_name, status_code=204) + def delete(self, webservice_ident): + """Delete a webservice. + + :param webservice_ident: UUID or logical name of a webservice. + """ + context = pecan.request.context + cdict = context.to_policy_values() + policy.authorize('iot:webservice:delete', cdict, cdict) + + rpc_webservice = api_utils.get_rpc_webservice(webservice_ident) + pecan.request.rpcapi.destroy_webservice(pecan.request.context, + rpc_webservice.uuid) + + # @expose.expose(Webservice, types.uuid_or_name, body=Webservice, + # status_code=200) + # def patch(self, webservice_ident, val_Webservice): + # """Update a webservice. + # + # :param webservice_ident: UUID or logical name of a webservice. + # :param Webservice: values to be changed + # :return updated_webservice: updated_webservice + # """ + # + # rpc_webservice = api_utils.get_rpc_webservice(webservice_ident) + # cdict = pecan.request.context.to_policy_values() + # cdict['project'] = rpc_webservice.project + # policy.authorize('iot:webservice:update', cdict, cdict) + # + # val_Webservice = val_Webservice.as_dict() + # for key in val_Webservice: + # try: + # rpc_webservice[key] = val_Webservice[key] + # except Exception: + # pass + # + # updated_webservice = pecan.request.rpcapi.update_webservice( + # pecan.request.context, rpc_webservice) + # return Webservice.convert_with_links(updated_webservice) + + @expose.expose(WebserviceCollection, types.uuid, int, wtypes.text, + wtypes.text, types.listtype, types.boolean, types.boolean) + def detail(self, marker=None, + limit=None, sort_key='id', sort_dir='asc', + fields=None, with_public=False, all_webservs=False): + """Retrieve a list of webservices. + + :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 with_public: Optional boolean to get also public webservice. + :param all_webservs: Optional boolean to get all the webservices. + Only for the admin + :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:webservice:get', cdict, cdict) + + # /detail should only work against collections + parent = pecan.request.path.split('/')[:-1][-1] + if parent != "webservices": + raise exception.HTTPNotFound() + + return self._get_webservices_collection(marker, + limit, sort_key, sort_dir, + with_public=with_public, + all_webservices=all_webservs, + fields=fields) diff --git a/iotronic/common/designate.py b/iotronic/common/designate.py new file mode 100644 index 0000000..87034c8 --- /dev/null +++ b/iotronic/common/designate.py @@ -0,0 +1,92 @@ +# 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 designateclient.v2 import client + +from keystoneauth1.identity import generic +from keystoneauth1 import session as keystone_session +from oslo_config import cfg + +CONF = cfg.CONF + +designate_opts = [ + cfg.StrOpt('url', + default='http://localhost:9696/', + help=('URL designate')), + cfg.StrOpt('retries', + default=3, + help=('retries designate')), + cfg.StrOpt('auth_strategy', + default='noauth', + help=('auth_strategy designate')), + cfg.StrOpt('username', + default='designate', + help=('designate username')), + cfg.StrOpt('password', + default='', + help=('password')), + cfg.StrOpt('project_name', + default='service', + help=('service')), + cfg.StrOpt('project_domain_name', + default='default', + help=('domain id')), + cfg.StrOpt('auth_url', + default='http://localhost:35357', + help=('auth')), + cfg.StrOpt('project_domain_id', + default='default', + help=('project domain id')), + cfg.StrOpt('user_domain_id', + default='default', + help=('user domain id')), +] + +CONF.register_opts(designate_opts, 'designate') + + +def get_client(): + auth = generic.Password( + auth_url=CONF.designate.auth_url, + username=CONF.designate.username, + password=CONF.designate.password, + project_name=CONF.designate.project_name, + project_domain_id=CONF.designate.project_domain_id, + user_domain_id=CONF.designate.user_domain_id, + ) + + session = keystone_session.Session(auth=auth) + cl = client.Client(session=session) + return cl + + +def create_record(name, ip, zone_name): + client = get_client() + zone = client.zones.get(zone_name + ".") + record = None + try: + record = client.recordsets.get(zone["id"], name + "." + zone["name"]) + except Exception: + pass + if not record: + client.recordsets.create(zone["id"], name, 'A', [ip]) + + +def delete_record(name, zone_name): + client = get_client() + zone = client.zones.get(zone_name + ".") + try: + record = client.recordsets.get(zone["id"], name + "." + zone["name"]) + except Exception: + pass + if record: + client.recordsets.delete(zone["id"], name + "." + zone["name"]) diff --git a/iotronic/common/exception.py b/iotronic/common/exception.py index 95dd78e..44e406a 100644 --- a/iotronic/common/exception.py +++ b/iotronic/common/exception.py @@ -142,6 +142,11 @@ class BoardAlreadyExists(Conflict): message = _("A board with UUID %(uuid)s already exists.") +class BoardInvalidStatus(Conflict): + message = _( + "A board with UUID %(uuid)s has a not valid state: %(status)s.") + + class MACAlreadyExists(Conflict): message = _("A port with MAC address %(mac)s already exists.") @@ -641,3 +646,19 @@ class FleetAlreadyExists(Conflict): class FleetAlreadyExposed(Conflict): message = _("A Fleet with UUID %(uuid)s already exposed.") + + +class WebserviceNotFound(NotFound): + message = _("Webservice %(Webservice)s could not be found.") + + +class WebserviceAlreadyExists(Conflict): + message = _("A Service with UUID %(uuid)s already exists.") + + +class EnabledWebserviceNotFound(NotFound): + message = _("No Webservice enabled for %(enabled_webservice)s.") + + +class EnabledWebserviceAlreadyExists(Conflict): + message = _("Already enabled for %(enabled_webservice)s.") diff --git a/iotronic/common/neutron.py b/iotronic/common/neutron.py index 57056b2..ccd6cfe 100644 --- a/iotronic/common/neutron.py +++ b/iotronic/common/neutron.py @@ -38,7 +38,7 @@ neutron_opts = [ default='neutron', help=('neutron username')), cfg.StrOpt('password', - default='0penstack', + default='', help=('password')), cfg.StrOpt('project_name', default='service', diff --git a/iotronic/common/policy.py b/iotronic/common/policy.py index 122e30d..bd9e4f9 100644 --- a/iotronic/common/policy.py +++ b/iotronic/common/policy.py @@ -181,6 +181,31 @@ fleet_policies = [ ] +webservice_policies = [ + policy.RuleDefault('iot:webservice:get', + 'rule:is_admin or rule:is_iot_member', + description='Retrieve Webservice records'), + policy.RuleDefault('iot:webservice:create', + 'rule:is_iot_member', + description='Create Webservice records'), + policy.RuleDefault('iot:webservice:get_one', 'rule:admin_or_owner', + description='Retrieve a Webservice record'), + policy.RuleDefault('iot:webservice:delete', 'rule:admin_or_owner', + description='Delete Webservice records'), + policy.RuleDefault('iot:webservice:update', 'rule:admin_or_owner', + description='Update Webservice records'), + +] + +enabledwebservice_policies = [ + policy.RuleDefault('iot:enabledwebservice:get', + 'rule:is_admin or rule:is_iot_member', + description='Retrieve EnabledWebservice records'), + policy.RuleDefault('iot:enabledwebservice:get_one', + 'rule:admin_or_owner', + description='Retrieve a EnabledWebservice record'), +] + def list_policies(): policies = (default_policies @@ -191,6 +216,8 @@ def list_policies(): + exposed_service_policies + port_on_board_policies + fleet_policies + + webservice_policies + + enabledwebservice_policies ) return policies diff --git a/iotronic/conductor/endpoints.py b/iotronic/conductor/endpoints.py index dfbe99b..763b8e2 100644 --- a/iotronic/conductor/endpoints.py +++ b/iotronic/conductor/endpoints.py @@ -21,7 +21,7 @@ except Exception: # allow iotronic api to run also with python2.7 import pickle as cpickle -from iotronic.common import exception +from iotronic.common import exception, designate from iotronic.common import neutron from iotronic.common import states from iotronic.conductor.provisioner import Provisioner @@ -49,7 +49,7 @@ def get_best_agent(ctx): def random_public_port(): - return random.randint(6000, 7000) + return random.randint(50000, 60000) def manage_result(res, wamp_rpc_call, board_uuid): @@ -68,13 +68,48 @@ def manage_result(res, wamp_rpc_call, board_uuid): return res.message +def create_record_dns_webservice(ctx, board, webs_name, board_dns, zone): + agent = objects.WampAgent.get_by_hostname(ctx, board.agent) + wsurl = agent.wsurl + ip = wsurl.split("//")[1].split(":")[0] + + LOG.debug('Create dns record %s for board %s', + webs_name + "." + board_dns + "." + zone, + board.uuid) + LOG.debug('using %s %s %s', webs_name + "." + board_dns, + ip, zone) + + designate.create_record(webs_name + "." + board_dns, ip, + zone) + + +def create_record_dns(ctx, board, board_dns, zone): + agent = objects.WampAgent.get_by_hostname(ctx, board.agent) + wsurl = agent.wsurl + ip = wsurl.split("//")[1].split(":")[0] + + LOG.debug('Create dns record %s for board %s', + board_dns + "." + zone, + board.uuid) + LOG.debug('using %s %s %s', board_dns, + ip, zone) + + designate.create_record(board_dns, ip, + zone) + + LOG.debug('Configure Web Proxy on WampAgent %s (%s) for board %s', + board.agent, ip, board.uuid) + + class ConductorEndpoint(object): def __init__(self, ragent): transport = oslo_messaging.get_transport(cfg.CONF) self.target = oslo_messaging.Target() + self.wamp_agent_client = oslo_messaging.RPCClient(transport, self.target) - self.wamp_agent_client.prepare(timeout=10) + self.wamp_agent_client = self.wamp_agent_client.prepare(timeout=120, + topic='s4t') self.ragent = ragent def echo(self, ctx, data): @@ -180,19 +215,21 @@ class ConductorEndpoint(object): wamp_rpc_call, board_uuid) board = objects.Board.get_by_uuid(ctx, board_uuid) + # session should be taken from board, not from the object - s4t_topic = 's4t_invoke_wamp' - full_topic = board.agent + '.' + s4t_topic - self.target.topic = full_topic - full_wamp_call = 'iotronic.' + board.uuid + "." + wamp_rpc_call + session = objects.SessionWP.get_session_by_board_uuid(ctx, board_uuid) + full_wamp_call = 'iotronic.' + \ + session.session_id + "." + \ + board.uuid + "." + wamp_rpc_call # check the session; it rise an excpetion if session miss if not board.is_online(): raise exception.BoardNotConnected(board=board.uuid) - res = self.wamp_agent_client.call(ctx, full_topic, - wamp_rpc_call=full_wamp_call, - data=wamp_rpc_args) + cctx = self.wamp_agent_client.prepare(server=board.agent) + res = cctx.call(ctx, 's4t_invoke_wamp', + wamp_rpc_call=full_wamp_call, + data=wamp_rpc_args) res = wm.deserialize(res) return res @@ -415,10 +452,6 @@ class ConductorEndpoint(object): Port.insert(0, port_socat) r_tcp_port = str(port_socat) - s4t_topic = 'create_tap_interface' - full_topic = str(board.agent) + '.' + s4t_topic - self.target.topic = full_topic - try: LOG.info('Creation of the VIF on the board') self.execute_on_board(ctx, board_uuid, "Create_VIF", @@ -426,9 +459,10 @@ class ConductorEndpoint(object): try: LOG.debug('starting the wamp client') - self.wamp_agent_client.call(ctx, full_topic, - port_uuid=p, - tcp_port=r_tcp_port) + cctx = self.wamp_agent_client.prepare(server=board.agent) + cctx.call(ctx, 'create_tap_interface', + port_uuid=p, + tcp_port=r_tcp_port) try: LOG.info('Updating the DB') @@ -515,3 +549,239 @@ class ConductorEndpoint(object): LOG.debug('Updating fleet %s', fleet.name) fleet.save() return serializer.serialize_entity(ctx, fleet) + + def create_webservice(self, ctx, webservice_obj): + newwbs = serializer.deserialize_entity(ctx, webservice_obj) + LOG.debug('Creating webservice %s', + newwbs.name) + + en_webservice = objects.enabledwebservice. \ + EnabledWebservice.get_by_board_uuid(ctx, + newwbs.board_uuid) + + full_zone_domain = en_webservice.dns + "." + en_webservice.zone + dns_domain = newwbs.name + "." + full_zone_domain + + board = objects.Board.get_by_uuid(ctx, newwbs.board_uuid) + + create_record_dns_webservice(ctx, board, newwbs.name, + en_webservice.dns, + en_webservice.zone) + + LOG.debug('Creating webservice with full domain %s', + dns_domain) + + list_webs = objects.Webservice.list(ctx, + filters={ + 'board_uuid': newwbs.board_uuid + }) + list_dns = full_zone_domain + "," + for webs in list_webs: + dname = webs.name + "." + full_zone_domain + "," + list_dns = list_dns + dname + + list_dns = list_dns + dns_domain + + try: + self.execute_on_board(ctx, + newwbs.board_uuid, + 'ExposeWebservice', + (full_zone_domain, + dns_domain, + newwbs.port, + list_dns,)) + except exception: + return exception + + newwbs.create() + return serializer.serialize_entity(ctx, newwbs) + + def destroy_webservice(self, ctx, webservice_id): + LOG.info('Destroying webservice with id %s', + webservice_id) + wbsrv = objects.Webservice.get_by_uuid(ctx, webservice_id) + + en_webservice = objects.enabledwebservice. \ + EnabledWebservice.get_by_board_uuid(ctx, + wbsrv.board_uuid) + + full_zone_domain = en_webservice.dns + "." + en_webservice.zone + dns_domain = wbsrv.name + "." + full_zone_domain + + list_webs = objects.Webservice.list(ctx, + filters={ + 'board_uuid': wbsrv.board_uuid + }) + + list_dns = full_zone_domain + "," + for webs in list_webs: + if webs.name != wbsrv.name: + dname = webs.name + "." + full_zone_domain + "," + list_dns = list_dns + dname + list_dns = list_dns[:-1] + + try: + # result = + self.execute_on_board(ctx, + wbsrv.board_uuid, + 'UnexposeWebservice', + (dns_domain, list_dns,)) + except exception: + return exception + + wbsrv.destroy() + designate.delete_record(wbsrv.name + "." + en_webservice.dns, + en_webservice.zone) + return + + def enable_webservice(self, ctx, dns, zone, email, board_uuid): + + board = objects.Board.get_by_uuid(ctx, board_uuid) + if board.agent == None: + raise exception.BoardInvalidStatus(uuid=board.uuid, + status=board.status) + + try: + create_record_dns(ctx, board, dns, zone) + except exception: + return exception + + try: + en_webservice = objects.enabledwebservice. \ + EnabledWebservice.get_by_board_uuid(ctx, + board.uuid) + + LOG.debug('Webservice data already exists for board %s', + board.uuid) + https_port = en_webservice.https_port + http_port = en_webservice.http_port + + except Exception: + + # TO BE CHANGED + https_port = random_public_port() + http_port = random_public_port() + + en_webservice = { + 'board_uuid': board.uuid, + 'http_port': http_port, + 'https_port': https_port, + 'dns': dns, + 'zone': zone + } + en_webservice = objects.enabledwebservice.EnabledWebservice( + ctx, **en_webservice) + + LOG.debug('Save webservice data %s for board %s', dns + "." + zone, + board.uuid) + en_webservice.create() + + LOG.debug('Open ports on WampAgent %s for http and %s for https ' + 'on board %s', http_port, https_port, board.uuid) + + service = objects.Service.get_by_name(ctx, 'webservice') + + res = self.execute_on_board(ctx, board.uuid, "ServiceEnable", + (service, http_port)) + result = manage_result(res, "ServiceEnable", board.uuid) + + exp_data = { + 'board_uuid': board_uuid, + 'service_uuid': service.uuid, + 'public_port': http_port, + } + exposed = objects.ExposedService(ctx, **exp_data) + exposed.create() + + LOG.debug(result) + + service = objects.Service.get_by_name(ctx, 'webservice_ssl') + + res = self.execute_on_board(ctx, board.uuid, "ServiceEnable", + (service, https_port)) + result = manage_result(res, "ServiceEnable", board.uuid) + + exp_data = { + 'board_uuid': board_uuid, + 'service_uuid': service.uuid, + 'public_port': https_port, + } + exposed = objects.ExposedService(ctx, **exp_data) + exposed.create() + + LOG.debug(result) + + cctx = self.wamp_agent_client.prepare(server=board.agent) + cctx.call(ctx, 'enable_webservice', board=dns, + https_port=https_port, http_port=http_port) + cctx.call(ctx, 'reload_proxy') + + LOG.debug('Configure Web Proxy on Board %s with dns %s (email: %s) ', + board.uuid, dns, email) + + try: + # result = + self.execute_on_board(ctx, + board.uuid, + 'EnableWebService', + (dns + "." + zone, email,)) + except exception: + return exception + + return serializer.serialize_entity(ctx, en_webservice) + + def disable_webservice(self, ctx, board_uuid): + LOG.info('Disabling webservice on board id %s', + board_uuid) + + board = objects.Board.get_by_uuid(ctx, board_uuid) + + if board.agent == None: + raise exception.BoardInvalidStatus(uuid=board.uuid, + status=board.status) + + en_webservice = objects.enabledwebservice. \ + EnabledWebservice.get_by_board_uuid(ctx, + board.uuid) + + https_port = en_webservice.https_port + http_port = en_webservice.http_port + + LOG.debug('Disable Webservices ports %s for http and %s for https ' + 'on board %s', http_port, https_port, board.uuid) + + service = objects.Service.get_by_name(ctx, 'webservice') + + exposed = objects.ExposedService.get(ctx, + board_uuid, + service.uuid) + + res = self.execute_on_board(ctx, board.uuid, "ServiceDisable", + (service,)) + LOG.debug(res.message) + exposed.destroy() + + service = objects.Service.get_by_name(ctx, 'webservice_ssl') + + exposed = objects.ExposedService.get(ctx, + board_uuid, + service.uuid) + res = self.execute_on_board(ctx, board.uuid, "ServiceDisable", + (service,)) + LOG.debug(res.message) + exposed.destroy() + + webservice = objects.EnabledWebservice.get_by_board_uuid( + ctx, board_uuid) + + LOG.debug('Remove dns record %s for board %s', + webservice.dns, board.uuid) + + designate.delete_record(webservice.dns, en_webservice.zone) + + cctx = self.wamp_agent_client.prepare(server=board.agent) + cctx.call(ctx, 'disable_webservice', board=webservice.dns) + cctx.call(ctx, 'reload_proxy') + + webservice.destroy() + return diff --git a/iotronic/conductor/rpcapi.py b/iotronic/conductor/rpcapi.py index c8f75b9..45f7521 100644 --- a/iotronic/conductor/rpcapi.py +++ b/iotronic/conductor/rpcapi.py @@ -351,3 +351,47 @@ class ConductorAPI(object): """ cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') return cctxt.call(context, 'update_fleet', fleet_obj=fleet_obj) + + def create_webservice(self, context, webservice_obj, topic=None): + """Add a webservice on the cloud + + :param context: request context. + :param webservice_obj: a changed (but not saved) webservice object. + :param topic: RPC topic. Defaults to self.topic. + :returns: created webservice object + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'create_webservice', + webservice_obj=webservice_obj) + + def destroy_webservice(self, context, webservice_id, topic=None): + """Delete a webservice. + + :param context: request context. + :param webservice_id: webservice id or uuid. + :raises: WebserviceLocked if webservice is locked by another conductor. + :raises: WebserviceAssociated if the webservice contains an instance + associated with it. + :raises: InvalidState if the webservice is in the wrong provision + state to perform deletion. + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'destroy_webservice', + webservice_id=webservice_id) + + def enable_webservice(self, context, dns, zone, email, board, topic=None): + """Eneble a webservice on the board + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'enable_webservice', + dns=dns, zone=zone, email=email, board_uuid=board) + + def disable_webservice(self, context, board_uuid, topic=None): + """Disable webservice manager. + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'disable_webservice', + board_uuid=board_uuid) diff --git a/iotronic/db/api.py b/iotronic/db/api.py index dc6ec74..ca8a111 100644 --- a/iotronic/db/api.py +++ b/iotronic/db/api.py @@ -626,3 +626,86 @@ class Connection(object): :raises: FleetAssociated :raises: FleetNotFound """ + + @abc.abstractmethod + def get_webservice_by_id(self, webservice_id): + """Return a webservice. + + :param webservice_id: The id of a webservice. + :returns: A webservice. + """ + + @abc.abstractmethod + def get_webservice_by_uuid(self, webservice_uuid): + """Return a webservice. + + :param webservice_uuid: The uuid of a webservice. + :returns: A webservice. + """ + + @abc.abstractmethod + def get_webservice_by_name(self, webservice_name): + """Return a webservice. + + :param webservice_name: The logical name of a webservice. + :returns: A webservice. + """ + + @abc.abstractmethod + def create_webservice(self, values): + """Create a new webservice. + + :param values: A dict containing several items used to identify + and track the webservice + :returns: A webservice. + """ + + @abc.abstractmethod + def destroy_webservice(self, webservice_id): + """Destroy a webservice and all associated interfaces. + + :param webservice_id: The id or uuid of a webservice. + """ + + @abc.abstractmethod + def update_webservice(self, webservice_id, values): + """Update properties of a webservice. + + :param webservice_id: The id or uuid of a webservice. + :param values: Dict of values to update. + :returns: A webservice. + :raises: WebserviceAssociated + :raises: WebserviceNotFound + """ + + @abc.abstractmethod + def get_enabled_webservice_by_id(self, enabled_webservice_id): + """Return a enabled_webservice. + + :param enabled_webservice_id: The id of a enabled_webservice. + :returns: A enabled_webservice. + """ + + @abc.abstractmethod + def get_enabled_webservice_by_board_uuid(self, board_uuid): + """Return a enabled_webservice. + + :param board_uuid: The uuid of a board enabled_webservice. + :returns: A enabled_webservice. + """ + + @abc.abstractmethod + def create_enabled_webservice(self, values): + """Create a new enabled_webservice. + + :param values: A dict containing several items used to identify + and track the enabled_webservice + :returns: A enabled_webservice. + """ + + @abc.abstractmethod + def destroy_enabled_webservice(self, enabled_webservice_id): + """Destroy a enabled_webservice and all associated interfaces. + + :param enabled_webservice_id: The id or uuid of a enabled_webservice. + """ diff --git a/iotronic/db/sqlalchemy/alembic/versions/f28f3f4494b3_add_webservices.py b/iotronic/db/sqlalchemy/alembic/versions/f28f3f4494b3_add_webservices.py new file mode 100644 index 0000000..26a064c --- /dev/null +++ b/iotronic/db/sqlalchemy/alembic/versions/f28f3f4494b3_add_webservices.py @@ -0,0 +1,61 @@ +# 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. + +# revision identifiers, used by Alembic. +revision = 'f28f3f4494b3' +down_revision = '9c5c34dfd9f1' + +from alembic import op +import iotronic.db.sqlalchemy.models +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + 'enabled_webservices', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('board_uuid', sa.String(length=36), nullable=True), + sa.Column('http_port', sa.Integer(), nullable=True), + sa.Column('https_port', sa.Integer(), nullable=True), + sa.Column('dns', sa.String(length=100), nullable=True), + sa.Column('zone', sa.String(length=100), nullable=True), + sa.Column('extra', + iotronic.db.sqlalchemy.models.JSONEncodedDict(), + nullable=True), + sa.ForeignKeyConstraint(['board_uuid'], ['boards.uuid'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table( + 'webservices', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.String(length=36), nullable=True), + sa.Column('port', sa.Integer(), nullable=True), + sa.Column('name', sa.String(length=45), nullable=True), + sa.Column('board_uuid', sa.String(length=36), nullable=True), + sa.Column('secure', sa.Boolean(), nullable=True), + sa.Column('extra', + iotronic.db.sqlalchemy.models.JSONEncodedDict(), + nullable=True), + sa.ForeignKeyConstraint(['board_uuid'], ['boards.uuid'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('uuid', + name='uniq_enabled_webservices0uuid'), + sa.UniqueConstraint('port', 'board_uuid', + name='uniq_webservices_port_and_board'), + sa.UniqueConstraint('board_uuid', 'port', 'name', + name='uniq_webservices_on_board') + ) diff --git a/iotronic/db/sqlalchemy/api.py b/iotronic/db/sqlalchemy/api.py index 068a71f..4b0c981 100644 --- a/iotronic/db/sqlalchemy/api.py +++ b/iotronic/db/sqlalchemy/api.py @@ -163,6 +163,35 @@ class Connection(api.Connection): query = query.filter(models.Plugin.owner == filters['owner']) return query + def _add_enabled_webservices_filters(self, query, filters): + if filters is None: + filters = [] + + if 'project_id' in filters: + query = query.join(models.Board, + models.EnabledWebservice.board_uuid == + models.Board.uuid) + query = query.filter( + models.Board.project == filters['project_id']) + + return query + + def _add_webservices_filters(self, query, filters): + # if filters is None: + # filters = [] + if 'project_id' in filters: + query = query.join(models.Board, + models.Webservice.board_uuid == + models.Board.uuid) + query = query.filter( + models.Board.project == filters['project_id']) + + if 'board_uuid' in filters: + query = query.filter( + models.Webservice.board_uuid == filters['board_uuid']) + + return query + def _add_fleets_filters(self, query, filters): if filters is None: filters = [] @@ -194,21 +223,8 @@ class Connection(api.Connection): filters = [] if 'board_uuid' in filters: - query = query.\ + query = query. \ filter(models.Port.board_uuid == filters['board_uuid']) -# if 'uuid' in filters: -# query = query.filter(models.Port.uuid == filters['uuid']) -# if 'with_public' in filters and filters['with_public']: -# query = query.filter( -# or_( -# models.Port.owner == filters['owner'], -# models.Port.public == 1) -# ) -# else: -# query = query.filter(models.Port.owner == filters['owner']) -# elif 'public' in filters and filters['public']: -# query = query.filter(models.Port.public == 1) - print (str(query)) def _do_update_board(self, board_id, values): session = get_session() @@ -325,8 +341,8 @@ class Connection(api.Connection): except NoResultFound: raise exception.BoardNotFound(board=board_code) -# def get_board_by_port_uuid(self, port_uuid): -# query = model_query(models.Port).filter_by(uuid=port_uuid) + # def get_board_by_port_uuid(self, port_uuid): + # query = model_query(models.Port).filter_by(uuid=port_uuid) def destroy_board(self, board_id): session = get_session() @@ -938,7 +954,7 @@ class Connection(api.Connection): if count == 0: raise exception.PortNotFound(uuid=uuid) -# FLEET api + # FLEET api def get_fleet_by_id(self, fleet_id): query = model_query(models.Fleet).filter_by(id=fleet_id) @@ -1026,3 +1042,152 @@ class Connection(api.Connection): ref.update(values) return ref + + # WEBSERVICE api + + def get_webservice_by_id(self, webservice_id): + query = model_query(models.Webservice).filter_by(id=webservice_id) + try: + return query.one() + except NoResultFound: + raise exception.WebserviceNotFound(webservice=webservice_id) + + def get_webservice_by_uuid(self, webservice_uuid): + query = model_query(models.Webservice).filter_by(uuid=webservice_uuid) + try: + return query.one() + except NoResultFound: + raise exception.WebserviceNotFound(webservice=webservice_uuid) + + def get_webservice_by_name(self, webservice_name): + query = model_query(models.Webservice).filter_by(name=webservice_name) + try: + return query.one() + except NoResultFound: + raise exception.WebserviceNotFound(webservice=webservice_name) + + def destroy_webservice(self, webservice_id): + + session = get_session() + with session.begin(): + query = model_query(models.Webservice, session=session) + query = add_identity_filter(query, webservice_id) + try: + webservice_ref = query.one() + except NoResultFound: + raise exception.WebserviceNotFound(webservice=webservice_id) + + # Get webservice ID, if an UUID was supplied. The ID is + # required for deleting all ports, attached to the webservice. + if uuidutils.is_uuid_like(webservice_id): + webservice_id = webservice_ref['id'] + + query.delete() + + def update_webservice(self, webservice_id, values): + # NOTE(dtantsur): this can lead to very strange errors + if 'uuid' in values: + msg = _("Cannot overwrite UUID for an existing Webservice.") + raise exception.InvalidParameterValue(err=msg) + + try: + return self._do_update_webservice(webservice_id, values) + except db_exc.DBDuplicateEntry as e: + if 'name' in e.columns: + raise exception.DuplicateName(name=values['name']) + elif 'uuid' in e.columns: + raise exception.WebserviceAlreadyExists(uuid=values['uuid']) + else: + raise e + + def create_webservice(self, values): + # ensure defaults are present for new webservices + if 'uuid' not in values: + values['uuid'] = uuidutils.generate_uuid() + webservice = models.Webservice() + webservice.update(values) + try: + webservice.save() + except db_exc.DBDuplicateEntry: + raise exception.WebserviceAlreadyExists(uuid=values['uuid']) + return webservice + + def get_webservice_list(self, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.Webservice) + query = self._add_webservices_filters(query, filters) + return _paginate_query(models.Webservice, limit, marker, + sort_key, sort_dir, query) + + def _do_update_webservice(self, webservice_id, values): + session = get_session() + with session.begin(): + query = model_query(models.Webservice, session=session) + query = add_identity_filter(query, webservice_id) + try: + ref = query.with_lockmode('update').one() + except NoResultFound: + raise exception.WebserviceNotFound(webservice=webservice_id) + + ref.update(values) + return ref + + # ENABLED_WEBSERIVCE api + + def get_enabled_webservice_by_id(self, enabled_webservice_id): + query = model_query(models.EnabledWebservice).filter_by( + id=enabled_webservice_id) + try: + return query.one() + except NoResultFound: + raise exception.EnabledWebserviceNotFound( + enabled_webservice=enabled_webservice_id) + + def get_enabled_webservice_by_board_uuid(self, board_uuid): + query = model_query(models.EnabledWebservice).filter_by( + board_uuid=board_uuid) + try: + return query.one() + except NoResultFound: + raise exception.EnabledWebserviceNotFound( + enabled_webservice=board_uuid) + + def destroy_enabled_webservice(self, enabled_webservice_id): + + session = get_session() + with session.begin(): + query = model_query(models.EnabledWebservice, session=session) + query = add_identity_filter(query, enabled_webservice_id) + try: + enabled_webservice_ref = query.one() + except NoResultFound: + raise exception.EnabledWebserviceNotFound( + enabled_webservice=enabled_webservice_id) + + # Get enabled_webservice ID, if an UUID was supplied. The ID is + # required for deleting all ports, attached to the enabled_ + # webservice. + if uuidutils.is_uuid_like(enabled_webservice_id): + enabled_webservice_id = enabled_webservice_ref['id'] + + query.delete() + + def create_enabled_webservice(self, values): + # ensure defaults are present for new enabled_webservices + if 'uuid' not in values: + values['uuid'] = uuidutils.generate_uuid() + enabled_webservice = models.EnabledWebservice() + enabled_webservice.update(values) + try: + enabled_webservice.save() + except db_exc.DBDuplicateEntry: + raise exception.EnabledWebserviceAlreadyExists(uuid=values['uuid']) + return enabled_webservice + + def get_enabled_webservice_list(self, filters=None, limit=None, + marker=None, + sort_key=None, sort_dir=None): + query = model_query(models.EnabledWebservice) + query = self._add_enabled_webservices_filters(query, filters) + return _paginate_query(models.EnabledWebservice, limit, marker, + sort_key, sort_dir, query) diff --git a/iotronic/db/sqlalchemy/models.py b/iotronic/db/sqlalchemy/models.py index d585a12..01d2ced 100644 --- a/iotronic/db/sqlalchemy/models.py +++ b/iotronic/db/sqlalchemy/models.py @@ -1,7 +1,4 @@ # -*- encoding: utf-8 -*- -# -# Copyright 2013 Hewlett-Packard Development Company, L.P. -# # 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 @@ -282,3 +279,36 @@ class Fleet(Base): project = Column(String(36)) description = Column(String(300)) extra = Column(JSONEncodedDict) + + +class Webservice(Base): + """Represents a webservices.""" + + __tablename__ = 'webservices' + __table_args__ = ( + schema.UniqueConstraint('uuid', name='uniq_webservices0uuid'), + schema.UniqueConstraint('board_uuid', 'port', 'name', + name='uniq_webservices_on_board'), + schema.UniqueConstraint('port', 'board_uuid', + name='uniq_webservices_port_and_board'), + table_args()) + id = Column(Integer, primary_key=True) + uuid = Column(String(36)) + port = Column(Integer) + name = Column(String(45)) + board_uuid = Column(String(36), ForeignKey('boards.uuid'), nullable=True) + secure = Column(Boolean, default=True) + extra = Column(JSONEncodedDict) + + +class EnabledWebservice(Base): + """The boards in which webservices are enabled.""" + + __tablename__ = 'enabled_webservices' + id = Column(Integer, primary_key=True) + board_uuid = Column(String(36), ForeignKey('boards.uuid'), nullable=True) + http_port = Column(Integer) + https_port = Column(Integer) + dns = Column(String(100)) + zone = Column(String(100)) + extra = Column(JSONEncodedDict) diff --git a/iotronic/objects/__init__.py b/iotronic/objects/__init__.py index 499a4f6..3316002 100644 --- a/iotronic/objects/__init__.py +++ b/iotronic/objects/__init__.py @@ -14,6 +14,7 @@ from iotronic.objects import board from iotronic.objects import conductor +from iotronic.objects import enabledwebservice from iotronic.objects import exposedservice from iotronic.objects import fleet from iotronic.objects import injectionplugin @@ -23,6 +24,7 @@ from iotronic.objects import port from iotronic.objects import service from iotronic.objects import sessionwp from iotronic.objects import wampagent +from iotronic.objects import webservice Conductor = conductor.Conductor Board = board.Board @@ -33,8 +35,10 @@ ExposedService = exposedservice.ExposedService SessionWP = sessionwp.SessionWP WampAgent = wampagent.WampAgent Service = service.Service +Webservice = webservice.Webservice Port = port.Port Fleet = fleet.Fleet +EnabledWebservice = enabledwebservice.EnabledWebservice __all__ = ( Conductor, @@ -47,5 +51,7 @@ __all__ = ( InjectionPlugin, ExposedService, Port, - Fleet + Fleet, + Webservice, + EnabledWebservice ) diff --git a/iotronic/objects/enabledwebservice.py b/iotronic/objects/enabledwebservice.py new file mode 100644 index 0000000..80233cd --- /dev/null +++ b/iotronic/objects/enabledwebservice.py @@ -0,0 +1,186 @@ +# coding=utf-8 +# +# +# 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_utils import strutils +from oslo_utils import uuidutils + +from iotronic.common import exception +from iotronic.db import api as db_api +from iotronic.objects import base +from iotronic.objects import utils as obj_utils + + +class EnabledWebservice(base.IotronicObject): + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = { + 'id': int, + 'board_uuid': obj_utils.str_or_none, + 'http_port': int, + 'https_port': int, + 'dns': obj_utils.str_or_none, + 'zone': obj_utils.str_or_none, + 'extra': obj_utils.dict_or_none, + } + + @staticmethod + def _from_db_object(enabled_webservice, db_enabled_webservice): + """Converts a database entity to a formal object.""" + for field in enabled_webservice.fields: + enabled_webservice[field] = db_enabled_webservice[field] + enabled_webservice.obj_reset_changes() + return enabled_webservice + + @base.remotable_classmethod + def get(cls, context, enabled_webservice_id): + """Find a enabled_webservice based on its id or uuid and return a + Board object. + + :param enabled_webservice_id: the id *or* uuid of a enabled_webservice. + :returns: a :class:`Board` object. + """ + if strutils.is_int_like(enabled_webservice_id): + return cls.get_by_id(context, enabled_webservice_id) + elif uuidutils.is_uuid_like(enabled_webservice_id): + return cls.get_by_uuid(context, enabled_webservice_id) + else: + raise exception.InvalidIdentity(identity=enabled_webservice_id) + + @base.remotable_classmethod + def get_by_id(cls, context, enabled_webservice_id): + """Find a enabled_webservice based on its integer id and return a + Board object. + + :param enabled_webservice_id: the id of a enabled_webservice. + :returns: a :class:`Board` object. + """ + db_enabled_webservice = cls.dbapi.get_enabled_webservice_by_id( + enabled_webservice_id) + en_webserv = EnabledWebservice._from_db_object(cls(context), + db_enabled_webservice) + return en_webserv + + @base.remotable_classmethod + def get_by_board_uuid(cls, context, uuid): + """Find a enabled_webservice based on uuid and return a Board object. + + :param uuid: the uuid of a enabled_webservice. + :returns: a :class:`Board` object. + """ + db_enabled_webservice = cls.dbapi.get_enabled_webservice_by_board_uuid( + uuid) + en_webserv = EnabledWebservice._from_db_object(cls(context), + db_enabled_webservice) + return en_webserv + + @base.remotable_classmethod + def list(cls, context, limit=None, marker=None, sort_key=None, + sort_dir=None, filters=None): + """Return a list of EnabledWebservice objects. + + :param context: Security context. + :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". + :param filters: Filters to apply. + :returns: a list of :class:`EnabledWebservice` object. + + """ + db_enabled_webservices = cls.dbapi.get_enabled_webservice_list( + filters=filters, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return [EnabledWebservice._from_db_object(cls(context), obj) + for obj in db_enabled_webservices] + + @base.remotable + def create(self, context=None): + """Create a EnabledWebservice record in the DB. + + Column-wise updates will be made based on the result of + self.what_changed(). If target_power_state is provided, + it will be checked against the in-database copy of the + enabled_webservice before updates are made. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: EnabledWebservice(context) + + """ + + values = self.obj_get_changes() + db_enabled_webservice = self.dbapi.create_enabled_webservice(values) + self._from_db_object(self, db_enabled_webservice) + + @base.remotable + def destroy(self, context=None): + """Delete the EnabledWebservice from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: EnabledWebservice(context) + """ + self.dbapi.destroy_enabled_webservice(self.id) + self.obj_reset_changes() + + @base.remotable + def save(self, context=None): + """Save updates to this EnabledWebservice. + + Column-wise updates will be made based on the result of + self.what_changed(). If target_power_state is provided, + it will be checked against the in-database copy of the + enabled_webservice before updates are made. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: EnabledWebservice(context) + """ + updates = self.obj_get_changes() + self.dbapi.update_enabled_webservice(self.uuid, updates) + self.obj_reset_changes() + + @base.remotable + def refresh(self, context=None): + """Refresh the object by re-fetching from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: EnabledWebservice(context) + """ + current = self.__class__.get_by_uuid(self._context, self.uuid) + for field in self.fields: + if (hasattr( + self, base.get_attrname(field)) + and self[field] != current[field]): + self[field] = current[field] diff --git a/iotronic/objects/webservice.py b/iotronic/objects/webservice.py new file mode 100644 index 0000000..4c33aba --- /dev/null +++ b/iotronic/objects/webservice.py @@ -0,0 +1,190 @@ +# coding=utf-8 +# +# +# 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_utils import strutils +from oslo_utils import uuidutils + +from iotronic.common import exception +from iotronic.db import api as db_api +from iotronic.objects import base +from iotronic.objects import utils as obj_utils + + +class Webservice(base.IotronicObject): + # Version 1.0: Initial version + VERSION = '1.0' + + dbapi = db_api.get_instance() + + fields = { + 'id': int, + 'uuid': obj_utils.str_or_none, + 'port': int, + 'name': obj_utils.str_or_none, + 'board_uuid': obj_utils.str_or_none, + 'secure': bool, + 'extra': obj_utils.dict_or_none, + } + + @staticmethod + def _from_db_object(webservice, db_webservice): + """Converts a database entity to a formal object.""" + for field in webservice.fields: + webservice[field] = db_webservice[field] + webservice.obj_reset_changes() + return webservice + + @base.remotable_classmethod + def get(cls, context, webservice_id): + """Find a webservice based on its id or uuid and return a Board object. + + :param webservice_id: the id *or* uuid of a webservice. + :returns: a :class:`Board` object. + """ + if strutils.is_int_like(webservice_id): + return cls.get_by_id(context, webservice_id) + elif uuidutils.is_uuid_like(webservice_id): + return cls.get_by_uuid(context, webservice_id) + else: + raise exception.InvalidIdentity(identity=webservice_id) + + @base.remotable_classmethod + def get_by_id(cls, context, webservice_id): + """Find a webservice based on its integer id and return a Board object. + + :param webservice_id: the id of a webservice. + :returns: a :class:`Board` object. + """ + db_webservice = cls.dbapi.get_webservice_by_id(webservice_id) + webservice = Webservice._from_db_object(cls(context), db_webservice) + return webservice + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + """Find a webservice based on uuid and return a Board object. + + :param uuid: the uuid of a webservice. + :returns: a :class:`Board` object. + """ + db_webservice = cls.dbapi.get_webservice_by_uuid(uuid) + webservice = Webservice._from_db_object(cls(context), db_webservice) + return webservice + + @base.remotable_classmethod + def get_by_name(cls, context, name): + """Find a webservice based on name and return a Board object. + + :param name: the logical name of a webservice. + :returns: a :class:`Board` object. + """ + db_webservice = cls.dbapi.get_webservice_by_name(name) + webservice = Webservice._from_db_object(cls(context), db_webservice) + return webservice + + @base.remotable_classmethod + def list(cls, context, limit=None, marker=None, sort_key=None, + sort_dir=None, filters=None): + """Return a list of Webservice objects. + + :param context: Security context. + :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". + :param filters: Filters to apply. + :returns: a list of :class:`Webservice` object. + + """ + db_webservices = cls.dbapi.get_webservice_list(filters=filters, + limit=limit, + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) + return [Webservice._from_db_object(cls(context), obj) + for obj in db_webservices] + + @base.remotable + def create(self, context=None): + """Create a Webservice record in the DB. + + Column-wise updates will be made based on the result of + self.what_changed(). If target_power_state is provided, + it will be checked against the in-database copy of the + webservice before updates are made. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Webservice(context) + + """ + + values = self.obj_get_changes() + db_webservice = self.dbapi.create_webservice(values) + self._from_db_object(self, db_webservice) + + @base.remotable + def destroy(self, context=None): + """Delete the Webservice from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Webservice(context) + """ + self.dbapi.destroy_webservice(self.uuid) + self.obj_reset_changes() + + @base.remotable + def save(self, context=None): + """Save updates to this Webservice. + + Column-wise updates will be made based on the result of + self.what_changed(). If target_power_state is provided, + it will be checked against the in-database copy of the + webservice before updates are made. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Webservice(context) + """ + updates = self.obj_get_changes() + self.dbapi.update_webservice(self.uuid, updates) + self.obj_reset_changes() + + @base.remotable + def refresh(self, context=None): + """Refresh the object by re-fetching from the DB. + + :param context: Security context. NOTE: This should only + be used internally by the indirection_api. + Unfortunately, RPC requires context as the first + argument, even though we don't use it. + A context should be set when instantiating the + object, e.g.: Webservice(context) + """ + current = self.__class__.get_by_uuid(self._context, self.uuid) + for field in self.fields: + if (hasattr( + self, base.get_attrname(field)) + and self[field] != current[field]): + self[field] = current[field] diff --git a/iotronic/wamp/agent.py b/iotronic/wamp/agent.py index 8ef6318..3d443f9 100644 --- a/iotronic/wamp/agent.py +++ b/iotronic/wamp/agent.py @@ -27,6 +27,7 @@ from oslo_log import log as logging import oslo_messaging from oslo_messaging.rpc import dispatcher +import importlib from threading import Thread import ssl @@ -74,8 +75,15 @@ wamp_opts = [ ] +proxy_opts = [ + cfg.StrOpt('proxy', + choices=[('nginx', _('nginx proxy')), ], + help=_('Proxy for webservices')), +] + CONF = cfg.CONF cfg.CONF.register_opts(service_opts) +cfg.CONF.register_opts(proxy_opts) CONF.register_opts(wamp_opts, 'wamp') txaio.start_logging(level="info") @@ -94,11 +102,6 @@ async def wamp_request(kwarg): # OSLO ENDPOINT class WampEndpoint(object): - def __init__(self, agent_uuid): - setattr(self, agent_uuid + '.s4t_invoke_wamp', self.s4t_invoke_wamp) - - setattr(self, agent_uuid + '.create_tap_interface', - self.create_tap_interface) def s4t_invoke_wamp(self, ctx, **kwarg): LOG.debug("CONDUCTOR sent me: " + kwarg['wamp_rpc_call']) @@ -107,6 +110,14 @@ class WampEndpoint(object): return r.result() + +class AgentEndpoint(object): + + # used for testing + def echo(self, ctx, text): + LOG.debug("ECHO of " + text) + return text + def create_tap_interface(self, ctx, port_uuid, tcp_port): time.sleep(12) LOG.debug('Creating tap interface on the wamp agent host') @@ -121,18 +132,20 @@ class WampEndpoint(object): class RPCServer(Thread): def __init__(self): # AMQP CONFIG + + proxy = importlib.import_module("iotronic.wamp.proxies." + CONF.proxy) + endpoints = [ - WampEndpoint(AGENT_HOST), + WampEndpoint(), + AgentEndpoint(), + proxy.ProxyManager() ] Thread.__init__(self) transport = oslo_messaging.get_transport(CONF) - target = oslo_messaging.Target(topic=AGENT_HOST + '.s4t_invoke_wamp', - server='server1') - target1 = oslo_messaging.Target(topic=AGENT_HOST + - '.create_tap_interface', - server='server1') + target = oslo_messaging.Target(topic='s4t', + server=AGENT_HOST) access_policy = dispatcher.DefaultRPCAccessPolicy self.server = oslo_messaging.get_rpc_server( @@ -140,20 +153,13 @@ class RPCServer(Thread): endpoints, executor='threading', access_policy=access_policy) - self.server1 = oslo_messaging.get_rpc_server( - transport, target1, - endpoints, executor='threading', - access_policy=access_policy) - def run(self): LOG.info("Starting AMQP server... ") self.server.start() - self.server1.start() def stop(self): LOG.info("Stopping AMQP server... ") self.server.stop() - self.server1.stop() LOG.info("AMQP server stopped. ") @@ -223,6 +229,10 @@ class WampManager(object): AGENT_HOST + u'.stack4things.connection') session.register(fun.echo, AGENT_HOST + u'.stack4things.echo') + session.register(fun.alive, + AGENT_HOST + u'.stack4things.alive') + session.register(fun.wamp_alive, + AGENT_HOST + u'.stack4things.wamp_alive') LOG.debug("procedure registered") except Exception as e: diff --git a/iotronic/wamp/functions.py b/iotronic/wamp/functions.py index e716374..b0415d1 100644 --- a/iotronic/wamp/functions.py +++ b/iotronic/wamp/functions.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from datetime import datetime from iotronic.common import rpc from iotronic.common import states from iotronic.conductor import rpcapi @@ -45,6 +46,19 @@ def echo(data): return data +def wamp_alive(board_uuid, board_name): + LOG.debug("Alive board: %s (%s)", board_uuid, board_name) + return "Iotronic alive @ " + datetime.now().strftime( + '%Y-%m-%dT%H:%M:%S.%f') + + +# to be removed +def alive(): + LOG.debug("Alive") + return "Iotronic alive @ " + datetime.now().strftime( + '%Y-%m-%dT%H:%M:%S.%f') + + def update_sessions(session_list): session_list = set(session_list) list_from_db = objects.SessionWP.valid_list(ctxt) diff --git a/iotronic/wamp/proxies/__init__.py b/iotronic/wamp/proxies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic/wamp/proxies/nginx.py b/iotronic/wamp/proxies/nginx.py new file mode 100644 index 0000000..6ec71bf --- /dev/null +++ b/iotronic/wamp/proxies/nginx.py @@ -0,0 +1,97 @@ +# Copyright 2017 MDSLAB - University of Messina +# 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 iotronic.wamp.proxies.proxy import Proxy +from oslo_config import cfg +from oslo_log import log as logging +from subprocess import call + +LOG = logging.getLogger(__name__) + +nginx_opts = [ + cfg.StrOpt('nginx_path', + default='/etc/nginx/conf.d/iotronic', + help=('Default Nginx Path')), + cfg.StrOpt('dns_zone', + default='openstack.iotronic', + help=('Default zone')), +] + +CONF = cfg.CONF +CONF.register_opts(nginx_opts, 'nginx') + + +def save_map(board, dns): + fp = CONF.nginx.nginx_path + "/maps/map_" + board + with open(fp, "w") as text_file: + text_file.write("~" + board + "." + dns + " " + board + ";") + + +def save_upstream(board, https_port): + fp = CONF.nginx.nginx_path + "/upstreams/upstream_" + board + string = '''upstream {0} {{ + server localhost:{1} max_fails=3 fail_timeout=10s; + }} + '''.format(board, https_port) + + with open(fp, "w") as text_file: + text_file.write("%s" % string) + + +def save_server(board, http_port, dns): + fp = CONF.nginx.nginx_path + "/servers/" + board + string = '''server {{ + listen 80; + server_name .{0}.{2}; + + location / {{ + proxy_pass http://localhost:{1}; + }} + }} + '''.format(board, http_port, dns) + + with open(fp, "w") as text_file: + text_file.write("%s" % string) + + +def remove(board): + call(["rm", + CONF.nginx.nginx_path + "/servers/" + board, + CONF.nginx.nginx_path + "/upstreams/upstream_" + board, + CONF.nginx.nginx_path + "/maps/map_" + board + ]) + + +class ProxyManager(Proxy): + + def __init__(self): + self.dns = CONF.nginx.dns_zone + super(ProxyManager, self).__init__("nginx") + + def reload_proxy(self, ctx): + call(["nginx", "-s", "reload"]) + + def enable_webservice(self, ctx, board, https_port, http_port): + LOG.debug( + 'Enabling WebService with ports %s for http and %s for https ' + 'on board %s', http_port, https_port, board) + save_map(board, self.dns) + save_upstream(board, https_port) + save_server(board, http_port, self.dns) + + def disable_webservice(self, ctx, board): + LOG.debug('Disabling WebService on board %s', + board) + remove(board) diff --git a/iotronic/wamp/proxies/proxy.py b/iotronic/wamp/proxies/proxy.py new file mode 100644 index 0000000..11fed16 --- /dev/null +++ b/iotronic/wamp/proxies/proxy.py @@ -0,0 +1,34 @@ +# Copyright 2011 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc +import six + +from oslo_log import log as logging + +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class Proxy(object): + """Base class for proxies supported by Iotornic. + + """ + + def __init__(self, proxy_type): + self.proxy_type = proxy_type + + # def enable_webservice(self,board,https_port,http_port): + # pass diff --git a/requirements.txt b/requirements.txt index dccc01b..98404bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT keystonemiddleware>=4.17.0 # Apache-2.0 autobahn>=0.10.1 # MIT License python-neutronclient>=6.7.0 # Apache-2.0 +python-designateclient>=2.11.0 # Apache-2.0 pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD PyMySQL>=0.7.6 # MIT License osprofiler>=1.5.0 # Apache-2.0