Skip to content

Commit 2178dc6

Browse files
authored
Merge pull request #160 from tmikuska/fix-group-permissions
[deprecation] Convert permissions to the new format
2 parents e33d6f5 + c74fbb4 commit 2178dc6

7 files changed

Lines changed: 340 additions & 24 deletions

File tree

tests/v2/groups.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ class TestCMLGroups(BaseCMLTest):
4747
"name": "group",
4848
"description": "",
4949
"members": ["00000000-0000-4000-a000-000000000000"],
50-
"labs": [
51-
{"id": "88119b68-9d08-40c4-90f5-6dc533fd0254", "permission": "read_write"},
50+
"associations": [
51+
{
52+
"id": "88119b68-9d08-40c4-90f5-6dc533fd0254",
53+
"permissions": ["lab_view", "lab_edit", "lab_exec", "lab_admin"],
54+
},
5255
],
5356
}
5457

@@ -62,8 +65,16 @@ class TestCMLGroups(BaseCMLTest):
6265
"name": "group",
6366
"description": "",
6467
"members": ["00000000-0000-4000-a000-000000000000"],
65-
"labs": [
66-
{"id": "88119b68-9d08-40c4-90f5-6dc533fd0254", "permission": "read_write"},
68+
"associations": [
69+
{
70+
"id": "88119b68-9d08-40c4-90f5-6dc533fd0254",
71+
"permissions": [
72+
"lab_view",
73+
"lab_edit",
74+
"lab_exec",
75+
"lab_admin",
76+
],
77+
},
6778
],
6879
},
6980
]

tests/v2/test_helpers.py

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
import os
2+
import types
3+
import importlib
4+
import errno
5+
6+
import pytest
7+
from requests.exceptions import HTTPError
8+
9+
import virl.helpers as helpers
10+
11+
12+
class _FakeNode:
13+
def __init__(self, label="n1", booted=True, exc=None):
14+
self.label = label
15+
self._booted = booted
16+
self._exc = exc
17+
self.extracted = False
18+
19+
def is_booted(self):
20+
return self._booted
21+
22+
def extract_configuration(self):
23+
if self._exc:
24+
raise self._exc
25+
self.extracted = True
26+
27+
28+
class _FakeLab:
29+
def __init__(self, nodes):
30+
self._nodes = nodes
31+
32+
def nodes(self):
33+
return self._nodes
34+
35+
36+
class _FakeInterface:
37+
def __init__(self, ipv4=None, ipv6=None):
38+
self.discovered_ipv4 = ipv4 or []
39+
self.discovered_ipv6 = ipv6 or []
40+
41+
42+
def test_find_virl_or_else_and_cache_paths(monkeypatch):
43+
monkeypatch.setattr(helpers, "find_virl", lambda: None)
44+
assert helpers.find_virl_or_else() == "."
45+
assert helpers.get_cache_root().endswith("/.virl/cached_cml_labs")
46+
assert helpers.get_current_lab_link().endswith("/.virl/current_cml_lab")
47+
assert helpers.get_default_plugin_dir().endswith("/.virl/plugins")
48+
49+
50+
def test_safe_join_existing_lab_variants():
51+
client = types.SimpleNamespace(
52+
get_lab_list=lambda: ["lab-1"],
53+
join_existing_lab=lambda lab_id: {"id": lab_id},
54+
find_labs_by_title=lambda title: ["lab-1"] if title == "one" else ["a", "b"],
55+
)
56+
assert helpers.safe_join_existing_lab("lab-1", client) == {"id": "lab-1"}
57+
assert helpers.safe_join_existing_lab("missing", client) is None
58+
assert helpers.safe_join_existing_lab_by_title("one", client) == "lab-1"
59+
assert helpers.safe_join_existing_lab_by_title("dup", client) is None
60+
61+
62+
def test_cache_lab_data_and_current_lab_link(monkeypatch, tmp_path):
63+
cache_root = tmp_path / ".virl" / "cached_cml_labs"
64+
current = tmp_path / ".virl" / "current_cml_lab"
65+
monkeypatch.setattr(helpers, "get_cache_root", lambda: str(cache_root))
66+
monkeypatch.setattr(helpers, "get_current_lab_link", lambda: str(current))
67+
68+
helpers.cache_lab_data("lab-1", "topology-data")
69+
assert (cache_root / "lab-1").read_text() == "topology-data"
70+
71+
helpers.set_current_lab("lab-1")
72+
assert helpers.get_current_lab() == "lab-1"
73+
74+
helpers.clear_current_lab("other")
75+
assert os.path.exists(current)
76+
helpers.clear_current_lab("lab-1")
77+
assert not os.path.exists(current)
78+
79+
80+
def test_check_lab_cache_handles_errors(monkeypatch, tmp_path):
81+
monkeypatch.setattr(helpers, "get_cache_root", lambda: str(tmp_path))
82+
cached = tmp_path / "lab-1"
83+
cached.write_text("x")
84+
assert helpers.check_lab_cache("lab-1").endswith("lab-1")
85+
86+
monkeypatch.setattr(helpers, "get_cache_root", lambda: (_ for _ in ()).throw(RuntimeError("boom")))
87+
assert helpers.check_lab_cache("lab-1") is None
88+
89+
90+
def test_extract_configurations_handles_http_and_generic_errors(monkeypatch):
91+
good = _FakeNode(label="good")
92+
bad_http = _FakeNode(label="bad-http")
93+
bad_generic = _FakeNode(label="bad-generic")
94+
95+
http_error = HTTPError("bad")
96+
http_error.response = types.SimpleNamespace(status_code=500)
97+
bad_http._exc = http_error
98+
bad_generic._exc = RuntimeError("oops")
99+
100+
warnings = []
101+
monkeypatch.setattr(helpers.click, "secho", lambda msg, fg="yellow": warnings.append((msg, fg)))
102+
103+
helpers.extract_configurations(_FakeLab([good, bad_http, bad_generic]))
104+
105+
assert good.extracted is True
106+
assert any("bad-http" in msg for msg, _ in warnings)
107+
assert any("bad-generic" in msg for msg, _ in warnings)
108+
109+
110+
def test_extract_configurations_ignores_http_400(monkeypatch):
111+
bad_http_400 = _FakeNode(label="bad-http-400")
112+
err = HTTPError("bad-400")
113+
err.response = types.SimpleNamespace(status_code=400)
114+
bad_http_400._exc = err
115+
116+
warnings = []
117+
monkeypatch.setattr(helpers.click, "secho", lambda msg, fg="yellow": warnings.append((msg, fg)))
118+
helpers.extract_configurations(_FakeLab([bad_http_400]))
119+
assert warnings == []
120+
121+
122+
def test_get_node_mgmt_ip_prefers_ipv4_then_non_link_local_ipv6():
123+
node_v4 = types.SimpleNamespace(interfaces=lambda: [_FakeInterface(ipv4=["10.10.10.10"])])
124+
assert helpers.get_node_mgmt_ip(node_v4) == "10.10.10.10"
125+
126+
node_v6 = types.SimpleNamespace(interfaces=lambda: [_FakeInterface(ipv6=["fe80::1", "2001:db8::1"])])
127+
assert helpers.get_node_mgmt_ip(node_v6) == "2001:db8::1"
128+
129+
130+
def test_get_node_mgmt_ip_skips_link_local_only_ipv6():
131+
node_v6 = types.SimpleNamespace(interfaces=lambda: [_FakeInterface(ipv6=["fe80::1"])])
132+
assert helpers.get_node_mgmt_ip(node_v6) is None
133+
134+
135+
def test_get_cml_client_uses_verify_flag_and_clears_env(monkeypatch):
136+
captured = {}
137+
138+
def fake_client(host, user, password, raise_for_auth_failure, ssl_verify):
139+
captured.update(
140+
{
141+
"host": host,
142+
"user": user,
143+
"password": password,
144+
"raise_for_auth_failure": raise_for_auth_failure,
145+
"ssl_verify": ssl_verify,
146+
}
147+
)
148+
return "client"
149+
150+
monkeypatch.setattr(helpers, "ClientLibrary", fake_client)
151+
os.environ["VIRL2_USER"] = "u"
152+
os.environ["VIRL2_PASS"] = "p"
153+
os.environ["VIRL2_URL"] = "url"
154+
155+
server = types.SimpleNamespace(host="h", user="u", passwd="p", config={"CML_VERIFY_CERT": "false"})
156+
assert helpers.get_cml_client(server) == "client"
157+
assert captured["ssl_verify"] is False
158+
assert "VIRL2_USER" not in os.environ
159+
160+
server.config["CML_VERIFY_CERT"] = "/tmp/cert.pem"
161+
helpers.get_cml_client(server)
162+
assert captured["ssl_verify"] == "/tmp/cert.pem"
163+
helpers.get_cml_client(server, ignore=True)
164+
assert captured["ssl_verify"] is False
165+
166+
167+
def test_group_permission_helpers_and_command_detection(monkeypatch):
168+
assert helpers.convert_permissions("read_write") == ["lab_view", "lab_edit", "lab_exec", "lab_admin"]
169+
assert helpers.convert_permissions("read_only") == ["lab_view"]
170+
171+
users = [{"id": "1", "username": "a"}, {"id": "2", "username": "b"}]
172+
assert helpers.get_group_member_ids(users, ["b"], False) == ["2"]
173+
assert helpers.get_group_member_ids(users, ["b"], True) == ["1", "2"]
174+
175+
client = types.SimpleNamespace(get_lab_list=lambda: ["lab-1", "lab-2"])
176+
assert helpers.get_group_associations(client, None, "read_only") == [
177+
{"id": "lab-1", "permissions": ["lab_view"]},
178+
{"id": "lab-2", "permissions": ["lab_view"]},
179+
]
180+
assert helpers.get_group_associations(client, [("lab-9", "read_write")], None) == [
181+
{"id": "lab-9", "permissions": ["lab_view", "lab_edit", "lab_exec", "lab_admin"]}
182+
]
183+
assert helpers.get_group_associations(client, None, None) == []
184+
185+
monkeypatch.setattr(helpers.sys, "argv", ["cml"])
186+
assert helpers.get_command() == "cml"
187+
monkeypatch.setattr(helpers.sys, "argv", ["virl"])
188+
assert helpers.get_command() == "virl"
189+
190+
191+
def test_set_current_lab_raises_when_cache_missing(monkeypatch, tmp_path):
192+
monkeypatch.setattr(helpers, "get_cache_root", lambda: str(tmp_path / "cache"))
193+
monkeypatch.setattr(helpers, "get_current_lab_link", lambda: str(tmp_path / "link"))
194+
with pytest.raises(FileNotFoundError):
195+
helpers.set_current_lab("missing")
196+
197+
198+
def test_mkdir_p_raises_non_eexist(monkeypatch):
199+
def _boom(_path):
200+
raise OSError(errno.EPERM, "denied")
201+
202+
monkeypatch.setattr(helpers.os, "makedirs", _boom)
203+
with pytest.raises(OSError):
204+
helpers.mkdir_p("/tmp/denied")
205+
206+
207+
def test_find_virl_windows_and_edge_cases(monkeypatch):
208+
# Force while loop to exit immediately (covers while false branch).
209+
class _RootLike(str):
210+
def split(self, _sep):
211+
return "\\"
212+
213+
monkeypatch.setattr(helpers.os, "getcwd", lambda: _RootLike("\\"))
214+
monkeypatch.setattr(helpers.os.path, "abspath", lambda _p: "\\")
215+
monkeypatch.setattr(helpers.platform, "system", lambda: "Windows")
216+
assert helpers.find_virl() is None
217+
218+
# Trigger Windows path building and IndexError path in pop().
219+
monkeypatch.setattr(helpers.os, "getcwd", lambda: "")
220+
monkeypatch.setattr(helpers.os.path, "abspath", lambda _p: "\\")
221+
monkeypatch.setattr(helpers.os, "listdir", lambda _p: [])
222+
assert helpers.find_virl() is None
223+
224+
# Cover non-empty Windows lookin path branch.
225+
monkeypatch.setattr(helpers.os, "getcwd", lambda: "foo/bar")
226+
monkeypatch.setattr(helpers.os.path, "abspath", lambda _p: "\\")
227+
monkeypatch.setattr(helpers.os, "listdir", lambda _p: [".virl"])
228+
assert helpers.find_virl() == "foo\\bar"
229+
230+
231+
def test_windows_redirection_context_manager(monkeypatch):
232+
# Reload helpers as if running on Windows so class attributes are created.
233+
import platform
234+
import ctypes
235+
import virl.helpers as helpers_mod
236+
237+
monkeypatch.setattr(platform, "system", lambda: "Windows")
238+
monkeypatch.setattr(ctypes, "windll", types.SimpleNamespace(kernel32=types.SimpleNamespace()), raising=False)
239+
monkeypatch.setattr(
240+
ctypes.windll.kernel32,
241+
"Wow64DisableWow64FsRedirection",
242+
lambda *_args: 1,
243+
raising=False,
244+
)
245+
monkeypatch.setattr(
246+
ctypes.windll.kernel32,
247+
"Wow64RevertWow64FsRedirection",
248+
lambda *_args: 1,
249+
raising=False,
250+
)
251+
252+
win_helpers = importlib.reload(helpers_mod)
253+
ctx = win_helpers.disable_file_system_redirection()
254+
ctx.__enter__()
255+
ctx.__exit__(None, None, None)
256+
257+
# Cover __exit__ no-op branch when disabling redirection fails.
258+
monkeypatch.setattr(ctypes.windll.kernel32, "Wow64DisableWow64FsRedirection", lambda *_args: 0, raising=False)
259+
win_helpers = importlib.reload(helpers_mod)
260+
ctx = win_helpers.disable_file_system_redirection()
261+
ctx.__enter__()
262+
ctx.__exit__(None, None, None)
263+
264+
265+
def test_get_cml_client_without_verify_cert_key(monkeypatch):
266+
captured = {}
267+
268+
def fake_client(host, user, password, raise_for_auth_failure, ssl_verify):
269+
captured["ssl_verify"] = ssl_verify
270+
return "client"
271+
272+
monkeypatch.setattr(helpers, "ClientLibrary", fake_client)
273+
server = types.SimpleNamespace(host="h", user="u", passwd="p", config={})
274+
assert helpers.get_cml_client(server) == "client"
275+
assert captured["ssl_verify"] is True

virl/cli/groups/create/commands.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import click
44

55
from virl.api import VIRLServer
6-
from virl.helpers import get_cml_client
6+
from virl.helpers import get_cml_client, get_group_associations, get_group_member_ids
77

88

99
@click.command()
@@ -32,17 +32,14 @@ def create_groups(groupnames, member, add_all_users, lab, add_all_labs):
3232
client = get_cml_client(server)
3333

3434
all_users = client.user_management.users()
35-
all_users_ids = [u["id"] for u in all_users]
36-
members_ids = all_users_ids if add_all_users else [u["id"] for u in all_users if u["username"] in member]
37-
38-
lab_ids = [{"id": lab_id, "permission": permission} for lab_id, permission in lab]
39-
lab_ids = None if add_all_labs is None else [{"id": lid, "permission": add_all_labs} for lid in client.get_lab_list()]
35+
members_ids = get_group_member_ids(all_users, member, add_all_users)
36+
associations = get_group_associations(client, lab, add_all_labs)
4037

4138
for name in groupnames:
4239
kwargs = {
4340
"name": name,
4441
"members": members_ids,
45-
"labs": lab_ids,
42+
"associations": associations,
4643
}
4744
try:
4845
client.group_management.create_group(**kwargs)

virl/cli/groups/ls/commands.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,13 @@ def list_groups(verbose):
1818
groups = client.group_management.groups()
1919
for group in groups:
2020
group["members"] = [user_mapping[uid] for uid in group["members"]]
21-
group["labs"] = [{"title": labs_mapping[lab["id"]], "permission": lab["permission"]} for lab in group["labs"]]
21+
group["associations"] = [
22+
{
23+
"title": labs_mapping.get(assoc["id"], assoc["id"]),
24+
"permissions": assoc.get("permissions"),
25+
}
26+
for assoc in group.get("associations", [])
27+
]
2228
try:
2329
pl = ViewerPlugin(viewer="group")
2430
pl.visualize(groups=groups)

virl/cli/groups/update/commands.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import click
44

55
from virl.api import VIRLServer
6-
from virl.helpers import get_cml_client
6+
from virl.helpers import get_cml_client, get_group_associations, get_group_member_ids
77

88

99
@click.command()
@@ -32,13 +32,8 @@ def update_groups(groupnames, member, add_all_users, lab, add_all_labs):
3232
client = get_cml_client(server)
3333

3434
all_users = client.user_management.users()
35-
all_users_ids = [u["id"] for u in all_users]
36-
members_ids = all_users_ids if add_all_users else [u["id"] for u in all_users if u["username"] in member]
37-
members_ids = members_ids if members_ids else None
38-
39-
lab_ids = [{"id": lab_id, "permission": permission} for lab_id, permission in lab]
40-
lab_ids = None if add_all_labs is None else [{"id": lid, "permission": add_all_labs} for lid in client.get_lab_list()]
41-
lab_ids = lab_ids if lab_ids else None
35+
members_ids = get_group_member_ids(all_users, member, add_all_users)
36+
associations = get_group_associations(client, lab, add_all_labs)
4237

4338
groups = client.group_management.groups()
4439
group_mapping = {group["name"]: group["id"] for group in groups}
@@ -47,10 +42,8 @@ def update_groups(groupnames, member, add_all_users, lab, add_all_labs):
4742
kwargs = {
4843
"group_id": group_id,
4944
"members": members_ids,
50-
"labs": lab_ids,
45+
"associations": associations,
5146
}
52-
# only pass kwargs that are not None
53-
kwargs = {k: v for k, v in kwargs.items() if v is not None}
5447
try:
5548
client.group_management.update_group(**kwargs)
5649
click.secho(f"Group {name} successfully updated", fg="green")

0 commit comments

Comments
 (0)