diff --git a/.travis.yml b/.travis.yml index 67ec308..3bcd6ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: python +cache: pip python: - "2.7" @@ -6,6 +7,8 @@ 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 diff --git a/pynsot/commands/cmd_circuits.py b/pynsot/commands/cmd_circuits.py index f0a4578..96d511e 100644 --- a/pynsot/commands/cmd_circuits.py +++ b/pynsot/commands/cmd_circuits.py @@ -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 @@ -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'), @@ -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', @@ -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): @@ -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} @@ -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( @@ -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', @@ -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 @@ -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( @@ -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', @@ -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. @@ -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( diff --git a/pynsot/commands/cmd_devices.py b/pynsot/commands/cmd_devices.py index b1b91f2..b422b3b 100644 --- a/pynsot/commands/cmd_devices.py +++ b/pynsot/commands/cmd_devices.py @@ -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'), ) diff --git a/pynsot/commands/cmd_interfaces.py b/pynsot/commands/cmd_interfaces.py index 0c95bb1..8ef00e5 100644 --- a/pynsot/commands/cmd_interfaces.py +++ b/pynsot/commands/cmd_interfaces.py @@ -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 @@ -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'), @@ -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'), ) @@ -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]' @@ -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( @@ -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. @@ -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( @@ -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( @@ -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): @@ -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'), @@ -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, ) @@ -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( diff --git a/pynsot/commands/cmd_networks.py b/pynsot/commands/cmd_networks.py index 664beac..02b6a01 100644 --- a/pynsot/commands/cmd_networks.py +++ b/pynsot/commands/cmd_networks.py @@ -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'), ) diff --git a/pynsot/commands/types.py b/pynsot/commands/types.py index 7d80b55..03df331 100644 --- a/pynsot/commands/types.py +++ b/pynsot/commands/types.py @@ -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() diff --git a/requirements-dev.txt b/requirements-dev.txt index 753bd3c..730c9ec 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..406bfc3 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +exclude = tests/*,docs diff --git a/tests/app/test_circuits.py b/tests/app/test_circuits.py index 6a4e142..dda7bd4 100644 --- a/tests/app/test_circuits.py +++ b/tests/app/test_circuits.py @@ -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, @@ -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): diff --git a/tests/test_app.py b/tests/test_app.py index a3547f8..b49e01b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -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) @@ -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 ###########