From f4c724fbf1653a1c66d2f34d2417ee960715cccb Mon Sep 17 00:00:00 2001 From: Smruti Soumitra Khuntia Date: Thu, 12 Jul 2018 16:58:53 +0530 Subject: [PATCH] A new Shipyard site statuses API and CLI A new Shipyard site statuses API and CLI supporting nodes provisoning status and node power state. This API can be further developed to support new status requirements by expanding the filters option. Change-Id: I620aefd82d4a17b616f3f253265605e519506257 --- docs/source/API.rst | 155 ++++++++++++- docs/source/CLI.rst | 71 ++++++ .../etc/shipyard/policy.yaml.sample | 3 + .../shipyard_airflow/control/api.py | 2 + .../control/helpers/status_helper.py | 155 +++++++++++++ .../control/status/__init__.py | 0 .../control/status/status_api.py | 46 ++++ .../shipyard_airflow/policy.py | 9 + .../tests/unit/control/test_status_api.py | 36 +++ .../tests/unit/control/test_status_helper.py | 217 ++++++++++++++++++ .../api_client/shipyard_api_client.py | 16 ++ .../shipyard_client/cli/cli_format_common.py | 76 ++++++ .../shipyard_client/cli/get/actions.py | 35 +++ .../shipyard_client/cli/get/commands.py | 40 +++- .../test_shipyard_api_client.py | 13 ++ 15 files changed, 869 insertions(+), 5 deletions(-) create mode 100644 src/bin/shipyard_airflow/shipyard_airflow/control/helpers/status_helper.py create mode 100644 src/bin/shipyard_airflow/shipyard_airflow/control/status/__init__.py create mode 100644 src/bin/shipyard_airflow/shipyard_airflow/control/status/status_api.py create mode 100644 src/bin/shipyard_airflow/tests/unit/control/test_status_api.py create mode 100644 src/bin/shipyard_airflow/tests/unit/control/test_status_helper.py diff --git a/docs/source/API.rst b/docs/source/API.rst index 53c9dd55..0dc5ff30 100644 --- a/docs/source/API.rst +++ b/docs/source/API.rst @@ -18,13 +18,14 @@ Shipyard API ============ -Logically, the API has three parts to handle the three areas of -functionality in Shipyard. +Logically, the API has several parts, each to handle each area of +Shipyard functionality: 1. Document Staging 2. Action Handling 3. Airflow Monitoring -4. Logs Retrieval +4. Site Statuses +5. Logs Retrieval Standards used by the API ------------------------- @@ -965,6 +966,154 @@ Example } +Site Statuses API +----------------- + +Site Statuses API retrieves node provision status and/or node power state +for all nodes in the site. + +/v1.0/site-statuses +~~~~~~~~~~~~~~~~~~~ + +GET /v1.0/site-statuses +^^^^^^^^^^^^^^^^^^^^^^^ +Returns the dictionary with nodes provision status and nodes power state status + +Query Parameters +'''''''''''''''' +- filters=nodes-provision-status,machines-power-state + filters query parameter allows to specify one or more status types to return statuses + of those types. The filter value ``nodes-provision-status`` will fetch provisioning + statuses of all nodes in the site. The filter value ``machines-power-state`` will fetch + power states of all baremetal machines in the site. By omitting the filters + query parameter, statuses of all status types will be returned. + +Responses +''''''''' +200 OK + If statuses are retrieved successfully. +400 Bad Request + If invalid filters option is given. + +Example +''''''' + +:: + + $ curl -X GET $URL/api/v1.0/site-statuses -H "X-Auth-Token:$TOKEN" + + HTTP/1.1 200 OK + x-shipyard-req: 0804d13e-08fc-4e60-a819-3b7532cac4ec + content-type: application/json; charset=UTF-8 + + { + { + "nodes-provision-status": [ + { + "hostname": "abc.xyz.com", + "status": "Ready" + }, + { + "hostname": "def.xyz.com", + "status": "Ready" + } + ], + "machines-power-state": [ + { + "hostname": "abc.xyz.com", + "power_state": "On", + }, + { + "hostname": "def.xyz.com", + "power_state": "On", + } + ] + } + } + +:: + + $ curl -X GET $URL/api/v1.0/site-statuses?filters=nodes-provision-status \ + -H "X-Auth-Token:$TOKEN" + + HTTP/1.1 200 OK + x-shipyard-req: 0804d13e-08fc-4e60-a819-3b7532cac4ec + content-type: application/json; charset=UTF-8 + + { + { + "nodes-provision-status": [ + { + "hostname": "abc.xyz.com", + "status": "Ready" + }, + { + "hostname": "def.xyz.com", + "status": "Ready" + } + ] + } + } + +:: + + $ curl -X GET $URL/api/v1.0/site-statuses?filters=machines-power-state \ + -H "X-Auth-Token:$TOKEN" + + HTTP/1.1 200 OK + x-shipyard-req: 0804d13e-08fc-4e60-a819-3b7532cac4ec + content-type: application/json; charset=UTF-8 + + { + { + "machines-power-state": [ + { + "hostname": "abc.xyz.com", + "power_state": "On", + }, + { + "hostname": "def.xyz.com", + "power_state": "On", + } + ] + } + } + + :: + + $ curl -X GET $URL/api/v1.0/site-statuses?filters=nodes-provision-status,machines-power-state \ + -H "X-Auth-Token:$TOKEN" + + HTTP/1.1 200 OK + x-shipyard-req: 0804d13e-08fc-4e60-a819-3b7532cac4ec + content-type: application/json; charset=UTF-8 + + { + { + "nodes-provision-status": [ + { + "hostname": "abc.xyz.com", + "status": "Ready" + }, + { + "hostname": "def.xyz.com", + "status": "Ready" + } + ], + "machines-power-state": [ + { + "hostname": "abc.xyz.com", + "power_state": "On", + }, + { + "hostname": "def.xyz.com", + "power_state": "On", + } + ] + } + } + + Logs Retrieval API ------------------ This API allows users to query and view logs. Its usuage is currently limited diff --git a/docs/source/CLI.rst b/docs/source/CLI.rst index 8ac0cba8..a37159a8 100644 --- a/docs/source/CLI.rst +++ b/docs/source/CLI.rst @@ -737,6 +737,77 @@ Sample deploy_site__2017-11-27T20:34:33.000000 failed update_site__2017-11-27T20:45:47.000000 running +get site-statuses +~~~~~~~~~~~~~~~~~ + +Retrieve the provisioning status of nodes and/or power states of the baremetal +machines in the site. If no option provided, retrieve records for both status types. + +:: + + shipyard get site-statuses + [--status-type=] (repeatable) + | + + Example: + shipyard get site-statuses + shipyard get site-statuses --status-type=nodes-provision-status + shipyard get site-statuses --status-type=machines-power-state + shipyard get site-statuses --status-type=nodes-provision-status --status-type=machines-power-state + +\--status-type= + Retrieve provisioning statuses of all nodes for status-type + "nodes-provision-status" and retrieve power states of all baremetal + machines in the site for status-type "machines-power-state". + +Sample +^^^^^^ + +:: + + $ shipyard get site-statuses + + Nodes Provision Status: + Hostname Status + abc.xyz.com Ready + def.xyz.com Deploying + + Machines Power State: + Hostname Power State + abc.xyz.com On + def.xyz.com On + +:: + + $ shipyard get site-statuses --status-type=nodes-provision-status + + Nodes Provision Status: + Hostname Status + abc.xyz.com Ready + def.xyz.com Deploying + +:: + + $ shipyard get site-statuses --status-type=nodes-power-state + + Machines Power State: + Hostname Power State + abc.xyz.com On + def.xyz.com On + +:: + + $ shipyard get site-statuses --status-type=nodes-provision-status --status-type=nodes-power-state + + Nodes Provision Status: + Hostname Status + abc.xyz.com Ready + def.xyz.com Deploying + + Machines Power State: + Hostname Power State + abc.xyz.com On + def.xyz.com On Logs Commands ------------- diff --git a/src/bin/shipyard_airflow/etc/shipyard/policy.yaml.sample b/src/bin/shipyard_airflow/etc/shipyard/policy.yaml.sample index a03094df..ffbdc7ef 100644 --- a/src/bin/shipyard_airflow/etc/shipyard/policy.yaml.sample +++ b/src/bin/shipyard_airflow/etc/shipyard/policy.yaml.sample @@ -59,3 +59,6 @@ # GET /api/v1.0/workflows/{id} #"workflow_orchestrator:get_workflow": "rule:admin_required" +# Retrieve the status for node provision status +# GET /api/v1.0/site_statuses +#"workflow_orchestrator:get_site_statuses": "rule:admin_required" diff --git a/src/bin/shipyard_airflow/shipyard_airflow/control/api.py b/src/bin/shipyard_airflow/shipyard_airflow/control/api.py index 7f204cfa..d7016014 100644 --- a/src/bin/shipyard_airflow/shipyard_airflow/control/api.py +++ b/src/bin/shipyard_airflow/shipyard_airflow/control/api.py @@ -40,6 +40,7 @@ from shipyard_airflow.control.health import HealthResource from shipyard_airflow.control.middleware.auth import AuthMiddleware from shipyard_airflow.control.middleware.context import ContextMiddleware from shipyard_airflow.control.middleware.logging_mw import LoggingMiddleware +from shipyard_airflow.control.status.status_api import StatusResource from shipyard_airflow.errors import (AppError, default_error_serializer, default_exception_handler) @@ -77,6 +78,7 @@ def start_api(): ('/renderedconfigdocs', RenderedConfigDocsResource()), ('/workflows', WorkflowResource()), ('/workflows/{workflow_id}', WorkflowIdResource()), + ('/site_statuses', StatusResource()), ] # Set up the 1.0 routes diff --git a/src/bin/shipyard_airflow/shipyard_airflow/control/helpers/status_helper.py b/src/bin/shipyard_airflow/shipyard_airflow/control/helpers/status_helper.py new file mode 100644 index 00000000..2e2df2cf --- /dev/null +++ b/src/bin/shipyard_airflow/shipyard_airflow/control/helpers/status_helper.py @@ -0,0 +1,155 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Status helper is a layer for status API, which interacts with +multiple components to fetch required status as per the filter +values. +""" +import logging + +from drydock_provisioner import error as dderrors +import falcon +from oslo_config import cfg + +import shipyard_airflow.control.service_clients as sc +from shipyard_airflow.errors import ApiError, AppError + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + +NODE_PROVISION_STATUS = 'nodes-provision-status' +MACHINES_POWER_STATE = 'machines-power-state' + +# This will be expanded with new status filters to support more status types. +valid_filters = [NODE_PROVISION_STATUS, MACHINES_POWER_STATE] + + +def get_machines_powerstate(drydock): + # Calls Drydock client to fetch nodes power state + try: + machines = drydock.get_nodes() + + machines_ps = [] + for machine in machines: + machines_ps.append( + { + 'hostname': machine.get('hostname'), + 'power_state': machine.get('power_state') + }) + except dderrors.ClientError as ddex: + raise AppError( + title='Unable to retrieve nodes power-state', + description=( + 'Drydock has responded unexpectedly: {}'.format( + ddex.response_message)), + status=falcon.HTTP_500, + retry=False, ) + + machines_powerstate = {'machines_powerstate': machines_ps} + + return machines_powerstate + + +def get_nodes_provision_status(drydock): + # Calls Drydock client to fetch node provision status + try: + nodes = drydock.get_nodes() + + nodes_status = [] + for node in nodes: + nodes_status.append( + { + 'hostname': node.get('hostname'), + 'status': node.get('status_name') + }) + except dderrors.ClientError as ddex: + raise AppError( + title='Unable to retrieve nodes status', + description=( + 'Drydock has responded unexpectedly: ' + '{}'.format(ddex.response_message)), + status=falcon.HTTP_500, + retry=False, ) + + machine_status = {'nodes_provision_status': nodes_status} + + return machine_status + + +class StatusHelper(object): + """ + StatusHelper provides a layer to fetch statuses from respective + components based on filter values. + A new status_helper is intended to be used for each invocation.. + """ + + def __init__(self, context): + """ + Sets up this status helper with the supplied + request context + """ + # Instantiate the client for a component api interaction + self.drydock = None + self.ctx = context + + def get_site_statuses(self, sts_filters=None): + """ + :param sts_filters: A list of filters representing statuses + that needs to be fetched + + Returns dictionary of statuses + """ + # check for filters else set to all valid filters. + if sts_filters: + pass + else: + sts_filters = valid_filters + + LOG.debug("Filters for status search are %s", sts_filters) + + # check for valid status filters + for sts_filter in sts_filters: + if sts_filter not in valid_filters: + raise ApiError( + title='Not a valid status filter', + description='filter {} is not supported'.format( + sts_filter), + status=falcon.HTTP_400, + retry=False) + + # get Drydock client + if not self.drydock: + self.drydock = sc.drydock_client() + + statuses = {} + # iterate through filters to invoke required fun + for sts_filter in sts_filters: + call_func = self._switcher(sts_filter) + status = call_func(self.drydock) + statuses.update(status) + + return statuses + + def _switcher(self, fltr): + # Helper that returns mapped function name as per filter + + status_func_switcher = { + NODE_PROVISION_STATUS: get_nodes_provision_status, + MACHINES_POWER_STATE: get_machines_powerstate, + } + + call_func = status_func_switcher.get(fltr, lambda: None) + + # return the function name from switcher dictionary + return call_func diff --git a/src/bin/shipyard_airflow/shipyard_airflow/control/status/__init__.py b/src/bin/shipyard_airflow/shipyard_airflow/control/status/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/bin/shipyard_airflow/shipyard_airflow/control/status/status_api.py b/src/bin/shipyard_airflow/shipyard_airflow/control/status/status_api.py new file mode 100644 index 00000000..a7d0449f --- /dev/null +++ b/src/bin/shipyard_airflow/shipyard_airflow/control/status/status_api.py @@ -0,0 +1,46 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +import falcon + +from shipyard_airflow import policy +from shipyard_airflow.control.helpers.status_helper import ( + StatusHelper) +from shipyard_airflow.control.base import BaseResource + +LOG = logging.getLogger(__name__) + + +# /api/v1.0/site_statuses +class StatusResource(BaseResource): + """ + The status resource handles the retrieval of Drydock provisioning + node status and power state + """ + + @policy.ApiEnforcer('workflow_orchestrator:get_site_statuses') + def on_get(self, req, resp, **kwargs): + """ + Return site based statuses that has been invoked through shipyard. + :returns: a json array of site status entities + """ + status_filters = req.get_param(name='filters') or None + if status_filters: + fltrs = status_filters.split(',') + else: + fltrs = None + helper = StatusHelper(req.context) + resp.body = self.to_json(helper.get_site_statuses(fltrs)) + resp.status = falcon.HTTP_200 diff --git a/src/bin/shipyard_airflow/shipyard_airflow/policy.py b/src/bin/shipyard_airflow/shipyard_airflow/policy.py index 4a22bf8e..e68ba1ca 100644 --- a/src/bin/shipyard_airflow/shipyard_airflow/policy.py +++ b/src/bin/shipyard_airflow/shipyard_airflow/policy.py @@ -174,6 +174,15 @@ class ShipyardPolicy(object): 'method': 'GET' }] ), + policy.DocumentedRuleDefault( + 'workflow_orchestrator:get_site_statuses', + RULE_ADMIN_REQUIRED, + 'Retrieve the statuses for the site', + [{ + 'path': '/api/v1.0/site_statuses', + 'method': 'GET' + }] + ), ] # Regions Policy diff --git a/src/bin/shipyard_airflow/tests/unit/control/test_status_api.py b/src/bin/shipyard_airflow/tests/unit/control/test_status_api.py new file mode 100644 index 00000000..cea044bb --- /dev/null +++ b/src/bin/shipyard_airflow/tests/unit/control/test_status_api.py @@ -0,0 +1,36 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" Tests for the status_api""" +import json +from unittest.mock import patch + +from shipyard_airflow.control.helpers.status_helper import \ + StatusHelper +from shipyard_airflow.control.base import ShipyardRequestContext +from tests.unit.control import common + +CTX = ShipyardRequestContext() + + +class TestStatusResource(): + @patch.object(StatusHelper, 'get_site_statuses', + common.str_responder) + def test_on_get(self, api_client): + """Validate the on_get method returns 200 on success""" + result = api_client.simulate_get( + "/api/v1.0/site_statuses", headers=common.AUTH_HEADERS) + assert result.status_code == 200 + assert result.text == json.dumps(common.str_responder(), default=str) + assert result.headers[ + 'content-type'] == 'application/json; charset=UTF-8' diff --git a/src/bin/shipyard_airflow/tests/unit/control/test_status_helper.py b/src/bin/shipyard_airflow/tests/unit/control/test_status_helper.py new file mode 100644 index 00000000..8ac8e734 --- /dev/null +++ b/src/bin/shipyard_airflow/tests/unit/control/test_status_helper.py @@ -0,0 +1,217 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unittest import mock + +from shipyard_airflow.control.base import ShipyardRequestContext +from shipyard_airflow.control.helpers.status_helper import ( + StatusHelper) +import shipyard_airflow.control.helpers.status_helper as sh + + +CTX = ShipyardRequestContext() + +MACH_STATUS_DICT = { + 'nodes_provision_status': [ + { + 'hostname': 'abc.xyz.com', + 'status': 'READY' + }, + { + 'hostname': 'def.xyz.com', + 'status': 'DEPLOYING' + } + ] +} + +MACH_POWERSTATE_DICT = { + 'machines_powerstate': [ + { + 'hostname': 'abc.xyz.com', + 'power_state': 'ON' + }, + { + 'hostname': 'def.xyz.com', + 'power_state': 'ON' + } + ] +} + +STATUS_LIST = [ + { + 'hostname': 'abc.xyz.com', + 'status': 'READY' + }, + { + 'hostname': 'def.xyz.com', + 'status': 'DEPLOYING' + } +] + +MACH_PS_LIST = [ + { + 'hostname': 'abc.xyz.com', + 'power_state': 'ON' + }, + { + 'hostname': 'def.xyz.com', + 'power_state': 'ON' + } +] + + +ALL_STATUSES_DICT = { + 'nodes_provision_status': [ + { + 'hostname': 'abc.xyz.com', + 'status': 'READY' + }, + { + 'hostname': 'def.xyz.com', + 'status': 'DEPLOYING' + } + ], + 'machines_powerstate': [ + { + 'hostname': 'abc.xyz.com', + 'power_state': 'ON' + }, + { + 'hostname': 'def.xyz.com', + 'power_state': 'ON' + } + ] +} + +NODE_LIST = [ + { + 'hostname': 'abc.xyz.com', + 'memory': 12800, + 'cpu_count': 32, + 'status_name': 'READY', + 'boot_mac': '08:00:27:76:c1:2c', + 'power_state': 'ON', + 'power_address': 'dummy', + 'boot_ip': '1.2.3.4' + }, + { + 'hostname': 'def.xyz.com', + 'memory': 12800, + 'cpu_count': 32, + 'status_name': 'DEPLOYING', + 'boot_mac': '08:00:27:76:c1:2e', + 'power_state': 'ON', + 'power_address': 'dummy', + 'boot_ip': '1.2.3.5' + } +] + +NODE_PROVISION_STATUS = 'nodes-provision-status' +MACHINES_POWER_STATE = 'machines-power-state' + + +def test_construct_status_helper(): + """ + Creates a status helper, tests that the context + is passed to the sub-helper + """ + helper = StatusHelper(CTX) + assert helper.ctx == CTX + + +@mock.patch( + 'shipyard_airflow.control.helpers.status_helper.get_machines_powerstate', + return_value=MACH_POWERSTATE_DICT) +@mock.patch( + 'shipyard_airflow.control.helpers.status_helper' + '.get_nodes_provision_status', + return_value=MACH_STATUS_DICT) +def test_get_site_statuses(patch1, patch2): + """ + Testing status according to filter values + """ + helper = StatusHelper(CTX) + + helper.drydock = 'Dummy' + # test with filter for machine provision status + ret_mach_status = helper.get_site_statuses([NODE_PROVISION_STATUS]) + prov_status_list = ret_mach_status.get('nodes_provision_status') + + assert STATUS_LIST == sorted(prov_status_list, key=lambda x: x['hostname']) + + # test with filter for machine power state + ret_mach_powerstate = helper.get_site_statuses([MACHINES_POWER_STATE]) + mach_ps_list = ret_mach_powerstate.get('machines_powerstate') + + assert MACH_PS_LIST == sorted(mach_ps_list, key=lambda x: x['hostname']) + + # test without filters + ret_wo_filters = helper.get_site_statuses() + psl_wo = ret_wo_filters.get('nodes_provision_status') + + assert STATUS_LIST == sorted(psl_wo, key=lambda x: x['hostname']) + + mpl_wo = ret_wo_filters.get('machines_powerstate') + + assert MACH_PS_LIST == sorted(mpl_wo, key=lambda x: x['hostname']) + + # test with both filters + all_filters = [NODE_PROVISION_STATUS, MACHINES_POWER_STATE] + ret_all_statuses = helper.get_site_statuses(all_filters) + psl_with = ret_all_statuses.get('nodes_provision_status') + + assert STATUS_LIST == sorted(psl_with, key=lambda x: x['hostname']) + + mpl_with = ret_all_statuses.get('machines_powerstate') + + assert MACH_PS_LIST == sorted(mpl_with, key=lambda x: x['hostname']) + + +@mock.patch("drydock_provisioner.drydock_client.client.DrydockClient") +def test_get_machines_powerstate(drydock): + """ + Tests the functionality of the get_machines_powerstate method + """ + drydock.get_nodes.return_value = NODE_LIST + mach_ps_dict = sh.get_machines_powerstate(drydock) + actual = mach_ps_dict.get('machines_powerstate') + expected = MACH_POWERSTATE_DICT.get('machines_powerstate') + + assert actual == sorted(expected, key=lambda x: x['hostname']) + + +@mock.patch("drydock_provisioner.drydock_client.client.DrydockClient") +def test_get_nodes_provision_status(drydock): + """ + Tests the functionality of the get_nodes_provision_status method + """ + drydock.get_nodes.return_value = NODE_LIST + nodes_provision_status = sh.get_nodes_provision_status(drydock) + actual = nodes_provision_status.get('nodes_provision_status') + expected = MACH_STATUS_DICT.get('nodes_provision_status') + + assert actual == sorted(expected, key=lambda x: x['hostname']) + + +def test__switcher(): + """ + Tests the functionality of the _switcher() method + """ + helper = StatusHelper(CTX) + pns = "get_nodes_provision_status" + mps = "get_machines_powerstate" + actual_pns = helper._switcher(NODE_PROVISION_STATUS) + actual_mps = helper._switcher(MACHINES_POWER_STATE) + + assert pns in str(actual_pns) + assert mps in str(actual_mps) diff --git a/src/bin/shipyard_client/shipyard_client/api_client/shipyard_api_client.py b/src/bin/shipyard_client/shipyard_client/api_client/shipyard_api_client.py index 744ad42c..4eae8f04 100644 --- a/src/bin/shipyard_client/shipyard_client/api_client/shipyard_api_client.py +++ b/src/bin/shipyard_client/shipyard_client/api_client/shipyard_api_client.py @@ -36,6 +36,7 @@ class ApiPaths(enum.Enum): POST_CONTROL_ACTION = _BASE_URL + 'actions/{}/control/{}' GET_WORKFLOWS = _BASE_URL + 'workflows' GET_DAG_DETAIL = _BASE_URL + 'workflows/{}' + GET_SITE_STATUSES = _BASE_URL + 'site_statuses' class ShipyardClient(BaseClient): @@ -246,3 +247,18 @@ class ShipyardClient(BaseClient): url = ApiPaths.GET_DAG_DETAIL.value.format(self.get_endpoint(), workflow_id) return self.get_resp(url) + + def get_site_statuses(self, fltrs=None): + """ + Get statuses for the site. + :param str filters: status-types for site statuses to retrieve. + :rtype: Response object + """ + if fltrs: + query_params = {'filters': fltrs} + else: + query_params = {} + + url = ApiPaths.GET_SITE_STATUSES.value.format( + self.get_endpoint()) + return self.get_resp(url, query_params) diff --git a/src/bin/shipyard_client/shipyard_client/cli/cli_format_common.py b/src/bin/shipyard_client/shipyard_client/cli/cli_format_common.py index 98ff7a8e..11500e59 100644 --- a/src/bin/shipyard_client/shipyard_client/cli/cli_format_common.py +++ b/src/bin/shipyard_client/shipyard_client/cli/cli_format_common.py @@ -260,3 +260,79 @@ def gen_sub_workflows(wf_list): for wf in wf_list: wfs.append(gen_workflow_details(wf)) return '\n\n'.join(wfs) + + +def gen_site_statuses(status_dict): + """Generates site statuses table. + + Assumes status_types as list of filters and status_dict a dictionary + with statuses lists + """ + formatted_output = '' + + status_types = status_dict.keys() + + for st in status_types: + call_func = _site_statuses_switcher(st) + op = call_func(status_dict) + formatted_output = "{}\n{}\n".format(formatted_output, op) + + return formatted_output + + +def _gen_machines_powerstate_table(status_dict): + # Generates machines power states status table + + machine_powerstate_table = format_utils.table_factory( + field_names=['Hostname', 'Power State']) + + pwrstate_list = status_dict.get('machines_powerstate') + + if pwrstate_list: + for pwrstate in pwrstate_list: + machine_powerstate_table.add_row( + [pwrstate.get('hostname'), + pwrstate.get('power_state')]) + else: + machine_powerstate_table.add_row(['', '']) + + return format_utils.table_get_string(table=machine_powerstate_table, + title="Machines Power State:", + vertical_char=' ') + + +def _gen_nodes_provision_status_table(status_dict): + # Generates nodes provision status table + + nodes_status_table = format_utils.table_factory( + field_names=['Hostname', 'Status']) + prov_status_list = status_dict.get('nodes_provision_status') + + if prov_status_list: + for status in prov_status_list: + nodes_status_table.add_row( + [status.get('hostname'), + status.get('status')]) + else: + nodes_status_table.add_row(['', '']) + + return format_utils.table_get_string(table=nodes_status_table, + title="Nodes Provision Status:", + vertical_char=' ') + + +def _site_statuses_switcher(status_type): + """Maps status types with a callabe function to the format + output. + + The dictionary will be updated with new functions + to map future supported status-types for "site-statuses" + """ + status_func_switcher = { + 'nodes_provision_status': _gen_nodes_provision_status_table, + 'machines_powerstate': _gen_machines_powerstate_table, + } + + call_func = status_func_switcher.get(status_type, lambda: None) + + return call_func diff --git a/src/bin/shipyard_client/shipyard_client/cli/get/actions.py b/src/bin/shipyard_client/shipyard_client/cli/get/actions.py index 0c600c5a..27bc4f85 100644 --- a/src/bin/shipyard_client/shipyard_client/cli/get/actions.py +++ b/src/bin/shipyard_client/shipyard_client/cli/get/actions.py @@ -175,3 +175,38 @@ class GetWorkflows(CliAction): resp_j = response.json() wf_list = resp_j if resp_j else [] return cli_format_common.gen_workflow_table(wf_list) + + +class GetSiteStatuses(CliAction): + """Action to get the site statuses""" + + def __init__(self, ctx, fltr=None): + """Sets parameters.""" + super().__init__(ctx) + self.logger.debug("GetSiteStatuses action with filter %s " + "invoked", fltr) + self.fltr = fltr + + def invoke(self): + """Calls API Client and formats response from API Client""" + self.logger.debug("Calling API Client get_site_statuses") + + return self.get_api_client().get_site_statuses(self.fltr) + + # Handle 400 with default error handler for cli. + cli_handled_err_resp_codes = [400] + + # Handle 200 responses using the cli_format_response_handler + cli_handled_succ_resp_codes = [200] + + def cli_format_response_handler(self, response): + """CLI output handler + + :param response: a requests response object + :returns: a string representing a CLI appropriate response + Handles 200 responses + """ + resp_j = response.json() + status_dict = resp_j if resp_j else [] + + return cli_format_common.gen_site_statuses(status_dict) diff --git a/src/bin/shipyard_client/shipyard_client/cli/get/commands.py b/src/bin/shipyard_client/shipyard_client/cli/get/commands.py index be2704ed..fdc6db49 100644 --- a/src/bin/shipyard_client/shipyard_client/cli/get/commands.py +++ b/src/bin/shipyard_client/shipyard_client/cli/get/commands.py @@ -21,6 +21,7 @@ from shipyard_client.cli.get.actions import GetConfigdocs from shipyard_client.cli.get.actions import GetConfigdocsStatus from shipyard_client.cli.get.actions import GetRenderedConfigdocs from shipyard_client.cli.get.actions import GetWorkflows +from shipyard_client.cli.get.actions import GetSiteStatuses from shipyard_client.cli.input_checks import check_reformat_versions @@ -63,8 +64,8 @@ EXAMPLE: shipyard get configdocs --colllection=design """ SHORT_DESC_CONFIGDOCS = ("Retrieve documents loaded into Shipyard, either " - "committed, last site action, successful site action " - "or from the Shipyard Buffer. Allows comparison " + "committed, last site action, successful site action" + " or from the Shipyard Buffer. Allows comparison " "between 2 revisions using valid revision tags") @@ -229,3 +230,38 @@ def get_version(ctx, buffer, committed, last_site_action, else: return 'buffer' + + +DESC_STATUS = """ +COMMAND: status +DESCRIPTION: Retrieve statuses of different status types for the site. +Supported status types are nodes-provision-status and machines-power-state. \n +Status type nodes-provision-status will fetch provisioning status for all nodes +and machines-power-state will fetch power state for all baremetal machines +in the site. Supports fetching statuses of multiple types. Without status-type +option, command fetches statuses of all status types. \n +FORMAT: shipyard get site-statuses [--status-type=] (repeatable)\n +EXAMPLE: shipyard get status --status-type=nodes-provision-status \ + --status-type=machines-power-state +""" + +SHORT_DESC_STATUS = "Retrieve statuses for the site." + + +@get.command( + name='site-statuses', + help=DESC_STATUS, + short_help=SHORT_DESC_STATUS) +@click.option( + '--status-type', + '-s', + multiple=True, + help='Fetches statuses of specific status type.(repeatable) \n' + 'Supported status types are: \n' + 'nodes-provision-status \n' + 'machines-power-state') +@click.pass_context +def get_site_statuses(ctx, status_type): + + fltr = ",".join(status_type) + click.echo(GetSiteStatuses(ctx, fltr).invoke_and_return_resp()) diff --git a/src/bin/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py b/src/bin/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py index f4d54ae3..c8776b9a 100644 --- a/src/bin/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py +++ b/src/bin/shipyard_client/tests/unit/apiclient_test/test_shipyard_api_client.py @@ -234,3 +234,16 @@ def test_get_step_log(*args): assert result['url'] == '{}/actions/{}/steps/{}/logs'.format( shipyard_client.get_endpoint(), action_id, step_id) assert params['try'] == try_number + + +@mock.patch.object(BaseClient, 'post_resp', replace_post_rep) +@mock.patch.object(BaseClient, 'get_resp', replace_get_resp) +@mock.patch.object(BaseClient, 'get_endpoint', replace_get_endpoint) +def test_get_site_statuses(*args): + shipyard_client = get_api_client() + fltrs = 'nodes-provision-status,machines-power-state' + result = shipyard_client.get_site_statuses(fltrs=fltrs) + params = result['params'] + assert result['url'] == '{}/site_statuses'.format( + shipyard_client.get_endpoint()) + assert params['filters'] == fltrs