Merge branch 'master' into patch-1

This commit is contained in:
Jeremy Grossmann 2026-02-12 21:13:38 +08:00 committed by GitHub
commit 898a9ce208
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 47 additions and 286 deletions

View File

@ -18,11 +18,7 @@ jobs:
strategy:
matrix:
os: ["ubuntu-latest"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
#include:
# only test with Python 3.10 on Windows
# - os: windows-latest
# python-version: "3.10"
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4

View File

@ -1,5 +1,11 @@
# Change Log
## 2.2.56.1 28/01/2026
* Fix telnet keepalive options on macOS
* Upgrade dependencies
* Drop Python 3.8 support
## 2.2.56 21/01/2026
* Set default location of udhcpc in "/etc/network/udhcpc". Fixes #2582

View File

@ -1,6 +1,6 @@
-rrequirements.txt
pytest==8.3.3
flake8==7.1.0
pytest-timeout==2.3.1
pytest-aiohttp==1.0.5
pytest==8.4.2 # version 8.4.2 is the last one supporting Python 3.9
flake8==7.3.0
pytest-timeout==2.4.0
pytest-aiohttp==1.1.0

View File

@ -9,6 +9,7 @@
"registry_version": 6,
"status": "stable",
"maintainer": "Asterfusion Product Team",
"maintainer_email": "bd@cloudswit.ch",
"images": [
{
"filename": "AsterNOS-VPP_V6.1-R0101P02_x86.img.gz",

View File

@ -57,7 +57,7 @@ class CrashReport:
Report crash to a third party service
"""
DSN = "https://9b9fce29bf59d9a99c230240e1070600@o19455.ingest.us.sentry.io/38482"
DSN = "https://5bfcec70239c428d44aafef68f3eea79@o19455.ingest.us.sentry.io/38482"
_instance = None
def __init__(self):

View File

@ -235,9 +235,9 @@ def run():
return
log.info("HTTP authentication is enabled with username '{}'".format(user))
# we only support Python 3 version >= 3.8
if sys.version_info < (3, 8, 0):
raise SystemExit("Python 3.8 or higher is required")
# we only support Python 3 version >= 3.9
if sys.version_info < (3, 9, 0):
raise SystemExit("Python 3.9 or higher is required")
user_log.info("Running with Python {major}.{minor}.{micro} and has PID {pid}".format(major=sys.version_info[0], minor=sys.version_info[1],
micro=sys.version_info[2], pid=os.getpid()))

View File

@ -91,25 +91,16 @@ async def wait_for_process_termination(process, timeout=10):
In theory this can be implemented by just:
await asyncio.wait_for(self._iou_process.wait(), timeout=100)
But it's broken before Python 3.4:
http://bugs.python.org/issue23140
:param process: An asyncio subprocess
:param timeout: Timeout in seconds
"""
if sys.version_info >= (3, 5):
try:
await asyncio.wait_for(process.wait(), timeout=timeout)
except ProcessLookupError:
while timeout > 0:
if process.returncode is not None:
return
else:
while timeout > 0:
if process.returncode is not None:
return
await asyncio.sleep(0.1)
timeout -= 0.1
raise asyncio.TimeoutError()
await asyncio.sleep(0.1)
timeout -= 0.1
raise asyncio.TimeoutError()
async def _check_process(process, termination_callback):

View File

@ -40,10 +40,7 @@ class Pool():
while len(self._tasks) > 0 or len(pending) > 0:
while len(self._tasks) > 0 and len(pending) < self._concurrency:
task, args, kwargs = self._tasks.pop(0)
if sys.version_info >= (3, 7):
t = asyncio.create_task(task(*args, **kwargs))
else:
t = asyncio.get_event_loop().create_task(task(*args, **kwargs))
t = asyncio.create_task(task(*args, **kwargs))
pending.add(t)
(done, pending) = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
for task in done:

View File

@ -186,15 +186,25 @@ class AsyncioTelnetServer:
await writer.drain()
async def run(self, network_reader, network_writer):
sock = network_writer.get_extra_info("socket")
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# 60 sec keep alives, close tcp session after 4 missed
# Will keep a firewall from aging out telnet console.
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4)
try:
# Keepalive options are platform dependent: Linux uses TCP_KEEPIDLE,
# while macOS exposes TCP_KEEPALIVE (in Python >= 3.10).
if hasattr(socket, "TCP_KEEPIDLE"):
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
elif hasattr(socket, "TCP_KEEPALIVE"):
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPALIVE, 60)
else:
raise AttributeError("module 'socket' has no attribute 'TCP_KEEPIDLE' or 'TCP_KEEPALIVE'")
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 10)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 4)
except (AttributeError, OSError):
log.debug("Failed to tune TCP keepalive for telnet client; using OS defaults", exc_info=True)
#log.debug("New connection from {}".format(sock.getpeername()))
# Keep track of connected clients

View File

@ -23,7 +23,7 @@
# or negative for a release candidate or beta (after the base version
# number has been incremented)
__version__ = "2.2.56"
__version__ = "2.2.56.1"
__version_info__ = (2, 2, 56, 0)
if "dev" in __version__:

View File

@ -1,13 +1,12 @@
jsonschema>=4.23,<4.24
aiohttp>=3.10.11,<3.11 # version 3.10.11 is the last compatible version with Python 3.8
aiohttp-cors>=0.7.0,<0.8
aiofiles>=24.1.0,<25.0
jsonschema>=4.25.1,<4.26 # version 4.25.1 is the last to support Python 3.9
aiohttp>=3.13.3,<3.14
aiohttp-cors>=0.8.1,<0.9
aiofiles>=25.1.0,<26.0
Jinja2>=3.1.6,<3.2
sentry-sdk>=2.44.0,<2.45 # optional dependency
psutil>=7.1.3
async-timeout>=5.0.1,<5.1
sentry-sdk>=2.50.0,<3 # optional dependency
psutil>=7.2.1
async-timeout>=5.0.1,<5.1 # this library has effectively been upstreamed into Python 3.11+
distro>=1.9.0
py-cpuinfo>=9.0.0,<10.0
platformdirs>=2.4.0,<3 # platformdirs >=3 conflicts when building Debian packages
importlib-resources>=1.3; python_version < '3.9'
truststore>=0.10.0; python_version >= '3.10'
truststore>=0.10.4; python_version >= '3.10'

View File

@ -1,238 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2016 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/>.
"""
This script connect to the local GNS3 server and will create a random topology
"""
import sys
import json
import math
import aiohttp
import aiohttp.web
import asyncio
import random
import coloredlogs
import logging
coloredlogs.install(fmt=" %(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
PROJECT_ID = "9e26e37d-4962-4921-8c0e-136d3b04ba9c"
HOST = "192.168.84.151:3080"
# Use for node names uniqueness
node_i = 1
def die(*args):
log.error(*args)
sys.exit(1)
class HTTPError(Exception):
def __init__(self, method, path, response):
self._method = method
self._path = path
self._response = response
@property
def response(self):
return self._response
@property
def path(self):
return self._path
@property
def method(self):
return self._method
class HTTPConflict(HTTPError):
pass
class HTTPNotFound(HTTPError):
pass
async def query(method, path, body=None, **kwargs):
global session
if body:
kwargs["data"] = json.dumps(body)
async with session.request(method, "http://" + HOST + "/v2" + path, **kwargs) as response:
if response.status == 409:
raise HTTPConflict(method, path, response)
elif response.status == 404:
raise HTTPNotFound(method, path, response)
elif response.status >= 300:
raise HTTPError(method, path, response)
log.info("%s %s %d", method, path, response.status)
if response.headers["content-type"] == "application/json":
return await response.json()
else:
return "{}"
async def post(path, **kwargs):
return await query("POST", path, **kwargs)
async def get(path, **kwargs):
return await query("GET", path, **kwargs)
async def delete(path, **kwargs):
return await query("DELETE", path, **kwargs)
async def create_project():
# Delete project if already exists
response = await get("/projects")
project_exists = False
for project in response:
if project["name"] == "random" and project["project_id"] != PROJECT_ID:
await delete("/projects/" + project["project_id"])
elif project["project_id"] == PROJECT_ID:
project_exists = True
tasks = []
for node in await get("/projects/" + PROJECT_ID + "/nodes"):
tasks.append(delete_node(project, node))
await asyncio.gather(*tasks)
if project_exists:
response = await post("/projects/" + PROJECT_ID + "/open")
else:
response = await post("/projects", body={"name": "random", "project_id": PROJECT_ID, "auto_close": False})
return response
async def create_node(project):
global node_i
r = random.randint(0, 1)
if r == 0:
node_type = "ethernet_switch"
symbol = ":/symbols/ethernet_switch.svg"
elif r == 1:
node_type = "vpcs"
symbol = ":/symbols/vpcs_guest.svg"
response = await post("/projects/{}/nodes".format(project["project_id"]), body={
"node_type": node_type,
"compute_id": "local",
"symbol": symbol,
"name": "Node{}".format(node_i),
"x": (math.floor((node_i - 1) % 12.0) * 100) - 500,
"y": (math.ceil((node_i) / 12.0) * 100) - 300
})
node_i += 1
return response
async def delete_node(project, node):
await delete("/projects/{}/nodes/{}".format(project["project_id"], node["node_id"]))
async def create_link(project, nodes):
"""
Create all possible link of a node
"""
node1 = random.choice(list(nodes.values()))
for port in range(0, 8):
node2 = random.choice(list(nodes.values()))
if node1 == node2:
continue
data = {"nodes":
[
{
"adapter_number": 0,
"node_id": node1["node_id"],
"port_number": port
},
{
"adapter_number": 0,
"node_id": node2["node_id"],
"port_number": port
}
]
}
try:
await post("/projects/{}/links".format(project["project_id"]), body=data)
except (HTTPConflict, HTTPNotFound):
pass
async def build_topology():
global node_i
nodes = {}
project = await create_project()
while True:
rand = random.randint(0, 1000)
if rand < 500: # chance to create a new node
if len(nodes.keys()) < 255: # Limit of VPCS:
node = await create_node(project)
nodes[node["node_id"]] = node
elif rand < 600: # start all nodes
await post("/projects/{}/nodes/start".format(project["project_id"]))
elif rand < 700: # stop all nodes
await post("/projects/{}/nodes/stop".format(project["project_id"]))
elif rand < 950: # create a link
if len(nodes.keys()) >= 2:
await create_link(project, nodes)
elif rand < 999: # chance to delete a node
continue
if len(nodes.keys()) > 0:
node = random.choice(list(nodes.values()))
await delete_node(project, node)
del nodes[node["node_id"]]
elif len(nodes.keys()) > 0: # % chance to delete all nodes
continue
node_i = 1
tasks = []
for node in nodes.values():
tasks.append(delete_node(project, node))
await asyncio.gather(*tasks)
nodes = {}
await asyncio.sleep(0.2)
async def main(loop):
global session
async with aiohttp.ClientSession() as session:
try:
await build_topology()
except HTTPError as error:
try:
j = await error.response.json()
die("%s %s invalid status %d:\n%s", error.method, error.path, error.response.status, json.dumps(j, indent=4))
except (ValueError, aiohttp.ServerDisconnectedError):
die("%s %s invalid status %d", error.method, error.path, error.response.status)
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
if session:
session.close()

View File

@ -21,8 +21,8 @@ from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
# we only support Python 3 version >= 3.8
if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 8):
raise SystemExit("Python 3.8 or higher is required")
if len(sys.argv) >= 2 and sys.argv[1] == "install" and sys.version_info < (3, 9):
raise SystemExit("Python 3.9 or higher is required")
class PyTest(TestCommand):
@ -78,7 +78,6 @@ setup(
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",