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
|
||||
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
|
||||
--------
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user