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