vantage6-node 5.0.3rc6__tar.gz → 5.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/PKG-INFO +5 -10
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/pyproject.toml +5 -13
- vantage6_node-5.1.0/tests_node/test_algorithm_policies.py +80 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/test_container_manager.py +154 -1
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/test_network_isolation.py +15 -4
- vantage6_node-5.1.0/tests_node/test_node_status_emit.py +51 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/test_pod_status_mapping.py +26 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/test_share_algorithm_logs.py +3 -8
- vantage6_node-5.1.0/tests_node/test_socket_transport.py +46 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/__init__.py +119 -60
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/globals.py +4 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/container_manager.py +107 -46
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/jobpod_state_to_run_status_mapper.py +30 -7
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/socket.py +1 -9
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/.gitignore +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/Makefile +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/README.md +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/dev_start.sh +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests/docker/__init__.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/__init__.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/utest.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/dev_start.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/cli/__init__.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/cli/node.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/enum.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/data_classes.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/exceptions.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/network_isolation.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/run_io.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/session_manager.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/proxy_server.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/util/__init__.py +0 -0
- {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/util/colorer.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vantage6-node
|
|
3
|
-
Version: 5.0
|
|
3
|
+
Version: 5.1.0
|
|
4
4
|
Summary: vantage6 node
|
|
5
5
|
Author: Vantage6 Team
|
|
6
6
|
Maintainer-email: Frank Martin <f.martin@iknl.nl>, Bart van Beusekom <b.vanbeusekom@iknl.nl>
|
|
7
|
-
License:
|
|
7
|
+
License: Apache-2.0
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
9
|
Requires-Dist: click==8.1.3
|
|
10
10
|
Requires-Dist: flask==3.1.3
|
|
@@ -12,17 +12,12 @@ Requires-Dist: gevent==25.4.2
|
|
|
12
12
|
Requires-Dist: jinja2==3.1.6
|
|
13
13
|
Requires-Dist: kubernetes==36.0.2
|
|
14
14
|
Requires-Dist: nvidia-ml-py==12.535.133
|
|
15
|
-
Requires-Dist: parquet-tools==0.2.16
|
|
16
15
|
Requires-Dist: psutil==7.0.0
|
|
17
16
|
Requires-Dist: python-socketio==5.16.2
|
|
18
17
|
Requires-Dist: requests==2.33.1
|
|
19
|
-
Requires-Dist: vantage6-client==5.0
|
|
20
|
-
Requires-Dist: vantage6-common==5.0
|
|
21
|
-
Requires-Dist: vantage6==5.0
|
|
22
|
-
Provides-Extra: dev
|
|
23
|
-
Requires-Dist: black; extra == 'dev'
|
|
24
|
-
Requires-Dist: coverage==7.10.2; extra == 'dev'
|
|
25
|
-
Requires-Dist: pre-commit; extra == 'dev'
|
|
18
|
+
Requires-Dist: vantage6-client==5.1.0
|
|
19
|
+
Requires-Dist: vantage6-common==5.1.0
|
|
20
|
+
Requires-Dist: vantage6==5.1.0
|
|
26
21
|
Description-Content-Type: text/markdown
|
|
27
22
|
|
|
28
23
|
<h1 align="center">
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vantage6-node"
|
|
7
|
-
version = "5.0
|
|
7
|
+
version = "5.1.0"
|
|
8
8
|
description = "vantage6 node"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
license = {text = "
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
11
|
authors = [
|
|
12
12
|
{name = "Vantage6 Team"}
|
|
13
13
|
]
|
|
@@ -23,20 +23,12 @@ dependencies = [
|
|
|
23
23
|
"jinja2==3.1.6",
|
|
24
24
|
"kubernetes==36.0.2",
|
|
25
25
|
"nvidia-ml-py==12.535.133",
|
|
26
|
-
"parquet-tools==0.2.16",
|
|
27
26
|
"psutil==7.0.0",
|
|
28
27
|
"python-socketio==5.16.2",
|
|
29
28
|
"requests==2.33.1",
|
|
30
|
-
"vantage6==5.0
|
|
31
|
-
"vantage6-client==5.0
|
|
32
|
-
"vantage6-common==5.0
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
[project.optional-dependencies]
|
|
36
|
-
dev = [
|
|
37
|
-
"coverage==7.10.2",
|
|
38
|
-
"black",
|
|
39
|
-
"pre-commit",
|
|
29
|
+
"vantage6==5.1.0",
|
|
30
|
+
"vantage6-client==5.1.0",
|
|
31
|
+
"vantage6-common==5.1.0",
|
|
40
32
|
]
|
|
41
33
|
|
|
42
34
|
[project.scripts]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from vantage6.common.globals import NodePolicy
|
|
5
|
+
|
|
6
|
+
from vantage6.node import Node
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestCheckAlgorithmPolicies(unittest.TestCase):
|
|
10
|
+
"""
|
|
11
|
+
Tests for Node.check_algorithm_policies().
|
|
12
|
+
|
|
13
|
+
Node.__init__ connects to HQ and Kubernetes, so these tests bypass it (via __new__)
|
|
14
|
+
and set up only the attributes the check reads: `config` and `log`.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def _node(self, config: dict) -> Node:
|
|
18
|
+
node = Node.__new__(Node)
|
|
19
|
+
node.config = config
|
|
20
|
+
node.log = MagicMock()
|
|
21
|
+
return node
|
|
22
|
+
|
|
23
|
+
def _assert_exits(self, config: dict):
|
|
24
|
+
node = self._node(config)
|
|
25
|
+
with (
|
|
26
|
+
patch("vantage6.node.sys.exit", side_effect=SystemExit(1)) as mock_exit,
|
|
27
|
+
self.assertRaises(SystemExit),
|
|
28
|
+
):
|
|
29
|
+
node.check_algorithm_policies()
|
|
30
|
+
mock_exit.assert_called_once_with(1)
|
|
31
|
+
node.log.error.assert_called_once()
|
|
32
|
+
|
|
33
|
+
def _assert_starts(self, config: dict) -> Node:
|
|
34
|
+
node = self._node(config)
|
|
35
|
+
with patch("vantage6.node.sys.exit") as mock_exit:
|
|
36
|
+
node.check_algorithm_policies()
|
|
37
|
+
mock_exit.assert_not_called()
|
|
38
|
+
return node
|
|
39
|
+
|
|
40
|
+
def test_production_without_policies_exits(self):
|
|
41
|
+
self._assert_exits({"production": True})
|
|
42
|
+
|
|
43
|
+
def test_production_is_default(self):
|
|
44
|
+
self._assert_exits({})
|
|
45
|
+
|
|
46
|
+
def test_production_with_empty_policies_exits(self):
|
|
47
|
+
self._assert_exits(
|
|
48
|
+
{
|
|
49
|
+
"policies": {
|
|
50
|
+
NodePolicy.ALLOWED_ALGORITHMS.value: [],
|
|
51
|
+
NodePolicy.ALLOWED_ALGORITHM_STORES.value: [],
|
|
52
|
+
NodePolicy.ALLOWED_USERS.value: [1],
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def test_production_with_allowed_algorithms_starts(self):
|
|
58
|
+
node = self._assert_starts(
|
|
59
|
+
{
|
|
60
|
+
"production": True,
|
|
61
|
+
"policies": {NodePolicy.ALLOWED_ALGORITHMS.value: ["^some/image$"]},
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
node.log.warning.assert_not_called()
|
|
65
|
+
|
|
66
|
+
def test_production_with_allowed_stores_starts(self):
|
|
67
|
+
node = self._assert_starts(
|
|
68
|
+
{
|
|
69
|
+
"production": True,
|
|
70
|
+
"policies": {
|
|
71
|
+
NodePolicy.ALLOWED_ALGORITHM_STORES.value: ["https://store.example"]
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
node.log.warning.assert_not_called()
|
|
76
|
+
|
|
77
|
+
def test_non_production_without_policies_warns(self):
|
|
78
|
+
node = self._assert_starts({"production": False})
|
|
79
|
+
node.log.warning.assert_called()
|
|
80
|
+
node.log.error.assert_not_called()
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import tempfile
|
|
3
4
|
import unittest
|
|
5
|
+
from pathlib import Path
|
|
4
6
|
from unittest.mock import MagicMock, patch
|
|
5
7
|
|
|
6
|
-
from vantage6.common.
|
|
8
|
+
from vantage6.common.enum import AlgorithmStepType, TaskDatabaseType
|
|
9
|
+
from vantage6.common.globals import ContainerEnvNames, NodePolicy
|
|
7
10
|
|
|
8
11
|
from vantage6.node.k8s.container_manager import ContainerManager
|
|
9
12
|
|
|
@@ -215,5 +218,155 @@ class TestGetImagePullPolicy:
|
|
|
215
218
|
assert manager._get_image_pull_policy() == "Always"
|
|
216
219
|
|
|
217
220
|
|
|
221
|
+
class TestDatabaseMountMode(unittest.TestCase):
|
|
222
|
+
"""
|
|
223
|
+
Tests that the configured database mount mode is applied to algorithm pods
|
|
224
|
+
(GHSA-27gf-8rcw-jqgh): 'ro' (default) databases are mounted read-only and file
|
|
225
|
+
databases in 'copy' mode are copied, so algorithms never get write access to the
|
|
226
|
+
original.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
HOST_DATA_DIR = "/host/tasks"
|
|
230
|
+
|
|
231
|
+
def setUp(self):
|
|
232
|
+
tmp_dir = tempfile.TemporaryDirectory()
|
|
233
|
+
self.addCleanup(tmp_dir.cleanup)
|
|
234
|
+
self.db_dir = Path(tmp_dir.name) / "databases"
|
|
235
|
+
self.tasks_dir = Path(tmp_dir.name) / "tasks"
|
|
236
|
+
self.db_dir.mkdir()
|
|
237
|
+
(self.db_dir / "file_db.csv").write_text("a,b\n1,2\n")
|
|
238
|
+
(self.db_dir / "dir_db.parquet").mkdir()
|
|
239
|
+
|
|
240
|
+
for name, value in [
|
|
241
|
+
("DATABASE_BASE_PATH", str(self.db_dir)),
|
|
242
|
+
("TASK_FILES_ROOT", str(self.tasks_dir)),
|
|
243
|
+
]:
|
|
244
|
+
patcher = patch(f"vantage6.node.k8s.container_manager.{name}", value)
|
|
245
|
+
patcher.start()
|
|
246
|
+
self.addCleanup(patcher.stop)
|
|
247
|
+
|
|
248
|
+
self.manager = ContainerManager.__new__(ContainerManager)
|
|
249
|
+
self.manager.log = _get_null_logger()
|
|
250
|
+
self.manager.host_data_dir = self.HOST_DATA_DIR
|
|
251
|
+
|
|
252
|
+
def _get_databases(self, env: dict) -> dict:
|
|
253
|
+
with patch.dict(os.environ, env, clear=True):
|
|
254
|
+
return self.manager._get_database_metadata()
|
|
255
|
+
|
|
256
|
+
@staticmethod
|
|
257
|
+
def _file_db_env(mount_mode: str | None = None) -> dict:
|
|
258
|
+
env = {
|
|
259
|
+
"DATABASE_LABELS": "file_db",
|
|
260
|
+
"DATABASE_FILE_DB_URI": "/data/file_db.csv",
|
|
261
|
+
"DATABASE_FILE_DB_TYPE": "csv",
|
|
262
|
+
}
|
|
263
|
+
if mount_mode is not None:
|
|
264
|
+
env["DATABASE_FILE_DB_MOUNT_MODE"] = mount_mode
|
|
265
|
+
return env
|
|
266
|
+
|
|
267
|
+
def test_read_only_file_is_not_copied(self):
|
|
268
|
+
db = self._get_databases(self._file_db_env("ro"))["file_db"]
|
|
269
|
+
|
|
270
|
+
self.assertEqual(db.mount_mode, "ro")
|
|
271
|
+
self.assertEqual(db.uri, "/data/file_db.csv")
|
|
272
|
+
self.assertEqual(db.local_uri, str(self.db_dir / "file_db.csv"))
|
|
273
|
+
self.assertFalse((self.tasks_dir / "databases").exists())
|
|
274
|
+
|
|
275
|
+
def test_mount_mode_defaults_to_read_only(self):
|
|
276
|
+
db = self._get_databases(self._file_db_env())["file_db"]
|
|
277
|
+
|
|
278
|
+
self.assertEqual(db.mount_mode, "ro")
|
|
279
|
+
self.assertEqual(db.uri, "/data/file_db.csv")
|
|
280
|
+
self.assertFalse((self.tasks_dir / "databases").exists())
|
|
281
|
+
|
|
282
|
+
def test_copy_mode_copies_file(self):
|
|
283
|
+
db = self._get_databases(self._file_db_env("copy"))["file_db"]
|
|
284
|
+
|
|
285
|
+
self.assertEqual(db.mount_mode, "copy")
|
|
286
|
+
self.assertEqual(db.uri, f"{self.HOST_DATA_DIR}/databases/file_db.csv")
|
|
287
|
+
self.assertEqual(db.local_uri, str(self.db_dir / "file_db.csv"))
|
|
288
|
+
self.assertEqual(
|
|
289
|
+
(self.tasks_dir / "databases" / "file_db.csv").read_text(), "a,b\n1,2\n"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def test_mount_mode_is_case_insensitive(self):
|
|
293
|
+
db = self._get_databases(self._file_db_env("RO"))["file_db"]
|
|
294
|
+
self.assertEqual(db.mount_mode, "ro")
|
|
295
|
+
|
|
296
|
+
def test_copy_mode_directory_is_not_copied(self):
|
|
297
|
+
env = {
|
|
298
|
+
"DATABASE_LABELS": "dir_db",
|
|
299
|
+
"DATABASE_DIR_DB_URI": "/data/dir_db",
|
|
300
|
+
"DATABASE_DIR_DB_TYPE": "parquet",
|
|
301
|
+
"DATABASE_DIR_DB_MOUNT_MODE": "copy",
|
|
302
|
+
}
|
|
303
|
+
db = self._get_databases(env)["dir_db"]
|
|
304
|
+
|
|
305
|
+
self.assertEqual(db.mount_mode, "copy")
|
|
306
|
+
self.assertEqual(db.uri, "/data/dir_db")
|
|
307
|
+
self.assertFalse((self.tasks_dir / "databases").exists())
|
|
308
|
+
|
|
309
|
+
def test_invalid_mount_mode_raises(self):
|
|
310
|
+
with self.assertRaises(ValueError):
|
|
311
|
+
self._get_databases(self._file_db_env("rw"))
|
|
312
|
+
|
|
313
|
+
def test_mount_mode_not_passed_to_algorithm_env(self):
|
|
314
|
+
env = self._file_db_env("ro")
|
|
315
|
+
env["DATABASE_FILE_DB_SOME_KEY"] = "some_value"
|
|
316
|
+
db = self._get_databases(env)["file_db"]
|
|
317
|
+
|
|
318
|
+
self.assertEqual(db.env, {"SOME_KEY": "some_value"})
|
|
319
|
+
|
|
320
|
+
def _create_extraction_mounts(self, env: dict, label: str):
|
|
321
|
+
self.manager.databases = self._get_databases(env)
|
|
322
|
+
run_io = MagicMock()
|
|
323
|
+
run_io.action = AlgorithmStepType.DATA_EXTRACTION
|
|
324
|
+
run_io.run_id = 1
|
|
325
|
+
run_io.create_files.return_value = ("1/input", "1/output")
|
|
326
|
+
run_io.session_file_manager.session_folder = "session"
|
|
327
|
+
run_io.session_file_manager.session_state_file_name = "state.parquet"
|
|
328
|
+
databases_to_use = [{"label": label, "type": TaskDatabaseType.SOURCE}]
|
|
329
|
+
|
|
330
|
+
with patch.object(self.manager, "_validate_source_database"):
|
|
331
|
+
return self.manager._create_volume_mounts(run_io, b"", databases_to_use)
|
|
332
|
+
|
|
333
|
+
def _get_extraction_db_mount(self, mount_mode: str):
|
|
334
|
+
volumes, vol_mounts, _, _ = self._create_extraction_mounts(
|
|
335
|
+
self._file_db_env(mount_mode), "file_db"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
volume = next(v for v in volumes if v.name == "task-1-db-file_db")
|
|
339
|
+
vol_mount = next(m for m in vol_mounts if m.name == "task-1-db-file_db")
|
|
340
|
+
return volume, vol_mount
|
|
341
|
+
|
|
342
|
+
def test_read_only_database_mounted_read_only(self):
|
|
343
|
+
volume, vol_mount = self._get_extraction_db_mount("ro")
|
|
344
|
+
|
|
345
|
+
self.assertTrue(vol_mount.read_only)
|
|
346
|
+
self.assertEqual(volume.host_path.path, "/data/file_db.csv")
|
|
347
|
+
|
|
348
|
+
def test_copy_database_mounts_copy_read_write(self):
|
|
349
|
+
volume, vol_mount = self._get_extraction_db_mount("copy")
|
|
350
|
+
|
|
351
|
+
self.assertFalse(vol_mount.read_only)
|
|
352
|
+
self.assertEqual(
|
|
353
|
+
volume.host_path.path, f"{self.HOST_DATA_DIR}/databases/file_db.csv"
|
|
354
|
+
)
|
|
355
|
+
self.assertEqual(vol_mount.mount_path, str(self.db_dir / "file_db.csv"))
|
|
356
|
+
|
|
357
|
+
def test_service_database_ignores_default_read_only(self):
|
|
358
|
+
env = {
|
|
359
|
+
"DATABASE_LABELS": "pg_db",
|
|
360
|
+
"DATABASE_PG_DB_URI": "postgresql://host:5432/db",
|
|
361
|
+
"DATABASE_PG_DB_TYPE": "sql",
|
|
362
|
+
}
|
|
363
|
+
volumes, _, env_vars, _ = self._create_extraction_mounts(env, "pg_db")
|
|
364
|
+
|
|
365
|
+
self.assertFalse(any(v.name == "task-1-db-pg_db" for v in volumes))
|
|
366
|
+
self.assertEqual(
|
|
367
|
+
env_vars[ContainerEnvNames.DATABASE_URI.value], "postgresql://host:5432/db"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
218
371
|
if __name__ == "__main__":
|
|
219
372
|
unittest.main()
|
|
@@ -3,6 +3,8 @@ from unittest.mock import MagicMock, patch
|
|
|
3
3
|
import pytest
|
|
4
4
|
from kubernetes import client as k8s_client
|
|
5
5
|
|
|
6
|
+
from vantage6.common.globals import NodePolicy
|
|
7
|
+
|
|
6
8
|
from vantage6.node import Node
|
|
7
9
|
from vantage6.node.k8s.network_isolation import (
|
|
8
10
|
_probe_pod_reached_target,
|
|
@@ -69,10 +71,13 @@ class TestValidateAlgorithmIsolation:
|
|
|
69
71
|
assert "verified" in message
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
_ALGORITHM_POLICY = {NodePolicy.ALLOWED_ALGORITHMS.value: ["^some/image$"]}
|
|
75
|
+
|
|
76
|
+
|
|
72
77
|
class TestNodeProductionGating:
|
|
73
78
|
def test_production_true_exits_on_isolation_failure(self):
|
|
74
79
|
ctx = MagicMock()
|
|
75
|
-
ctx.config = {"production": True}
|
|
80
|
+
ctx.config = {"production": True, "policies": _ALGORITHM_POLICY}
|
|
76
81
|
|
|
77
82
|
with (
|
|
78
83
|
patch("vantage6.node.validate_required_env_vars"),
|
|
@@ -84,7 +89,9 @@ class TestNodeProductionGating:
|
|
|
84
89
|
False,
|
|
85
90
|
"probe failed",
|
|
86
91
|
)
|
|
87
|
-
with patch(
|
|
92
|
+
with patch(
|
|
93
|
+
"vantage6.node.sys.exit", side_effect=SystemExit(1)
|
|
94
|
+
) as mock_exit:
|
|
88
95
|
with (
|
|
89
96
|
patch.object(Node, "_setup_node_client", return_value=MagicMock()),
|
|
90
97
|
pytest.raises(SystemExit),
|
|
@@ -94,7 +101,11 @@ class TestNodeProductionGating:
|
|
|
94
101
|
|
|
95
102
|
def test_production_false_warns_on_isolation_failure(self, caplog):
|
|
96
103
|
ctx = MagicMock()
|
|
97
|
-
ctx.config = {
|
|
104
|
+
ctx.config = {
|
|
105
|
+
"production": False,
|
|
106
|
+
"debug": {},
|
|
107
|
+
"policies": _ALGORITHM_POLICY,
|
|
108
|
+
}
|
|
98
109
|
|
|
99
110
|
def _connect_socket(self):
|
|
100
111
|
self.socketIO = MagicMock()
|
|
@@ -109,7 +120,7 @@ class TestNodeProductionGating:
|
|
|
109
120
|
False,
|
|
110
121
|
"probe failed",
|
|
111
122
|
)
|
|
112
|
-
with patch("vantage6.node.exit") as mock_exit:
|
|
123
|
+
with patch("vantage6.node.sys.exit") as mock_exit:
|
|
113
124
|
with (
|
|
114
125
|
patch.object(Node, "_setup_node_client", return_value=MagicMock()),
|
|
115
126
|
patch.object(Node, "authenticate"),
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
|
|
4
|
+
from socketio.exceptions import BadNamespaceError
|
|
5
|
+
|
|
6
|
+
from vantage6.common.enum import RunStatus
|
|
7
|
+
|
|
8
|
+
from vantage6.node import Node
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestEmitAlgorithmStatusChange(unittest.TestCase):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
self.node = Node.__new__(Node)
|
|
14
|
+
self.node.log = Mock()
|
|
15
|
+
self.node.socketIO = Mock()
|
|
16
|
+
self.node.socketIO.namespaces = {"/tasks": "sid"}
|
|
17
|
+
|
|
18
|
+
def _emit(self):
|
|
19
|
+
with patch("vantage6.node.time.sleep"):
|
|
20
|
+
self.node._Node__emit_algorithm_status_change(
|
|
21
|
+
run_id=20, status=RunStatus.ACTIVE
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def test_emit_when_connected(self):
|
|
25
|
+
self._emit()
|
|
26
|
+
|
|
27
|
+
self.node.socketIO.emit.assert_called_once_with(
|
|
28
|
+
"algorithm_status_change",
|
|
29
|
+
data={"run_id": 20, "status": RunStatus.ACTIVE.value},
|
|
30
|
+
namespace="/tasks",
|
|
31
|
+
)
|
|
32
|
+
self.node.log.warning.assert_not_called()
|
|
33
|
+
|
|
34
|
+
def test_skip_when_namespace_never_connects(self):
|
|
35
|
+
self.node.socketIO.namespaces = {}
|
|
36
|
+
|
|
37
|
+
self._emit()
|
|
38
|
+
|
|
39
|
+
self.node.socketIO.emit.assert_not_called()
|
|
40
|
+
self.node.log.warning.assert_called_once()
|
|
41
|
+
|
|
42
|
+
def test_no_exception_when_namespace_drops_during_emit(self):
|
|
43
|
+
self.node.socketIO.emit.side_effect = BadNamespaceError("/tasks")
|
|
44
|
+
|
|
45
|
+
self._emit()
|
|
46
|
+
|
|
47
|
+
self.node.log.warning.assert_called_once()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
unittest.main()
|
|
@@ -96,6 +96,32 @@ class TestPodStatus(unittest.TestCase):
|
|
|
96
96
|
RunStatus.NO_DOCKER_IMAGE,
|
|
97
97
|
)
|
|
98
98
|
|
|
99
|
+
def test_image_being_pulled(self):
|
|
100
|
+
self.mock_pod.status.phase = "Pending"
|
|
101
|
+
self.mock_pod.status.container_statuses[0].state = self.waiting_container_state
|
|
102
|
+
self.mock_pod.status.container_statuses[
|
|
103
|
+
0
|
|
104
|
+
].state.waiting.reason = "ContainerCreating"
|
|
105
|
+
self.assertEqual(
|
|
106
|
+
compute_run_pod_status(
|
|
107
|
+
log=self.silent_logger, task_namespace="", label="", pod=self.mock_pod
|
|
108
|
+
),
|
|
109
|
+
RunStatus.PULLING_IMAGE,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def test_pod_still_initializing(self):
|
|
113
|
+
self.mock_pod.status.phase = "Pending"
|
|
114
|
+
self.mock_pod.status.container_statuses[0].state = self.waiting_container_state
|
|
115
|
+
self.mock_pod.status.container_statuses[
|
|
116
|
+
0
|
|
117
|
+
].state.waiting.reason = "PodInitializing"
|
|
118
|
+
self.assertEqual(
|
|
119
|
+
compute_run_pod_status(
|
|
120
|
+
log=self.silent_logger, task_namespace="", label="", pod=self.mock_pod
|
|
121
|
+
),
|
|
122
|
+
RunStatus.INITIALIZING,
|
|
123
|
+
)
|
|
124
|
+
|
|
99
125
|
def test_waiting_reason_not_ready_err(self):
|
|
100
126
|
self.mock_pod.status.phase = "Pending"
|
|
101
127
|
self.mock_pod.status.container_statuses[0].state = self.waiting_container_state
|
|
@@ -33,7 +33,7 @@ class TestContainerManagerLogSharing(unittest.TestCase):
|
|
|
33
33
|
with patch(
|
|
34
34
|
"vantage6.node.k8s.container_manager.watch.Watch", return_value=fake_watch
|
|
35
35
|
):
|
|
36
|
-
manager._ContainerManager__log_stream(run_io=run_io
|
|
36
|
+
manager._ContainerManager__log_stream(run_io=run_io)
|
|
37
37
|
|
|
38
38
|
manager.socket_io.emit.assert_not_called()
|
|
39
39
|
|
|
@@ -46,16 +46,11 @@ class TestContainerManagerLogSharing(unittest.TestCase):
|
|
|
46
46
|
with patch(
|
|
47
47
|
"vantage6.node.k8s.container_manager.watch.Watch", return_value=fake_watch
|
|
48
48
|
):
|
|
49
|
-
manager._ContainerManager__log_stream(run_io=run_io
|
|
49
|
+
manager._ContainerManager__log_stream(run_io=run_io)
|
|
50
50
|
|
|
51
51
|
manager.socket_io.emit.assert_called_once_with(
|
|
52
52
|
"algorithm_log",
|
|
53
|
-
{
|
|
54
|
-
"collaboration_id": 7,
|
|
55
|
-
"run_id": 42,
|
|
56
|
-
"task_id": 99,
|
|
57
|
-
"log": "hello from algorithm",
|
|
58
|
-
},
|
|
53
|
+
{"run_id": 42, "log": "hello from algorithm"},
|
|
59
54
|
namespace="/tasks",
|
|
60
55
|
)
|
|
61
56
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
|
|
4
|
+
from engineio.payload import Payload
|
|
5
|
+
|
|
6
|
+
from vantage6.common.globals import SOCKETIO_MAX_DECODE_PACKETS
|
|
7
|
+
|
|
8
|
+
from vantage6.node import Node
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestSocketTransport(unittest.TestCase):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
# the packet limit is a class attribute, so reset it to the library default
|
|
14
|
+
self.addCleanup(
|
|
15
|
+
setattr, Payload, "max_decode_packets", Payload.max_decode_packets
|
|
16
|
+
)
|
|
17
|
+
Payload.max_decode_packets = 16
|
|
18
|
+
|
|
19
|
+
def _make_node(self, config: dict) -> Node:
|
|
20
|
+
node = Node.__new__(Node)
|
|
21
|
+
node.log = Mock()
|
|
22
|
+
node.config = config
|
|
23
|
+
node.debug = {}
|
|
24
|
+
node.client = Mock()
|
|
25
|
+
node.client.hq_url = "http://hq.example"
|
|
26
|
+
node.client.headers = {"Authorization": "Bearer token"}
|
|
27
|
+
return node
|
|
28
|
+
|
|
29
|
+
def _connect(self, config: dict) -> Mock:
|
|
30
|
+
node = self._make_node(config)
|
|
31
|
+
with patch("vantage6.node.SocketIO") as socketio_cls:
|
|
32
|
+
socketio_cls.return_value.connected = True
|
|
33
|
+
node.connect_to_socket()
|
|
34
|
+
return socketio_cls.return_value.connect
|
|
35
|
+
|
|
36
|
+
def test_websocket_only_by_default(self):
|
|
37
|
+
connect = self._connect({})
|
|
38
|
+
self.assertEqual(connect.call_args.kwargs["transports"], ["websocket"])
|
|
39
|
+
|
|
40
|
+
def test_http_polling_allowed(self):
|
|
41
|
+
connect = self._connect({"allow_http_polling": True})
|
|
42
|
+
self.assertIsNone(connect.call_args.kwargs["transports"])
|
|
43
|
+
|
|
44
|
+
def test_max_decode_packets_raised(self):
|
|
45
|
+
self._connect({})
|
|
46
|
+
self.assertEqual(Payload.max_decode_packets, SOCKETIO_MAX_DECODE_PACKETS)
|