diracx-cli 0.0.1a22__py3-none-any.whl → 0.0.1a24__py3-none-any.whl
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.
- diracx/cli/__init__.py +3 -134
- diracx/cli/__main__.py +2 -0
- diracx/cli/auth.py +140 -0
- diracx/cli/config.py +1 -0
- diracx/cli/internal/__init__.py +4 -188
- diracx/cli/internal/config.py +192 -0
- diracx/cli/internal/legacy.py +2 -11
- diracx/cli/jobs.py +1 -0
- {diracx_cli-0.0.1a22.dist-info → diracx_cli-0.0.1a24.dist-info}/METADATA +3 -3
- diracx_cli-0.0.1a24.dist-info/RECORD +15 -0
- {diracx_cli-0.0.1a22.dist-info → diracx_cli-0.0.1a24.dist-info}/WHEEL +1 -1
- diracx_cli-0.0.1a22.dist-info/RECORD +0 -13
- {diracx_cli-0.0.1a22.dist-info → diracx_cli-0.0.1a24.dist-info}/entry_points.txt +0 -0
- {diracx_cli-0.0.1a22.dist-info → diracx_cli-0.0.1a24.dist-info}/top_level.txt +0 -0
diracx/cli/__init__.py
CHANGED
|
@@ -1,144 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
from datetime import datetime, timedelta, timezone
|
|
5
|
-
from typing import Annotated, Optional
|
|
1
|
+
from __future__ import annotations
|
|
6
2
|
|
|
7
|
-
import typer
|
|
8
|
-
|
|
9
|
-
from diracx.client.aio import DiracClient
|
|
10
|
-
from diracx.client.models import DeviceFlowErrorResponse
|
|
11
3
|
from diracx.core.extensions import select_from_extension
|
|
12
|
-
from diracx.core.preferences import get_diracx_preferences
|
|
13
|
-
from diracx.core.utils import read_credentials, write_credentials
|
|
14
|
-
|
|
15
|
-
from .utils import AsyncTyper
|
|
16
|
-
|
|
17
|
-
app = AsyncTyper()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
async def installation_metadata():
|
|
21
|
-
async with DiracClient() as api:
|
|
22
|
-
return await api.well_known.installation_metadata()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def vo_callback(vo: str | None) -> str:
|
|
26
|
-
metadata = asyncio.run(installation_metadata())
|
|
27
|
-
vos = list(metadata.virtual_organizations)
|
|
28
|
-
if not vo:
|
|
29
|
-
raise typer.BadParameter(
|
|
30
|
-
f"VO must be specified, available options are: {' '.join(vos)}"
|
|
31
|
-
)
|
|
32
|
-
if vo not in vos:
|
|
33
|
-
raise typer.BadParameter(
|
|
34
|
-
f"Unknown VO {vo}, available options are: {' '.join(vos)}"
|
|
35
|
-
)
|
|
36
|
-
return vo
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@app.async_command()
|
|
40
|
-
async def login(
|
|
41
|
-
vo: Annotated[
|
|
42
|
-
Optional[str],
|
|
43
|
-
typer.Argument(callback=vo_callback, help="Virtual Organization name"),
|
|
44
|
-
] = None,
|
|
45
|
-
group: Optional[str] = typer.Option(
|
|
46
|
-
None,
|
|
47
|
-
help="Group name within the VO. If not provided, the default group for the VO will be used.",
|
|
48
|
-
),
|
|
49
|
-
property: Optional[list[str]] = typer.Option(
|
|
50
|
-
None,
|
|
51
|
-
help=(
|
|
52
|
-
"List of properties to add to the default properties of the group. "
|
|
53
|
-
"If not provided, default properties of the group will be used."
|
|
54
|
-
),
|
|
55
|
-
),
|
|
56
|
-
):
|
|
57
|
-
"""Login to the DIRAC system using the device flow.
|
|
58
|
-
|
|
59
|
-
- If only VO is provided: Uses the default group and its properties for the VO.
|
|
60
|
-
|
|
61
|
-
- If VO and group are provided: Uses the specified group and its properties for the VO.
|
|
62
4
|
|
|
63
|
-
|
|
64
|
-
provided properties.
|
|
5
|
+
from .auth import app
|
|
65
6
|
|
|
66
|
-
|
|
67
|
-
provided properties.
|
|
68
|
-
"""
|
|
69
|
-
scopes = [f"vo:{vo}"]
|
|
70
|
-
if group:
|
|
71
|
-
scopes.append(f"group:{group}")
|
|
72
|
-
if property:
|
|
73
|
-
scopes += [f"property:{p}" for p in property]
|
|
74
|
-
|
|
75
|
-
print(f"Logging in with scopes: {scopes}")
|
|
76
|
-
async with DiracClient() as api:
|
|
77
|
-
data = await api.auth.initiate_device_flow(
|
|
78
|
-
client_id=api.client_id,
|
|
79
|
-
scope=" ".join(scopes),
|
|
80
|
-
)
|
|
81
|
-
print("Now go to:", data.verification_uri_complete)
|
|
82
|
-
expires = datetime.now(tz=timezone.utc) + timedelta(
|
|
83
|
-
seconds=data.expires_in - 30
|
|
84
|
-
)
|
|
85
|
-
while expires > datetime.now(tz=timezone.utc):
|
|
86
|
-
print(".", end="", flush=True)
|
|
87
|
-
response = await api.auth.token(device_code=data.device_code, client_id=api.client_id) # type: ignore
|
|
88
|
-
if isinstance(response, DeviceFlowErrorResponse):
|
|
89
|
-
if response.error == "authorization_pending":
|
|
90
|
-
# TODO: Setting more than 5 seconds results in an error
|
|
91
|
-
# Related to keep-alive disconnects from uvicon (--timeout-keep-alive)
|
|
92
|
-
await asyncio.sleep(2)
|
|
93
|
-
continue
|
|
94
|
-
raise RuntimeError(f"Device flow failed with {response}")
|
|
95
|
-
break
|
|
96
|
-
else:
|
|
97
|
-
raise RuntimeError("Device authorization flow expired")
|
|
98
|
-
|
|
99
|
-
# Save credentials
|
|
100
|
-
write_credentials(response)
|
|
101
|
-
credentials_path = get_diracx_preferences().credentials_path
|
|
102
|
-
print(f"Saved credentials to {credentials_path}")
|
|
103
|
-
print("\nLogin successful!")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@app.async_command()
|
|
107
|
-
async def whoami():
|
|
108
|
-
async with DiracClient() as api:
|
|
109
|
-
user_info = await api.auth.userinfo()
|
|
110
|
-
# TODO: Add a RICH output format
|
|
111
|
-
print(json.dumps(user_info.as_dict(), indent=2))
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
@app.async_command()
|
|
115
|
-
async def logout():
|
|
116
|
-
async with DiracClient() as api:
|
|
117
|
-
credentials_path = get_diracx_preferences().credentials_path
|
|
118
|
-
if credentials_path.exists():
|
|
119
|
-
credentials = read_credentials(credentials_path)
|
|
120
|
-
|
|
121
|
-
# Revoke refresh token
|
|
122
|
-
try:
|
|
123
|
-
await api.auth.revoke_refresh_token(credentials.refresh_token)
|
|
124
|
-
except Exception as e:
|
|
125
|
-
print(f"Error revoking the refresh token {e!r}")
|
|
126
|
-
pass
|
|
127
|
-
|
|
128
|
-
# Remove credentials
|
|
129
|
-
credentials_path.unlink(missing_ok=True)
|
|
130
|
-
print(f"Removed credentials from {credentials_path}")
|
|
131
|
-
print("\nLogout successful!")
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
@app.callback()
|
|
135
|
-
def callback(output_format: Optional[str] = None):
|
|
136
|
-
if output_format is not None:
|
|
137
|
-
os.environ["DIRACX_OUTPUT_FORMAT"] = output_format
|
|
7
|
+
__all__ = ("app",)
|
|
138
8
|
|
|
139
9
|
|
|
140
10
|
# Load all the sub commands
|
|
141
|
-
|
|
142
11
|
cli_names = set(
|
|
143
12
|
[entry_point.name for entry_point in select_from_extension(group="diracx.cli")]
|
|
144
13
|
)
|
diracx/cli/__main__.py
CHANGED
diracx/cli/auth.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
__all__ = ("app",)
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import Annotated, Optional
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from diracx.client.aio import DiracClient
|
|
14
|
+
from diracx.client.models import DeviceFlowErrorResponse
|
|
15
|
+
from diracx.core.preferences import get_diracx_preferences
|
|
16
|
+
from diracx.core.utils import read_credentials, write_credentials
|
|
17
|
+
|
|
18
|
+
from .utils import AsyncTyper
|
|
19
|
+
|
|
20
|
+
app = AsyncTyper()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def installation_metadata():
|
|
24
|
+
async with DiracClient() as api:
|
|
25
|
+
return await api.well_known.installation_metadata()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def vo_callback(vo: str | None) -> str:
|
|
29
|
+
metadata = asyncio.run(installation_metadata())
|
|
30
|
+
vos = list(metadata.virtual_organizations)
|
|
31
|
+
if not vo:
|
|
32
|
+
raise typer.BadParameter(
|
|
33
|
+
f"VO must be specified, available options are: {' '.join(vos)}"
|
|
34
|
+
)
|
|
35
|
+
if vo not in vos:
|
|
36
|
+
raise typer.BadParameter(
|
|
37
|
+
f"Unknown VO {vo}, available options are: {' '.join(vos)}"
|
|
38
|
+
)
|
|
39
|
+
return vo
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.async_command()
|
|
43
|
+
async def login(
|
|
44
|
+
vo: Annotated[
|
|
45
|
+
Optional[str],
|
|
46
|
+
typer.Argument(callback=vo_callback, help="Virtual Organization name"),
|
|
47
|
+
] = None,
|
|
48
|
+
group: Optional[str] = typer.Option(
|
|
49
|
+
None,
|
|
50
|
+
help="Group name within the VO. If not provided, the default group for the VO will be used.",
|
|
51
|
+
),
|
|
52
|
+
property: Optional[list[str]] = typer.Option(
|
|
53
|
+
None,
|
|
54
|
+
help=(
|
|
55
|
+
"List of properties to add to the default properties of the group. "
|
|
56
|
+
"If not provided, default properties of the group will be used."
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
):
|
|
60
|
+
"""Login to the DIRAC system using the device flow.
|
|
61
|
+
|
|
62
|
+
- If only VO is provided: Uses the default group and its properties for the VO.
|
|
63
|
+
|
|
64
|
+
- If VO and group are provided: Uses the specified group and its properties for the VO.
|
|
65
|
+
|
|
66
|
+
- If VO and properties are provided: Uses the default group and combines its properties with the
|
|
67
|
+
provided properties.
|
|
68
|
+
|
|
69
|
+
- If VO, group, and properties are provided: Uses the specified group and combines its properties with the
|
|
70
|
+
provided properties.
|
|
71
|
+
"""
|
|
72
|
+
scopes = [f"vo:{vo}"]
|
|
73
|
+
if group:
|
|
74
|
+
scopes.append(f"group:{group}")
|
|
75
|
+
if property:
|
|
76
|
+
scopes += [f"property:{p}" for p in property]
|
|
77
|
+
|
|
78
|
+
print(f"Logging in with scopes: {scopes}")
|
|
79
|
+
async with DiracClient() as api:
|
|
80
|
+
data = await api.auth.initiate_device_flow(
|
|
81
|
+
client_id=api.client_id,
|
|
82
|
+
scope=" ".join(scopes),
|
|
83
|
+
)
|
|
84
|
+
print("Now go to:", data.verification_uri_complete)
|
|
85
|
+
expires = datetime.now(tz=timezone.utc) + timedelta(
|
|
86
|
+
seconds=data.expires_in - 30
|
|
87
|
+
)
|
|
88
|
+
while expires > datetime.now(tz=timezone.utc):
|
|
89
|
+
print(".", end="", flush=True)
|
|
90
|
+
response = await api.auth.token(device_code=data.device_code, client_id=api.client_id) # type: ignore
|
|
91
|
+
if isinstance(response, DeviceFlowErrorResponse):
|
|
92
|
+
if response.error == "authorization_pending":
|
|
93
|
+
# TODO: Setting more than 5 seconds results in an error
|
|
94
|
+
# Related to keep-alive disconnects from uvicon (--timeout-keep-alive)
|
|
95
|
+
await asyncio.sleep(2)
|
|
96
|
+
continue
|
|
97
|
+
raise RuntimeError(f"Device flow failed with {response}")
|
|
98
|
+
break
|
|
99
|
+
else:
|
|
100
|
+
raise RuntimeError("Device authorization flow expired")
|
|
101
|
+
|
|
102
|
+
# Save credentials
|
|
103
|
+
write_credentials(response)
|
|
104
|
+
credentials_path = get_diracx_preferences().credentials_path
|
|
105
|
+
print(f"Saved credentials to {credentials_path}")
|
|
106
|
+
print("\nLogin successful!")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.async_command()
|
|
110
|
+
async def whoami():
|
|
111
|
+
async with DiracClient() as api:
|
|
112
|
+
user_info = await api.auth.userinfo()
|
|
113
|
+
# TODO: Add a RICH output format
|
|
114
|
+
print(json.dumps(user_info.as_dict(), indent=2))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.async_command()
|
|
118
|
+
async def logout():
|
|
119
|
+
async with DiracClient() as api:
|
|
120
|
+
credentials_path = get_diracx_preferences().credentials_path
|
|
121
|
+
if credentials_path.exists():
|
|
122
|
+
credentials = read_credentials(credentials_path)
|
|
123
|
+
|
|
124
|
+
# Revoke refresh token
|
|
125
|
+
try:
|
|
126
|
+
await api.auth.revoke_refresh_token(credentials.refresh_token)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
print(f"Error revoking the refresh token {e!r}")
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
# Remove credentials
|
|
132
|
+
credentials_path.unlink(missing_ok=True)
|
|
133
|
+
print(f"Removed credentials from {credentials_path}")
|
|
134
|
+
print("\nLogout successful!")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.callback()
|
|
138
|
+
def callback(output_format: Optional[str] = None):
|
|
139
|
+
if output_format is not None:
|
|
140
|
+
os.environ["DIRACX_OUTPUT_FORMAT"] = output_format
|
diracx/cli/config.py
CHANGED
diracx/cli/internal/__init__.py
CHANGED
|
@@ -1,192 +1,8 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import Annotated, Optional
|
|
1
|
+
from __future__ import annotations
|
|
3
2
|
|
|
4
|
-
import git
|
|
5
|
-
import typer
|
|
6
|
-
import yaml
|
|
7
|
-
from pydantic import TypeAdapter
|
|
8
|
-
|
|
9
|
-
from diracx.core.config import ConfigSource, ConfigSourceUrl
|
|
10
|
-
from diracx.core.config.schema import (
|
|
11
|
-
Config,
|
|
12
|
-
DIRACConfig,
|
|
13
|
-
GroupConfig,
|
|
14
|
-
IdpConfig,
|
|
15
|
-
OperationsConfig,
|
|
16
|
-
RegistryConfig,
|
|
17
|
-
UserConfig,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
from ..utils import AsyncTyper
|
|
21
3
|
from . import legacy
|
|
4
|
+
from .config import app
|
|
22
5
|
|
|
23
|
-
|
|
24
|
-
app.add_typer(legacy.app, name="legacy")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@app.command()
|
|
28
|
-
def generate_cs(config_repo: str):
|
|
29
|
-
"""Generate a minimal DiracX configuration repository."""
|
|
30
|
-
# TODO: The use of TypeAdapter should be moved in to typer itself
|
|
31
|
-
config_repo = TypeAdapter(ConfigSourceUrl).validate_python(config_repo)
|
|
32
|
-
if config_repo.scheme != "git+file" or config_repo.path is None:
|
|
33
|
-
raise NotImplementedError("Only git+file:// URLs are supported")
|
|
34
|
-
repo_path = Path(config_repo.path)
|
|
35
|
-
if repo_path.exists() and list(repo_path.iterdir()):
|
|
36
|
-
typer.echo(f"ERROR: Directory {repo_path} already exists", err=True)
|
|
37
|
-
raise typer.Exit(1)
|
|
38
|
-
|
|
39
|
-
config = Config(
|
|
40
|
-
Registry={},
|
|
41
|
-
DIRAC=DIRACConfig(),
|
|
42
|
-
Operations={"Defaults": OperationsConfig()},
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
git.Repo.init(repo_path, initial_branch="master")
|
|
46
|
-
update_config_and_commit(
|
|
47
|
-
repo_path=repo_path, config=config, message="Initial commit"
|
|
48
|
-
)
|
|
49
|
-
typer.echo(f"Successfully created repo in {config_repo}", err=True)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@app.command()
|
|
53
|
-
def add_vo(
|
|
54
|
-
config_repo: str,
|
|
55
|
-
*,
|
|
56
|
-
vo: Annotated[str, typer.Option()],
|
|
57
|
-
default_group: Optional[str] = "user",
|
|
58
|
-
idp_url: Annotated[str, typer.Option()],
|
|
59
|
-
idp_client_id: Annotated[str, typer.Option()],
|
|
60
|
-
):
|
|
61
|
-
"""Add a registry entry (vo) to an existing configuration repository."""
|
|
62
|
-
# TODO: The use of TypeAdapter should be moved in to typer itself
|
|
63
|
-
config_repo = TypeAdapter(ConfigSourceUrl).validate_python(config_repo)
|
|
64
|
-
if config_repo.scheme != "git+file" or config_repo.path is None:
|
|
65
|
-
raise NotImplementedError("Only git+file:// URLs are supported")
|
|
66
|
-
repo_path = Path(config_repo.path)
|
|
67
|
-
|
|
68
|
-
# A VO should at least contain a default group
|
|
69
|
-
new_registry = RegistryConfig(
|
|
70
|
-
IdP=IdpConfig(URL=idp_url, ClientID=idp_client_id),
|
|
71
|
-
DefaultGroup=default_group,
|
|
72
|
-
Users={},
|
|
73
|
-
Groups={
|
|
74
|
-
default_group: GroupConfig(
|
|
75
|
-
Properties={"NormalUser"}, Quota=None, Users=set()
|
|
76
|
-
)
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
config = ConfigSource.create_from_url(backend_url=repo_path).read_config()
|
|
81
|
-
|
|
82
|
-
if vo in config.Registry:
|
|
83
|
-
typer.echo(f"ERROR: VO {vo} already exists", err=True)
|
|
84
|
-
raise typer.Exit(1)
|
|
85
|
-
|
|
86
|
-
config.Registry[vo] = new_registry
|
|
87
|
-
|
|
88
|
-
update_config_and_commit(
|
|
89
|
-
repo_path=repo_path,
|
|
90
|
-
config=config,
|
|
91
|
-
message=f"Added vo {vo} registry (default group {default_group} and idp {idp_url})",
|
|
92
|
-
)
|
|
93
|
-
typer.echo(f"Successfully added vo to {config_repo}", err=True)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@app.command()
|
|
97
|
-
def add_group(
|
|
98
|
-
config_repo: str,
|
|
99
|
-
*,
|
|
100
|
-
vo: Annotated[str, typer.Option()],
|
|
101
|
-
group: Annotated[str, typer.Option()],
|
|
102
|
-
properties: list[str] = ["NormalUser"],
|
|
103
|
-
):
|
|
104
|
-
"""Add a group to an existing vo in the configuration repository."""
|
|
105
|
-
# TODO: The use of TypeAdapter should be moved in to typer itself
|
|
106
|
-
config_repo = TypeAdapter(ConfigSourceUrl).validate_python(config_repo)
|
|
107
|
-
if config_repo.scheme != "git+file" or config_repo.path is None:
|
|
108
|
-
raise NotImplementedError("Only git+file:// URLs are supported")
|
|
109
|
-
repo_path = Path(config_repo.path)
|
|
110
|
-
|
|
111
|
-
new_group = GroupConfig(Properties=set(properties), Quota=None, Users=set())
|
|
112
|
-
|
|
113
|
-
config = ConfigSource.create_from_url(backend_url=repo_path).read_config()
|
|
6
|
+
__all__ = ("app",)
|
|
114
7
|
|
|
115
|
-
|
|
116
|
-
typer.echo(f"ERROR: Virtual Organization {vo} does not exist", err=True)
|
|
117
|
-
raise typer.Exit(1)
|
|
118
|
-
|
|
119
|
-
if group in config.Registry[vo].Groups.keys():
|
|
120
|
-
typer.echo(f"ERROR: Group {group} already exists in {vo}", err=True)
|
|
121
|
-
raise typer.Exit(1)
|
|
122
|
-
|
|
123
|
-
config.Registry[vo].Groups[group] = new_group
|
|
124
|
-
|
|
125
|
-
update_config_and_commit(
|
|
126
|
-
repo_path=repo_path, config=config, message=f"Added group {group} in {vo}"
|
|
127
|
-
)
|
|
128
|
-
typer.echo(f"Successfully added group to {config_repo}", err=True)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
@app.command()
|
|
132
|
-
def add_user(
|
|
133
|
-
config_repo: str,
|
|
134
|
-
*,
|
|
135
|
-
vo: Annotated[str, typer.Option()],
|
|
136
|
-
groups: Annotated[Optional[list[str]], typer.Option("--group")] = None,
|
|
137
|
-
sub: Annotated[str, typer.Option()],
|
|
138
|
-
preferred_username: Annotated[str, typer.Option()],
|
|
139
|
-
):
|
|
140
|
-
"""Add a user to an existing vo and group."""
|
|
141
|
-
# TODO: The use of TypeAdapter should be moved in to typer itself
|
|
142
|
-
config_repo = TypeAdapter(ConfigSourceUrl).validate_python(config_repo)
|
|
143
|
-
if config_repo.scheme != "git+file" or config_repo.path is None:
|
|
144
|
-
raise NotImplementedError("Only git+file:// URLs are supported")
|
|
145
|
-
|
|
146
|
-
repo_path = Path(config_repo.path)
|
|
147
|
-
|
|
148
|
-
new_user = UserConfig(PreferedUsername=preferred_username)
|
|
149
|
-
|
|
150
|
-
config = ConfigSource.create_from_url(backend_url=repo_path).read_config()
|
|
151
|
-
|
|
152
|
-
if vo not in config.Registry:
|
|
153
|
-
typer.echo(f"ERROR: Virtual Organization {vo} does not exist", err=True)
|
|
154
|
-
raise typer.Exit(1)
|
|
155
|
-
|
|
156
|
-
if sub in config.Registry[vo].Users:
|
|
157
|
-
typer.echo(f"ERROR: User {sub} already exists", err=True)
|
|
158
|
-
raise typer.Exit(1)
|
|
159
|
-
|
|
160
|
-
config.Registry[vo].Users[sub] = new_user
|
|
161
|
-
|
|
162
|
-
if not groups:
|
|
163
|
-
groups = [config.Registry[vo].DefaultGroup]
|
|
164
|
-
|
|
165
|
-
for group in set(groups):
|
|
166
|
-
if group not in config.Registry[vo].Groups:
|
|
167
|
-
typer.echo(f"ERROR: Group {group} does not exist in {vo}", err=True)
|
|
168
|
-
raise typer.Exit(1)
|
|
169
|
-
if sub in config.Registry[vo].Groups[group].Users:
|
|
170
|
-
typer.echo(f"ERROR: User {sub} already exists in group {group}", err=True)
|
|
171
|
-
raise typer.Exit(1)
|
|
172
|
-
|
|
173
|
-
config.Registry[vo].Groups[group].Users.add(sub)
|
|
174
|
-
|
|
175
|
-
update_config_and_commit(
|
|
176
|
-
repo_path=repo_path,
|
|
177
|
-
config=config,
|
|
178
|
-
message=f"Added user {sub} ({preferred_username}) to vo {vo} and groups {groups}",
|
|
179
|
-
)
|
|
180
|
-
typer.echo(f"Successfully added user to {config_repo}", err=True)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def update_config_and_commit(repo_path: Path, config: Config, message: str):
|
|
184
|
-
"""Update the yaml file in the repo and commit it."""
|
|
185
|
-
repo = git.Repo(repo_path)
|
|
186
|
-
yaml_path = repo_path / "default.yml"
|
|
187
|
-
typer.echo(f"Writing back configuration to {yaml_path}", err=True)
|
|
188
|
-
yaml_path.write_text(
|
|
189
|
-
yaml.safe_dump(config.model_dump(exclude_unset=True, mode="json"))
|
|
190
|
-
)
|
|
191
|
-
repo.index.add([yaml_path.relative_to(repo_path)])
|
|
192
|
-
repo.index.commit(message)
|
|
8
|
+
app.add_typer(legacy.app, name="legacy")
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import git
|
|
7
|
+
import typer
|
|
8
|
+
import yaml
|
|
9
|
+
from pydantic import TypeAdapter
|
|
10
|
+
|
|
11
|
+
from diracx.core.config import ConfigSource, ConfigSourceUrl
|
|
12
|
+
from diracx.core.config.schema import (
|
|
13
|
+
Config,
|
|
14
|
+
DIRACConfig,
|
|
15
|
+
GroupConfig,
|
|
16
|
+
IdpConfig,
|
|
17
|
+
OperationsConfig,
|
|
18
|
+
RegistryConfig,
|
|
19
|
+
UserConfig,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ..utils import AsyncTyper
|
|
23
|
+
|
|
24
|
+
app = AsyncTyper()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.command()
|
|
28
|
+
def generate_cs(config_repo: str):
|
|
29
|
+
"""Generate a minimal DiracX configuration repository."""
|
|
30
|
+
# TODO: The use of TypeAdapter should be moved in to typer itself
|
|
31
|
+
config_repo = TypeAdapter(ConfigSourceUrl).validate_python(config_repo)
|
|
32
|
+
if config_repo.scheme != "git+file" or config_repo.path is None:
|
|
33
|
+
raise NotImplementedError("Only git+file:// URLs are supported")
|
|
34
|
+
repo_path = Path(config_repo.path)
|
|
35
|
+
if repo_path.exists() and list(repo_path.iterdir()):
|
|
36
|
+
typer.echo(f"ERROR: Directory {repo_path} already exists", err=True)
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
|
|
39
|
+
config = Config(
|
|
40
|
+
Registry={},
|
|
41
|
+
DIRAC=DIRACConfig(),
|
|
42
|
+
Operations={"Defaults": OperationsConfig()},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
git.Repo.init(repo_path, initial_branch="master")
|
|
46
|
+
update_config_and_commit(
|
|
47
|
+
repo_path=repo_path, config=config, message="Initial commit"
|
|
48
|
+
)
|
|
49
|
+
typer.echo(f"Successfully created repo in {config_repo}", err=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def add_vo(
|
|
54
|
+
config_repo: str,
|
|
55
|
+
*,
|
|
56
|
+
vo: Annotated[str, typer.Option()],
|
|
57
|
+
default_group: Optional[str] = "user",
|
|
58
|
+
idp_url: Annotated[str, typer.Option()],
|
|
59
|
+
idp_client_id: Annotated[str, typer.Option()],
|
|
60
|
+
):
|
|
61
|
+
"""Add a registry entry (vo) to an existing configuration repository."""
|
|
62
|
+
# TODO: The use of TypeAdapter should be moved in to typer itself
|
|
63
|
+
config_repo = TypeAdapter(ConfigSourceUrl).validate_python(config_repo)
|
|
64
|
+
if config_repo.scheme != "git+file" or config_repo.path is None:
|
|
65
|
+
raise NotImplementedError("Only git+file:// URLs are supported")
|
|
66
|
+
repo_path = Path(config_repo.path)
|
|
67
|
+
|
|
68
|
+
# A VO should at least contain a default group
|
|
69
|
+
new_registry = RegistryConfig(
|
|
70
|
+
IdP=IdpConfig(URL=idp_url, ClientID=idp_client_id),
|
|
71
|
+
DefaultGroup=default_group,
|
|
72
|
+
Users={},
|
|
73
|
+
Groups={
|
|
74
|
+
default_group: GroupConfig(
|
|
75
|
+
Properties={"NormalUser"}, Quota=None, Users=set()
|
|
76
|
+
)
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
config = ConfigSource.create_from_url(backend_url=repo_path).read_config()
|
|
81
|
+
|
|
82
|
+
if vo in config.Registry:
|
|
83
|
+
typer.echo(f"ERROR: VO {vo} already exists", err=True)
|
|
84
|
+
raise typer.Exit(1)
|
|
85
|
+
|
|
86
|
+
config.Registry[vo] = new_registry
|
|
87
|
+
|
|
88
|
+
update_config_and_commit(
|
|
89
|
+
repo_path=repo_path,
|
|
90
|
+
config=config,
|
|
91
|
+
message=f"Added vo {vo} registry (default group {default_group} and idp {idp_url})",
|
|
92
|
+
)
|
|
93
|
+
typer.echo(f"Successfully added vo to {config_repo}", err=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command()
|
|
97
|
+
def add_group(
|
|
98
|
+
config_repo: str,
|
|
99
|
+
*,
|
|
100
|
+
vo: Annotated[str, typer.Option()],
|
|
101
|
+
group: Annotated[str, typer.Option()],
|
|
102
|
+
properties: list[str] = ["NormalUser"],
|
|
103
|
+
):
|
|
104
|
+
"""Add a group to an existing vo in the configuration repository."""
|
|
105
|
+
# TODO: The use of TypeAdapter should be moved in to typer itself
|
|
106
|
+
config_repo = TypeAdapter(ConfigSourceUrl).validate_python(config_repo)
|
|
107
|
+
if config_repo.scheme != "git+file" or config_repo.path is None:
|
|
108
|
+
raise NotImplementedError("Only git+file:// URLs are supported")
|
|
109
|
+
repo_path = Path(config_repo.path)
|
|
110
|
+
|
|
111
|
+
new_group = GroupConfig(Properties=set(properties), Quota=None, Users=set())
|
|
112
|
+
|
|
113
|
+
config = ConfigSource.create_from_url(backend_url=repo_path).read_config()
|
|
114
|
+
|
|
115
|
+
if vo not in config.Registry:
|
|
116
|
+
typer.echo(f"ERROR: Virtual Organization {vo} does not exist", err=True)
|
|
117
|
+
raise typer.Exit(1)
|
|
118
|
+
|
|
119
|
+
if group in config.Registry[vo].Groups.keys():
|
|
120
|
+
typer.echo(f"ERROR: Group {group} already exists in {vo}", err=True)
|
|
121
|
+
raise typer.Exit(1)
|
|
122
|
+
|
|
123
|
+
config.Registry[vo].Groups[group] = new_group
|
|
124
|
+
|
|
125
|
+
update_config_and_commit(
|
|
126
|
+
repo_path=repo_path, config=config, message=f"Added group {group} in {vo}"
|
|
127
|
+
)
|
|
128
|
+
typer.echo(f"Successfully added group to {config_repo}", err=True)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@app.command()
|
|
132
|
+
def add_user(
|
|
133
|
+
config_repo: str,
|
|
134
|
+
*,
|
|
135
|
+
vo: Annotated[str, typer.Option()],
|
|
136
|
+
groups: Annotated[Optional[list[str]], typer.Option("--group")] = None,
|
|
137
|
+
sub: Annotated[str, typer.Option()],
|
|
138
|
+
preferred_username: Annotated[str, typer.Option()],
|
|
139
|
+
):
|
|
140
|
+
"""Add a user to an existing vo and group."""
|
|
141
|
+
# TODO: The use of TypeAdapter should be moved in to typer itself
|
|
142
|
+
config_repo = TypeAdapter(ConfigSourceUrl).validate_python(config_repo)
|
|
143
|
+
if config_repo.scheme != "git+file" or config_repo.path is None:
|
|
144
|
+
raise NotImplementedError("Only git+file:// URLs are supported")
|
|
145
|
+
|
|
146
|
+
repo_path = Path(config_repo.path)
|
|
147
|
+
|
|
148
|
+
new_user = UserConfig(PreferedUsername=preferred_username)
|
|
149
|
+
|
|
150
|
+
config = ConfigSource.create_from_url(backend_url=repo_path).read_config()
|
|
151
|
+
|
|
152
|
+
if vo not in config.Registry:
|
|
153
|
+
typer.echo(f"ERROR: Virtual Organization {vo} does not exist", err=True)
|
|
154
|
+
raise typer.Exit(1)
|
|
155
|
+
|
|
156
|
+
if sub in config.Registry[vo].Users:
|
|
157
|
+
typer.echo(f"ERROR: User {sub} already exists", err=True)
|
|
158
|
+
raise typer.Exit(1)
|
|
159
|
+
|
|
160
|
+
config.Registry[vo].Users[sub] = new_user
|
|
161
|
+
|
|
162
|
+
if not groups:
|
|
163
|
+
groups = [config.Registry[vo].DefaultGroup]
|
|
164
|
+
|
|
165
|
+
for group in set(groups):
|
|
166
|
+
if group not in config.Registry[vo].Groups:
|
|
167
|
+
typer.echo(f"ERROR: Group {group} does not exist in {vo}", err=True)
|
|
168
|
+
raise typer.Exit(1)
|
|
169
|
+
if sub in config.Registry[vo].Groups[group].Users:
|
|
170
|
+
typer.echo(f"ERROR: User {sub} already exists in group {group}", err=True)
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
|
|
173
|
+
config.Registry[vo].Groups[group].Users.add(sub)
|
|
174
|
+
|
|
175
|
+
update_config_and_commit(
|
|
176
|
+
repo_path=repo_path,
|
|
177
|
+
config=config,
|
|
178
|
+
message=f"Added user {sub} ({preferred_username}) to vo {vo} and groups {groups}",
|
|
179
|
+
)
|
|
180
|
+
typer.echo(f"Successfully added user to {config_repo}", err=True)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def update_config_and_commit(repo_path: Path, config: Config, message: str):
|
|
184
|
+
"""Update the yaml file in the repo and commit it."""
|
|
185
|
+
repo = git.Repo(repo_path)
|
|
186
|
+
yaml_path = repo_path / "default.yml"
|
|
187
|
+
typer.echo(f"Writing back configuration to {yaml_path}", err=True)
|
|
188
|
+
yaml_path.write_text(
|
|
189
|
+
yaml.safe_dump(config.model_dump(exclude_unset=True, mode="json"))
|
|
190
|
+
)
|
|
191
|
+
repo.index.add([yaml_path.relative_to(repo_path)])
|
|
192
|
+
repo.index.commit(message)
|
diracx/cli/internal/legacy.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import base64
|
|
2
4
|
import hashlib
|
|
3
5
|
import json
|
|
@@ -40,17 +42,6 @@ class ConversionConfig(BaseModel):
|
|
|
40
42
|
VOs: dict[str, VOConfig]
|
|
41
43
|
|
|
42
44
|
|
|
43
|
-
# def parse_args():
|
|
44
|
-
# parser = argparse.ArgumentParser("Convert the legacy DIRAC CS to the new format")
|
|
45
|
-
# parser.add_argument("old_file", type=Path)
|
|
46
|
-
# parser.add_argument("conversion_config", type=Path)
|
|
47
|
-
# parser.add_argument("repo", type=Path)
|
|
48
|
-
# args = parser.parse_args()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# main(args.old_file, args.conversion_config, args.repo / DEFAULT_CONFIG_FILE)
|
|
52
|
-
|
|
53
|
-
|
|
54
45
|
@app.command()
|
|
55
46
|
def cs_sync(old_file: Path, new_file: Path):
|
|
56
47
|
"""Load the old CS and convert it to the new YAML format."""
|
diracx/cli/jobs.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: diracx-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.1a24
|
|
4
4
|
Summary: TODO
|
|
5
5
|
License: GPL-3.0-only
|
|
6
6
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -17,7 +17,7 @@ Requires-Dist: diracx-core
|
|
|
17
17
|
Requires-Dist: gitpython
|
|
18
18
|
Requires-Dist: pydantic>=2.10
|
|
19
19
|
Requires-Dist: rich
|
|
20
|
-
Requires-Dist: typer
|
|
20
|
+
Requires-Dist: typer>=0.12.4
|
|
21
21
|
Requires-Dist: pyyaml
|
|
22
22
|
Provides-Extra: testing
|
|
23
23
|
Requires-Dist: diracx-testing; extra == "testing"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
diracx/cli/__init__.py,sha256=HB9Umd1DXskg8lrRY0ZnSaOBVYPD5Pl4PYRWts-ZR0E,808
|
|
2
|
+
diracx/cli/__main__.py,sha256=yGjYWjRcrrp5mJ0xD0v3rc7VIA9bzDib5D7LPAdH4OI,92
|
|
3
|
+
diracx/cli/auth.py,sha256=N9rBvRgyUeznTqG0Ck2pylClvXwR3IR6OyP4mTA50BA,4733
|
|
4
|
+
diracx/cli/config.py,sha256=-QPTTuH83epgFviO_af_DJBVjRakIrc6MbxkHg1KLlI,853
|
|
5
|
+
diracx/cli/jobs.py,sha256=qxV7y_WVtYzO6h55lgbWgk_ZC9eN1i1hIFHTpwxC9mE,4760
|
|
6
|
+
diracx/cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
diracx/cli/utils.py,sha256=NwhMMHwveKOdW2aoSqpnLnfOKhPnjmPPLpX69naPAzc,855
|
|
8
|
+
diracx/cli/internal/__init__.py,sha256=KZrzVcKu3YhNev2XF2KA2nttAa9ONU3CVUgatVMonJ4,143
|
|
9
|
+
diracx/cli/internal/config.py,sha256=xPT7lnJ3QPqJgaNJuMoUpV6CIIxZY_d7HKFb4uINb_8,6552
|
|
10
|
+
diracx/cli/internal/legacy.py,sha256=AQJnLZZDNmE1N6Vd6WdjG8kZPFWufBSez4Hhi0aQZYc,10538
|
|
11
|
+
diracx_cli-0.0.1a24.dist-info/METADATA,sha256=7O65DRbTkxA7kC0K2Z2JDS1N7oHYWefW_IwAslI9eMo,803
|
|
12
|
+
diracx_cli-0.0.1a24.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
13
|
+
diracx_cli-0.0.1a24.dist-info/entry_points.txt,sha256=b1909GHVOkFUiHVglNlpwia4Ug-7Ncrg-8D5xtYVAlw,169
|
|
14
|
+
diracx_cli-0.0.1a24.dist-info/top_level.txt,sha256=vJx10tdRlBX3rF2Psgk5jlwVGZNcL3m_7iQWwgPXt-U,7
|
|
15
|
+
diracx_cli-0.0.1a24.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
diracx/cli/__init__.py,sha256=eenV2ezX845cIvDV2mTKqd23TLwXQBwD6Ho252h8WwA,5407
|
|
2
|
-
diracx/cli/__main__.py,sha256=SM9tEc-fiW7cDHTKQRwgKobe5FfijHLYiAWfWaIM_zg,56
|
|
3
|
-
diracx/cli/config.py,sha256=r5Lq_SN-1t3IzGAeS57ZzS-ukLhP6PMnmTXNld2pZXU,818
|
|
4
|
-
diracx/cli/jobs.py,sha256=wV7-YMI_jUIflj87cBM4yuI2EF9K5mu2WsCG_eakWrg,4725
|
|
5
|
-
diracx/cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
diracx/cli/utils.py,sha256=NwhMMHwveKOdW2aoSqpnLnfOKhPnjmPPLpX69naPAzc,855
|
|
7
|
-
diracx/cli/internal/__init__.py,sha256=DCKzknHUEvo7PYiatZis15-gkxhga5WU4cyVZ6LCkmA,6578
|
|
8
|
-
diracx/cli/internal/legacy.py,sha256=bhq8vfHoL0fZGhtye0EqMsucwepOYpQkj3UGe-rHNhY,10882
|
|
9
|
-
diracx_cli-0.0.1a22.dist-info/METADATA,sha256=k9WJtMesgKGxEfkv5yKXy7vCfpkPD2wREsYKgLZbk1k,795
|
|
10
|
-
diracx_cli-0.0.1a22.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
11
|
-
diracx_cli-0.0.1a22.dist-info/entry_points.txt,sha256=b1909GHVOkFUiHVglNlpwia4Ug-7Ncrg-8D5xtYVAlw,169
|
|
12
|
-
diracx_cli-0.0.1a22.dist-info/top_level.txt,sha256=vJx10tdRlBX3rF2Psgk5jlwVGZNcL3m_7iQWwgPXt-U,7
|
|
13
|
-
diracx_cli-0.0.1a22.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|