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
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
--------

View File

@ -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(

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.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)

View File

@ -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)

View File

@ -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.

View File

@ -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)