From 11119fb13b07a5c34270bcc62aea1998e3be2728 Mon Sep 17 00:00:00 2001 From: Bill Sanders Date: Wed, 23 Jan 2019 14:31:02 -0800 Subject: [PATCH] FEATURE: Add a parameter to 'list host attr' to separate the display of common and distinct attributes. 'stack list host attr display=[all|common|distinct] [hosts...] [attr=]' changes which attributes are displayed. display=all will display all attributes for each host, grouped by host. This is the default, traditional behavior. display=common will display only attributes which are identical for each host, under the name '_common_'. display=distinct will display only attributes which are *not* identical for each host. --- .../stack/commands/list/attr/__init__.py | 25 +++- .../stack/commands/list/host/attr/__init__.py | 14 ++ .../tests/list/test_list_host_attr.py | 129 ++++++++++++++++++ 3 files changed, 164 insertions(+), 4 deletions(-) diff --git a/common/src/stack/command/stack/commands/list/attr/__init__.py b/common/src/stack/command/stack/commands/list/attr/__init__.py index 1616fde28..ee420f741 100644 --- a/common/src/stack/command/stack/commands/list/attr/__init__.py +++ b/common/src/stack/command/stack/commands/list/attr/__init__.py @@ -215,13 +215,14 @@ def addHostAttrs(self, attributes): def run(self, params, args): - (glob, shadow, scope, resolve, var, const) = self.fillParams([ + (glob, shadow, scope, resolve, var, const, display) = self.fillParams([ ('attr', None), ('shadow', True), ('scope', 'global'), ('resolve', None), ('var', True), - ('const', True) + ('const', True), + ('display', 'all'), ]) shadow = self.str2bool(shadow) @@ -327,7 +328,6 @@ def run(self, params, args): # Mix in any const attributes lookup[s]['const'](attributes[s]) - targets = sorted(lookup[scope]['fn'](args)) if resolve and scope == 'host': @@ -362,7 +362,24 @@ def run(self, params, args): matches[key] = attributes[scope][o][key] attributes[scope][o] = matches - + if display != 'all' and scope == 'host': + host_attrs = { + host: {k: str(v[0]) for k,v in attributes['host'][host].items()} + for host in attributes['host'] if host in targets + } + + common_attrs = set.intersection(*(set(d.items()) for d in host_attrs.values())) + filtered_attrs = {} + + if display == 'common': + filtered_attrs['_common_'] = dict((k,(v, None, None)) for k,v in common_attrs) + targets = ['_common_'] + elif display == 'distinct': + for host in host_attrs: + host_pairs = set(d for d in host_attrs[host].items()) + filtered_attrs[host] = dict((k,attributes['host'][host][k]) for k,v in host_pairs.difference(common_attrs)) + + attributes['host'] = filtered_attrs self.beginOutput() diff --git a/common/src/stack/command/stack/commands/list/host/attr/__init__.py b/common/src/stack/command/stack/commands/list/host/attr/__init__.py index 5e5348d5b..fd8945292 100644 --- a/common/src/stack/command/stack/commands/list/host/attr/__init__.py +++ b/common/src/stack/command/stack/commands/list/host/attr/__init__.py @@ -26,6 +26,20 @@ class Command(stack.commands.list.host.command): be listed. + + Control which attributes are displayed for the provided + hosts. + + 'all' will display all attributes for each host, grouped + by host. This is the default. + + 'common' will display only attributes which are identical + for every host. + + 'distinct' will display only attributes which are not + identical for every host. + + List the attributes for backend-0-0. diff --git a/test-framework/test-suites/integration/tests/list/test_list_host_attr.py b/test-framework/test-suites/integration/tests/list/test_list_host_attr.py index e69de29bb..9125ff0c6 100644 --- a/test-framework/test-suites/integration/tests/list/test_list_host_attr.py +++ b/test-framework/test-suites/integration/tests/list/test_list_host_attr.py @@ -0,0 +1,129 @@ +import json +from operator import itemgetter +from itertools import groupby + +class TestListHostAttr: + def test_invalid(self, host): + result = host.run('stack list host attr test') + assert result.rc == 255 + assert result.stderr.startswith('error - ') + + def test_no_args_frontend_only(self, host): + result = host.run('stack list host attr output-format=json') + assert result.rc == 0 + attr_obj = json.loads(result.stdout) + + # with no other hosts in the db, these commands produce identical output + result = host.run('stack list host attr localhost output-format=json') + assert result.rc == 0 + json.loads(result.stdout) == attr_obj + + # test appliance selector, too + result = host.run('stack list host attr a:frontend output-format=json') + assert result.rc == 0 + json.loads(result.stdout) == attr_obj + + # there should be exactly one host + assert len({row['host'] for row in attr_obj}) == 1 + + def test_with_backend(self, host, add_host): + result = host.run('stack list host attr output-format=json') + assert result.rc == 0 + attr_obj = json.loads(result.stdout) + + # with other hosts in the db, this will be different + result = host.run('stack list host attr localhost output-format=json') + assert result.rc == 0 + json.loads(result.stdout) != attr_obj + + # test appliance selector, too + result = host.run('stack list host attr a:frontend output-format=json') + assert result.rc == 0 + json.loads(result.stdout) != attr_obj + + # both selectors should work together, though + result = host.run('stack list host attr a:frontend a:backend output-format=json') + assert result.rc == 0 + json.loads(result.stdout) == attr_obj + + # both hostnames specified should be the same as none by default + result = host.run('stack list host attr localhost backend-0-0 output-format=json') + assert result.rc == 0 + json.loads(result.stdout) == attr_obj + + # there should be exactly two hosts + assert len({row['host'] for row in attr_obj}) == 2 + + def test_common_with_only_frontend(self, host): + result = host.run('stack list host attr display=common output-format=json') + assert result.rc == 0 + attr_obj = json.loads(result.stdout) + + # there should be one "host" called '_common_' + assert len({row['host'] for row in attr_obj}) == 1 + assert attr_obj[0]['host'] == '_common_' + + def test_distinct_with_multiple_hosts(self, host, add_host): + result = host.run('stack list host attr display=distinct output-format=json') + assert result.rc == 0 + attr_obj = json.loads(result.stdout) + + host_attrs = { + k: {i['attr']: i['value'] for i in v} + for k, v in groupby(attr_obj, itemgetter('host')) + } + + # don't hardcode FE hostname + fe_hostname = [h for h in host_attrs if h != 'backend-0-0'].pop() + assert len(host_attrs) == 2 + assert {'backend-0-0', fe_hostname} == set(host_attrs) + + # some keys will only be in common (by default) + assert 'Kickstart_PrivateRootPassword' not in host_attrs[fe_hostname] + assert 'Kickstart_PrivateRootPassword' not in host_attrs['backend-0-0'] + # some keys will always be distinct + assert 'hostname' in host_attrs['backend-0-0'] + # backend doesn't have a hostaddr here + assert 'hostaddr' in host_attrs[fe_hostname] + + result = host.run('stack add host attr backend-0-0 attr=foo value=bar') + assert result.rc == 0 + + result = host.run('stack list host attr display=distinct output-format=json') + assert result.rc == 0 + new_attr_obj = json.loads(result.stdout) + + new_host_attrs = { + k: {i['attr']: i['value'] for i in v} + for k, v in groupby(new_attr_obj, itemgetter('host')) + } + + assert len(new_host_attrs['backend-0-0']) == len(host_attrs['backend-0-0']) + 1 + assert len(new_host_attrs[fe_hostname]) == len(host_attrs[fe_hostname]) + + result = host.run('stack list host attr display=common output-format=json') + assert result.rc == 0 + common_attr_obj = json.loads(result.stdout) + + common_host_attrs = { + k: {i['attr']: i['value'] for i in v} + for k, v in groupby(common_attr_obj, itemgetter('host')) + } + + # the set of common attrs and distinct attrs should never overlap + assert set(common_host_attrs['_common_']).isdisjoint(new_host_attrs['backend-0-0']) + + def test_common_with_multiple_hosts_single_attr_param(self, host, add_host): + result = host.run('stack list host attr display=distinct attr=hostname output-format=json') + assert result.rc == 0 + attr_obj = json.loads(result.stdout) + + # only two hosts, no common attrs here + assert len({row['host'] for row in attr_obj}) == 2 + + result = host.run('stack list host attr display=common attr=box output-format=json') + assert result.rc == 0 + attr_obj = json.loads(result.stdout) + + # by default these will resolve to the same, so only common will be listed + assert {row['host'] for row in attr_obj} == {'_common_'}