diff --git a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst
index 1ff726ba..0d8e9eab 100644
--- a/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst
+++ b/docs/api/v1/dynamips_device/projectsprojectiddynamipsdevicesdeviceid.rst
@@ -63,6 +63,7 @@ Ethernet switch port
port | ✔ | integer | Port number |
type | ✔ | enum | Possible values: access, dot1q, qinq |
vlan | ✔ | integer | VLAN number |
+ ethertype | ✔ | enum | Possible values: 0x8100, 0x88A8, 0x9100, 0x9200 |
Body
@@ -103,4 +104,3 @@ Response status codes
- **400**: Invalid request
- **404**: Instance doesn't exist
- **204**: Instance deleted
-
diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py
index 68297c25..d2fb9b5f 100644
--- a/gns3server/handlers/__init__.py
+++ b/gns3server/handlers/__init__.py
@@ -24,6 +24,7 @@ from gns3server.handlers.api.dynamips_device_handler import DynamipsDeviceHandle
from gns3server.handlers.api.dynamips_vm_handler import DynamipsVMHandler
from gns3server.handlers.api.qemu_handler import QEMUHandler
from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler
+from gns3server.handlers.api.docker_handler import DockerHandler
from gns3server.handlers.api.vpcs_handler import VPCSHandler
from gns3server.handlers.api.vmware_handler import VMwareHandler
from gns3server.handlers.api.config_handler import ConfigHandler
diff --git a/gns3server/handlers/api/docker_handler.py b/gns3server/handlers/api/docker_handler.py
new file mode 100644
index 00000000..a7b1f2a5
--- /dev/null
+++ b/gns3server/handlers/api/docker_handler.py
@@ -0,0 +1,183 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from ...web.route import Route
+from ...modules.docker import Docker
+
+from ...schemas.docker import (
+ DOCKER_CREATE_SCHEMA, DOCKER_UPDATE_SCHEMA, DOCKER_CAPTURE_SCHEMA,
+ DOCKER_OBJECT_SCHEMA
+)
+
+
+class DockerHandler:
+ """API entry points for Docker."""
+
+ @classmethod
+ @Route.get(
+ r"/docker/images",
+ status_codes={
+ 200: "Success",
+ },
+ description="Get all available Docker images")
+ def show(request, response):
+ docker_manager = Docker.instance()
+ images = yield from docker_manager.list_images()
+ response.json(images)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images",
+ parameters={
+ "project_id": "UUID for the project"
+ },
+ status_codes={
+ 201: "Instance created",
+ 400: "Invalid request",
+ 409: "Conflict"
+ },
+ description="Create a new Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def create(request, response):
+ docker_manager = Docker.instance()
+ container = yield from docker_manager.create_vm(
+ request.json.pop("name"),
+ request.match_info["project_id"],
+ request.json.pop("imagename")
+ )
+ # FIXME: DO WE NEED THIS?
+ for name, value in request.json.items():
+ if name != "vm_id":
+ if hasattr(container, name) and getattr(container, name) != value:
+ setattr(container, name, value)
+
+ response.set_status(201)
+ response.json(container)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images/{id}/start",
+ parameters={
+ "project_id": "UUID of the project",
+ "id": "ID of the container"
+ },
+ status_codes={
+ 204: "Instance started",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Start a Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def start(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.start()
+ response.set_status(204)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images/{id}/stop",
+ parameters={
+ "project_id": "UUID of the project",
+ "id": "ID of the container"
+ },
+ status_codes={
+ 204: "Instance stopped",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Stop a Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def stop(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.stop()
+ response.set_status(204)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images/{id}/reload",
+ parameters={
+ "project_id": "UUID of the project",
+ "id": "ID of the container"
+ },
+ status_codes={
+ 204: "Instance restarted",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Restart a Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def reload(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.restart()
+ response.set_status(204)
+
+ @classmethod
+ @Route.delete(
+ r"/projects/{project_id}/docker/images/{id}",
+ parameters={
+ "id": "ID for the container",
+ "project_id": "UUID for the project"
+ },
+ status_codes={
+ 204: "Instance deleted",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Delete a Docker container")
+ def delete(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.remove()
+ response.set_status(204)
+
+ @classmethod
+ @Route.post(
+ r"/projects/{project_id}/docker/images/{id}/suspend",
+ parameters={
+ "project_id": "UUID of the project",
+ "id": "ID of the container"
+ },
+ status_codes={
+ 204: "Instance paused",
+ 400: "Invalid request",
+ 404: "Instance doesn't exist"
+ },
+ description="Pause a Docker container",
+ input=DOCKER_CREATE_SCHEMA,
+ output=DOCKER_OBJECT_SCHEMA)
+ def suspend(request, response):
+ docker_manager = Docker.instance()
+ container = docker_manager.get_container(
+ request.match_info["id"],
+ project_id=request.match_info["project_id"])
+ yield from container.pause()
+ response.set_status(204)
diff --git a/gns3server/modules/docker/__init__.py b/gns3server/modules/docker/__init__.py
new file mode 100644
index 00000000..49a7f4b1
--- /dev/null
+++ b/gns3server/modules/docker/__init__.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Docker server module.
+"""
+
+import os
+import sys
+import shutil
+import asyncio
+import subprocess
+import logging
+import docker
+
+log = logging.getLogger(__name__)
+
+from ..base_manager import BaseManager
+from ..project_manager import ProjectManager
+from .docker_vm import Container
+from .docker_error import DockerError
+
+
+class Docker(BaseManager):
+
+ _VM_CLASS = Container
+
+ def __init__(self):
+ super().__init__()
+ # FIXME: make configurable and start docker before trying
+ self._server_url = 'unix://var/run/docker.sock'
+ # FIXME: handle client failure
+ self._client = docker.Client(base_url=self._server_url)
+ self._execute_lock = asyncio.Lock()
+
+ @property
+ def server_url(self):
+ """Returns the Docker server url.
+
+ :returns: url
+ :rtype: string
+ """
+ return self._server_url
+
+ @server_url.setter
+ def server_url(self, value):
+ self._server_url = value
+ # FIXME: handle client failure
+ self._client = docker.Client(base_url=value)
+
+ @asyncio.coroutine
+ def execute(self, command, kwargs, timeout=60):
+ command = getattr(self._client, command)
+ log.debug("Executing Docker with command: {}".format(command))
+ try:
+ # FIXME: async wait
+ result = command(**kwargs)
+ except Exception as error:
+ raise DockerError("Docker has returned an error: {}".format(error))
+ return result
+
+ # FIXME: do this in docker
+ @asyncio.coroutine
+ def project_closed(self, project):
+ """Called when a project is closed.
+
+ :param project: Project instance
+ """
+ yield from super().project_closed(project)
+ hdd_files_to_close = yield from self._find_inaccessible_hdd_files()
+ for hdd_file in hdd_files_to_close:
+ log.info("Closing VirtualBox VM disk file {}".format(os.path.basename(hdd_file)))
+ try:
+ yield from self.execute("closemedium", ["disk", hdd_file])
+ except VirtualBoxError as e:
+ log.warning("Could not close VirtualBox VM disk file {}: {}".format(os.path.basename(hdd_file), e))
+ continue
+
+ @asyncio.coroutine
+ def list_images(self):
+ """Gets Docker image list.
+
+ :returns: list of dicts
+ :rtype: list
+ """
+ images = []
+ for image in self._client.images():
+ for tag in image['RepoTags']:
+ images.append({'imagename': tag})
+ return images
+
+ @asyncio.coroutine
+ def list_containers(self):
+ """Gets Docker container list.
+
+ :returns: list of dicts
+ :rtype: list
+ """
+ return self._client.containers()
+
+ def get_container(self, cid, project_id=None):
+ """Returns a Docker container.
+
+ :param id: Docker container identifier
+ :param project_id: Project identifier
+
+ :returns: Docker container
+ """
+ if project_id:
+ # check if the project_id exists
+ project = ProjectManager.instance().get_project(project_id)
+
+ if cid not in self._vms:
+ raise aiohttp.web.HTTPNotFound(
+ text="Docker container with ID {} doesn't exist".format(vm_id))
+
+ container = self._vms[cid]
+ if project_id:
+ if container.project.id != project.id:
+ raise aiohttp.web.HTTPNotFound(
+ text="Project ID {} doesn't belong to container {}".format(
+ project_id, container.name))
+ return container
diff --git a/gns3server/modules/docker/docker_error.py b/gns3server/modules/docker/docker_error.py
new file mode 100644
index 00000000..2e03ace5
--- /dev/null
+++ b/gns3server/modules/docker/docker_error.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Custom exceptions for the Docker module.
+"""
+
+from ..vm_error import VMError
+
+
+class DockerError(VMError):
+ pass
diff --git a/gns3server/modules/docker/docker_vm.py b/gns3server/modules/docker/docker_vm.py
new file mode 100644
index 00000000..d08cff02
--- /dev/null
+++ b/gns3server/modules/docker/docker_vm.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Docker container instance.
+"""
+
+import sys
+import shlex
+import re
+import os
+import tempfile
+import json
+import socket
+import asyncio
+import docker
+
+from pkg_resources import parse_version
+from .docker_error import DockerError
+from ..base_vm import BaseVM
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class Container(BaseVM):
+ """Docker container implementation.
+
+ :param name: Docker container name
+ :param project: Project instance
+ :param manager: Manager instance
+ :param image: Docker image
+ """
+ def __init__(self, name, image, project, manager):
+ self._name = name
+ self._project = project
+ self._manager = manager
+ self._image = image
+
+ log.debug(
+ "{module}: {name} [{image}] initialized.".format(
+ module=self.manager.module_name,
+ name=self.name,
+ image=self._image))
+
+ def __json__(self):
+ return {
+ "name": self._name,
+ "id": self._id,
+ "project_id": self._project.id,
+ "image": self._image,
+ }
+
+ @asyncio.coroutine
+ def _get_container_state(self):
+ """Returns the container state (e.g. running, paused etc.)
+
+ :returns: state
+ :rtype: str
+ """
+ try:
+ result = yield from self.manager.execute(
+ "inspect_container", {"container": self._id})
+ for state, value in result["State"].items():
+ if value is True:
+ return state.lower()
+ return 'exited'
+ except Exception as err:
+ raise DockerError("Could not get container state for {0}: ".format(
+ self._name), str(err))
+
+ @asyncio.coroutine
+ def create(self):
+ """Creates the Docker container."""
+ result = yield from self.manager.execute(
+ "create_container", {"name": self._name, "image": self._image})
+ self._id = result['Id']
+ log.info("Docker container '{name}' [{id}] created".format(
+ name=self._name, id=self._id))
+ return True
+
+ @asyncio.coroutine
+ def start(self):
+ """Starts this Docker container."""
+ state = yield from self._get_container_state()
+ if state == "paused":
+ self.unpause()
+ else:
+ result = yield from self.manager.execute(
+ "start", {"container": self._id})
+ log.info("Docker container '{name}' [{image}] started".format(
+ name=self._name, image=self._image))
+
+ def is_running(self):
+ """Checks if the container is running.
+
+ :returns: True or False
+ :rtype: bool
+ """
+ state = self._get_container_state()
+ if state == "running":
+ return True
+ return False
+
+ @asyncio.coroutine
+ def restart(self):
+ """Restarts this Docker container."""
+ result = yield from self.manager.execute(
+ "restart", {"container": self._id})
+ log.info("Docker container '{name}' [{image}] restarted".format(
+ name=self._name, image=self._image))
+
+ @asyncio.coroutine
+ def stop(self):
+ """Stops this Docker container."""
+ result = yield from self.manager.execute(
+ "stop", {"container": self._id})
+ log.info("Docker container '{name}' [{image}] stopped".format(
+ name=self._name, image=self._image))
+
+ @asyncio.coroutine
+ def pause(self):
+ """Pauses this Docker container."""
+ result = yield from self.manager.execute(
+ "pause", {"container": self._id})
+ log.info("Docker container '{name}' [{image}] paused".format(
+ name=self._name, image=self._image))
+
+ @asyncio.coroutine
+ def unpause(self):
+ """Unpauses this Docker container."""
+ result = yield from self.manager.execute(
+ "unpause", {"container": self._id})
+ log.info("Docker container '{name}' [{image}] unpaused".format(
+ name=self._name, image=self._image))
+
+ @asyncio.coroutine
+ def remove(self):
+ """Removes this Docker container."""
+ state = yield from self._get_container_state()
+ if state == "paused":
+ self.unpause()
+ result = yield from self.manager.execute(
+ "remove_container", {"container": self._id, "force": True})
+ log.info("Docker container '{name}' [{image}] removed".format(
+ name=self._name, image=self._image))
diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py
index 2f8f0d87..72645679 100644
--- a/gns3server/modules/dynamips/nodes/ethernet_switch.py
+++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py
@@ -59,7 +59,8 @@ class EthernetSwitch(Device):
for port_number, settings in self._mappings.items():
ports.append({"port": port_number,
"type": settings[0],
- "vlan": settings[1]})
+ "vlan": settings[1],
+ "ethertype": settings[2] if len(settings) > 2 else ""})
ethernet_switch_info["ports"] = ports
return ethernet_switch_info
@@ -192,7 +193,7 @@ class EthernetSwitch(Device):
elif settings["type"] == "dot1q":
yield from self.set_dot1q_port(port_number, settings["vlan"])
elif settings["type"] == "qinq":
- yield from self.set_qinq_port(port_number, settings["vlan"])
+ yield from self.set_qinq_port(port_number, settings["vlan"], settings["ethertype"] )
@asyncio.coroutine
def set_access_port(self, port_number, vlan_id):
@@ -242,7 +243,7 @@ class EthernetSwitch(Device):
self._mappings[port_number] = ("dot1q", native_vlan)
@asyncio.coroutine
- def set_qinq_port(self, port_number, outer_vlan):
+ def set_qinq_port(self, port_number, outer_vlan, ethertype):
"""
Sets the specified port as a trunk (QinQ) port.
@@ -254,15 +255,17 @@ class EthernetSwitch(Device):
raise DynamipsError("Port {} is not allocated".format(port_number))
nio = self._nios[port_number]
- yield from self._hypervisor.send('ethsw set_qinq_port "{name}" {nio} {outer_vlan}'.format(name=self._name,
+ yield from self._hypervisor.send('ethsw set_qinq_port "{name}" {nio} {outer_vlan} {ethertype}'.format(name=self._name,
nio=nio,
- outer_vlan=outer_vlan))
+ outer_vlan=outer_vlan,
+ ethertype=ethertype if ethertype != "0x8100" else ""))
- log.info('Ethernet switch "{name}" [{id}]: port {port} set as a QinQ port with outer VLAN {vlan_id}'.format(name=self._name,
+ log.info('Ethernet switch "{name}" [{id}]: port {port} set as a QinQ ({ethertype}) port with outer VLAN {vlan_id}'.format(name=self._name,
id=self._id,
port=port_number,
- vlan_id=outer_vlan))
- self._mappings[port_number] = ("qinq", outer_vlan)
+ vlan_id=outer_vlan,
+ ethertype=ethertype))
+ self._mappings[port_number] = ("qinq", outer_vlan, ethertype)
@asyncio.coroutine
def get_mac_addr_table(self):
diff --git a/gns3server/schemas/docker.py b/gns3server/schemas/docker.py
new file mode 100644
index 00000000..15c03ab3
--- /dev/null
+++ b/gns3server/schemas/docker.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+
+DOCKER_CREATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to create a new Docker container",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "Docker container name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "imagename": {
+ "description": "Docker image name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "adapters": {
+ "description": "number of adapters",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 64,
+ },
+ "adapter_type": {
+ "description": "VirtualBox adapter type",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+}
+
+DOCKER_UPDATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to update a Docker container",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "Docker container name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "image": {
+ "description": "Docker image name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "adapters": {
+ "description": "number of adapters",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 64,
+ },
+ "adapter_type": {
+ "description": "VirtualBox adapter type",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+}
+
+DOCKER_CAPTURE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to start a packet capture on a Docker container port",
+ "type": "object",
+ "properties": {
+ "capture_file_name": {
+ "description": "Capture file name",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+ "required": ["capture_file_name"]
+}
+
+DOCKER_OBJECT_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Docker instance",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "Docker container name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "id": {
+ "description": "Docker container ID",
+ "type": "string",
+ "minLength": 64,
+ "maxLength": 64,
+ "pattern": "^[a-zA-Z0-9_.-]{64}$"
+ },
+ "project_id": {
+ "description": "Project UUID",
+ "type": "string",
+ "minLength": 36,
+ "maxLength": 36,
+ "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
+ },
+ "image": {
+ "description": "Docker image name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "adapters": {
+ "description": "number of adapters",
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 64,
+ },
+ "adapter_type": {
+ "description": "VirtualBox adapter type",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id", "project_id"]
+}
diff --git a/gns3server/schemas/dynamips_device.py b/gns3server/schemas/dynamips_device.py
index 201cd805..45d6da0c 100644
--- a/gns3server/schemas/dynamips_device.py
+++ b/gns3server/schemas/dynamips_device.py
@@ -63,10 +63,15 @@ DEVICE_UPDATE_SCHEMA = {
"description": "Port type",
"enum": ["access", "dot1q", "qinq"],
},
+
"vlan": {"description": "VLAN number",
"type": "integer",
"minimum": 1
},
+ "ethertype": {
+ "description": "QinQ Ethertype",
+ "enum": ["", "0x8100", "0x88A8", "0x9100", "0x9200"],
+ },
},
"required": ["port", "type", "vlan"],
"additionalProperties": False
@@ -112,6 +117,10 @@ DEVICE_OBJECT_SCHEMA = {
"type": "integer",
"minimum": 1
},
+ "ethertype": {
+ "description": "QinQ Ethertype",
+ "enum": ["", "0x8100", "0x88A8", "0x9100", "0x9200"],
+ },
},
"required": ["port", "type", "vlan"],
"additionalProperties": False
@@ -321,6 +330,10 @@ DEVICE_NIO_SCHEMA = {
"type": "integer",
"minimum": 1
},
+ "ethertype": {
+ "description": "QinQ Ethertype",
+ "enum": ["", "0x8100", "0x88A8", "0x9100", "0x9200"],
+ },
},
"required": ["type", "vlan"],
"additionalProperties": False