Add Drydock API Client Options

- Added builddata, subtaskerrors, and layers as options to get_task.
- Added get_nodes_for_filter to call POST /nodefilter.
- Updated endpoint for POST /nodes to POST /nodefilter.
- Made node_filter optional in POST /nodefilter.

Change-Id: I456a6e9991d03af3d375c448f5cbf07a21e91f1d
This commit is contained in:
Aaron Sheffield 2018-05-02 14:29:49 -05:00 committed by Scott Hussey
parent 3b41868802
commit d052664f74
6 changed files with 115 additions and 69 deletions

View File

@ -29,13 +29,6 @@ GET nodes
The Nodes API will provide a report of current nodes as known by the node provisioner The Nodes API will provide a report of current nodes as known by the node provisioner
and their status with a few hardware details. 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 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 the most recently collected data for each ``generator`` will be included in the
response. 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 bootdata
-------- --------

View File

@ -22,6 +22,7 @@ from .tasks import TasksResource
from .tasks import TaskResource from .tasks import TaskResource
from .nodes import NodesResource from .nodes import NodesResource
from .nodes import NodeBuildDataResource from .nodes import NodeBuildDataResource
from .nodes import NodeFilterResource
from .health import HealthResource from .health import HealthResource
from .health import HealthExtendedResource from .health import HealthExtendedResource
from .bootaction import BootactionUnitsResource from .bootaction import BootactionUnitsResource
@ -81,12 +82,14 @@ def start_api(state_manager=None, ingester=None, orchestrator=None):
state_manager=state_manager, orchestrator=orchestrator)), state_manager=state_manager, orchestrator=orchestrator)),
# API to list current MaaS nodes # API to list current MaaS nodes
('/nodes', ('/nodes', NodesResource()),
NodesResource(state_manager=state_manager,
orchestrator=orchestrator)),
# API to get build data for a node # API to get build data for a node
('/nodes/{hostname}/builddata', ('/nodes/{hostname}/builddata',
NodeBuildDataResource(state_manager=state_manager)), 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 # API for nodes to discover their boot actions during curtin install
('/bootactions/nodes/{hostname}/units', ('/bootactions/nodes/{hostname}/units',
BootactionUnitsResource( BootactionUnitsResource(

View File

@ -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.api_client import MaasRequestFactory
from drydock_provisioner.drivers.node.maasdriver.models.machine import Machines from drydock_provisioner.drivers.node.maasdriver.models.machine import Machines
from .base import StatefulResource from .base import BaseResource, StatefulResource
class NodesResource(StatefulResource): class NodesResource(BaseResource):
def __init__(self, orchestrator=None, **kwargs): def __init__(self):
"""Object initializer. super().__init__()
:param orchestrator: instance of orchestrator.Orchestrator
"""
super().__init__(**kwargs)
self.orchestrator = orchestrator
@policy.ApiEnforcer('physical_provisioner:read_data') @policy.ApiEnforcer('physical_provisioner:read_data')
def on_get(self, req, resp): def on_get(self, req, resp):
@ -63,37 +58,6 @@ class NodesResource(StatefulResource):
self.return_error( self.return_error(
resp, falcon.HTTP_500, message="Unknown error", retry=False) 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): class NodeBuildDataResource(StatefulResource):
"""Resource for returning build data for a node.""" """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.error(req.context, "Unknown error: %s" % str(ex), exc_info=ex)
self.return_error( self.return_error(
resp, falcon.HTTP_500, message="Unknown error", retry=False) 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)

View File

@ -39,6 +39,24 @@ class DrydockClient(object):
return resp.json() 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): def get_tasks(self):
""" """
Get a list of all the tasks, completed or running. Get a list of all the tasks, completed or running.
@ -54,16 +72,31 @@ class DrydockClient(object):
return resp.json() 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 Get the current description of a Drydock task
:param string task_id: The string uuid task id to query :param string task_id: The string uuid task id to query.
:return: A dict representing the current state of the task :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) 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) resp = self.session.get(endpoint)
self._check_response(resp) self._check_response(resp)

View File

@ -416,7 +416,7 @@ class Orchestrator(object):
return self.list_intersection(*result_sets) return self.list_intersection(*result_sets)
else: else:
raise errors.OrchestratorError( 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): def process_filter(self, node_set, filter_set):
"""Take a filter and apply it to the node_set. """Take a filter and apply it to the node_set.

View File

@ -21,6 +21,7 @@ import logging
from drydock_provisioner import policy from drydock_provisioner import policy
from drydock_provisioner.control.api import start_api from drydock_provisioner.control.api import start_api
import drydock_provisioner.objects as objects
import falcon import falcon
@ -33,13 +34,8 @@ class TestNodesApiUnit(object):
input_file = input_files.join("deckhand_fullsite.yaml") input_file = input_files.join("deckhand_fullsite.yaml")
design_ref = "file://%s" % str(input_file) design_ref = "file://%s" % str(input_file)
url = '/api/v1.0/nodes' url = '/api/v1.0/nodefilter'
hdr = { hdr = self.get_standard_header()
'Content-Type': 'application/json',
'X-IDENTITY-STATUS': 'Confirmed',
'X-USER-NAME': 'Test',
'X-ROLES': 'admin'
}
body = { body = {
'node_filter': 'filters', 'node_filter': 'filters',
'site_design': design_ref, 'site_design': design_ref,
@ -50,15 +46,12 @@ class TestNodesApiUnit(object):
LOG.debug(result.text) LOG.debug(result.text)
assert result.status == falcon.HTTP_200 assert result.status == falcon.HTTP_200
assert result.text.count('n1') == 1
assert result.text.count('n2') == 1
def test_input_error(self, falcontest): def test_input_error(self, falcontest):
url = '/api/v1.0/nodes' url = '/api/v1.0/nodefilter'
hdr = { hdr = self.get_standard_header()
'Content-Type': 'application/json',
'X-IDENTITY-STATUS': 'Confirmed',
'X-USER-NAME': 'Test',
'X-ROLES': 'admin'
}
body = {} body = {}
result = falcontest.simulate_post( result = falcontest.simulate_post(
@ -80,10 +73,25 @@ class TestNodesApiUnit(object):
ingester=deckhand_ingester, ingester=deckhand_ingester,
orchestrator=deckhand_orchestrator)) 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() @pytest.fixture()
def mock_process_node_filter(deckhand_orchestrator): def mock_process_node_filter(deckhand_orchestrator):
def side_effect(**kwargs): 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.real_process_node_filter = deckhand_orchestrator.process_node_filter
deckhand_orchestrator.process_node_filter = Mock(side_effect=side_effect) deckhand_orchestrator.process_node_filter = Mock(side_effect=side_effect)