├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── TODO.md ├── Vagrantfile ├── bump.sh ├── demo ├── README.rst ├── demo_fixtures.json.gz ├── demo_settings.py └── run_demo.sh ├── docker ├── Dockerfile ├── Dockerfile.sub ├── README.md └── conf │ └── nsot.conf.py ├── docs ├── Makefile ├── _static │ ├── logo_128.png │ └── web_login.png ├── admin.rst ├── api │ ├── index.rst │ ├── python.rst │ └── rest.rst ├── changelog.rst ├── conf.py ├── config.rst ├── development.rst ├── dockerfile.rst ├── index.rst ├── install │ ├── centos.rst │ ├── docker.rst │ ├── fedora.rst │ ├── macosx.rst │ ├── suse.rst │ ├── ubuntu.rst │ └── vagrant.rst ├── installation.rst ├── models.rst ├── quickstart.rst ├── requirements.rst ├── support.rst ├── tutorial.rst └── usage.rst ├── gulpfile.js ├── iftypes.txt ├── npm-shrinkwrap.json ├── nsot ├── __init__.py ├── admin.py ├── api │ ├── __init__.py │ ├── auth.py │ ├── filters.py │ ├── renderers.py │ ├── routers.py │ ├── serializers.py │ ├── urls.py │ └── views.py ├── conf │ ├── __init__.py │ ├── settings.py │ └── urls.py ├── exc.py ├── fields.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── generate_key.py │ │ ├── start.py │ │ ├── upgrade.py │ │ └── user_proxy.py ├── middleware │ ├── __init__.py │ ├── auth.py │ └── request_logging.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20150810_1718.py │ ├── 0003_auto_20150810_1751.py │ ├── 0004_auto_20150810_1806.py │ ├── 0005_auto_20150810_1847.py │ ├── 0006_auto_20150810_1947.py │ ├── 0007_auto_20150811_1201.py │ ├── 0008_auto_20150811_1222.py │ ├── 0009_auto_20150811_1245.py │ ├── 0010_auto_20150921_2120.py │ ├── 0011_auto_20150930_1557.py │ ├── 0012_auto_20151002_1427.py │ ├── 0013_auto_20151002_1443.py │ ├── 0014_auto_20151002_1653.py │ ├── 0015_move_attribute_fields.py │ ├── 0016_move_device_data.py │ ├── 0017_move_network_data.py │ ├── 0018_move_interface_data.py │ ├── 0019_move_assignment_data.py │ ├── 0020_move_value_data.py │ ├── 0021_remove_resource_object.py │ ├── 0022_auto_20151007_1847.py │ ├── 0023_auto_20151008_1351.py │ ├── 0024_network_state.py │ ├── 0025_value_site.py │ ├── 0026_model_field_verbose_names.py │ ├── 0027_interface_device_hostname.py │ ├── 0028_populate_interface_device_hostname.py │ ├── 0029_auto__add_circuit.py │ ├── 0030_add_circuit_name_slug.py │ ├── 0031_populate_circuit_name_slug.py │ ├── 0032_add_indicies_to_change.py │ ├── 0033_add_interface_name_slug.py │ ├── 0034_populate_interface_name_slug.py │ ├── 0035_fix_interface_name_slug.py │ ├── 0036_add_protocol.py │ ├── 0037_protocoltype_site__unique_together.py │ ├── 0038_make_interface_speed_nullable.py │ └── __init__.py ├── models │ ├── __init__.py │ ├── assignment.py │ ├── attribute.py │ ├── change.py │ ├── circuit.py │ ├── constants.py │ ├── device.py │ ├── interface.py │ ├── network.py │ ├── protocol.py │ ├── protocol_type.py │ ├── resource.py │ ├── site.py │ ├── user.py │ └── value.py ├── services │ ├── __init__.py │ ├── base.py │ └── http.py ├── static │ └── src │ │ ├── images │ │ ├── favicon │ │ │ └── favicon.ico │ │ └── halftone │ │ │ ├── halftone.png │ │ │ └── readme.txt │ │ ├── js │ │ ├── app.js │ │ ├── controllers.js │ │ ├── directives.js │ │ ├── filters.js │ │ ├── nsot.js │ │ └── services.js │ │ ├── style │ │ └── nsot.css │ │ └── templates │ │ ├── attribute.html │ │ ├── attributes.html │ │ ├── change.html │ │ ├── changes.html │ │ ├── device.html │ │ ├── devices.html │ │ ├── directives │ │ ├── dropdown.html │ │ ├── loading-panel.html │ │ ├── nsot-modal.html │ │ └── paginator.html │ │ ├── includes │ │ ├── attributes-form.html │ │ ├── devices-form.html │ │ ├── interfaces-form.html │ │ └── networks-form.html │ │ ├── index.html │ │ ├── interface.html │ │ ├── interfaces.html │ │ ├── network.html │ │ ├── networks.html │ │ ├── site.html │ │ ├── sites.html │ │ ├── user.html │ │ └── users.html ├── templates │ ├── rest_framework │ │ ├── api.html │ │ └── login.html │ └── ui │ │ ├── app.html │ │ ├── error.html │ │ ├── media.html │ │ ├── menu.html │ │ └── scripts.html ├── ui │ ├── __init__.py │ ├── context_processors.py │ └── views.py ├── util │ ├── __init__.py │ ├── cache.py │ ├── commands.py │ ├── core.py │ └── stats.py ├── validators.py ├── version.py └── wsgi.py ├── package.json ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── api_tests │ ├── __init__.py │ ├── conftest.py │ ├── data │ │ ├── attributes.json │ │ ├── devices.json │ │ └── networks.json │ ├── fixtures.py │ ├── test_attributes.py │ ├── test_auth.py │ ├── test_circuits.py │ ├── test_devices.py │ ├── test_interfaces.py │ ├── test_networks.py │ ├── test_permissions.py │ ├── test_protocols.py │ ├── test_regressions.py │ ├── test_sites.py │ ├── test_user.py │ ├── test_values.py │ ├── test_xforwardfor.py │ └── util.py ├── benchmarks.py ├── conftest.py ├── fixtures.py ├── model_tests │ ├── __init__.py │ ├── data │ │ └── networks.json │ ├── fixtures.py │ ├── test_attributes.py │ ├── test_changes.py │ ├── test_circuits.py │ ├── test_devices.py │ ├── test_interfaces.py │ ├── test_middleware_auth.py │ ├── test_networks.py │ ├── test_protocols.py │ ├── test_regressions.py │ ├── test_sites.py │ └── test_values.py ├── test_settings.py ├── test_util.py └── util.py └── vagrant └── README.rst /.gitignore: -------------------------------------------------------------------------------- 1 | tests/api_tests/cassettes/*.json 2 | staticfiles 3 | 4 | *.sqlite 5 | *.sqlite-journal 6 | *.sqlite3 7 | 8 | # Node 9 | /npm-debug.log 10 | /node_modules 11 | 12 | __pycache__ 13 | 14 | MANIFEST 15 | *.py[cod] 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Packages 21 | *.egg 22 | *.egg-info 23 | dist 24 | build 25 | eggs 26 | parts 27 | var 28 | sdist 29 | develop-eggs 30 | .installed.cfg 31 | lib 32 | lib64 33 | 34 | # Installer logs 35 | pip-log.txt 36 | 37 | # Unit test / coverage reports, caches 38 | .pytest_cache 39 | .coverage 40 | .tox 41 | nosetests.xml 42 | 43 | # Translations 44 | *.mo 45 | 46 | # Mr Developer 47 | .mr.developer.cfg 48 | .project 49 | .pydevproject 50 | 51 | docs/_build 52 | .*sw? 53 | .vagrant 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | cache: pip 3 | 4 | python: 5 | - '2.7' 6 | 7 | install: 8 | - pip install -r requirements-dev.txt 9 | - pip install . 10 | 11 | script: 12 | - flake8 13 | - NSOT_API_VERSION=1.0 py.test -vv tests/ 14 | 15 | after_success: curl -X POST http://readthedocs.org/build/nsot 16 | 17 | notifications: 18 | slack: 19 | secure: UdmH92LEpYke+NnslEx5lE4vjuqu719Wl5OPQPHPUACaKb1teA2yew5YjR/GxWh0Gy9nOcxpSL6xw1ODj3v6EzjvtVAcOzScFHbYUemdWlPLlYE5r9/rYIYVEChYIPQJ3uEhPJRys9ugXcZBdH2vgeXF4FW8Ftjxoqd+XCExB04= 20 | 21 | sudo: false 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Dropbox, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include pytest.ini 3 | include README.md 4 | include requirements.txt 5 | include requirements-dev.txt 6 | 7 | graft nsot/templates 8 | graft nsot/static/build 9 | 10 | recursive-include tests * 11 | recursive-exclude tests *.pyc 12 | recursive-exclude tests/api_tests/cassettes *.json 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | #### 2 | NSoT 3 | #### 4 | 5 | .. image:: https://raw.githubusercontent.com/dropbox/nsot/master/docs/_static/logo_128.png 6 | :alt: Network Source of Truth 7 | :width: 128px 8 | 9 | |Build Status| |Documentation Status| |PyPI Status| 10 | 11 | Network Source of Truth (NSoT) is a source of truth database and repository for 12 | tracking inventory and metadata of network entities to ease management and 13 | automation of network infrastructure. 14 | 15 | NSoT is an API-first application that provides a REST API and a web application 16 | front-end for managing IP addresses (IPAM), network devices, and network 17 | interfaces. 18 | 19 | Resources 20 | ========= 21 | 22 | + `Documentation `_ 23 | + `Python API client/CLI utility `_ 24 | + IRC: ``#nsot`` on ``irc.freenode.net`` 25 | 26 | .. |Build Status| image:: https://img.shields.io/travis/dropbox/nsot/master.svg?style=flat 27 | :target: https://travis-ci.org/dropbox/nsot 28 | :width: 88px 29 | :height: 20px 30 | .. |Documentation Status| image:: https://readthedocs.org/projects/nsot/badge/?version=latest&style=flat 31 | :target: https://readthedocs.org/projects/nsot/?badge=latest 32 | :width: 76px 33 | :height: 20px 34 | .. |PyPI Status| image:: https://img.shields.io/pypi/v/nsot.svg?style=flat 35 | :target: https://pypi.python.org/pypi/nsot 36 | :width: 68px 37 | :height: 20px 38 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * API 2 | - Bulk Insert/Update 3 | - Hostnames and reverse 4 | - API Tokens for role accounts 5 | - PATCH support 6 | - Default Site for User 7 | 8 | * Web UI 9 | - Pages 10 | * Networks (Create/List/Show/Update/Delete) 11 | * Attributes (Show) 12 | * Permissions (Update) 13 | -------------------------------------------------------------------------------- /bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USAGE=`cat <&2 && exit 1;; 20 | * ) proceed;; 21 | esac 22 | } 23 | 24 | function replace() { 25 | sed -i.bak "s/'${CURVER}'/'${VERSION}'/" nsot/version.py 26 | echo "Updated nsot/version.py" 27 | rm nsot/version.py.bak 28 | 29 | sed "s/{{ NSOT_VERSION }}/${VERSION}/" docker/Dockerfile.sub > \ 30 | docker/Dockerfile 31 | echo "Updated docker/Dockerfile" 32 | } 33 | 34 | function usage() { 35 | echo "$USAGE" >&2 36 | exit 37 | } 38 | 39 | # Entrypoint actually starts here 40 | 41 | while [[ $# > 0 ]]; do 42 | 43 | key="$1" 44 | 45 | case $key in 46 | -h|--help) 47 | usage 48 | ;; 49 | -v|--version) 50 | VERSION="$2" && shift;; 51 | *) ;; 52 | esac 53 | shift 54 | done 55 | 56 | if [ -z $VERSION ]; then echo "You must provide -v|--version!" >&2; usage; fi 57 | 58 | CURVER=`cat nsot/version.py | grep -Eow "'(\S+)\'$" | cut -d\' -f 2` 59 | proceed 60 | -------------------------------------------------------------------------------- /demo/README.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | NSoT Demo app 3 | ############# 4 | 5 | This is a basic server instance with a single Site full of dummy Networks, 6 | Devices, and Attributes you can use to experiment. 7 | 8 | Running the demo 9 | ================ 10 | 11 | To try out the demo, make sure you've already installed NSoT. If it's 12 | installed in a Python virtualenv, make sure that it is activated. 13 | 14 | Run the demo:: 15 | 16 | $ ./run_demo.sh 17 | 18 | This script will: 19 | 20 | * Set environment variable ``NSOT_CONF=./demo_settings.py`` to tell NSoT to 21 | read the config from there. 22 | * Create a demo SQLite database at ``demo.sqlite3`` 23 | * Load the test fixtures from ``demo_fixtures.json.gz`` 24 | * Start up the web service on ``8990/tcp`` 25 | 26 | If you encounter an issue 27 | ------------------------- 28 | 29 | * Have you run the demo previously? Try deleting ``demo.sqlite3`` and starting 30 | over! 31 | 32 | Explore 33 | ======= 34 | 35 | Once it's running, point your browser to http://localhost:8990/ 36 | 37 | 38 | You'll be prompted for a username/password 39 | 40 | Username 41 | admin@localhost 42 | 43 | Password 44 | admin 45 | 46 | **Note:** If NSoT isn't installed on ``localhost``, substitute the IP or 47 | hostname where it is installed. 48 | 49 | Screenshot: 50 | 51 | .. image:: ../docs/_static/web_login.png 52 | :alt: NSoT Login 53 | 54 | CLI 55 | --- 56 | 57 | The CLI utility is maintained in a separate project called pynsot. Read more 58 | about it at https://pynsot.readthedocs.io. 59 | 60 | You may install it by running:: 61 | 62 | $ pip install pynsot 63 | 64 | You may use it by running:: 65 | 66 | $ nsot 67 | 68 | API 69 | --- 70 | 71 | The Browsable API can be found at http://localhost:8990/api/ 72 | 73 | Docs 74 | ---- 75 | 76 | The interactive API explorer can be found at http://localhost:8990/docs/ 77 | -------------------------------------------------------------------------------- /demo/demo_fixtures.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/demo/demo_fixtures.json.gz -------------------------------------------------------------------------------- /demo/demo_settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | NSoT demo settings. 3 | """ 4 | from __future__ import absolute_import 5 | from nsot.conf.settings import * 6 | import os 7 | import os.path 8 | 9 | # Path where the config is found. 10 | CONF_ROOT = os.path.dirname(__file__) 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', 15 | 'NAME': 'demo.sqlite3', 16 | } 17 | } 18 | 19 | SECRET_KEY = u'fMK68NKgazLCjjTXjDtthhoRUS8IV4lwD-9G7iVd2Xs=' 20 | 21 | AUTH_TOKEN_EXPIRY = 60 * 60 * 3 # 30 minutes 22 | 23 | STATIC_ROOT = 'staticfiles' 24 | 25 | # The address on which the application will listen. 26 | # Default: localhost 27 | NSOT_HOST = '0.0.0.0' 28 | 29 | # The port on which the application will be accessed. 30 | # Default: 8990 31 | NSOT_PORT = 8990 32 | 33 | # Enable DEBUG logging to console 34 | if os.getenv('NSOT_DEBUG'): 35 | DEBUG = True 36 | LOGGING['loggers']['nsot']['level'] = 'DEBUG' 37 | LOGGING['loggers']['django.db.backends'] = {'handlers': ['console'], 'level': 'DEBUG'} 38 | -------------------------------------------------------------------------------- /demo/run_demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export NSOT_CONF=$(pwd)/demo_settings.py 4 | DB_FILE=$(pwd)/demo.sqlite3 5 | 6 | # Only create the database if it doesn't exist. 7 | if [ ! -f ${DB_FILE} ]; then 8 | # Create the database. 9 | echo "Database not found; creating database..." 10 | nsot-server upgrade --noinput 11 | 12 | # Load demo data fixtures. 13 | echo -e "\nLoading demo data fixtures..." 14 | nsot-server loaddata demo_fixtures 15 | else 16 | echo -e "Database already exists; continuing..." 17 | fi 18 | 19 | # Start web service 20 | echo "Starting web service..." 21 | nsot-server start 22 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER Codey oxley 3 | EXPOSE 8990 4 | 5 | # These are the supported environment variables. 6 | # Use them to control the 'default' Django database configuration: 7 | # DB_ENGINE 8 | # DB_NAME 9 | # DB_USER 10 | # DB_PASSWORD 11 | # DB_HOST 12 | # DB_PORT 13 | # 14 | # NSOT_EMAIL 15 | # NSOT_SECRET 16 | 17 | # Install necessary packages 18 | # 19 | # The development packages are for building certain dependencies that pip pulls 20 | # in 21 | ENV DEBIAN_FRONTEND noninteractive 22 | RUN apt-get --quiet=2 update 23 | RUN apt-get --quiet=2 install -y \ 24 | python \ 25 | python-dev \ 26 | python-pip \ 27 | libffi6 \ 28 | libffi-dev \ 29 | libssl-dev \ 30 | libmysqlclient-dev \ 31 | curl 32 | 33 | RUN apt-get --quiet=2 install -y python-psycopg2 34 | RUN apt-get --quiet=2 install -y sqlite3 35 | RUN pip install MySQL-Python 36 | RUN pip install psycopg2 37 | # upgrade pip to fix https://github.com/dropbox/nsot/issues/277 38 | RUN pip install -U setuptools 39 | 40 | # Try to run this as late as possible for layer caching - this version will be 41 | # updated every update so let the build not take longer than necessary 42 | RUN pip install nsot==1.4.6 43 | 44 | COPY conf /etc/nsot 45 | 46 | ENTRYPOINT ["nsot-server", "--config=/etc/nsot/nsot.conf.py"] 47 | 48 | # If using --no-upgrade then the database won't be built for first run. Use 49 | # should specify --no-upgrade manually if they don't want it 50 | CMD ["start", "--noinput"] 51 | # CMD ["start", "--noinput", "--no-upgrade"] 52 | -------------------------------------------------------------------------------- /docker/Dockerfile.sub: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER Codey oxley 3 | EXPOSE 8990 4 | 5 | # These are the supported environment variables. 6 | # Use them to control the 'default' Django database configuration: 7 | # DB_ENGINE 8 | # DB_NAME 9 | # DB_USER 10 | # DB_PASSWORD 11 | # DB_HOST 12 | # DB_PORT 13 | # 14 | # NSOT_EMAIL 15 | # NSOT_SECRET 16 | 17 | # Install necessary packages 18 | # 19 | # The development packages are for building certain dependencies that pip pulls 20 | # in 21 | ENV DEBIAN_FRONTEND noninteractive 22 | RUN apt-get --quiet=2 update 23 | RUN apt-get --quiet=2 install -y \ 24 | python \ 25 | python-dev \ 26 | python-pip \ 27 | libffi6 \ 28 | libffi-dev \ 29 | libssl-dev \ 30 | libmysqlclient-dev \ 31 | curl 32 | 33 | RUN apt-get --quiet=2 install -y python-psycopg2 34 | RUN apt-get --quiet=2 install -y sqlite3 35 | RUN pip install MySQL-Python 36 | RUN pip install psycopg2 37 | # upgrade pip to fix https://github.com/dropbox/nsot/issues/277 38 | RUN pip install -U setuptools 39 | 40 | # Try to run this as late as possible for layer caching - this version will be 41 | # updated every update so let the build not take longer than necessary 42 | RUN pip install nsot=={{ NSOT_VERSION }} 43 | COPY conf /etc/nsot 44 | 45 | ENTRYPOINT ["nsot-server", "--config=/etc/nsot/nsot.conf.py"] 46 | 47 | # If using --no-upgrade then the database won't be built for first run. Use 48 | # should specify --no-upgrade manually if they don't want it 49 | CMD ["start", "--noinput"] 50 | # CMD ["start", "--noinput", "--no-upgrade"] 51 | -------------------------------------------------------------------------------- /docker/conf/nsot.conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | This configuration file is just Python code. You may override any global 3 | defaults by specifying them here. 4 | 5 | For more information on this file, see 6 | https://docs.djangoproject.com/en/1.8/topics/settings/ 7 | 8 | For the full list of settings and their values, see 9 | https://docs.djangoproject.com/en/1.8/ref/settings/ 10 | """ 11 | from __future__ import absolute_import 12 | from nsot.conf.settings import * # noqa 13 | 14 | import os 15 | 16 | # Path where the config is found. 17 | CONF_ROOT = '/etc/nsot' 18 | 19 | # A boolean that turns on/off debug mode. Never deploy a site into production 20 | # with DEBUG turned on. 21 | # Default: False 22 | DEBUG = False 23 | 24 | ############ 25 | # Database # 26 | ############ 27 | # https://docs.djangoproject.com/en/dev/ref/settings/#databases 28 | DATABASES = { 29 | 'default': { 30 | 'ENGINE': os.environ.get('DB_ENGINE', 'django.db.backends.sqlite3'), 31 | 'NAME': os.environ.get('DB_NAME', 'nsot.sqlite3'), 32 | 'USER': os.environ.get('DB_USER', 'nsot'), 33 | 'PASSWORD': os.environ.get('DB_PASSWORD', ''), 34 | 'HOST': os.environ.get('DB_HOST', ''), 35 | 'PORT': os.environ.get('DB_PORT', '') 36 | } 37 | } 38 | 39 | ############### 40 | # Application # 41 | ############### 42 | 43 | # The address on which the application will listen. 44 | # Default: localhost 45 | NSOT_HOST = '0.0.0.0' 46 | 47 | # The port on which the application will be accessed. 48 | # Default: 8990 49 | NSOT_PORT = 8990 50 | 51 | # If True, serve static files directly from the app. 52 | # Default: True 53 | SERVE_STATIC_FILES = True 54 | 55 | ############ 56 | # Security # 57 | ############ 58 | 59 | # A URL-safe base64-encoded 32-byte key. This must be kept secret. Anyone with 60 | # this key is able to create and read messages. This key is used for 61 | # encryption/decryption of sessions and auth tokens. 62 | # 63 | # WARNING: This default value provided here is for development purposes only! 64 | # If deploying to a production environment, be sure to specify a NSOT_SECRET 65 | # environment variable. See docker/README.md for more details. 66 | SECRET_KEY = os.environ.get( 67 | 'NSOT_SECRET', 'nJvyRB8tckUWvquJZ3ax4QnhpmqTgVX2k3CDY13yK9E=' 68 | ) 69 | 70 | # Header to check for Authenticated Email. This is intended for use behind an 71 | # authenticating reverse proxy. 72 | USER_AUTH_HEADER = os.environ.get('NSOT_EMAIL', 'X-NSoT-Email') 73 | 74 | # The age, in seconds, until an AuthToken granted by the API will expire. 75 | # Default: 600 76 | AUTH_TOKEN_EXPIRY = 600 # 10 minutes 77 | 78 | # A list of strings representing the host/domain names that this Django site 79 | # can serve. This is a security measure to prevent an attacker from poisoning 80 | # caches and triggering password reset emails with links to malicious hosts by 81 | # submitting requests with a fake HTTP Host header, which is possible even 82 | # under many seemingly-safe web server configurations. 83 | # https://docs.djangoproject.com/en/1.8/ref/settings/#allowed-hosts 84 | ALLOWED_HOSTS = ['*'] 85 | -------------------------------------------------------------------------------- /docs/_static/logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/docs/_static/logo_128.png -------------------------------------------------------------------------------- /docs/_static/web_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/docs/_static/web_login.png -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | ############# 2 | API Reference 3 | ############# 4 | 5 | If you are looking for information on how to utilize the REST API, or a 6 | specific function, class or method, this part of the documentation is for you. 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | rest 12 | python 13 | -------------------------------------------------------------------------------- /docs/api/python.rst: -------------------------------------------------------------------------------- 1 | Python API 2 | ========== 3 | 4 | .. contents:: 5 | :local: 6 | :depth: 2 7 | 8 | Database Primitives 9 | ------------------- 10 | 11 | Models 12 | ~~~~~~ 13 | 14 | .. automodule:: nsot.models 15 | :members: 16 | 17 | Fields 18 | ~~~~~~ 19 | 20 | .. automodule:: nsot.fields 21 | :members: 22 | 23 | Validators 24 | ~~~~~~~~~~ 25 | 26 | .. automodule:: nsot.validators 27 | :members: 28 | 29 | Exceptions 30 | ---------- 31 | 32 | .. automodule:: nsot.exc 33 | :members: 34 | 35 | REST API Primitives 36 | ------------------- 37 | 38 | .. module:: nsot.api 39 | 40 | Views 41 | ~~~~~ 42 | 43 | .. automodule:: nsot.api.views 44 | :members: 45 | 46 | Serializers 47 | ~~~~~~~~~~~ 48 | 49 | .. automodule:: nsot.api.serializers 50 | :members: 51 | 52 | Filters 53 | ~~~~~~~ 54 | 55 | .. automodule:: nsot.api.filters 56 | :members: 57 | 58 | Pagination 59 | ~~~~~~~~~~ 60 | 61 | .. automodule:: nsot.api.pagination 62 | :members: 63 | 64 | Routers 65 | ~~~~~~~ 66 | 67 | .. automodule:: nsot.api.routers 68 | :members: 69 | 70 | URLs 71 | ~~~~ 72 | 73 | .. automodule:: nsot.api.urls 74 | :members: 75 | 76 | Authentication 77 | ~~~~~~~~~~~~~~ 78 | 79 | .. automodule:: nsot.api.auth 80 | :members: 81 | 82 | Utilities 83 | --------- 84 | 85 | .. automodule:: nsot.util 86 | :members: 87 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: 2 | ../CHANGELOG.rst 3 | -------------------------------------------------------------------------------- /docs/config.rst: -------------------------------------------------------------------------------- 1 | .. _configuration: 2 | 3 | ############# 4 | Configuration 5 | ############# 6 | 7 | .. contents:: 8 | :local: 9 | :depth: 2 10 | 11 | Configuring NSoT 12 | ================ 13 | 14 | This section describes how to get started with configuring the NSoT server. 15 | 16 | Initializing the Configuration 17 | ------------------------------ 18 | 19 | You may generate an initial configuration by executing ``nsot-server init``. By 20 | default the file will be created at ``~/.nsot/nsot.conf.py``. You may specify a 21 | different location for the configuration as the argument to ``init``: 22 | 23 | .. code-block:: bash 24 | 25 | nsot-server init /etc/nsot.conf.py 26 | 27 | Specifying your Configuration 28 | ----------------------------- 29 | 30 | If you do not wish to utilize the default location, you must provide the 31 | ``--config`` argument when executing ``nsot-server`` so that it knows where to 32 | find it. For example, to start the server with the configuration in an 33 | alternate location: 34 | 35 | .. code-block:: bash 36 | 37 | nsot-server --config=/etc/nsot.conf.py start 38 | 39 | You may also set the ``NSOT_CONF`` enviroment variable to the location of your 40 | configuration file so that you don't have to provide the ``--config`` 41 | argument: 42 | 43 | .. code-block:: bash 44 | 45 | $ export NSOT_CONF=/etc/nsot.conf.py 46 | $ nsot-server start 47 | 48 | Sample Configuration 49 | -------------------- 50 | 51 | Below is a sample configuration file that covers the primary settings you may 52 | care about, and their default values. 53 | 54 | .. literalinclude:: ../tests/test_settings.py 55 | :language: python 56 | 57 | Advanced Configuration 58 | ====================== 59 | 60 | This section covers additional configuration options available to the NSoT 61 | server and advanced configuration topics. 62 | 63 | Database 64 | -------- 65 | 66 | NSoT defaults to utilizing SQLite as a database backend, but supports any database 67 | backend supported by Django. The default backends available are SQLite, MySQL, 68 | PostgreSQL, and Oracle. 69 | 70 | .. code-block:: python 71 | 72 | DATABASES = { 73 | 'default': { 74 | 'ENGINE': 'django.db.backends.sqlite3', 75 | 'NAME': 'nsot.sqlite3', 76 | } 77 | } 78 | 79 | For more information on configuring the database, please see the `official 80 | Django database documentation 81 | `_. 82 | 83 | Caching 84 | ------- 85 | 86 | **Note:** At this time only Interface objects are cached if caching is enabled! 87 | 88 | NSoT includes built-in support for caching of API results. The default is to 89 | use to the "dummy" cache that doesn't actually cache -- it just implements the 90 | cache interface without doing anything. 91 | 92 | .. code-block:: python 93 | 94 | CACHES = { 95 | 'default': { 96 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 97 | } 98 | } 99 | 100 | The cache is invalidated on any update or delete of an object. Caching can 101 | dramatically perform read operations of databases with a large amount of 102 | network Interface objects. 103 | 104 | If you need caching, see the `official Django caching documentation 105 | `_ on how to set 106 | it up. 107 | -------------------------------------------------------------------------------- /docs/dockerfile.rst: -------------------------------------------------------------------------------- 1 | ########## 2 | Dockerfile 3 | ########## 4 | 5 | .. literalinclude:: 6 | ../docker/Dockerfile 7 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ####################### 2 | Network Source of Truth 3 | ####################### 4 | 5 | Network Source of Truth (NSoT) a source of truth database and repository for tracking 6 | inventory and metadata of network entities to ease management and automation of 7 | network infrastructure. 8 | 9 | NSoT is an API-first application that provides a REST API and a web application 10 | front-end for managing IP addresses (IPAM), network devices, and network 11 | interfaces. 12 | 13 | |Build Status| |Documentation Status| |PyPI Status| 14 | 15 | **Contents:** 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | quickstart 21 | installation 22 | config 23 | tutorial 24 | admin 25 | models 26 | api/index 27 | development 28 | changelog 29 | support 30 | 31 | .. |Build Status| image:: https://img.shields.io/travis/dropbox/nsot/master.svg?style=flat 32 | :target: https://travis-ci.org/dropbox/nsot 33 | :width: 88px 34 | :height: 20px 35 | .. |Documentation Status| image:: https://readthedocs.org/projects/nsot/badge/?version=latest&style=flat 36 | :target: https://readthedocs.org/projects/nsot/?badge=latest 37 | :width: 76px 38 | :height: 20px 39 | .. |PyPI Status| image:: https://img.shields.io/pypi/v/nsot.svg?style=flat 40 | :target: https://pypi.python.org/pypi/nsot 41 | :width: 68px 42 | :height: 20px 43 | 44 | Logo_ by Vecteezy_ is licensed under `CC BY-SA 3.0`_ 45 | 46 | .. _Logo: https://www.iconfinder.com/icons/532251 47 | .. _Vecteezy: https://www.iconfinder.com/Vecteezy 48 | .. _CC BY-SA 3.0: http://creativecommons.org/licenses/by-sa/3.0/ 49 | -------------------------------------------------------------------------------- /docs/install/centos.rst: -------------------------------------------------------------------------------- 1 | ###### 2 | CentOS 3 | ###### 4 | 5 | This installation guide assumes that you have installed CentOS 6.4 on your 6 | machine, and are wanting to install NSoT. This guide will help you install NSoT 7 | and then run it locally from a browser window. 8 | 9 | Installation 10 | ============ 11 | 12 | To ensure your CentOS installation is up to date, please update it. 13 | Once complete, open a command prompt and run the following: 14 | 15 | .. code-block:: bash 16 | 17 | $ sudo yum install -y openssl-devel python-devel libffi-devel gcc-plugin-devel 18 | $ sudo yum install -y epel-release 19 | $ sudo yum install -y python-pip 20 | 21 | Next you'll need to upgrade Pip to the latest version with some security addons: 22 | 23 | .. code-block:: bash 24 | 25 | $ sudo pip install --upgrade pip 26 | $ sudo pip install requests[security] 27 | 28 | Now we are ready to Pip install NSoT: 29 | 30 | .. code-block:: bash 31 | 32 | $ sudo pip install nsot 33 | 34 | Now you are ready to follow the :doc:`../quickstart` starting at step 2! 35 | -------------------------------------------------------------------------------- /docs/install/docker.rst: -------------------------------------------------------------------------------- 1 | ###### 2 | Docker 3 | ###### 4 | 5 | Want to use Docker? More on this later. For now you may look at the ``docker`` 6 | directory at the top of the repository on GitHub, or if you're feeling plucky, 7 | check out the contents of :doc:`../dockerfile`. 8 | 9 | Quick start 10 | =========== 11 | 12 | .. code-block:: bash 13 | 14 | $ cd docker 15 | $ docker run -p 8990:8990 -d --name=nsot nsot/nsot start --noinput 16 | 17 | README 18 | ====== 19 | 20 | Here is the readme until we clean up these docs and include them for real here. 21 | 22 | .. literalinclude:: ../../docker/README.md 23 | :language: md 24 | -------------------------------------------------------------------------------- /docs/install/fedora.rst: -------------------------------------------------------------------------------- 1 | ###### 2 | Fedora 3 | ###### 4 | 5 | This installation guide assumes that you have installed Fedora 22 on your 6 | machine, and are wanting to install NSoT. This guide will help you install NSoT 7 | and then run it locally from a browser window. 8 | 9 | Installation 10 | ============ 11 | 12 | To ensure your Fedora installation is up to date, please update it. 13 | Once complete, open a command prompt and run the following: 14 | 15 | .. code-block:: bash 16 | 17 | $ sudo dnf -y install gcc gcc-c++ libffi libffi-devel python-devel openssl-devel 18 | $ sudo dnf -y gcc-plugin-devel make automake kernel kernel-devel psmisc 19 | $ sudo dnf -y install python2-devel 20 | 21 | Next you'll need to upgrade Pip to the latest version: 22 | 23 | .. code-block:: bash 24 | 25 | $ sudo pip install --upgrade pip 26 | 27 | Now we are ready to install NSoT: 28 | 29 | .. code-block:: bash 30 | 31 | $ sudo pip install nsot 32 | 33 | Now you are ready to follow the :doc:`../quickstart` starting at step 2! 34 | -------------------------------------------------------------------------------- /docs/install/macosx.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | Mac OS X 3 | ######## 4 | 5 | This tutorial is designed to get a working version os NSoT installed inside a 6 | Mac OS X system. We will install virtual environment wrappers to keep things 7 | tidy, and isolate our installation. 8 | 9 | Installation 10 | ============ 11 | 12 | Xcode 13 | ----- 14 | 15 | It is assumed that you have a Mac, running OS X (written for 10.10.5), and 16 | xcode already installed. If you don't have Xcode, please install it. You may 17 | need to agree to a command line license. We suggest running this via command 18 | line prior to install: 19 | 20 | .. code-block:: bash 21 | 22 | $ xcodebuild -license 23 | 24 | Prerequisites 25 | ------------- 26 | 27 | We will put our installation of NSoT inside a Python virtual environment to 28 | isolate the installation. To do so we need virtualenv, and virtualenvwrapper. 29 | Open a command prompt, and install them with pip: 30 | 31 | .. code-block:: bash 32 | 33 | $ pip install virtualenv 34 | $ pip install virtualenvwrapper 35 | 36 | Ensure installation by running a which, and finding out where they now live: 37 | 38 | .. code-block:: bash 39 | 40 | $ which virtualenvwrapper 41 | $ which virtualenv 42 | 43 | Next we tell bash where these virtual environments are, and where to save the 44 | associated data: 45 | 46 | .. code-block:: bash 47 | 48 | $ vi ~/.bashrc 49 | 50 | Add these three lines: 51 | 52 | .. code-block:: bash 53 | 54 | export WORKON_HOME=$HOME/.virtualenvs 55 | export PROJECT_HOME=$HOME/Devel 56 | source /usr/local/bin/virtualenvwrapper.sh 57 | 58 | Now restart bash to implement the changes: 59 | 60 | .. code-block:: bash 61 | 62 | $ source ~/.bashrc 63 | 64 | Install NSoT 65 | ------------ 66 | 67 | NSoT will be installed via command line, into the folder of your choice. If you 68 | don't have a preffered folder, may we suggest this: 69 | 70 | .. code-block:: bash 71 | 72 | $ mkdir ~/sandbox && cd ~/sandbox 73 | 74 | CD into the folder, make a virtual environment, and start it: 75 | 76 | .. code-block:: bash 77 | 78 | $ mkvirtualenv nsot 79 | $ pip install 80 | 81 | Once in the folder of choice, install NSoT: 82 | 83 | .. code-block:: bash 84 | 85 | $ pip install nsot 86 | 87 | Now you are ready to follow the :doc:`../quickstart` starting at step 2! 88 | -------------------------------------------------------------------------------- /docs/install/suse.rst: -------------------------------------------------------------------------------- 1 | #### 2 | SuSe 3 | #### 4 | 5 | This installation guide assumes that you have installed SuSe 13 on your 6 | machine, and are wanting to install NSoT. This guide will help you install NSoT 7 | and then run it locally from a browser window. 8 | 9 | Installation 10 | ============ 11 | 12 | To ensure your SuSe installation is up to date, please update it. We'll begin 13 | by opening a command prompt. Make sure your certificates are properly 14 | installed, or use this certificate: 15 | 16 | .. code-block:: bash 17 | 18 | $ wget --no-check-certificate 'https://raw.githubusercontent.com/mitchellh/vagrant/master/keys/vagrant.pub' -O /home/vagrant/.ssh/authorized_keys 19 | 20 | Now we'll install the prerequisite software with zypper: 21 | 22 | .. code-block:: bash 23 | 24 | $ sudo zypper --non-interactive in python-devel gcc gcc-c++ git libffi48-devel libopenssl-devel python-pip 25 | 26 | Next you'll need to upgrade Pip and security addons: 27 | 28 | .. code-block:: bash 29 | 30 | $ sudo pip install --upgrade pip 31 | $ sudo pip install requests[security] 32 | 33 | Now we are ready to install NSoT: 34 | 35 | .. code-block:: bash 36 | 37 | $ sudo pip install nsot 38 | 39 | SuSe Firewall 40 | ------------- 41 | 42 | To access NSoT from a local browser we'll need to turn off the security for 43 | this demo: 44 | 45 | .. code-block:: bash 46 | 47 | $ sudo /sbin/service SuSEfirewall2_setup stop 48 | 49 | For production installations we reccomend adding a rule to your iptables for 50 | NSoT on ports ``8990/tcp`` (or the port of your choosing). 51 | 52 | Now you are ready to follow the :doc:`../quickstart` starting at step 2! 53 | -------------------------------------------------------------------------------- /docs/install/ubuntu.rst: -------------------------------------------------------------------------------- 1 | ###### 2 | Ubuntu 3 | ###### 4 | 5 | This installation guide assumes that you are running Ubuntu version 12.04, 6 | 14.04, or 16.04 on your machine, and are wanting to install NSoT. This guide 7 | will help you install NSoT and then run it locally from a browser window. 8 | 9 | Installation 10 | ============ 11 | 12 | To ensure your Ubuntu installation is up to date, please update it. Open a 13 | command prompt and run the following: 14 | 15 | .. code-block:: bash 16 | 17 | $ sudo apt-get -y update 18 | 19 | Once your machine is up to date, we need to install development libraries to 20 | allow NSoT to build: 21 | 22 | .. code-block:: bash 23 | 24 | $ sudo apt-get -y install build-essential python-dev libffi-dev libssl-dev 25 | 26 | The Python Pip installer and the git repository management tools are needed 27 | too. We'll go ahead and get those next: 28 | 29 | .. code-block:: bash 30 | 31 | $ sudo apt-get --yes install python-pip git 32 | 33 | .. code-block:: bash 34 | 35 | $ sudo pip install nsot 36 | 37 | Now you are ready to follow the :doc:`../quickstart` starting at step 2! 38 | -------------------------------------------------------------------------------- /docs/install/vagrant.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../vagrant/README.rst 2 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | ############ 2 | Installation 3 | ############ 4 | 5 | Dependencies 6 | ============ 7 | 8 | Network Source of Truth (NSoT) should run on any Unix-like platform that has: 9 | 10 | + Python 2.7 11 | + `pip `_ 12 | 13 | Python dependencies 14 | ------------------- 15 | 16 | If you install using pip (which you should) these will be installed for you 17 | automatically. For the brave, check out the contents of :doc:`requirements`. 18 | 19 | Platform-Specific Installation Instructions 20 | =========================================== 21 | 22 | These guides go into detail on how to install NSoT on a given platform. 23 | 24 | .. toctree:: 25 | :maxdepth: 1 26 | :glob: 27 | 28 | install/centos 29 | install/fedora 30 | install/macosx 31 | install/suse 32 | install/ubuntu 33 | 34 | Virtual Machine Install Instructions 35 | ==================================== 36 | 37 | These guides go into detail on how to get running NSoT on virtual machines. 38 | 39 | .. toctree:: 40 | :maxdepth: 1 41 | :glob: 42 | 43 | install/docker 44 | install/vagrant 45 | 46 | .. _demo: 47 | 48 | Official Client 49 | =============== 50 | 51 | We maintain the official NSoT client under a separate project called `pyNSoT 52 | `_. PyNSoT provides a Python API client and an 53 | excellent CLI utility. 54 | 55 | If you wish to utilize NSoT from the command-line, or follow along in the 56 | :doc:`tutorial`, you're going to need this! 57 | 58 | Installing the client is as easy as running ``pip install pynsot``. Setup is a 59 | breeze, too. If you run into any issues, please refer to the `official pyNSoT 60 | documentation `_. 61 | 62 | Demo 63 | ==== 64 | 65 | If you would like to run the demo, make sure you've got NSoT installed and that 66 | you have a fresh clone of the NSoT repository from GitHub. 67 | 68 | If you don't already have a clone, clone it and change into the ``nsot`` 69 | directory: 70 | 71 | .. code-block:: bash 72 | 73 | $ git clone https://github.com/dropbox/nsot 74 | $ cd nsot 75 | 76 | Then to switch to the ``demo`` directory and fire up the demo: 77 | 78 | .. code-block:: bash 79 | 80 | $ cd nsot/demo 81 | $ ./run_demo.sh 82 | 83 | The demo will be available at http://localhost:8990/ 84 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | ########### 2 | Quick Start 3 | ########### 4 | 5 | Network Source of Truth is super easy to get running. If you just can't wait to 6 | skip ahead, this guide is for you. 7 | 8 | .. note:: 9 | This quick start assumes a lot. If it doesn't work for you, please skip 10 | this and read the installation_ guide. 11 | 12 | .. _installation: https://github.com/dropbox/nsot/blob/develop/docs/installation.rst 13 | 14 | 1. Install NSoT: 15 | 16 | .. code-block:: bash 17 | 18 | $ pip install nsot 19 | 20 | 2. Initialize the config (this will create a default config in 21 | ``~/.nsot/nsot.conf.py``): 22 | 23 | .. code-block:: bash 24 | 25 | $ nsot-server init 26 | 27 | 3. Create a superuser and start the server on ``8990/tcp`` (the default): 28 | 29 | .. code-block:: bash 30 | 31 | $ nsot-server createsuperuser --email admin@localhost 32 | Password: 33 | Password (again): 34 | Superuser created successfully. 35 | 36 | .. code-block:: bash 37 | 38 | $ nsot-server start 39 | 40 | 4. Now fire up your browser and visit http://localhost:8990! 41 | 42 | .. image:: _static/web_login.png 43 | :alt: NSoT Login 44 | 45 | 5. Use the username/password created in step 3 to login. 46 | 47 | Now, head over to the tutorial_ to start getting acquainted with NSoT! 48 | 49 | .. _tutorial: https://github.com/dropbox/nsot/blob/develop/docs/tutorial.rst 50 | -------------------------------------------------------------------------------- /docs/requirements.rst: -------------------------------------------------------------------------------- 1 | ################ 2 | requirements.txt 3 | ################ 4 | 5 | .. literalinclude:: 6 | ../requirements.txt 7 | -------------------------------------------------------------------------------- /docs/support.rst: -------------------------------------------------------------------------------- 1 | Support 2 | ======= 3 | 4 | Network Source of Truth is a primarily a Pacific coast operation, so your best 5 | chance of getting a real-time response is during the weekdays, Pacific time. 6 | 7 | The best way to get support, provide feedback, ask questions, or to just talk 8 | shop is to find us online on Slack. 9 | 10 | We have a bi-directional Slack/IRC bridge for our channels so that users can 11 | use whichever they prefer. 12 | 13 | Slack 14 | ----- 15 | 16 | We hangout on `Slack `_ on the `NetworkToCode 17 | `_ team. 18 | 19 | 1. `Request to join the team `_ (it's free!) 20 | 2. Join us in ``#nsot``. 21 | 22 | IRC 23 | --- 24 | 25 | If you want to keep it old school and use IRC, find us at ``#nsot`` on Freenode 26 | (irc://irc.freenode.net/nsot). 27 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | ######## 2 | Tutorial 3 | ######## 4 | 5 | Here's how to use NSoT. 6 | 7 | This document assumes that you've already have an instance of NSoT installed, 8 | configured, and running on your system. If you don't, please either check out 9 | the :doc:`quickstart` or head over to the full-blown :doc:`installation` guide 10 | and then return here. 11 | 12 | First Steps 13 | =========== 14 | 15 | .. important:: 16 | Because this is a work-in-progress, we're going to use the command-line 17 | utility provided by the official NSoT client to get you acquainted with 18 | NSoT. 19 | 20 | Install the Client 21 | ------------------ 22 | 23 | First things first, you'll need to install `pyNSoT 24 | `_, the official Python API client and CLI 25 | utility: 26 | 27 | .. code-block:: bash 28 | 29 | $ pip install pynsot 30 | 31 | Configure the Client 32 | -------------------- 33 | 34 | After you've installed the client, please follow the `pyNSoT Configuration 35 | `_ guide to establish a 36 | ``.pynsotrc`` file. 37 | 38 | Using the Command-Line 39 | ====================== 40 | 41 | Once you've got a working ``nsot`` CLI setup, please follow the `pyNSoT 42 | Command-Line `_ guide. This 43 | will get you familiarized with the basics of how NSoT works. 44 | 45 | Understanding the Data Model 46 | ============================ 47 | 48 | NSoT has a relatively simple data model, but the objects themselves can be 49 | quite sophisticated. Familiarize yourself with the :doc:`models`. 50 | 51 | Using the REST API 52 | ================== 53 | 54 | Familiarize yourself with the basics of the :doc:`api/rest`. 55 | 56 | Administering the Server 57 | ======================== 58 | 59 | Familiarize with the ``nsot-server`` command that is used to manage your server 60 | instance by checking out the :doc:`admin` guide. 61 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Using NSoT 2 | ========== 3 | 4 | Coming Soon. 5 | -------------------------------------------------------------------------------- /iftypes.txt: -------------------------------------------------------------------------------- 1 | Distinct types 2 | 3 | mysql> select if_type, count(*) as num from ports group by if_type; 4 | +---------+--------+ 5 | | if_type | num | 6 | +---------+--------+ 7 | | 1 | 4482 | 8 | | 6 | 178033 | 9 | | 24 | 2031 | 10 | | 53 | 20402 | 11 | | 131 | 875 | 12 | | 135 | 17 | 13 | | 136 | 1590 | 14 | | 150 | 175 | 15 | | 161 | 3918 | 16 | +---------+--------+ 17 | 9 rows in set (0.15 sec) 18 | 19 | Type mappings 20 | 21 | These are mappings to the formal integer types from SNMP IF-MIB::ifType. The 22 | types listed here are the most commonly found in the wild. 23 | 24 | Ref: https://www.iana.org/assignments/ianaiftype-mib/ianaiftype-mib 25 | 26 | other(1), -- none of the following 27 | ethernetCsmacd(6), -- for all ethernet-like interfaces, 28 | -- regardless of speed, as per RFC3635 29 | softwareLoopback(24), 30 | tunnel (131), -- Encapsulation interface 31 | l2vlan (135), -- Layer 2 Virtual LAN using 802.1Q 32 | l3ipvlan (136), -- Layer 3 Virtual LAN using IP 33 | mplsTunnel (150), -- MPLS Tunnel Virtual Interface 34 | ieee8023adLag (161), -- IEEE 802.3ad Link Aggregate 35 | -------------------------------------------------------------------------------- /nsot/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ # noqa 2 | -------------------------------------------------------------------------------- /nsot/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from __future__ import absolute_import 4 | from custom_user.admin import EmailUserAdmin 5 | from django.contrib.auth import get_user_model 6 | from django.contrib import admin 7 | from django.utils.translation import ugettext_lazy as _ 8 | from guardian.admin import GuardedModelAdmin 9 | 10 | from . import models 11 | 12 | 13 | # Register our custom User model 14 | class UserAdmin(EmailUserAdmin): 15 | fieldsets = ( 16 | (None, { 17 | 'fields': ('email', 'secret_key', 'password'), 18 | }), 19 | (_('Permissions'), { 20 | 'fields': ( 21 | 'is_active', 'is_staff', 'is_superuser', 'groups', 22 | 'user_permissions' 23 | ), 24 | }), 25 | (_('Important dates'), { 26 | 'fields': ('last_login', 'date_joined') 27 | }), 28 | ) 29 | 30 | 31 | admin.site.register(get_user_model(), UserAdmin) 32 | 33 | 34 | class SiteAdmin(GuardedModelAdmin): 35 | list_display = ('name', 'description') 36 | list_filter = ('name',) 37 | 38 | 39 | admin.site.register(models.Site, SiteAdmin) 40 | 41 | 42 | class AttributeAdmin(GuardedModelAdmin): 43 | list_display = ('name', 'resource_name', 'description', 'required', 44 | 'display', 'multi', 'site') 45 | list_filter = ('name', 'resource_name', 'required', 'multi', 'site') 46 | 47 | 48 | admin.site.register(models.Attribute, AttributeAdmin) 49 | 50 | 51 | class ValueAdmin(GuardedModelAdmin): 52 | list_display = ('name', 'value', 'resource_name', 'resource_id') 53 | list_filter = ('name', 'value', 'resource_name') 54 | 55 | 56 | admin.site.register(models.Value, ValueAdmin) 57 | 58 | 59 | class ChangeAdmin(admin.ModelAdmin): 60 | list_display = ('id', 'user', 'event', 'resource_name', 'resource_id', 61 | 'get_change_at', 'resource_name', 'site') 62 | list_filter = ('event', 'site') 63 | 64 | 65 | admin.site.register(models.Change, ChangeAdmin) 66 | 67 | 68 | class DeviceAdmin(GuardedModelAdmin): 69 | list_display = ('hostname', 'site') 70 | list_filter = ('site',) 71 | 72 | fields = list_display 73 | 74 | 75 | admin.site.register(models.Device, DeviceAdmin) 76 | 77 | 78 | class NetworkAdmin(GuardedModelAdmin): 79 | mptt_level_indent = 10 80 | mptt_indent_field = 'cidr' 81 | list_display = ('cidr', 'network_address', 'prefix_length', 'ip_version', 82 | 'is_ip', 'parent', 'site') 83 | list_filter = ('prefix_length', 'is_ip', 'ip_version', 'site') 84 | 85 | def get_cidr(self): 86 | return '%s/%s' % (self.network_address, self.prefix_length) 87 | 88 | fields = ('network_address', 'broadcast_address', 'prefix_length', 89 | 'ip_version', 'is_ip', 'site') 90 | 91 | 92 | admin.site.register(models.Network, NetworkAdmin) 93 | 94 | 95 | class InterfaceAdmin(GuardedModelAdmin): 96 | list_display = ('name', 'device', 'parent', 'mac_address', 'type', 'speed') 97 | list_filter = ('type', 'speed') 98 | 99 | fields = list_display 100 | 101 | 102 | admin.site.register(models.Interface, InterfaceAdmin) 103 | 104 | 105 | class ProtocolTypeAdmin(GuardedModelAdmin): 106 | list_display = ('name', 'description', 'site') 107 | list_filter = ('name', 'site') 108 | 109 | 110 | admin.site.register(models.ProtocolType, ProtocolTypeAdmin) 111 | 112 | 113 | class ProtocolAdmin(GuardedModelAdmin): 114 | list_display = ('type', 'description', 'device', 'interface', 'circuit') 115 | list_filter = ('type', 'description', 'device', 'site') 116 | 117 | fields = ('type', 'device', 'interface', 'circuit', 'auth_string', 118 | 'description', 'site') 119 | 120 | 121 | admin.site.register(models.Protocol, ProtocolAdmin) 122 | -------------------------------------------------------------------------------- /nsot/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/nsot/api/__init__.py -------------------------------------------------------------------------------- /nsot/api/auth.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.conf import settings 3 | from django.contrib.auth import get_user_model 4 | from django.core.exceptions import ObjectDoesNotExist 5 | import logging 6 | from rest_framework import authentication 7 | from rest_framework import exceptions 8 | 9 | 10 | from ..util import normalize_auth_header 11 | 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | class AuthTokenAuthentication(authentication.BaseAuthentication): 17 | def authenticate(self, request): 18 | log.debug('Fetching AuthToken header.') 19 | 20 | authz = authentication.get_authorization_header(request).split() 21 | if not authz or authz[0].lower() != b'authtoken': 22 | return None 23 | 24 | if len(authz) == 1: 25 | raise exceptions.AuthenticationFailed( 26 | 'Invalid token header. No credentials provided.' 27 | ) 28 | elif len(authz) > 2: 29 | raise exceptions.AuthenticationFailed( 30 | 'Invalid token header. Token should not contain spaces.' 31 | ) 32 | 33 | auth_type, data = authz 34 | email, auth_token = data.split(':', 1) 35 | 36 | log.debug(' email: %r', email) 37 | log.debug('auth_token: %r', auth_token) 38 | 39 | return self.authenticate_credentials(email, auth_token) 40 | 41 | def authenticate_credentials(self, email, auth_token): 42 | user = get_user_model().verify_auth_token(email, auth_token) 43 | 44 | # If user is bad this time, it's an invalid login 45 | if user is None: 46 | raise exceptions.AuthenticationFailed( 47 | 'Invalid login/token expired.' 48 | ) 49 | # raise exc.Unauthorized('Invalid login/token expired.') 50 | 51 | log.debug('token_auth authenticated user: %s' % email) 52 | 53 | return (user, auth_token) 54 | 55 | def authenticate_header(self, request): 56 | return 'AuthToken' 57 | 58 | 59 | class EmailHeaderAuthentication(authentication.BaseAuthentication): 60 | def authenticate(self, request): 61 | user_auth_header = normalize_auth_header(settings.USER_AUTH_HEADER) 62 | log.debug('EmailHeaderAuthentication.authenticate(): auth_header = %r', 63 | user_auth_header) 64 | 65 | # Naively fetch the user email from the auth_header 66 | email = request.META.get(user_auth_header) 67 | log.debug('EmailHeaderAuthentication.authenticate(): email = %r', 68 | email) 69 | if email is None: 70 | return None 71 | 72 | # Fetch a stinkin' user 73 | try: 74 | user = get_user_model().objects.get(email=email) 75 | except ObjectDoesNotExist: 76 | # Make this a 400 for now since it's failing validation. 77 | raise exceptions.ValidationError( 78 | 'Username must contain a valid email address' 79 | ) 80 | 81 | # And return it. 82 | return (user, None) 83 | 84 | 85 | class SecretKeyAuthentication(authentication.BaseAuthentication): 86 | def authenticate_credentials(self, email, secret_key): 87 | try: 88 | user = get_user_model().objects.get(email=email) 89 | except ObjectDoesNotExist: 90 | user = None 91 | 92 | # Make sure we've got a user and the secret_key is valid 93 | if user is not None and user.verify_secret_key(secret_key): 94 | return user, secret_key # Auth success 95 | 96 | raise exceptions.AuthenticationFailed( 97 | 'Invalid email/secret_key' 98 | ) 99 | -------------------------------------------------------------------------------- /nsot/api/renderers.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import absolute_import 3 | from rest_framework.renderers import BrowsableAPIRenderer 4 | 5 | 6 | class FilterlessBrowsableAPIRenderer(BrowsableAPIRenderer): 7 | """Custom browsable API renderer that doesn't show filter forms.""" 8 | def get_filter_form(self, data, view, request): 9 | """ 10 | Disable filter form display. 11 | 12 | This is because of major performance problems with large installations, 13 | especially with large sets of related objects. 14 | 15 | FIXME(jathan): Revisit this after browsable API rendering has improved 16 | in future versions of DRF. 17 | """ 18 | return 19 | -------------------------------------------------------------------------------- /nsot/api/routers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from rest_framework_bulk.routes import BulkRouter 6 | from rest_framework_nested.routers import NestedSimpleRouter 7 | 8 | 9 | __all__ = ('BulkRouter', 'BulkNestedRouter') 10 | 11 | 12 | # Map of HTTP verbs to rest_framework_bulk operations. 13 | BULK_OPERATIONS_MAP = { 14 | 'put': 'bulk_update', 15 | 'patch': 'partial_bulk_update', 16 | 'delete': 'bulk_destroy', 17 | } 18 | 19 | 20 | class BulkNestedRouter(NestedSimpleRouter): 21 | """ 22 | Bulk-enabled nested router. 23 | """ 24 | def __init__(self, *args, **kwargs): 25 | super(BulkNestedRouter, self).__init__(*args, **kwargs) 26 | self.routes[0].mapping.update(BULK_OPERATIONS_MAP) 27 | -------------------------------------------------------------------------------- /nsot/api/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.conf.urls import url, include 6 | from django.conf import settings 7 | 8 | from . import routers, views 9 | 10 | 11 | # Register all endpoints as a top-level resource 12 | router = routers.BulkRouter(trailing_slash=settings.APPEND_SLASH) 13 | 14 | # Resources pinned to API index at / 15 | router.register(r'sites', views.SiteViewSet) 16 | router.register(r'attributes', views.AttributeViewSet) 17 | router.register(r'changes', views.ChangeViewSet) 18 | router.register(r'circuits', views.CircuitViewSet) 19 | router.register(r'devices', views.DeviceViewSet) 20 | router.register(r'interfaces', views.InterfaceViewSet) 21 | router.register(r'networks', views.NetworkViewSet) 22 | router.register(r'protocols', views.ProtocolViewSet) 23 | router.register(r'protocol_types', views.ProtocolTypeViewSet) 24 | router.register(r'users', views.UserViewSet) 25 | router.register(r'values', views.ValueViewSet) 26 | 27 | # Nested router for resources under /sites 28 | sites_router = routers.BulkNestedRouter( 29 | router, r'sites', lookup='site', trailing_slash=settings.APPEND_SLASH 30 | ) 31 | 32 | # Resources that are nested under /sites 33 | sites_router.register(r'attributes', views.AttributeViewSet) 34 | sites_router.register(r'changes', views.ChangeViewSet) 35 | sites_router.register(r'circuits', views.CircuitViewSet) 36 | sites_router.register(r'devices', views.DeviceViewSet) 37 | sites_router.register(r'interfaces', views.InterfaceViewSet) 38 | sites_router.register(r'networks', views.NetworkViewSet) 39 | sites_router.register(r'protocols', views.ProtocolViewSet) 40 | sites_router.register(r'protocol_types', views.ProtocolTypeViewSet) 41 | sites_router.register(r'values', views.ValueViewSet) 42 | 43 | # Wire up our API using automatic URL routing. 44 | # Additionally, we include login URLs for the browsable API. 45 | urlpatterns = [ 46 | # API routes 47 | url(r'^', include(router.urls)), 48 | url(r'^', include(sites_router.urls)), 49 | 50 | # Browsable API auth login 51 | url(r'^auth/', include('rest_framework.urls', 52 | namespace='rest_framework')), 53 | 54 | # API auth_token login/verify (email/secret_key) 55 | url(r'^authenticate/', views.AuthTokenLoginView.as_view(), 56 | name='authenticate'), 57 | url(r'^verify_token/', views.AuthTokenVerifyView.as_view(), 58 | name='verify_token'), 59 | ] 60 | -------------------------------------------------------------------------------- /nsot/conf/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /nsot/conf/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.conf import settings 4 | from django.conf.urls import include, url 5 | from django.contrib import admin 6 | from django.views.generic import RedirectView 7 | from rest_framework_swagger.views import get_swagger_view 8 | 9 | from ..api.views import NotFoundViewSet 10 | from ..ui.views import FeView 11 | 12 | 13 | # Custom error-handling views. 14 | handler400 = 'nsot.ui.views.handle400' 15 | handler403 = 'nsot.ui.views.handle403' 16 | handler404 = 'nsot.ui.views.handle404' 17 | handler500 = 'nsot.ui.views.handle500' 18 | 19 | # This is the basic API explorer for Swagger/OpenAPI 2.0 20 | schema_view = get_swagger_view(title='NSoT API') 21 | 22 | 23 | urlpatterns = [ 24 | # API 25 | url(r'^api/', include('nsot.api.urls')), 26 | 27 | # Catchall for missing endpoints 28 | url(r'^api/.*/$', NotFoundViewSet.as_view({'get': 'list'})), 29 | 30 | # Docs (Swagger 2.0) 31 | url(r'^docs/', schema_view, name='swagger'), 32 | 33 | # Admin 34 | url(r'^admin/', include(admin.site.urls)), 35 | 36 | # Favicon redirect for when people insist on fetching it from /favicon.ico 37 | url( 38 | r'^favicon\.ico$', 39 | RedirectView.as_view( 40 | url='%sbuild/images/favicon/favicon.ico' % settings.STATIC_URL, 41 | permanent=True 42 | ), 43 | name='favicon' 44 | ), 45 | 46 | # FE handlers 47 | # Catch index 48 | url(r'^$', FeView.as_view(), name='index'), 49 | 50 | # Catch all for remaining URLs 51 | url(r'^.*/$', FeView.as_view(), name='index'), 52 | ] 53 | -------------------------------------------------------------------------------- /nsot/exc.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from __future__ import absolute_import 3 | from collections import OrderedDict 4 | import logging 5 | 6 | from django.core.exceptions import ( 7 | ValidationError as DjangoValidationError, ObjectDoesNotExist, 8 | MultipleObjectsReturned 9 | ) 10 | from django.db import IntegrityError 11 | from django.db.models import ProtectedError 12 | from rest_framework.exceptions import ValidationError 13 | from rest_framework.exceptions import APIException 14 | from rest_framework.views import exception_handler 15 | 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | __all__ = ( 21 | 'Error', 'ModelError', 'BaseHttpError', 'BadRequest', 'Unauthorized', 22 | 'Forbidden', 'NotFound', 'Conflict', 'DjangoValidationError', 23 | 'ObjectDoesNotExist', 'ProtectedError', 'ValidationError', 24 | 'MultipleObjectsReturned' 25 | ) 26 | 27 | 28 | def custom_exception_handler(exc, context): 29 | """Always handle errors all pretty-like.""" 30 | # Call REST framework's default exception handler first to get the standard 31 | # error response. 32 | response = exception_handler(exc, context) 33 | 34 | # Now add the HTTP status code and message to the response. 35 | # We want an error response to look like this: 36 | # { 37 | # "error": { 38 | # "message": "Endpoint not found", 39 | # "code": 404 40 | # } 41 | # } 42 | log.debug('custom_exception_handler: exc = %r', exc) 43 | log.debug('custom_exception_handler: context = %r', context) 44 | if response is not None: 45 | orig_data = response.data 46 | log.debug('custom_exception_handler: orig_data = %r', orig_data) 47 | 48 | try: 49 | message = orig_data['detail'] 50 | if message == 'Not found.': 51 | message = 'Endpoint not found.' 52 | except KeyError: 53 | message = orig_data 54 | except TypeError: 55 | message = orig_data[0] 56 | 57 | data = OrderedDict([ 58 | ('error', { 59 | 'message': message, 60 | 'code': response.status_code, 61 | }), 62 | ]) 63 | response.data = data 64 | 65 | request = context['request'] 66 | log.debug('custom_exception_handler: request = %r', request) 67 | log.debug('custom_exception_handler: dir(request) = %r', dir(request)) 68 | log.debug('custom_exception_handler: request.data = %r', request.data) 69 | 70 | return response 71 | 72 | 73 | class Error(APIException): 74 | """ Baseclass for NSoT Exceptions.""" 75 | 76 | 77 | class ModelError(Error): 78 | """Base class for NSoT Model Exceptions.""" 79 | 80 | 81 | class BaseHttpError(Error): 82 | """Base HTTP error.""" 83 | pass 84 | 85 | 86 | class BadRequest(BaseHttpError): 87 | """HTTP 400 error.""" 88 | status_code = 400 89 | 90 | 91 | class Unauthorized(BaseHttpError): 92 | """HTTP 401 error.""" 93 | status_code = 401 94 | 95 | 96 | class Forbidden(BaseHttpError): 97 | """HTTP 403 error.""" 98 | status_code = 403 99 | 100 | 101 | class NotFound(BaseHttpError): 102 | """HTTP 404 error.""" 103 | status_code = 404 104 | default_detail = 'Endpoint not found.' 105 | 106 | 107 | class Conflict(BaseHttpError, IntegrityError): 108 | """HTTP 409 error.""" 109 | status_code = 409 110 | -------------------------------------------------------------------------------- /nsot/management/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /nsot/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /nsot/management/commands/generate_key.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | """Command to generate a secret key.""" 4 | 5 | from cryptography.fernet import Fernet 6 | 7 | from nsot.util.commands import NsotCommand 8 | 9 | 10 | class Command(NsotCommand): 11 | help = ( 12 | 'Generate a URL-safe base64-encoded 32-byte key for use in ' 13 | 'settings.SECRET_KEY.' 14 | ) 15 | 16 | def handle(self, **options): 17 | print(Fernet.generate_key()) 18 | -------------------------------------------------------------------------------- /nsot/management/commands/upgrade.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | """ 4 | Command for running any pending upgrades. 5 | """ 6 | 7 | from django.core.management import call_command 8 | 9 | from nsot.util.commands import NsotCommand 10 | 11 | 12 | class Command(NsotCommand): 13 | help = 'Performs any pending database migrations and upgrades' 14 | 15 | def add_arguments(self, parser): 16 | parser.add_argument( 17 | '--noinput', 18 | action='store_true', 19 | default=False, 20 | help='Tells Django to NOT prompt the user for input of any kind.', 21 | ) 22 | 23 | def handle(self, **options): 24 | call_command( 25 | 'migrate', 26 | interactive=(not options['noinput']), 27 | traceback=options['traceback'], 28 | verbosity=options['verbosity'], 29 | ) 30 | -------------------------------------------------------------------------------- /nsot/management/commands/user_proxy.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | """ 4 | Command for starting up an authenticating reverse proxy for use in development. 5 | 6 | Please, don't use me in production! 7 | """ 8 | 9 | 10 | import six.moves.BaseHTTPServer 11 | from django.conf import settings 12 | import getpass 13 | import socket 14 | 15 | from nsot.util.commands import NsotCommand, CommandError 16 | 17 | 18 | class Command(NsotCommand): 19 | help = 'Start an authenticating reverse proxy for use in development.' 20 | 21 | def add_arguments(self, parser): 22 | parser.add_argument( 23 | 'username', 24 | nargs='?', 25 | default=getpass.getuser(), 26 | help='Username used for authentication.', 27 | ) 28 | parser.add_argument( 29 | '-a', '--address', 30 | type=str, 31 | default=settings.NSOT_HOST, 32 | help='Address to listen on.', 33 | ) 34 | parser.add_argument( 35 | '-d', '--domain', 36 | type=str, 37 | default='localhost', 38 | help='Domain for user account.', 39 | ) 40 | parser.add_argument( 41 | '-H', '--auth-header', 42 | type=str, 43 | default=settings.USER_AUTH_HEADER, 44 | help='HTTP user auth header name.', 45 | ) 46 | parser.add_argument( 47 | '-P', '--backend-port', 48 | type=int, 49 | default=settings.NSOT_PORT, 50 | help='Port to proxy to.', 51 | ) 52 | parser.add_argument( 53 | '-p', '--listen-port', 54 | type=int, 55 | default=settings.NSOT_PORT + 1, 56 | help='Port to listen on.', 57 | ) 58 | 59 | def handle(self, **options): 60 | username = options.get('username') 61 | 62 | try: 63 | from mrproxy import UserProxyHandler 64 | except ImportError: 65 | raise SystemExit( 66 | 'mrproxy is required for the user proxy. Please see ' 67 | 'README.rst.' 68 | ) 69 | 70 | class ServerArgs(object): 71 | """Argument container for http service.""" 72 | def __init__(self, backend_port, username, auth_header): 73 | self.backend_port = backend_port 74 | self.header = ['%s: %s' % (auth_header, username)] 75 | 76 | username = '%s@%s' % (username, options.get('domain')) 77 | address = options.get('address') 78 | auth_header = options.get('auth_header') 79 | backend_port = options.get('backend_port') 80 | listen_port = options.get('listen_port') 81 | 82 | # Try to start the server 83 | try: 84 | server = six.moves.BaseHTTPServer.HTTPServer( 85 | (address, listen_port), UserProxyHandler 86 | ) 87 | except socket.error as err: 88 | raise CommandError(err) 89 | else: 90 | server.args = ServerArgs(backend_port, username, auth_header) 91 | 92 | # Run until we hit ctrl-C 93 | try: 94 | print( 95 | "Starting proxy on %s %s => %s, auth '%s: %s'" % 96 | (address, backend_port, listen_port, auth_header, username) 97 | ) 98 | server.serve_forever() 99 | except KeyboardInterrupt: 100 | print('Bye!') 101 | -------------------------------------------------------------------------------- /nsot/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /nsot/middleware/auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Middleware for authentication. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import logging 7 | 8 | from django.contrib.auth import backends, middleware 9 | from django.conf import settings 10 | from django.core.exceptions import ValidationError 11 | from django.core.validators import EmailValidator 12 | from guardian.backends import ObjectPermissionBackend 13 | from guardian.core import ObjectPermissionChecker 14 | 15 | from ..util import normalize_auth_header 16 | 17 | 18 | log = logging.getLogger('nsot_server') 19 | 20 | 21 | class EmailHeaderMiddleware(middleware.RemoteUserMiddleware): 22 | header = normalize_auth_header(settings.USER_AUTH_HEADER) 23 | 24 | 25 | class EmailHeaderBackend(backends.RemoteUserBackend): 26 | """Custom backend that validates username is an email.""" 27 | def authenticate(self, request, remote_user): 28 | """Override default to return None if username is invalid.""" 29 | if not remote_user: 30 | return 31 | 32 | username = self.clean_username(remote_user) 33 | if not username: 34 | return 35 | 36 | return super(EmailHeaderBackend, self).authenticate( 37 | request, remote_user 38 | ) 39 | 40 | def clean_username(self, username): 41 | """Makes sure that the username is a valid email address.""" 42 | validator = EmailValidator() 43 | try: 44 | validator(username) # If invalid, will raise a ValidationError 45 | except ValidationError: 46 | log.debug('Invalid email address: %r', username) 47 | return None 48 | else: 49 | return username 50 | 51 | def configure_user(self, user): 52 | """Check whether to make new users superusers.""" 53 | if settings.NSOT_NEW_USERS_AS_SUPERUSER: 54 | user.is_superuser = True 55 | user.is_staff = True 56 | user.save() 57 | 58 | log.debug('Created new user: %s', user) 59 | return user 60 | 61 | 62 | class NsotObjectPermissionsBackend(ObjectPermissionBackend): 63 | """Custom backend that overloads django-guardian's has_perm method.""" 64 | def has_perm(self, user_obj, perm, obj=None): 65 | """ 66 | Returns ``True`` if ``user_obj`` has ``perm`` for ``obj``. If no 67 | ``obj`` is provided, ``False`` is returned. 68 | 69 | However, if ``grp_obj`` does not have ``perm`` for ``obj``, 70 | the ancestor tree for ``obj`` is checked against. If any node in 71 | the ancestor tree has ``perm`` for the ``obj``, then ``True`` is 72 | returned, else ``False`` is returned. 73 | """ 74 | check = super(NsotObjectPermissionsBackend, self).has_perm( 75 | user_obj, perm, obj 76 | ) 77 | if check: 78 | return True 79 | 80 | if hasattr(obj, 'get_ancestors'): 81 | ancestors = obj.get_ancestors() 82 | ancestor_perm_check = ObjectPermissionChecker(user_obj) 83 | 84 | return any(ancestor_perm_check.has_perm(perm, a) 85 | for a in ancestors) 86 | 87 | return False 88 | -------------------------------------------------------------------------------- /nsot/middleware/request_logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | Middleware to log HTTP requests. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import logging 7 | from time import time 8 | 9 | 10 | class LoggingMiddleware(object): 11 | def __init__(self): 12 | self.logger = logging.getLogger('nsot_server') 13 | 14 | def process_request(self, request): 15 | request.timer = time() 16 | return None 17 | 18 | def process_response(self, request, response): 19 | if 'HTTP_X_FORWARDED_FOR' in request.META: 20 | request_ip_path = '%s, %s' % ( 21 | request.META.get('REMOTE_ADDR'), 22 | request.META.get('HTTP_X_FORWARDED_FOR') 23 | ) 24 | else: 25 | request_ip_path = request.META.get('REMOTE_ADDR') 26 | self.logger.info( 27 | '%s %s %s (%s) %.2fms', 28 | response.status_code, 29 | request.method, 30 | request.get_full_path(), 31 | request_ip_path, 32 | (time() - request.timer) * 1000 # ms 33 | ) 34 | return response 35 | -------------------------------------------------------------------------------- /nsot/migrations/0002_auto_20150810_1718.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import macaddress.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Assignment', 18 | fields=[ 19 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 20 | ('created', models.DateTimeField(auto_now_add=True)), 21 | ('address', models.ForeignKey(to='nsot.Network')), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='Interface', 26 | fields=[ 27 | ('resource_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='nsot.Resource')), 28 | ('name', models.CharField(max_length=255, db_index=True)), 29 | ('description', models.CharField(default='', max_length=255)), 30 | ('type', models.IntegerField(db_index=True, verbose_name='Interface Type', choices=[('other', 1), ('ethernet', 6), ('loopback', 24), ('tunnel', 131), ('l2vlan', 135), ('l3vlan', 136), ('mpls', 150), ('lag', 161)])), 31 | ('mac', macaddress.fields.MACAddressField(integer=True, db_index=True, blank=True)), 32 | ('speed', models.IntegerField(db_index=True)), 33 | ('addresses', models.ManyToManyField(related_name='addresses', through='nsot.Assignment', to='nsot.Network', db_index=True)), 34 | ('device', models.ForeignKey(related_name='interfaces', to='nsot.Device')), 35 | ], 36 | bases=('nsot.resource',), 37 | ), 38 | migrations.AddField( 39 | model_name='assignment', 40 | name='interface', 41 | field=models.ForeignKey(to='nsot.Interface'), 42 | ), 43 | migrations.AlterUniqueTogether( 44 | name='interface', 45 | unique_together=set([('device', 'name')]), 46 | ), 47 | migrations.AlterIndexTogether( 48 | name='interface', 49 | index_together=set([('device', 'name')]), 50 | ), 51 | migrations.AlterUniqueTogether( 52 | name='assignment', 53 | unique_together=set([('address', 'interface')]), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /nsot/migrations/0003_auto_20150810_1751.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import macaddress.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0002_auto_20150810_1718'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='interface', 18 | name='mac', 19 | field=macaddress.fields.MACAddressField(db_index=True, integer=True, null=True, blank=True), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /nsot/migrations/0004_auto_20150810_1806.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import macaddress.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0003_auto_20150810_1751'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='interface', 18 | name='mac', 19 | field=macaddress.fields.MACAddressField(default=0, integer=True, null=True, db_index=True, blank=True), 20 | ), 21 | migrations.AlterField( 22 | model_name='interface', 23 | name='speed', 24 | field=models.IntegerField(default=1000, db_index=True), 25 | ), 26 | migrations.AlterField( 27 | model_name='interface', 28 | name='type', 29 | field=models.IntegerField(default=6, db_index=True, verbose_name='Interface Type', choices=[('other', 1), ('ethernet', 6), ('loopback', 24), ('tunnel', 131), ('l2vlan', 135), ('l3vlan', 136), ('mpls', 150), ('lag', 161)]), 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /nsot/migrations/0005_auto_20150810_1847.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0004_auto_20150810_1806'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='interface', 18 | name='site', 19 | field=models.ForeignKey(related_name='interfaces', on_delete=django.db.models.deletion.PROTECT, default=1, to='nsot.Site'), 20 | preserve_default=False, 21 | ), 22 | migrations.AlterField( 23 | model_name='attribute', 24 | name='resource_name', 25 | field=models.CharField(db_index=True, max_length=20, verbose_name='Resource Name', choices=[('Device', 'Device'), ('Interface', 'Interface'), ('Network', 'Network')]), 26 | ), 27 | migrations.AlterField( 28 | model_name='change', 29 | name='resource_name', 30 | field=models.CharField(db_index=True, max_length=20, verbose_name='Resource Type', choices=[('Network', 'Network'), ('Permission', 'Permission'), ('Attribute', 'Attribute'), ('Site', 'Site'), ('Interface', 'Interface'), ('Device', 'Device')]), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /nsot/migrations/0006_auto_20150810_1947.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0005_auto_20150810_1847'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='assignment', 17 | name='interface', 18 | field=models.ForeignKey(related_name='assignments', to='nsot.Interface'), 19 | ), 20 | migrations.AlterIndexTogether( 21 | name='assignment', 22 | index_together=set([('address', 'interface')]), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /nsot/migrations/0007_auto_20150811_1201.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0006_auto_20150810_1947'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='interface', 17 | old_name='mac', 18 | new_name='mac_address', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /nsot/migrations/0008_auto_20150811_1222.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0007_auto_20150811_1201'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='assignment', 17 | name='address', 18 | field=models.ForeignKey(related_name='assignments', to='nsot.Network'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /nsot/migrations/0009_auto_20150811_1245.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0008_auto_20150811_1222'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='interface', 17 | name='type', 18 | field=models.IntegerField(default=6, db_index=True, verbose_name='Interface Type', choices=[(1, 'other'), (6, 'ethernet'), (24, 'loopback'), (131, 'tunnel'), (135, 'l2vlan'), (136, 'l3vlan'), (150, 'mpls'), (161, 'lag')]), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /nsot/migrations/0010_auto_20150921_2120.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import nsot.fields 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0009_auto_20150811_1245'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='attribute', 18 | name='description', 19 | field=models.CharField(default='', max_length=255, blank=True), 20 | ), 21 | migrations.AlterField( 22 | model_name='interface', 23 | name='description', 24 | field=models.CharField(default='', max_length=255, blank=True), 25 | ), 26 | migrations.AlterField( 27 | model_name='interface', 28 | name='mac_address', 29 | field=nsot.fields.MACAddressField(default=0, blank=True, help_text='If not provided, defaults to 00:00:00:00:00:00.', integer=True, null=True, verbose_name='MAC Address', db_index=True), 30 | ), 31 | migrations.AlterField( 32 | model_name='interface', 33 | name='name', 34 | field=models.CharField(help_text='The name of the interface as it appears on the device.', max_length=255, db_index=True), 35 | ), 36 | migrations.AlterField( 37 | model_name='interface', 38 | name='speed', 39 | field=models.IntegerField(default=10000, help_text='Integer of Mbps of interface (e.g. 20000 for 20 Gbps). If not provided, defaults to 10000.', db_index=True, blank=True), 40 | ), 41 | migrations.AlterField( 42 | model_name='interface', 43 | name='type', 44 | field=models.IntegerField(default=6, help_text="If not provided, defaults to 'ethernet'.", db_index=True, verbose_name='Interface Type', choices=[(6, b'ethernet'), (1, b'other'), (135, b'l2vlan'), (136, b'l3vlan'), (161, b'lag'), (24, b'loopback'), (150, b'mpls'), (131, b'tunnel')]), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /nsot/migrations/0011_auto_20150930_1557.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0010_auto_20150921_2120'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RemoveField( 17 | model_name='resource', 18 | name='parent', 19 | ), 20 | migrations.AddField( 21 | model_name='interface', 22 | name='parent', 23 | field=models.ForeignKey(related_name='children', on_delete=django.db.models.deletion.PROTECT, default=None, blank=True, to='nsot.Interface', null=True), 24 | ), 25 | migrations.AddField( 26 | model_name='network', 27 | name='parent', 28 | field=models.ForeignKey(related_name='children', on_delete=django.db.models.deletion.PROTECT, default=None, blank=True, to='nsot.Network', null=True), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /nsot/migrations/0012_auto_20151002_1427.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0011_auto_20150930_1557'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='interface', 18 | name='description', 19 | field=models.CharField(default='', help_text='A brief yet helpful description.', max_length=255, blank=True), 20 | ), 21 | migrations.AlterField( 22 | model_name='interface', 23 | name='device', 24 | field=models.ForeignKey(related_name='interfaces', verbose_name='Device', to='nsot.Device', help_text='Unique ID of the connected Device.'), 25 | ), 26 | migrations.AlterField( 27 | model_name='interface', 28 | name='parent', 29 | field=models.ForeignKey(related_name='children', on_delete=django.db.models.deletion.PROTECT, default=None, to='nsot.Interface', blank=True, help_text='Unique ID of the parent Interface.', null=True, verbose_name='Parent'), 30 | ), 31 | migrations.AlterField( 32 | model_name='interface', 33 | name='type', 34 | field=models.IntegerField(default=6, help_text="If not provided, defaults to 'ethernet'.", db_index=True, verbose_name='Interface Type', choices=[(6, b'ethernet'), (1, b'other'), (135, b'l2vlan'), (136, b'l3vlan'), (161, b'lag'), (24, b'loopback'), (150, b'mpls'), (53, b'prop_virtual'), (131, b'tunnel')]), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /nsot/migrations/0013_auto_20151002_1443.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0012_auto_20151002_1427'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='interface', 17 | name='name', 18 | field=models.CharField(help_text='The name of the interface as it appears on the Device.', max_length=255, db_index=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /nsot/migrations/0014_auto_20151002_1653.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0013_auto_20151002_1443'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='interface', 18 | name='parent', 19 | field=models.ForeignKey(related_name='children', on_delete=django.db.models.deletion.PROTECT, default=None, to='nsot.Interface', blank=True, help_text='Unique ID of the parent Interface.', null=True, verbose_name='Parent'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /nsot/migrations/0016_move_device_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import django_extensions.db.fields.json 7 | 8 | 9 | def migrate_device_fields(apps, schema_editor): 10 | """ 11 | Migrate new Device fields. 12 | """ 13 | Device = apps.get_model('nsot', 'Device') 14 | Device_temp = apps.get_model('nsot', 'Device_temp') 15 | for dev in Device.objects.iterator(): 16 | dev_tmp = Device_temp.objects.create( 17 | id=dev.resource_ptr_id, 18 | hostname=dev.hostname, 19 | _attributes_cache = dev._attributes, 20 | site=dev.site, 21 | ) 22 | 23 | 24 | class Migration(migrations.Migration): 25 | 26 | dependencies = [ 27 | ('nsot', '0015_move_attribute_fields'), 28 | ] 29 | 30 | operations = [ 31 | 32 | # Device _attributes_cache, new_id 33 | migrations.RunPython(migrate_device_fields), 34 | 35 | ] 36 | -------------------------------------------------------------------------------- /nsot/migrations/0017_move_network_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | import django_extensions.db.fields.json 7 | 8 | 9 | def migrate_network_fields(apps, schema_editor): 10 | """ 11 | Migrate new Network fields. 12 | """ 13 | Network = apps.get_model('nsot', 'Network') 14 | Network_temp = apps.get_model('nsot', 'Network_temp') 15 | for net in Network.objects.iterator(): 16 | net_tmp = Network_temp.objects.create( 17 | network_address=net.network_address, 18 | broadcast_address=net.broadcast_address, 19 | prefix_length=net.prefix_length, 20 | ip_version=net.ip_version, 21 | is_ip=net.is_ip, 22 | id=net.resource_ptr_id, 23 | parent_id=net.parent_id, 24 | _attributes_cache=net._attributes, 25 | site=net.site, 26 | ) 27 | 28 | 29 | class Migration(migrations.Migration): 30 | 31 | dependencies = [ 32 | ('nsot', '0016_move_device_data'), 33 | ] 34 | 35 | operations = [ 36 | 37 | # Network _attributes_cache, new_id 38 | migrations.RunPython(migrate_network_fields), 39 | 40 | ] 41 | -------------------------------------------------------------------------------- /nsot/migrations/0018_move_interface_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | 7 | 8 | def migrate_interface_fields(apps, schema_editor): 9 | """ 10 | Migrate new Interface fields. 11 | """ 12 | Interface = apps.get_model('nsot', 'Interface') 13 | Interface_temp = apps.get_model('nsot', 'Interface_temp') 14 | for ifc in Interface.objects.iterator(): 15 | ifc_tmp = Interface_temp.objects.create( 16 | id=ifc.resource_ptr_id, 17 | name=ifc.name, 18 | description=ifc.description, 19 | device_id=ifc.device_id, 20 | parent_id=ifc.parent_id, 21 | type=ifc.type, 22 | speed=ifc.speed, 23 | mac_address=ifc.mac_address, 24 | site=ifc.site, 25 | _attributes_cache = ifc._attributes, 26 | ) 27 | 28 | 29 | class Migration(migrations.Migration): 30 | 31 | dependencies = [ 32 | ('nsot', '0017_move_network_data'), 33 | ] 34 | 35 | operations = [ 36 | 37 | # Interface _attributes_cache, new_id 38 | migrations.RunPython(migrate_interface_fields), 39 | 40 | ] 41 | -------------------------------------------------------------------------------- /nsot/migrations/0019_move_assignment_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | 7 | 8 | def migrate_assignment_fields(apps, schema_editor): 9 | """Migrate new Assignment fields.""" 10 | Assignment = apps.get_model('nsot', 'Assignment') 11 | Assignment_temp = apps.get_model('nsot', 'Assignment_temp') 12 | for asn in Assignment.objects.iterator(): 13 | asn_tmp = Assignment_temp.objects.create( 14 | id = asn.id, 15 | address_id=asn.address_id, 16 | interface_id=asn.interface_id, 17 | ) 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ('nsot', '0018_move_interface_data'), 24 | ] 25 | 26 | operations = [ 27 | 28 | # Assignment id, address_id, interface_id 29 | migrations.RunPython(migrate_assignment_fields), 30 | 31 | ] 32 | -------------------------------------------------------------------------------- /nsot/migrations/0020_move_value_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | 7 | 8 | def migrate_value_fields(apps, schema_editor): 9 | """ 10 | Migrate new Value fields. 11 | """ 12 | Value = apps.get_model('nsot', 'Value') 13 | for val in Value.objects.iterator(): 14 | val.resource_name = val.resource.polymorphic_ctype.model.title() 15 | val.new_resource_id = val.resource_id 16 | val.name = val.attribute.name 17 | val.save() 18 | 19 | 20 | class Migration(migrations.Migration): 21 | 22 | dependencies = [ 23 | ('nsot', '0019_move_assignment_data'), 24 | ] 25 | 26 | operations = [ 27 | 28 | # Value name, resource_id, resource_name 29 | migrations.RunPython(migrate_value_fields), 30 | 31 | ] 32 | -------------------------------------------------------------------------------- /nsot/migrations/0022_auto_20151007_1847.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | import django_extensions.db.fields.json 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0021_remove_resource_object'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='interface', 18 | name='_addresses_cache', 19 | field=django_extensions.db.fields.json.JSONField(default=b'[]', blank=True), 20 | ), 21 | migrations.AddField( 22 | model_name='interface', 23 | name='_networks_cache', 24 | field=django_extensions.db.fields.json.JSONField(default=b'[]', blank=True), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /nsot/migrations/0023_auto_20151008_1351.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0022_auto_20151007_1847'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterIndexTogether( 16 | name='value', 17 | index_together=set([('resource_name', 'resource_id'), ('name', 'value', 'resource_name')]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /nsot/migrations/0024_network_state.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0023_auto_20151008_1351'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='network', 17 | name='state', 18 | field=models.CharField(default='allocated', max_length=20, db_index=True, choices=[('allocated', 'Allocated'), ('assigned', 'Assigned'), ('orphaned', 'Orphaned'), ('reserved', 'Reserved')]), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /nsot/migrations/0025_value_site.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0024_network_state'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='value', 18 | name='site', 19 | field=models.ForeignKey(related_name='values', on_delete=django.db.models.deletion.PROTECT, default=1, to='nsot.Site'), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /nsot/migrations/0027_interface_device_hostname.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0026_model_field_verbose_names'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='interface', 17 | name='device_hostname', 18 | field=models.CharField(help_text='The hostname of the Device to which the interface is bound. (Internal use only)', max_length=255, db_index=True, blank=True, editable=False), 19 | ), 20 | migrations.AlterIndexTogether( 21 | name='interface', 22 | index_together=set([('device_hostname', 'name'), ('device', 'name')]), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /nsot/migrations/0028_populate_interface_device_hostname.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import models, migrations 6 | 7 | 8 | def update_interface_device_hostname(apps, schema_editor): 9 | """Update all interfaces with hostname from associated device""" 10 | Device = apps.get_model('nsot', 'Device') 11 | for dev in Device.objects.iterator(): 12 | dev.interfaces.update(device_hostname=dev.hostname) 13 | 14 | 15 | class Migration(migrations.Migration): 16 | 17 | dependencies = [ 18 | ('nsot', '0027_interface_device_hostname'), 19 | ] 20 | 21 | operations = [ 22 | 23 | # Save all devices 24 | migrations.RunPython(update_interface_device_hostname), 25 | 26 | ] 27 | -------------------------------------------------------------------------------- /nsot/migrations/0029_auto__add_circuit.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import django_extensions.db.fields.json 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('nsot', '0028_populate_interface_device_hostname'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Circuit', 19 | fields=[ 20 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 21 | ('_attributes_cache', django_extensions.db.fields.json.JSONField(help_text='Local cache of attributes. (Internal use only)', blank=True)), 22 | ('name', models.CharField(default='', help_text="Unique display name of the Circuit. If not provided, defaults to '{device_a}:{interface_a}_{device_z}:{interface_z}'", unique=True, max_length=255)), 23 | ('endpoint_a', models.OneToOneField(related_name='circuit_a', on_delete=django.db.models.deletion.PROTECT, verbose_name='A-side endpoint Interface', to='nsot.Interface', help_text='Unique ID of Interface at the A-side.')), 24 | ('site', models.ForeignKey(related_name='circuits', on_delete=django.db.models.deletion.PROTECT, to='nsot.Site', help_text='Unique ID of the Site this Circuit is under.')), 25 | ('endpoint_z', models.OneToOneField(related_name='circuit_z', null=True, on_delete=django.db.models.deletion.PROTECT, to='nsot.Interface', help_text='Unique ID of Interface at the Z-side.', verbose_name='Z-side endpoint Interface')), 26 | ], 27 | options={ 28 | 'abstract': False, 29 | }, 30 | ), 31 | migrations.AlterField( 32 | model_name='attribute', 33 | name='resource_name', 34 | field=models.CharField(help_text='The name of the Resource to which this Attribute is bound.', max_length=20, verbose_name='Resource Name', db_index=True, choices=[('Device', 'Device'), ('Interface', 'Interface'), ('Network', 'Network'), ('Circuit', 'Circuit')]), 35 | ), 36 | migrations.AlterField( 37 | model_name='change', 38 | name='resource_name', 39 | field=models.CharField(help_text='The name of the Resource for this Change.', max_length=20, verbose_name='Resource Type', db_index=True, choices=[('Network', 'Network'), ('Attribute', 'Attribute'), ('Site', 'Site'), ('Interface', 'Interface'), ('Circuit', 'Circuit'), ('Device', 'Device')]), 40 | ), 41 | migrations.AlterField( 42 | model_name='value', 43 | name='resource_name', 44 | field=models.CharField(help_text='The name of the Resource type to which the Value is bound.', max_length=20, verbose_name='Resource Type', db_index=True, choices=[('Network', 'Network'), ('Attribute', 'Attribute'), ('Site', 'Site'), ('Interface', 'Interface'), ('Circuit', 'Circuit'), ('Device', 'Device')]), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /nsot/migrations/0030_add_circuit_name_slug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0029_auto__add_circuit'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='circuit', 17 | name='name_slug', 18 | field=models.CharField(db_index=True, editable=False, max_length=255, help_text='Slugified version of the name field, used for the natural key', null=True, unique=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /nsot/migrations/0031_populate_circuit_name_slug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | 7 | from nsot.util import slugify 8 | 9 | 10 | def add_name_slug(apps, schema_editor): 11 | """ Add a name_slug for every Circuit that doesn't already have one """ 12 | 13 | Circuit = apps.get_model('nsot', 'Circuit') 14 | for c in Circuit.objects.all(): 15 | if not c.name_slug: 16 | c.name_slug = slugify(c.name) 17 | c.save() 18 | 19 | 20 | def remove_name_slug(apps, schema_editor): 21 | Circuit = apps.get_model('nsot', 'Circuit') 22 | for c in Circuit.objects.all(): 23 | c.name_slug = None 24 | c.save() 25 | 26 | 27 | class Migration(migrations.Migration): 28 | 29 | dependencies = [ 30 | ('nsot', '0030_add_circuit_name_slug'), 31 | ] 32 | 33 | operations = [ 34 | migrations.RunPython(add_name_slug, remove_name_slug) 35 | ] 36 | -------------------------------------------------------------------------------- /nsot/migrations/0032_add_indicies_to_change.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0031_populate_circuit_name_slug'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='change', 17 | name='change_at', 18 | field=models.DateTimeField(help_text='The timestamp of this Change.', auto_now_add=True, db_index=True), 19 | ), 20 | migrations.AlterIndexTogether( 21 | name='change', 22 | index_together=set([('resource_name', 'resource_id'), ('resource_name', 'event')]), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /nsot/migrations/0033_add_interface_name_slug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('nsot', '0032_add_indicies_to_change'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='interface', 17 | name='name_slug', 18 | field=models.CharField(null=True, editable=False, max_length=255, help_text='Slugified version of the name field, used for the natural key', unique=True, db_index=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /nsot/migrations/0034_populate_interface_name_slug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | 7 | 8 | def remove_name_slug(apps, schema_editor): 9 | Interface = apps.get_model('nsot', 'Interface') 10 | Interface.objects.update(name_slug=None) 11 | 12 | 13 | class Migration(migrations.Migration): 14 | 15 | dependencies = [ 16 | ('nsot', '0033_add_interface_name_slug'), 17 | ] 18 | 19 | operations = [ 20 | # The "forwards" action was changed to a noop so that migration 0035 21 | # fully replaces this one without having to do migration gymnastics. 22 | # Users who previously upgraded to v1.2.0 and had migration 0034 23 | # already applied will have to apply 0035, but new users will have 0034 24 | # migration be a noop. 25 | # In summary: 26 | # - v1.2.0 = 0033 -> 0034 -> 0035 27 | # - v1.2.1 = 0033 -> 0035 28 | migrations.RunPython(migrations.RunPython.noop, remove_name_slug) 29 | ] 30 | -------------------------------------------------------------------------------- /nsot/migrations/0035_fix_interface_name_slug.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | 7 | from nsot.util import slugify_interface 8 | 9 | 10 | def add_name_slug(apps, schema_editor): 11 | """Correctly name_slug for every Interface with slash in the name.""" 12 | Interface = apps.get_model('nsot', 'Interface') 13 | for i in Interface.objects.iterator(): 14 | name_slug = slugify_interface( 15 | device_hostname=i.device_hostname, name=i.name 16 | ) 17 | i.name_slug = name_slug 18 | i.save() 19 | 20 | 21 | def remove_name_slug(apps, schema_editor): 22 | Interface = apps.get_model('nsot', 'Interface') 23 | Interface.objects.update(name_slug=None) 24 | 25 | 26 | class Migration(migrations.Migration): 27 | 28 | dependencies = [ 29 | ('nsot', '0034_populate_interface_name_slug'), 30 | ] 31 | 32 | operations = [ 33 | migrations.RunPython(add_name_slug, remove_name_slug) 34 | ] 35 | -------------------------------------------------------------------------------- /nsot/migrations/0037_protocoltype_site__unique_together.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('nsot', '0036_add_protocol'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='protocoltype', 18 | name='site', 19 | field=models.ForeignKey(related_name='protocol_types', on_delete=django.db.models.deletion.PROTECT, default=1, verbose_name='Site', to='nsot.Site', help_text='Unique ID of the Site this ProtocolType is under.'), 20 | preserve_default=False, 21 | ), 22 | migrations.AlterField( 23 | model_name='change', 24 | name='resource_name', 25 | field=models.CharField(help_text='The name of the Resource for this Change.', max_length=20, verbose_name='Resource Type', db_index=True, choices=[('Protocol', 'Protocol'), ('Network', 'Network'), ('ProtocolType', 'ProtocolType'), ('Attribute', 'Attribute'), ('Site', 'Site'), ('Interface', 'Interface'), ('Circuit', 'Circuit'), ('Device', 'Device')]), 26 | ), 27 | migrations.AlterField( 28 | model_name='protocol', 29 | name='auth_string', 30 | field=models.CharField(default='', help_text='Authentication string (such as MD5 sum)', max_length=255, verbose_name='Auth String', blank=True), 31 | ), 32 | migrations.AlterField( 33 | model_name='protocoltype', 34 | name='name', 35 | field=models.CharField(help_text='Name of this type of protocol (e.g. OSPF, BGP, etc.)', max_length=16, db_index=True), 36 | ), 37 | migrations.AlterField( 38 | model_name='value', 39 | name='resource_name', 40 | field=models.CharField(help_text='The name of the Resource type to which the Value is bound.', max_length=20, verbose_name='Resource Type', db_index=True, choices=[('Device', 'Device'), ('Interface', 'Interface'), ('Protocol', 'Protocol'), ('Network', 'Network'), ('Circuit', 'Circuit')]), 41 | ), 42 | migrations.AlterUniqueTogether( 43 | name='protocoltype', 44 | unique_together=set([('site', 'name')]), 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /nsot/migrations/0038_make_interface_speed_nullable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.16 on 2018-11-07 11:56 3 | from __future__ import unicode_literals 4 | 5 | from __future__ import absolute_import 6 | from django.db import migrations, models 7 | import django_extensions.db.fields.json 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('nsot', '0037_protocoltype_site__unique_together'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AlterField( 18 | model_name='interface', 19 | name='speed', 20 | field=models.IntegerField(blank=True, db_index=True, default=1000, help_text='Integer of Mbps of interface (e.g. 20000 for 20 Gbps). If not provided, defaults to 1000.', null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /nsot/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/nsot/migrations/__init__.py -------------------------------------------------------------------------------- /nsot/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from django.db import models as djmodels 4 | 5 | from .assignment import Assignment 6 | from .attribute import Attribute 7 | from .change import Change 8 | from .circuit import Circuit 9 | from .device import Device 10 | from .interface import Interface 11 | from .network import Network 12 | from .protocol import Protocol 13 | from .protocol_type import ProtocolType 14 | from .resource import Resource 15 | from .site import Site 16 | from .user import User 17 | from .value import Value 18 | 19 | 20 | __all__ = [ 21 | 'Assignment', 22 | 'Attribute', 23 | 'Change', 24 | 'Circuit', 25 | 'Device', 26 | 'Interface', 27 | 'Network', 28 | 'Protocol', 29 | 'ProtocolType', 30 | 'Site', 31 | 'User', 32 | 'Value', 33 | ] 34 | 35 | 36 | # Global signals 37 | def delete_resource_values(sender, instance, **kwargs): 38 | """Delete values when a Resource object is deleted.""" 39 | instance.attributes.delete() # These are instances of Value 40 | 41 | 42 | resource_subclasses = Resource.__subclasses__() 43 | for model_class in resource_subclasses: 44 | # Value post_delete 45 | djmodels.signals.post_delete.connect( 46 | delete_resource_values, 47 | sender=model_class, 48 | dispatch_uid='value_post_delete_' + model_class.__name__ 49 | ) 50 | -------------------------------------------------------------------------------- /nsot/models/assignment.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from __future__ import absolute_import 4 | from django.db import models 5 | 6 | from .. import exc, validators 7 | 8 | 9 | class Assignment(models.Model): 10 | """ 11 | DB object for assignment of addresses to interfaces (on devices). 12 | 13 | This is used to enforce constraints at the relationship level for addition 14 | of new address assignments. 15 | """ 16 | address = models.ForeignKey( 17 | 'Network', related_name='assignments', db_index=True, 18 | help_text='Network to which this assignment is bound.' 19 | ) 20 | interface = models.ForeignKey( 21 | 'Interface', related_name='assignments', db_index=True, 22 | help_text='Interface to which this assignment is bound.' 23 | ) 24 | created = models.DateTimeField(auto_now_add=True) 25 | 26 | def __unicode__(self): 27 | return u'interface=%s, address=%s' % (self.interface, self.address) 28 | 29 | class Meta: 30 | unique_together = ('address', 'interface') 31 | index_together = unique_together 32 | 33 | def clean_address(self, value): 34 | """Enforce that new addresses can only be host addresses.""" 35 | addr = validators.validate_host_address(value) 36 | 37 | # Enforce uniqueness upon assignment. 38 | existing = Assignment.objects.filter(address=addr) 39 | if existing.filter(interface__device=self.interface.device).exists(): 40 | raise exc.ValidationError({ 41 | 'address': 'Address already assigned to this Device.' 42 | }) 43 | 44 | return value 45 | 46 | def clean_fields(self, exclude=None): 47 | self.clean_address(self.address) 48 | self.address.set_assigned() 49 | 50 | def save(self, *args, **kwargs): 51 | self.full_clean() 52 | super(Assignment, self).save(*args, **kwargs) 53 | 54 | def to_dict(self): 55 | return { 56 | 'id': self.id, 57 | 'device': self.interface.device.id, 58 | 'hostname': self.interface.device_hostname, 59 | 'interface': self.interface.id, 60 | 'interface_name': self.interface.name, 61 | 'address': self.address.cidr, 62 | } 63 | -------------------------------------------------------------------------------- /nsot/models/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.conf import settings 3 | 4 | # These are constants that becuase they are tied directly to the underlying 5 | # objects are explicitly NOT USER CONFIGURABLE. 6 | RESOURCE_BY_IDX = ( 7 | 'Site', 'Network', 'Attribute', 'Device', 'Interface', 'Circuit', 8 | 'Protocol', 'ProtocolType' 9 | ) 10 | RESOURCE_BY_NAME = { 11 | obj_type: idx 12 | for idx, obj_type in enumerate(RESOURCE_BY_IDX) 13 | } 14 | 15 | CHANGE_EVENTS = ('Create', 'Update', 'Delete') 16 | 17 | VALID_CHANGE_RESOURCES = set(RESOURCE_BY_IDX) 18 | VALID_ATTRIBUTE_RESOURCES = set([ 19 | 'Network', 'Device', 'Interface', 'Circuit', 'Protocol' 20 | ]) 21 | 22 | # Lists of 2-tuples of (value, option) for displaying choices in certain model 23 | # serializer/form fields. 24 | CHANGE_RESOURCE_CHOICES = [(c, c) for c in VALID_CHANGE_RESOURCES] 25 | EVENT_CHOICES = [(c, c) for c in CHANGE_EVENTS] 26 | IP_VERSION_CHOICES = [(c, c) for c in settings.IP_VERSIONS] 27 | RESOURCE_CHOICES = [(c, c) for c in VALID_ATTRIBUTE_RESOURCES] 28 | 29 | # Unique interface type IDs. 30 | INTERFACE_TYPES = [t[0] for t in settings.INTERFACE_TYPE_CHOICES] 31 | -------------------------------------------------------------------------------- /nsot/models/device.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from __future__ import absolute_import 4 | from django.conf import settings 5 | from django.db import models 6 | 7 | from .. import exc 8 | from .circuit import Circuit 9 | from .resource import Resource 10 | 11 | 12 | class Device(Resource): 13 | """Represents a network device.""" 14 | hostname = models.CharField( 15 | max_length=255, null=False, db_index=True, 16 | help_text='The hostname of the Device.' 17 | ) 18 | site = models.ForeignKey( 19 | 'Site', db_index=True, related_name='devices', 20 | on_delete=models.PROTECT, verbose_name='Site', 21 | help_text='Unique ID of the Site this Device is under.' 22 | ) 23 | 24 | def __unicode__(self): 25 | return u'%s' % self.hostname 26 | 27 | class Meta: 28 | unique_together = ('site', 'hostname') 29 | index_together = unique_together 30 | 31 | @property 32 | def circuits(self): 33 | """All circuits related to this Device.""" 34 | interfaces = self.interfaces.all() 35 | circuits = [] 36 | for intf in interfaces: 37 | try: 38 | circuits.append(intf.circuit) 39 | except Circuit.DoesNotExist: 40 | continue 41 | return circuits 42 | 43 | def clean_hostname(self, value): 44 | if not value: 45 | raise exc.ValidationError({ 46 | 'hostname': 'Hostname must be non-zero length string.' 47 | }) 48 | if not settings.DEVICE_NAME.match(value): 49 | raise exc.ValidationError({ 50 | 'name': 'Invalid name: %r.' % value 51 | }) 52 | return value 53 | 54 | def clean_fields(self, exclude=None): 55 | self.hostname = self.clean_hostname(self.hostname) 56 | 57 | def save(self, *args, **kwargs): 58 | self.full_clean() 59 | super(Device, self).save(*args, **kwargs) 60 | 61 | def to_dict(self): 62 | return { 63 | 'id': self.id, 64 | 'site_id': self.site_id, 65 | 'hostname': self.hostname, 66 | 'attributes': self.get_attributes(), 67 | } 68 | -------------------------------------------------------------------------------- /nsot/models/protocol_type.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.db import models 3 | 4 | from .. import exc 5 | 6 | 7 | class ProtocolType(models.Model): 8 | """ 9 | Representation of protocol types (e.g. bgp, is-is, ospf, etc.) 10 | """ 11 | name = models.CharField( 12 | max_length=16, db_index=True, 13 | help_text='Name of this type of protocol (e.g. OSPF, BGP, etc.)', 14 | ) 15 | description = models.CharField( 16 | max_length=255, default='', blank=True, null=False, 17 | help_text='A description for this ProtocolType', 18 | ) 19 | required_attributes = models.ManyToManyField( 20 | 'Attribute', db_index=True, related_name='protocol_types', 21 | help_text=( 22 | 'All Attributes which are required by this ProtocolType. If a' 23 | ' Protocol of this type is saved and is missing one of these' 24 | ' attributes, a ValidationError will be raised.' 25 | ) 26 | ) 27 | site = models.ForeignKey( 28 | 'Site', db_index=True, related_name='protocol_types', 29 | on_delete=models.PROTECT, verbose_name='Site', 30 | help_text='Unique ID of the Site this ProtocolType is under.' 31 | ) 32 | 33 | def __unicode__(self): 34 | return u'%s' % self.name 35 | 36 | class Meta: 37 | unique_together = ('site', 'name') 38 | 39 | def get_required_attributes(self): 40 | """Return a list of the names of ``self.required_attributes``.""" 41 | # FIXME(jathan): These should probably cached on the model and updated 42 | # on write. Revisit after we see how performance plays out in practice. 43 | return list(self.required_attributes.values_list('name', flat=True)) 44 | 45 | def to_dict(self): 46 | return { 47 | 'id': self.id, 48 | 'name': self.name, 49 | 'description': self.description, 50 | 'required_attributes': self.get_required_attributes(), 51 | 'site': self.site_id, 52 | } 53 | 54 | 55 | # Signals 56 | def required_attributes_changed(sender, instance, action, reverse, model, 57 | pk_set, **kwargs): 58 | """ 59 | Signal handler that disallows anything but Protocol attributes to be added 60 | to a ProtocolType.required_attributes. 61 | """ 62 | if action == 'pre_add': 63 | # First filter in Protocol attributes. 64 | attrs = model.objects.filter(pk__in=pk_set) 65 | if attrs.exclude(resource_name='Protocol').exists(): 66 | raise exc.ValidationError({ 67 | 'required_attributes': 'Only Protocol attributes are allowed' 68 | }) 69 | 70 | # Then make sure that they match the site of the incoming instance. 71 | wrong_site = attrs.exclude(site_id=instance.site_id) 72 | if wrong_site.exists(): 73 | bad_attrs = [str(w) for w in wrong_site] 74 | raise exc.ValidationError({ 75 | 'required_attributes': ( 76 | 'Attributes must share the same site as ' 77 | 'ProtocolType.site. Got: %s' % bad_attrs 78 | ) 79 | }) 80 | 81 | 82 | # Register required_attributes_changed -> ProtocolType.required_attributes 83 | models.signals.m2m_changed.connect( 84 | required_attributes_changed, 85 | sender=ProtocolType.required_attributes.through 86 | ) 87 | -------------------------------------------------------------------------------- /nsot/models/site.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from __future__ import absolute_import 4 | from django.db import models 5 | 6 | from .. import validators 7 | 8 | 9 | class Site(models.Model): 10 | """A namespace for attribtues, devices, and networks.""" 11 | name = models.CharField( 12 | max_length=255, unique=True, help_text='The name of the Site.' 13 | ) 14 | description = models.TextField( 15 | default='', blank=True, help_text='A helpful description for the Site.' 16 | ) 17 | 18 | def __unicode__(self): 19 | return self.name 20 | 21 | def clean_name(self, value): 22 | return validators.validate_name(value) 23 | 24 | def clean_fields(self, exclude=None): 25 | self.name = self.clean_name(self.name) 26 | 27 | def save(self, *args, **kwargs): 28 | self.full_clean() # First validate fields are correct 29 | super(Site, self).save(*args, **kwargs) 30 | 31 | def to_dict(self): 32 | return { 33 | 'id': self.id, 34 | 'name': self.name, 35 | 'description': self.description, 36 | } 37 | -------------------------------------------------------------------------------- /nsot/models/value.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from __future__ import absolute_import 4 | from django.db import models 5 | 6 | from .. import exc 7 | from . import constants 8 | from .attribute import Attribute 9 | 10 | 11 | class Value(models.Model): 12 | """Represents a value for an attribute attached to a Resource.""" 13 | attribute = models.ForeignKey( 14 | 'Attribute', related_name='values', db_index=True, 15 | on_delete=models.PROTECT, 16 | help_text='The Attribute to which this Value is assigned.' 17 | ) 18 | value = models.CharField( 19 | max_length=255, null=False, blank=True, db_index=True, 20 | help_text='The Attribute value.' 21 | ) 22 | resource_id = models.IntegerField( 23 | 'Resource ID', null=False, 24 | help_text='The unique ID of the Resource to which the Value is bound.', 25 | ) 26 | resource_name = models.CharField( 27 | 'Resource Type', max_length=20, null=False, db_index=True, 28 | choices=constants.RESOURCE_CHOICES, 29 | help_text='The name of the Resource type to which the Value is bound.', 30 | ) 31 | name = models.CharField( 32 | 'Name', max_length=64, null=False, blank=True, 33 | help_text=( 34 | 'The name of the Attribute to which the Value is bound. ' 35 | '(Internal use only)' 36 | ) 37 | ) 38 | 39 | # We are currently inferring the site_id from the parent Attribute in 40 | # .save() method. We don't want to even care about the site_id, but it 41 | # simplifies managing them this way. 42 | site = models.ForeignKey( 43 | 'Site', db_index=True, related_name='values', 44 | on_delete=models.PROTECT, verbose_name='Site', 45 | help_text='Unique ID of the Site this Value is under.' 46 | ) 47 | 48 | def __init__(self, *args, **kwargs): 49 | self._obj = kwargs.pop('obj', None) 50 | super(Value, self).__init__(*args, **kwargs) 51 | 52 | def __unicode__(self): 53 | return u'%s:%s %s=%s' % (self.resource_name, self.resource_id, 54 | self.name, self.value) 55 | 56 | class Meta: 57 | unique_together = ('name', 'value', 'resource_name', 'resource_id') 58 | 59 | # This is most commonly looked up 60 | index_together = [ 61 | ('name', 'value', 'resource_name'), 62 | ('resource_name', 'resource_id'), 63 | ] 64 | 65 | def clean_resource_name(self, value): 66 | if value not in constants.VALID_ATTRIBUTE_RESOURCES: 67 | raise exc.ValidationError('Invalid resource name: %r.' % value) 68 | return value 69 | 70 | def clean_name(self, attr): 71 | return attr.name 72 | 73 | def clean_site(self, value): 74 | """Always enforce that site is set.""" 75 | if value is None: 76 | try: 77 | return self.attribute.site_id 78 | except Attribute.DoesNotExist: 79 | return Attribute.objects.get(id=self.attribute_id).site_id 80 | 81 | return value 82 | 83 | def clean_fields(self, exclude=None): 84 | obj = self._obj 85 | if obj is None: 86 | return None 87 | 88 | self.site_id = self.clean_site(self.site_id) 89 | self.resource_name = self.clean_resource_name(obj.__class__.__name__) 90 | self.resource_id = obj.id 91 | self.name = self.clean_name(self.attribute) 92 | 93 | def save(self, *args, **kwargs): 94 | self.full_clean() 95 | super(Value, self).save(*args, **kwargs) 96 | 97 | def to_dict(self): 98 | return { 99 | 'id': self.id, 100 | 'name': self.name, 101 | 'value': self.value, 102 | 'attribute': self.attribute_id, 103 | 'resource_name': self.resource_name, 104 | 'resource_id': self.resource_id, 105 | } 106 | -------------------------------------------------------------------------------- /nsot/services/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | -------------------------------------------------------------------------------- /nsot/services/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | 4 | class Service(object): 5 | name = '' 6 | 7 | def __init__(self, debug=False): 8 | self.debug = debug 9 | -------------------------------------------------------------------------------- /nsot/services/http.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | from gunicorn.app.base import Application 4 | 5 | from nsot.services.base import Service 6 | 7 | 8 | class NsotGunicornCommand(Application): 9 | """Gunicorn WSGI service.""" 10 | def __init__(self, options): 11 | self.usage = None 12 | self.prog = None 13 | self.cfg = None 14 | self.config_file = "" 15 | self.options = options 16 | self.callable = None 17 | self.project_path = None 18 | self.do_load_config() 19 | 20 | def init(self, *args): 21 | cfg = {} 22 | for k, v in self.options.items(): 23 | if k.lower() in self.cfg.settings and v is not None: 24 | cfg[k.lower()] = v 25 | return cfg 26 | 27 | def load(self): 28 | import nsot.wsgi 29 | return nsot.wsgi.application 30 | 31 | 32 | class NsotHTTPServer(Service): 33 | """HTTP service options.""" 34 | name = 'http' 35 | 36 | def __init__(self, host=None, port=None, debug=False, workers=None, 37 | worker_class=None, timeout=None, loglevel='info', 38 | preload=False, max_requests=0, max_requests_jitter=0): 39 | 40 | options = { 41 | 'bind': '%s:%s' % (host, port), 42 | 'workers': workers, 43 | 'worker_class': worker_class, 44 | 'timeout': timeout, 45 | 'proc_name': 'NSoT', 46 | 'access_logfile': '-', # 'accesslog': '-', 47 | 'errorlog': '-', 48 | 'loglevel': loglevel, 49 | 'limit_request_line': 0, 50 | 'preload_app': preload, 51 | 'max_requests': max_requests, 52 | 'max_requests_jitter': max_requests_jitter, 53 | } 54 | 55 | self.options = options 56 | 57 | print( 58 | 'Running service: %r, num workers: %s, worker timeout: %s' % ( 59 | self.name, self.options['workers'], self.options['timeout'] 60 | ) 61 | ) 62 | 63 | def run(self): 64 | NsotGunicornCommand(self.options).run() 65 | -------------------------------------------------------------------------------- /nsot/static/src/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/nsot/static/src/images/favicon/favicon.ico -------------------------------------------------------------------------------- /nsot/static/src/images/halftone/halftone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/nsot/static/src/images/halftone/halftone.png -------------------------------------------------------------------------------- /nsot/static/src/images/halftone/readme.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | ======================================================== 4 | This pattern is downloaded from www.subtlepatterns.com 5 | If you need more, that's where to get'em. 6 | ======================================================== 7 | 8 | 9 | -------------------------------------------------------------------------------- /nsot/static/src/js/directives.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var app = angular.module("nsotApp"); 5 | 6 | app.directive("panel", function(){ 7 | return { 8 | restrict: "E", 9 | transclude: true, 10 | template: "
" + 11 | " " + 12 | "
" 13 | }; 14 | }); 15 | 16 | app.directive("panelHeading", function(){ 17 | return { 18 | restrict: "E", 19 | transclude: true, 20 | template: "
" + 21 | " " + 22 | "
" 23 | }; 24 | }); 25 | 26 | app.directive("panelBody", function(){ 27 | return { 28 | restrict: "E", 29 | transclude: true, 30 | template: "
" + 31 | " " + 32 | "
" 33 | }; 34 | }); 35 | 36 | app.directive("panelFooter", function(){ 37 | return { 38 | restrict: "E", 39 | transclude: true, 40 | template: "" 43 | }; 44 | }); 45 | 46 | app.directive("loadingPanel", function(){ 47 | return { 48 | restrict: "E", 49 | templateUrl: "directives/loading-panel.html" 50 | }; 51 | }); 52 | 53 | app.directive("headingBar", function(){ 54 | return { 55 | restrict: "E", 56 | scope: { 57 | "heading": "@", 58 | "subheading": "@" 59 | }, 60 | transclude: true, 61 | template: "
" + 62 | "
" + 63 | "

[[heading]]

" + 64 | "

[[subheading]]

" + 65 | "
" + 66 | " " + 67 | "
" + 68 | "
" + 69 | "
" 70 | }; 71 | }); 72 | 73 | app.directive("nsotModal", function(){ 74 | return { 75 | restrict: "E", 76 | scope: { 77 | "title": "@", 78 | "modalId": "@", 79 | "modalSize": "@" 80 | }, 81 | transclude: true, 82 | templateUrl: "directives/nsot-modal.html" 83 | }; 84 | }); 85 | 86 | app.directive("paginator", function(){ 87 | return { 88 | restrict: "E", 89 | scope: { 90 | "pager": "=", 91 | }, 92 | templateUrl: "directives/paginator.html" 93 | }; 94 | }); 95 | 96 | app.directive("dropdown", function(){ 97 | return { 98 | restrict: "E", 99 | scope: { 100 | "ctxtObj": "=", 101 | }, 102 | templateUrl: "directives/dropdown.html" 103 | }; 104 | }); 105 | 106 | })(); 107 | -------------------------------------------------------------------------------- /nsot/static/src/js/filters.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var app = angular.module("nsotApp"); 5 | 6 | app.filter("from_now", function(){ 7 | return function(input){ 8 | return moment.unix(input).fromNow(); 9 | }; 10 | }); 11 | 12 | app.filter("ts_fmt", function(){ 13 | return function(input){ 14 | return moment.unix(input).format("YYYY/MM/DD hh:mm:ss a"); 15 | }; 16 | }); 17 | 18 | })(); 19 | -------------------------------------------------------------------------------- /nsot/static/src/templates/attribute.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 9 | 14 | 15 | 16 |
17 | 18 | 19 | Attribute 20 | 21 | 22 |
23 |
Name
24 |
[[attribute.name]]
25 | 26 |
Resource Type
27 |
[[attribute.resource_name]]
28 | 29 |
Description
30 |
[[attribute.description]]
31 | 32 |
Value Pattern
33 |
[[attribute.constraints.pattern]]
34 | 35 |
Valid Values
36 |
[[attribute.constraints.valid_values.join(", ")]]
37 | 38 |
Required
39 |
40 | 41 |
Display
42 |
43 | 44 |
Allow Multiple Values
45 |
46 | 47 |
Allow Empty Values
48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 |
56 |
57 | 58 | 59 | 67 | 77 | 78 | 79 | 80 | 86 | 97 | 98 | 99 |
100 | -------------------------------------------------------------------------------- /nsot/static/src/templates/attributes.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 10 | 11 | 12 | 13 | 21 | 31 | 32 | 33 |
34 | 35 | Attributes 36 | 37 | No Attributes 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 58 | 59 | 62 | 65 | 68 | 69 | 70 |
Resource TypeNameDescriptionRequiredDisplayMulti
[[attr.resource_name]] 55 | [[attr.name]] 57 | [[attr.description]] 60 | 61 | 63 | 64 | 66 | 67 |
71 |
72 |
73 | 74 |
75 | 76 | 77 |
78 | -------------------------------------------------------------------------------- /nsot/static/src/templates/change.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 7 | 8 |
9 | 10 | 11 | Change 12 | 13 | 14 |
15 |
Event
16 |
[[change.event]]
17 | 18 |
Resource Type
19 |
[[change.resource_name]]
20 | 21 |
Resource ID
22 |
[[change.resource_id]]
23 | 24 |
User
25 |
[[change.user.email]]
26 | 27 |
Change At
28 |
[[change.change_at|from_now]]
29 | 30 |
Resource
31 |
[[change.resource|json:4]]
32 |
33 | 34 |
35 |
36 |
37 | 38 |
39 | -------------------------------------------------------------------------------- /nsot/static/src/templates/changes.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | Changes 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
IDUserEventResource TypeResource IDChange At
26 | 27 | [[change.id]] 28 | 29 | [[change.user.email]][[change.event]][[change.resource_name]][[change.resource_id]][[change.change_at|from_now]]
38 |
39 |
40 | 41 |
42 |
43 | -------------------------------------------------------------------------------- /nsot/static/src/templates/devices.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 24 | 34 | 35 | 36 |
37 | 38 | Devices 39 | 40 | No Devices 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 55 | 56 | 57 |
Hostname
52 | [[device.hostname]] 54 |
58 |
59 |
60 | 61 |
62 | 63 | 64 |
65 | -------------------------------------------------------------------------------- /nsot/static/src/templates/directives/dropdown.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 12 |
13 | -------------------------------------------------------------------------------- /nsot/static/src/templates/directives/loading-panel.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Loading... 4 | 5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /nsot/static/src/templates/directives/nsot-modal.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | -------------------------------------------------------------------------------- /nsot/static/src/templates/directives/paginator.html: -------------------------------------------------------------------------------- 1 | 22 | -------------------------------------------------------------------------------- /nsot/static/src/templates/includes/attributes-form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
7 | 8 | 15 |
16 | 17 | [[attribute.name]] 18 | 19 |
20 | 21 |
22 |
26 | 27 | 37 |
38 | 39 | [[attribute.resource_name]] 40 | 41 |
42 |
43 |
44 | 45 | 51 |
52 |

Constraints

53 |
54 | 55 | 61 |
62 |
63 | 64 | 69 |
70 | 74 | 78 | 82 | 86 | -------------------------------------------------------------------------------- /nsot/static/src/templates/includes/devices-form.html: -------------------------------------------------------------------------------- 1 |
5 | 13 |
14 | 15 |

Attributes

16 | 17 |
18 | 19 |
20 | 23 |
24 |
25 | 41 |
42 | 43 |
44 |
48 | 56 | 62 |
63 |
64 | 65 |
66 | 67 | 70 | 71 |
72 |
73 | 74 | 82 | 83 | -------------------------------------------------------------------------------- /nsot/static/src/templates/includes/networks-form.html: -------------------------------------------------------------------------------- 1 |
5 | 14 | 15 | [[network.network_address]]/[[network.prefix_length]] 16 | 17 |
18 | 19 |

Attributes

20 | 21 |
22 | 23 |
24 | 27 |
28 |
29 | 45 |
46 | 47 |
48 |
52 | 60 | 66 |
67 |
68 | 69 |
70 | 71 | 74 | 75 |
76 |
77 | 78 | 86 | 87 | -------------------------------------------------------------------------------- /nsot/static/src/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /nsot/static/src/templates/interfaces.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 24 | 34 | 35 | 36 |
37 | 38 | Interfaces 39 | 40 | No Interfaces 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | 65 | 68 | 71 | 74 | 75 | 76 |
NameDeviceSpeedTypeMAC
56 | [[iface.name]] 59 | 61 | [[iface.device]] 64 | 66 | [[iface.speed]] 67 | 69 | [[iface.type]] 70 | 72 | [[iface.mac_address]] 73 |
77 |
78 |
79 | 80 |
81 | 82 | 83 |
84 | -------------------------------------------------------------------------------- /nsot/static/src/templates/sites.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 9 | 10 | 11 | 12 | 42 | 52 | 53 | 54 |
55 | 56 | Welcome 57 | 58 | Welcome to the Network Source of Truth. It looks like you're new 59 | here. Lets start by creating a site with the button above. 60 | This will be a namespace for all of your data. 61 | 62 | 63 |
64 |
65 | 66 | Sites 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 |
NameDescription
78 | [[site.name]] 79 | [[site.description]]
84 |
85 |
86 | 87 |
88 |
89 | -------------------------------------------------------------------------------- /nsot/static/src/templates/user.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 6 | 7 | 8 |
9 | 10 | 11 | User 12 | 13 | 14 |
15 |
Email
16 |
[[profileUser.email]]
17 | 18 | 19 |
Secret Key
20 |
21 | Click to show key! 22 |
23 |
24 | [[secret_key]] 25 | (Rotate Key) 26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 |
35 | -------------------------------------------------------------------------------- /nsot/static/src/templates/users.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | -------------------------------------------------------------------------------- /nsot/templates/rest_framework/api.html: -------------------------------------------------------------------------------- 1 | {% extends "rest_framework/base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block title %} 5 | NSoT | REST API 6 | {% endblock %} 7 | 8 | {% block bootstrap_theme %} 9 | {% include "ui/media.html" %} 10 | {% endblock %} 11 | 12 | {% block navbar %} 13 |
14 | {% include "ui/menu.html" %} 15 |
16 | {% endblock %} 17 | 18 | {% block script %} 19 | {% include "ui/scripts.html" %} 20 | 21 | 22 | 23 | 24 | 29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /nsot/templates/rest_framework/login.html: -------------------------------------------------------------------------------- 1 | {% extends "rest_framework/login_base.html" %} 2 | {% load staticfiles %} 3 | 4 | {% block title %} 5 | NSoT | Login 6 | {% endblock %} 7 | 8 | {% block bootstrap_theme %} 9 | {% include "ui/media.html" %} 10 | {% endblock %} 11 | 12 | {% block branding %} 13 |

Network Source of Truth

14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /nsot/templates/ui/app.html: -------------------------------------------------------------------------------- 1 | 2 | {% autoescape on %} 3 | 4 | 5 | NSoT 6 | 7 | {% include "ui/media.html" %} 8 | 9 | 10 |
11 | {% include "ui/menu.html" %} 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 | {% include "ui/scripts.html" %} 23 | 24 | 25 | {% endautoescape %} 26 | -------------------------------------------------------------------------------- /nsot/templates/ui/error.html: -------------------------------------------------------------------------------- 1 | 2 | {% autoescape on %} 3 | 4 | 5 | NSoT Error 6 | 7 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | Error Loading Application 19 |
20 |
21 | You've failed to access the NSoT Frontend Application. 22 | This usually means you've tried to access without an 23 | authenticating proxy. Please contact your administrator. 24 |
25 |
26 | {{code}} - {{message}} 27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | 35 | {% endautoescape %} 36 | -------------------------------------------------------------------------------- /nsot/templates/ui/media.html: -------------------------------------------------------------------------------- 1 | 4 | 7 | 10 | 13 | 16 | 19 | 22 | -------------------------------------------------------------------------------- /nsot/templates/ui/scripts.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /nsot/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/nsot/ui/__init__.py -------------------------------------------------------------------------------- /nsot/ui/context_processors.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom context processors for templates. 3 | 4 | Put me in ``settings.py`` like so:: 5 | 6 | TEMPLATE_CONTEXT_PROCESSORS = ( 7 | # ... 8 | 'nsot.ui.context_processors.app_version', 9 | ) 10 | 11 | Credit: http://stackoverflow.com/a/4256485/194311 12 | """ 13 | 14 | 15 | from __future__ import absolute_import 16 | 17 | 18 | def app_version(request): 19 | """A template variable to display current version.""" 20 | from nsot import __version__ 21 | return {'NSOT_VERSION': __version__} 22 | -------------------------------------------------------------------------------- /nsot/ui/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import unicode_literals 4 | from __future__ import absolute_import 5 | from six.moves.http_client import responses 6 | import logging 7 | 8 | from django.shortcuts import render 9 | from django.views.generic import TemplateView 10 | 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class FeView(TemplateView): 16 | """ 17 | Front-end UI view that hands-off rendering to Angular.js. 18 | 19 | Any additional context needed to be passed to the templates, should be 20 | added in ``nsot.ui.context_processors`` 21 | """ 22 | template_name = 'ui/app.html' 23 | 24 | 25 | def render_error(request, status_code, template_name='ui/error.html'): 26 | """Generic base for rendering error pages.""" 27 | message = responses[status_code].upper() 28 | context = {'code': status_code, 'message': message} 29 | return render(request, template_name, context, status=status_code) 30 | 31 | 32 | def handle400(request): 33 | """Handler for 400.""" 34 | return render_error(request, 400) 35 | 36 | 37 | def handle403(request): 38 | """Handler for 403.""" 39 | return render_error(request, 403) 40 | 41 | 42 | def handle404(request): 43 | """Handler for 404.""" 44 | return render_error(request, 404) 45 | 46 | 47 | def handle500(request): 48 | """Handler for 500.""" 49 | return render_error(request, 500) 50 | -------------------------------------------------------------------------------- /nsot/util/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities used across the project. 3 | """ 4 | 5 | # Core 6 | from __future__ import absolute_import 7 | from . import core 8 | from .core import * # noqa 9 | 10 | # Stats 11 | from . import stats 12 | from .stats import * # noqa 13 | 14 | 15 | __all__ = [] 16 | __all__.extend(core.__all__) 17 | __all__.extend(stats.__all__) 18 | -------------------------------------------------------------------------------- /nsot/util/cache.py: -------------------------------------------------------------------------------- 1 | """ 2 | Used for caching read-only REST API responses (provided by drf-extensions). 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import logging 7 | from rest_framework_extensions.key_constructor import bits, constructors 8 | from django.core.cache import cache as djcache 9 | from django.utils import timezone 10 | from django.utils.encoding import force_text 11 | 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | __all__ = ('object_key_func', 'list_key_func') 17 | 18 | 19 | class UpdatedAtKeyBit(bits.KeyBitBase): 20 | """Used to store/retrieve timestamp from the cache.""" 21 | def get_data(self, **kwargs): 22 | key = 'api_updated_at_timestamp' 23 | value = djcache.get(key, None) 24 | if not value: 25 | value = timezone.now() 26 | djcache.set(key, value=value) 27 | 28 | return force_text(value) 29 | 30 | 31 | class ObjectKeyConstructor(constructors.DefaultKeyConstructor): 32 | """Cache key generator for object/detail views.""" 33 | retrieve_sql = bits.RetrieveSqlQueryKeyBit() 34 | updated_at = UpdatedAtKeyBit() 35 | kwargs = bits.KwargsKeyBit() 36 | params = bits.QueryParamsKeyBit() 37 | unique_view_id = bits.UniqueMethodIdKeyBit() 38 | format = bits.FormatKeyBit() 39 | 40 | 41 | object_key_func = ObjectKeyConstructor() 42 | 43 | 44 | class ListKeyConstructor(constructors.DefaultKeyConstructor): 45 | """Cache key generator for list views.""" 46 | list_sql = bits.ListSqlQueryKeyBit() 47 | pagination = bits.PaginationKeyBit() 48 | updated_at = UpdatedAtKeyBit() 49 | kwargs = bits.KwargsKeyBit() 50 | params = bits.QueryParamsKeyBit() 51 | unique_view_id = bits.UniqueMethodIdKeyBit() 52 | format = bits.FormatKeyBit() 53 | 54 | 55 | list_key_func = ListKeyConstructor() 56 | -------------------------------------------------------------------------------- /nsot/util/commands.py: -------------------------------------------------------------------------------- 1 | """ 2 | Customized base Django management command specialized for NSoT. 3 | """ 4 | 5 | from __future__ import absolute_import, print_function 6 | import argparse 7 | import logging 8 | 9 | from django.core.management.base import BaseCommand, CommandError 10 | 11 | 12 | __all__ = ('NsotCommand', 'CommandError') 13 | 14 | 15 | class NsotCommand(BaseCommand): 16 | """Base management command for NSoT that implements a custom logger.""" 17 | 18 | # This is here to alleviate an AttributeError when getting /admin/jsi18n/ 19 | # Ref: https://github.com/dropbox/nsot/issues/279 20 | leave_locale_alone = True 21 | 22 | def create_parser(self, prog_name, subcommand): 23 | """Override default parser to include default values in help.""" 24 | parser = super(NsotCommand, self).create_parser( 25 | prog_name, subcommand 26 | ) 27 | 28 | # So that we can see default values in the help text. 29 | parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter 30 | 31 | return parser 32 | 33 | def get_loglevel(self, verbosity, as_string=False): 34 | """Get the log-level.""" 35 | if verbosity < 1: 36 | level_name = 'notset' 37 | elif verbosity > 1: 38 | level_name = 'debug' 39 | else: 40 | level_name = 'info' 41 | 42 | if as_string: 43 | return level_name 44 | else: 45 | return getattr(logging, level_name.upper()) 46 | 47 | def set_logging(self, verbosity): 48 | """Set the log-level.""" 49 | log = logging.getLogger('nsot_server') 50 | 51 | loglevel = self.get_loglevel(verbosity) 52 | log.setLevel(loglevel) 53 | 54 | self.log = log 55 | 56 | def execute(self, *args, **options): 57 | """Setup our logging object before execution.""" 58 | self.set_logging(options.get('verbosity')) 59 | 60 | super(NsotCommand, self).execute(*args, **options) 61 | -------------------------------------------------------------------------------- /nsot/util/stats.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, print_function 2 | from __future__ import absolute_import 3 | 4 | """ 5 | Gettings stats out of NSoT. 6 | """ 7 | 8 | from netaddr import IPNetwork, IPSet 9 | 10 | 11 | __all__ = ('calculate_network_utilization', 'get_network_utilization') 12 | 13 | 14 | def calculate_network_utilization(parent, hosts, as_string=False): 15 | """ 16 | Calculate utilization for a network and its descendants. 17 | 18 | :param parent: 19 | The parent network 20 | 21 | :param hosts: 22 | List of host IPs descendant from parent 23 | 24 | :param as_string: 25 | Whether to return stats as a string 26 | """ 27 | parent = IPNetwork(str(parent)) 28 | hosts = IPSet(str(ip) for ip in hosts if IPNetwork(str(ip)) in parent) 29 | 30 | used = float(hosts.size) / float(parent.size) 31 | free = 1 - used 32 | num_free = parent.size - hosts.size 33 | 34 | stats = { 35 | 'percent_used': used, 36 | 'num_used': hosts.size, 37 | 'percent_free': free, 38 | 'num_free': num_free, 39 | 'max': parent.size, 40 | } 41 | 42 | # 10.47.216.0/22 - 14% used (139), 86% free (885) 43 | if as_string: 44 | return '{} - {:.0%} used ({}), {:.0%} free ({})'.format( 45 | parent, used, hosts.size, free, num_free 46 | ) 47 | 48 | return stats 49 | 50 | 51 | def get_network_utilization(network, as_string=False): 52 | """ 53 | Get utilization from Network instance. 54 | 55 | :param network: 56 | A Network model instance 57 | 58 | :param as_string: 59 | Whether to return stats as a string 60 | """ 61 | descendants = network.get_descendants().filter(is_ip=True) 62 | return calculate_network_utilization(network, descendants, as_string) 63 | -------------------------------------------------------------------------------- /nsot/validators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validators for validating object fields. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | from django.conf import settings 7 | from django.core.validators import EmailValidator 8 | import ipaddress 9 | import netaddr 10 | import six 11 | 12 | from . import exc 13 | 14 | 15 | def validate_mac_address(value): 16 | """Validate whether ``value`` is a valid MAC address.""" 17 | if value is None: 18 | return value 19 | 20 | # If the incoming value is a string, cast it to an int 21 | if isinstance(value, six.string_types) and value.isdigit(): 22 | value = int(value) 23 | 24 | # Directly invoke EUI object instead of using MACAddressField 25 | try: 26 | value = netaddr.EUI(value, version=48) 27 | except (ValueError, TypeError, netaddr.AddrFormatError): 28 | raise exc.ValidationError({ 29 | 'mac_address': 'Enter a valid MAC Address.' 30 | }) 31 | 32 | return value 33 | 34 | 35 | def validate_name(value): 36 | """Validate whether ``value`` is a valid name.""" 37 | if not value: 38 | raise exc.ValidationError({ 39 | 'name': 'This is a required field.' 40 | }) 41 | return value 42 | 43 | 44 | def validate_cidr(value): 45 | """Validate whether ``value`` is a validr IPv4/IPv6 CIDR.""" 46 | try: 47 | cidr = ipaddress.ip_network(six.text_type(value)) 48 | except ValueError: 49 | raise exc.ValidationError({ 50 | 'cidr': '%r does not appear to be an IPv4 or IPv6 network' % value 51 | }) 52 | else: 53 | return cidr 54 | 55 | 56 | def validate_host_address(value): 57 | """Validate whether ``value`` is a host IP address.""" 58 | cidr = validate_cidr(value) 59 | if cidr.prefixlen not in settings.HOST_PREFIXES: 60 | raise exc.ValidationError({ 61 | 'address': '%r is not a valid host address!' % value 62 | }) 63 | return value 64 | 65 | 66 | def validate_email(value): 67 | """Validate whether ``value`` is an email address.""" 68 | validator = EmailValidator() 69 | try: 70 | validator(value) 71 | except exc.DjangoValidationError as err: 72 | raise exc.ValidationError({ 73 | 'email': err.message 74 | }) 75 | return value 76 | -------------------------------------------------------------------------------- /nsot/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.4.6' 2 | -------------------------------------------------------------------------------- /nsot/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for nsot project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | from __future__ import absolute_import 11 | import os 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nsot.conf.settings") 15 | 16 | from django.conf import settings 17 | 18 | 19 | # If we're set to serve static files ourself (default), wrap the app w/ Cling 20 | # (provided by dj-static). 21 | if settings.SERVE_STATIC_FILES: 22 | from dj_static import Cling 23 | application = Cling(get_wsgi_application()) 24 | else: 25 | application = get_wsgi_application() 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nsot", 3 | "description": "NSoT is a Network Source of Truth API and Frontend for managing Network Assets.", 4 | "private": true, 5 | "dependencies": { 6 | "angular": "1.3.15", 7 | "angular-chart.js": "~0.8.5", 8 | "angular-resource": "1.3.15", 9 | "angular-route": "1.3.15", 10 | "bootstrap": "3.3.2", 11 | "chart.js": "1.1.1", 12 | "font-awesome": "4.3.0", 13 | "jquery": "2.1.4", 14 | "lodash": "3.7.0", 15 | "lodash-cli": "3.7.0", 16 | "moment": "2.10.2", 17 | "ng-tags-input": "2.3.0" 18 | }, 19 | "devDependencies": { 20 | "add-stream": "~1.0.0", 21 | "del": "~1.2.0", 22 | "gulp": "~3.9.0", 23 | "gulp-angular-templatecache": "~1.7.0", 24 | "gulp-concat": "~2.6.0", 25 | "gulp-csslint": "~0.1.5", 26 | "gulp-filter": "~2.0.2", 27 | "gulp-jshint": "~1.11.2", 28 | "gulp-minify-css": "~1.2.0", 29 | "gulp-ng-annotate": "~1.0.0", 30 | "gulp-rename": "~1.2.2", 31 | "gulp-sort": "~1.1.1", 32 | "gulp-uglify": "~1.2.0", 33 | "jshint-stylish": "~2.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = tests.test_settings 3 | django_find_project = false 4 | python_paths = . 5 | addopts = -vv 6 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | attrs~=17.4.0 3 | argh~=0.26.1 4 | betamax~=0.4.2 5 | docutils~=0.12.0 6 | ipdb~=0.9.3 7 | flake8~=3.3.0 8 | funcsigs~=1.0.2 9 | livereload~=2.4.0 10 | mrproxy~=0.4.0 11 | pluggy~=0.6.0 12 | py~=1.5.2 13 | pytest~=3.4.1 14 | pytest-django~=3.1.2 15 | pytest-pythonpath~=0.6.0 16 | PyYAML~=5.1 17 | Sphinx~=1.3.6 18 | sphinx-autobuild~=0.6.0 19 | sphinx-rtd-theme~=0.1.9 20 | twine~=1.9.1 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | enum34~=1.1.6 2 | cffi>=1.4.1,<2.0.0 3 | pyasn1~=0.1.9 4 | six~=1.10.0 5 | pycparser~=2.14.0 6 | backports.ssl-match-hostname~=3.4.0.2 7 | idna~=2.6.0 8 | coreapi~=2.3.3 9 | coreschema~=0.0.4 10 | cryptography~=2.8 11 | certifi~=2018.1.18 12 | dj-static~=0.0.6 13 | Django~=1.11.11 14 | django-custom-user~=0.7.0 15 | django-extensions~=2.0.0 16 | django-filter~=1.1.0 17 | django-guardian~=1.4.9 18 | django-jinja~=2.4.1 19 | django-macaddress~=1.4.1 20 | django-rest-swagger~=2.1.2 21 | djangorestframework~=3.7.7 22 | djangorestframework-bulk~=0.2.1 23 | drf-extensions~=0.3.1 24 | drf-nested-routers~=0.11.1 25 | gevent~=1.4.0 26 | gunicorn~=19.5.0 27 | greenlet~=0.4.9 28 | ipaddress~=1.0.14 29 | ipython~=3.1.0 30 | itypes~=1.1.0 31 | Jinja2~=2.8.0 32 | logan~=0.7.2 33 | MarkupSafe~=0.23.0 34 | netaddr~=0.7.18 35 | openapi-codec~=1.3.2 36 | requests~=2.20.0 37 | simplejson~=3.13.2 38 | static3~=0.6.1 39 | typing~=3.6.4 40 | uritemplate~=3.0.0 41 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = migrations,settings.py,tests/*,docs,demo,wsgi.py 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/tests/__init__.py -------------------------------------------------------------------------------- /tests/api_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/tests/api_tests/__init__.py -------------------------------------------------------------------------------- /tests/api_tests/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration file for the unit tests via py.test. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import betamax 7 | import os 8 | 9 | 10 | CASSETTE_DIR = 'tests/api_tests/cassettes' 11 | 12 | 13 | # Tell Betamax where to save the cassette fixtures. 14 | with betamax.Betamax.configure() as config: 15 | 16 | if not os.path.exists(CASSETTE_DIR): 17 | os.makedirs(CASSETTE_DIR) 18 | 19 | config.cassette_library_dir = 'tests/api_tests/cassettes' 20 | config.default_cassette_options['record_mode'] = 'all' 21 | -------------------------------------------------------------------------------- /tests/api_tests/data/attributes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constraints": { 4 | "allow_empty": true 5 | }, 6 | "description": "Device cluster.", 7 | "name": "cluster", 8 | "resource_name": "Device" 9 | }, 10 | { 11 | "description": "Foo for Devices.", 12 | "name": "foo", 13 | "resource_name": "Device" 14 | }, 15 | { 16 | "description": "Device owner.", 17 | "name": "owner", 18 | "resource_name": "Device" 19 | }, 20 | { 21 | "constraints": { 22 | "allow_empty": true 23 | }, 24 | "description": "Network cluster.", 25 | "name": "cluster", 26 | "resource_name": "Network" 27 | }, 28 | { 29 | "description": "Foo for Networks.", 30 | "name": "foo", 31 | "resource_name": "Network" 32 | }, 33 | { 34 | "description": "Network owner.", 35 | "name": "owner", 36 | "resource_name": "Network" 37 | }, 38 | { 39 | "description": "Address hostname.", 40 | "name": "hostname", 41 | "resource_name": "Network" 42 | }, 43 | { 44 | "constraints": { 45 | "valid_values": [ 46 | "300", 47 | "400" 48 | ] 49 | }, 50 | "description": "Network VLAN.", 51 | "name": "vlan", 52 | "resource_name": "Network" 53 | }, 54 | { 55 | "constraints": { 56 | "valid_values": [ 57 | "300", 58 | "400" 59 | ] 60 | }, 61 | "description": "Interface VLAN.", 62 | "name": "vlan", 63 | "resource_name": "Interface" 64 | }, 65 | { 66 | "description": "Interface scope.", 67 | "name": "scope", 68 | "resource_name": "Interface" 69 | }, 70 | { 71 | "description": "Circuit ID.", 72 | "name": "cid", 73 | "resource_name": "Circuit" 74 | }, 75 | { 76 | "description": "Circuit Vendor.", 77 | "name": "vendor", 78 | "resource_name": "Circuit" 79 | }, 80 | { 81 | "description": "Peer AS", 82 | "name": "peer_as", 83 | "resource_name": "Protocol" 84 | }, 85 | { 86 | "constraints": { 87 | "valid_values": [ 88 | "up", 89 | "down" 90 | ] 91 | }, 92 | "description": "Admin Status", 93 | "name": "admin_status", 94 | "resource_name": "Protocol" 95 | } 96 | ] 97 | -------------------------------------------------------------------------------- /tests/api_tests/data/devices.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "attributes": { 4 | "cluster": "", 5 | "foo": "bar", 6 | "owner": "jathan" 7 | }, 8 | "hostname": "foo-bar1" 9 | }, 10 | { 11 | "attributes": { 12 | "cluster": "", 13 | "foo": "baz", 14 | "owner": "gary" 15 | }, 16 | "hostname": "foo-bar2" 17 | }, 18 | { 19 | "attributes": { 20 | "cluster": "lax", 21 | "foo": "baz", 22 | "owner": "jathan" 23 | }, 24 | "hostname": "foo-bar3" 25 | }, 26 | { 27 | "attributes": { 28 | "cluster": "sjc", 29 | "foo": "bar", 30 | "owner": "gary" 31 | }, 32 | "hostname": "foo-bar4" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /tests/api_tests/data/networks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "attributes": { 4 | "cluster": "", 5 | "foo": "baz", 6 | "owner": "jathan" 7 | }, 8 | "cidr": "192.168.0.0/16" 9 | }, 10 | { 11 | "attributes": { 12 | "cluster": "lax", 13 | "foo": "bar", 14 | "owner": "gary" 15 | }, 16 | "cidr": "10.0.0.0/8" 17 | }, 18 | { 19 | "attributes": { 20 | "cluster": "lax", 21 | "foo": "baz", 22 | "owner": "gary" 23 | }, 24 | "cidr": "172.16.0.0/12" 25 | }, 26 | { 27 | "attributes": { 28 | "cluster": "sjc", 29 | "foo": "bar", 30 | "owner": "jathan" 31 | }, 32 | "cidr": "169.254.0.0/16" 33 | }, 34 | { 35 | "attributes": { 36 | "hostname": "foo-bar1", 37 | "vlan": "300" 38 | }, 39 | "cidr": "192.168.0.1/32" 40 | }, 41 | { 42 | "attributes": { 43 | "hostname": "foo-bar1" 44 | }, 45 | "cidr": "192.168.0.0/24" 46 | }, 47 | { 48 | "attributes": { 49 | "hostname": "foo-bar1" 50 | }, 51 | "cidr": "192.168.0.0/25" 52 | } 53 | ] 54 | -------------------------------------------------------------------------------- /tests/api_tests/fixtures.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.core.urlresolvers import reverse 3 | import json 4 | import logging 5 | import os 6 | import pytest 7 | from pytest_django.fixtures import live_server, django_user_model, settings 8 | import requests 9 | 10 | from .util import Client, SiteHelper 11 | 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | # API version to use for the API client 17 | API_VERSION = os.getenv('NSOT_API_VERSION') 18 | 19 | 20 | @pytest.fixture 21 | def user(django_user_model): 22 | """Create and return a non-admin user.""" 23 | user = django_user_model.objects.create(email='user@localhost') 24 | return user 25 | 26 | 27 | @pytest.fixture 28 | def site(live_server): 29 | client = Client(live_server) 30 | site_uri = reverse('site-list') # /api/sites/ 31 | resp = client.create(site_uri, name='Test Site') 32 | 33 | site = SiteHelper(resp.json()) 34 | return site 35 | 36 | 37 | @pytest.fixture 38 | def client(live_server): 39 | """Create and return an admin client.""" 40 | return Client(live_server, api_version=API_VERSION) 41 | 42 | 43 | @pytest.fixture 44 | def user_client(live_server): 45 | """Create and return a non-admin client.""" 46 | return Client(live_server, user='user', api_version=API_VERSION) 47 | 48 | 49 | @pytest.fixture 50 | def nosuperuser_settings(settings): 51 | """Return settings that have default superuser users disabled.""" 52 | settings.NSOT_NEW_USERS_AS_SUPERUSER = False 53 | return settings 54 | -------------------------------------------------------------------------------- /tests/api_tests/test_permissions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | import pytest 6 | 7 | # Allow everything in there to access the DB 8 | pytestmark = pytest.mark.django_db 9 | 10 | import copy 11 | from django.core.urlresolvers import reverse 12 | import json 13 | import logging 14 | from rest_framework import status 15 | 16 | from .fixtures import live_server, client, user, site, user_client 17 | from .util import ( 18 | assert_created, assert_error, assert_success, assert_deleted, load_json, 19 | Client, load, get_result 20 | ) 21 | 22 | 23 | log = logging.getLogger(__name__) 24 | 25 | 26 | def test_permissions(client, user_client, user, site): 27 | # URIs 28 | attr_uri = site.list_uri('attribute') 29 | net_uri = site.list_uri('network') 30 | dev_uri = site.list_uri('device') 31 | 32 | # Create an Attribute 33 | attr_resp = client.create( 34 | attr_uri, resource_name='Network', name='attr1' 35 | ) 36 | attr = get_result(attr_resp) 37 | attr_obj_uri = site.detail_uri('attribute', id=attr['id']) 38 | 39 | # Create a Network 40 | net_resp = client.create(net_uri, cidr='10.0.0.0/24') 41 | net = get_result(net_resp) 42 | net_obj_uri = site.detail_uri('network', id=net['id']) 43 | 44 | # Create a Device 45 | dev_resp = client.create(dev_uri, hostname='dev1') 46 | dev = get_result(dev_resp) 47 | dev_obj_uri = site.detail_uri('device', id=dev['id']) 48 | 49 | # User shouldn't be able to update site or create/update other resources 50 | # Site 51 | assert_error( 52 | user_client.update(site.detail_uri(), name='site1'), 53 | status.HTTP_403_FORBIDDEN 54 | ) 55 | # Attribute 56 | assert_error( 57 | user_client.create(attr_uri, name='attr2'), 58 | status.HTTP_403_FORBIDDEN 59 | ) 60 | assert_error( 61 | user_client.update(attr_obj_uri, required=True), 62 | status.HTTP_403_FORBIDDEN 63 | ) 64 | # Network 65 | assert_error( 66 | user_client.create(net_uri, cidr='10.0.0.0/8'), 67 | status.HTTP_403_FORBIDDEN 68 | ) 69 | assert_error( 70 | user_client.update(net_obj_uri, attributes={'attr1': 'foo'}), 71 | status.HTTP_403_FORBIDDEN 72 | ) 73 | # Device 74 | assert_error( 75 | user_client.create(dev_uri, name='dev2'), 76 | status.HTTP_403_FORBIDDEN 77 | ) 78 | assert_error( 79 | user_client.update(dev_obj_uri, hostname='foobar'), 80 | status.HTTP_403_FORBIDDEN 81 | ) 82 | -------------------------------------------------------------------------------- /tests/api_tests/test_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | import pytest 6 | 7 | # Allow everything in there to access the DB 8 | pytestmark = pytest.mark.django_db 9 | 10 | import copy 11 | from django.core.urlresolvers import reverse 12 | import json 13 | import logging 14 | from rest_framework import status 15 | 16 | 17 | from .fixtures import live_server, client, user, site 18 | from .util import ( 19 | assert_created, assert_error, assert_success, assert_deleted, load_json, 20 | Client, load, get_result 21 | ) 22 | 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | def test_user_with_secret_key(live_server): 28 | user1_client = Client(live_server, 'user1') 29 | user2_client = Client(live_server, 'user2') 30 | 31 | # URI for user 0 32 | user_uri = reverse('user-detail', args=(0,)) 33 | 34 | # Small requests to make user accounts in order. 35 | user1_resp = user1_client.get(user_uri) 36 | user1 = get_result(user1_resp) 37 | user1_uri = reverse('user-detail', args=(user1['id'],)) 38 | 39 | user2_resp = user2_client.get(user_uri) 40 | user2 = get_result(user2_resp) 41 | user2_uri = reverse('user-detail', args=(user2['id'],)) 42 | 43 | # User should be able to get user 0 (self) 44 | assert_success( 45 | user1_client.get(user_uri), 46 | user1 47 | ) 48 | 49 | # And see their own secret key as user 0 50 | response = user1_client.get(user_uri + '?with_secret_key') 51 | expected = copy.deepcopy(user1) 52 | result = get_result(response) 53 | expected['secret_key'] = result['secret_key'] 54 | assert_success(response, expected) 55 | 56 | # And their own secret key by their user id 57 | response = user1_client.get(user1_uri + '?with_secret_key') 58 | assert_success(response, expected) 59 | 60 | # But not user 2's secret_key. 61 | response = user1_client.get(user2_uri + '?with_secret_key') 62 | assert_error(response, status.HTTP_403_FORBIDDEN) 63 | 64 | 65 | def test_user_rotate_secret_key(live_server): 66 | user1_client = Client(live_server, 'user1') 67 | user2_client = Client(live_server, 'user2') 68 | 69 | # URI for user 0 70 | user_uri = reverse('user-detail', args=(0,)) 71 | 72 | # Small requests to make user accounts in order. 73 | user1_resp = user1_client.get(user_uri) 74 | user1 = get_result(user1_resp) 75 | user1_key_uri = reverse('user-rotate-secret-key', args=(user1['id'],)) 76 | 77 | user2_resp = user2_client.get(user_uri) 78 | user2 = get_result(user2_resp) 79 | user2_key_uri = reverse('user-rotate-secret-key', args=(user2['id'],)) 80 | 81 | # User1 should be able to rotate their own secret_key 82 | assert_success(user1_client.post(user1_key_uri)) 83 | 84 | # But not user 2's secret_key 85 | assert_error(user1_client.post(user2_key_uri), status.HTTP_403_FORBIDDEN) 86 | -------------------------------------------------------------------------------- /tests/api_tests/test_values.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | import pytest 6 | 7 | # Allow everything in there to access the DB 8 | pytestmark = pytest.mark.django_db 9 | 10 | import copy 11 | from django.core.urlresolvers import reverse 12 | import json 13 | import logging 14 | from rest_framework import status 15 | 16 | from .fixtures import live_server, client, user, site 17 | from .util import ( 18 | assert_created, assert_error, assert_success, assert_deleted, load_json, 19 | Client, load, filter_values, get_result 20 | ) 21 | 22 | 23 | log = logging.getLogger(__name__) 24 | 25 | 26 | def test_filters(site, client): 27 | """Test field-based filters for Values.""" 28 | # URIs 29 | attr_uri = site.list_uri('attribute') 30 | dev_uri = site.list_uri('device') 31 | val_uri = site.list_uri('value') 32 | 33 | # Pre-load the Attributes 34 | client.post(attr_uri, data=load('attributes.json')) 35 | 36 | # Populate the Device objects 37 | client.post(dev_uri, data=load('devices.json')) 38 | 39 | # Get all the Values for testing 40 | val_resp = client.get(val_uri) 41 | values = get_result(val_resp) 42 | 43 | # Test lookup by name 44 | kwargs = {'name': 'owner'} 45 | wanted = filter_values(values, **kwargs) 46 | expected = wanted 47 | assert_success( 48 | client.retrieve(val_uri, **kwargs), 49 | expected 50 | ) 51 | 52 | # Test lookup by name + value 53 | kwargs = {'name': 'owner', 'value': 'jathan'} 54 | wanted = filter_values(values, **kwargs) 55 | expected = wanted 56 | assert_success( 57 | client.retrieve(val_uri, **kwargs), 58 | expected 59 | ) 60 | 61 | # Test lookup by resource_name + resource_id 62 | kwargs = {'resource_name': 'Device', 'resource_id': 4} 63 | wanted = filter_values(values, **kwargs) 64 | expected = wanted 65 | assert_success( 66 | client.retrieve(val_uri, **kwargs), 67 | expected 68 | ) 69 | -------------------------------------------------------------------------------- /tests/api_tests/test_xforwardfor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from __future__ import absolute_import 4 | import logging 5 | 6 | import pytest 7 | 8 | # Allow everything in here to access the DB 9 | pytestmark = pytest.mark.django_db 10 | 11 | from django.core.urlresolvers import reverse 12 | from django.conf import settings 13 | import requests 14 | from rest_framework import status 15 | 16 | from .fixtures import client 17 | from .util import ( 18 | assert_created, assert_deleted, assert_error, assert_success, SiteHelper 19 | ) 20 | 21 | 22 | log = logging.getLogger(__name__) 23 | 24 | 25 | def test_request_xforwardfor(live_server): 26 | """Test processing of X-Forwarded-For header.""" 27 | url = '{}/api/sites/'.format(live_server.url) 28 | headers = { 29 | 'X-NSoT-Email': 'gary@localhost', 30 | 'X-Forward-For': '10.1.1.1' 31 | } 32 | 33 | expected = [] 34 | 35 | assert_success( 36 | requests.get(url, headers=headers), 37 | expected 38 | ) 39 | -------------------------------------------------------------------------------- /tests/benchmarks.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from __future__ import absolute_import 4 | from django.db import transaction 5 | import ipaddress 6 | import pytest 7 | import time 8 | 9 | from nsot import exc, models 10 | 11 | from .model_tests.fixtures import site 12 | 13 | 14 | @pytest.mark.django_db 15 | def test_create_1024(site): 16 | 17 | address = u'10.0.0.0/20' 18 | models.Network.objects.create(site=site, cidr=address) 19 | models.Attribute.objects.create( 20 | site=site, resource_name='Network', name='aaaa' 21 | ) 22 | 23 | start = time.time() 24 | network = ipaddress.ip_network(address) 25 | 26 | with transaction.atomic(): 27 | for ip in network.subnets(new_prefix=30): 28 | models.Network.objects.create( 29 | site=site, cidr=ip.exploded, attributes={'aaaa': 'value'} 30 | ) 31 | 32 | print('Finished in {} seconds.'.format(time.time() - start)) 33 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import os 3 | 4 | 5 | def pytest_report_header(config): 6 | """Customize the report header to display API version.""" 7 | api_version = os.getenv('NSOT_API_VERSION') 8 | return 'Using NSoT API version: %s' % api_version 9 | -------------------------------------------------------------------------------- /tests/model_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dropbox/nsot/941b11f84f5c0d210f638654a6ed34a5610af22a/tests/model_tests/__init__.py -------------------------------------------------------------------------------- /tests/model_tests/data/networks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "cidr": "10.20.0.0/16" 4 | }, 5 | { 6 | "cidr": "10.20.0.0/24" 7 | }, 8 | { 9 | "cidr": "10.20.1.0/24" 10 | }, 11 | { 12 | "cidr": "10.20.2.0/24" 13 | }, 14 | { 15 | "cidr": "10.20.3.0/24" 16 | }, 17 | { 18 | "cidr": "10.20.4.0/24" 19 | }, 20 | { 21 | "cidr": "10.20.5.0/24" 22 | }, 23 | { 24 | "cidr": "10.20.6.0/24" 25 | }, 26 | { 27 | "cidr": "10.20.7.0/24" 28 | }, 29 | { 30 | "cidr": "10.20.8.0/24" 31 | }, 32 | { 33 | "cidr": "10.20.9.0/24" 34 | }, 35 | { 36 | "cidr": "10.20.10.0/24" 37 | }, 38 | { 39 | "cidr": "10.20.11.0/24" 40 | }, 41 | { 42 | "cidr": "10.20.12.0/24" 43 | }, 44 | { 45 | "cidr": "10.20.13.0/24" 46 | }, 47 | { 48 | "cidr": "10.20.14.0/24" 49 | }, 50 | { 51 | "cidr": "10.20.15.0/24" 52 | }, 53 | { 54 | "cidr": "10.20.16.0/24" 55 | }, 56 | { 57 | "cidr": "10.20.17.0/24" 58 | }, 59 | { 60 | "cidr": "10.20.18.0/24" 61 | }, 62 | { 63 | "cidr": "10.20.19.0/24" 64 | }, 65 | { 66 | "cidr": "10.20.20.0/24" 67 | }, 68 | { 69 | "cidr": "10.20.21.0/24" 70 | }, 71 | { 72 | "cidr": "10.20.22.0/24" 73 | }, 74 | { 75 | "cidr": "10.20.23.0/24" 76 | }, 77 | { 78 | "cidr": "10.20.24.0/24" 79 | }, 80 | { 81 | "cidr": "10.20.25.0/24" 82 | }, 83 | { 84 | "cidr": "10.20.26.0/24" 85 | }, 86 | { 87 | "cidr": "10.20.27.0/24" 88 | }, 89 | { 90 | "cidr": "10.20.28.0/24" 91 | }, 92 | { 93 | "cidr": "10.20.29.0/24" 94 | }, 95 | { 96 | "cidr": "10.20.32.0/21" 97 | }, 98 | { 99 | "cidr": "10.20.40.0/21" 100 | }, 101 | { 102 | "cidr": "10.20.48.0/21" 103 | }, 104 | { 105 | "cidr": "10.20.56.0/21" 106 | }, 107 | { 108 | "cidr": "10.20.64.0/21" 109 | }, 110 | { 111 | "cidr": "10.20.72.0/21" 112 | }, 113 | { 114 | "cidr": "10.20.80.0/21" 115 | }, 116 | { 117 | "cidr": "10.20.88.0/21" 118 | }, 119 | { 120 | "cidr": "10.20.96.0/21" 121 | }, 122 | { 123 | "cidr": "10.20.104.0/21" 124 | }, 125 | { 126 | "cidr": "10.20.112.0/21" 127 | }, 128 | { 129 | "cidr": "10.20.120.0/21" 130 | }, 131 | { 132 | "cidr": "10.20.128.0/21" 133 | }, 134 | { 135 | "cidr": "10.20.136.0/21" 136 | }, 137 | { 138 | "cidr": "10.20.144.0/21" 139 | }, 140 | { 141 | "cidr": "10.20.152.0/21" 142 | }, 143 | { 144 | "cidr": "10.20.160.0/21" 145 | }, 146 | { 147 | "cidr": "10.20.168.0/21" 148 | }, 149 | { 150 | "cidr": "10.20.176.0/21" 151 | }, 152 | { 153 | "cidr": "10.20.184.0/21" 154 | }, 155 | { 156 | "cidr": "10.20.224.0/24" 157 | }, 158 | { 159 | "cidr": "10.20.225.0/24" 160 | }, 161 | { 162 | "cidr": "10.20.226.0/24" 163 | }, 164 | { 165 | "cidr": "10.20.227.0/24" 166 | }, 167 | { 168 | "cidr": "10.20.228.0/24" 169 | }, 170 | { 171 | "cidr": "10.20.229.0/24" 172 | }, 173 | { 174 | "cidr": "10.20.230.0/24" 175 | }, 176 | { 177 | "cidr": "10.20.231.0/24" 178 | } 179 | ] 180 | -------------------------------------------------------------------------------- /tests/model_tests/fixtures.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from django.contrib.auth.models import Group 3 | import pytest 4 | from pytest_django.fixtures import django_user_model, transactional_db 5 | import logging 6 | 7 | from nsot import models 8 | 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | @pytest.fixture 14 | def user(django_user_model): 15 | """Create and return a non-admin user.""" 16 | user = django_user_model.objects.create(email='user@localhost') 17 | return user 18 | 19 | 20 | @pytest.fixture 21 | def admin_user(django_user_model): 22 | """Create and return an admin user.""" 23 | user = django_user_model.objects.create( 24 | email='admin@localhost', is_superuser=True, is_staff=True 25 | ) 26 | return user 27 | 28 | 29 | @pytest.fixture 30 | def site(): 31 | """Create and return a Site object.""" 32 | site = models.Site.objects.create( 33 | name='Test Site', description='This is a Test Site.' 34 | ) 35 | return site 36 | 37 | 38 | @pytest.fixture 39 | def device(site): 40 | """Create and return a Device object bound to ``site``.""" 41 | device = models.Device.objects.create(site=site, hostname='foo-bar1') 42 | return device 43 | 44 | 45 | @pytest.fixture 46 | def circuit(site): 47 | """Create and return a Circuit object bound to ``site``.""" 48 | device_a = models.Device.objects.create(site=site, hostname='foo-bar1') 49 | device_z = models.Device.objects.create(site=site, hostname='foo-bar2') 50 | 51 | # Create a network for interface assignments 52 | network = models.Network.objects.create( 53 | cidr='10.32.0.0/24', site=site, 54 | ) 55 | 56 | # Create A/Z-side interfaces 57 | iface_a = models.Interface.objects.create( 58 | device=device_a, name='eth0', addresses=['10.32.0.1/32'] 59 | ) 60 | iface_z = models.Interface.objects.create( 61 | device=device_z, name='eth0', addresses=['10.32.0.2/32'] 62 | ) 63 | 64 | # Create the circuit 65 | circuit = models.Circuit.objects.create( 66 | endpoint_a=iface_a, endpoint_z=iface_z 67 | ) 68 | return circuit 69 | 70 | 71 | @pytest.fixture 72 | def test_group(): 73 | """Create and return a Group object.""" 74 | test_group = Group.objects.create(name='test_group') 75 | return test_group 76 | -------------------------------------------------------------------------------- /tests/model_tests/test_changes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | import pytest 6 | 7 | from django.db import IntegrityError 8 | from django.db.models import ProtectedError 9 | from django.core.exceptions import ValidationError as DjangoValidationError 10 | import ipaddress 11 | import json 12 | import logging 13 | import re 14 | 15 | from nsot import exc, models 16 | 17 | from .fixtures import device, user, site 18 | from six.moves import zip 19 | 20 | 21 | # Allow everything in there to access the DB 22 | pytestmark = pytest.mark.django_db 23 | 24 | 25 | @pytest.fixture 26 | def create(device, user): 27 | models.Change.objects.create(event='Create', obj=device, user=user) 28 | 29 | 30 | def test_diff_device_hostname(create, device, user): 31 | device.hostname = 'foo-bar3' 32 | device.save() 33 | update = models.Change.objects.create(event='Update', obj=device, 34 | user=user) 35 | 36 | assert '- "hostname": "foo-bar1"' in update.diff 37 | assert '+ "hostname": "foo-bar3"' in update.diff 38 | 39 | 40 | def test_diff_noop(create, device, user): 41 | update = models.Change.objects.create(event='Update', obj=device, 42 | user=user) 43 | 44 | blob = json.dumps(update.resource, indent=2, sort_keys=True) 45 | 46 | for line_a, line_b in zip(update.diff.splitlines(), blob.splitlines()): 47 | assert line_a.strip() == line_b.strip() 48 | 49 | 50 | def test_diff_delete(create, device, user): 51 | delete = models.Change.objects.create(event='Delete', obj=device, 52 | user=user) 53 | blob = json.dumps(delete.resource, indent=2, sort_keys=True) 54 | 55 | for line_a, line_b in zip(delete.diff.splitlines(), blob.splitlines()): 56 | assert line_a == '- ' + line_b 57 | -------------------------------------------------------------------------------- /tests/model_tests/test_devices.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | import pytest 6 | # Allow everything in there to access the DB 7 | pytestmark = pytest.mark.django_db 8 | 9 | from django.db import IntegrityError 10 | from django.db.models import ProtectedError 11 | from django.core.exceptions import ValidationError as DjangoValidationError 12 | import logging 13 | 14 | from nsot import exc, models 15 | 16 | from .fixtures import admin_user, user, site, transactional_db 17 | 18 | 19 | def test_device_attributes(site): 20 | models.Attribute.objects.create( 21 | site=site, 22 | resource_name='Device', name='owner' 23 | ) 24 | 25 | device = models.Device.objects.create( 26 | site=site, hostname='foobarhost', attributes={'owner': 'gary'} 27 | ) 28 | 29 | assert device.get_attributes() == {'owner': 'gary'} 30 | 31 | # Verify property successfully zeros out attributes 32 | device.set_attributes({}) 33 | assert device.get_attributes() == {} 34 | 35 | with pytest.raises(exc.ValidationError): 36 | device.set_attributes(None) 37 | 38 | with pytest.raises(exc.ValidationError): 39 | device.set_attributes({0: 'value'}) 40 | 41 | with pytest.raises(exc.ValidationError): 42 | device.set_attributes({'key': 0}) 43 | 44 | with pytest.raises(exc.ValidationError): 45 | device.set_attributes({'made_up': 'value'}) 46 | 47 | 48 | def test_retrieve_device(site): 49 | models.Attribute.objects.create( 50 | site=site, 51 | resource_name='Device', name='test' 52 | ) 53 | 54 | device1 = models.Device.objects.create( 55 | site=site, hostname='device1', 56 | attributes={'test': 'foo'} 57 | ) 58 | device2 = models.Device.objects.create( 59 | site=site, hostname='device2', 60 | attributes={'test': 'bar'} 61 | ) 62 | device3 = models.Device.objects.create( 63 | site=site, hostname='device3' 64 | ) 65 | 66 | assert list(site.devices.all()) == [device1, device2, device3] 67 | 68 | # Filter by attributes 69 | assert list(site.devices.by_attribute(None, 'foo')) == [] 70 | assert list(site.devices.by_attribute('test', 'foo')) == [device1] 71 | 72 | 73 | def test_validation(site, transactional_db): 74 | with pytest.raises(exc.ValidationError): 75 | models.Device.objects.create( 76 | site=site, hostname=None, 77 | ) 78 | 79 | with pytest.raises(exc.ValidationError): 80 | models.Device.objects.create( 81 | site=site, hostname='a b', 82 | ) 83 | 84 | device = models.Device.objects.create( 85 | site=site, hostname='testhost' 86 | ) 87 | 88 | with pytest.raises(exc.ValidationError): 89 | device.hostname = '' 90 | device.save() 91 | 92 | with pytest.raises(exc.ValidationError): 93 | device.hostname = None 94 | device.save() 95 | 96 | device.hostname = 'newtesthostname' 97 | device.save() 98 | -------------------------------------------------------------------------------- /tests/model_tests/test_middleware_auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | from django.contrib.auth.models import Group 6 | from guardian.shortcuts import assign_perm 7 | import pytest 8 | # Allow everything in there to access the DB 9 | pytestmark = pytest.mark.django_db 10 | 11 | import logging 12 | 13 | from nsot import exc, models 14 | from nsot.middleware.auth import NsotObjectPermissionsBackend 15 | 16 | from .fixtures import test_group, user, site 17 | 18 | def test_object_level_permissions_with_ancestors(site, user, test_group): 19 | """Test to check object level permissions for objects with a 20 | ``get_ancestors`` method implementation""" 21 | net_8 = models.Network.objects.create(site=site, cidr=u'8.0.0.0/8') 22 | net_24 = models.Network.objects.create(site=site, cidr=u'8.0.0.0/24') 23 | net_16 = models.Network.objects.create(site=site, cidr=u'8.0.0.0/16') 24 | 25 | # Need to refresh the objects from the db so the updated parent_ids are 26 | # reflected. 27 | net_8.refresh_from_db() 28 | net_24.refresh_from_db() 29 | net_16.refresh_from_db() 30 | 31 | user.groups.add(test_group) 32 | 33 | check_perms = NsotObjectPermissionsBackend() 34 | assert check_perms.has_perm(user, 'delete_network', net_24) is False 35 | 36 | assign_perm('delete_network', user, net_8) 37 | assert check_perms.has_perm(user, 'delete_network', net_8) is True 38 | assert check_perms.has_perm(user, 'delete_network', net_24) is True 39 | -------------------------------------------------------------------------------- /tests/model_tests/test_sites.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | import pytest 6 | # Allow everything in there to access the DB 7 | pytestmark = pytest.mark.django_db 8 | 9 | from django.db import IntegrityError 10 | from django.db.models import ProtectedError 11 | from django.core.exceptions import ValidationError as DjangoValidationError 12 | import logging 13 | 14 | from nsot import exc, models 15 | 16 | from .fixtures import user, transactional_db 17 | 18 | 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | def test_site_creation(): 23 | site = models.Site.objects.create( 24 | name='Test Site', 25 | description='This is a Test Site.' 26 | ) 27 | sites = models.Site.objects.all() 28 | 29 | assert sites.count() == 1 30 | assert sites[0].id == site.id 31 | assert sites[0].name == site.name 32 | assert sites[0].description == site.description 33 | 34 | 35 | def test_site_conflict(transactional_db): 36 | models.Site.objects.create( 37 | name='Test Site', 38 | description='This is a Test Site.' 39 | ) 40 | 41 | with pytest.raises(DjangoValidationError): 42 | models.Site.objects.create( 43 | name='Test Site', 44 | description='This is a Test Site.' 45 | ) 46 | 47 | models.Site.objects.create( 48 | name='Test Site 2', 49 | description='This is a Test Site.' 50 | ) 51 | 52 | 53 | def test_site_validation(transactional_db): 54 | with pytest.raises(exc.ValidationError): 55 | models.Site.objects.create( 56 | name=None, 57 | description='This is a Test Site.' 58 | ) 59 | 60 | with pytest.raises(exc.ValidationError): 61 | models.Site.objects.create( 62 | name='', 63 | description='This is a Test Site.' 64 | ) 65 | 66 | site = models.Site.objects.create( 67 | name='Test Site', 68 | description='This is a Test Site.' 69 | ) 70 | 71 | with pytest.raises(exc.ValidationError): 72 | site.name = '' 73 | site.save() 74 | 75 | with pytest.raises(exc.ValidationError): 76 | site.name = None 77 | site.save() 78 | 79 | site.name = 'Test Site New' 80 | site.save() 81 | -------------------------------------------------------------------------------- /tests/model_tests/test_values.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from __future__ import absolute_import 5 | import pytest 6 | # Allow everything in there to access the DB 7 | pytestmark = pytest.mark.django_db 8 | 9 | from django.db import IntegrityError 10 | from django.db.models import ProtectedError 11 | from django.core.exceptions import (ValidationError as DjangoValidationError, 12 | MultipleObjectsReturned) 13 | import logging 14 | 15 | from nsot import exc, models 16 | 17 | from .fixtures import admin_user, user, site, transactional_db 18 | 19 | 20 | def test_creation(site): 21 | """Test explicit value creation.""" 22 | attr = models.Attribute.objects.create( 23 | resource_name='Device', 24 | site=site, name='test_attribute' 25 | ) 26 | dev = models.Device.objects.create( 27 | hostname='foo-bar1', site=site 28 | ) 29 | 30 | # Explicitly create a Value without providing site_id 31 | val = models.Value.objects.create( 32 | obj=dev, attribute=attr, value='foo' 33 | ) 34 | 35 | # Value site should match attribute 36 | assert val.site == attr.site 37 | 38 | # Device attributes should match a simple dict 39 | dev.clean_attributes() 40 | dev.save() 41 | assert dev.get_attributes() == {'test_attribute': 'foo'} 42 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | General purpose utilities for unit-testing. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | import json 7 | import os 8 | 9 | 10 | __all__ = ('load_json',) 11 | 12 | 13 | def load_json(relpath): 14 | """ 15 | Load JSON files relative to ``tests`` directory. 16 | 17 | Files are loaded from the 'data' directory. So for example for 18 | ``/path/to/data/devices/foo.json`` the ``relpath`` would be 19 | ``data/devices/foo.json``. 20 | 21 | :param relpath: 22 | Relative path to our directory's "data" dir 23 | """ 24 | our_path = os.path.dirname(os.path.abspath(__file__)) 25 | filepath = os.path.join(our_path, relpath) 26 | with open(filepath, 'rb') as f: 27 | return json.load(f) 28 | -------------------------------------------------------------------------------- /vagrant/README.rst: -------------------------------------------------------------------------------- 1 | ####### 2 | Vagrant 3 | ####### 4 | 5 | The `Vagrantfile` in the root of this repo creates a fresh Vagrant box running 6 | Ubuntu and NSoT. 7 | 8 | Prerequisites 9 | ============= 10 | 11 | To proceed you must have working installations of Vagrant and Virtualbox on 12 | your machine. If you already have these, you may skip this step. 13 | 14 | If you do not have a working Vagrant environment configured along with 15 | Virtualbox, please follow the `Vagrant's "Getting Started" instructions 16 | `_ before proceeding. 17 | 18 | Instructions 19 | ============ 20 | 21 | Provision the box 22 | ----------------- 23 | 24 | *5-10 minutes on a fast connection* 25 | 26 | To provision the virtual machine open a command prompt, and run the 27 | following command from this directory: 28 | 29 | .. code-block:: bash 30 | 31 | $ vagrant up 32 | 33 | This will build a new Vagrant box, and pre-install NSoT for you. 34 | 35 | Launch NSoT 36 | ----------- 37 | 38 | Login to the new virtual machine via ssh: 39 | 40 | .. code-block:: bash 41 | 42 | $ vagrant ssh 43 | 44 | Start the server on ``8990/tcp`` (the default) and create a superuser when 45 | prompted: 46 | 47 | .. code-block:: bash 48 | 49 | $ nsot-server start 50 | 51 | Point your browser to http://192.168.33.11:8990 and login! 52 | 53 | Now you are ready to follow the :doc:`../tutorial` to start playing around. 54 | --------------------------------------------------------------------------------