Giter Site home page Giter Site logo

vapor-ware / netbox-virtual-circuit-plugin Goto Github PK

View Code? Open in Web Editor NEW
55.0 13.0 19.0 1.07 MB

A plugin for NetBox that supports Virtual Circuit management

License: GNU General Public License v3.0

Python 70.13% HTML 22.23% Makefile 2.50% Dockerfile 1.11% Shell 4.03%
netbox netbox-plugin

netbox-virtual-circuit-plugin's Introduction

NetBox Virtual Circuit Plugin

A plugin for NetBox that supports Virtual Circuit management.

Definitions

A Virtual Circuit is a circuit created by routing two or more VLANs together.

Each Virtual Circuit is identified by a name, a numeric ID (0-32767), along with a context. Each Virtual Circuit must be assigned one of the following operational statuses:

  • Pending Configuration
  • Configured
  • Pending Deletion
  • Configuration Error

When a VLAN is assigned to a Virtual Circuit, it can not exist in another Virtual Circuit without first being removed.

Installing

Since the plugin is published on PyPI, simply issue:

pip install netbox-virtual-circuit-plugin

to download and install it.

To enable to plugin, add the plugin's name to the PLUGINS list in configuration.py like so:

PLUGINS = ['netbox_virtual_circuit_plugin'] # Note that the name here use underscore, not hyphen.

Don't forget to restart NetBox to load the new plugin.

You might also have to manually run the database migrations for Netbox to create the appropriate tables for virtual circuits.

python3 manage.py migrate

For more information about installing plugins, refer to NetBox's documentation.

Compatibility

Below is a table describing the compatibility of various NetBox versions with NetBox Virtual Circuit Plugin versions.

Netbox 2.8 NetBox 2.9 Netbox 2.10 Netbox 2.11 Netbox 3.0 Netbox 3.1
1.0.x x x x
1.1.x to 1.5.x x x x
1.6.x x x x
1.7.x x x x
2.0.0 x x

Using

Once the plugin is installed correctly as instructed above, one can find the Virtual Circuit section under Plugins navigation tab via NetBox UI that is ready to use with correct admin permission.

As for REST API use cases, the 2 group endpoints are exposed at:

  • /api/plugins/virtual-circuit/virtual-circuits
  • /api/plugins/virtual-circuit/vlans

While the former one is for creating/retrieving/modifying/deleting Virtual Circuits, the later one is for assigning and managing Virtual-Circuit-to-VLAN connections. For more information, refer to /api/docs as it also conforms to Swagger Specification for hosted visual documentations.

Developing

Plugins are essentially self-contained Django apps which integrate with NetBox to provide custom functionality. For more information, see NetBox documentation.

To help setup the development environment, it comes with a CLI helper based on Makefile, including the following commands:

changelog        Generate a changelog file from GitHub Issue Tracker
clean            Clean up build artifacts
deploy           Run a local development deployment of the plugin with NetBox
docker           Build a local docker image
github-tag       Create and push a tag with the current version
help             Print usage information
migrate          Run makemigrations in Django and produce a migration file locally
release          Package and distribute the current release to PyPI
test             Run unit tests
version          Print the version

That said, one can simply build and run a local development image of the plugin with NetBox with a single line:

make build && make deploy

The application will be available after a few minutes at http://0.0.0.0:8000/. The default credentials are:

  • Username: admin
  • Password: admin
  • API Token: 0123456789abcdef0123456789abcdef01234567

Below are several screenshots of the UI:

Navigation view

Add a Virtual Circuit

List all Virtual Circuits

Assign a VLAN to a Virtual Circuit

Virtual Circuit Single View (2 VLANs assigned)

List of all connections

Contributing

If you experience a bug, would like to ask a question, or request a feature, open a new issue and provide as much context as possible. All contributions, questions, and feedback are welcomed and appreciated.

License

NetBox Virtual Circuit Plugin is licensed under GPLv3. See LICENSE for more info.

netbox-virtual-circuit-plugin's People

Contributors

hoanhan101 avatar jinaloo7 avatar marcoceppi avatar ndom91 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

netbox-virtual-circuit-plugin's Issues

Filters no longer working

Looks like netbox has completely removed the NameSlugSearchFilterSet class from their utilities.filters. Would it be possible to get this patched? At current it doesn't seem possible to run this plugin until the filters file has been updated to reflect this.

Thanks!

Support for Netbox 2.11

Currently it is not supported for version 2.11 of Netbox. I need to install it with pip3. When i do i get a lot off error's.
Could you give a timeline when this plugin will be supported for the version of Netbox?

Server error when clicking on the plugin

I am getting the below server error when clicking on the virtual circuit from the plugins tab. Clicking on the + icon opens without any issue.

<class 'django.db.utils.ProgrammingError'>

relation "netbox_virtual_circuit_plugin_virtualcircuit" does not exist
LINE 1: SELECT COUNT(*) AS "__count" FROM "netbox_virtual_circuit_pl...
                                          ^


Python version: 3.7.9
NetBox version: 3.1.9


pip show netbox-virtual-circuit-plugin
Name: netbox-virtual-circuit-plugin
Version: 2.0.0
Summary: A Netbox plugin that supports Virtual Circuit management
Home-page: https://github.com/vapor-ware/netbox-virtual-circuit-plugin
Author: Hoanh An
Author-email:
License: GNU General Public License v3.0
Location: /opt/netbox-3.1.9/venv/lib/python3.7/site-packages
Requires:
Required-by:

Add of virtual circuit results in Django exception

I suspect this is related to #60, but can't confirm, I'm rather weak in Python and Django. However...

Fresh install (via docker) with this plugin and attempting to create virtual circuit results in this exception:

<class 'django.template.exceptions.TemplateDoesNotExist'>

base.html

Python version: 3.9.5
NetBox version: 3.0.1

I can't find a reference to base.html in the code, but who knows what I'm missing? I'll see if I can fix myself, and if so, expect a PR.

Netbox extras.models error

I kept getting error unable to load module from extras.models ChangeLoggedModel.

Looks like Netbox has updated this to be netbox.ChangeLoggedModel instead of extras.

Breaks Swagger API

It appears that this plugin is causing the Swagger API interface to bomb. Upon doing some investigation it seems like your VLAN serializer is using the same ref_name as the actual VLAN serializer.

"Schema for <class 'spam.api.serializers.VLANSerializer'> would override distinct serializer <class 'netbox_virtual_circuit_plugin.api.serializers.VLANSerializer'> because they implicitly share the same ref_name; explicitly set the ref_name atribute on both serializers' Meta classes"

Add `Deleted` status

Sometimes a customer might want to delete a Virtual Circuit but we don't want to delete it immediately in our database yet. What we can do instead is to mark it Deleted and manually delete it later.

Flatten the `vlans` list inside a virtual circuit object response

From #15 discussion, @marcoceppi is wondering if it is possible to flatten the vlans list inside a virtual circuit object response so that this

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "vcid": 101,
      "name": "curl",
      "status": "pending-configuration",
      "context": "",
      "vlans": [
        {
          "vlan": {
            "id": 1,
            "created": "2020-06-02",
            "last_updated": "2020-06-02T19:08:07.817046Z",
            "vid": 1,
            "name": "foo",
            "status": "active",
            "description": "",
            "site": null,
            "group": null,
            "tenant": null,
            "role": null
          }
        },
        {
          "vlan": {
            "id": 2,
            "created": "2020-06-02",
            "last_updated": "2020-06-02T19:08:12.453937Z",
            "vid": 2,
            "name": "bar",
            "status": "active",
            "description": "",
            "site": null,
            "group": null,
            "tenant": null,
            "role": null
          }
        }
      ]
    }
  ]
}

becomes

{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "vcid": 101,
      "name": "curl",
      "status": "pending-configuration",
      "context": "",
      "vlans": [
        {
          "id": 1,
          "created": "2020-06-02",
          "last_updated": "2020-06-02T19:08:07.817046Z",
          "vid": 1,
          "name": "foo",
          "status": "active",
          "description": "",
          "site": null,
          "group": null,
          "tenant": null,
          "role": null
        },
        {
          "id": 2,
          "created": "2020-06-02",
          "last_updated": "2020-06-02T19:08:12.453937Z",
          "vid": 2,
          "name": "bar",
          "status": "active",
          "description": "",
          "site": null,
          "group": null,
          "tenant": null,
          "role": null
        }
      ]
    }
  ]
}

instead?

However,

In this implementation, it seems like a vlan key is necessary for the serializer to traverse along and bring out the nested models.

Assigning a Virtual Circuit to 2 VLANs in a single API request

As in version 0.1.3, in order to assign a Virtual Circuit to 2 VLANs, one needs to call the API twice as the plugin only supports assigning 1 Virtual Circuit to 1 VLAN in 1 request. Behind the scene, Virtual-Circuit-to-VLAN connections/models are stored in a separate 2-column database table where VLAN ID is the primary key. That said, in order to have a Virtual Circuit connected to 2 VLANS, one way is to have 2 Virtual-Circuit-to-VLAN connections registered in the same table. As Django's Serializers directly serializes these models to native Python types that are renderable in JSON, this translates to 2 API requests that are able to register 2 connections in the table. This implementation is simple and straightforward enough as it does the job and can scale fine if in the future we want a Virtual Circuit to connect to more than 2 VLANs. The one downside that we (me, @marcoceppi and @jinaloo7) agree on is that it takes 2 API requests to assign 2 VLANs that eventually make up a meaningful Virtual Circuit. It would be ideal if we could assign 2 VLANs in a single request.

Another way to have a Virtual Circuit connected to 2 VLANS is to modify our models so that we have a 3-column table instead of a 2-column one, where 1 Virtual Circuit must hold 2 VLANs connections. This way we don't mess with Django's Serializers but still end up with an API endpoint that is able to register 2 VLANs using a single request. However, in order to have a 3-column table that satisfies our uniqueness constraints (when a VLAN is assigned to a Virtual Circuit, it can not exist in another Virtual Circuit without first being removed), both VLANs fields must be primary keys. This is not currently supported in Django and also, there is still no good way to get around it.

Let me know if you have better ideas on how to go about this. As for me, I think it's fine to keep what we have as it is now so it doesn't block some work over the Network API. And since we have more control of the Network API as we build one from scratch, a possible workaround is to combine 2 API calls to the plugin in a single call so that the consumer of Network API only needs to call it once.

VCID modeling problem

As per networkapi specs the maximum allowed vcid number should be 4294967295(2^32-1) rather then present maximum number which is 32767.

Plugin netbox-virtual-circuit-plugin does not provide a 'config' variable.

Hiya there,

I'm seeing issues trying to install the plugin.

Using PIP, Everything goes smoothly up until the point where I add the plugin to configuration.py and restart the Netbox services, with this reporting that it cannot import the module. I've added this to the local_requirements.txt then, and ran upgrade.sh, and that results in the same error after enabling in configuration.py

I then cloned the git, and ran the python3 setup.py install command, which install absolutely fine and the logging shows it's successfully built the egg and put it in the venv python dist-packages folder, however now it gets reported that Plugin netbox-virtual-circuit-plugin does not provide a 'config' variable.

I know this isn't true as I've gone and taken a look at the init.py, and I can see it there. However, I do have another plugin that installs absolutely fine, and has a similar thing to yourselves (in the way of the install method of using plugins, netbox_topology_viewer) so I'm sure there's something amiss here that's not clear in logs here.

Here's the error log.

May 15 14:11:10 ipam gunicorn[8515]: Traceback (most recent call last):
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/netbox/netbox/settings.py", line 661, in <module>
May 15 14:11:10 ipam gunicorn[8515]:     plugin_config = plugin.config
May 15 14:11:10 ipam gunicorn[8515]: AttributeError: module 'netbox-virtual-circuit-plugin' has no attribute 'config'
May 15 14:11:10 ipam gunicorn[8515]: During handling of the above exception, another exception occurred:
May 15 14:11:10 ipam gunicorn[8515]: Traceback (most recent call last):
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/gunicorn/arbiter.py", line 583, in spawn_worker
May 15 14:11:10 ipam gunicorn[8515]:     worker.init_process()
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/gunicorn/workers/gthread.py", line 92, in init_process
May 15 14:11:10 ipam gunicorn[8515]:     super().init_process()
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/gunicorn/workers/base.py", line 119, in init_process
May 15 14:11:10 ipam gunicorn[8515]:     self.load_wsgi()
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/gunicorn/workers/base.py", line 144, in load_wsgi
May 15 14:11:10 ipam gunicorn[8515]:     self.wsgi = self.app.wsgi()
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/gunicorn/app/base.py", line 67, in wsgi
May 15 14:11:10 ipam gunicorn[8515]:     self.callable = self.load()
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/gunicorn/app/wsgiapp.py", line 49, in load
May 15 14:11:10 ipam gunicorn[8515]:     return self.load_wsgiapp()
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/gunicorn/app/wsgiapp.py", line 39, in load_wsgiapp
May 15 14:11:10 ipam gunicorn[8515]:     return util.import_app(self.app_uri)
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/gunicorn/util.py", line 358, in import_app
May 15 14:11:10 ipam gunicorn[8515]:     mod = importlib.import_module(module)
May 15 14:11:10 ipam gunicorn[8515]:   File "/usr/lib/python3.7/importlib/__init__.py", line 127, in import_module
May 15 14:11:10 ipam gunicorn[8515]:     return _bootstrap._gcd_import(name[level:], package, level)
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 983, in _find_and_load
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap_external>", line 728, in exec_module
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/netbox/netbox/wsgi.py", line 7, in <module>
May 15 14:11:10 ipam gunicorn[8515]:     application = get_wsgi_application()
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/django/core/wsgi.py", line 12, in get_wsgi_application
May 15 14:11:10 ipam gunicorn[8515]:     django.setup(set_prefix=False)
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/django/__init__.py", line 19, in setup
May 15 14:11:10 ipam gunicorn[8515]:     configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/django/conf/__init__.py", line 76, in __getattr__
May 15 14:11:10 ipam gunicorn[8515]:     self._setup(name)
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/django/conf/__init__.py", line 63, in _setup
May 15 14:11:10 ipam gunicorn[8515]:     self._wrapped = Settings(settings_module)
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/venv/lib/python3.7/site-packages/django/conf/__init__.py", line 142, in __init__
May 15 14:11:10 ipam gunicorn[8515]:     mod = importlib.import_module(self.SETTINGS_MODULE)
May 15 14:11:10 ipam gunicorn[8515]:   File "/usr/lib/python3.7/importlib/__init__.py", line 127, in import_module
May 15 14:11:10 ipam gunicorn[8515]:     return _bootstrap._gcd_import(name[level:], package, level)
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 983, in _find_and_load
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap_external>", line 728, in exec_module
May 15 14:11:10 ipam gunicorn[8515]:   File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
May 15 14:11:10 ipam gunicorn[8515]:   File "/srv/netbox/netbox/netbox/settings.py", line 666, in <module>
May 15 14:11:10 ipam gunicorn[8515]:     "and point to the PluginConfig subclass.".format(plugin_name)
May 15 14:11:10 ipam gunicorn[8515]: django.core.exceptions.ImproperlyConfigured: Plugin netbox-virtual-circuit-plugin does not provide a 'config' variable. This should be defined in the plugin's __init__.py file and point to the PluginConfig
May 15 14:11:10 ipam gunicorn[8515]: [2020-05-15 13:11:10 +0000] [8525] [INFO] Worker exiting (pid: 8525)

Should this be published on PyPI?

Picking up from the comment in #4, do we want to publish the plugin on PyPI at all?

A good point @edaniszewski mentioned is that since we're gonna use it internally only, it might not be necessary to be published on PyPI, though there might be other use cases that we haven't looked into yet.

An argument for publishing the plugin is that it would save us a ton of time to install and enable it in Netbox. However, there could be another way to do so too without having it published.

Regardless, it's up at https://pypi.org/project/netbox-virtual-circuit-plugin/ to play around with.

Netbox 3.6 support

Hi,

does anybody use this plugin with netbox 3.6? how to fix installation errors?

Add support for VC's between interfaces

Hi vapor-ware team,

Great plugin. This is something I've been looking for in netbox for sometime.
In an MPLS environment we generally pop the tags and create virtual circuits between interfaces / sub-interfaces.
Would you look to expand the feature set to support the creation of VC's between interfaces? Or would you accept a PR that creates this functionality?

I understand if you would prefer to keep this plugin limited to your own use case, but happy to contribute should you be willing to expand it.

Thanks

Add Scope to list and selection

Please add Scope (Site and VLAN Group) to list and selection.

Our VLAN ID and names are the same at the different sites.
This makes the selection very difficult at the moment

cannot import name 'BulkDeleteView' from 'utilities.views'

Hello,
I have installation error:

(venv) root@netbox:/opt/netbox/netbox# python3 manage.py migrate
Traceback (most recent call last):
  File "manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 395, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/base.py", line 330, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/base.py", line 371, in execute
    output = self.handle(*args, **options)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/base.py", line 85, in wrapped
    res = handle_func(*args, **kwargs)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/commands/migrate.py", line 75, in handle
    self.check(databases=[database])
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/base.py", line 396, in check
    databases=databases,
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/checks/registry.py", line 70, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/checks/urls.py", line 13, in check_url_config
    return check_resolver(resolver)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/checks/urls.py", line 23, in check_resolver
    return check_method()
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/urls/resolvers.py", line 408, in check
    for pattern in self.url_patterns:
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/urls/resolvers.py", line 589, in url_patterns
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/urls/resolvers.py", line 582, in urlconf_module
    return import_module(self.urlconf_name)
  File "/usr/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/opt/netbox/netbox/netbox/urls.py", line 8, in <module>
    from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
  File "/opt/netbox/netbox/extras/plugins/urls.py", line 28, in <module>
    urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
  File "/opt/netbox/netbox/extras/plugins/utils.py", line 31, in import_object
    spec.loader.exec_module(module)
  File "/opt/netbox/venv/lib/python3.7/site-packages/netbox_virtual_circuit_plugin/urls.py", line 2, in <module>
    from . import views
  File "/opt/netbox/venv/lib/python3.7/site-packages/netbox_virtual_circuit_plugin/views.py", line 4, in <module>
    from utilities.views import BulkDeleteView, ObjectEditView, ObjectListView, ObjectDeleteView
ImportError: cannot import name 'BulkDeleteView' from 'utilities.views' (/opt/netbox/netbox/utilities/views.py)

Python version: 3.7.3
NetBox version: 2.10.3

Latest version of plugin have a minor bug while getting device information

in latest VCID plugin the template of vcid is not resolving the device information of the vlan it is associated.
The issue is generated from this template

<td><a href="{% url 'dcim:device_list' %}{{ vlan.get_interfaces.0.parent.id }}">{{ vlan.get_interfaces.0.parent }}</a></td>
where it is not able to find the appropriate device.

Screen Shot 2021-09-13 at 6 25 08 PM

Solution:

Need to update the template to query the device information properly.

cannot import name 'BulkDeleteView' from 'utilities.views'

Hello,
I have installation error:

(venv) root@netbox:/opt/netbox/netbox# python3 manage.py migrate
Traceback (most recent call last):
  File "manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 395, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/base.py", line 330, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/base.py", line 371, in execute
    output = self.handle(*args, **options)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/base.py", line 85, in wrapped
    res = handle_func(*args, **kwargs)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/commands/migrate.py", line 75, in handle
    self.check(databases=[database])
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/management/base.py", line 396, in check
    databases=databases,
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/checks/registry.py", line 70, in run_checks
    new_errors = check(app_configs=app_configs, databases=databases)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/checks/urls.py", line 13, in check_url_config
    return check_resolver(resolver)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/core/checks/urls.py", line 23, in check_resolver
    return check_method()
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/urls/resolvers.py", line 408, in check
    for pattern in self.url_patterns:
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/urls/resolvers.py", line 589, in url_patterns
    patterns = getattr(self.urlconf_module, "urlpatterns", self.urlconf_module)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/opt/netbox/venv/lib/python3.7/site-packages/django/urls/resolvers.py", line 582, in urlconf_module
    return import_module(self.urlconf_name)
  File "/usr/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/opt/netbox/netbox/netbox/urls.py", line 8, in <module>
    from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
  File "/opt/netbox/netbox/extras/plugins/urls.py", line 28, in <module>
    urlpatterns = import_object(f"{plugin_path}.urls.urlpatterns")
  File "/opt/netbox/netbox/extras/plugins/utils.py", line 31, in import_object
    spec.loader.exec_module(module)
  File "/opt/netbox/venv/lib/python3.7/site-packages/netbox_virtual_circuit_plugin/urls.py", line 2, in <module>
    from . import views
  File "/opt/netbox/venv/lib/python3.7/site-packages/netbox_virtual_circuit_plugin/views.py", line 4, in <module>
    from utilities.views import BulkDeleteView, ObjectEditView, ObjectListView, ObjectDeleteView
ImportError: cannot import name 'BulkDeleteView' from 'utilities.views' (/opt/netbox/netbox/utilities/views.py)

Python version: 3.7.3
NetBox version: 2.10.3

Plugin fails jsonschema/open api validation

When attempting to access the list endpoints with aionetbox the results are a failure in validation schema:

>>> from aionetbox import AIONetbox                                                                                                                                                                             
>>> c = AIONetbox.from_openapi(url='...', api_key='...')
>>> result_interfaces = await c.dcim.dcim_interfaces_list()
>>> result_interfaces.count
760
>>> result_vclist = await c.plugins.plugins_virtual_circuit_virtual_circuits_list()
InvalidResponse: Response did not include required "id"

Traceback:

~/Projects/vapor-ware/aionetbox/aionetbox/api.py in __call__(self, **kwargs)
    372         """
    373         try:
--> 374             return await self.request(**kwargs)
    375         except MissingRequiredParam as e:
    376             raise AttributeError(str(e))

~/Projects/vapor-ware/aionetbox/aionetbox/api.py in request(self, **kwargs)
    317                     query[kw] = kwargs.get(kw)
    318 
--> 319         output = await self._request(path, query, body)
    320 
    321         if self.operation_method != 'list':

~/Projects/vapor-ware/aionetbox/aionetbox/api.py in _request(self, path, query, body)
    295         resp.raise_for_status()
    296         data = await resp.json()
--> 297         return NetboxResponseObject.from_response(
    298             data=data,
    299             **self.config.get('responses', {}).get(str(resp.status), {}).get('schema', {})

~/Projects/vapor-ware/aionetbox/aionetbox/api.py in from_response(cls, data, **kwargs)
    421                 # If we have an array of objects, make sure the value is iterable, then produce a list of objects
    422                 if isinstance(val, Iterable):
--> 423                     value = [cls.from_response(data=v, **spec.get('items')) for v in val]
    424 
    425             setattr(output, key, value)

~/Projects/vapor-ware/aionetbox/aionetbox/api.py in <listcomp>(.0)
    421                 # If we have an array of objects, make sure the value is iterable, then produce a list of objects
    422                 if isinstance(val, Iterable):
--> 423                     value = [cls.from_response(data=v, **spec.get('items')) for v in val]
    424 
    425             setattr(output, key, value)

~/Projects/vapor-ware/aionetbox/aionetbox/api.py in from_response(cls, data, **kwargs)
    421                 # If we have an array of objects, make sure the value is iterable, then produce a list of objects
    422                 if isinstance(val, Iterable):
--> 423                     value = [cls.from_response(data=v, **spec.get('items')) for v in val]
    424 
    425             setattr(output, key, value)

~/Projects/vapor-ware/aionetbox/aionetbox/api.py in <listcomp>(.0)
    421                 # If we have an array of objects, make sure the value is iterable, then produce a list of objects
    422                 if isinstance(val, Iterable):
--> 423                     value = [cls.from_response(data=v, **spec.get('items')) for v in val]
    424 
    425             setattr(output, key, value)

~/Projects/vapor-ware/aionetbox/aionetbox/api.py in from_response(cls, data, **kwargs)
    404             # Check for all required response parameters
    405             if req not in data:
--> 406                 raise InvalidResponse('Response did not include required "{}"'.format(req))
    407 
    408         # if type == 'array':

InvalidResponse: Response did not include required "id"

It appears the schema says that id is required in the response object but not provided. Tracing the code, it's trying to parse VCVLAN:

{
  "paths":{
      "/plugins/virtual-circuit/virtual-circuits/":{
         "get":{
            "operationId":"plugins_virtual-circuit_virtual-circuits_list",
            "description":"",
            "parameters":[
               {
                  "name":"limit",
                  "in":"query",
                  "description":"Number of results to return per page.",
                  "required":false,
                  "type":"integer"
               },
               {
                  "name":"offset",
                  "in":"query",
                  "description":"The initial index from which to return the results.",
                  "required":false,
                  "type":"integer"
               }
            ],
            "responses":{
               "200":{
                  "description":"",
                  "schema":{
                     "required":[
                        "count",
                        "results"
                     ],
                     "type":"object",
                     "properties":{
                        "count":{
                           "type":"integer"
                        },
                        "next":{
                           "type":"string",
                           "format":"uri",
                           "x-nullable":true
                        },
                        "previous":{
                           "type":"string",
                           "format":"uri",
                           "x-nullable":true
                        },
                        "results":{
                           "type":"array",
                           "items":{
                              "$ref":"#/definitions/VirtualCircuit"
                           }
                        }
                     }
                  }
               }
            },
            "tags":[
               "plugins"
            ]
         },
         "post":{
            "operationId":"plugins_virtual-circuit_virtual-circuits_create",
            "description":"",
            "parameters":[
               {
                  "name":"data",
                  "in":"body",
                  "required":true,
                  "schema":{
                     "$ref":"#/definitions/VirtualCircuit"
                  }
               }
            ],
            "responses":{
               "201":{
                  "description":"",
                  "schema":{
                     "$ref":"#/definitions/VirtualCircuit"
                  }
               }
            },
            "tags":[
               "plugins"
            ]
         },
         "parameters":[

         ]
      },
   },
   "definitions":{
      "VCVLAN":{
         "required":[
            "id"
         ],
         "type":"object",
         "properties":{
            "id":{
               "title":"Id",
               "type":"integer"
            },
            "vlan":{
               "required":[
                  "vid",
                  "name",
                  "group"
               ],
               "type":"object",
               "properties":{
                  "id":{
                     "title":"ID",
                     "type":"integer",
                     "readOnly":true
                  },
                  "created":{
                     "title":"Created",
                     "type":"string",
                     "format":"date",
                     "readOnly":true
                  },
                  "last_updated":{
                     "title":"Last updated",
                     "type":"string",
                     "format":"date-time",
                     "readOnly":true
                  },
                  "vid":{
                     "title":"ID",
                     "type":"integer",
                     "maximum":4094,
                     "minimum":1
                  },
                  "name":{
                     "title":"Name",
                     "type":"string",
                     "maxLength":64,
                     "minLength":1
                  },
                  "status":{
                     "title":"Status",
                     "type":"string",
                     "enum":[
                        "active",
                        "reserved",
                        "deprecated"
                     ]
                  },
                  "description":{
                     "title":"Description",
                     "type":"string",
                     "maxLength":200
                  },
                  "site":{
                     "title":"Site",
                     "type":"integer",
                     "x-nullable":true
                  },
                  "group":{
                     "title":"Group",
                     "type":"integer"
                  },
                  "tenant":{
                     "title":"Tenant",
                     "type":"integer",
                     "x-nullable":true
                  },
                  "role":{
                     "title":"Role",
                     "type":"integer",
                     "x-nullable":true
                  }
               },
               "readOnly":true
            }
         }
      },
      "VirtualCircuit":{
         "required":[
            "vcid",
            "name",
            "vlans"
         ],
         "type":"object",
         "properties":{
            "vcid":{
               "title":"ID",
               "type":"integer",
               "maximum":4294967295,
               "minimum":1
            },
            "name":{
               "title":"Name",
               "type":"string",
               "maxLength":64,
               "minLength":1
            },
            "status":{
               "title":"Status",
               "type":"string",
               "enum":[
                  "pending-configuration",
                  "configured",
                  "configuration-error",
                  "pending-deactivation",
                  "deactivated"
               ]
            },
            "context":{
               "title":"Context",
               "type":"string",
               "maxLength":100
            },
            "vlans":{
               "type":"array",
               "items":{
                  "$ref":"#/definitions/VCVLAN"
               }
            }
         }
      },
      "VirtualCircuitVLAN":{
         "required":[
            "virtual_circuit",
            "vlan"
         ],
         "type":"object",
         "properties":{
            "id":{
               "title":"ID",
               "type":"integer",
               "readOnly":true
            },
            "virtual_circuit":{
               "title":"Virtual Circuit",
               "type":"integer"
            },
            "vlan":{
               "title":"VLAN",
               "type":"integer"
            }
         }
      }
   }
}

I'm not sure if this is because the vlans key of VirtualCircuit needs to be the VirtualCircuitVLAN key or if something is wrong with VCVLAN data.

Assign multiple Virtual Circuits to a Physical Port

Ability to assign multiple virtual circuits separated by their VLAN to one physical port.
Use case - we have many sites that have multiple services coming in/out via one port - services are segregated via VLAN.
Ability to track all those circuits to the one inbound physical port would be great.

Pin NetBox version

Came up in #38, it's worthwhile to figure out what the best practices are for pinning NetBox version.

Use setUpClass instead of setUp in unit test

From #14 discussion, it makes sense that we should use setUpClass instead of setUp in our unit test so that test data and token are only created once for all test cases. However, while trying to do so, most requests now return a 403 error for some reason. I haven't been able to figure out why so I am opening this issue to keep track of it.

Add field/model for linking Virtual Circuits to Circuits

I would find a ton of value in being able to link the virtual circuit model to a physical "Circuit" model from NetBox.

Just a rough draft idea of what that could look like:

class VirtualCircuitPhysicalCircuit(ChangeLoggedModel):
    """Virtual Circuit to Physical Circuit relationship."""

    virtual_circuit = models.ForeignKey(
        to=VirtualCircuit,
        on_delete=models.CASCADE,
        related_name='circuits',
        verbose_name='Virtual Circuit',
    )
    circuit = models.OneToOneField(
        to=Circuit,
        on_delete=models.CASCADE,
        related_name='circuit_of',
        verbose_name='Circuit',
    )

Happy to have more discussion around the idea.

Creating a virtual circuit with an existed VLAN should result in 400 request?

While attempting to write tests in #14 and #15, there is one case when creating a virtual circuit with an existed VLAN that has unexpected behavior. What I am expecting is that it would return a 400 request saying that the VLAN is already assigned to another Virtual Circuit. However, it errors out with a TypeError: argument of type 'QuerySet' is not iterable as follow:

    def test_create_400_existed_vlan(self):
        data = {'vcid': 6, 'name': 'foo', 'context': 'bar', 'vlans': [{'id': self.vlan1.id}]}
        response = self.client.post(self.url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
======================================================================
ERROR: test_create_400_existed_vlan (netbox_virtual_circuit_plugin.tests.test_api.VirtualCircuitEndpointTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/netbox-virtual-circuit-plugin/netbox_virtual_circuit_plugin/tests/test_api.py", line 159, in test_create_400_existed_vlan
    response = self.client.post(self.url, data, format='json')
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 294, in post
    path, data=data, format=format, content_type=content_type, **extra)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 207, in post
    return self.generic('POST', path, data, content_type, **extra)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 232, in generic
    method, path, data, content_type, secure, **extra)
  File "/usr/local/lib/python3.7/site-packages/django/test/client.py", line 421, in generic
    return self.request(**r)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 283, in request
    return super().request(**kwargs)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 235, in request
    request = super().request(**kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/test/client.py", line 496, in request
    raise exc_value
  File "/usr/local/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/opt/netbox/netbox/extras/middleware.py", line 126, in __call__
    enqueue_webhooks(instance, request.user, request.id, action)
  File "/opt/netbox/netbox/extras/webhooks.py", line 34, in enqueue_webhooks
    if obj_type not in webhook_models:
TypeError: argument of type 'QuerySet' is not iterable

While trying to make a request manually using curl, it makes sense that it returns a IntegrityError meaning that VLAN is already taken.

~ curl -X POST -H "Authorization: Token 0123456789abcdef0123456789abcdef01234567" -H "Content-Type: application/json" -H "Accept: application/json; indent=4" http://localhost:8000/api/plugins/virtual-circuit/virtual-circuits/ --data '{"vcid": 103, "name": "curl", "vlans": [{"id": 1}]}'

IntegrityError at /api/plugins/virtual-circuit/virtual-circuits/
duplicate key value violates unique constraint "netbox_virtual_circuit_plugin_virtualcircuitvlan_vlan_id_key"
DETAIL:  Key (vlan_id)=(1) already exists.

In the server's log, it returns a 400 request, which is expected, though right after that, it errors out the 500 request, which is issued initially by postgres.

netbox_1    | [09/Jun/2020 17:00:15] "POST /api/plugins/virtual-circuit/virtual-circuits/ HTTP/1.1" 400 78
postgres_1  | 2020-06-09 17:00:29.471 UTC [107] ERROR:  duplicate key value violates unique constraint "netbox_virtual_circuit_plugin_virtualcircuitvlan_vlan_id_key"
postgres_1  | 2020-06-09 17:00:29.471 UTC [107] DETAIL:  Key (vlan_id)=(1) already exists.
postgres_1  | 2020-06-09 17:00:29.471 UTC [107] STATEMENT:  INSERT INTO "netbox_virtual_circuit_plugin_virtualcircuitvlan" ("virtual_circuit_id", "vlan_id") VALUES (103, 1) RETURNING "netbox_virtual_circuit_plugin_virtualcircuitvlan"."id"
netbox_1    | Internal Server Error: /api/plugins/virtual-circuit/virtual-circuits/
netbox_1    | Traceback (most recent call last):
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/backends/utils.py", line 86, in _execute
netbox_1    |     return self.cursor.execute(sql, params)
netbox_1    | psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "netbox_virtual_circuit_plugin_virtualcircuitvlan_vlan_id_key"
netbox_1    | DETAIL:  Key (vlan_id)=(1) already exists.

netbox_1    | The above exception was the direct cause of the following exception:
netbox_1    |
netbox_1    | Traceback (most recent call last):
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
netbox_1    |     response = get_response(request)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
netbox_1    |     response = self.process_exception_by_middleware(e, request)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
netbox_1    |     response = wrapped_callback(request, *callback_args, **callback_kwargs)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
netbox_1    |     return view_func(*args, **kwargs)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/rest_framework/viewsets.py", line 114, in view
netbox_1    |     return self.dispatch(request, *args, **kwargs)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 505, in dispatch
netbox_1    |     response = self.handle_exception(exc)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 465, in handle_exception
netbox_1    |     self.raise_uncaught_exception(exc)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 476, in raise_uncaught_exception
netbox_1    |     raise exc
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 502, in dispatch
netbox_1    |     response = handler(request, *args, **kwargs)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/rest_framework/mixins.py", line 19, in create
netbox_1    |     self.perform_create(serializer)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/rest_framework/mixins.py", line 24, in perform_create
netbox_1    |     serializer.save()
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/rest_framework/serializers.py", line 212, in save
netbox_1    |     self.instance = self.create(validated_data)
netbox_1    |   File "/netbox-virtual-circuit-plugin/netbox_virtual_circuit_plugin/api/serializers.py", line 24, in create
netbox_1    |     VirtualCircuitVLAN.objects.create(virtual_circuit=virtual_circuit, **vlan)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
netbox_1    |     return getattr(self.get_queryset(), name)(*args, **kwargs)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/models/query.py", line 433, in create
netbox_1    |     obj.save(force_insert=True, using=self.db)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 746, in save
netbox_1    |     force_update=force_update, update_fields=update_fields)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 784, in save_base
netbox_1    |     force_update, using, update_fields,
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 887, in _save_table
netbox_1    |     results = self._do_insert(cls._base_manager, using, fields, returning_fields, raw)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 926, in _do_insert
netbox_1    |     using=using, raw=raw,
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
netbox_1    |     return getattr(self.get_queryset(), name)(*args, **kwargs)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/models/query.py", line 1204, in _insert
netbox_1    |     return query.get_compiler(using=using).execute_sql(returning_fields)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/models/sql/compiler.py", line 1391, in execute_sql
netbox_1    |     cursor.execute(sql, params)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/backends/utils.py", line 100, in execute
netbox_1    |     return super().execute(sql, params)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/cacheops/transaction.py", line 99, in execute
netbox_1    |     result = self._no_monkey.execute(self, sql, params)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/backends/utils.py", line 68, in execute
netbox_1    |     return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/backends/utils.py", line 77, in _execute_with_wrappers
netbox_1    |     return executor(sql, params, many, context)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/backends/utils.py", line 86, in _execute
netbox_1    |     return self.cursor.execute(sql, params)
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/utils.py", line 90, in __exit__
netbox_1    |     raise dj_exc_value.with_traceback(traceback) from exc_value
netbox_1    |   File "/usr/local/lib/python3.7/site-packages/django/db/backends/utils.py", line 86, in _execute
netbox_1    |     return self.cursor.execute(sql, params)
netbox_1    | django.db.utils.IntegrityError: duplicate key value violates unique constraint "netbox_virtual_circuit_plugin_virtualcircuitvlan_vlan_id_key"
netbox_1    | DETAIL:  Key (vlan_id)=(1) already exists.
netbox_1    |
netbox_1    | [09/Jun/2020 17:00:29] "POST /api/plugins/virtual-circuit/virtual-circuits/ HTTP/1.1" 500 25295

It seems like the 400 request is there. It should be returned right away so that the database does not get written and errored out a 500.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.