diff --git a/iotronic/api/controllers/v1/board.py b/iotronic/api/controllers/v1/board.py index a4a6561..730ec2e 100644 --- a/iotronic/api/controllers/v1/board.py +++ b/iotronic/api/controllers/v1/board.py @@ -416,7 +416,8 @@ class BoardPluginsController(rest.RestController): class BoardServicesController(rest.RestController): _custom_actions = { 'action': ['POST'], - 'restore': ['GET'] + 'restore': ['GET'], + 'status': ['GET'] } def __init__(self, board_ident): @@ -462,6 +463,7 @@ class BoardServicesController(rest.RestController): except exception: return exception + # add here the check on force parameter rpc_board.check_if_online() result = pecan.request.rpcapi.action_service(pecan.request.context, @@ -491,11 +493,33 @@ class BoardServicesController(rest.RestController): return self._get_services_on_board_collection(rpc_board.uuid) + @expose.expose(wtypes.text, status_code=200) + def status(self): + """Get status of all services 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) + + rpc_board.check_if_online() + + result = pecan.request.rpcapi.status_services_on_board( + pecan.request.context, + rpc_board.uuid + ) + + return result + class BoardWebservicesController(rest.RestController): _custom_actions = { 'enable': ['POST'], - 'disable': ['DELETE'] + 'disable': ['DELETE'], + 'renew': ['GET'] } invalid_sort_key_list = ['extra', ] @@ -575,6 +599,7 @@ class BoardWebservicesController(rest.RestController): :param Webservice: a Webservice within the request body. """ + context = pecan.request.context cdict = context.to_policy_values() policy.authorize('iot:webservice:create', cdict, cdict) @@ -652,6 +677,23 @@ class BoardWebservicesController(rest.RestController): pecan.request.rpcapi.disable_webservice(pecan.request.context, rpc_board.uuid) + @expose.expose(None) + def renew(self): + """Renew webservices certificates 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) + + rpc_board.check_if_online() + + pecan.request.rpcapi.renew_webservice(pecan.request.context, + rpc_board.uuid) + class BoardPortsController(rest.RestController): diff --git a/iotronic/api/controllers/v1/location.py b/iotronic/api/controllers/v1/location.py index dea46b6..8f4bc76 100644 --- a/iotronic/api/controllers/v1/location.py +++ b/iotronic/api/controllers/v1/location.py @@ -31,7 +31,7 @@ class Location(base.APIBase): fields = list(objects.Location.fields) for k in fields: # Skip fields we do not expose. - if k != 'created_at': + if k is not 'created_at': if not hasattr(self, k): continue self.fields.append(k) diff --git a/iotronic/api/controllers/v1/types.py b/iotronic/api/controllers/v1/types.py index fa23de1..30f7254 100644 --- a/iotronic/api/controllers/v1/types.py +++ b/iotronic/api/controllers/v1/types.py @@ -327,7 +327,6 @@ class LocalLinkConnectionType(wtypes.UserType): return None return LocalLinkConnectionType.validate(value) - locallinkconnectiontype = LocalLinkConnectionType() diff --git a/iotronic/common/exception.py b/iotronic/common/exception.py index 0880005..db45c64 100644 --- a/iotronic/common/exception.py +++ b/iotronic/common/exception.py @@ -260,6 +260,10 @@ class BoardAssociated(InvalidState): message = _("Board %(board)s is associated with instance %(instance)s.") +class BoardNameAlreadyExists(Conflict): + message = _("A board with %(name)s is already associated to another board.") + + class PortNotFound(NotFound): message = _("Port %(port)s could not be found.") @@ -665,8 +669,12 @@ class WebserviceAlreadyExists(Conflict): class EnabledWebserviceNotFound(NotFound): - message = _("No Webservice enabled for %(enabled_webservice)s.") + message = _("Webservice module not enabled for %(enabled_webservice)s device.") class EnabledWebserviceAlreadyExists(Conflict): - message = _("Already enabled for %(enabled_webservice)s.") + message = _("Already enabled for %(enabled_webservice)s [req: %(req)s].") + + +class DnsWebserviceAlreadyExists(Conflict): + message = _("DNS %(dns)s already exists [req: %(req)s].") \ No newline at end of file diff --git a/iotronic/conductor/endpoints.py b/iotronic/conductor/endpoints.py index 7cdae95..d45ecd5 100644 --- a/iotronic/conductor/endpoints.py +++ b/iotronic/conductor/endpoints.py @@ -23,6 +23,7 @@ except Exception: import random import socket +import json from oslo_config import cfg @@ -46,6 +47,7 @@ Port = list() SERVICE_PORT_LIST = [] + def versionCompare(v1, v2): """Method to compare two versions. Return 1 if v2 is smaller, @@ -53,20 +55,25 @@ def versionCompare(v1, v2): 0 if equal """ - v1_list = v1.split(".")[:3] - v2_list = v2.split(".")[:3] - i = 0 - while (i < len(v1_list)): + if "freedom" in v1: + return 1 + + else: - # v2 > v1 - if int(v2_list[i]) > int(v1_list[i]): - return -1 - # v1 > v1 - if int(v1_list[i]) > int(v2_list[i]): - return 1 - i += 1 - # v2 == v1 - return 0 + v1_list = v1.split(".")[:3] + v2_list = v2.split(".")[:3] + i = 0 + while (i < len(v1_list)): + + # v2 > v1 + if int(v2_list[i]) > int(v1_list[i]): + return -1 + # v1 > v1 + if int(v1_list[i]) > int(v2_list[i]): + return 1 + i += 1 + # v2 == v1 + return 0 def new_req(ctx, board, type, action, main_req=None, pending_requests=0): @@ -154,6 +161,7 @@ def create_record_dns_webservice(ctx, board, webs_name, board_dns, zone): LOG.debug('using %s %s %s', webs_name + "." + board_dns, ip, zone) + # CHECK IF DNS EXISTS designate.create_record(webs_name + "." + board_dns, ip, zone) @@ -170,8 +178,8 @@ def create_record_dns(ctx, board, board_dns, zone): LOG.debug('using %s %s %s', board_dns, ip, zone) - designate.create_record(board_dns, ip, - zone) + # CHECK IF DNS EXISTS + designate.create_record(board_dns, ip, zone) LOG.debug('Configure Web Proxy on WampAgent %s (%s) for board %s', board.agent, ip, board.uuid) @@ -253,20 +261,34 @@ class ConductorEndpoint(object): board_id) board = objects.Board.get_by_uuid(ctx, board_id) result = None + if board.is_online(): - prov = Provisioner() - prov.conf_clean() - p = prov.get_config() - LOG.debug('sending this conf %s', p) + try: result = self.execute_on_board(ctx, board_id, - 'destroyBoard', - (p,)) - + 'DeviceFactoryReset', + ()) except exception: return exception + + exposed_list = objects.ExposedService.get_by_board_uuid(ctx, + board_id) + + LOG.debug('starting the wamp client') + cctx = self.wamp_agent_client.prepare(server=board.agent) + + LOG.debug('Service to remove from allowlist:') + LOG.debug(exposed_list) + + for exposed in exposed_list: + LOG.debug(exposed.public_port) + cctx.call(ctx, 'remove_from_allowlist', + device=board_id, + port=exposed.public_port) + board.destroy() + if result: result = manage_result(result, 'destroyBoard', board_id) LOG.debug(result) @@ -280,19 +302,26 @@ class ConductorEndpoint(object): return serializer.serialize_entity(ctx, board) def create_board(self, ctx, board_obj, location_obj): - new_board = serializer.deserialize_entity(ctx, board_obj) - LOG.debug('Creating board %s', - new_board.name) - new_location = serializer.deserialize_entity(ctx, location_obj) - new_board.create() - new_location.board_id = new_board.id - new_location.create() - return serializer.serialize_entity(ctx, new_board) + new_board = serializer.deserialize_entity(ctx, board_obj) + LOG.debug('Creating board %s', new_board.name) + + board_exists = objects.Board.exists_by_name(ctx, new_board.name) + #print(board_exists) + + if board_exists: + raise exception.BoardNameAlreadyExists(name=new_board.name) + else: + new_location = serializer.deserialize_entity(ctx, location_obj) + new_board.create() + new_location.board_id = new_board.id + new_location.create() + + return serializer.serialize_entity(ctx, new_board) def execute_on_board(self, ctx, board_uuid, wamp_rpc_call, wamp_rpc_args, main_req=None): - LOG.debug('Executing \"%s\" on the board: %s (req %s)', + LOG.debug('Executing \"%s\" on the board: %s (main_req %s)', wamp_rpc_call, board_uuid, main_req) board = objects.Board.get_by_uuid(ctx, board_uuid) @@ -308,18 +337,20 @@ class ConductorEndpoint(object): req = new_req(ctx, board, objects.request.BOARD, wamp_rpc_call, main_req) + LOG.info(" - exec_request: " + str(req.uuid)) + res = new_res(ctx, board, req.uuid) cctx = self.wamp_agent_client.prepare(server=board.agent) # for previous LR version (to be removed asap) + if (versionCompare(board.lr_version, "0.4.9") == -1): response = cctx.call(ctx, 's4t_invoke_wamp', wamp_rpc_call=full_wamp_call, data=wamp_rpc_args) else: - response = cctx.call(ctx, 's4t_invoke_wamp', wamp_rpc_call=full_wamp_call, req=req, @@ -351,7 +382,7 @@ class ConductorEndpoint(object): board = objects.Board.get(ctx, board_uuid) if not board.is_online(): raise exception.BoardNotConnected(board=board.uuid) - + objects.board.is_valid_action(action) try: @@ -484,6 +515,10 @@ class ConductorEndpoint(object): service = objects.Service.get(ctx, service_uuid) objects.service.is_valid_action(action) + board = objects.Board.get_by_uuid(ctx, board_uuid) + if not board.is_online(): + raise exception.BoardNotConnected(board=board.uuid) + if action == "ServiceEnable": LOG.info('Enabling service with id %s into the board %s', service_uuid, board_uuid) @@ -493,11 +528,23 @@ class ConductorEndpoint(object): service_uuid) return exception.ServiceAlreadyExposed(uuid=service_uuid) except Exception: + public_port = random_public_port() if not public_port: return exception.NotEnoughPortForService() + # Add into service allow list + #if cfg.CONF.conductor.service_allow_list: + # addin_allowlist(board_uuid, public_port) + LOG.debug('starting the wamp client') + cctx = self.wamp_agent_client.prepare(server=board.agent) + cctx.call(ctx, 'addin_allowlist', + device=board_uuid, + port=public_port) + + + res = self.execute_on_board(ctx, board_uuid, action, (service, public_port)) result = manage_result(res, action, board_uuid) @@ -531,6 +578,18 @@ class ConductorEndpoint(object): except exception: pass exposed.destroy() + + + # Remove from service allow list + #if cfg.CONF.conductor.service_allow_list: + #remove_from_allowlist(board_uuid, exposed.public_port) + LOG.debug('starting the wamp client') + cctx = self.wamp_agent_client.prepare(server=board.agent) + cctx.call(ctx, 'remove_from_allowlist', + device=board_uuid, + port=exposed.public_port) + + return result elif action == "ServiceRestore": @@ -559,6 +618,25 @@ class ConductorEndpoint(object): return 0 + def status_services_on_board(self, ctx, board_uuid): + LOG.info('Getting services status into devide %s', + board_uuid) + + action = "ServicesStatus" + + board = objects.Board.get(ctx, board_uuid) + if not board.is_online(): + raise exception.BoardNotConnected(board=board.uuid) + + try: + result = self.execute_on_board(ctx, board_uuid, action, ()) + except exception: + return exception + + result = manage_result(result, action, board_uuid) + LOG.debug(result) + return result + def create_port_on_board(self, ctx, board_uuid, network_uuid, subnet_uuid, security_groups=None): @@ -691,57 +769,102 @@ class ConductorEndpoint(object): def create_webservice(self, ctx, webservice_obj): newwbs = serializer.deserialize_entity(ctx, webservice_obj) - LOG.debug('Creating webservice %s', - newwbs.name) + LOG.debug('Creating webservice %s', newwbs.name) board = objects.Board.get_by_uuid(ctx, newwbs.board_uuid) if not board.is_online(): raise exception.BoardNotConnected(board=board.uuid) - en_webservice = objects.enabledwebservice. \ - EnabledWebservice.get_by_board_uuid(ctx, - newwbs.board_uuid) + LOG.info(" - expose "+str(newwbs.name)+" in " + str(board.name) + " ["+str(newwbs.board_uuid)+"]") + list_webs = objects.Webservice.list( + ctx, + filters={'board_uuid': newwbs.board_uuid} + ) + + dns_check=True + webs_found=None - full_zone_domain = en_webservice.dns + "." + en_webservice.zone - dns_domain = newwbs.name + "." + full_zone_domain - - 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 + wboard = objects.Board.get(ctx, webs.board_uuid) + LOG.info(" --> " + str(webs.name) + ", " + str(wboard.name) + ", " + str(webs.board_uuid) + ", " + str(webs.port) ) + if (str(newwbs.board_uuid) == str(webs.board_uuid)) and (newwbs.name == str(webs.name)): + dns_check = False + webs_found = webs + break - list_dns = list_dns + dns_domain + if dns_check: - try: - self.execute_on_board(ctx, - newwbs.board_uuid, - 'ExposeWebservice', - (full_zone_domain, - dns_domain, - newwbs.port, - list_dns,)) - except exception: - return exception + LOG.info("Webservice can be exposed!") - cctx = self.wamp_agent_client.prepare(server=board.agent) - cctx.call(ctx, 'add_redirect', board_dns=en_webservice.dns, - zone=en_webservice.zone, dns=newwbs.name) - cctx.call(ctx, 'reload_proxy') + en_webservice = objects.enabledwebservice. \ + EnabledWebservice.get_by_board_uuid(ctx, + newwbs.board_uuid) - newwbs.create() + full_zone_domain = en_webservice.dns + "." + en_webservice.zone + dns_domain = newwbs.name + "." + full_zone_domain - return serializer.serialize_entity(ctx, newwbs) + 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_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: + res = self.execute_on_board(ctx, + newwbs.board_uuid, + 'ExposeWebservice', + (full_zone_domain, + dns_domain, + newwbs.port, + list_dns,)) + except exception: + return exception + + cctx = self.wamp_agent_client.prepare(server=board.agent) + cctx.call(ctx, 'add_redirect', board_dns=en_webservice.dns, + zone=en_webservice.zone, dns=newwbs.name) + cctx.call(ctx, 'reload_proxy') + + newwbs.create() + + result = manage_result(res, "ExposeWebservice", board.uuid) + LOG.info(result) + #return result + + return serializer.serialize_entity(ctx, newwbs) + + else: + + mreq = new_req(ctx, board, objects.request.BOARD, + "expose_webservice", pending_requests=0) + LOG.info(" - request: " + str(mreq.uuid)) + + + + msg="Webservice already exposed for this device!" + w_msg = wm.WampWarning(msg, req_id=mreq.uuid) + + res = new_res(ctx, board, mreq.uuid) + res.result = w_msg.result + res.message = w_msg.message + res.save() + + mreq.status = objects.request.COMPLETED + mreq.save() + + result = manage_result(w_msg, "ExposeWebservice", board.uuid) + + LOG.info(result) + #return result + return serializer.serialize_entity(ctx, webs_found) def destroy_webservice(self, ctx, webservice_id): LOG.info('Destroying webservice with id %s', @@ -750,49 +873,53 @@ class ConductorEndpoint(object): wbsrv = objects.Webservice.get_by_uuid(ctx, webservice_id) board = objects.Board.get_by_uuid(ctx, wbsrv.board_uuid) - if not board.is_online(): - raise exception.BoardNotConnected(board=board.uuid) + + #if not board.is_online(): + # raise exception.BoardNotConnected(board=board.uuid) 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 + if board.is_online(): - list_webs = objects.Webservice.list(ctx, - filters={ - 'board_uuid': wbsrv.board_uuid - }) + full_zone_domain = en_webservice.dns + "." + en_webservice.zone + dns_domain = wbsrv.name + "." + full_zone_domain - 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] + list_webs = objects.Webservice.list(ctx, + filters={ + 'board_uuid': wbsrv.board_uuid + }) - try: - # result = - self.execute_on_board(ctx, - wbsrv.board_uuid, - 'UnexposeWebservice', - (dns_domain, list_dns,)) - except exception: - return exception + 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] - if board.agent == None: - raise exception.BoardInvalidStatus(uuid=board.uuid, - status=board.status) + try: + # result = + self.execute_on_board(ctx, + wbsrv.board_uuid, + 'UnexposeWebservice', + (dns_domain, list_dns,)) + except exception: + return exception - cctx = self.wamp_agent_client.prepare(server=board.agent) - cctx.call(ctx, 'remove_redirect', board_dns=en_webservice.dns, - zone=en_webservice.zone, dns=wbsrv.name) - cctx.call(ctx, 'reload_proxy') + if board.agent == None: + raise exception.BoardInvalidStatus(uuid=board.uuid, + status=board.status) + + cctx = self.wamp_agent_client.prepare(server=board.agent) + cctx.call(ctx, 'remove_redirect', board_dns=en_webservice.dns, + zone=en_webservice.zone, dns=wbsrv.name) + cctx.call(ctx, 'reload_proxy') wbsrv.destroy() designate.delete_record(wbsrv.name + "." + en_webservice.dns, en_webservice.zone) + return def enable_webservice(self, ctx, dns, zone, email, board_uuid): @@ -804,110 +931,195 @@ class ConductorEndpoint(object): if board.agent == None: raise exception.BoardInvalidStatus(uuid=board.uuid, status=board.status) + #print("DNS: " + str(dns)) - mreq = new_req(ctx, board, objects.request.BOARD, - "enable_webservice", pending_requests=3) + avl_webservice = objects.enabledwebservice.EnabledWebservice.checkDnsAvailable(ctx, dns) + #print("Available: " + str(avl_webservice)) - try: - create_record_dns(ctx, board, dns, zone) - except exception: - return exception + LOG.info("Enabling Webservice module for device " + str(board.name) + " ["+board_uuid+"]") - try: - en_webservice = objects.enabledwebservice. \ - EnabledWebservice.get_by_board_uuid(ctx, - board.uuid) + if avl_webservice: - LOG.debug('Webservice data already exists for board %s', - board.uuid) - https_port = en_webservice.https_port - http_port = en_webservice.http_port + en_webservice = objects.enabledwebservice.EnabledWebservice.isWebserviceEnabled(ctx, board.uuid) - except Exception: + if(en_webservice): - https_port = random_public_port() - http_port = random_public_port() - if not https_port or not http_port: - return exception.NotEnoughPortForService() + mreq = new_req(ctx, board, objects.request.BOARD, + "enable_webservice", pending_requests=0) + LOG.info(" - request: " + str(mreq.uuid)) - 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) + res = new_res(ctx, board, mreq.uuid) - LOG.debug('Save webservice data %s for board %s', dns + "." + zone, - board.uuid) - en_webservice.create() + en_webservice = objects.enabledwebservice.EnabledWebservice.get_by_board_uuid(ctx, board.uuid) - LOG.debug('Open ports on WampAgent %s for http and %s for https ' - 'on board %s', http_port, https_port, board.uuid) + LOG.info("Webservice and dns already enabled for this device:") + full_zone_domain = en_webservice.dns + "." + en_webservice.zone - service = objects.Service.get_by_name(ctx, 'webservice') + LOG.info(" --> " + str(full_zone_domain)) - res = self.execute_on_board(ctx, board.uuid, "ServiceEnable", - (service, http_port,), main_req=mreq.uuid) - result = manage_result(res, "ServiceEnable", board.uuid) + msg="Webservice and dns already enabled for this device!" + w_msg = wm.WampWarning(msg, req_id=mreq.uuid) - exp_data = { - 'board_uuid': board_uuid, - 'service_uuid': service.uuid, - 'public_port': http_port, - } - exposed = objects.ExposedService(ctx, **exp_data) - exposed.create() + res.result = w_msg.result + res.message = w_msg.message + res.save() - LOG.debug(result) + mreq.status = objects.request.COMPLETED + mreq.save() - service = objects.Service.get_by_name(ctx, 'webservice_ssl') + result = manage_result(w_msg, "EnableWebservice", board.uuid) - res = self.execute_on_board(ctx, board.uuid, "ServiceEnable", - (service, https_port,), main_req=mreq.uuid) - result = manage_result(res, "ServiceEnable", board.uuid) + LOG.info(result) - exp_data = { - 'board_uuid': board_uuid, - 'service_uuid': service.uuid, - 'public_port': https_port, - } - exposed = objects.ExposedService(ctx, **exp_data) - exposed.create() + raise exception.EnabledWebserviceAlreadyExists(enabled_webservice=full_zone_domain, req=mreq.uuid) - LOG.debug(result) + #return serializer.serialize_entity(ctx, en_webservice) - cctx = self.wamp_agent_client.prepare(server=board.agent) - cctx.call(ctx, 'enable_webservice', board=dns, - https_port=https_port, http_port=http_port, zone=zone) - cctx.call(ctx, 'reload_proxy') - LOG.debug('Configure Web Proxy on Board %s with dns %s (email: %s) ', - board.uuid, dns, email) + else: + + mreq = new_req(ctx, board, objects.request.BOARD, + "enable_webservice", pending_requests=3) - try: - self.execute_on_board(ctx, - board.uuid, - 'EnableWebService', - (dns + "." + zone, email,), - main_req=mreq.uuid) - except exception: - return exception + try: + create_record_dns(ctx, board, dns, zone) + except exception: + return exception - cctx.call(ctx, 'add_redirect', board_dns=dns, zone=zone) - cctx.call(ctx, 'reload_proxy') + try: + en_webservice = objects.enabledwebservice. \ + EnabledWebservice.get_by_board_uuid(ctx, + board.uuid) - return serializer.serialize_entity(ctx, en_webservice) + 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: + + https_port = random_public_port() + http_port = random_public_port() + if not https_port or not http_port: + return exception.NotEnoughPortForService() + + 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') + + + LOG.debug('starting the wamp client') + cctx = self.wamp_agent_client.prepare(server=board.agent) + cctx.call(ctx, 'addin_allowlist', + device=board.uuid, + port=http_port) + cctx.call(ctx, 'addin_allowlist', + device=board.uuid, + port=https_port) + + res = self.execute_on_board(ctx, board.uuid, "ServiceEnable", + (service, http_port,), main_req=mreq.uuid) + 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,), main_req=mreq.uuid) + 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, zone=zone) + cctx.call(ctx, 'reload_proxy') + + LOG.debug('Configure Web Proxy on Board %s with dns %s (email: %s) ', + board.uuid, dns, email) + + try: + self.execute_on_board(ctx, + board.uuid, + 'EnableWebService', + (dns + "." + zone, email,), + main_req=mreq.uuid) + except exception: + return exception + + cctx.call(ctx, 'add_redirect', board_dns=dns, zone=zone) + cctx.call(ctx, 'reload_proxy') + + return serializer.serialize_entity(ctx, en_webservice) + + + else: + mreq = new_req(ctx, board, objects.request.BOARD, + "enable_webservice", pending_requests=0) + LOG.info(" - request: " + str(mreq.uuid)) + + res = new_res(ctx, board, mreq.uuid) + + msg="DNS already exists!" + w_msg = wm.WampWarning(msg, req_id=mreq.uuid) + + res.result = w_msg.result + res.message = w_msg.message + res.save() + + mreq.status = objects.request.COMPLETED + mreq.save() + + result = manage_result(w_msg, "EnableWebservice", board.uuid) + + LOG.info(" - result: " + str(result)) + + raise exception.DnsWebserviceAlreadyExists(dns=dns, req=mreq.uuid) 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 not board.is_online(): - raise exception.BoardNotConnected(board=board.uuid) + + #if not board.is_online(): + # raise exception.BoardNotConnected(board=board.uuid) if board.agent == None: raise exception.BoardInvalidStatus(uuid=board.uuid, @@ -923,7 +1135,7 @@ class ConductorEndpoint(object): https_port = en_webservice.https_port http_port = en_webservice.http_port - LOG.debug('Disable Webservices ports %s for http and %s for https ' + 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') @@ -932,9 +1144,11 @@ class ConductorEndpoint(object): board_uuid, service.uuid) - res = self.execute_on_board(ctx, board.uuid, "ServiceDisable", + if board.is_online(): + res = self.execute_on_board(ctx, board.uuid, "ServiceDisable", (service,), main_req=mreq.uuid) - LOG.debug(res.message) + LOG.debug(res.message) + exposed.destroy() service = objects.Service.get_by_name(ctx, 'webservice_ssl') @@ -942,9 +1156,13 @@ class ConductorEndpoint(object): exposed = objects.ExposedService.get(ctx, board_uuid, service.uuid) - res = self.execute_on_board(ctx, board.uuid, "ServiceDisable", + + if board.is_online(): + res = self.execute_on_board(ctx, board.uuid, "ServiceDisable", (service,), main_req=mreq.uuid) - LOG.debug(res.message) + LOG.debug(res.message) + + global SERVICE_PORT_LIST try: SERVICE_PORT_LIST.append(exposed.public_port) @@ -952,14 +1170,15 @@ class ConductorEndpoint(object): pass exposed.destroy() - try: - self.execute_on_board(ctx, - board.uuid, - 'DisableWebService', - (), - main_req=mreq.uuid) - except exception: - return exception + if board.is_online(): + try: + self.execute_on_board(ctx, + board.uuid, + 'DisableWebService', + (), + main_req=mreq.uuid) + except exception: + return exception webservice = objects.EnabledWebservice.get_by_board_uuid( ctx, board_uuid) @@ -969,13 +1188,73 @@ class ConductorEndpoint(object): designate.delete_record(webservice.dns, en_webservice.zone) + if board.is_online(): + cctx = self.wamp_agent_client.prepare(server=board.agent) + cctx.call(ctx, 'disable_webservice', board=webservice.dns) + + # cctx.call(ctx, 'remove_redirect', board_dns=en_webservice.dns, + # zone=en_webservice.zone) + + cctx.call(ctx, 'reload_proxy') + + + LOG.debug('starting the wamp client') cctx = self.wamp_agent_client.prepare(server=board.agent) - cctx.call(ctx, 'disable_webservice', board=webservice.dns) + cctx.call(ctx, 'remove_from_allowlist', + device=board.uuid, + port=http_port) - # cctx.call(ctx, 'remove_redirect', board_dns=en_webservice.dns, - # zone=en_webservice.zone) - cctx.call(ctx, 'reload_proxy') + cctx.call(ctx, 'remove_from_allowlist', + device=board.uuid, + port=https_port) webservice.destroy() + return + + def renew_webservice(self, ctx, board_uuid): + + board = objects.Board.get_by_uuid(ctx, board_uuid) + if not board.is_online(): + raise exception.BoardNotConnected(board=board.uuid) + + if board.agent == None: + raise exception.BoardInvalidStatus(uuid=board.uuid, + status=board.status) + + LOG.info('Renewing certificates on device %s', board.uuid) + + en_webservice = objects.enabledwebservice.EnabledWebservice.isWebserviceEnabled(ctx, board.uuid) + + if(en_webservice): + + mreq = new_req(ctx, board, objects.request.BOARD, + "RenewWebservice", pending_requests=1) + + res = self.execute_on_board(ctx, board.uuid, "RenewWebservice", (), main_req=mreq.uuid ) + + else: + + mreq = new_req(ctx, board, objects.request.BOARD, + "RenewWebservice", pending_requests=0) + LOG.info(" - request: " + str(mreq.uuid)) + + msg="Webservice module not enabled for this device!" + w_msg = wm.WampWarning(msg, req_id=mreq.uuid) + + res = new_res(ctx, board, mreq.uuid) + res.result = w_msg.result + res.message = w_msg.message + res.save() + + mreq.status = objects.request.COMPLETED + mreq.save() + + raise exception.EnabledWebserviceNotFound(enabled_webservice=board.uuid) + + + result = manage_result(res, "RenewWebservice", board.uuid) + LOG.info(" - " + str(result)) + + return serializer.serialize_entity(ctx, en_webservice) diff --git a/iotronic/conductor/manager.py b/iotronic/conductor/manager.py index 37e0628..70b8dcc 100644 --- a/iotronic/conductor/manager.py +++ b/iotronic/conductor/manager.py @@ -46,6 +46,7 @@ conductor_opts = [ cfg.IntOpt('service_port_max', default=60000, help='Max value for genereting random ports for services'), + ] CONF = cfg.CONF diff --git a/iotronic/conductor/rpcapi.py b/iotronic/conductor/rpcapi.py index 3166215..2cdc151 100644 --- a/iotronic/conductor/rpcapi.py +++ b/iotronic/conductor/rpcapi.py @@ -291,6 +291,20 @@ class ConductorAPI(object): return cctxt.call(context, 'restore_services_on_board', board_uuid=board_uuid) + + def status_services_on_board(self, context, + board_uuid, topic=None): + """Get status of all services' tunnel on a board. + + :param context: request context. + :param board_uuid: board id or uuid. + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'status_services_on_board', + board_uuid=board_uuid) + + def create_port_on_board(self, context, board_uuid, network, subnet, sec_groups, topic=None): """Add a port on a Board @@ -409,3 +423,11 @@ class ConductorAPI(object): cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') return cctxt.call(context, 'disable_webservice', board_uuid=board_uuid) + + def renew_webservice(self, context, board_uuid, topic=None): + """Renew webservice certificate. + + """ + cctxt = self.client.prepare(topic=topic or self.topic, version='1.0') + return cctxt.call(context, 'renew_webservice', + board_uuid=board_uuid) diff --git a/iotronic/db/sqlalchemy/alembic/versions/10460765f337_extended_board_code_attribute.py b/iotronic/db/sqlalchemy/alembic/versions/10460765f337_extended_board_code_attribute.py new file mode 100644 index 0000000..4568ed4 --- /dev/null +++ b/iotronic/db/sqlalchemy/alembic/versions/10460765f337_extended_board_code_attribute.py @@ -0,0 +1,23 @@ +"""extended board code attribute + +Revision ID: 10460765f337 +Revises: 57a99337e843 +Create Date: 2021-11-03 16:17:19.310762 + +""" + +# revision identifiers, used by Alembic. +revision = '10460765f337' +down_revision = '57a99337e843' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('boards', 'code', + existing_type=mysql.VARCHAR(length=36), + nullable=False) + + # ### end Alembic commands ### diff --git a/iotronic/db/sqlalchemy/alembic/versions/57a99337e843_hard_device_delete.py b/iotronic/db/sqlalchemy/alembic/versions/57a99337e843_hard_device_delete.py new file mode 100644 index 0000000..cf7be02 --- /dev/null +++ b/iotronic/db/sqlalchemy/alembic/versions/57a99337e843_hard_device_delete.py @@ -0,0 +1,64 @@ +"""hard device delete + +Revision ID: 57a99337e843 +Revises: 76c628d60004 +Create Date: 2020-07-07 15:45:20.892424 + +""" + +# revision identifiers, used by Alembic. +revision = '57a99337e843' +down_revision = '76c628d60004' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('enabled_webservices_ibfk_1', 'enabled_webservices', type_='foreignkey') + op.create_foreign_key(None, 'enabled_webservices', 'boards', ['board_uuid'], ['uuid'], ondelete='CASCADE') + op.drop_constraint('exposed_services_ibfk_1', 'exposed_services', type_='foreignkey') + op.create_foreign_key(None, 'exposed_services', 'boards', ['board_uuid'], ['uuid'], ondelete='CASCADE') + op.drop_constraint('injection_plugins_ibfk_1', 'injection_plugins', type_='foreignkey') + op.create_foreign_key(None, 'injection_plugins', 'boards', ['board_uuid'], ['uuid'], ondelete='CASCADE') + op.drop_constraint('locations_ibfk_1', 'locations', type_='foreignkey') + op.create_foreign_key(None, 'locations', 'boards', ['board_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('ports_on_boards_ibfk_1', 'ports_on_boards', type_='foreignkey') + op.create_foreign_key(None, 'ports_on_boards', 'boards', ['board_uuid'], ['uuid'], ondelete='CASCADE') + op.alter_column('requests', 'action', + existing_type=mysql.VARCHAR(length=20), + nullable=True) + op.alter_column('requests', 'destination_uuid', + existing_type=mysql.VARCHAR(length=36), + nullable=True) + op.alter_column('requests', 'pending_requests', + existing_type=mysql.INTEGER(display_width=11), + nullable=True) + op.alter_column('requests', 'status', + existing_type=mysql.VARCHAR(length=10), + nullable=True) + op.alter_column('requests', 'type', + existing_type=mysql.INTEGER(display_width=11), + nullable=True) + op.alter_column('requests', 'uuid', + existing_type=mysql.VARCHAR(length=36), + nullable=True) + op.alter_column('results', 'board_uuid', + existing_type=mysql.VARCHAR(length=36), + nullable=True) + op.alter_column('results', 'request_uuid', + existing_type=mysql.VARCHAR(length=36), + nullable=True) + op.alter_column('results', 'result', + existing_type=mysql.VARCHAR(length=10), + nullable=True) + op.drop_constraint('results_ibfk_1', 'results', type_='foreignkey') + op.drop_constraint('results_ibfk_2', 'results', type_='foreignkey') + op.drop_constraint('sessions_ibfk_1', 'sessions', type_='foreignkey') + op.create_foreign_key(None, 'sessions', 'boards', ['board_id'], ['id'], ondelete='CASCADE') + op.create_unique_constraint('uniq_webservices0uuid', 'webservices', ['uuid']) + op.drop_index('uniq_enabled_webservices0uuid', table_name='webservices') + op.drop_constraint('webservices_ibfk_1', 'webservices', type_='foreignkey') + op.create_foreign_key(None, 'webservices', 'boards', ['board_uuid'], ['uuid'], ondelete='CASCADE') + # ### end Alembic commands ### diff --git a/iotronic/db/sqlalchemy/api.py b/iotronic/db/sqlalchemy/api.py index 0b75953..829d54c 100644 --- a/iotronic/db/sqlalchemy/api.py +++ b/iotronic/db/sqlalchemy/api.py @@ -1175,6 +1175,15 @@ class Connection(api.Connection): # ENABLED_WEBSERIVCE api + def get_enabled_webservice_by_name(self, enabled_webservice_name): + query = model_query(models.EnabledWebservice).filter_by( + dns=enabled_webservice_name) + try: + return query.one() + except NoResultFound: + return False + #exception.EnabledWebserviceNotFound(enabled_webservice=enabled_webservice_name) + def get_enabled_webservice_by_id(self, enabled_webservice_id): query = model_query(models.EnabledWebservice).filter_by( id=enabled_webservice_id) diff --git a/iotronic/db/sqlalchemy/models.py b/iotronic/db/sqlalchemy/models.py index 11de53f..72485e3 100644 --- a/iotronic/db/sqlalchemy/models.py +++ b/iotronic/db/sqlalchemy/models.py @@ -144,7 +144,7 @@ class Board(Base): table_args()) id = Column(Integer, primary_key=True) uuid = Column(String(36)) - code = Column(String(25)) + code = Column(String(36)) status = Column(String(15), nullable=True) name = Column(String(255), nullable=True) type = Column(String(255)) @@ -169,7 +169,7 @@ class Location(Base): longitude = Column(String(18), nullable=True) latitude = Column(String(18), nullable=True) altitude = Column(String(18), nullable=True) - board_id = Column(Integer, ForeignKey('boards.id')) + board_id = Column(Integer, ForeignKey('boards.id', ondelete="CASCADE")) class SessionWP(Base): @@ -185,7 +185,7 @@ class SessionWP(Base): valid = Column(Boolean, default=True) session_id = Column(String(20)) board_uuid = Column(String(36)) - board_id = Column(Integer, ForeignKey('boards.id')) + board_id = Column(Integer, ForeignKey('boards.id', ondelete="CASCADE")) class Plugin(Base): @@ -213,7 +213,7 @@ class InjectionPlugin(Base): __table_args__ = ( table_args()) id = Column(Integer, primary_key=True) - board_uuid = Column(String(36), ForeignKey('boards.uuid')) + board_uuid = Column(String(36), ForeignKey('boards.uuid', ondelete="CASCADE")) plugin_uuid = Column(String(36), ForeignKey('plugins.uuid')) onboot = Column(Boolean, default=False) status = Column(String(15)) @@ -242,7 +242,7 @@ class ExposedService(Base): __table_args__ = ( table_args()) id = Column(Integer, primary_key=True) - board_uuid = Column(String(36), ForeignKey('boards.uuid')) + board_uuid = Column(String(36), ForeignKey('boards.uuid', ondelete="CASCADE")) service_uuid = Column(String(36), ForeignKey('services.uuid')) public_port = Column(Integer) @@ -256,7 +256,7 @@ class Port(Base): # table_args() # ) id = Column(Integer, primary_key=True) - board_uuid = Column(String(40), ForeignKey('boards.uuid')) + board_uuid = Column(String(40), ForeignKey('boards.uuid', ondelete="CASCADE")) uuid = Column(String(40)) VIF_name = Column(String(30)) # project = Column(String(36)) @@ -298,7 +298,7 @@ class Webservice(Base): uuid = Column(String(36)) port = Column(Integer) name = Column(String(45)) - board_uuid = Column(String(36), ForeignKey('boards.uuid'), nullable=True) + board_uuid = Column(String(36), ForeignKey('boards.uuid', ondelete="CASCADE"), nullable=True) secure = Column(Boolean, default=True) extra = Column(JSONEncodedDict) @@ -308,7 +308,7 @@ class EnabledWebservice(Base): __tablename__ = 'enabled_webservices' id = Column(Integer, primary_key=True) - board_uuid = Column(String(36), ForeignKey('boards.uuid'), nullable=True) + board_uuid = Column(String(36), ForeignKey('boards.uuid', ondelete="CASCADE"), nullable=True) http_port = Column(Integer) https_port = Column(Integer) dns = Column(String(100)) diff --git a/iotronic/objects/board.py b/iotronic/objects/board.py index 2065f84..35cc9f1 100644 --- a/iotronic/objects/board.py +++ b/iotronic/objects/board.py @@ -24,7 +24,7 @@ from iotronic.objects import utils as obj_utils ACTIONS = ['DevicePing', 'DeviceReboot', 'DeviceRestartLR', 'DeviceNetConfig', 'DeviceEcho', 'DeviceUpgradeLR', - 'DevicePkgOperation'] + 'DevicePkgOperation', 'DeviceRestSubmit'] def is_valid_action(action): @@ -132,6 +132,22 @@ class Board(base.IotronicObject): board = Board._from_db_object(cls(context), db_board) return board + @base.remotable_classmethod + def exists_by_name(cls, context, name): + """Find a board based on name and return a boolean. + + :param name: the logical name of a board. + :returns: a boolean object. + """ + res=False + try: + db_board = cls.dbapi.get_board_by_name(name) + res = True + except exception.BoardNotFound as E: + return res + + return res + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, sort_key=None, sort_dir=None, filters=None): diff --git a/iotronic/objects/enabledwebservice.py b/iotronic/objects/enabledwebservice.py index 80233cd..c86d67e 100644 --- a/iotronic/objects/enabledwebservice.py +++ b/iotronic/objects/enabledwebservice.py @@ -88,6 +88,46 @@ class EnabledWebservice(base.IotronicObject): db_enabled_webservice) return en_webserv + @base.remotable_classmethod + def isWebserviceEnabled(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 boolean object. + """ + res=False + try: + db_enabled_webservice = cls.dbapi.get_enabled_webservice_by_board_uuid( + uuid) + res = True + except exception.EnabledWebserviceNotFound as E: + return res + + return res + + @base.remotable_classmethod + def checkDnsAvailable(cls, context, dns): + """Check if a dns is already assigned. + + :param dns: dns chosen to enabled_webservice into a device. + :returns: a boolean object. + """ + res=False + try: + db_enabled_webservice = cls.dbapi.get_enabled_webservice_by_name( + dns) + if db_enabled_webservice == False: + # OK dns approved + res = True + else: + res = False + except exception.EnabledWebserviceAlreadyExists as E: + return res + + return res + + + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, sort_key=None, sort_dir=None, filters=None): diff --git a/iotronic/objects/plugin.py b/iotronic/objects/plugin.py index 66c34af..201a06f 100644 --- a/iotronic/objects/plugin.py +++ b/iotronic/objects/plugin.py @@ -204,6 +204,7 @@ class Plugin(base.IotronicObject): """ 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] + 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 67abd6e..1c9adb0 100644 --- a/iotronic/wamp/agent.py +++ b/iotronic/wamp/agent.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. import asyncio +import json import subprocess import time import txaio @@ -72,6 +73,12 @@ wamp_opts = [ cfg.IntOpt('autoPingTimeout', default=2, help=('autoPingInterval parameter for wamp')), + cfg.BoolOpt('service_allow_list', + default=False, + help='Enable service allow list checks.'), + cfg.StrOpt('service_allow_list_path', + default="(/var/lib/wstun/allowlist)", + help='Path of allowlist.json file.'), ] @@ -121,6 +128,20 @@ class WampEndpoint(object): return r.result() +def read_allowlist(): + try: + + with open(CONF.wamp.service_allow_list_path, "r") as allow_file: + + allow_list_str = allow_file.read() + + allow_list = json.loads(allow_list_str) + #LOG.debug(allow_list) + + return allow_list + + except Exception as err: + LOG.error(err) class AgentEndpoint(object): @@ -140,6 +161,54 @@ class AgentEndpoint(object): return 1 + + + def addin_allowlist(self, ctx, device, port): + try: + + allow_list = read_allowlist() + + new_node={} + new_node['client']=device + new_node['port']=str(port) + + if new_node in allow_list: + LOG.warning("This device already exposes this port!") + else: + allow_list.append(new_node) + with open(CONF.wamp.service_allow_list_path, "r+") as allow_file: + allow_file.seek(0) + allow_file.write("%s" % json.dumps(allow_list)) + allow_file.truncate() + + read_allowlist() + LOG.debug("Added device/service port in allow list.") + + except Exception as err: + print(err) + + + def remove_from_allowlist(self, ctx, device, port): + try: + allow_list = read_allowlist() + + new_node={} + new_node['client']=device + new_node['port']=str(port) + + if new_node in allow_list: + allow_list.remove(new_node) + with open(CONF.wamp.service_allow_list_path, "r+") as allow_file: + allow_file.seek(0) + allow_file.write("%s" % json.dumps(allow_list)) + allow_file.truncate() + + LOG.debug("Removed device/service port from allow list.") + + except Exception as err: + print(err) + + class RPCServer(Thread): def __init__(self): # AMQP CONFIG diff --git a/iotronic/wamp/proxies/nginx.py b/iotronic/wamp/proxies/nginx.py index 017b902..668ae23 100644 --- a/iotronic/wamp/proxies/nginx.py +++ b/iotronic/wamp/proxies/nginx.py @@ -23,7 +23,10 @@ LOG = logging.getLogger(__name__) nginx_opts = [ cfg.StrOpt('nginx_path', default='/etc/nginx/conf.d/iotronic', - help=('Default Nginx Path')) + help=('Default Nginx Path')), + cfg.StrOpt('wstun_endpoint', + default='localhost', + help=('Default Nginx Path')) ] CONF = cfg.CONF @@ -39,9 +42,9 @@ def save_map(board, zone): 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; + server {2}:{1} max_fails=3 fail_timeout=10s; }} - '''.format(board, https_port) + '''.format(board, https_port, CONF.nginx.wstun_endpoint ) with open(fp, "w") as text_file: text_file.write("%s" % string) @@ -54,10 +57,10 @@ def save_server(board, http_port, zone): server_name .{0}.{2}; location / {{ - proxy_pass http://localhost:{1}; + proxy_pass http://{3}:{1}; }} }} - '''.format(board, http_port, zone) + '''.format(board, http_port, zone, CONF.nginx.wstun_endpoint) with open(fp, "w") as text_file: text_file.write("%s" % string)