Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce user settings #1778

Merged
merged 122 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
122 commits
Select commit Hold shift + click to select a range
a9139cb
Add `--set-db --skip-init-data` (ref #1775)
tcompa Sep 18, 2024
6d5a62d
Add `UserSettings` model (ref #1774)
tcompa Sep 18, 2024
af75af5
Do not hide exceptions from `_create_first_user`
tcompa Sep 18, 2024
5b16a8a
Add FK and ORM column to `UserOAuth` (ref #1774)
tcompa Sep 18, 2024
dbde913
Add migration script
tcompa Sep 18, 2024
f0b8d93
FIXME
tcompa Sep 18, 2024
28d16cd
Add `test_unit_user_settings.py`
tcompa Sep 18, 2024
cae1d8c
Provisionally enable GHA also when targeting `1774-introduce-user-set…
tcompa Sep 18, 2024
f3475b9
Update test_unit_user_settings.py
tcompa Sep 18, 2024
ba59b55
Add empty-settings creation to `on_after_register` (ref #1776)
tcompa Sep 18, 2024
4fc4066
Add constraint name to migration script
tcompa Sep 18, 2024
0a8f7b0
Add docstring
tcompa Sep 19, 2024
954d674
Introduce preliminary `SlurmSshUserSettings`
tcompa Sep 19, 2024
6c8f94e
Add validate_user_settings aux function
tcompa Sep 19, 2024
17964df
Use validate_user_settings in API
tcompa Sep 19, 2024
151d69c
Add missing `await` [skip ci]
tcompa Sep 19, 2024
8c22706
Also create user_settings within `MockCurrentUser` fixture
tcompa Sep 19, 2024
5d2655c
Propagate user_settings through `workflow_with_non_python_task` aux f…
tcompa Sep 19, 2024
36603e8
Set user settings as part of test_full_workflow_slurm_ssh_v2.py tests
tcompa Sep 19, 2024
fb69f52
Always expunge user_settings at the end of MockCurrentUser - cc @ychi…
tcompa Sep 19, 2024
78cf6e3
Minor cleanup of `MockCurrentUser` fixture
tcompa Sep 19, 2024
8ae9c9d
Add user settings to `test_task_collection_ssh_from_pypi`
tcompa Sep 19, 2024
22f7942
Only expunge existing objects in MockCurrentUser fixture
tcompa Sep 19, 2024
a86d633
user settings api
ychiucco Sep 19, 2024
cd6c9d0
rename symbols
ychiucco Sep 19, 2024
76c14c6
Move validate_user_settings into routes/aux
tcompa Sep 19, 2024
415e56b
aux function
ychiucco Sep 19, 2024
4c49ad1
simplify aux function
ychiucco Sep 19, 2024
a12d084
raise 500
ychiucco Sep 19, 2024
a5bcf80
Add `test_validate_user_settings`
tcompa Sep 19, 2024
006a39e
without aux function
ychiucco Sep 19, 2024
5e8bc77
validators
ychiucco Sep 19, 2024
ff19c19
Merge pull request #1779 from fractal-analytics-platform/use-new-sett…
tcompa Sep 19, 2024
9c55a32
endpoints in users router
ychiucco Sep 19, 2024
ef30633
new user settings columns
ychiucco Sep 19, 2024
e454577
remove slurm_user from UserSettingsUpdateStrict
ychiucco Sep 19, 2024
6cbdc0d
update test
ychiucco Sep 19, 2024
06c26db
SUDO-SLURM and SSH-SLURM
ychiucco Sep 19, 2024
e857b99
coverage
ychiucco Sep 19, 2024
f25313a
coverage
ychiucco Sep 19, 2024
de85cea
default None
ychiucco Sep 19, 2024
4234d4f
Merge pull request #1780 from fractal-analytics-platform/user-setting…
tcompa Sep 19, 2024
ed813a2
stash
tcompa Sep 19, 2024
95cd896
Expand test_validate_user_settings
tcompa Sep 19, 2024
0341b62
Adapt sudo-slurm full-workflow tests to new required user settings
tcompa Sep 19, 2024
4322209
Update test_project_apply_missing_user_attributes
tcompa Sep 19, 2024
50a2269
Merge pull request #1785 from fractal-analytics-platform/sudo-slurm-v…
tcompa Sep 19, 2024
bc34675
Merge branch '1774-introduce-user-settings' into 1786-deprecate-use-o…
ychiucco Sep 20, 2024
a1a7651
Merge branch '1774-introduce-user-settings' into 1786-deprecate-use-o…
ychiucco Sep 20, 2024
47f0b99
data migration script 2_6_0
ychiucco Sep 20, 2024
cfa8a44
Merge branch 'main' into 1774-introduce-user-settings
ychiucco Sep 20, 2024
b387ed9
move _check_current_version
ychiucco Sep 20, 2024
f6c84ae
Merge branch '1774-introduce-user-settings' into 1786-deprecate-use-o…
ychiucco Sep 20, 2024
8128067
ssh settings in data migration script
ychiucco Sep 20, 2024
1651b2a
improve script logs
ychiucco Sep 20, 2024
7228364
Merge pull request #1788 from fractal-analytics-platform/1786-depreca…
ychiucco Sep 20, 2024
f83fc13
removed slurm_users, slurm_account and cache_dir from User schemas
ychiucco Sep 23, 2024
18686c3
fix tests (one still broken)
ychiucco Sep 23, 2024
a403370
fix last test
ychiucco Sep 23, 2024
441602d
Merge pull request #1794 from fractal-analytics-platform/1774-tmp
ychiucco Sep 23, 2024
22b4e55
Perform validation in V1 job submission endpoint
ychiucco Sep 23, 2024
2066278
Propagate the user settings to v1/v2 job-execution background task
ychiucco Sep 23, 2024
bcdf0f7
fix circular import with new logger
ychiucco Sep 23, 2024
36d5ff8
remove FIXME [skip ci]
ychiucco Sep 23, 2024
2203935
rename variable
ychiucco Sep 23, 2024
940eda4
fix tests/v1/04_api/test_project_apply_api.py
ychiucco Sep 23, 2024
c1b25a0
fix ci v2
ychiucco Sep 23, 2024
74ad84c
fix MockCurrentUser in test_full_workflow v1
ychiucco Sep 23, 2024
05b99ad
always add slurm_user
ychiucco Sep 23, 2024
cd79cec
Merge pull request #1795 from fractal-analytics-platform/1774-tmp-2
tcompa Sep 24, 2024
5c25412
Merge branch 'main' into 1774-introduce-user-settings
tcompa Sep 24, 2024
c353954
Update docs about `--skip-init-data` [skip ci]
tcompa Sep 24, 2024
3398b87
Use new `user_settings` based attributes for SSH background jobs
tcompa Sep 24, 2024
f1ca040
Remove validation of obsolete `FRACTAL_SLURM_SSH_*` configuration var…
tcompa Sep 24, 2024
cb5e264
Adapt tests so that they use user-settings for SSH
tcompa Sep 24, 2024
9c3b95e
Fix unit tests in `tests/v2/04_runner/`
tcompa Sep 24, 2024
5a51d26
Merge pull request #1797 from fractal-analytics-platform/propagate-ss…
ychiucco Sep 24, 2024
4077898
validate UserSettingsUpdate.slurm_accounts
ychiucco Sep 24, 2024
07ed2c7
Improve docstring and comments [skip ci]
tcompa Sep 24, 2024
62a1de5
slurm_accounts required by SlurmSshUserSettings
ychiucco Sep 24, 2024
6df1252
validor more explicit
ychiucco Sep 24, 2024
c0a56c2
Remove comments from UserSettings schemas [skip ci]
tcompa Sep 24, 2024
f156c4b
validor more explicit
ychiucco Sep 24, 2024
8d9c190
Remove obsolete logger from `api/v2/_aux_functions.py`
tcompa Sep 24, 2024
14ed2a9
move fractal_server/user_settings.py into fractal_server/app
ychiucco Sep 24, 2024
722d7ee
Remove obsolete `alembic-upgrade` CLI command
tcompa Sep 24, 2024
12caf0d
remove slurm_user from MockCurrentUser
ychiucco Sep 24, 2024
1e812b7
coverage
ychiucco Sep 24, 2024
3341841
Merge pull request #1798 from fractal-analytics-platform/1774-api-and…
tcompa Sep 24, 2024
9ab8987
TMP stash
ychiucco Sep 24, 2024
80616af
Merge branch '1774-introduce-user-settings' into 1774-remove-leftovers
ychiucco Sep 24, 2024
b889dce
Alwasy call `verify_user_has_settings` before `db.get(UserSettings, u…
tcompa Sep 24, 2024
136b877
fix tests/v1/04_api/test_task_api.py
ychiucco Sep 24, 2024
c6713dc
Improve docstring [skip ci]
tcompa Sep 24, 2024
6abad91
Improve docstrings [skip ci]
tcompa Sep 24, 2024
5deec35
Remove FIXME [skip ci]
tcompa Sep 24, 2024
bd4e69c
Merge pull request #1802 from fractal-analytics-platform/protect-db-g…
tcompa Sep 24, 2024
17809a2
Remove FIXME comment [skip ci]
tcompa Sep 24, 2024
adb6586
fix tests/v1/04_api/test_workflow_api.py
ychiucco Sep 24, 2024
4fdbb60
Revert "fix tests/v1/04_api/test_workflow_api.py"
ychiucco Sep 24, 2024
0de280b
Revert "fix tests/v1/04_api/test_task_api.py"
ychiucco Sep 24, 2024
016859f
remove debug [skip ci]
ychiucco Sep 24, 2024
0b18a8a
remove user_settings_dict
ychiucco Sep 24, 2024
2cc2928
remove debug [skip ci]
ychiucco Sep 24, 2024
9a5ae4d
Combine two migration scripts into one
tcompa Sep 24, 2024
d4c5905
add slurm_user to MockCurrentUser default user
ychiucco Sep 24, 2024
a5f6887
Update CHANGELOG [skip ci]
tcompa Sep 24, 2024
96d6042
fix ci v1
ychiucco Sep 24, 2024
c0b597d
Forbid extras in `UserSettingsUpdate*` models
tcompa Sep 24, 2024
9e023e0
Add unit tests for UserSettings API schemas
tcompa Sep 24, 2024
353f7eb
fix tests/v2/03_api/test_api_task.py
ychiucco Sep 24, 2024
d703f04
fix typo
ychiucco Sep 24, 2024
dd88321
Merge branch '1774-introduce-user-settings' into 1774-remove-leftovers
ychiucco Sep 24, 2024
77e45e5
verify_user_has_settings
ychiucco Sep 24, 2024
43ca6aa
improve check
ychiucco Sep 24, 2024
a6f9bd6
if-else structure
ychiucco Sep 24, 2024
ad48151
coverage
ychiucco Sep 24, 2024
60b9351
Merge branch 'main' into 1774-introduce-user-settings
tcompa Sep 24, 2024
b54b821
Merge pull request #1799 from fractal-analytics-platform/1774-remove-…
ychiucco Sep 24, 2024
6008439
Expand test_user_settings_update
tcompa Sep 24, 2024
b15d004
Merge branch '1774-introduce-user-settings' of github.com:fractal-ana…
tcompa Sep 24, 2024
7caa69d
Update CHANGELOG [skip ci]
tcompa Sep 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: ci

on:
push:
branches: ["main"]
branches: ["main", "1774-introduce-user-settings"]
pull_request:
branches: ["main"]
branches: ["main", "1774-introduce-user-settings"]

jobs:

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ name: migrations

on:
push:
branches: ["main"]
branches: ["main", "1774-introduce-user-settings"]
pull_request:
branches: ["main"]
branches: ["main", "1774-introduce-user-settings"]


env:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/precommit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: precommit

on:
push:
branches: ["main"]
branches: ["main", "1774-introduce-user-settings"]
pull_request:
branches: ["main"]
branches: ["main", "1774-introduce-user-settings"]

jobs:
precommit:
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
**Note**: Numbers like (\#1234) point to closed Pull Requests on the fractal-server repository.

# 2.6.0 (unreleased)

> WARNING: This release requires running `fractalctl update-db-data` (after `fractalctl set-db`).

* API:
* Introduce user-settings API, in `/auth/users/{user_id}/settings/` and `/auth/current-user/settings/` (\#1778).
* Add the creation of empty settings to `UserManager.on_after_register` hook (\#1778).
* Remove deprecated user's attributes (`slurm_user`, `cache_dir`, `slurm_accounts`) from API, in favor of new `UserSetting` ones (\#1778).
* Validate user settings in endpoints that rely on them (\#1778).
* Propagate user settings to background tasks when needed (\#1778).
* Database:
* Introduce new `user_settings` table, and link it to `user_oauth` (\#1778).


# 2.5.2

* App:
Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ an incremental migration script, as in
```
$ export SQLITE_PATH=some-test.db
$ rm some-test.db
$ poetry run fractalctl set-db
$ poetry run fractalctl set-db --skip-init-data
$ poetry run alembic revision --autogenerate -m "Some migration message"

# UserWarning: SQLite is partially supported but discouraged in production environment.SQLite offers partial support for ForeignKey constraints. As such, consistency of the database cannot be guaranteed.
Expand Down
33 changes: 24 additions & 9 deletions fractal_server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,17 @@
)

# fractalctl set-db
subparsers.add_parser(
set_db_parser = subparsers.add_parser(
"set-db",
description="Initialise the database and apply schema migrations",
description=(
"Initialise/upgrade database schemas and create first group&user."
),
)
set_db_parser.add_argument(
"--skip-init-data",
action="store_true",
help="If set, do not try creating first group and user.",
default=False,
)

# fractalctl update-db-data
Expand All @@ -66,27 +74,34 @@ def save_openapi(dest="openapi.json"):
json.dump(openapi_schema, f)


def set_db():
def set_db(skip_init_data: bool = False):
"""
Set-up / Upgrade database schema
Upgrade database schema *and* create first group/user

Call alembic to upgrade to the latest migration.

Ref: https://stackoverflow.com/a/56683030/283972

Arguments:
skip_init_data: If `True`, skip creation of first group and user.
"""
import alembic.config
from pathlib import Path
import fractal_server
from fractal_server.app.security import _create_first_user
from fractal_server.app.security import _create_first_group
from fractal_server.syringe import Inject
from fractal_server.config import get_settings

import alembic.config
from pathlib import Path
import fractal_server

alembic_ini = Path(fractal_server.__file__).parent / "alembic.ini"
alembic_args = ["-c", alembic_ini.as_posix(), "upgrade", "head"]
print(f"START: Run alembic.config, with argv={alembic_args}")
alembic.config.main(argv=alembic_args)
print("END: alembic.config")

if skip_init_data:
return

# Insert default group
print()
_create_first_group()
Expand Down Expand Up @@ -179,7 +194,7 @@ def run():
if args.cmd == "openapi":
save_openapi(dest=args.openapi_file)
elif args.cmd == "set-db":
set_db()
set_db(skip_init_data=args.skip_init_data)
elif args.cmd == "update-db-data":
update_db_data()
elif args.cmd == "start":
Expand Down
1 change: 1 addition & 0 deletions fractal_server/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
from .linkuserproject import LinkUserProject # noqa: F401
from .linkuserproject import LinkUserProjectV2 # noqa: F401
from .security import * # noqa
from .user_settings import UserSettings # noqa
from .v1 import * # noqa
from .v2 import * # noqa
8 changes: 8 additions & 0 deletions fractal_server/app/models/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sqlmodel import Relationship
from sqlmodel import SQLModel

from .user_settings import UserSettings
from fractal_server.utils import get_timestamp


Expand Down Expand Up @@ -104,6 +105,13 @@ class UserOAuth(SQLModel, table=True):
sa_relationship_kwargs={"lazy": "joined", "cascade": "all, delete"},
)

user_settings_id: Optional[int] = Field(
foreign_key="user_settings.id", default=None
)
settings: Optional[UserSettings] = Relationship(
tcompa marked this conversation as resolved.
Show resolved Hide resolved
sa_relationship_kwargs=dict(lazy="selectin", cascade="all, delete")
)

class Config:
orm_mode = True

Expand Down
38 changes: 38 additions & 0 deletions fractal_server/app/models/user_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Optional

from sqlalchemy import Column
from sqlalchemy.types import JSON
from sqlmodel import Field
from sqlmodel import SQLModel


class UserSettings(SQLModel, table=True):
"""
Comprehensive list of user settings.

Attributes:
id: ID of database object
slurm_accounts:
List of SLURM accounts, to be used upon Fractal job submission.
ssh_host: SSH-reachable host where a SLURM client is available.
tcompa marked this conversation as resolved.
Show resolved Hide resolved
ssh_username: User on `ssh_host`.
ssh_private_key_path: Path of private SSH key for `ssh_username`.
ssh_tasks_dir: Task-venvs base folder on `ssh_host`.
ssh_jobs_dir: Jobs base folder on `ssh_host`.
slurm_user: Local user, to be impersonated via `sudo -u`
cache_dir: Folder where `slurm_user` can write.
"""

__tablename__ = "user_settings"

id: Optional[int] = Field(default=None, primary_key=True)
slurm_accounts: list[str] = Field(
sa_column=Column(JSON, server_default="[]", nullable=False)
)
ssh_host: Optional[str] = None
ssh_username: Optional[str] = None
ssh_private_key_path: Optional[str] = None
ssh_tasks_dir: Optional[str] = None
ssh_jobs_dir: Optional[str] = None
slurm_user: Optional[str] = None
cache_dir: Optional[str] = None
7 changes: 6 additions & 1 deletion fractal_server/app/routes/api/v1/_aux_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ....models.v1 import Workflow
from ....models.v1 import WorkflowTask
from ....schemas.v1 import JobStatusTypeV1
from ...aux.validate_user_settings import verify_user_has_settings
from fractal_server.app.models import UserOAuth


Expand Down Expand Up @@ -367,7 +368,11 @@ async def _get_task_check_owner(
),
)
else:
owner = user.username or user.slurm_user
if user.username:
owner = user.username
else:
verify_user_has_settings(user)
owner = user.settings.slurm_user
if owner != task.owner:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
Expand Down
35 changes: 11 additions & 24 deletions fractal_server/app/routes/api/v1/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from ....schemas.v1 import ProjectCreateV1
from ....schemas.v1 import ProjectReadV1
from ....schemas.v1 import ProjectUpdateV1
from ...aux.validate_user_settings import validate_user_settings
from ._aux_functions import _check_project_exists
from ._aux_functions import _get_dataset_check_owner
from ._aux_functions import _get_project_check_owner
Expand Down Expand Up @@ -321,25 +322,11 @@ async def apply_workflow(
),
)

# If backend is SLURM, check that the user has required attributes
backend = settings.FRACTAL_RUNNER_BACKEND
if backend == "slurm":
if not user.slurm_user:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=(
f"FRACTAL_RUNNER_BACKEND={backend}, "
f"but {user.slurm_user=}."
),
)
if not user.cache_dir:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=(
f"FRACTAL_RUNNER_BACKEND={backend}, "
f"but {user.cache_dir=}."
),
)
# Validate user settings
FRACTAL_RUNNER_BACKEND = settings.FRACTAL_RUNNER_BACKEND
user_settings = await validate_user_settings(
user=user, backend=FRACTAL_RUNNER_BACKEND, db=db
)

# Check that datasets have the right number of resources
if not input_dataset.resource_list:
Expand Down Expand Up @@ -386,7 +373,7 @@ async def apply_workflow(
)

if apply_workflow.slurm_account is not None:
if apply_workflow.slurm_account not in user.slurm_accounts:
if apply_workflow.slurm_account not in user_settings.slurm_accounts:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=(
Expand All @@ -395,8 +382,8 @@ async def apply_workflow(
),
)
else:
if len(user.slurm_accounts) > 0:
apply_workflow.slurm_account = user.slurm_accounts[0]
if len(user_settings.slurm_accounts) > 0:
apply_workflow.slurm_account = user_settings.slurm_accounts[0]

# Add new ApplyWorkflow object to DB
job = ApplyWorkflow(
Expand Down Expand Up @@ -480,8 +467,8 @@ async def apply_workflow(
output_dataset_id=output_dataset.id,
job_id=job.id,
worker_init=apply_workflow.worker_init,
slurm_user=user.slurm_user,
user_cache_dir=user.cache_dir,
slurm_user=user_settings.slurm_user,
user_cache_dir=user_settings.cache_dir,
)
request.app.state.jobsV1.append(job.id)
logger.info(
Expand Down
21 changes: 12 additions & 9 deletions fractal_server/app/routes/api/v1/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ....schemas.v1 import TaskCreateV1
from ....schemas.v1 import TaskReadV1
from ....schemas.v1 import TaskUpdateV1
from ...aux.validate_user_settings import verify_user_has_settings
from ._aux_functions import _get_task_check_owner
from ._aux_functions import _raise_if_v1_is_read_only
from fractal_server.app.models import UserOAuth
Expand Down Expand Up @@ -126,16 +127,18 @@ async def create_task(
# Set task.owner attribute
if user.username:
owner = user.username
elif user.slurm_user:
owner = user.slurm_user
else:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=(
"Cannot add a new task because current user does not "
"have `username` or `slurm_user` attributes."
),
)
verify_user_has_settings(user)
if user.settings.slurm_user:
owner = user.settings.slurm_user
else:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=(
"Cannot add a new task because current user does not "
"have `username` or `slurm_user` attributes."
),
)

# Prepend owner to task.source
task.source = f"{owner}:{task.source}"
Expand Down
7 changes: 6 additions & 1 deletion fractal_server/app/routes/api/v2/_aux_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from ....models.v2 import WorkflowTaskV2
from ....models.v2 import WorkflowV2
from ....schemas.v2 import JobStatusTypeV2
from ...aux.validate_user_settings import verify_user_has_settings
from fractal_server.app.models import UserOAuth
from fractal_server.images import Filters

Expand Down Expand Up @@ -362,7 +363,11 @@ async def _get_task_check_owner(
),
)
else:
owner = user.username or user.slurm_user
if user.username:
owner = user.username
else:
verify_user_has_settings(user)
owner = user.settings.slurm_user
if owner != task.owner:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
Expand Down
Loading