vantage6-node 4.15.1__tar.gz → 4.15.2__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.
Files changed (32) hide show
  1. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/PKG-INFO +4 -4
  2. vantage6_node-4.15.2/tests/docker/test_docker_manager.py +140 -0
  3. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/_version.py +1 -1
  4. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/docker/docker_manager.py +58 -33
  5. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6_node.egg-info/PKG-INFO +4 -4
  6. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6_node.egg-info/SOURCES.txt +1 -0
  7. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6_node.egg-info/requires.txt +3 -3
  8. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/setup.cfg +0 -0
  9. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/setup.py +0 -0
  10. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/tests/__init__.py +0 -0
  11. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/tests/docker/__init__.py +0 -0
  12. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/tests/test_ssh_tunnel.py +0 -0
  13. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/__build__ +0 -0
  14. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/__init__.py +0 -0
  15. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/cli/__init__.py +0 -0
  16. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/cli/node.py +0 -0
  17. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/context.py +0 -0
  18. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/docker/docker_base.py +0 -0
  19. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/docker/exceptions.py +0 -0
  20. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/docker/squid.py +0 -0
  21. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/docker/ssh_tunnel.py +0 -0
  22. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/docker/task_manager.py +0 -0
  23. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/docker/utils.py +0 -0
  24. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/docker/vpn_manager.py +0 -0
  25. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/globals.py +0 -0
  26. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/proxy_server.py +0 -0
  27. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/socket.py +0 -0
  28. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/util/__init__.py +0 -0
  29. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6/node/util/colorer.py +0 -0
  30. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6_node.egg-info/dependency_links.txt +0 -0
  31. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6_node.egg-info/entry_points.txt +0 -0
  32. {vantage6_node-4.15.1 → vantage6_node-4.15.2}/vantage6_node.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vantage6_node
3
- Version: 4.15.1
3
+ Version: 4.15.2
4
4
  Summary: vantage6 node
5
5
  Home-page: https://github.com/vantage6/vantage6
6
6
  Requires-Python: >=3.10
@@ -13,9 +13,9 @@ Requires-Dist: python-socketio==5.15.0
13
13
  Requires-Dist: requests==2.33.1
14
14
  Requires-Dist: psutil==5.9.0
15
15
  Requires-Dist: nvidia-ml-py==12.535.133
16
- Requires-Dist: vantage6==4.15.1
17
- Requires-Dist: vantage6-client==4.15.1
18
- Requires-Dist: vantage6-algorithm-tools==4.15.1
16
+ Requires-Dist: vantage6==4.15.2
17
+ Requires-Dist: vantage6-client==4.15.2
18
+ Requires-Dist: vantage6-algorithm-tools==4.15.2
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: coverage==6.4.4; extra == "dev"
21
21
  Requires-Dist: python-coveralls==2.9.3; extra == "dev"
@@ -0,0 +1,140 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock
3
+
4
+ from vantage6.common.globals import NodePolicy
5
+
6
+ from vantage6.node.docker.docker_manager import DockerManager
7
+
8
+
9
+ class TestIsDockerImageAllowed(unittest.TestCase):
10
+ """
11
+ Tests for DockerManager.is_docker_image_allowed().
12
+
13
+ DockerManager.__init__ connects to the Docker daemon, so these tests bypass it
14
+ (via __new__) and set up only the attributes is_docker_image_allowed() actually
15
+ reads: `_policies` and `client` (`log` is already a class attribute, so it's
16
+ available for free). This keeps `_is_regex_pattern` (a real staticmethod on the
17
+ class) working, unlike a fully mocked `self` would.
18
+ """
19
+
20
+ def setUp(self):
21
+ self.manager = DockerManager.__new__(DockerManager)
22
+ self.manager.client = MagicMock()
23
+ self.manager._policies = {}
24
+
25
+ @staticmethod
26
+ def _task_info(store_id: int | None = None) -> dict:
27
+ task_info = {"init_org": {"id": 1}, "init_user": {"id": 1}}
28
+ if store_id is not None:
29
+ task_info["algorithm_store"] = {"id": store_id}
30
+ return task_info
31
+
32
+ def test_allowed_algorithms_exact_match(self):
33
+ """Baseline regression check for the (unchanged) allowed_algorithms policy."""
34
+ self.manager._policies = {
35
+ NodePolicy.ALLOWED_ALGORITHMS: ["some/image:tag"]
36
+ }
37
+
38
+ self.assertTrue(
39
+ self.manager.is_docker_image_allowed("some/image:tag", self._task_info())
40
+ )
41
+ self.assertFalse(
42
+ self.manager.is_docker_image_allowed(
43
+ "some/other-image:tag", self._task_info()
44
+ )
45
+ )
46
+
47
+ def test_allowed_algorithm_stores_verified(self):
48
+ """
49
+ The store URL is whitelisted and the store itself confirms the image is a
50
+ registered, approved algorithm there - the image is allowed.
51
+ """
52
+ self.manager._policies = {
53
+ NodePolicy.ALLOWED_ALGORITHM_STORES: ["https://store.example"]
54
+ }
55
+ self.manager.client.algorithm_store.get.return_value = {
56
+ "url": "https://store.example"
57
+ }
58
+ self.manager.client.algorithm_store.get_algorithm.return_value = {
59
+ "image": "some/image:tag",
60
+ "digest": "abc123",
61
+ }
62
+
63
+ self.assertTrue(
64
+ self.manager.is_docker_image_allowed(
65
+ "some/image:tag", self._task_info(store_id=1)
66
+ )
67
+ )
68
+ self.manager.client.algorithm_store.get_algorithm.assert_called_once_with(
69
+ "some/image:tag"
70
+ )
71
+
72
+ def test_allowed_algorithm_stores_not_confirmed_by_store(self):
73
+ """
74
+ The store URL is whitelisted, but the store does not confirm the image is
75
+ registered/approved there (e.g. it genuinely isn't, the store hasn't been
76
+ upgraded to support node requests, or it could not be reached) - the image
77
+ is denied. This is the fix for GHSA-j43v-wwjr-9grj: previously the store URL
78
+ match alone was enough.
79
+ """
80
+ self.manager._policies = {
81
+ NodePolicy.ALLOWED_ALGORITHM_STORES: ["https://store.example"]
82
+ }
83
+ self.manager.client.algorithm_store.get.return_value = {
84
+ "url": "https://store.example"
85
+ }
86
+ self.manager.client.algorithm_store.get_algorithm.return_value = None
87
+
88
+ self.assertFalse(
89
+ self.manager.is_docker_image_allowed(
90
+ "some/image:tag", self._task_info(store_id=1)
91
+ )
92
+ )
93
+
94
+ def test_allowed_algorithm_stores_url_not_whitelisted(self):
95
+ """
96
+ The store's URL isn't on the whitelist at all - the image is denied without
97
+ even asking the store to verify the image (no wasted network call).
98
+ """
99
+ self.manager._policies = {
100
+ NodePolicy.ALLOWED_ALGORITHM_STORES: ["https://trusted.example"]
101
+ }
102
+ self.manager.client.algorithm_store.get.return_value = {
103
+ "url": "https://untrusted.example"
104
+ }
105
+
106
+ self.assertFalse(
107
+ self.manager.is_docker_image_allowed(
108
+ "some/image:tag", self._task_info(store_id=1)
109
+ )
110
+ )
111
+ self.manager.client.algorithm_store.get_algorithm.assert_not_called()
112
+
113
+ def test_allow_either_whitelist_or_store(self):
114
+ """
115
+ Baseline regression check: with allow_either_whitelist_or_store, an image
116
+ that fails the allowed_algorithms whitelist but is confirmed by the
117
+ (whitelisted) store is still allowed.
118
+ """
119
+ self.manager._policies = {
120
+ NodePolicy.ALLOWED_ALGORITHMS: ["some/other-image:tag"],
121
+ NodePolicy.ALLOWED_ALGORITHM_STORES: ["https://store.example"],
122
+ "allow_either_whitelist_or_store": True,
123
+ }
124
+ self.manager.client.algorithm_store.get.return_value = {
125
+ "url": "https://store.example"
126
+ }
127
+ self.manager.client.algorithm_store.get_algorithm.return_value = {
128
+ "image": "some/image:tag",
129
+ "digest": "abc123",
130
+ }
131
+
132
+ self.assertTrue(
133
+ self.manager.is_docker_image_allowed(
134
+ "some/image:tag", self._task_info(store_id=1)
135
+ )
136
+ )
137
+
138
+
139
+ if __name__ == "__main__":
140
+ unittest.main()
@@ -7,7 +7,7 @@ with open(os.path.join(here, "__build__")) as fp:
7
7
  __build__ = json.load(fp)
8
8
 
9
9
  # Module version
10
- version_info = (4, 15, 1, "final", __build__, 0)
10
+ version_info = (4, 15, 2, "final", __build__, 0)
11
11
 
12
12
  # Module version stage suffix map
13
13
  _specifier_ = {"alpha": "a", "beta": "b", "candidate": "rc", "final": ""}
@@ -7,25 +7,27 @@ for creating docker networks, docker volumes, start containers and retrieve
7
7
  results from finished containers.
8
8
  """
9
9
 
10
- import os
11
- from socket import SocketIO
12
- import time
13
10
  import logging
14
- import docker
11
+ import os
15
12
  import re
16
13
  import shutil
17
-
18
- from typing import NamedTuple
14
+ import time
19
15
  from pathlib import Path
16
+ from socket import SocketIO
17
+ from typing import NamedTuple
20
18
 
21
- from vantage6.common import logger_name
22
- from vantage6.common import get_database_config
19
+ import docker
20
+ from vantage6.algorithm.tools.wrappers import get_column_names
21
+ from vantage6.cli.context.node import NodeContext
22
+ from vantage6.common import get_database_config, logger_name
23
+ from vantage6.common.client.node_client import NodeClient
23
24
  from vantage6.common.docker.addons import (
24
25
  get_container,
25
26
  get_digest,
26
27
  get_image_name_wo_tag,
27
28
  running_in_docker,
28
29
  )
30
+ from vantage6.common.docker.network_manager import NetworkManager
29
31
  from vantage6.common.globals import (
30
32
  APPNAME,
31
33
  BASIC_PROCESSING_IMAGE,
@@ -33,22 +35,18 @@ from vantage6.common.globals import (
33
35
  NodePolicy,
34
36
  )
35
37
  from vantage6.common.task_status import TaskStatus, has_task_failed
36
- from vantage6.common.docker.network_manager import NetworkManager
37
- from vantage6.algorithm.tools.wrappers import get_column_names
38
- from vantage6.cli.context.node import NodeContext
39
38
  from vantage6.node.context import DockerNodeContext
40
39
  from vantage6.node.docker.docker_base import DockerBaseManager
41
- from vantage6.node.docker.vpn_manager import VPNManager
42
- from vantage6.node.docker.task_manager import DockerTaskManager
43
- from vantage6.node.docker.squid import Squid
44
- from vantage6.common.client.node_client import NodeClient
45
40
  from vantage6.node.docker.exceptions import (
46
- UnknownAlgorithmStartFail,
47
- PermanentAlgorithmStartFail,
48
41
  AlgorithmContainerNotFound,
42
+ PermanentAlgorithmStartFail,
43
+ UnknownAlgorithmStartFail,
49
44
  )
50
- from vantage6.node.globals import DEFAULT_REQUIRE_ALGO_IMAGE_PULL
45
+ from vantage6.node.docker.squid import Squid
46
+ from vantage6.node.docker.task_manager import DockerTaskManager
51
47
  from vantage6.node.docker.utils import login_to_registries
48
+ from vantage6.node.docker.vpn_manager import VPNManager
49
+ from vantage6.node.globals import DEFAULT_REQUIRE_ALGO_IMAGE_PULL
52
50
 
53
51
  log = logging.getLogger(logger_name(__name__))
54
52
 
@@ -414,7 +412,7 @@ class DockerManager(DockerBaseManager):
414
412
  )
415
413
  if evaluated_img.startswith(BASIC_PROCESSING_IMAGE):
416
414
  if not allow_basics:
417
- self.log.warn(
415
+ self.log.warning(
418
416
  "A task was sent with a basics algorithm that "
419
417
  "this node does not allow to run."
420
418
  )
@@ -515,20 +513,47 @@ class DockerManager(DockerBaseManager):
515
513
  except Exception:
516
514
  store_id = None
517
515
  if store_id:
518
- store = self.client.algorithm_store.get(store_id)
519
- store_from_task = store["url"]
516
+ store_info = self.client.algorithm_store.get(store_id)
517
+ store_from_task = store_info["url"]
520
518
  # check if the store matches any of the regex cases
521
519
  if isinstance(allowed_stores, str):
522
520
  allowed_stores = [allowed_stores]
523
- for store in allowed_stores:
524
- if not self._is_regex_pattern(store):
521
+ store_url_whitelisted = False
522
+ for allowed_store in allowed_stores:
523
+ if not self._is_regex_pattern(allowed_store):
525
524
  # check if string matches exactly
526
- if store == store_from_task:
527
- store_whitelisted = True
525
+ if allowed_store == store_from_task:
526
+ store_url_whitelisted = True
528
527
  else:
529
- expr_ = re.compile(store)
528
+ expr_ = re.compile(allowed_store)
530
529
  if expr_.match(store_from_task):
531
- store_whitelisted = True
530
+ store_url_whitelisted = True
531
+
532
+ if store_url_whitelisted:
533
+ # The store's URL is whitelisted, but that alone does not prove
534
+ # that the evaluated image is actually a registered, approved
535
+ # algorithm in that store - the server is the one asserting that
536
+ # association, and the server is not fully trusted here.
537
+ # Re-verify directly with the store itself instead of trusting
538
+ # the server's claim.
539
+ # `store_info['url']` is the full base URL the store was
540
+ # registered with, including its API path (default `/api`, but
541
+ # configurable per store) - nothing needs to be appended here.
542
+ self.client.algorithm_store.url = store_info["url"]
543
+ self.client.algorithm_store.store_id = store_id
544
+ algorithm = self.client.algorithm_store.get_algorithm(evaluated_img)
545
+ if algorithm:
546
+ store_whitelisted = True
547
+ else:
548
+ self.log.warning(
549
+ "Algorithm store %s did not confirm that image %s is a "
550
+ "registered, approved algorithm there. Denying the "
551
+ "allowed_algorithm_stores policy for this image. This "
552
+ "can also happen if the algorithm store does not yet "
553
+ "support node verification requests.",
554
+ store_from_task,
555
+ evaluated_img,
556
+ )
532
557
 
533
558
  allowed_from_whitelist = not allowed_algorithms or algorithm_whitelisted
534
559
  allowed_from_store = not allowed_stores or store_whitelisted
@@ -587,11 +612,11 @@ class DockerManager(DockerBaseManager):
587
612
  "}",
588
613
  "|",
589
614
  "+",
590
- "\.",
615
+ r"\.",
591
616
  ]
592
617
  # Use common characters used in regular expressions as a proxy
593
618
  # for if this string is in fact a regex.
594
- return any((c in pattern for c in common_regex_chars))
619
+ return any(c in pattern for c in common_regex_chars)
595
620
 
596
621
  def is_running(self, run_id: int) -> bool:
597
622
  """
@@ -910,7 +935,7 @@ class DockerManager(DockerBaseManager):
910
935
  )
911
936
  )
912
937
  else:
913
- self.log.warn(
938
+ self.log.warning(
914
939
  "Received instruction to kill run_id="
915
940
  f"{container_to_kill['run_id']}, but it was not "
916
941
  "found running on this node."
@@ -940,18 +965,18 @@ class DockerManager(DockerBaseManager):
940
965
  killed_runs = self.kill_selected_tasks(org_id=org_id, kill_list=kill_list)
941
966
  else:
942
967
  # received instruction to kill all tasks on this node
943
- self.log.warn(
968
+ self.log.warning(
944
969
  "Received instruction from server to kill all algorithms "
945
970
  "running on this node. Executing that now..."
946
971
  )
947
972
  killed_runs = self.cleanup_tasks()
948
973
  if len(killed_runs):
949
- self.log.warn(
974
+ self.log.warning(
950
975
  "Killed the following run ids as instructed via socket:"
951
976
  f" {', '.join([str(r.run_id) for r in killed_runs])}"
952
977
  )
953
978
  else:
954
- self.log.warn("Instructed to kill tasks but none were running")
979
+ self.log.warning("Instructed to kill tasks but none were running")
955
980
  return killed_runs
956
981
 
957
982
  def get_column_names(self, label: str, type_: str) -> list[str]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vantage6_node
3
- Version: 4.15.1
3
+ Version: 4.15.2
4
4
  Summary: vantage6 node
5
5
  Home-page: https://github.com/vantage6/vantage6
6
6
  Requires-Python: >=3.10
@@ -13,9 +13,9 @@ Requires-Dist: python-socketio==5.15.0
13
13
  Requires-Dist: requests==2.33.1
14
14
  Requires-Dist: psutil==5.9.0
15
15
  Requires-Dist: nvidia-ml-py==12.535.133
16
- Requires-Dist: vantage6==4.15.1
17
- Requires-Dist: vantage6-client==4.15.1
18
- Requires-Dist: vantage6-algorithm-tools==4.15.1
16
+ Requires-Dist: vantage6==4.15.2
17
+ Requires-Dist: vantage6-client==4.15.2
18
+ Requires-Dist: vantage6-algorithm-tools==4.15.2
19
19
  Provides-Extra: dev
20
20
  Requires-Dist: coverage==6.4.4; extra == "dev"
21
21
  Requires-Dist: python-coveralls==2.9.3; extra == "dev"
@@ -2,6 +2,7 @@ setup.py
2
2
  tests/__init__.py
3
3
  tests/test_ssh_tunnel.py
4
4
  tests/docker/__init__.py
5
+ tests/docker/test_docker_manager.py
5
6
  vantage6/node/__build__
6
7
  vantage6/node/__init__.py
7
8
  vantage6/node/_version.py
@@ -6,9 +6,9 @@ python-socketio==5.15.0
6
6
  requests==2.33.1
7
7
  psutil==5.9.0
8
8
  nvidia-ml-py==12.535.133
9
- vantage6==4.15.1
10
- vantage6-client==4.15.1
11
- vantage6-algorithm-tools==4.15.1
9
+ vantage6==4.15.2
10
+ vantage6-client==4.15.2
11
+ vantage6-algorithm-tools==4.15.2
12
12
 
13
13
  [dev]
14
14
  coverage==6.4.4
File without changes
File without changes