From 246775da422db523304a5d27f45bba6c18789d2e Mon Sep 17 00:00:00 2001 From: Scott Hussey Date: Fri, 20 Jul 2018 08:35:52 -0500 Subject: [PATCH] Add build data access to Drydock client - Add CLI actions to output the build data for a node or a task - Add API methods to access the Drydock API to retrieve node or task build data Change-Id: I0ee01bd4b165b93c2bc0e3050554514ba40f152a --- Makefile | 4 +- drydock_provisioner/cli/node/actions.py | 19 ++++++ drydock_provisioner/cli/node/commands.py | 26 ++++++++ drydock_provisioner/cli/task/actions.py | 15 +++++ drydock_provisioner/cli/task/commands.py | 25 ++++++++ .../drivers/node/maasdriver/actions/node.py | 13 ++-- drydock_provisioner/drydock_client/client.py | 25 ++++++++ drydock_provisioner/objects/site.py | 1 + tests/unit/test_cli_task.py | 59 +++++++++++++++++++ 9 files changed, 179 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index fd974d19..89fbcabf 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ external_dep: requirements-host.txt # Run unit and Postgres integration tests in coverage mode .PHONY: coverage_test -coverage_test: build_drydock external_dep +coverage_test: build_drydock tox -re cover # Run just unit tests @@ -101,7 +101,7 @@ helm-install: # Make targets intended for use by the primary targets above. .PHONY: build_drydock -build_drydock: +build_drydock: external_dep ifeq ($(USE_PROXY), true) docker build --network host -t $(IMAGE) --label $(LABEL) -f images/drydock/Dockerfile \ --build-arg http_proxy=$(PROXY) \ diff --git a/drydock_provisioner/cli/node/actions.py b/drydock_provisioner/cli/node/actions.py index eadb797b..cbcd9875 100644 --- a/drydock_provisioner/cli/node/actions.py +++ b/drydock_provisioner/cli/node/actions.py @@ -30,3 +30,22 @@ class NodeList(CliAction): # pylint: disable=too-few-public-methods def invoke(self): return self.api_client.get_nodes() + + +class NodeBuildData(CliAction): + """ Action to print node build data.""" + + def __init__(self, api_client, nodename, latest): + """ + :param DrydockClient api_client: the api client used for invocation. + :param str nodename: The name of the node to retrieve data for. + :param bool latest: If only the latest build data should be retrieved. + """ + super().__init__(api_client) + self.nodename = nodename + self.latest = latest + self.logger.debug('NodeBuildData action initialized') + + def invoke(self): + return self.api_client.get_node_build_data( + self.nodename, latest=self.latest) diff --git a/drydock_provisioner/cli/node/commands.py b/drydock_provisioner/cli/node/commands.py index 9e270d74..40ed14de 100644 --- a/drydock_provisioner/cli/node/commands.py +++ b/drydock_provisioner/cli/node/commands.py @@ -16,10 +16,12 @@ """ import click import json +import yaml from prettytable import PrettyTable from drydock_provisioner.cli.node.actions import NodeList +from drydock_provisioner.cli.node.actions import NodeBuildData @click.group() @@ -54,3 +56,27 @@ def node_list(ctx, output='table'): click.echo(pt) elif output == 'json': click.echo(json.dumps(nodelist)) + + +@node.command(name='builddata') +@click.option( + '--latest/--no-latest', + help='Retrieve only the latest data items.', + default=True) +@click.option( + '--output', '-o', help='Output format: yaml|json', default='yaml') +@click.argument('nodename') +@click.pass_context +def node_builddata(ctx, nodename, latest=True, output='yaml'): + """List build data for ``nodename``.""" + node_bd = NodeBuildData(ctx.obj['CLIENT'], nodename, latest).invoke() + + if output == 'json': + click.echo(json.dumps(node_bd)) + else: + if output != 'yaml': + click.echo( + "Invalid output format {}, default to YAML.".format(output)) + click.echo( + yaml.safe_dump( + node_bd, allow_unicode=True, default_flow_style=False)) diff --git a/drydock_provisioner/cli/task/actions.py b/drydock_provisioner/cli/task/actions.py index 6d185d07..18da7ea6 100644 --- a/drydock_provisioner/cli/task/actions.py +++ b/drydock_provisioner/cli/task/actions.py @@ -141,3 +141,18 @@ class TaskShow(CliAction): # pylint: disable=too-few-public-methods task = self.api_client.get_task(task_id=task_id) if task.status in [TaskStatus.Complete, TaskStatus.Terminated]: return task + + +class TaskBuildData(CliAction): + """Action to retrieve task build data.""" + + def __init__(self, api_client, task_id): + """ + :param DrydockClient api_client: the api client instance used for invocation. + :param str task_id: A UUID-like task_id + """ + super().__init__(api_client) + self.task_id = task_id + + def invoke(self): + return self.api_client.get_task_build_data(self.task_id) diff --git a/drydock_provisioner/cli/task/commands.py b/drydock_provisioner/cli/task/commands.py index 626d06f8..1e592c93 100644 --- a/drydock_provisioner/cli/task/commands.py +++ b/drydock_provisioner/cli/task/commands.py @@ -14,10 +14,12 @@ """Contains commands related to tasks against designs.""" import click import json +import yaml from drydock_provisioner.cli.task.actions import TaskList from drydock_provisioner.cli.task.actions import TaskShow from drydock_provisioner.cli.task.actions import TaskCreate +from drydock_provisioner.cli.task.actions import TaskBuildData @click.group() @@ -105,3 +107,26 @@ def task_show(ctx, task_id=None, block=False): click.echo( json.dumps(TaskShow(ctx.obj['CLIENT'], task_id=task_id).invoke())) + + +@task.command(name='builddata') +@click.option('--task-id', '-t', help='The required task id') +@click.option( + '--output', '-o', help='The output format (yaml|json)', default='yaml') +@click.pass_context +def task_builddata(ctx, task_id=None, output='yaml'): + """Show builddata assoicated with ``task_id``.""" + if not task_id: + ctx.fail('The task id must be specified by --task-id') + + task_bd = TaskBuildData(ctx.obj['CLIENT'], task_id=task_id).invoke() + + if output == 'json': + click.echo(json.dumps(task_bd)) + else: + if output != 'yaml': + click.echo( + 'Invalid output format {}, defaulting to YAML.'.format(output)) + click.echo( + yaml.safe_dump( + task_bd, allow_unicode=True, default_flow_style=False)) diff --git a/drydock_provisioner/drivers/node/maasdriver/actions/node.py b/drydock_provisioner/drivers/node/maasdriver/actions/node.py index 9801c7b3..bac81ca7 100644 --- a/drydock_provisioner/drivers/node/maasdriver/actions/node.py +++ b/drydock_provisioner/drivers/node/maasdriver/actions/node.py @@ -711,10 +711,9 @@ class ConfigureNodeProvisioner(BaseMaasAction): self.task.failure() if repo_list.remove_unlisted: defined_repos = [x.get_id() for x in repo_list] - to_delete = [r - for r - in current_repos - if r.name not in defined_repos] + to_delete = [ + r for r in current_repos if r.name not in defined_repos + ] for r in to_delete: if r.name not in self.DEFAULT_REPOS: r.delete() @@ -745,11 +744,13 @@ class ConfigureNodeProvisioner(BaseMaasAction): model_fields['distributions'] = ','.join(repo_obj.distributions) if repo_obj.components: if repo_obj.get_id() in ConfigureNodeProvisioner.DEFAULT_REPOS: - model_fields['disabled_components'] = ','.join(repo_obj.get_disabled_components()) + model_fields['disabled_components'] = ','.join( + repo_obj.get_disabled_components()) else: model_fields['components'] = ','.join(repo_obj.components) if repo_obj.get_disabled_subrepos(): - model_fields['disabled_pockets'] = ','.join(repo_obj.get_disabled_subrepos()) + model_fields['disabled_pockets'] = ','.join( + repo_obj.get_disabled_subrepos()) if repo_obj.arches: model_fields['arches'] = ','.join(repo_obj.arches) diff --git a/drydock_provisioner/drydock_client/client.py b/drydock_provisioner/drydock_client/client.py index 16fdd66a..23ba3e32 100644 --- a/drydock_provisioner/drydock_client/client.py +++ b/drydock_provisioner/drydock_client/client.py @@ -29,6 +29,31 @@ class DrydockClient(object): self.session = session self.logger = logging.getLogger(__name__) + def get_task_build_data(self, task_id): + """Get the build data associated with ``task_id``. + + :param str task_id: A UUID-formatted task ID + :return: A list of dictionaries resembling objects.builddata.BuildData + """ + endpoint = 'v1.0/tasks/{}/builddata'.format(task_id) + + resp = self.session.get(endpoint) + self._check_response(resp) + return resp.json() + + def get_node_build_data(self, nodename, latest=True): + """Get the build data associated with ``nodename``. + + :param str nodename: Name of the node + :param bool latest: Whether to request only the latest version of each data item + :return: A list of dictionaries resembling objects.builddata.BuildData + """ + endpoint = 'v1.0/nodes/{}/builddata?latest={}'.format(nodename, latest) + + resp = self.session.get(endpoint) + self._check_response(resp) + return resp.json() + def get_nodes(self): """Get list of nodes in MaaS and their status.""" endpoint = 'v1.0/nodes' diff --git a/drydock_provisioner/objects/site.py b/drydock_provisioner/objects/site.py index 3186a9e7..4bab9c1b 100644 --- a/drydock_provisioner/objects/site.py +++ b/drydock_provisioner/objects/site.py @@ -132,6 +132,7 @@ class Repository(base.DrydockObject): std = self.STANDARD_SUBREPOS.get(self.repo_type, ()) return std - enabled + @base.DrydockObjectRegistry.register class RepositoryList(base.DrydockObjectListBase, base.DrydockObject): diff --git a/tests/unit/test_cli_task.py b/tests/unit/test_cli_task.py index 9e0f7564..c9a25e40 100644 --- a/tests/unit/test_cli_task.py +++ b/tests/unit/test_cli_task.py @@ -11,11 +11,18 @@ # 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 yaml +import pytest + +from click.testing import CliRunner + import drydock_provisioner.drydock_client.session as dc_session import drydock_provisioner.drydock_client.client as dc_client from drydock_provisioner.cli.task.actions import TaskCreate +from drydock_provisioner.cli.task.actions import TaskBuildData +import drydock_provisioner.cli.commands as cli def test_taskcli_blank_nodefilter(): """If no filter values are specified, node filter should be None.""" @@ -29,3 +36,55 @@ def test_taskcli_blank_nodefilter(): dd_client, "http://foo.bar", action_name="deploy_nodes") assert action.node_filter is None + +def test_taskcli_builddata_action(mocker): + """Test the CLI task get build data routine.""" + task_id = "aaaa-bbbb-cccc-dddd" + build_data = [{ + "node_name": "foo", + "task_id": task_id, + "collected_data": "1/1/2000", + "generator": "test", + "data_format": "text/plain", + "data_element": "Hello World!", + }] + + api_client = mocker.MagicMock() + api_client.get_task_build_data.return_value = build_data + + bd_action = TaskBuildData(api_client, task_id) + + assert bd_action.invoke() == build_data + api_client.get_task_build_data.assert_called_with(task_id) + +@pytest.mark.skip(reason='Working on mocking needed for click.testing') +def test_taskcli_builddata_command(mocker): + """Test the CLI task get build data command.""" + task_id = "aaaa-bbbb-cccc-dddd" + build_data = [{ + "node_name": "foo", + "task_id": task_id, + "collected_data": "1/1/2000", + "generator": "test", + "data_format": "text/plain", + "data_element": "Hello World!", + }] + + api_client = mocker.MagicMock() + api_client.get_task_build_data.return_value = build_data + + mocker.patch('drydock_provisioner.cli.commands.DrydockClient', new=api_client) + mocker.patch('drydock_provisioner.cli.commands.KeystoneClient') + + runner = CliRunner() + result = runner.invoke(cli.drydock, ['-u', + 'http://foo', + 'task', + 'builddata', + '-t', + task_id]) + + print(result.exc_info) + api_client.get_task_build_data.assert_called_with(task_id) + + assert yaml.safe_dump(build_data, allow_unicode=True, default_flow_style=False) in result.output