diff --git a/.github/linters/.codespellrc b/.github/linters/.codespellrc new file mode 100644 index 0000000..f1f8496 --- /dev/null +++ b/.github/linters/.codespellrc @@ -0,0 +1,2 @@ +[codespell] +ignore-words-list = implementor diff --git a/app/conftest.py b/app/conftest.py index 60511d9..a97be2b 100644 --- a/app/conftest.py +++ b/app/conftest.py @@ -4,7 +4,8 @@ import json import pytest -from app.glue import GlueImage, GlueInstanceType, GlueShare, GlueSite + +from .glue import GlueImage, GlueInstanceType, GlueShare, GlueSite @pytest.fixture @@ -163,7 +164,7 @@ def site_info(): "InterfaceName": "org.openstack.nova", "InterfaceVersion": "2.0", "HealthState": "ok", - "HealthStateInfo": "Endpoint funtioning properly", + "HealthStateInfo": "Endpoint functioning properly", "ServingState": "production", "Technology": "webservice", "Implementor": "OpenStack Foundation", diff --git a/app/main.py b/app/main.py index 692a139..c8d53d6 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,5 @@ """ -Appdb Information Sistem +Appdb Information System A simple wrapper around the cloud-info jsons to deliver the information needed by IM @@ -9,11 +9,13 @@ from contextlib import asynccontextmanager from typing import Optional -from app.glue import Discipline, FileSiteStore, VOStore -from fastapi import FastAPI, HTTPException +import yaml +from fastapi import FastAPI, HTTPException, Request, Response from pydantic import BaseModel from pydantic_settings import BaseSettings +from .glue import Discipline, FileSiteStore, VOStore + class Image(BaseModel): egi_id: str @@ -77,6 +79,10 @@ async def lifespan(app: FastAPI): "name": "images", "description": "Discovery of images.", }, + { + "name": "fedcloudclient", + "description": "Fedcloudclient configuration files.", + }, ] @@ -222,3 +228,29 @@ def get_all_images(vo_name: str = "", only_egi_images: bool = True) -> list[Imag else: images.extend(Image(**img, endpoint=site.url) for img in site.image_list()) return filter_images(images, only_egi_images) + + +@app.get("/fedcloudclient/", tags=["fedcloudclient"]) +def get_fedcloudclient_sites(request: Request) -> list[str]: + """Get a list of available site configurations for fedcloudclient.""" + return [ + str(request.url_for("get_fedcloudclient_site", site_name=s.name)) + for s in site_store.get_sites() + ] + + +@app.get("/fedcloudclient/{site_name}/", tags=["fedcloudclient"]) +def get_fedcloudclient_site(site_name: str) -> str: + """Get site information as yaml compatible with fedcloudclient + + Name of the site in the GOCDB + """ + site = _get_site(site_name) + fedcloud_site = { + "gocdb": site.name, + "endpoint": site.url, + "vos": [ + {"name": p.vo, "auth": {"project_id": p.project_id}} for p in site.shares + ], + } + return Response(content=yaml.dump(fedcloud_site), media_type="application/yaml") diff --git a/app/test_glue.py b/app/test_glue.py index 0f4cbf2..d260e95 100644 --- a/app/test_glue.py +++ b/app/test_glue.py @@ -5,10 +5,11 @@ from http import HTTPStatus from unittest import mock -import app.glue import httpx import pytest +from . import glue + def test_gluesite_object(site): site = site @@ -65,8 +66,8 @@ def test_vo_store_get_vos(ops_portal): ) ) ) - vos = [app.glue.VO(**vo) for vo in ops_portal["data"]] - vo_store = app.glue.VOStore( + vos = [glue.VO(**vo) for vo in ops_portal["data"]] + vo_store = glue.VOStore( ops_portal_url="https://example.com", httpx_client=test_client ) assert vos == vo_store.get_vos() @@ -78,7 +79,7 @@ def test_vo_store_get_vos_failure(): lambda request: httpx.Response(HTTPStatus.FORBIDDEN, content="foo") ) ) - vo_store = app.glue.VOStore( + vo_store = glue.VOStore( ops_portal_url="https://example.com", httpx_client=test_client ) assert [] == vo_store.get_vos() @@ -86,13 +87,13 @@ def test_vo_store_get_vos_failure(): def test_vo_store_get_disciplines(disciplines_json, discipline): with mock.patch("builtins.open", mock.mock_open(read_data=disciplines_json)): - vo_store = app.glue.VOStore(vo_disciplines_file="foo.json") - assert [app.glue.Discipline(**discipline)] == vo_store.get_disciplines() + vo_store = glue.VOStore(vo_disciplines_file="foo.json") + assert [glue.Discipline(**discipline)] == vo_store.get_disciplines() def test_vo_store_get_disciplines_bad_json(): with mock.patch("builtins.open", mock.mock_open(read_data="")): - vo_store = app.glue.VOStore(vo_disciplines_file="foo.json") + vo_store = glue.VOStore(vo_disciplines_file="foo.json") assert [] == vo_store.get_disciplines() @@ -103,8 +104,8 @@ def test_gocdb_info(gocdb): ) ) with mock.patch("app.glue.SiteStore.get_mp_image_data"): - site_store = app.glue.SiteStore( - gocdb_url="https://exmaple.com", httpx_client=test_client + site_store = glue.SiteStore( + gocdb_url="https://example.com", httpx_client=test_client ) hostname = site_store._get_gocdb_hostname("7513G0") assert hostname == "api.cloud.ifca.es" @@ -119,13 +120,13 @@ def test_create_site(site_info, site, images): goc_hostname.return_value = "foo" image_data.return_value = images[0] m_datetime.return_value = datetime.datetime.now() - site_store = app.glue.SiteStore() + site_store = glue.SiteStore() loaded_site = site_store.create_site(site_info) assert site == loaded_site def test_valid_info_check(site_info): - site_store = app.glue.SiteStore() + site_store = glue.SiteStore() with pytest.raises(ValueError): site_store.create_site(site_info) @@ -133,7 +134,7 @@ def test_valid_info_check(site_info): def test_validity_disabled(site_info): with mock.patch("app.glue.SiteStore._get_gocdb_hostname") as goc_hostname: goc_hostname.return_value = "foo" - site_store = app.glue.SiteStore(check_glue_validity=False) + site_store = glue.SiteStore(check_glue_validity=False) site = site_store.create_site(site_info) assert site is not None @@ -143,7 +144,7 @@ def test_get_sites(site): mock.patch("app.glue.SiteStore.get_mp_image_data"), mock.patch("app.glue.SiteStore._sites") as _sites, ): - site_store = app.glue.SiteStore() + site_store = glue.SiteStore() _sites.return_value = [site] # no VO assert site_store.get_sites() == [site] @@ -158,7 +159,7 @@ def test_get_site_by_goc_id(site): mock.patch("app.glue.SiteStore.get_mp_image_data"), mock.patch("app.glue.SiteStore._sites") as _sites, ): - site_store = app.glue.SiteStore() + site_store = glue.SiteStore() _sites.return_value = [site] # unknown ID assert site_store.get_site_by_goc_id("foo") is None @@ -171,7 +172,7 @@ def test_get_site_by_name(site): mock.patch("app.glue.SiteStore.get_mp_image_data"), mock.patch("app.glue.SiteStore._sites") as _sites, ): - site_store = app.glue.SiteStore() + site_store = glue.SiteStore() _sites.return_value = [site] # unknown name assert site_store.get_site_by_name("foo") is None @@ -184,7 +185,7 @@ def test_get_site_summary(site): mock.patch("app.glue.SiteStore.get_mp_image_data"), mock.patch("app.glue.SiteStore._sites") as _sites, ): - site_store = app.glue.SiteStore() + site_store = glue.SiteStore() _sites.return_value = [site] site_summary = site.summary() # no VO @@ -196,7 +197,7 @@ def test_get_site_summary(site): def test_get_mp_image_data(glue_image): - site_store = app.glue.SiteStore() + site_store = glue.SiteStore() image_info = glue_image mp_data = site_store.get_mp_image_data(image_info) assert mp_data == { @@ -207,7 +208,7 @@ def test_get_mp_image_data(glue_image): def test_load_bad_json_site_file(): - site_store = app.glue.FileSiteStore() + site_store = glue.FileSiteStore() with mock.patch("builtins.open", mock.mock_open(read_data="xxx")) as m_open: site = site_store._load_site_file("foo") m_open.assert_called_with("foo") @@ -215,7 +216,7 @@ def test_load_bad_json_site_file(): def test_load_json_site_file(site_info_json): - site_store = app.glue.FileSiteStore(check_glue_validity=False) + site_store = glue.FileSiteStore(check_glue_validity=False) with mock.patch( "builtins.open", mock.mock_open(read_data=site_info_json) ) as m_open: @@ -225,8 +226,8 @@ def test_load_json_site_file(site_info_json): def test_glue_site_load_duplicated(site): - site_store = app.glue.FileSiteStore(check_glue_validity=False) - duplicated = app.glue.GlueSite(**site.model_dump()) + site_store = glue.FileSiteStore(check_glue_validity=False) + duplicated = glue.GlueSite(**site.model_dump()) duplicated.gocdb_id = "0G" sites = site_store._clean_up_duplicated_sites({site.name: [duplicated, site]}) assert set([s.name for s in sites]) == set(["BIFI", "BIFI-0G"]) diff --git a/app/test_main.py b/app/test_main.py index f6efeb3..d177d1e 100644 --- a/app/test_main.py +++ b/app/test_main.py @@ -3,11 +3,13 @@ from unittest import mock import pytest -from app.glue import VO, Discipline -from app.main import _get_site, app, site_store, vo_store +import yaml from fastapi import HTTPException from fastapi.testclient import TestClient +from .glue import VO, Discipline +from .main import _get_site, app, site_store, vo_store + client = TestClient(app) @@ -175,3 +177,32 @@ def test_get_images_non_egi(site, another_site, more_images): response = client.get("/images", params={"only_egi_images": False}) assert response.status_code == 200 assert response.json() == more_images + + +def test_get_fedcloud_sites(site, another_site): + with mock.patch.object(site_store, "get_sites") as m_get_sites: + m_get_sites.return_value = [site, another_site] + response = client.get("/fedcloudclient/") + assert response.status_code == 200 + assert response.json() == [ + "http://testserver/fedcloudclient/BIFI/", + "http://testserver/fedcloudclient/FAKE/", + ] + + +def test_get_fedcloud_site(site): + with mock.patch.object(site_store, "get_site_by_name") as m_get_site: + m_get_site.return_value = site + response = client.get("/fedcloudclient/foo") + assert response.status_code == 200 + expected_site = { + "endpoint": "https://colossus.cesar.unizar.es:5000/v3", + "gocdb": "BIFI", + "vos": [ + { + "name": "ops", + "auth": {"project_id": "038db3eeca5c4960a443a89b92373cd2"}, + } + ], + } + assert yaml.safe_load(response.text) == expected_site