forked from CentOS/duffy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This lets API users list pools and query individual ones. In the course, sort the API online doc sections a little. Related: CentOS#417 Signed-off-by: Nils Philippsen <nils@redhat.com>
- Loading branch information
Showing
6 changed files
with
223 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
from abc import ABC | ||
from typing import List, Union | ||
|
||
from pydantic import BaseModel, Field, conint | ||
|
||
from .common import APIResult | ||
|
||
# pool model | ||
|
||
|
||
class PoolLevelsModel(BaseModel): | ||
provisioning: conint(ge=0) | ||
ready: conint(ge=0) | ||
contextualizing: conint(ge=0) | ||
deployed: conint(ge=0) | ||
deprovisioning: conint(ge=0) | ||
|
||
|
||
class PoolBase(BaseModel, ABC): | ||
name: str | ||
fill_level: conint(ge=0) = Field(alias="fill-level") | ||
|
||
class Config: | ||
extra = "forbid" | ||
|
||
|
||
class PoolConciseModel(PoolBase): | ||
pass | ||
|
||
|
||
class PoolVerboseModel(PoolConciseModel): | ||
levels: PoolLevelsModel | ||
|
||
|
||
PoolModel = Union[PoolConciseModel, PoolVerboseModel] | ||
|
||
|
||
# API results | ||
|
||
|
||
class PoolResult(APIResult): | ||
pool: PoolModel | ||
|
||
|
||
class PoolResultCollection(APIResult): | ||
pools: List[PoolModel] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
"""This is the pool controller.""" | ||
|
||
from fastapi import APIRouter, Depends, HTTPException | ||
from sqlalchemy import func, select | ||
from sqlalchemy.ext.asyncio import AsyncSession | ||
from starlette.status import HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY | ||
|
||
from ...api_models import PoolResult, PoolResultCollection | ||
from ...database.model import Node | ||
from ...nodes.pools import ConcreteNodePool | ||
from ..database import req_db_async_session | ||
|
||
router = APIRouter(prefix="/pools") | ||
|
||
|
||
# http get http://localhost:8080/api/v1/pools | ||
@router.get("", response_model=PoolResultCollection, tags=["pools"]) | ||
async def get_all_pools(db_async_session: AsyncSession = Depends(req_db_async_session)): | ||
"""Return all pools.""" | ||
pools = [ | ||
{"name": pool.name, "fill-level": pool["fill-level"]} | ||
for pool in ConcreteNodePool.iter_pools() | ||
if "fill-level" in pool | ||
] | ||
|
||
return {"action": "get", "pools": pools} | ||
|
||
|
||
# http get http://localhost:8080/api/v1/pool/name-of-the-pool | ||
@router.get("/{name}", response_model=PoolResult, tags=["pools"]) | ||
async def get_pool(name: str, db_async_session: AsyncSession = Depends(req_db_async_session)): | ||
"""Return the pool with the specified **NAME**.""" | ||
pool = ConcreteNodePool.known_pools.get(name) | ||
|
||
if pool is None: | ||
raise HTTPException(HTTP_404_NOT_FOUND) | ||
|
||
if "fill-level" not in pool: | ||
raise HTTPException(HTTP_422_UNPROCESSABLE_ENTITY) | ||
|
||
pool_result = { | ||
"name": name, | ||
"fill-level": pool["fill-level"], | ||
"levels": { | ||
"provisioning": 0, | ||
"ready": 0, | ||
"contextualizing": 0, | ||
"deployed": 0, | ||
"deprovisioning": 0, | ||
}, | ||
} | ||
|
||
for state, quantity in await db_async_session.execute( | ||
select(Node.state, func.count(Node.state)) | ||
.filter(Node.active == True, Node.pool == name) # noqa: E712 | ||
.group_by(Node.state) | ||
): | ||
pool_result["levels"][state.name] = quantity | ||
|
||
return {"action": "get", "pool": pool_result} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
from unittest import mock | ||
|
||
import pytest | ||
from starlette.status import HTTP_200_OK, HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY | ||
|
||
from duffy.database.model import Node | ||
|
||
|
||
class MockPool(dict): | ||
def __init__(self, name, **kwargs): | ||
self.name = name | ||
super().__init__(**kwargs) | ||
|
||
|
||
@pytest.mark.duffy_config(example_config=True, clear=True) | ||
class TestPool: | ||
@mock.patch("duffy.app.controllers.pool.ConcreteNodePool") | ||
async def test_get_all_pools(self, ConcreteNodePool, client): | ||
ConcreteNodePool.iter_pools.return_value = [ | ||
MockPool(name="foo"), # missing fill-level, shouldn't be listed | ||
MockPool(name="bar", **{"fill-level": 64}), | ||
] | ||
|
||
response = await client.get("/api/v1/pools") | ||
result = response.json() | ||
|
||
ConcreteNodePool.iter_pools.assert_called_once_with() | ||
|
||
assert result["pools"] == [{"name": "bar", "fill-level": 64}] | ||
|
||
@pytest.mark.parametrize("pool", ("foo", "bar", "baz")) | ||
@mock.patch("duffy.app.controllers.pool.ConcreteNodePool") | ||
async def test_get_pools( | ||
self, ConcreteNodePool, pool, client, db_async_session, db_async_model_initialized | ||
): | ||
ConcreteNodePool.known_pools = { | ||
"foo": MockPool(name="foo"), # missing fill-level, shouldn't be listed | ||
"bar": MockPool(name="bar", **{"fill-level": 64}), | ||
} | ||
|
||
ipaddr_octet = 1 | ||
for state, quantity in (("ready", 3), ("deployed", 2)): | ||
for idx in range(quantity): | ||
ipaddr_octet += 1 | ||
db_async_session.add( | ||
Node( | ||
hostname=f"node-{ipaddr_octet}", | ||
ipaddr=f"192.168.1.{ipaddr_octet}", | ||
pool="bar", | ||
state=state, | ||
) | ||
) | ||
await db_async_session.flush() | ||
|
||
if pool == "bar": | ||
expected_status = HTTP_200_OK | ||
expected_result = { | ||
"action": "get", | ||
"pool": { | ||
"name": "bar", | ||
"fill-level": 64, | ||
"levels": { | ||
"provisioning": 0, | ||
"ready": 3, | ||
"contextualizing": 0, | ||
"deployed": 2, | ||
"deprovisioning": 0, | ||
}, | ||
}, | ||
} | ||
else: | ||
expected_result = None | ||
if pool == "foo": | ||
expected_status = HTTP_422_UNPROCESSABLE_ENTITY | ||
else: | ||
expected_status = HTTP_404_NOT_FOUND | ||
|
||
response = await client.get(f"/api/v1/pools/{pool}") | ||
result = response.json() | ||
|
||
assert response.status_code == expected_status | ||
|
||
if expected_result: | ||
assert result == expected_result | ||
else: | ||
assert "detail" in result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters