Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 6b9d788

Browse files
committed
Add enums and exporter status reporting
1 parent 615f9ea commit 6b9d788

9 files changed

Lines changed: 237 additions & 31 deletions

File tree

packages/jumpstarter-cli/jumpstarter_cli/get.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ def get():
2121
@opt_output_all
2222
@opt_comma_separated(
2323
"with",
24-
{"leases", "online"},
25-
help_text="Include fields: leases, online (comma-separated or repeated)"
24+
{"leases", "online", "status"},
25+
help_text="Include fields: leases, online, status (comma-separated or repeated)",
2626
)
2727
@handle_exceptions_with_reauthentication(relogin_client)
2828
def get_exporters(config, selector: str | None, output: OutputType, with_options: list[str]):
@@ -32,7 +32,10 @@ def get_exporters(config, selector: str | None, output: OutputType, with_options
3232

3333
include_leases = "leases" in with_options
3434
include_online = "online" in with_options
35-
exporters = config.list_exporters(filter=selector, include_leases=include_leases, include_online=include_online)
35+
include_status = "status" in with_options
36+
exporters = config.list_exporters(
37+
filter=selector, include_leases=include_leases, include_online=include_online, include_status=include_status
38+
)
3639

3740
model_print(exporters, output)
3841

packages/jumpstarter/jumpstarter/client/core.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from jumpstarter_protocol import jumpstarter_pb2, jumpstarter_pb2_grpc, router_pb2_grpc
1515
from rich.logging import RichHandler
1616

17-
from jumpstarter.common import Metadata
17+
from jumpstarter.common import ExporterStatus, Metadata
1818
from jumpstarter.common.exceptions import JumpstarterException
1919
from jumpstarter.common.resources import ResourceMetadata
2020
from jumpstarter.common.serde import decode_value, encode_value
@@ -48,6 +48,12 @@ class DriverInvalidArgument(DriverError, ValueError):
4848
"""
4949

5050

51+
class ExporterNotReady(DriverError):
52+
"""
53+
Raised when the exporter is not ready to accept driver calls
54+
"""
55+
56+
5157
@dataclass(kw_only=True)
5258
class AsyncDriverClient(
5359
Metadata,
@@ -76,9 +82,28 @@ def __post_init__(self):
7682
handler = RichHandler()
7783
self.logger.addHandler(handler)
7884

85+
async def check_exporter_status(self):
86+
"""Check if the exporter is ready to accept driver calls"""
87+
try:
88+
response = await self.stub.GetStatus(jumpstarter_pb2.GetStatusRequest())
89+
status = ExporterStatus.from_proto(response.status)
90+
91+
if status != ExporterStatus.LEASE_READY:
92+
raise ExporterNotReady(f"Exporter status is {status}: {response.status_message}")
93+
94+
except AioRpcError as e:
95+
# If GetStatus is not implemented, assume ready for backward compatibility
96+
if e.code() == StatusCode.UNIMPLEMENTED:
97+
self.logger.debug("GetStatus not implemented, assuming exporter is ready")
98+
return
99+
raise DriverError(f"Failed to check exporter status: {e.details()}") from e
100+
79101
async def call_async(self, method, *args):
80102
"""Make DriverCall by method name and arguments"""
81103

104+
# Check exporter status before making the call
105+
await self.check_exporter_status()
106+
82107
request = jumpstarter_pb2.DriverCallRequest(
83108
uuid=str(self.uuid),
84109
method=method,
@@ -105,6 +130,9 @@ async def call_async(self, method, *args):
105130
async def streamingcall_async(self, method, *args):
106131
"""Make StreamingDriverCall by method name and arguments"""
107132

133+
# Check exporter status before making the call
134+
await self.check_exporter_status()
135+
108136
request = jumpstarter_pb2.StreamingDriverCallRequest(
109137
uuid=str(self.uuid),
110138
method=method,

packages/jumpstarter/jumpstarter/client/grpc.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
from jumpstarter_protocol import client_pb2, client_pb2_grpc, jumpstarter_pb2_grpc, kubernetes_pb2, router_pb2_grpc
1414
from pydantic import BaseModel, ConfigDict, Field, field_serializer
1515

16+
from jumpstarter.common import ExporterStatus
1617
from jumpstarter.common.grpc import translate_grpc_exceptions
1718

1819

1920
@dataclass
2021
class WithOptions:
2122
show_online: bool = False
2223
show_leases: bool = False
24+
show_status: bool = False
2325

2426

2527
def add_display_columns(table, options: WithOptions = None):
@@ -28,6 +30,8 @@ def add_display_columns(table, options: WithOptions = None):
2830
table.add_column("NAME")
2931
if options.show_online:
3032
table.add_column("ONLINE")
33+
if options.show_status:
34+
table.add_column("STATUS")
3135
table.add_column("LABELS")
3236
if options.show_leases:
3337
table.add_column("LEASED BY")
@@ -42,6 +46,9 @@ def add_exporter_row(table, exporter, options: WithOptions = None, lease_info: t
4246
row_data.append(exporter.name)
4347
if options.show_online:
4448
row_data.append("yes" if exporter.online else "no")
49+
if options.show_status:
50+
status_str = str(exporter.status) if exporter.status else "UNKNOWN"
51+
row_data.append(status_str)
4552
row_data.append(",".join(("{}={}".format(k, v) for k, v in sorted(exporter.labels.items()))))
4653
if options.show_leases:
4754
if lease_info:
@@ -81,12 +88,16 @@ class Exporter(BaseModel):
8188
name: str
8289
labels: dict[str, str]
8390
online: bool = False
91+
status: ExporterStatus | None = None
8492
lease: Lease | None = None
8593

8694
@classmethod
8795
def from_protobuf(cls, data: client_pb2.Exporter) -> Exporter:
8896
namespace, name = parse_exporter_identifier(data.name)
89-
return cls(namespace=namespace, name=name, labels=data.labels, online=data.online)
97+
status = None
98+
if hasattr(data, "status") and data.status:
99+
status = ExporterStatus.from_proto(data.status)
100+
return cls(namespace=namespace, name=name, labels=data.labels, online=data.online, status=status)
90101

91102
@classmethod
92103
def rich_add_columns(cls, table, options: WithOptions = None):
@@ -244,6 +255,7 @@ class ExporterList(BaseModel):
244255
next_page_token: str | None = Field(exclude=True)
245256
include_online: bool = Field(default=False, exclude=True)
246257
include_leases: bool = Field(default=False, exclude=True)
258+
include_status: bool = Field(default=False, exclude=True)
247259

248260
@classmethod
249261
def from_protobuf(cls, data: client_pb2.ListExportersResponse) -> ExporterList:
@@ -253,11 +265,15 @@ def from_protobuf(cls, data: client_pb2.ListExportersResponse) -> ExporterList:
253265
)
254266

255267
def rich_add_columns(self, table):
256-
options = WithOptions(show_online=self.include_online, show_leases=self.include_leases)
268+
options = WithOptions(
269+
show_online=self.include_online, show_leases=self.include_leases, show_status=self.include_status
270+
)
257271
Exporter.rich_add_columns(table, options)
258272

259273
def rich_add_rows(self, table):
260-
options = WithOptions(show_online=self.include_online, show_leases=self.include_leases)
274+
options = WithOptions(
275+
show_online=self.include_online, show_leases=self.include_leases, show_status=self.include_status
276+
)
261277
for exporter in self.exporters:
262278
exporter.rich_add_rows(table, options)
263279

@@ -274,6 +290,8 @@ def model_dump_json(self, **kwargs):
274290
exclude_fields.add("lease")
275291
if not self.include_online:
276292
exclude_fields.add("online")
293+
if not self.include_status:
294+
exclude_fields.add("status")
277295

278296
data = {"exporters": [exporter.model_dump(mode="json", exclude=exclude_fields) for exporter in self.exporters]}
279297
return json.dumps(data, **json_kwargs)
@@ -284,6 +302,8 @@ def model_dump(self, **kwargs):
284302
exclude_fields.add("lease")
285303
if not self.include_online:
286304
exclude_fields.add("online")
305+
if not self.include_status:
306+
exclude_fields.add("status")
287307

288308
return {"exporters": [exporter.model_dump(mode="json", exclude=exclude_fields) for exporter in self.exporters]}
289309

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
from .enums import ExporterStatus, LogSource
12
from .metadata import Metadata
23
from .tempfile import TemporarySocket, TemporaryTcpListener, TemporaryUnixListener
34

4-
__all__ = ["Metadata", "TemporarySocket", "TemporaryUnixListener", "TemporaryTcpListener"]
5+
__all__ = [
6+
"ExporterStatus",
7+
"LogSource",
8+
"Metadata",
9+
"TemporarySocket",
10+
"TemporaryUnixListener",
11+
"TemporaryTcpListener",
12+
]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Human-readable enum wrappers for protobuf-generated constants."""
2+
3+
from enum import IntEnum
4+
5+
from jumpstarter_protocol.jumpstarter.v1 import common_pb2
6+
7+
8+
class ExporterStatus(IntEnum):
9+
"""Exporter status states."""
10+
11+
UNSPECIFIED = common_pb2.EXPORTER_STATUS_UNSPECIFIED
12+
"""Unknown/unspecified exporter status"""
13+
14+
OFFLINE = common_pb2.EXPORTER_STATUS_OFFLINE
15+
"""The exporter is currently offline"""
16+
17+
AVAILABLE = common_pb2.EXPORTER_STATUS_AVAILABLE
18+
"""Exporter is available to be leased"""
19+
20+
BEFORE_LEASE_HOOK = common_pb2.EXPORTER_STATUS_BEFORE_LEASE_HOOK
21+
"""Exporter is leased, but currently executing before lease hook"""
22+
23+
LEASE_READY = common_pb2.EXPORTER_STATUS_LEASE_READY
24+
"""Exporter is leased and ready to accept commands"""
25+
26+
AFTER_LEASE_HOOK = common_pb2.EXPORTER_STATUS_AFTER_LEASE_HOOK
27+
"""Lease was releaseed, but exporter is executing after lease hook"""
28+
29+
BEFORE_LEASE_HOOK_FAILED = common_pb2.EXPORTER_STATUS_BEFORE_LEASE_HOOK_FAILED
30+
"""The before lease hook failed and the exporter is no longer available"""
31+
32+
AFTER_LEASE_HOOK_FAILED = common_pb2.EXPORTER_STATUS_AFTER_LEASE_HOOK_FAILED
33+
"""The after lease hook failed and the exporter is no longer available"""
34+
35+
def __str__(self):
36+
return self.name
37+
38+
@classmethod
39+
def from_proto(cls, value: int) -> "ExporterStatus":
40+
"""Convert from protobuf integer to enum."""
41+
return cls(value)
42+
43+
def to_proto(self) -> int:
44+
"""Convert to protobuf integer."""
45+
return self.value
46+
47+
48+
class LogSource(IntEnum):
49+
"""Log source types."""
50+
51+
UNSPECIFIED = common_pb2.LOG_SOURCE_UNSPECIFIED
52+
"""Unspecified/unknown log source"""
53+
54+
DRIVER = common_pb2.LOG_SOURCE_DRIVER
55+
"""Logs produced by a Jumpstarter driver"""
56+
57+
BEFORE_LEASE_HOOK = common_pb2.LOG_SOURCE_BEFORE_LEASE_HOOK
58+
"""Logs produced by a before lease hook"""
59+
60+
AFTER_LEASE_HOOK = common_pb2.LOG_SOURCE_AFTER_LEASE_HOOK
61+
"""Logs produced by an after lease hook"""
62+
63+
SYSTEM = common_pb2.LOG_SOURCE_SYSTEM
64+
"""System/exporter logs"""
65+
66+
def __str__(self):
67+
return self.name
68+
69+
@classmethod
70+
def from_proto(cls, value: int) -> "LogSource":
71+
"""Convert from protobuf integer to enum."""
72+
return cls(value)
73+
74+
def to_proto(self) -> int:
75+
"""Convert to protobuf integer."""
76+
return self.value

packages/jumpstarter/jumpstarter/config/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,14 @@ async def list_exporters(
160160
filter: str | None = None,
161161
include_leases: bool = False,
162162
include_online: bool = False,
163+
include_status: bool = False,
163164
):
164165
svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace)
165166
exporters_response = await svc.ListExporters(page_size=page_size, page_token=page_token, filter=filter)
166167

167-
# Set the include_online flag for display purposes
168+
# Set the include flags for display purposes
168169
exporters_response.include_online = include_online
170+
exporters_response.include_status = include_status
169171

170172
if not include_leases:
171173
return exporters_response

0 commit comments

Comments
 (0)