Merge remote-tracking branch 'origin/2.2' into 3.0

# Conflicts:
#	.github/workflows/testing.yml
#	CHANGELOG
#	README.md
#	gns3server/appliances/infix.gns3a
#	gns3server/compute/docker/__init__.py
#	gns3server/compute/docker/docker_vm.py
#	gns3server/crash_report.py
#	gns3server/version.py
#	gns3server/web/web_server.py
#	setup.py
#	tests/controller/test_project.py
This commit is contained in:
grossmj 2025-11-29 11:42:02 +10:00
commit 60c10e7ecc
No known key found for this signature in database
GPG Key ID: 1E7DD6DBB53FF3D7
18 changed files with 232 additions and 60 deletions

40
.github/workflows/docker-build.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Build and Push Docker Image
on:
workflow_dispatch:
push:
tags:
- v2.*
- v3.*
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push to GitHub Container Registry
run: |
docker build -t ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]'):latest .
docker push ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]'):latest
- name: Build and push to Docker Hub
run: |
docker build -t gns3/${{ github.event.repository.name }}:latest .
docker push gns3/${{ github.event.repository.name }}:latest

View File

@ -18,7 +18,7 @@ jobs:
strategy:
matrix:
os: ["ubuntu-latest"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
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

View File

@ -1,5 +1,18 @@
# Change Log
## 2.2.55 19/11/2025
* Docker API version requirements and process to handle older daemons
* Use docker_name property when creating a container
* Set name of container in Docker
* Support for Python 3.14
* Enable ip cef in IOU L2 and IOU L3 base configs
* Only build and push Docker image when releasing a new version
* Fix pushing Docker image to Docker hub
* Update security issues in README.md. Fixes #2535
* Add missing 'nat' template type in schema. Fixes #2529
* Resolve deprecation warnings of regex library
## 3.0.5 14/05/2025
* Bundle web-ui v3.0.5

View File

@ -6,7 +6,7 @@
"vendor_name": "Asterfusion",
"vendor_url": "https://cloudswit.ch/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/asterfusion.png",
"documentation_url": "https://help.cloudswit.ch/portal/en/kb/articles/vasternos",
"documentation_url": "https://asternos.com/community/space/vasternos/post/vasternos-for-data-center-network",
"product_name": "vAsterNOS",
"product_url": "https://cloudswit.ch/product/sonic-enterprise-distribution",
"registry_version": 4,
@ -30,18 +30,18 @@
},
"images": [
{
"filename": "vAsterNOS-V3.1.img",
"filename": "AsterNOS_V3.1_R0408P01-VS.img.gz",
"version": "V3.1",
"md5sum": "c323c9c3f60e1a93eca2acdc5034b85c",
"filesize": 2724659200,
"download_url": "https://drive.cloudswitch.io/external/8ae2e3932ad8bb2ec30dd25be415d288ff3e4a949c557c6bd48ac6e6265bcfc1"
"md5sum": "5c182306af67be2c79a5642551412e48",
"filesize": 1425945924,
"download_url": "http://pub.asternos.com/AsterNOS_V3.1_R0408P01-VS.img.gz"
}
],
"versions": [
{
"name": "V3.1",
"images": {
"hda_disk_image": "vAsterNOS-V3.1.img"
"hda_disk_image": "AsterNOS_V3.1_R0408P01-VS.img.gz"
}
}
]

View File

@ -50,6 +50,12 @@
"version": "124-25G",
"md5sum": "9c7cc9b3f3b3571411a7f62faaa2c036",
"filesize": 71528984
},
{
"filename": "c7200-itpk9-mz.124-15.SW.image",
"version": "124-15.SW",
"md5sum": "3334a0facdbd26164e83d3c201fff5b5",
"filesize": 46423952
}
],
"versions": [
@ -87,6 +93,13 @@
"images": {
"image": "c7200-a3jk9s-mz.124-25g.image"
}
},
{
"name": "124-15.SW",
"idlepc": "0x60b2bc68",
"images": {
"image": "c7200-itpk9-mz.124-15.SW.image"
}
}
]
}

View File

@ -28,6 +28,13 @@
"options": "-nographic"
},
"images": [
{
"filename": "FEGNS3.9.3.0.0.qcow2",
"version": "v9.3.0.0",
"md5sum": "2abfa8d64219aa75522238f87b782726",
"filesize": 490209280,
"direct_download_url": "https://akamai-ep.extremenetworks.com/Extreme_P/github-en/Virtual_VOSS/FEGNS3.9.3.0.0.qcow2"
},
{
"filename": "VOSSGNS3.8.10.1.0.qcow2",
"version": "v8.10.1.0",
@ -93,6 +100,13 @@
}
],
"versions": [
{
"name": "v9.3.0.0",
"images":
{
"hda_disk_image": "FEGNS3.9.3.0.0.qcow2"
}
},
{
"name": "v8.10.1.0",
"images":

View File

@ -25,19 +25,19 @@
},
"images": [
{
"filename": "rtr.qcow2",
"version": "20.7.1",
"md5sum": "4707415d15d20ced92423d90e52eedcd",
"filesize": 254767616,
"download_url": "http://freerouter.nop.hu/",
"direct_download_url": "http://dl.nop.hu/rtr.qcow2"
"filename": "rtr-x86_64.qcow2",
"version": "25.8.29",
"md5sum": "13d72fdfd772258808a9d4b75fa67083",
"filesize": 73269248,
"download_url": "http://www.freertr.org/",
"direct_download_url": "http://dl.nop.hu/rtr-x86_64.qcow2"
}
],
"versions": [
{
"name": "20.7.1",
"name": "25.8.29",
"images": {
"hda_disk_image": "rtr.qcow2"
"hda_disk_image": "rtr-x86_64.qcow2"
}
}
]

View File

@ -23,6 +23,15 @@
"kvm": "allow"
},
"images": [
{
"filename": "openwrt-24.10.2-x86-64-generic-ext4-combined.img",
"version": "24.10.2",
"md5sum": "0769916238524161b0a9a639ae261381",
"filesize": 126353408,
"download_url": "https://downloads.openwrt.org/releases/24.10.2/targets/x86/64/",
"direct_download_url": "https://downloads.openwrt.org/releases/24.10.2/targets/x86/64/openwrt-24.10.2-x86-64-generic-ext4-combined.img.gz",
"compression": "gzip"
},
{
"filename": "openwrt-23.05.0-x86-64-generic-ext4-combined.img",
"version": "23.05.0",
@ -223,7 +232,13 @@
}
],
"versions": [
{
{
"name": "24.10.2",
"images": {
"hda_disk_image": "openwrt-24.10.2-x86-64-generic-ext4-combined.img"
}
},
{
"name": "23.05.0",
"images": {
"hda_disk_image": "openwrt-23.05.0-x86-64-generic-ext4-combined.img"

View File

@ -25,6 +25,20 @@
"kvm": "require"
},
"images": [
{
"filename": "OPNsense-25.7-nano-amd64.img",
"version": "25.7",
"md5sum": "07aa8b44ebc57fa9ca0145f777901a59",
"filesize": 3221225472,
"download_url": "https://opnsense.c0urier.net/releases/25.7/"
},
{
"filename": "OPNsense-25.1-nano-amd64.img",
"version": "25.1",
"md5sum": "9c119e4fed7dd7c1c162d8be4088d983",
"filesize": 3221225472,
"download_url": "https://opnsense.c0urier.net/releases/25.1/"
},
{
"filename": "OPNsense-24.7-nano-amd64.img",
"version": "24.7",

View File

@ -30,6 +30,20 @@
"on_close": "shutdown_signal"
},
"images": [
{
"filename": "vyos-1.4.3-kvm-amd64.qcow2",
"version": "1.4.3",
"md5sum": "4eec0ba2ec367bbbdf2d2f212f947746",
"filesize": 515047424,
"download_url": "https://support.vyos.io/"
},
{
"filename": "vyos-1.4.2-kvm-amd64.qcow2",
"version": "1.4.2",
"md5sum": "78c5935e05a7bbd94a639fcee18f5799",
"filesize": 514064384,
"download_url": "https://support.vyos.io/"
},
{
"filename": "vyos-1.4.1-kvm-amd64.qcow2",
"version": "1.4.1",
@ -116,6 +130,18 @@
}
],
"versions": [
{
"name": "1.4.3",
"images": {
"hda_disk_image": "vyos-1.4.3-kvm-amd64.qcow2"
}
},
{
"name": "1.4.2",
"images": {
"hda_disk_image": "vyos-1.4.2-kvm-amd64.qcow2"
}
},
{
"name": "1.4.1",
"images": {

View File

@ -38,9 +38,9 @@ log = logging.getLogger(__name__)
# Be careful to keep it consistent
DOCKER_MINIMUM_API_VERSION = "1.25"
DOCKER_MINIMUM_VERSION = "1.13"
DOCKER_PREFERRED_API_VERSION = "1.30"
DOCKER_MINIMUM_API_VERSION = "1.40"
DOCKER_MINIMUM_VERSION = "19.03.8"
DOCKER_PREFERRED_API_VERSION = "1.44"
CHUNK_SIZE = 1024 * 8 # 8KB
@ -125,22 +125,28 @@ class Docker(BaseManager):
if not self._connected:
try:
self._connected = True
version = await self.query("GET", "version")
docker_info = await self.query("GET", "version")
except (aiohttp.ClientError, FileNotFoundError):
self._connected = False
raise DockerError("Can't connect to docker daemon")
raise DockerError("Can't connect to Docker daemon")
docker_version = parse_version(version["ApiVersion"])
api_version = parse_version(docker_info['ApiVersion'])
version = docker_info["Version"]
if docker_version < parse_version(DOCKER_MINIMUM_API_VERSION):
raise DockerError(
f"Docker version is {version['Version']}. "
f"GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}"
if api_version < parse_version(DOCKER_MINIMUM_API_VERSION):
raise DockerError(f"Docker version is {version}. "
f"GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}"
)
preferred_api_version = parse_version(DOCKER_PREFERRED_API_VERSION)
if docker_version >= preferred_api_version:
if api_version >= preferred_api_version:
self._api_version = DOCKER_PREFERRED_API_VERSION
else:
# use the Min API version supported by the daemon
self._api_version = docker_info['MinAPIVersion']
log.warning("Using Docker client with the minimum API version {}".format(self._api_version))
log.info("Connected to Docker daemon version {} using API version {}".format(version, self._api_version))
def connector(self):
@ -188,7 +194,7 @@ class Docker(BaseManager):
:param method: HTTP method
:param path: Endpoint in API
:param data: Dictionnary with the body. Will be transformed to a JSON
:param data: Dictionary with the body. Will be transformed to a JSON
:param params: Parameters added as a query arg
:param timeout: Timeout
:returns: HTTP response
@ -199,12 +205,11 @@ class Docker(BaseManager):
timeout = 60 * 60 * 24 * 31 # One month timeout
if path == 'version':
url = "http://docker/v1.24/" + path
url = "http://docker/" + path
else:
url = "http://docker/v" + DOCKER_MINIMUM_API_VERSION + "/" + path
await self._check_connection() # version is use by check connection
url = "http://docker/v" + self._api_version + "/" + path
try:
if path != "version": # version is use by check connection
await self._check_connection()
if self._session is None or self._session.closed:
connector = self.connector()
self._session = aiohttp.ClientSession(connector=connector)

View File

@ -195,6 +195,14 @@ class DockerVM(BaseNode):
def ethernet_adapters(self):
return self._ethernet_adapters
@property
def docker_name(self):
"""
Container name in Docker
"""
return "GNS3.{}.{}".format(self.name, self._project.id)
@property
def mac_address(self):
"""
@ -539,7 +547,8 @@ class DockerVM(BaseNode):
if extra_hosts:
params["Env"].append(f"GNS3_EXTRA_HOSTS={extra_hosts}")
result = await self.manager.query("POST", "containers/create", data=params)
# Support name in Doker: [a-zA-Z0-9][a-zA-Z0-9_.-]
result = await self.manager.query("POST", f"containers/create?name={self.docker_name}", data=params)
self._cid = result["Id"]
log.info(f"Docker container '{self._name}' [{self._id}] created")
if self._cpus > 0:

View File

@ -13,8 +13,7 @@ logging console discriminator EXCESS
!
no ip icmp rate-limit unreachable
!
! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work
no ip cef
ip cef
no ip domain lookup
!
!

View File

@ -12,8 +12,7 @@ no ip icmp rate-limit unreachable
!
!
!
! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work
no ip cef
ip cef
no ip domain lookup
!
!

View File

@ -60,7 +60,14 @@ class FileWatcher:
self._hashed[path] = zlib.adler32(open(path, "rb").read())
except OSError:
self._hashed[path] = None
asyncio.get_event_loop().call_later(self._delay, self._check_config_file_change)
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.call_later(self._delay, self._check_config_file_change)
def __del__(self):
self._closed = True

View File

@ -7,8 +7,8 @@ aiohttp>=3.11.16,<3.12
async-timeout==5.0.1; python_version < '3.11'
aiofiles>=24.1.0,<25.0
Jinja2>=3.1.6,<3.2
sentry-sdk>=2.26.1,<2.27 # optional dependency
psutil>=7.0.0
sentry-sdk>=2.44.0,<2.45 # optional dependency
psutil>=7.1.3
async-timeout>=5.0.1,<5.1
distro>=1.9.0
py-cpuinfo>=9.0.0,<10.0

View File

@ -49,7 +49,7 @@ async def test_query_success(vm):
vm._session.request = AsyncioMagicMock(return_value=response)
data = await vm.query("POST", "test", data={"a": True}, params={"b": 1})
vm._session.request.assert_called_with('POST',
'http://docker/v1.25/test',
'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION),
data='{"a": true}',
headers={'content-type': 'application/json'},
params={'b': 1},
@ -72,7 +72,7 @@ async def test_query_error(vm):
with pytest.raises(DockerError):
await vm.query("POST", "test", data={"a": True}, params={"b": 1})
vm._session.request.assert_called_with('POST',
'http://docker/v1.25/test',
'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION),
data='{"a": true}',
headers={'content-type': 'application/json'},
params={'b': 1},
@ -93,7 +93,7 @@ async def test_query_error_json(vm):
with pytest.raises(DockerError):
await vm.query("POST", "test", data={"a": True}, params={"b": 1})
vm._session.request.assert_called_with('POST',
'http://docker/v1.25/test',
'http://docker/v{}/test'.format(DOCKER_MINIMUM_API_VERSION),
data='{"a": true}',
headers={'content-type': 'application/json'},
params={'b': 1},
@ -188,7 +188,9 @@ async def test_docker_check_connection_docker_minimum_version(vm):
async def test_docker_check_connection_docker_preferred_version_against_newer(vm):
response = {
'ApiVersion': '1.31'
'ApiVersion': '1.52',
'Version': '29.0.1',
}
with patch("gns3server.compute.docker.Docker.connector"), \
@ -202,7 +204,9 @@ async def test_docker_check_connection_docker_preferred_version_against_newer(vm
async def test_docker_check_connection_docker_preferred_version_against_older(vm):
response = {
'ApiVersion': '1.27',
'ApiVersion': '1.43',
'Version': '24.0.2',
'MinAPIVersion': '1.40'
}
with patch("gns3server.compute.docker.Docker.connector"), \
@ -212,6 +216,20 @@ async def test_docker_check_connection_docker_preferred_version_against_older(vm
assert vm._api_version == DOCKER_MINIMUM_API_VERSION
async def test_docker_check_connection_docker_unsupported_version(vm):
response = {
'ApiVersion': '1.25',
'Version': '1.13.1',
}
with patch("gns3server.compute.docker.Docker.connector"), \
asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response):
vm._connected = False
with pytest.raises(DockerError) as e:
await vm._check_connection()
@pytest.mark.asyncio
async def test_install_busybox():

View File

@ -99,7 +99,7 @@ async def test_create(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest")
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -150,7 +150,7 @@ async def test_create_with_tag(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:16.04")
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -204,7 +204,7 @@ async def test_create_vnc(compute_project, manager):
vm._start_vnc = MagicMock()
vm._display = 42
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -356,7 +356,7 @@ async def test_create_start_cmd(compute_project, manager):
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest")
vm._start_command = "/bin/ls"
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -469,7 +469,7 @@ async def test_create_image_not_available(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.DockerVM.pull_image", return_value=True) as mock_pull:
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -524,7 +524,7 @@ async def test_create_with_user(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest")
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -624,7 +624,7 @@ async def test_create_with_extra_volumes_duplicate_1_image(compute_project, mana
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1"])
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -680,7 +680,7 @@ async def test_create_with_extra_volumes_duplicate_2_user(compute_project, manag
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1", "/vol/1"])
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -736,7 +736,7 @@ async def test_create_with_extra_volumes_duplicate_3_subdir(compute_project, man
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/1/", "/vol"])
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -792,7 +792,7 @@ async def test_create_with_extra_volumes_duplicate_4_backslash(compute_project,
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol//", "/vol"])
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -848,7 +848,7 @@ async def test_create_with_extra_volumes_duplicate_5_subdir_issue_1595(compute_p
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc"])
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -899,7 +899,7 @@ async def test_create_with_extra_volumes_duplicate_6_subdir_issue_1595(compute_p
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/etc/test", "/etc"])
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -956,7 +956,7 @@ async def test_create_with_extra_volumes(compute_project, manager):
with asyncio_patch("gns3server.compute.docker.Docker.query", return_value=response) as mock:
vm = DockerVM("test", str(uuid.uuid4()), compute_project, manager, "ubuntu:latest", extra_volumes=["/vol/2"])
await vm.create()
mock.assert_called_with("POST", "containers/create", data={
mock.assert_called_with("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -1243,7 +1243,7 @@ async def test_update(vm):
await vm.update()
mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1})
mock_query.assert_any_call("POST", "containers/create", data={
mock_query.assert_any_call("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,
@ -1325,7 +1325,7 @@ async def test_update_running(vm):
await vm.update()
mock_query.assert_any_call("DELETE", "containers/e90e34656842", params={"force": 1, "v": 1})
mock_query.assert_any_call("POST", "containers/create", data={
mock_query.assert_any_call("POST", "containers/create?name={}".format(vm.docker_name), data={
"Tty": True,
"OpenStdin": True,
"StdinOnce": False,