← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~r00ta/maas-site-manager:MAASENG-1628 into maas-site-manager:main

 

Jacopo Rota has proposed merging ~r00ta/maas-site-manager:MAASENG-1628 into maas-site-manager:main.

Commit message:
add connection_status in GET sites response

Requested reviews:
  MAAS Lander (maas-lander): unittests
  MAAS Committers (maas-committers)

For more details, see:
https://code.launchpad.net/~r00ta/maas-site-manager/+git/site-manager/+merge/443281

What's included: 
- add `connection_status` property in GET sites response as per https://warthogs.atlassian.net/browse/MAASENG-1628
-- 
Your team MAAS Committers is requested to review the proposed merge of ~r00ta/maas-site-manager:MAASENG-1628 into maas-site-manager:main.
diff --git a/backend/msm/db/models.py b/backend/msm/db/models.py
index 078946a..edabb41 100644
--- a/backend/msm/db/models.py
+++ b/backend/msm/db/models.py
@@ -1,4 +1,5 @@
 from datetime import datetime
+from enum import Enum
 from uuid import UUID
 
 from pydantic import (
@@ -11,6 +12,12 @@ from pydantic import (
 from ..schema import TimeZone
 
 
+class ConnectionStatus(str, Enum):
+    STABLE = "stable"
+    LOST = "lost"
+    UNKNOWN = "unknown"
+
+
 class SiteData(BaseModel):
     """Data for a site."""
 
@@ -38,6 +45,7 @@ class Site(BaseModel):
     street: str | None
     timezone: TimeZone | None
     url: str
+    connection_status: str
     stats: SiteData | None
 
 
diff --git a/backend/msm/db/queries.py b/backend/msm/db/queries.py
index 77b173e..16df66f 100644
--- a/backend/msm/db/queries.py
+++ b/backend/msm/db/queries.py
@@ -23,6 +23,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.sql.expression import FromClause
 
 from ..schema import MAX_PAGE_SIZE
+from ..settings import SETTINGS
 from ._tables import (
     Site,
     SiteData,
@@ -30,6 +31,7 @@ from ._tables import (
     User,
 )
 from .models import (
+    ConnectionStatus,
     PendingSite as PendingSiteSchema,
     Site as SiteSchema,
     Token as TokenSchema,
@@ -146,6 +148,9 @@ async def get_sites(
     )
     filters.append(Site.c.accepted == True)  # noqa
     count = await row_count(session, Site, *filters)
+    connection_lost_timedelta = datetime.utcnow() - timedelta(
+        seconds=SETTINGS.lost_connection_threshold_seconds
+    )
     stmt = (
         select(
             Site.c.id,
@@ -163,6 +168,19 @@ async def get_sites(
             case(
                 (
                     SiteData.c.site_id != None,  # noqa: E711
+                    case(
+                        (
+                            SiteData.c.last_seen > connection_lost_timedelta,
+                            ConnectionStatus.STABLE,
+                        ),
+                        else_=ConnectionStatus.LOST,
+                    ),
+                ),
+                else_=ConnectionStatus.UNKNOWN,
+            ).label("connection_status"),
+            case(
+                (
+                    SiteData.c.site_id != None,  # noqa: E711
                     func.json_build_object(
                         "total_machines",
                         (
diff --git a/backend/msm/settings.py b/backend/msm/settings.py
index a010b54..4bfb651 100644
--- a/backend/msm/settings.py
+++ b/backend/msm/settings.py
@@ -44,5 +44,9 @@ class Settings(BaseSettings):
 
     access_token_expire_minutes = int(getenv("TOKEN_EXPIRATION_TIME", 30))
 
+    lost_connection_threshold_seconds = int(
+        getenv("LOST_CONNECTION_THRESHOLD", 60)
+    )
+
 
 SETTINGS = Settings()
diff --git a/backend/tests/user_api/test_handlers.py b/backend/tests/user_api/test_handlers.py
index b4ae28c..422a360 100644
--- a/backend/tests/user_api/test_handlers.py
+++ b/backend/tests/user_api/test_handlers.py
@@ -8,6 +8,8 @@ from httpx import AsyncClient
 import pytest
 
 from msm import __version__
+from msm.db.models import ConnectionStatus
+from msm.settings import SETTINGS
 
 from ..fixtures.app import AuthAsyncClient
 from ..fixtures.db import Fixture
@@ -65,10 +67,10 @@ class TestSitesHandler:
             ],
         )
         for site in sites:
+            site["connection_status"] = ConnectionStatus.UNKNOWN
             site["stats"] = None
             del site["created"]
             del site["accepted"]
-
         page1 = await authenticated_user_app_client.get("/sites")
         assert page1.status_code == 200
         assert page1.json() == {
@@ -109,6 +111,7 @@ class TestSitesHandler:
             ],
         )
         created_site["stats"] = None
+        created_site["connection_status"] = ConnectionStatus.UNKNOWN
         del created_site["created"]
         del created_site["accepted"]
 
@@ -132,6 +135,7 @@ class TestSitesHandler:
             ],
         )
         created_site["stats"] = None
+        created_site["connection_status"] = ConnectionStatus.UNKNOWN
         del created_site["created"]
         del created_site["accepted"]
         page1 = await authenticated_user_app_client.get(
@@ -168,6 +172,7 @@ class TestSitesHandler:
         site_data["last_seen"] = site_data["last_seen"].isoformat()
         site_data["total_machines"] = 105
         site["stats"] = site_data
+        site["connection_status"] = ConnectionStatus.STABLE
         del site["created"]
         del site["accepted"]
 
@@ -180,6 +185,38 @@ class TestSitesHandler:
             "items": [site],
         }
 
+    async def test_get_connection_status(
+        self, authenticated_user_app_client: AuthAsyncClient, fixture: Fixture
+    ) -> None:
+        [site] = await fixture.create("site", [site_details()])
+        await fixture.create(
+            "site_data",
+            [
+                {
+                    "site_id": site["id"],
+                    "allocated_machines": 10,
+                    "deployed_machines": 20,
+                    "ready_machines": 30,
+                    "error_machines": 40,
+                    "other_machines": 5,
+                    # Set last_seen to 1 second after the
+                    # lost_connection_threshold setting in order to test
+                    # that the connection is marked as lost
+                    "last_seen": datetime.utcnow()
+                    - timedelta(
+                        seconds=SETTINGS.lost_connection_threshold_seconds + 1
+                    ),
+                }
+            ],
+        )
+
+        page = await authenticated_user_app_client.get("/sites")
+        assert page.status_code == 200
+        assert (
+            page.json()["items"][0]["connection_status"]
+            == ConnectionStatus.LOST
+        )
+
 
 @pytest.mark.asyncio
 class TestPendingSitesHandler: