Skip to content

Commit

Permalink
Update nsot CLI to support natural key for Interfaces/Circuits (#139)
Browse files Browse the repository at this point in the history
* Update nsot CLI to support natural key for Interfaces/Circuits

Requires NSoT v1.2.0

- Interfaces may now be created/updated by referencing the device
  hostname or device ID
- Circuits may now be created/updated by referencing the interfaces by
  natural key (slug) OR interface ID
- The visual display of Networks, Interfaces, Circuits has been updated to be
  more compact/concise
  - Networks
    - cidr is now displayed instead of network_address/prefix_length
    - parent cidr is now displayed instead of parent_id
  - Interface
    - name_slug is now displayed instead of device_id/name
    - parent name is now displayed instead of parent_id
  - Circuits
    - interface slugs are now displayed instead of ID numbers
- Added flake8 to Travis CI config

* Include "(Key)" in the header for the natural key field on list views

* Fix mixed tab/spaces in cmd_circuits.py
  • Loading branch information
jathanism committed Jul 28, 2017
1 parent 79f66f5 commit 65c2150
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 36 deletions.
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
language: python
cache: pip
python:
- "2.7"

install:
- pip install -r requirements-dev.txt
- pip install .

script: py.test -v tests/
script:
- flake8
- py.test -v tests/

after_success: curl -X POST https://readthedocs.org/build/pynsot
41 changes: 28 additions & 13 deletions pynsot/commands/cmd_circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from pynsot.util import slugify
from pynsot.vendor import click
from . import callbacks
from . import callbacks, types
from .cmd_networks import DISPLAY_FIELDS as NETWORK_DISPLAY_FIELDS
from .cmd_interfaces import DISPLAY_FIELDS as INTERFACE_DISPLAY_FIELDS
from .cmd_devices import DISPLAY_FIELDS as DEVICE_DISPLAY_FIELDS
Expand All @@ -22,7 +22,7 @@
# field names oto their human-readable form when calling .print_list().
DISPLAY_FIELDS = (
('id', 'ID'),
('name', 'Name'),
('name', 'Name (Key)'),
('endpoint_a', 'Endpoint A'),
('endpoint_z', 'Endpoint Z'),
('attributes', 'Attributes'),
Expand Down Expand Up @@ -61,8 +61,8 @@ def cli(ctx):
'--endpoint-a',
metavar='INTERFACE_ID',
required=True,
type=int,
help='Unique ID of the interface of the A side of the Circuit',
type=types.NATURAL_KEY,
help='Unique ID or key of the interface of the A side of the Circuit',
)
@click.option(
'-n',
Expand All @@ -83,8 +83,8 @@ def cli(ctx):
'-Z',
'--endpoint-z',
metavar='INTERFACE_ID',
type=int,
help='Unique ID of the interface on the Z side of the Circuit',
type=types.NATURAL_KEY,
help='Unique ID or key of the interface on the Z side of the Circuit',
)
@click.pass_context
def add(ctx, attributes, endpoint_a, name, site_id, endpoint_z):
Expand All @@ -96,6 +96,9 @@ def add(ctx, attributes, endpoint_a, name, site_id, endpoint_z):
where it is not an Interface that is tracked by NSoT (like a provider's
interface).
For the -A/--endpoint-a and -Z/--endpoint-z options, you may provide either
the Interface ID or its natural key.
The name (-n/--name) is optional. If it is not specified, it will be
generated for you in the form of:
{device_a}:{interface_a}_{device_z}:{interface_z}
Expand Down Expand Up @@ -128,6 +131,7 @@ def add(ctx, attributes, endpoint_a, name, site_id, endpoint_z):
'-A',
'--endpoint-a',
metavar='INTERFACE_ID',
type=types.NATURAL_KEY,
help='Filter to Circuits with endpoint_a interfaces that match this ID'
)
@click.option(
Expand All @@ -142,7 +146,8 @@ def add(ctx, attributes, endpoint_a, name, site_id, endpoint_z):
'-i',
'--id',
metavar='ID',
help='Unique ID of the Circuit being retrieved.',
type=types.NATURAL_KEY,
help='Unique ID or natural key of the Circuit being retrieved.',
)
@click.option(
'-l',
Expand Down Expand Up @@ -187,6 +192,7 @@ def add(ctx, attributes, endpoint_a, name, site_id, endpoint_z):
'-Z',
'--endpoint-z',
metavar='INTERFACE_ID',
type=types.NATURAL_KEY,
help='Filter to Circuits with endpoint_z interfaces that match this ID'
)
@click.pass_context
Expand Down Expand Up @@ -257,14 +263,15 @@ def interfaces(ctx, *args, **kwargs):
'-A',
'--endpoint-a',
metavar='INTERFACE_ID',
type=int,
help='Unique ID of the interface of the A side of the Circuit',
type=types.NATURAL_KEY,
help='Unique ID or key of the interface of the A side of the Circuit',
)
@click.option(
'-i',
'--id',
metavar='ID',
help='Unique ID of the Circuit being retrieved.',
type=types.NATURAL_KEY,
help='Unique ID or natural key of the Circuit being retrieved.',
required=True,
)
@click.option(
Expand All @@ -286,8 +293,8 @@ def interfaces(ctx, *args, **kwargs):
'-Z',
'--endpoint-z',
metavar='INTERFACE_ID',
type=int,
help='Unique ID of the interface on the Z side of the Circuit',
type=types.NATURAL_KEY,
help='Unique ID or key of the interface on the Z side of the Circuit',
)
@click.option(
'--add-attributes',
Expand Down Expand Up @@ -327,6 +334,13 @@ def update(ctx, attributes, endpoint_a, id, name, site_id, endpoint_z,
You must either have a Site ID configured in your .pysnotrc file or specify
one using the -s/--site-id option.
When updating a Circuit you must provide the ID (-i/--id) and at least
one of the optional arguments. The ID can either be the numeric ID of the
Circuit or the natural key. (Example: lax-r1:ae0_jfk-r2:ae0)
For the -A/--endpoint-a and -Z/--endpoint-z options, you may provide either
the Interface ID or its natural key.
The -a/--attributes option may be provided multiple times, once for each
key-value pair.
Expand Down Expand Up @@ -362,7 +376,8 @@ def update(ctx, attributes, endpoint_a, id, name, site_id, endpoint_z,
'-i',
'--id',
metavar='ID',
help='Unique ID of the Circuit being deleted.',
help='Unique ID or natural key of the Circuit being deleted.',
type=types.NATURAL_KEY,
required=True,
)
@click.option(
Expand Down
2 changes: 1 addition & 1 deletion pynsot/commands/cmd_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
# field names oto their human-readable form when calling .print_list().
DISPLAY_FIELDS = (
('id', 'ID'),
('hostname', 'Hostname'),
('hostname', 'Hostname (Key)'),
# ('site_id': 'Site ID'),
('attributes', 'Attributes'),
)
Expand Down
25 changes: 14 additions & 11 deletions pynsot/commands/cmd_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import logging

from ..vendor import click
from . import callbacks
from . import callbacks, types
from .cmd_networks import DISPLAY_FIELDS as NETWORK_DISPLAY_FIELDS


Expand All @@ -28,8 +28,8 @@
# field names oto their human-readable form when calling .print_list().
DISPLAY_FIELDS = (
('id', 'ID'),
('device_hostname', 'Device'),
('name', 'Name'),
('name_slug', 'Name (Key)'),
('parent', 'Parent'),
('mac_address', 'MAC'),
('addresses', 'Addresses'),
('attributes', 'Attributes'),
Expand All @@ -38,14 +38,12 @@
# Fields to display when viewing a single record.
VERBOSE_FIELDS = (
('id', 'ID'),
('device', 'Device ID'),
('device_hostname', 'Device'),
('name', 'Name'),
('name_slug', 'Name'),
('parent', 'Parent'),
('mac_address', 'MAC'),
('addresses', 'Addresses'),
('speed', 'Speed'),
('type', 'Type'),
('parent_id', 'Parent'),
('attributes', 'Attributes'),
)

Expand Down Expand Up @@ -86,6 +84,7 @@ def cli(ctx):
'-D',
'--device',
metavar='DEVICE',
type=types.NATURAL_KEY,
help=(
'Unique ID of the Device to which this Interface is '
'attached. [required]'
Expand Down Expand Up @@ -116,7 +115,7 @@ def cli(ctx):
'-p',
'--parent-id',
metavar='PARENT_ID',
type=int,
type=types.NATURAL_KEY,
help='Unique ID of the parent interface.'
)
@click.option(
Expand Down Expand Up @@ -147,7 +146,7 @@ def add(ctx, attributes, addresses, device, description, mac_address,
"""
Add a new Interface.
You must provide a Device ID using the -D/--device option.
You must provide a Device hostname or ID using the -D/--device option.
When adding a new Interface, you must provide a value for the -n/--name
option.
Expand Down Expand Up @@ -196,6 +195,7 @@ def add(ctx, attributes, addresses, device, description, mac_address,
'-D',
'--device',
metavar='DEVICE',
type=types.NATURAL_KEY,
help='Unique ID or hostname of the Device being retrieved.',
)
@click.option(
Expand Down Expand Up @@ -256,7 +256,7 @@ def add(ctx, attributes, addresses, device, description, mac_address,
'-p',
'--parent-id',
metavar='PARENT_ID',
type=int,
type=types.NATURAL_KEY,
help='Filter by integer of the ID of the parent Interface.',
)
@click.option(
Expand Down Expand Up @@ -387,6 +387,7 @@ def root(ctx, *args, **kwargs):
ctx, display_fields=VERBOSE_FIELDS, my_name=ctx.info_name
)


@list.command()
@click.pass_context
def siblings(ctx, *args, **kwargs):
Expand All @@ -395,6 +396,7 @@ def siblings(ctx, *args, **kwargs):
ctx, display_fields=VERBOSE_FIELDS, my_name=ctx.info_name
)


ASSIGNMENT_FIELDS = (
('id', 'ID'),
('hostname', 'Device'),
Expand Down Expand Up @@ -491,6 +493,7 @@ def remove(ctx, id, site_id):
'-i',
'--id',
metavar='ID',
type=types.NATURAL_KEY,
help='Unique ID of the Interface being updated.',
required=True,
)
Expand All @@ -512,7 +515,7 @@ def remove(ctx, id, site_id):
'-p',
'--parent-id',
metavar='PARENT_ID',
type=int,
type=types.NATURAL_KEY,
help='Unique ID of the parent interface.',
)
@click.option(
Expand Down
6 changes: 2 additions & 4 deletions pynsot/commands/cmd_networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@
# field names oto their human-readable form when calling .print_list().
DISPLAY_FIELDS = (
('id', 'ID'),
('network_address', 'Network'),
('prefix_length', 'Prefix'),
# ('site_id': 'Site ID'),
('cidr', 'CIDR (Key)'),
('is_ip', 'Is IP?'),
('ip_version', 'IP Ver.'),
('parent_id', 'Parent ID'),
('parent', 'Parent'),
('state', 'State'),
('attributes', 'Attributes'),
)
Expand Down
32 changes: 32 additions & 0 deletions pynsot/commands/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,36 @@ def convert(self, value, param, ctx):
def __repr__(self):
return 'NETWORK_ID'


class NaturalKeyParamType(click.ParamType):
"""Custom paramer type that supports ID or natural key."""
name = 'natural key'

def convert(self, value, param, ctx):
if value is None:
return

tests = [int, str]
win = False
for test in tests:
try:
win = test(value)
except:
pass
else:
if not win:
continue
return value

else:
self.fail(
'%s is not an valid integer or natural key' % value, param, ctx
)

def __repr__(self):
return 'NATURAL_KEY'


# Constants for these types
NETWORK_ID = NetworkIdParamType()
NATURAL_KEY = NaturalKeyParamType()
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
-r requirements.txt
fake-factory~=0.5.0
flake8~=3.3.0
ipdb~=0.9.3
ipython~=5.0
nsot>=1.1.4,~=1.1.0
nsot>=1.2.0,~=1.2.0
py~=1.4.26,>=1.4.29
pytest~=2.7.0
pytest-django~=2.9.1
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
exclude = tests/*,docs
4 changes: 3 additions & 1 deletion tests/app/test_circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from __future__ import absolute_import, unicode_literals
import logging

import pytest

from tests.fixtures import (attribute, attributes, client, config, device,
interface, network, runner, site, site_client)
from tests.fixtures.circuits import (circuit, circuit_attributes, device_a,
Expand Down Expand Up @@ -62,7 +64,7 @@ def test_circuits_add_intf_reuse(runner, interface_a):

result = runner.run(cmd.format(interface_a['id'], 'bad_circuit'))
assert result.exit_code != 0
assert 'endpoint_a: This field must be unique' in result.output
assert 'A-side endpoint Interface already exists' in result.output


def test_circuits_add_dupe_name(runner, interface_a, interface_z):
Expand Down
8 changes: 4 additions & 4 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ def test_networks_list(site_client):
# Make sure 10.0.0.0 shows twice in the output. Lazy man's output
# checking.
result = runner.run('networks list')
assert result.output.count('10.0.0.0') == 2
assert result.output.count('10.0.0.0') == 3
assert result.exit_code == 0

# Set query display newline-delimited (default)
Expand Down Expand Up @@ -970,9 +970,9 @@ def test_interfaces_list(site_client, device):
assert result.output == expected_output

# Query by natural key
result = runner.run('interfaces list -i {0}:eth1'.format(hostname))
assert 'eth1' in result.output
assert str(device_id) in result.output
natural_key = '{0}:eth1'.format(hostname)
result = runner.run('interfaces list -i {}'.format(natural_key))
assert natural_key in result.output
assert result.exit_code == 0

###########
Expand Down

0 comments on commit 65c2150

Please sign in to comment.