Checks for valid hostname on server side for Dynamips, IOU, Qemu and Docker nodes

This commit is contained in:
grossmj committed 2022-07-17 11:51:29 +02:00
1 parent 86368a1177
commit 80f5ca0c3f
13 files changed
+233 -30

No files matched your search

+6 -6
View File
@@ -67,7 +67,7 @@ compute_api.state.controller_host = None
@compute_api.exception_handler(ComputeError)
async def controller_error_handler(request: Request, exc: ComputeError):
async def compute_error_handler(request: Request, exc: ComputeError):
log.error(f"Compute error: {exc}")
return JSONResponse(
status_code=409,
@@ -76,7 +76,7 @@ async def controller_error_handler(request: Request, exc: ComputeError):
@compute_api.exception_handler(ComputeTimeoutError)
async def controller_timeout_error_handler(request: Request, exc: ComputeTimeoutError):
async def compute_timeout_error_handler(request: Request, exc: ComputeTimeoutError):
log.error(f"Compute timeout error: {exc}")
return JSONResponse(
status_code=408,
@@ -85,7 +85,7 @@ async def controller_timeout_error_handler(request: Request, exc: ComputeTimeout
@compute_api.exception_handler(ComputeUnauthorizedError)
async def controller_unauthorized_error_handler(request: Request, exc: ComputeUnauthorizedError):
async def compute_unauthorized_error_handler(request: Request, exc: ComputeUnauthorizedError):
log.error(f"Compute unauthorized error: {exc}")
return JSONResponse(
status_code=401,
@@ -94,7 +94,7 @@ async def controller_unauthorized_error_handler(request: Request, exc: ComputeUn
@compute_api.exception_handler(ComputeForbiddenError)
async def controller_forbidden_error_handler(request: Request, exc: ComputeForbiddenError):
async def compute_forbidden_error_handler(request: Request, exc: ComputeForbiddenError):
log.error(f"Compute forbidden error: {exc}")
return JSONResponse(
status_code=403,
@@ -103,7 +103,7 @@ async def controller_forbidden_error_handler(request: Request, exc: ComputeForbi
@compute_api.exception_handler(ComputeNotFoundError)
async def controller_not_found_error_handler(request: Request, exc: ComputeNotFoundError):
async def compute_not_found_error_handler(request: Request, exc: ComputeNotFoundError):
log.error(f"Compute not found error: {exc}")
return JSONResponse(
status_code=404,
@@ -112,7 +112,7 @@ async def controller_not_found_error_handler(request: Request, exc: ComputeNotFo
@compute_api.exception_handler(GNS3VMError)
async def controller_error_handler(request: Request, exc: GNS3VMError):
async def compute_gns3vm_error_handler(request: Request, exc: GNS3VMError):
log.error(f"Compute GNS3 VM error: {exc}")
return JSONResponse(
status_code=409,
@@ -19,7 +19,6 @@ API routes for Dynamips nodes.
"""
import os
import sys
from fastapi import APIRouter, WebSocket, Depends, Response, status
from fastapi.encoders import jsonable_encoder
@@ -29,7 +28,6 @@ from uuid import UUID
from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.router import Router
from gns3server.compute.dynamips.dynamips_error import DynamipsError
from gns3server import schemas
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}}
+1 -1
View File
@@ -20,7 +20,7 @@ API routes for IOU nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Body, Response, status
from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
+16
View File
@@ -33,6 +33,7 @@ from gns3server.utils.asyncio.raw_command_server import AsyncioRawCommandServer
from gns3server.utils.asyncio import wait_for_file_creation
from gns3server.utils.asyncio import monitor_process
from gns3server.utils.get_resource import get_resource
from gns3server.utils.hostname import is_rfc1123_hostname_valid
from gns3server.compute.ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError
from ..base_node import BaseNode
@@ -89,6 +90,9 @@ class DockerVM(BaseNode):
cpus=0,
):
if not is_rfc1123_hostname_valid(name):
raise DockerError(f"'{name}' is an invalid name to create a Docker node")
super().__init__(
name, node_id, project, manager, console=console, console_type=console_type, aux=aux, aux_type=aux_type
)
@@ -171,6 +175,18 @@ class DockerVM(BaseNode):
return display
display += 1
@BaseNode.name.setter
def name(self, new_name):
"""
Sets the name of this Qemu VM.
:param new_name: name
"""
if not is_rfc1123_hostname_valid(new_name):
raise DockerError(f"'{new_name}' is an invalid name to rename Docker container '{self._name}'")
super(DockerVM, DockerVM).name.__set__(self, new_name)
@property
def ethernet_adapters(self):
return self._ethernet_adapters
@@ -37,6 +37,7 @@ from ..dynamips_error import DynamipsError
from gns3server.utils.file_watcher import FileWatcher
from gns3server.utils.asyncio import wait_run_in_executor, monitor_process
from gns3server.utils.hostname import is_ios_hostname_valid
from gns3server.utils.images import md5sum
@@ -75,6 +76,9 @@ class Router(BaseNode):
ghost_flag=False,
):
if not is_ios_hostname_valid(name):
raise DynamipsError(f"{name} is an invalid name to create a Dynamips node")
super().__init__(
name, node_id, project, manager, console=console, console_type=console_type, aux=aux, aux_type=aux_type
)
@@ -1653,6 +1657,9 @@ class Router(BaseNode):
:param new_name: new name string
"""
if not is_ios_hostname_valid(new_name):
raise DynamipsError(f"{new_name} is an invalid name to rename router '{self._name}'")
await self._hypervisor.send(f'vm rename "{self._name}" "{new_name}"')
# change the hostname in the startup-config
+6
View File
@@ -42,6 +42,7 @@ from .utils.iou_export import nvram_export
from gns3server.compute.ubridge.ubridge_error import UbridgeError
from gns3server.utils.file_watcher import FileWatcher
from gns3server.utils.asyncio.telnet_server import AsyncioTelnetServer
from gns3server.utils.hostname import is_ios_hostname_valid
from gns3server.utils.asyncio import locking
import gns3server.utils.asyncio
import gns3server.utils.images
@@ -70,6 +71,9 @@ class IOUVM(BaseNode):
self, name, node_id, project, manager, application_id=None, path=None, console=None, console_type="telnet"
):
if not is_ios_hostname_valid(name):
raise IOUError(f"'{name}' is an invalid name to create an IOU node")
super().__init__(name, node_id, project, manager, console=console, console_type=console_type)
log.info(
@@ -334,6 +338,8 @@ class IOUVM(BaseNode):
:param new_name: name
"""
if not is_ios_hostname_valid(new_name):
raise IOUError(f"'{new_name}' is an invalid name to rename IOU node '{self._name}'")
if self.startup_config_file:
content = self.startup_config_content
content = re.sub(r"hostname .+$", "hostname " + new_name, content, flags=re.MULTILINE)
+16 -1
View File
@@ -22,7 +22,6 @@ order to run a QEMU VM.
import sys
import os
import re
import shlex
import math
import shutil
import struct
@@ -47,6 +46,7 @@ from ..base_node import BaseNode
from ...utils.asyncio import monitor_process
from ...utils.images import md5sum
from ...utils import macaddress_to_int, int_to_macaddress
from ...utils.hostname import is_rfc1123_hostname_valid
from gns3server.schemas.compute.qemu_nodes import Qemu, QemuPlatform
@@ -86,6 +86,9 @@ class QemuVM(BaseNode):
platform=None,
):
if not is_rfc1123_hostname_valid(name):
raise QemuError(f"'{name}' is an invalid name to create a Qemu node")
super().__init__(
name,
node_id,
@@ -172,6 +175,18 @@ class QemuVM(BaseNode):
log.info(f'QEMU VM "{self._name}" [{self._id}] has been created')
@BaseNode.name.setter
def name(self, new_name):
"""
Sets the name of this Qemu VM.
:param new_name: name
"""
if not is_rfc1123_hostname_valid(new_name):
raise QemuError(f"'{new_name}' is an invalid name to rename Qemu node '{self._name}'")
super(QemuVM, QemuVM).name.__set__(self, new_name)
@property
def guest_cid(self):
"""
+7 -6
View File
@@ -23,7 +23,7 @@ import socket
import json
import sys
import io
from operator import itemgetter
from fastapi import HTTPException
from aiohttp import web
from ..utils import parse_version
@@ -576,12 +576,13 @@ class Compute:
# If the 409 doesn't come from a GNS3 server
except ValueError:
raise ControllerError(msg)
elif response.status == 500:
raise aiohttp.web.HTTPInternalServerError(text=f"Internal server error {url}")
elif response.status == 503:
raise aiohttp.web.HTTPServiceUnavailable(text=f"Service unavailable {url} {body}")
else:
raise NotImplementedError(f"{response.status} status code is not supported for {method} '{url}'\n{body}")
raise HTTPException(
status_code=response.status,
detail=f"HTTP error {response.status} received from compute "
f"'{self.name}' for request {method} {path}: {msg}"
)
if body and len(body):
if raw:
response.body = body
+8 -1
View File
@@ -427,6 +427,7 @@ class Node:
# When updating properties used only on controller we don't need to call the compute
update_compute = False
old_json = self.asdict()
old_name = self._name
compute_properties = None
# Update node properties with additional elements
@@ -454,7 +455,13 @@ class Node:
self._list_ports()
if update_compute:
data = self._node_data(properties=compute_properties)
response = await self.put(None, data=data)
try:
response = await self.put(None, data=data)
except ComputeConflictError:
if old_name != self.name:
# special case when the new name is already updated on controller but refused by the compute
self.name = old_name
raise
await self.parse_node_response(response.json)
elif old_json != self.asdict():
# We send notif only if object has changed
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python
#
# Copyright (C) 2022 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 <http://www.gnu.org/licenses/>.
import re
def is_ios_hostname_valid(hostname: str) -> bool:
"""
Check if an IOS hostname is valid
IOS hostname must start with a letter, end with a letter or digit, and
have as interior characters only letters, digits, and hyphens.
They must be 63 characters or fewer (ARPANET rules).
"""
if re.search(r"""^(?!-|[0-9])[a-zA-Z0-9-]{1,63}(?<!-)$""", hostname):
return True
return False
def is_rfc1123_hostname_valid(hostname: str) -> bool:
"""
Check if a hostname is valid according to RFC 1123
Each element of the hostname must be from 1 to 63 characters long
and the entire hostname, including the dots, can be at most 253
characters long. Valid characters for hostnames are ASCII
letters from a to z, the digits from 0 to 9, and the hyphen (-).
A hostname may not start with a hyphen.
"""
if hostname[-1] == ".":
hostname = hostname[:-1] # strip exactly one dot from the right, if present
if len(hostname) > 253:
return False
labels = hostname.split(".")
# the TLD must be not all-numeric
if re.match(r"[0-9]+$", labels[-1]):
return False
allowed = re.compile(r"(?!-)[a-zA-Z0-9-]{1,63}(?<!-)$")
return all(allowed.match(label) for label in labels)