Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- The System Name path parameter and the corresponding Cluster name configuration are case insensitive.

## [2.3.1]

### Added
Expand Down
4 changes: 2 additions & 2 deletions f7t-api-config.local-env-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ssh_credentials:
url: "http://192.168.240.20:5000"
max_connections: 500
clusters:
- name: "cluster-slurm-api"
- name: "cluster-SLURM-api"
ssh:
host: "192.168.240.2"
port: 22
Expand All @@ -33,7 +33,7 @@ clusters:
- path: '/home'
data_type: 'users'
default_work_dir: true
- name: "cluster-slurm-ssh"
- name: "cluster-SLURM-ssh"
ssh:
host: "192.168.240.2"
port: 22
Expand Down
11 changes: 10 additions & 1 deletion src/firecrest/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from enum import Enum
import os
import pydantic
import yaml
from pathlib import Path
from typing import Any, Dict, Literal, Tuple, Type, Union
Expand Down Expand Up @@ -314,7 +315,9 @@ class HPCCluster(CamelModel):
[the systems' section](../arch/systems//README.md).
"""

name: str = Field(..., description="Unique name for the cluster.")
name: str = Field(
..., description="Unique name for the cluster. This field is case insensitive."
)
ssh: SSHClientPool = Field(
..., description="SSH configuration for accessing the cluster nodes."
)
Expand Down Expand Up @@ -345,6 +348,12 @@ class HPCCluster(CamelModel):
description="Custom scheduler flags passed to data transfer jobs (e.g. `-pxfer` for a dedicated partition).",
)

@pydantic.field_validator("name", mode="before")
def to_lowercase(cls, value):
if isinstance(value, str):
return value.lower()
return value


class OpenFGA(CamelModel):
"""Authorization settings using OpenFGA."""
Expand Down
9 changes: 9 additions & 0 deletions src/firecrest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ async def log_middleware(request: Request, call_next):
)
raise e

@app.middleware("http")
async def lower_case_path(request: Request, call_next):

path = request.scope["path"].lower()
request.scope["path"] = path

response = await call_next(request)
return response


def register_routes(app: FastAPI, settings: config.Settings):
app.include_router(status_router)
Expand Down
6 changes: 5 additions & 1 deletion tests/clusters_config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ async def test_settings(app_settings: Settings):

assert app_settings is not None
assert len(app_settings.clusters) == 3
assert app_settings.clusters[0].name == "cluster-slurm-api"
assert (
# test case insensitive, name is always lower case
app_settings.clusters[0].name
== "cluster-slurm-api"
)
assert app_settings.clusters[0].scheduler is not None
assert app_settings.clusters[0].scheduler.type == SchedulerType.slurm

Expand Down
41 changes: 40 additions & 1 deletion tests/compute_slurm_api_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@ def mocked_get_job_response():
with response_file.open("r") as response:
return json.load(response)


@pytest.fixture(scope="module")
def mocked_get_jobs_allusers_response():
response_file = impresources.files(mocked_api_responses) / "slurm_get_allusers_jobs.json"
response_file = (
impresources.files(mocked_api_responses) / "slurm_get_allusers_jobs.json"
)
with response_file.open("r") as response:
return json.load(response)


@pytest.fixture(scope="module")
def mocked_get_job_not_found_response():
response_file = (
Expand Down Expand Up @@ -133,6 +137,41 @@ def test_get_job(client, mocked_get_job_response, slurm_cluster_with_api_config)
)


def test_case_insensitive_system_name(
client, mocked_get_job_response, slurm_cluster_with_api_config
):

job_id = mocked_get_job_response["jobs"][0]["job_id"]

with aioresponses() as mocked:
mocked.get(
f"{slurm_cluster_with_api_config.scheduler.api_url}/slurmdb/v{slurm_cluster_with_api_config.scheduler.api_version}/job/{job_id}",
status=200,
body=json.dumps(mocked_get_job_response),
)

response = client.get(
f"/compute/{slurm_cluster_with_api_config.name.upper()}/jobs/{job_id}"
)
assert response.status_code == 200
assert response.json() is not None
jobs_result = GetJobResponse(**response.json())
assert jobs_result.jobs[0].job_id == job_id
timeout = aiohttp.ClientTimeout(
total=slurm_cluster_with_api_config.scheduler.timeout
)
mocked.assert_called_once_with(
f"{slurm_cluster_with_api_config.scheduler.api_url}/slurmdb/v{slurm_cluster_with_api_config.scheduler.api_version}/job/{job_id}",
method="GET",
headers={
"Content-Type": "application/json",
"X-SLURM-USER-NAME": "test-user",
"X-SLURM-USER-TOKEN": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOiJ0ZXN0IiwicHJlZmZlcmVkLXVzZXJuYW1lIjoidGVzdCJ9.9lEMnYRwLVeOTQKoXxzMd81zJNOAEnrDI3QtcJsUi7A",
},
timeout=timeout,
)


def test_get_job_not_found(
client, mocked_get_job_not_found_response, slurm_cluster_with_api_config
):
Expand Down