Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions dim-testsuite/t/container-list-tree.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# test tree view for list containers (-T/--tree, -p/--pattern, -c/--case-sensitive, --no-color)
$ ndcli create container 10.0.0.0/8
INFO - Creating container 10.0.0.0/8 in layer3domain default

# basic tree with long flag; single container uses └── connector
$ ndcli list containers layer3domain default --tree
layer3domain: default
└── 10.0.0.0/8 (Container)
└── 10.0.0.0/8 (Available)

$ ndcli create layer3domain one type vrf rd 0:1
$ ndcli create container 10.0.0.0/8 layer3domain one
INFO - Creating container 10.0.0.0/8 in layer3domain one
$ ndcli create container 11.0.0.0/8 layer3domain one
INFO - Creating container 11.0.0.0/8 in layer3domain one

# short flag -T; multiple containers: non-last uses ├── and │ prefix, last uses └──
$ ndcli list containers layer3domain one -T
layer3domain: one
├── 10.0.0.0/8 (Container)
│ └── 10.0.0.0/8 (Available)
└── 11.0.0.0/8 (Container)
└── 11.0.0.0/8 (Available)

# pattern filter implies tree mode; only the matching subtree is shown
$ ndcli list containers layer3domain one -p 11
layer3domain: one
└── 11.0.0.0/8 (Container)
└── 11.0.0.0/8 (Available)

# pattern is case-insensitive by default; both containers carry "(Container)" in their text
$ ndcli list containers layer3domain one -p container
layer3domain: one
├── 10.0.0.0/8 (Container)
│ └── 10.0.0.0/8 (Available)
└── 11.0.0.0/8 (Container)
└── 11.0.0.0/8 (Available)

# case-sensitive flag: lowercase "container" does not match "(Container)"
$ ndcli list containers layer3domain one -p container -c
WARNING - No matches for pattern 'container' in layer3domain one

# --no-color is accepted without error; no visual difference for non-tty output
$ ndcli list containers layer3domain default -T --no-color
layer3domain: default
└── 10.0.0.0/8 (Container)
└── 10.0.0.0/8 (Available)
82 changes: 75 additions & 7 deletions ndcli/dimcli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1343,7 +1343,7 @@ def modify_pool_add_subnet(self, args):
created. The zone profile used for creating the reverse zones is taken from the
reverse_dns_profile attribute of an ancestor ipblock.
'''

options = OptionDict(include_messages=True)
options.set_attributes(args.attributes)
options.set_if(gateway=args.gw,
Expand Down Expand Up @@ -1646,17 +1646,68 @@ def list_vlans(self, args):
@cmd.register('list containers',
layer3domain_group,
Argument('container', nargs='?'),
Option('T', 'tree',
help='render as visual tree with Unicode box-drawing characters'),
Option('p', 'pattern', action='store',
help='regex to filter tree output (implies -T; shows matching nodes with full ancestor path and all children)'),
Option('c', 'case-sensitive', dest='case_sensitive',
help='case-sensitive pattern matching (default: case-insensitive)'),
Option(None, 'no-color', dest='no_color',
help='disable ANSI highlighting of matched terms'),
help='list the children of a container')
def list_containers(self, args):
use_tree = args.tree or bool(args.get('pattern'))
pattern_str = args.get('pattern')
case_sensitive = args.get('case_sensitive')
no_color = args.get('no_color')

HIGHLIGHT_ON = '\033[1;33m'
HIGHLIGHT_OFF = '\033[0m'

def node_text(node):
attributes = ['(%s)' % node['status']]
if 'pool' in node:
attributes.append('pool:%s' % node['pool'])
attributes.extend(['%s:%s' % (k, v) for k, v in sorted(node.get('attributes', {}).items())])
return '%s %s' % (node['ip'], ' '.join(attributes))

def print_tree(tree, level: int):
for node in tree:
attributes = ['(%s)' % node['status']]
if 'pool' in node:
attributes.append('pool:%s' % node['pool'])
attributes.extend(['%s:%s' % (k, v) for k, v in sorted(node.get('attributes', {}).items())])
print(' ' * level + '%s %s' % (node['ip'], ' '.join(attributes)))
print(' ' * level + node_text(node))
if 'children' in node:
print_tree(node['children'], level + 1)

def filter_tree(tree, regex):
"""Return pruned tree: matching nodes (with all their children) and their ancestors."""
result = []
for node in tree:
if regex.search(node_text(node)):
result.append(node) # keep node with all original children
else:
filtered_children = filter_tree(node.get('children', []), regex)
if filtered_children:
new_node = dict(node)
new_node['children'] = filtered_children
result.append(new_node)
return result

def render_unicode(tree, prefix='', regex=None, use_color=False):
lines = []
for i, node in enumerate(tree):
is_last = (i == len(tree) - 1)
connector = '└── ' if is_last else '├── '
text = node_text(node)
if use_color and regex:
text = regex.sub(
lambda m: HIGHLIGHT_ON + m.group() + HIGHLIGHT_OFF, text)
lines.append(prefix + connector + text)
children = node.get('children', [])
if children:
child_prefix = prefix + (' ' if is_last else '│ ')
child_output = render_unicode(children, child_prefix, regex, use_color)
if child_output:
lines.append(child_output)
return '\n'.join(lines)
layer3domains: List[str] = []
if args.layer3domain == "all" or get_layer3domain(args.layer3domain) is None:
layer3domains = [l['name'] for l in self.client.layer3domain_list()]
Expand Down Expand Up @@ -1684,14 +1735,31 @@ def print_tree(tree, level: int):
elif len(dim_errors) > 0:
for dimerror in dim_errors:
logging.debug(dimerror)
regex = None
if pattern_str:
flags = 0 if case_sensitive else re.IGNORECASE
try:
regex = re.compile(pattern_str, flags)
except re.error as ex:
raise DimError('Invalid regex: %s' % ex)
use_color = regex is not None and not no_color and sys.stdout.isatty()
idx = 0
for layer3domain, result in results.items():
if idx > 0:
print()
_print_messages(result)
if result['containers']:
containers = result['containers']
if use_tree and regex:
containers = filter_tree(containers, regex)
if not containers:
logging.warning('No matches for pattern %r in layer3domain %s', pattern_str, layer3domain)
continue
print('layer3domain: ' + layer3domain)
print_tree(result['containers'], 0)
if use_tree:
print(render_unicode(containers, regex=regex, use_color=use_color))
else:
print_tree(containers, 0)
idx += 1

@cmd.register('list ips',
Expand Down