Skip to content

Commit

Permalink
FEATURE: Add a parameter to 'list host attr' to separate the display …
Browse files Browse the repository at this point in the history
…of common and distinct attributes.

'stack list host attr display=[all|common|distinct] [hosts...] [attr=<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.
  • Loading branch information
bsanders committed Feb 5, 2019
1 parent ff94112 commit 11119fb
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 4 deletions.
25 changes: 21 additions & 4 deletions common/src/stack/command/stack/commands/list/attr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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':
Expand Down Expand Up @@ -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()

Expand Down
14 changes: 14 additions & 0 deletions common/src/stack/command/stack/commands/list/host/attr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ class Command(stack.commands.list.host.command):
be listed.
</param>
<param type='string' name='display'>
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.
</param>
<example cmd='list host attr backend-0-0'>
List the attributes for backend-0-0.
</example>
Expand Down
Original file line number Diff line number Diff line change
@@ -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_'}

0 comments on commit 11119fb

Please sign in to comment.