diff --git a/docs/source/API.rst b/docs/source/API.rst index e11ca5be..8a7835c3 100644 --- a/docs/source/API.rst +++ b/docs/source/API.rst @@ -29,13 +29,6 @@ GET nodes The Nodes API will provide a report of current nodes as known by the node provisioner and their status with a few hardware details. -POST nodes -^^^^^^^^^ - -The Nodes API will provide a report of current nodes as known by the node provisioner -and their status with a few hardware details. This API requires node_filter and site_design -in the POST body to return the proper node list. - GET nodes/hostname/builddata ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -55,6 +48,16 @@ If the query parameter ``latest`` is passed with a value of ``true``, then only the most recently collected data for each ``generator`` will be included in the response. +nodefilter API +-------------- + +POST nodefilter +^^^^^^^^^^^^^^^ + +The Nodes API will provide a list of node names based on site_design. This API +requires site_design in the POST body with an optional node_filter to return the node +names. + bootdata -------- diff --git a/drydock_provisioner/control/api.py b/drydock_provisioner/control/api.py index 914a6619..3265b6eb 100644 --- a/drydock_provisioner/control/api.py +++ b/drydock_provisioner/control/api.py @@ -22,6 +22,7 @@ from .tasks import TasksResource from .tasks import TaskResource from .nodes import NodesResource from .nodes import NodeBuildDataResource +from .nodes import NodeFilterResource from .health import HealthResource from .health import HealthExtendedResource from .bootaction import BootactionUnitsResource @@ -81,12 +82,14 @@ def start_api(state_manager=None, ingester=None, orchestrator=None): state_manager=state_manager, orchestrator=orchestrator)), # API to list current MaaS nodes - ('/nodes', - NodesResource(state_manager=state_manager, - orchestrator=orchestrator)), + ('/nodes', NodesResource()), # API to get build data for a node ('/nodes/{hostname}/builddata', NodeBuildDataResource(state_manager=state_manager)), + # API to list current node names based + ('/nodefilter', + NodeFilterResource(state_manager=state_manager, + orchestrator=orchestrator)), # API for nodes to discover their boot actions during curtin install ('/bootactions/nodes/{hostname}/units', BootactionUnitsResource( diff --git a/drydock_provisioner/control/nodes.py b/drydock_provisioner/control/nodes.py index 637167c5..37f8a499 100644 --- a/drydock_provisioner/control/nodes.py +++ b/drydock_provisioner/control/nodes.py @@ -20,17 +20,12 @@ from drydock_provisioner import config from drydock_provisioner.drivers.node.maasdriver.api_client import MaasRequestFactory from drydock_provisioner.drivers.node.maasdriver.models.machine import Machines -from .base import StatefulResource +from .base import BaseResource, StatefulResource -class NodesResource(StatefulResource): - def __init__(self, orchestrator=None, **kwargs): - """Object initializer. - - :param orchestrator: instance of orchestrator.Orchestrator - """ - super().__init__(**kwargs) - self.orchestrator = orchestrator +class NodesResource(BaseResource): + def __init__(self): + super().__init__() @policy.ApiEnforcer('physical_provisioner:read_data') def on_get(self, req, resp): @@ -63,37 +58,6 @@ class NodesResource(StatefulResource): self.return_error( resp, falcon.HTTP_500, message="Unknown error", retry=False) - @policy.ApiEnforcer('physical_provisioner:read_data') - def on_post(self, req, resp): - try: - json_data = self.req_json(req) - node_filter = json_data.get('node_filter', None) - site_design = json_data.get('site_design', None) - if node_filter is None or site_design is None: - not_provided = [] - if node_filter is None: - not_provided.append('node_filter') - if site_design is None: - not_provided.append('site_design') - self.info(req.context, 'Missing required input value(s) %s' % not_provided) - self.return_error( - resp, - falcon.HTTP_400, - message='Missing input required value(s) %s' % not_provided, - retry=False) - return - nodes = self.orchestrator.process_node_filter(node_filter=node_filter, - site_design=site_design) - # Guarantees an empty list is returned if there are no nodes - if not nodes: - nodes = [] - resp.body = json.dumps(nodes) - resp.status = falcon.HTTP_200 - except Exception as ex: - self.error(req.context, "Unknown error: %s" % str(ex), exc_info=ex) - self.return_error( - resp, falcon.HTTP_500, message="Unknown error", retry=False) - class NodeBuildDataResource(StatefulResource): """Resource for returning build data for a node.""" @@ -122,3 +86,38 @@ class NodeBuildDataResource(StatefulResource): self.error(req.context, "Unknown error: %s" % str(ex), exc_info=ex) self.return_error( resp, falcon.HTTP_500, message="Unknown error", retry=False) + + +class NodeFilterResource(StatefulResource): + def __init__(self, orchestrator=None, **kwargs): + """Object initializer. + + :param orchestrator: instance of orchestrator.Orchestrator + """ + super().__init__(**kwargs) + self.orchestrator = orchestrator + + @policy.ApiEnforcer('physical_provisioner:read_data') + def on_post(self, req, resp): + try: + json_data = self.req_json(req) + node_filter = json_data.get('node_filter', None) + site_design = json_data.get('site_design', None) + if site_design is None: + self.info(req.context, 'Missing required input value: site_design') + self.return_error( + resp, + falcon.HTTP_400, + message='Missing input required value: site_design', + retry=False) + return + nodes = self.orchestrator.process_node_filter(node_filter=node_filter, + site_design=site_design) + resp_list = [n.name for n in nodes if nodes] + + resp.body = json.dumps(resp_list) + resp.status = falcon.HTTP_200 + except Exception as ex: + self.error(req.context, "Unknown error: %s" % str(ex), exc_info=ex) + self.return_error( + resp, falcon.HTTP_500, message="Unknown error", retry=False) diff --git a/drydock_provisioner/drydock_client/client.py b/drydock_provisioner/drydock_client/client.py index ae63cbae..20542499 100644 --- a/drydock_provisioner/drydock_client/client.py +++ b/drydock_provisioner/drydock_client/client.py @@ -39,6 +39,24 @@ class DrydockClient(object): return resp.json() + def get_nodes_for_filter(self, design_ref, node_filter=None): + """Get list of nodes in MaaS and their status. + + :param SiteDesign design_ref: A SiteDesign object. + :param NodeFilter node_filter (optional): A NodeFilter object. + :return: A list of node names based on the node_filter and site_design. + """ + endpoint = 'v1.0/nodefilter' + body = { + 'node_filter': node_filter, + 'site_design': design_ref + } + resp = self.session.post(endpoint, data=body) + + self._check_response(resp) + + return resp.json() + def get_tasks(self): """ Get a list of all the tasks, completed or running. @@ -54,16 +72,31 @@ class DrydockClient(object): return resp.json() - def get_task(self, task_id): + def get_task(self, task_id, builddata=None, subtaskerrors=None, layers=None): """ Get the current description of a Drydock task - :param string task_id: The string uuid task id to query - :return: A dict representing the current state of the task + :param string task_id: The string uuid task id to query. + :param boolean builddata: If true will include the build_data in the response. + :param boolean subtaskerrors: If true it will add all the errors from the subtasks as a dictionary in + subtask_errors. + :param int layers: If -1 will include all subtasks, if a positive integer it will include that many layers + of subtasks. + :return: A dict representing the current state of the task. """ endpoint = "v1.0/tasks/%s" % (task_id) + query_params = [] + if builddata: + query_params.append('builddata=true') + if subtaskerrors: + query_params.append('subtaskerrors=true') + if layers: + query_params.append('layers=%s' % layers) + if query_params: + endpoint = '%s?%s' % (endpoint, '&'.join(query_params)) + resp = self.session.get(endpoint) self._check_response(resp) diff --git a/drydock_provisioner/orchestrator/orchestrator.py b/drydock_provisioner/orchestrator/orchestrator.py index af49fa5a..b1cfb24e 100644 --- a/drydock_provisioner/orchestrator/orchestrator.py +++ b/drydock_provisioner/orchestrator/orchestrator.py @@ -416,7 +416,7 @@ class Orchestrator(object): return self.list_intersection(*result_sets) else: raise errors.OrchestratorError( - "Unknow filter set type %s" % filter_set_type) + "Unknown filter set type %s" % filter_set_type) def process_filter(self, node_set, filter_set): """Take a filter and apply it to the node_set. diff --git a/tests/unit/test_api_nodes_unit.py b/tests/unit/test_api_nodes_unit.py index f590b4b5..76dc5a12 100644 --- a/tests/unit/test_api_nodes_unit.py +++ b/tests/unit/test_api_nodes_unit.py @@ -21,6 +21,7 @@ import logging from drydock_provisioner import policy from drydock_provisioner.control.api import start_api +import drydock_provisioner.objects as objects import falcon @@ -33,13 +34,8 @@ class TestNodesApiUnit(object): input_file = input_files.join("deckhand_fullsite.yaml") design_ref = "file://%s" % str(input_file) - url = '/api/v1.0/nodes' - hdr = { - 'Content-Type': 'application/json', - 'X-IDENTITY-STATUS': 'Confirmed', - 'X-USER-NAME': 'Test', - 'X-ROLES': 'admin' - } + url = '/api/v1.0/nodefilter' + hdr = self.get_standard_header() body = { 'node_filter': 'filters', 'site_design': design_ref, @@ -50,15 +46,12 @@ class TestNodesApiUnit(object): LOG.debug(result.text) assert result.status == falcon.HTTP_200 + assert result.text.count('n1') == 1 + assert result.text.count('n2') == 1 def test_input_error(self, falcontest): - url = '/api/v1.0/nodes' - hdr = { - 'Content-Type': 'application/json', - 'X-IDENTITY-STATUS': 'Confirmed', - 'X-USER-NAME': 'Test', - 'X-ROLES': 'admin' - } + url = '/api/v1.0/nodefilter' + hdr = self.get_standard_header() body = {} result = falcontest.simulate_post( @@ -80,10 +73,25 @@ class TestNodesApiUnit(object): ingester=deckhand_ingester, orchestrator=deckhand_orchestrator)) + def get_standard_header(self): + hdr = { + 'Content-Type': 'application/json', + 'X-IDENTITY-STATUS': 'Confirmed', + 'X-USER-NAME': 'Test', + 'X-ROLES': 'admin' + } + return hdr + @pytest.fixture() def mock_process_node_filter(deckhand_orchestrator): def side_effect(**kwargs): - return [] + n1 = objects.BaremetalNode() + n1.name = 'n1' + n1.site = 'test1' + n2 = objects.BaremetalNode() + n2.name = 'n2' + n2.site = 'test2' + return [n1, n2] deckhand_orchestrator.real_process_node_filter = deckhand_orchestrator.process_node_filter deckhand_orchestrator.process_node_filter = Mock(side_effect=side_effect)