ctfy.sdk.admin_resources.machines

client.admin.machines -- machine logins an organiser hands to teams (#2132).

The platform carries the login to each team's captain and does nothing else: it never connects to, initialises, resets or monitors a machine. Organisers only (global admins and the event's own competition admins). Nothing here returns a secret: the organiser holds the CSV already.

 1"""``client.admin.machines`` -- machine logins an organiser hands to teams (#2132).
 2
 3The platform carries the login to each team's captain and does nothing
 4else: it never connects to, initialises, resets or monitors a machine.
 5Organisers only (global admins and the event's own competition admins).
 6Nothing here returns a secret: the organiser holds the CSV already.
 7"""
 8
 9from __future__ import annotations
10
11from ctfy.sdk._helpers import _raise_for_status
12from ctfy.sdk.base import BaseHttpClient
13from ctfy.server.models import (
14    CompetitionMachineRow,
15    CompetitionMachines,
16    MachineAssignOutcome,
17    MachineAssignResult,
18    MachineImportResult,
19    MachineNotifyResult,
20)
21
22
23class AdminMachinesResource:
24    def __init__(self, http: BaseHttpClient) -> None:
25        self._http = http
26
27    def list(self, competition_id: str) -> CompetitionMachines:
28        """Every machine of the competition and who holds it (no secrets)."""
29        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/machines")
30        _raise_for_status(resp)
31        return CompetitionMachines.model_validate(resp.json())
32
33    def import_csv(
34        self, competition_id: str, csv_text: str, *, auto_assign: bool = False
35    ) -> MachineImportResult:
36        """Import ``host,port,username,password,private_key,note,team`` rows.
37
38        Rows that cannot be read are reported by line and code and the rest
39        still import; a machine already on the list is skipped. With
40        ``auto_assign`` the new machines are served to the teams without one.
41        """
42        resp = self._http.request(
43            "POST",
44            f"/admin/competitions/{competition_id}/machines/import",
45            json={"csv": csv_text, "auto_assign": auto_assign},
46        )
47        _raise_for_status(resp)
48        return MachineImportResult.model_validate(resp.json())
49
50    def auto_assign(self, competition_id: str) -> MachineAssignResult:
51        """Serve the unassigned machines to the teams that have none."""
52        resp = self._http.request(
53            "POST", f"/admin/competitions/{competition_id}/machines/auto-assign"
54        )
55        _raise_for_status(resp)
56        return MachineAssignResult.model_validate(resp.json())
57
58    def assign(
59        self, competition_id: str, machine_id: str, team_id: str, *, replace: bool = False
60    ) -> MachineAssignOutcome:
61        """Give an unassigned machine to a team.
62
63        A team that already holds one is refused (``409 team_has_machine``)
64        unless ``replace``: the old machine is then retired at once and the
65        captain is told.
66        """
67        resp = self._http.request(
68            "PUT",
69            f"/admin/competitions/{competition_id}/machines/{machine_id}/team",
70            json={"team_id": team_id, "replace": replace},
71        )
72        _raise_for_status(resp)
73        return MachineAssignOutcome.model_validate(resp.json())
74
75    def revoke(self, competition_id: str, machine_id: str) -> CompetitionMachineRow:
76        """Retire a machine; the team that held it loses it immediately."""
77        resp = self._http.request(
78            "DELETE", f"/admin/competitions/{competition_id}/machines/{machine_id}"
79        )
80        _raise_for_status(resp)
81        return CompetitionMachineRow.model_validate(resp.json())
82
83    def notify(self, competition_id: str, *, mail: bool = True) -> MachineNotifyResult:
84        """Tell every captain whose machine they have not been told about.
85
86        An inbox card with a link to the team page, and with ``mail`` an
87        email carrying the same link -- never the login.
88        """
89        resp = self._http.request(
90            "POST", f"/admin/competitions/{competition_id}/machines/notify", json={"mail": mail}
91        )
92        _raise_for_status(resp)
93        return MachineNotifyResult.model_validate(resp.json())
class AdminMachinesResource:
24class AdminMachinesResource:
25    def __init__(self, http: BaseHttpClient) -> None:
26        self._http = http
27
28    def list(self, competition_id: str) -> CompetitionMachines:
29        """Every machine of the competition and who holds it (no secrets)."""
30        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/machines")
31        _raise_for_status(resp)
32        return CompetitionMachines.model_validate(resp.json())
33
34    def import_csv(
35        self, competition_id: str, csv_text: str, *, auto_assign: bool = False
36    ) -> MachineImportResult:
37        """Import ``host,port,username,password,private_key,note,team`` rows.
38
39        Rows that cannot be read are reported by line and code and the rest
40        still import; a machine already on the list is skipped. With
41        ``auto_assign`` the new machines are served to the teams without one.
42        """
43        resp = self._http.request(
44            "POST",
45            f"/admin/competitions/{competition_id}/machines/import",
46            json={"csv": csv_text, "auto_assign": auto_assign},
47        )
48        _raise_for_status(resp)
49        return MachineImportResult.model_validate(resp.json())
50
51    def auto_assign(self, competition_id: str) -> MachineAssignResult:
52        """Serve the unassigned machines to the teams that have none."""
53        resp = self._http.request(
54            "POST", f"/admin/competitions/{competition_id}/machines/auto-assign"
55        )
56        _raise_for_status(resp)
57        return MachineAssignResult.model_validate(resp.json())
58
59    def assign(
60        self, competition_id: str, machine_id: str, team_id: str, *, replace: bool = False
61    ) -> MachineAssignOutcome:
62        """Give an unassigned machine to a team.
63
64        A team that already holds one is refused (``409 team_has_machine``)
65        unless ``replace``: the old machine is then retired at once and the
66        captain is told.
67        """
68        resp = self._http.request(
69            "PUT",
70            f"/admin/competitions/{competition_id}/machines/{machine_id}/team",
71            json={"team_id": team_id, "replace": replace},
72        )
73        _raise_for_status(resp)
74        return MachineAssignOutcome.model_validate(resp.json())
75
76    def revoke(self, competition_id: str, machine_id: str) -> CompetitionMachineRow:
77        """Retire a machine; the team that held it loses it immediately."""
78        resp = self._http.request(
79            "DELETE", f"/admin/competitions/{competition_id}/machines/{machine_id}"
80        )
81        _raise_for_status(resp)
82        return CompetitionMachineRow.model_validate(resp.json())
83
84    def notify(self, competition_id: str, *, mail: bool = True) -> MachineNotifyResult:
85        """Tell every captain whose machine they have not been told about.
86
87        An inbox card with a link to the team page, and with ``mail`` an
88        email carrying the same link -- never the login.
89        """
90        resp = self._http.request(
91            "POST", f"/admin/competitions/{competition_id}/machines/notify", json={"mail": mail}
92        )
93        _raise_for_status(resp)
94        return MachineNotifyResult.model_validate(resp.json())
AdminMachinesResource(http: ctfy.sdk.base.BaseHttpClient)
25    def __init__(self, http: BaseHttpClient) -> None:
26        self._http = http
def list( self, competition_id: str) -> ctfy.server.models.CompetitionMachines:
28    def list(self, competition_id: str) -> CompetitionMachines:
29        """Every machine of the competition and who holds it (no secrets)."""
30        resp = self._http.request("GET", f"/admin/competitions/{competition_id}/machines")
31        _raise_for_status(resp)
32        return CompetitionMachines.model_validate(resp.json())

Every machine of the competition and who holds it (no secrets).

def import_csv( self, competition_id: str, csv_text: str, *, auto_assign: bool = False) -> ctfy.server.models.MachineImportResult:
34    def import_csv(
35        self, competition_id: str, csv_text: str, *, auto_assign: bool = False
36    ) -> MachineImportResult:
37        """Import ``host,port,username,password,private_key,note,team`` rows.
38
39        Rows that cannot be read are reported by line and code and the rest
40        still import; a machine already on the list is skipped. With
41        ``auto_assign`` the new machines are served to the teams without one.
42        """
43        resp = self._http.request(
44            "POST",
45            f"/admin/competitions/{competition_id}/machines/import",
46            json={"csv": csv_text, "auto_assign": auto_assign},
47        )
48        _raise_for_status(resp)
49        return MachineImportResult.model_validate(resp.json())

Import host,port,username,password,private_key,note,team rows.

Rows that cannot be read are reported by line and code and the rest still import; a machine already on the list is skipped. With auto_assign the new machines are served to the teams without one.

def auto_assign( self, competition_id: str) -> ctfy.server.models.MachineAssignResult:
51    def auto_assign(self, competition_id: str) -> MachineAssignResult:
52        """Serve the unassigned machines to the teams that have none."""
53        resp = self._http.request(
54            "POST", f"/admin/competitions/{competition_id}/machines/auto-assign"
55        )
56        _raise_for_status(resp)
57        return MachineAssignResult.model_validate(resp.json())

Serve the unassigned machines to the teams that have none.

def assign( self, competition_id: str, machine_id: str, team_id: str, *, replace: bool = False) -> ctfy.server.models.MachineAssignOutcome:
59    def assign(
60        self, competition_id: str, machine_id: str, team_id: str, *, replace: bool = False
61    ) -> MachineAssignOutcome:
62        """Give an unassigned machine to a team.
63
64        A team that already holds one is refused (``409 team_has_machine``)
65        unless ``replace``: the old machine is then retired at once and the
66        captain is told.
67        """
68        resp = self._http.request(
69            "PUT",
70            f"/admin/competitions/{competition_id}/machines/{machine_id}/team",
71            json={"team_id": team_id, "replace": replace},
72        )
73        _raise_for_status(resp)
74        return MachineAssignOutcome.model_validate(resp.json())

Give an unassigned machine to a team.

A team that already holds one is refused (409 team_has_machine) unless replace: the old machine is then retired at once and the captain is told.

def revoke( self, competition_id: str, machine_id: str) -> ctfy.server.models.CompetitionMachineRow:
76    def revoke(self, competition_id: str, machine_id: str) -> CompetitionMachineRow:
77        """Retire a machine; the team that held it loses it immediately."""
78        resp = self._http.request(
79            "DELETE", f"/admin/competitions/{competition_id}/machines/{machine_id}"
80        )
81        _raise_for_status(resp)
82        return CompetitionMachineRow.model_validate(resp.json())

Retire a machine; the team that held it loses it immediately.

def notify( self, competition_id: str, *, mail: bool = True) -> ctfy.server.models.MachineNotifyResult:
84    def notify(self, competition_id: str, *, mail: bool = True) -> MachineNotifyResult:
85        """Tell every captain whose machine they have not been told about.
86
87        An inbox card with a link to the team page, and with ``mail`` an
88        email carrying the same link -- never the login.
89        """
90        resp = self._http.request(
91            "POST", f"/admin/competitions/{competition_id}/machines/notify", json={"mail": mail}
92        )
93        _raise_for_status(resp)
94        return MachineNotifyResult.model_validate(resp.json())

Tell every captain whose machine they have not been told about.

An inbox card with a link to the team page, and with mail an email carrying the same link -- never the login.