Merge ~lloydwaltersj/maas:machine-get into maas:master

Proposed by Jack Lloyd-Walters
Status: Merged
Approved by: Jacopo Rota
Approved revision: e9658ae891d0e3ad8ee318a95530eecf1969d1ec
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~lloydwaltersj/maas:machine-get
Merge into: maas:master
Diff against target: 540 lines (+436/-0)
12 files modified
src/maasapiserver/v3/api/handlers/__init__.py (+2/-0)
src/maasapiserver/v3/api/handlers/machines.py (+44/-0)
src/maasapiserver/v3/api/models/requests/machines.py (+6/-0)
src/maasapiserver/v3/api/models/responses/machines.py (+48/-0)
src/maasapiserver/v3/db/machines.py (+90/-0)
src/maasapiserver/v3/models/machines.py (+48/-0)
src/maasapiserver/v3/services/__init__.py (+3/-0)
src/maasapiserver/v3/services/machines.py (+26/-0)
src/tests/fixtures/factories/machines.py (+29/-0)
src/tests/fixtures/factories/node.py (+2/-0)
src/tests/maasapiserver/v3/api/test_machines.py (+87/-0)
src/tests/maasapiserver/v3/db/test_machines.py (+51/-0)
Reviewer Review Type Date Requested Status
Jacopo Rota Approve
MAAS Lander Approve
Review via email: mp+463595@code.launchpad.net

Commit message

MAASENG-2951: Add v3 api /machines GET

To post a comment you must log in.
~lloydwaltersj/maas:machine-get updated
9cad4f4... by Jack Lloyd-Walters

join with other tables

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b machine-get lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/5205/console
COMMIT: 8e84d91472ad2109f4e2b42dd9f6e3639476e794

review: Needs Fixing
~lloydwaltersj/maas:machine-get updated
b31ceb6... by Jack Lloyd-Walters

fix fixtures

0cd96ca... by Jack Lloyd-Walters

linting

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b machine-get lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/5217/console
COMMIT: 0cd96ca0d0e6daa2c5697834085ce02e98a2190e

review: Needs Fixing
~lloydwaltersj/maas:machine-get updated
b016757... by Jack Lloyd-Walters

linting

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b machine-get lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: FAILED
LOG: http://maas-ci.internal:8080/job/maas-tester/5229/console
COMMIT: b016757ec7e47734dd4b3b89c41bb19e2be07d03

review: Needs Fixing
~lloydwaltersj/maas:machine-get updated
9f56aee... by Jack Lloyd-Walters

ensure fixtures have data

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b machine-get lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 9f56aee146f0ae0abd60920c224a451aa11d9520

review: Approve
Revision history for this message
Jacopo Rota (r00ta) wrote :

It looks good to me, I have just a small question

review: Needs Information
~lloydwaltersj/maas:machine-get updated
e9658ae... by Jack Lloyd-Walters

fix according to feedback

Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b machine-get lp:~lloydwaltersj/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: e9658ae891d0e3ad8ee318a95530eecf1969d1ec

review: Approve
Revision history for this message
Jacopo Rota (r00ta) wrote :

lgtm

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasapiserver/v3/api/handlers/__init__.py b/src/maasapiserver/v3/api/handlers/__init__.py
2index e28615e..865492c 100644
3--- a/src/maasapiserver/v3/api/handlers/__init__.py
4+++ b/src/maasapiserver/v3/api/handlers/__init__.py
5@@ -1,5 +1,6 @@
6 from maasapiserver.common.api.base import API
7 from maasapiserver.v3.api.handlers.auth import AuthHandler
8+from maasapiserver.v3.api.handlers.machines import MachinesHandler
9 from maasapiserver.v3.api.handlers.resource_pools import ResourcePoolHandler
10 from maasapiserver.v3.api.handlers.root import RootHandler
11 from maasapiserver.v3.api.handlers.zones import ZonesHandler
12@@ -12,5 +13,6 @@ APIv3 = API(
13 ZonesHandler(),
14 ResourcePoolHandler(),
15 AuthHandler(),
16+ MachinesHandler(),
17 ],
18 )
19diff --git a/src/maasapiserver/v3/api/handlers/machines.py b/src/maasapiserver/v3/api/handlers/machines.py
20new file mode 100644
21index 0000000..13a2761
22--- /dev/null
23+++ b/src/maasapiserver/v3/api/handlers/machines.py
24@@ -0,0 +1,44 @@
25+from fastapi import Depends, Response
26+
27+from maasapiserver.common.api.base import Handler, handler
28+from maasapiserver.common.api.models.responses.errors import (
29+ ValidationErrorBodyResponse,
30+)
31+from maasapiserver.v3.api import services
32+from maasapiserver.v3.api.models.requests.query import PaginationParams
33+from maasapiserver.v3.api.models.responses.machines import MachinesListResponse
34+from maasapiserver.v3.constants import EXTERNAL_V3_API_PREFIX
35+from maasapiserver.v3.services import ServiceCollectionV3
36+
37+
38+class MachinesHandler(Handler):
39+ """Machines API handler."""
40+
41+ TAGS = ["Machines"]
42+
43+ @handler(
44+ path="/machines",
45+ methods=["GET"],
46+ tags=TAGS,
47+ responses={
48+ 200: {
49+ "model": MachinesListResponse,
50+ },
51+ 422: {"model": ValidationErrorBodyResponse},
52+ },
53+ response_model_exclude_none=True,
54+ status_code=200,
55+ )
56+ async def list_machines(
57+ self,
58+ pagination_params: PaginationParams = Depends(),
59+ services: ServiceCollectionV3 = Depends(services),
60+ ) -> Response:
61+ machines = await services.machines.list(pagination_params)
62+ return MachinesListResponse(
63+ items=[
64+ machine.to_response(f"{EXTERNAL_V3_API_PREFIX}/machines")
65+ for machine in machines.items
66+ ],
67+ total=machines.total,
68+ )
69diff --git a/src/maasapiserver/v3/api/models/requests/machines.py b/src/maasapiserver/v3/api/models/requests/machines.py
70new file mode 100644
71index 0000000..54aac9d
72--- /dev/null
73+++ b/src/maasapiserver/v3/api/models/requests/machines.py
74@@ -0,0 +1,6 @@
75+from maasapiserver.v3.api.models.requests.base import NamedBaseModel
76+
77+
78+class MachineRequest(NamedBaseModel):
79+ # TODO
80+ pass
81diff --git a/src/maasapiserver/v3/api/models/responses/machines.py b/src/maasapiserver/v3/api/models/responses/machines.py
82new file mode 100644
83index 0000000..038b133
84--- /dev/null
85+++ b/src/maasapiserver/v3/api/models/responses/machines.py
86@@ -0,0 +1,48 @@
87+from enum import Enum
88+
89+from maasapiserver.v3.api.models.responses.base import (
90+ BaseHal,
91+ HalResponse,
92+ PaginatedResponse,
93+)
94+from maasserver.enum import NODE_STATUS_CHOICES
95+from provisioningserver.drivers.pod.lxd import LXDPodDriver
96+from provisioningserver.drivers.pod.virsh import VirshPodDriver
97+from provisioningserver.drivers.power.registry import power_drivers
98+
99+MachineStatusEnum = Enum(
100+ "MachineStatus",
101+ dict({str(name).lower(): int(code) for code, name in NODE_STATUS_CHOICES}),
102+)
103+PowerTypeEnum = Enum(
104+ "PowerType",
105+ dict(
106+ {
107+ str(driver.name).lower(): str(driver.name).lower()
108+ for driver in power_drivers + [LXDPodDriver(), VirshPodDriver()]
109+ }
110+ ),
111+)
112+
113+
114+class MachineResponse(HalResponse[BaseHal]):
115+ kind = "Machine"
116+ id: int
117+ system_id: str
118+ description: str
119+ owner: str
120+ cpu_speed_MHz: int
121+ memory_MiB: int
122+ osystem: str
123+ architecture: str
124+ distro_series: str
125+ hwe_kernel: str
126+ locked: bool
127+ cpu_count: int
128+ status: MachineStatusEnum
129+ power_type: PowerTypeEnum
130+ fqdn: str
131+
132+
133+class MachinesListResponse(PaginatedResponse[MachineResponse]):
134+ kind = "MachinesList"
135diff --git a/src/maasapiserver/v3/db/machines.py b/src/maasapiserver/v3/db/machines.py
136new file mode 100644
137index 0000000..f17ddad
138--- /dev/null
139+++ b/src/maasapiserver/v3/db/machines.py
140@@ -0,0 +1,90 @@
141+from typing import Any
142+
143+from sqlalchemy import desc, select, Select
144+from sqlalchemy.sql.expression import func
145+from sqlalchemy.sql.functions import count
146+from sqlalchemy.sql.operators import eq
147+
148+from maasapiserver.common.db.tables import (
149+ BMCTable,
150+ DomainTable,
151+ NodeTable,
152+ UserTable,
153+)
154+from maasapiserver.v3.api.models.requests.machines import MachineRequest
155+from maasapiserver.v3.api.models.requests.query import PaginationParams
156+from maasapiserver.v3.db.base import BaseRepository
157+from maasapiserver.v3.models.base import ListResult
158+from maasapiserver.v3.models.machines import Machine
159+
160+
161+class MachinesRepository(BaseRepository[Machine, MachineRequest]):
162+ async def create(self, request: MachineRequest) -> Machine:
163+ raise Exception("Not implemented yet.")
164+
165+ async def find_by_id(self, id: int) -> Machine | None:
166+ raise Exception("Not implemented yet.")
167+
168+ async def list(
169+ self, pagination_params: PaginationParams
170+ ) -> ListResult[Machine]:
171+ total_stmt = select(count()).select_from(NodeTable)
172+ total = (await self.connection.execute(total_stmt)).scalar()
173+
174+ stmt = (
175+ self._select_all_statement()
176+ .order_by(desc(NodeTable.c.id))
177+ .offset((pagination_params.page - 1) * pagination_params.size)
178+ .limit(pagination_params.size)
179+ )
180+
181+ result = await self.connection.execute(stmt)
182+ return ListResult[Machine](
183+ items=[Machine(**row._asdict()) for row in result.all()],
184+ total=total,
185+ )
186+
187+ async def update(self, resource: Machine) -> Machine:
188+ raise Exception("Not implemented yet.")
189+
190+ async def delete(self, id: int) -> None:
191+ raise Exception("Not implemented yet.")
192+
193+ def _select_all_statement(self) -> Select[Any]:
194+ return (
195+ select(
196+ NodeTable.c.id,
197+ NodeTable.c.system_id,
198+ NodeTable.c.created,
199+ NodeTable.c.updated,
200+ func.coalesce(UserTable.c.username, "").label("owner"),
201+ NodeTable.c.description,
202+ NodeTable.c.cpu_speed,
203+ NodeTable.c.memory,
204+ NodeTable.c.osystem,
205+ NodeTable.c.architecture,
206+ NodeTable.c.distro_series,
207+ NodeTable.c.hwe_kernel,
208+ NodeTable.c.locked,
209+ NodeTable.c.cpu_count,
210+ NodeTable.c.status,
211+ BMCTable.c.power_type,
212+ func.concat(
213+ NodeTable.c.hostname, ".", DomainTable.c.name
214+ ).label("fqdn"),
215+ )
216+ .select_from(NodeTable)
217+ .join(
218+ DomainTable,
219+ eq(DomainTable.c.id, NodeTable.c.domain_id),
220+ isouter=True,
221+ )
222+ .join(
223+ UserTable,
224+ eq(UserTable.c.id, NodeTable.c.owner_id),
225+ isouter=True,
226+ )
227+ .join(
228+ BMCTable, eq(BMCTable.c.id, NodeTable.c.bmc_id), isouter=True
229+ )
230+ )
231diff --git a/src/maasapiserver/v3/models/machines.py b/src/maasapiserver/v3/models/machines.py
232new file mode 100644
233index 0000000..8b89862
234--- /dev/null
235+++ b/src/maasapiserver/v3/models/machines.py
236@@ -0,0 +1,48 @@
237+from maasapiserver.v3.api.models.responses.base import BaseHal, BaseHref
238+from maasapiserver.v3.api.models.responses.machines import (
239+ MachineResponse,
240+ MachineStatusEnum,
241+ PowerTypeEnum,
242+)
243+from maasapiserver.v3.models.base import MaasTimestampedBaseModel
244+
245+
246+class Machine(MaasTimestampedBaseModel):
247+ system_id: str
248+ description: str
249+ owner: str
250+ cpu_speed: int
251+ memory: int
252+ osystem: str
253+ architecture: str
254+ distro_series: str
255+ hwe_kernel: str
256+ locked: bool
257+ cpu_count: int
258+ status: MachineStatusEnum
259+ power_type: PowerTypeEnum
260+ fqdn: str
261+
262+ def to_response(self, self_base_hyperlink: str) -> MachineResponse:
263+ return MachineResponse(
264+ id=self.id,
265+ system_id=self.system_id,
266+ description=self.description,
267+ owner=self.owner,
268+ cpu_speed_MHz=self.cpu_speed,
269+ memory_MiB=self.memory,
270+ osystem=self.osystem,
271+ architecture=self.architecture,
272+ distro_series=self.distro_series,
273+ hwe_kernel=self.hwe_kernel,
274+ locked=self.locked,
275+ cpu_count=self.cpu_count,
276+ status=self.status,
277+ power_type=self.power_type,
278+ fqdn=self.fqdn,
279+ hal_links=BaseHal(
280+ self=BaseHref(
281+ href=f"{self_base_hyperlink.rstrip('/')}/{self.id}"
282+ )
283+ ),
284+ )
285diff --git a/src/maasapiserver/v3/services/__init__.py b/src/maasapiserver/v3/services/__init__.py
286index 2eac400..51628e0 100644
287--- a/src/maasapiserver/v3/services/__init__.py
288+++ b/src/maasapiserver/v3/services/__init__.py
289@@ -3,6 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncConnection
290 from maasapiserver.v3.services.auth import AuthService
291 from maasapiserver.v3.services.bmc import BmcService
292 from maasapiserver.v3.services.configurations import ConfigurationsService
293+from maasapiserver.v3.services.machines import MachinesService
294 from maasapiserver.v3.services.nodes import NodesService
295 from maasapiserver.v3.services.resource_pools import ResourcePoolsService
296 from maasapiserver.v3.services.secrets import (
297@@ -25,6 +26,7 @@ class ServiceCollectionV3:
298 configurations: ConfigurationsService
299 resource_pools: ResourcePoolsService
300 auth: AuthService
301+ machines: MachinesService
302
303 @classmethod
304 async def produce(
305@@ -51,4 +53,5 @@ class ServiceCollectionV3:
306 bmc_service=services.bmc,
307 )
308 services.resource_pools = ResourcePoolsService(connection=connection)
309+ services.machines = MachinesService(connection=connection)
310 return services
311diff --git a/src/maasapiserver/v3/services/machines.py b/src/maasapiserver/v3/services/machines.py
312new file mode 100644
313index 0000000..171ec23
314--- /dev/null
315+++ b/src/maasapiserver/v3/services/machines.py
316@@ -0,0 +1,26 @@
317+from sqlalchemy.ext.asyncio import AsyncConnection
318+
319+from maasapiserver.common.services._base import Service
320+from maasapiserver.v3.api.models.requests.query import PaginationParams
321+from maasapiserver.v3.db.machines import MachinesRepository
322+from maasapiserver.v3.models.base import ListResult
323+from maasapiserver.v3.models.machines import Machine
324+
325+
326+class MachinesService(Service):
327+ def __init__(
328+ self,
329+ connection: AsyncConnection,
330+ machines_repository: MachinesRepository | None = None,
331+ ):
332+ super().__init__(connection)
333+ self.machines_repository = (
334+ machines_repository
335+ if machines_repository
336+ else MachinesRepository(connection)
337+ )
338+
339+ async def list(
340+ self, pagination_params: PaginationParams
341+ ) -> ListResult[Machine]:
342+ return await self.machines_repository.list(pagination_params)
343diff --git a/src/tests/fixtures/factories/machines.py b/src/tests/fixtures/factories/machines.py
344new file mode 100644
345index 0000000..235dba6
346--- /dev/null
347+++ b/src/tests/fixtures/factories/machines.py
348@@ -0,0 +1,29 @@
349+from typing import Any
350+
351+from maasapiserver.v3.api.models.responses.machines import PowerTypeEnum
352+from maasapiserver.v3.models.bmc import Bmc
353+from maasapiserver.v3.models.machines import Machine
354+from maasapiserver.v3.models.users import User
355+from tests.fixtures.factories.node import create_test_machine_entry
356+from tests.maasapiserver.fixtures.db import Fixture
357+
358+
359+async def create_test_machine(
360+ fixture: Fixture, bmc: Bmc, user: User, **extra_details: Any
361+) -> Machine:
362+ created_machine = await create_test_machine_entry(
363+ fixture,
364+ bmc_id=bmc.id,
365+ owner_id=user.id,
366+ osystem="ubuntu",
367+ distro_series="jammy",
368+ archtecture="amd64",
369+ hwe_kernel="hwe-22.04",
370+ **extra_details,
371+ )
372+ created_machine["owner"] = user.username
373+ created_machine["power_type"] = PowerTypeEnum.virsh.name
374+ created_machine["fqdn"] = f"{created_machine['hostname']}."
375+ return Machine(
376+ **created_machine,
377+ )
378diff --git a/src/tests/fixtures/factories/node.py b/src/tests/fixtures/factories/node.py
379index 6798f13..7cfeaff 100644
380--- a/src/tests/fixtures/factories/node.py
381+++ b/src/tests/fixtures/factories/node.py
382@@ -74,6 +74,8 @@ async def _create_test_node_entry(
383 "last_applied_storage_layout": "flat",
384 "enable_hw_sync": False,
385 "node_type": node_type,
386+ "architecture": "",
387+ "hwe_kernel": "",
388 }
389 node.update(extra_details)
390 [created_node] = await fixture.create(
391diff --git a/src/tests/maasapiserver/v3/api/test_machines.py b/src/tests/maasapiserver/v3/api/test_machines.py
392new file mode 100644
393index 0000000..d40da3d
394--- /dev/null
395+++ b/src/tests/maasapiserver/v3/api/test_machines.py
396@@ -0,0 +1,87 @@
397+from httpx import AsyncClient
398+import pytest
399+
400+from maasapiserver.common.api.models.responses.errors import ErrorBodyResponse
401+from maasapiserver.v3.api.models.responses.machines import MachinesListResponse
402+from maasapiserver.v3.constants import EXTERNAL_V3_API_PREFIX
403+from maasapiserver.v3.models.machines import Machine
404+from tests.fixtures.factories.bmc import create_test_bmc
405+from tests.fixtures.factories.machines import create_test_machine
406+from tests.fixtures.factories.user import create_test_user
407+from tests.maasapiserver.fixtures.db import Fixture
408+
409+
410+@pytest.mark.usefixtures("ensuremaasdb")
411+@pytest.mark.asyncio
412+class TestMachinesApi:
413+ def _assert_machine_in_list(
414+ self, machine: Machine, machines_response: MachinesListResponse
415+ ) -> None:
416+ machine_response = next(
417+ filter(
418+ lambda machine_response: machine.id == machine_response.id,
419+ machines_response.items,
420+ )
421+ )
422+ assert machine.id == machine_response.id
423+ assert (
424+ machine.to_response(f"{EXTERNAL_V3_API_PREFIX}/machines")
425+ == machine_response
426+ )
427+
428+ @pytest.mark.parametrize("machines_size", range(0, 10))
429+ async def test_list_parameters_200(
430+ self,
431+ api_client: AsyncClient,
432+ fixture: Fixture,
433+ machines_size: int,
434+ ) -> None:
435+ bmc = await create_test_bmc(fixture)
436+ user = await create_test_user(fixture)
437+ created_machines = [
438+ (
439+ await create_test_machine(
440+ fixture, description=str(i), bmc=bmc, user=user
441+ )
442+ )
443+ for i in range(0, machines_size)
444+ ]
445+
446+ response = await api_client.get("/api/v3/machines")
447+ assert response.status_code == 200
448+
449+ machines_response = MachinesListResponse(**response.json())
450+ assert machines_response.kind == "MachinesList"
451+ assert machines_response.total == machines_size
452+ assert len(machines_response.items) == machines_size
453+ for machine in created_machines:
454+ self._assert_machine_in_list(machine, machines_response)
455+
456+ for page in range(1, machines_size // 2):
457+ response = await api_client.get(
458+ f"/api/v3/machines?page={page}&size=2"
459+ )
460+ assert response.status_code == 200
461+ machines_response = MachinesListResponse(**response.json())
462+ assert machines_response.kind == "MachinesList"
463+ assert machines_response.total == machines_size
464+ assert (
465+ len(machines_response.items) == 2
466+ if page != machines_size // 2
467+ else (machines_size % 2 or 2)
468+ )
469+
470+ @pytest.mark.parametrize(
471+ "page,size", [(1, 0), (0, 1), (-1, -1), (1, 1001)]
472+ )
473+ async def test_list_422(
474+ self, page: int, size: int, api_client: AsyncClient
475+ ) -> None:
476+ response = await api_client.get(
477+ f"/api/v3/machines?page={page}&size={size}"
478+ )
479+ assert response.status_code == 422
480+
481+ error_response = ErrorBodyResponse(**response.json())
482+ assert error_response.kind == "Error"
483+ assert error_response.code == 422
484diff --git a/src/tests/maasapiserver/v3/db/test_machines.py b/src/tests/maasapiserver/v3/db/test_machines.py
485new file mode 100644
486index 0000000..b079c63
487--- /dev/null
488+++ b/src/tests/maasapiserver/v3/db/test_machines.py
489@@ -0,0 +1,51 @@
490+from math import ceil
491+
492+import pytest
493+from sqlalchemy.ext.asyncio import AsyncConnection
494+
495+from maasapiserver.v3.api.models.requests.query import PaginationParams
496+from maasapiserver.v3.db.machines import MachinesRepository
497+from tests.fixtures.factories.bmc import create_test_bmc
498+from tests.fixtures.factories.machines import create_test_machine
499+from tests.fixtures.factories.user import create_test_user
500+from tests.maasapiserver.fixtures.db import Fixture
501+
502+
503+@pytest.mark.usefixtures("ensuremaasdb")
504+@pytest.mark.asyncio
505+class TestMachinesRepository:
506+ @pytest.mark.parametrize("page_size", range(1, 12))
507+ async def test_list(
508+ self, page_size: int, db_connection: AsyncConnection, fixture: Fixture
509+ ) -> None:
510+ bmc = await create_test_bmc(fixture)
511+ user = await create_test_user(fixture)
512+
513+ machine_count = 10
514+ machines_repository = MachinesRepository(db_connection)
515+ created_machines = [
516+ (
517+ await create_test_machine(
518+ fixture, description=str(i), bmc=bmc, user=user
519+ )
520+ )
521+ for i in range(0, machine_count)
522+ ][::-1]
523+ total_pages = ceil(machine_count / page_size)
524+ for page in range(1, total_pages + 1):
525+ machines_result = await machines_repository.list(
526+ PaginationParams(size=page_size, page=page)
527+ )
528+ assert machines_result.total == machine_count
529+ assert total_pages == ceil(machines_result.total / page_size)
530+ if page == total_pages: # last page may have fewer elements
531+ assert len(machines_result.items) == (
532+ page_size
533+ - ((total_pages * page_size) % machines_result.total)
534+ )
535+ else:
536+ assert len(machines_result.items) == page_size
537+ for machine in created_machines[
538+ ((page - 1) * page_size) : ((page * page_size))
539+ ]:
540+ assert machine in machines_result.items

Subscribers

People subscribed via source and target branches