diff --git a/gns3server/compute/dynamips/nodes/router.py b/gns3server/compute/dynamips/nodes/router.py index e53f6fc6..f3d0bc80 100644 --- a/gns3server/compute/dynamips/nodes/router.py +++ b/gns3server/compute/dynamips/nodes/router.py @@ -35,6 +35,8 @@ from ...base_node import BaseNode from ..dynamips_error import DynamipsError from ..nios.nio_udp import NIOUDP + +from gns3server.utils.file_watcher import FileWatcher from gns3server.utils.asyncio import wait_run_in_executor, monitor_process from gns3server.utils.images import md5sum @@ -92,6 +94,7 @@ class Router(BaseNode): self._system_id = "FTX0945W0MY" # processor board ID in IOS self._slots = [] self._ghost_flag = ghost_flag + self._memory_watcher = None if not ghost_flag: if not dynamips_id: @@ -160,6 +163,12 @@ class Router(BaseNode): return router_info + def _memory_changed(self, path): + """ + Called when the NVRAM file has changed + """ + asyncio.async(self.save_configs()) + @property def dynamips_id(self): """ @@ -248,6 +257,8 @@ class Router(BaseNode): yield from self._hypervisor.send('vm start "{name}"'.format(name=self._name)) self.status = "started" log.info('router "{name}" [{id}] has been started'.format(name=self._name, id=self._id)) + + self._memory_watcher = FileWatcher(self._memory_files(), self._memory_changed, strategy='hash', delay=30) monitor_process(self._hypervisor.process, self._termination_callback) @asyncio.coroutine @@ -278,6 +289,9 @@ class Router(BaseNode): log.warn("Could not stop {}: {}".format(self._name, e)) self.status = "stopped" log.info('Router "{name}" [{id}] has been stopped'.format(name=self._name, id=self._id)) + if self._memory_watcher: + self._memory_watcher.close() + self._memory_watcher = None yield from self.save_configs() @asyncio.coroutine @@ -1599,3 +1613,10 @@ class Router(BaseNode): yield from self._hypervisor.send('vm clean_delete "{}"'.format(self._name)) self._hypervisor.devices.remove(self) log.info('Router "{name}" [{id}] has been deleted (including associated files)'.format(name=self._name, id=self._id)) + + def _memory_files(self): + project_dir = os.path.join(self.project.module_working_directory(self.manager.module_name.lower())) + return [ + os.path.join(project_dir, "{}_i{}_rom".format(self.platform, self.dynamips_id)), + os.path.join(project_dir, "{}_i{}_nvram".format(self.platform, self.dynamips_id)) + ] diff --git a/gns3server/compute/iou/iou_vm.py b/gns3server/compute/iou/iou_vm.py index f5ea8937..ab510ef4 100644 --- a/gns3server/compute/iou/iou_vm.py +++ b/gns3server/compute/iou/iou_vm.py @@ -503,7 +503,7 @@ class IOUVM(BaseNode): # check if there is enough RAM to run self.check_available_ram(self.ram) - self._nvram_watcher = FileWatcher(self._nvram_file(), self._nvram_changed) + self._nvram_watcher = FileWatcher(self._nvram_file(), self._nvram_changed, delay=10) # created a environment variable pointing to the iourc file. env = os.environ.copy() diff --git a/gns3server/utils/file_watcher.py b/gns3server/utils/file_watcher.py index 2de0f7c1..415c6eb0 100644 --- a/gns3server/utils/file_watcher.py +++ b/gns3server/utils/file_watcher.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import zlib import asyncio import os @@ -22,20 +23,43 @@ import os class FileWatcher: """ Watch for file change and call the callback when something happen + + :param paths: A path or a list of file to watch + :param delay: Delay between file check (seconds) + :param strategy: File change strategy (mtime: modification time, hash: hash compute) """ - def __init__(self, path, callback, delay=1): - if not isinstance(path, str): - path = str(path) - self._path = path + def __init__(self, paths, callback, delay=1, strategy='mtime'): + self._paths = [] + if not isinstance(paths, list): + paths = [paths] + for path in paths: + if not isinstance(path, str): + path = str(path) + self._paths.append(path) + self._callback = callback self._delay = delay self._closed = False + self._strategy = strategy - try: - self._mtime = os.stat(path).st_mtime_ns - except OSError: - self._mtime = None + if self._strategy == 'mtime': + # Store modification time + self._mtime = {} + for path in self._paths: + try: + self._mtime[path] = os.stat(path).st_mtime_ns + except OSError: + self._mtime[path] = None + else: + # Store hash + self._hashed = {} + for path in self._paths: + try: + # Alder32 is a fast bu insecure hash algorithm + 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) def __del__(self): @@ -48,15 +72,26 @@ class FileWatcher: if self._closed: return changed = False - try: - mtime = os.stat(self._path).st_mtime_ns - if mtime != self._mtime: - changed = True - self._mtime = mtime - except OSError: - self._mtime = None - if changed: - self._callback(self._path) + + for path in self._paths: + if self._strategy == 'mtime': + try: + mtime = os.stat(path).st_mtime_ns + if mtime != self._mtime[path]: + changed = True + self._mtime[path] = mtime + except OSError: + self._mtime[path] = None + else: + try: + hashc = zlib.adler32(open(path, 'rb').read()) + if hashc != self._hashed[path]: + changed = True + self._hashed[path] = hashc + except OSError: + self._hashed[path] = None + if changed: + self._callback(path) asyncio.get_event_loop().call_later(self._delay, self._check_config_file_change) @property diff --git a/tests/utils/test_file_watcher.py b/tests/utils/test_file_watcher.py index 11939d5f..2f07315c 100644 --- a/tests/utils/test_file_watcher.py +++ b/tests/utils/test_file_watcher.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import pytest import asyncio from unittest.mock import MagicMock @@ -22,11 +23,12 @@ from unittest.mock import MagicMock from gns3server.utils.file_watcher import FileWatcher -def test_file_watcher(async_run, tmpdir): +@pytest.mark.parametrize("strategy", ['mtime', 'hash']) +def test_file_watcher(async_run, tmpdir, strategy): file = tmpdir / "test" file.write("a") callback = MagicMock() - fw = FileWatcher(file, callback, delay=0.5) + fw = FileWatcher(file, callback, delay=0.5, strategy=strategy) async_run(asyncio.sleep(1)) assert not callback.called file.write("b") @@ -34,12 +36,29 @@ def test_file_watcher(async_run, tmpdir): callback.assert_called_with(str(file)) -def test_file_watcher_not_existing(async_run, tmpdir): +@pytest.mark.parametrize("strategy", ['mtime', 'hash']) +def test_file_watcher_not_existing(async_run, tmpdir, strategy): file = tmpdir / "test" callback = MagicMock() - fw = FileWatcher(file, callback, delay=0.5) + fw = FileWatcher(file, callback, delay=0.5, strategy=strategy) async_run(asyncio.sleep(1)) assert not callback.called file.write("b") async_run(asyncio.sleep(1.5)) callback.assert_called_with(str(file)) + + +@pytest.mark.parametrize("strategy", ['mtime', 'hash']) +def test_file_watcher_list(async_run, tmpdir, strategy): + file = tmpdir / "test" + file.write("a") + + file2 = tmpdir / "test2" + + callback = MagicMock() + fw = FileWatcher([file, file2], callback, delay=0.5, strategy=strategy) + async_run(asyncio.sleep(1)) + assert not callback.called + file2.write("b") + async_run(asyncio.sleep(1.5)) + callback.assert_called_with(str(file2))