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:
parent
3b41868802
commit
d052664f74
|
@ -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
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue