gns3-server/tests/compute/test_base_node.py
YueGuobin 84e7ead465
fix: make node working directory deletion robust
The rmtree error handler in BaseNode.delete() was chmod'ing the failed
path to S_IWRITE (0o200). On POSIX this strips the search permission
from directories, turning a transient deletion failure into a directory
that can no longer be traversed or deleted. The node deletion itself
silently "succeeds" (rmtree gives up once its error handler returns)
and a later project deletion then fails with EACCES. The handler also
never retried the failed operation, so it did not help on Windows
either (the platform it was written for).

The transient failure exists in practice: a concurrent MD5 checksum
computation caching its result in the node directory (e.g. a properties
request racing the deletion) can recreate a file after rmtree has
listed the directory, making the final rmdir fail with ENOTEMPTY.

- add the missing user permissions instead of replacing the whole mode,
  and retry the failed unlink/rmdir
- retry the whole deletion a few times to absorb files recreated while
  the directory is being deleted
- raise a ComputeError when the directory cannot be fully deleted
  instead of failing silently
2026-09-15 01:23:31 +08:00

602 lines
25 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 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 os
import shutil
from collections import OrderedDict
import pytest
import pytest_asyncio
from tests.utils import asyncio_patch, AsyncioMagicMock
from unittest.mock import patch, MagicMock
from gns3server.compute.compute_error import ComputeError
from gns3server.compute.vpcs.vpcs_vm import VPCSVM
from gns3server.compute.docker.docker_vm import DockerVM
from gns3server.compute.error import NodeError
from gns3server.compute.vpcs import VPCS
from gns3server.compute.nios.nio_udp import NIOUDP
@pytest_asyncio.fixture(scope="function")
async def manager(port_manager):
m = VPCS.instance()
m.port_manager = port_manager
return m
@pytest.fixture(scope="function")
def node(compute_project, manager):
return VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
def test_temporary_directory(compute_project, manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
assert isinstance(node.temporary_directory, str)
def test_console(compute_project, manager, port_manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
# pick a port that is actually free on this host: a hardcoded one may be
# taken by a running gns3server/qemu on a dev machine, and the setter would
# silently replace it with the next free port
console_port = port_manager.get_free_tcp_port(node.project)
port_manager.release_tcp_port(console_port, node.project)
node.console = console_port
assert node.console == console_port
node.console = None
assert node.console is None
def test_change_console_port(node, port_manager):
port1 = port_manager.get_free_tcp_port(node.project)
port2 = port_manager.get_free_tcp_port(node.project)
port_manager.release_tcp_port(port1, node.project)
port_manager.release_tcp_port(port2, node.project)
node.console = port1
node.console = port2
assert node.console == port2
port_manager.reserve_tcp_port(port1, node.project)
def test_console_vnc_invalid(compute_project, manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._console_type = "vnc"
with pytest.raises(NodeError):
node.console = 2012
@pytest.mark.asyncio
async def test_close(node, port_manager):
assert node.console is not None
aux = port_manager.get_free_tcp_port(node.project)
port_manager.release_tcp_port(aux, node.project)
node.aux = aux
port = node.console
assert await node.close()
# Raise an exception if the port is not free
port_manager.reserve_tcp_port(port, node.project)
# Raise an exception if the port is not free
port_manager.reserve_tcp_port(aux, node.project)
assert node.console is None
assert node.aux is None
# Called twice closed should return False
assert await node.close() is False
def test_aux(compute_project, manager, port_manager):
aux = port_manager.get_free_tcp_port(compute_project)
port_manager.release_tcp_port(aux, compute_project)
node = DockerVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, "ubuntu", aux=aux, aux_type="telnet")
assert node.aux == aux
node.aux = None
assert node.aux is None
def test_allocate_aux(compute_project, manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
assert node.aux is None
# Docker has an aux port by default
node = DockerVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, "ubuntu", aux_type="telnet")
assert node.aux is not None
def test_change_aux_port(compute_project, manager, port_manager):
node = DockerVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager, "ubuntu", aux_type="telnet")
port1 = port_manager.get_free_tcp_port(node.project)
port2 = port_manager.get_free_tcp_port(node.project)
port_manager.release_tcp_port(port1, node.project)
port_manager.release_tcp_port(port2, node.project)
node.aux = port1
node.aux = port2
assert node.aux == port2
port_manager.reserve_tcp_port(port1, node.project)
@pytest.mark.asyncio
async def test_update_ubridge_udp_connection(node):
filters = {
"latency": [10]
}
snio = NIOUDP(1245, "localhost", 1246)
dnio = NIOUDP(1245, "localhost", 1244)
dnio.filters = filters
with asyncio_patch("gns3server.compute.base_node.BaseNode._ubridge_apply_filters") as mock:
await node.update_ubridge_udp_connection('VPCS-10', snio, dnio)
mock.assert_called_with("VPCS-10", filters)
@pytest.mark.asyncio
async def test_ubridge_apply_filters(node):
filters = OrderedDict((
('latency', [10]),
('bpf', ["icmp[icmptype] == 8\ntcp src port 53"])
))
node._ubridge_send = AsyncioMagicMock()
await node._ubridge_apply_filters("VPCS-10", filters)
node._ubridge_send.assert_any_call("bridge reset_packet_filters VPCS-10")
node._ubridge_send.assert_any_call("bridge add_packet_filter VPCS-10 filter0 latency 10")
@pytest.mark.asyncio
async def test_ubridge_apply_bpf_filters(node):
filters = {
"bpf": ["icmp[icmptype] == 8\ntcp src port 53"]
}
node._ubridge_send = AsyncioMagicMock()
await node._ubridge_apply_filters("VPCS-10", filters)
node._ubridge_send.assert_any_call("bridge reset_packet_filters VPCS-10")
node._ubridge_send.assert_any_call("bridge add_packet_filter VPCS-10 filter0 bpf \"icmp[icmptype] == 8\"")
node._ubridge_send.assert_any_call("bridge add_packet_filter VPCS-10 filter1 bpf \"tcp src port 53\"")
@pytest.mark.asyncio
async def test_set_marker_filter_state_off(compute_project, manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
node._marker_filter_bridges["m", "L"] = "VPCS-10"
await node._ubridge_set_marker_filter_state("m", False)
node._ubridge_send.assert_called_with("bridge enable_packet_filter VPCS-10 m off")
@pytest.mark.asyncio
async def test_set_marker_filter_state_on(compute_project, manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
node._marker_filter_bridges["m", "L"] = "VPCS-10"
await node._ubridge_set_marker_filter_state("m", True)
node._ubridge_send.assert_called_with("bridge enable_packet_filter VPCS-10 m on")
@pytest.mark.asyncio
async def test_marker_pause_sends_command(compute_project, manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_hypervisor = AsyncioMagicMock()
await node._ubridge_marker_pause()
node._ubridge_hypervisor.send.assert_called_with("marker pause")
@pytest.mark.asyncio
async def test_marker_resume_sends_command(compute_project, manager):
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_hypervisor = AsyncioMagicMock()
await node._ubridge_marker_resume()
node._ubridge_hypervisor.send.assert_called_with("marker resume")
@pytest.mark.asyncio
async def test_apply_markers_turns_disabled_filter_off(compute_project, manager):
# Part A: a disabled marker is installed (add_packet_filter) then turned off
# with enable_packet_filter … off, and its bridge is recorded for toggling.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
nio = NIOUDP(1234, "127.0.0.1", 4321)
nio.markers = {"m": {"bpf": "icmp", "tag": None, "link_id": "L1", "direction": None, "enabled": False}}
with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm:
mm.instance.return_value.register = MagicMock()
await node._ubridge_apply_markers("VPCS-10", nio)
node._ubridge_send.assert_any_call("bridge enable_packet_filter VPCS-10 m off")
assert node._marker_filter_bridges["m", "L1"] == "VPCS-10"
def test_marker_linktype_normalizes():
# Ethernet / unset → None (linktype omitted; uBridge defaults to EN10MB).
assert VPCSVM._marker_linktype(None) is None
assert VPCSVM._marker_linktype("") is None
assert VPCSVM._marker_linktype("DLT_EN10MB") is None
# Serial DLTs from SerialPort.data_link_types, DLT_ prefix stripped.
assert VPCSVM._marker_linktype("DLT_C_HDLC") == "C_HDLC"
assert VPCSVM._marker_linktype("DLT_PPP_SERIAL") == "PPP_SERIAL"
assert VPCSVM._marker_linktype("DLT_FRELAY") == "FRELAY"
assert VPCSVM._marker_linktype("DLT_ATM_RFC1483") == "ATM_RFC1483"
# Case-insensitive input → canonical uppercase (pcap_datalink_name_to_val is
# case-sensitive and expects the uppercase form). Shared by base_node and IOU.
assert VPCSVM._marker_linktype("dlt_c_hdlc") == "C_HDLC"
@pytest.mark.asyncio
async def test_apply_markers_appends_linktype_for_serial(compute_project, manager):
# A serial data_link_type reaches the uBridge mark command as `linktype C_HDLC`
# so the BPF offsets and pcap decode match the WAN encapsulation.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
nio = NIOUDP(1234, "127.0.0.1", 4321)
nio.markers = {"m": {"bpf": "icmp", "tag": None, "link_id": "L1",
"direction": None, "data_link_type": "DLT_C_HDLC", "enabled": True}}
with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm:
mm.instance.return_value.register = MagicMock()
await node._ubridge_apply_markers("VPCS-10", nio)
sent = [c.args[0] for c in node._ubridge_send.call_args_list]
assert any("linktype C_HDLC" in s for s in sent)
@pytest.mark.asyncio
async def test_apply_markers_omits_linktype_for_ethernet(compute_project, manager):
# Ethernet (DLT_EN10MB) → no linktype keyword; uBridge defaults to EN10MB.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
nio = NIOUDP(1234, "127.0.0.1", 4321)
nio.markers = {"m": {"bpf": "icmp", "tag": None, "link_id": "L1",
"direction": None, "data_link_type": "DLT_EN10MB", "enabled": True}}
with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm:
mm.instance.return_value.register = MagicMock()
await node._ubridge_apply_markers("VPCS-10", nio)
sent = [c.args[0] for c in node._ubridge_send.call_args_list]
assert not any("linktype" in s for s in sent)
@pytest.mark.asyncio
async def test_delete_marker_capture_removes_pcap_and_entry(compute_project, manager):
# Deleting a marker's capture removes its pcap and forgets the bridge entry.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
markers_dir = compute_project.markers_working_directory()
os.makedirs(markers_dir, exist_ok=True)
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
pcap = os.path.join(markers_dir, f"{node.id}_L1_m.pcap")
open(pcap, "wb").write(b"data")
await node.delete_marker_capture("m", "L1")
assert not os.path.exists(pcap)
assert ("m", "L1") not in node._marker_filter_bridges
@pytest.mark.asyncio
async def test_delete_marker_capture_idempotent_when_missing(compute_project, manager):
# No file on disk → must not raise, and still clears the entry.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
await node.delete_marker_capture("m", "L1")
assert ("m", "L1") not in node._marker_filter_bridges
@pytest.mark.asyncio
async def test_delete_marker_capture_sends_delete_filter(compute_project, manager):
# With uBridge running, removing a marker issues a fine-grained
# delete_packet_filter (not a bridge-wide reset) so sibling pcaps stay open.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
node._ubridge_hypervisor = MagicMock()
node._ubridge_hypervisor.is_running.return_value = True
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
await node.delete_marker_capture("m", "L1")
node._ubridge_send.assert_any_call("bridge delete_packet_filter VPCS-10 m")
assert ("m", "L1") not in node._marker_filter_bridges
@pytest.mark.asyncio
async def test_delete_marker_capture_drops_from_nio_markers(compute_project, manager):
# The marker spec cached on the port NIO (nio.markers) is what
# _ubridge_apply_markers reads on node start. delete_marker_capture must drop
# it, else deleting a marker while the node is stopped leaves the spec in
# nio.markers and starting the node reinstalls it (empty pcap reappears).
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
nio = NIOUDP(1234, "127.0.0.1", 4321)
nio.markers = {"m": {"bpf": "icmp", "tag": None, "link_id": "L1",
"direction": None, "enabled": True}}
await node.delete_marker_capture("m", "L1", nio)
assert "m" not in nio.markers
@pytest.mark.asyncio
async def test_rebuild_marker_filter_delete_then_add(compute_project, manager):
# rebuild re-installs a single filter (delete_packet_filter + add) with the
# new params, no bridge reset; enabled=False turns it off after re-add.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
node._ubridge_hypervisor = MagicMock()
node._ubridge_hypervisor.is_running.return_value = True
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
await node.rebuild_marker_filter("m", "L1", "tcp", tag=7, direction="rx", enabled=False)
cmds = [c.args[0] for c in node._ubridge_send.call_args_list]
assert any("delete_packet_filter VPCS-10 m" in c for c in cmds)
assert any("add_packet_filter VPCS-10 m mark" in c and "tcp" in c for c in cmds)
assert any("enable_packet_filter VPCS-10 m off" in c for c in cmds)
@pytest.mark.asyncio
async def test_apply_markers_skips_already_installed(compute_project, manager):
# Incremental apply: a marker already in _marker_filter_bridges is not
# re-added (uBridge keeps it across reset), so its pcap isn't reopened.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
node._marker_filter_bridges["m", "L1"] = "VPCS-10" # already installed
nio = NIOUDP(1234, "127.0.0.1", 4321)
nio.markers = {"m": {"bpf": "icmp", "tag": None, "link_id": "L1", "direction": None, "enabled": True}}
with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm:
mm.instance.return_value.register = MagicMock()
await node._ubridge_apply_markers("VPCS-10", nio)
cmds = [c.args[0] for c in node._ubridge_send.call_args_list]
assert not any("add_packet_filter" in c for c in cmds) # skipped, not re-added
@pytest.mark.asyncio
async def test_apply_markers_deletes_removed(compute_project, manager):
# Reconcile: a marker no longer in nio.markers is deleted from uBridge
# (filter + registry), not left as an orphan still matching packets.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
node._ubridge_hypervisor = MagicMock()
node._ubridge_hypervisor.is_running.return_value = True
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
node._marker_specs["m", "L1"] = {"bpf": "icmp", "enabled": True}
nio = NIOUDP(1234, "127.0.0.1", 4321)
nio.markers = {} # marker removed
with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm:
mm.instance.return_value.unregister = MagicMock()
await node._ubridge_apply_markers("VPCS-10", nio)
cmds = [c.args[0] for c in node._ubridge_send.call_args_list]
assert any("delete_packet_filter VPCS-10 m" in c for c in cmds)
assert ("m", "L1") not in node._marker_filter_bridges
assert ("m", "L1") not in node._marker_specs
@pytest.mark.asyncio
async def test_apply_markers_rebuilds_changed_bpf(compute_project, manager):
# Reconcile: a marker whose bpf changed is rebuilt (delete + re-add), so
# uBridge ends up with the new expression — not the stale original.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
node._ubridge_hypervisor = MagicMock()
node._ubridge_hypervisor.is_running.return_value = True
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
node._marker_specs["m", "L1"] = {"bpf": "icmp", "tag": None, "direction": None,
"data_link_type": None, "enabled": True}
nio = NIOUDP(1234, "127.0.0.1", 4321)
nio.markers = {"m": {"bpf": "tcp", "tag": None, "link_id": "L1",
"direction": None, "enabled": True}}
with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm:
mm.instance.return_value.register = MagicMock()
await node._ubridge_apply_markers("VPCS-10", nio)
cmds = [c.args[0] for c in node._ubridge_send.call_args_list]
assert any("delete_packet_filter VPCS-10 m" in c for c in cmds) # old removed
assert any("add_packet_filter VPCS-10 m mark" in c and "tcp" in c for c in cmds) # new added
@pytest.mark.asyncio
async def test_apply_markers_preserves_markers_on_other_bridges(compute_project, manager):
# Regression: reconciling one NIO must not delete markers installed on this
# node's OTHER bridges/NIOs. _marker_filter_bridges is node-wide, but
# `desired` only carries the current NIO's markers — the delete pass must be
# scoped to the current bridge, or updating one link wipes every other link's
# markers + pcaps.
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._ubridge_send = AsyncioMagicMock()
node._ubridge_hypervisor = MagicMock()
node._ubridge_hypervisor.is_running.return_value = True
# Two links, each with a marker on its own bridge:
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
node._marker_specs["m", "L1"] = {"bpf": "icmp", "enabled": True}
node._marker_filter_bridges["m", "L2"] = "VPCS-20"
node._marker_specs["m", "L2"] = {"bpf": "tcp", "enabled": True}
# Update only the VPCS-10 NIO; "m" is gone from this link — but the marker
# on VPCS-20 (L2) must survive untouched.
nio = NIOUDP(1234, "127.0.0.1", 4321)
nio.markers = {}
with patch("gns3server.compute.marker.marker_manager.MarkerManager") as mm:
mm.instance.return_value.unregister = MagicMock()
await node._ubridge_apply_markers("VPCS-10", nio)
cmds = [c.args[0] for c in node._ubridge_send.call_args_list]
assert any("delete_packet_filter VPCS-10 m" in c for c in cmds) # L1 removed on its bridge
assert not any("VPCS-20" in c for c in cmds) # other bridge untouched
assert ("m", "L1") not in node._marker_filter_bridges
assert ("m", "L2") in node._marker_filter_bridges # L2 preserved
@pytest.mark.asyncio
async def test_stop_ubridge_clears_marker_bridges(compute_project, manager):
# uBridge stopping drops every marker filter — the map must clear so the next
# apply re-installs them instead of skipping as "already installed".
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node._marker_filter_bridges["m", "L1"] = "VPCS-10"
await node._stop_ubridge()
assert node._marker_filter_bridges == {}
class _GoneConsoleWebsocket:
"""
Stand-in for the compute-side WebSocket when the peer (the controller, or a
browser) is already gone: receive() yields the disconnect message and
send_bytes raises WebSocketDisconnect — what starlette raises from send
after the transport reports OSError.
"""
def __init__(self):
from types import SimpleNamespace
self.client = SimpleNamespace(host="127.0.0.1", port=5000)
async def receive(self):
return {"type": "websocket.disconnect"}
async def send_bytes(self, data):
from starlette.websockets import WebSocketDisconnect
raise WebSocketDisconnect(code=1006)
@pytest.mark.asyncio
async def test_console_websocket_client_disconnect_while_node_output_streams(
compute_project, manager, port_manager, monkeypatch, caplog):
# regression test: the client disconnects while the node is still streaming
# console output. telnet_forward used to let the (empty-str) WebSocketDisconnect
# from send_bytes escape, logging a message-less WARNING.
import asyncio
import logging
node = VPCSVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", compute_project, manager)
node.status = "started"
telnet_reader = MagicMock()
telnet_reader.at_eof.return_value = False
telnet_reader.read = AsyncioMagicMock(return_value=b"device output")
telnet_writer = MagicMock()
telnet_writer.wait_closed = AsyncioMagicMock()
async def fake_open_connection(*args, **kwargs):
return telnet_reader, telnet_writer
monkeypatch.setattr(asyncio, "open_connection", fake_open_connection)
with caplog.at_level(logging.INFO):
await node.start_websocket_console(_GoneConsoleWebsocket())
assert telnet_writer.close.called
assert not [r for r in caplog.records if r.levelno >= logging.WARNING]
assert any(
"has disconnected from compute console WebSocket while node output" in r.message
for r in caplog.records
)
@pytest.mark.asyncio
async def test_delete_node_working_directory(node):
working_dir = node.working_dir
with open(os.path.join(working_dir, "test.txt"), "w") as f:
f.write("TEST")
await node.delete()
assert not os.path.exists(working_dir)
@pytest.mark.asyncio
async def test_delete_directory_without_user_permissions(node):
# regression test: a failed deletion must not chmod the directory to S_IWRITE (0o200),
# which removes the search permission and makes the directory undeletable
working_dir = node.working_dir
with open(os.path.join(working_dir, "test.txt"), "w") as f:
f.write("TEST")
os.chmod(working_dir, 0o200)
try:
await node.delete()
assert not os.path.exists(working_dir)
finally:
# restore the permissions so a failed test does not leave an undeletable
# directory behind on the shared project path
if os.path.exists(working_dir):
os.chmod(working_dir, 0o700)
@pytest.mark.asyncio
async def test_delete_directory_with_readonly_entries(node):
working_dir = node.working_dir
with open(os.path.join(working_dir, "test.txt"), "w") as f:
f.write("TEST")
os.chmod(os.path.join(working_dir, "test.txt"), 0o000)
os.chmod(working_dir, 0o500) # remove the write permission
try:
await node.delete()
assert not os.path.exists(working_dir)
finally:
if os.path.exists(working_dir):
os.chmod(working_dir, 0o700)
@pytest.mark.asyncio
async def test_delete_directory_with_file_recreated_during_deletion(node, monkeypatch):
# regression test: a concurrent MD5 checksum computation can cache its result in the
# node directory while it is being deleted, recreating a file after shutil.rmtree
# has listed the directory (rmtree then silently gives up on the final rmdir)
working_dir = node.working_dir
with open(os.path.join(working_dir, "hda_disk_image.md5sum"), "w") as f:
f.write("0" * 32)
real_rmtree = shutil.rmtree
calls = 0
def rmtree_recreating_a_file(directory, onerror=None, **kwargs):
nonlocal calls
calls += 1
if calls == 1:
# delete the entries but leave one behind, as if the final rmdir
# had failed with ENOTEMPTY after the error handler returned
for entry in os.listdir(directory):
os.remove(os.path.join(directory, entry))
with open(os.path.join(directory, "hda_disk_image.md5sum"), "w") as f:
f.write("0" * 32)
return
real_rmtree(directory, onerror=onerror, **kwargs)
monkeypatch.setattr("gns3server.compute.base_node.shutil.rmtree", rmtree_recreating_a_file)
await node.delete()
assert calls == 2
assert not os.path.exists(working_dir)
@pytest.mark.asyncio
async def test_delete_directory_failure_raises(node, monkeypatch):
working_dir = node.working_dir
with open(os.path.join(working_dir, "test.txt"), "w") as f:
f.write("TEST")
def rmtree_not_deleting(directory, onerror=None, **kwargs):
pass # simulate a persistent deletion failure
monkeypatch.setattr("gns3server.compute.base_node.shutil.rmtree", rmtree_not_deleting)
with pytest.raises(ComputeError):
await node.delete()
assert os.path.exists(working_dir)