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