diff --git a/Makefile b/Makefile index 49528a783..5fb033fe2 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ help: @echo "" @echo "Build targets:" @echo " build - Build all packages" - @echo " generate - Generate code from protobuf definitions" + @echo " protobuf-gen - Generate code from protobuf definitions" @echo " sync - Sync all packages and extras" @echo "" @echo "Documentation targets:" @@ -75,8 +75,18 @@ pkg-ty-all: $(addprefix pkg-ty-,$(PKG_TARGETS)) build: uv build --all --out-dir dist -generate: - buf generate +protobuf-gen: + podman run --volume "$(shell pwd):/workspace" --workdir /workspace docker.io/bufbuild/buf:latest generate + # Fix Python imports: convert absolute imports to relative imports + # In jumpstarter/client/v1: from jumpstarter.v1 import -> from ...v1 import + find packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1 -name "*_pb2*.py" -type f -exec sed -i.bak \ + -e 's|^from jumpstarter\.v1 import|from ...v1 import|g' \ + -e 's|^from jumpstarter\.client\.v1 import|from . import|g' \ + {} \; -exec rm {}.bak \; + # In jumpstarter/v1: from jumpstarter.v1 import -> from . import + find packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1 -name "*_pb2*.py" -type f -exec sed -i.bak \ + -e 's|^from jumpstarter\.v1 import|from . import|g' \ + {} \; -exec rm {}.bak \; sync: uv sync --all-packages --all-extras diff --git a/buf.gen.yaml b/buf.gen.yaml index a9d577519..fce4d534c 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -8,4 +8,5 @@ plugins: out: ./packages/jumpstarter-protocol/jumpstarter_protocol inputs: - git_repo: https://github.com/jumpstarter-dev/jumpstarter-protocol.git + branch: main subdir: proto diff --git a/packages/jumpstarter-cli/jumpstarter_cli/common.py b/packages/jumpstarter-cli/jumpstarter_cli/common.py index 3f17f648e..cd0968d3c 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/common.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/common.py @@ -1,8 +1,8 @@ -from datetime import timedelta +from datetime import datetime, timedelta from functools import partial import click -from pydantic import TypeAdapter +from pydantic import TypeAdapter, ValidationError opt_selector = click.option( "-l", @@ -21,7 +21,7 @@ def convert(self, value, param, ctx): try: return TypeAdapter(timedelta).validate_python(value) - except ValueError: + except (ValueError, ValidationError): self.fail(f"{value!r} is not a valid duration", param, ctx) @@ -44,3 +44,37 @@ def convert(self, value, param, ctx): See https://docs.rs/speedate/latest/speedate/ for details """, ) + + +class DateTimeParamType(click.ParamType): + name = "datetime" + + def convert(self, value, param, ctx): + if isinstance(value, datetime): + dt = value + else: + try: + dt = TypeAdapter(datetime).validate_python(value) + except (ValueError, ValidationError): + self.fail(f"{value!r} is not a valid datetime", param, ctx) + + # Normalize naive datetimes to local timezone + if dt.tzinfo is None: + dt = dt.astimezone() + + return dt + + +DATETIME = DateTimeParamType() + +opt_begin_time = click.option( + "--begin-time", + "begin_time", + type=DATETIME, + default=None, + help=""" +Begin time for the lease in ISO 8601 format (e.g., 2024-01-01T12:00:00 or 2024-01-01T12:00:00Z). +If not specified, the lease tries to be acquired immediately. The lease duration always starts +at the actual time of acquisition. +""", +) diff --git a/packages/jumpstarter-cli/jumpstarter_cli/common_test.py b/packages/jumpstarter-cli/jumpstarter_cli/common_test.py new file mode 100644 index 000000000..0e94b5d55 --- /dev/null +++ b/packages/jumpstarter-cli/jumpstarter_cli/common_test.py @@ -0,0 +1,90 @@ +from datetime import datetime, timedelta, timezone + +import click +import pytest + +from jumpstarter_cli.common import DATETIME, DURATION, DateTimeParamType, DurationParamType + + +class TestDateTimeParamType: + """Test DateTimeParamType parameter parsing and normalization.""" + + def test_parse_iso8601_with_timezone(self): + """Test parsing ISO 8601 datetime with timezone.""" + dt = DATETIME.convert("2024-01-01T12:00:00Z", None, None) + assert dt.year == 2024 + assert dt.month == 1 + assert dt.day == 1 + assert dt.hour == 12 + assert dt.minute == 0 + assert dt.second == 0 + assert dt.tzinfo is not None + assert dt.tzinfo == timezone.utc + + def test_parse_iso8601_naive_gets_normalized(self): + """Test that naive datetime gets normalized to local timezone.""" + dt = DATETIME.convert("2024-01-01T12:00:00", None, None) + assert dt.year == 2024 + assert dt.month == 1 + assert dt.day == 1 + assert dt.hour == 12 + assert dt.minute == 0 + assert dt.second == 0 + # Should have been normalized to local timezone + assert dt.tzinfo is not None + + def test_pass_through_datetime_object_with_timezone(self): + """Test that datetime object with timezone passes through.""" + input_dt = datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + dt = DATETIME.convert(input_dt, None, None) + assert dt == input_dt + assert dt.tzinfo == timezone.utc + + def test_pass_through_datetime_object_naive_gets_normalized(self): + """Test that naive datetime object gets normalized.""" + input_dt = datetime(2024, 1, 1, 12, 0, 0) # Naive + dt = DATETIME.convert(input_dt, None, None) + assert dt.year == 2024 + assert dt.month == 1 + assert dt.day == 1 + assert dt.hour == 12 + # Should have been normalized to local timezone + assert dt.tzinfo is not None + + def test_invalid_datetime_raises_click_exception(self): + """Test that invalid datetime string raises click exception.""" + param_type = DateTimeParamType() + with pytest.raises(click.BadParameter, match="is not a valid datetime"): + param_type.convert("not-a-datetime", None, None) + + +class TestDurationParamType: + """Test DurationParamType parameter parsing.""" + + def test_parse_iso8601_duration(self): + """Test parsing ISO 8601 duration.""" + td = DURATION.convert("PT1H30M", None, None) + assert td == timedelta(hours=1, minutes=30) + + def test_parse_time_format(self): + """Test parsing HH:MM:SS format.""" + td = DURATION.convert("01:30:00", None, None) + assert td == timedelta(hours=1, minutes=30) + + def test_parse_days_and_time(self): + """Test parsing 'D days, HH:MM:SS' format.""" + td = DURATION.convert("2 days, 01:30:00", None, None) + assert td == timedelta(days=2, hours=1, minutes=30) + + def test_pass_through_timedelta_object(self): + """Test that timedelta object passes through.""" + input_td = timedelta(hours=1, minutes=30) + td = DURATION.convert(input_td, None, None) + assert td == input_td + + def test_invalid_duration_raises_click_exception(self): + """Test that invalid duration string raises click exception.""" + param_type = DurationParamType() + with pytest.raises(click.BadParameter, match="is not a valid duration"): + param_type.convert("not-a-duration", None, None) + diff --git a/packages/jumpstarter-cli/jumpstarter_cli/create.py b/packages/jumpstarter-cli/jumpstarter_cli/create.py index 0c5a64038..0dff98b53 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/create.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/create.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta import click from jumpstarter_cli_common.config import opt_config @@ -6,7 +6,7 @@ from jumpstarter_cli_common.opt import OutputType, opt_output_all from jumpstarter_cli_common.print import model_print -from .common import opt_duration_partial, opt_selector +from .common import opt_begin_time, opt_duration_partial, opt_selector from .login import relogin_client @@ -21,9 +21,10 @@ def create(): @opt_config(exporter=False) @opt_selector @opt_duration_partial(required=True) +@opt_begin_time @opt_output_all @handle_exceptions_with_reauthentication(relogin_client) -def create_lease(config, selector: str, duration: timedelta, output: OutputType): +def create_lease(config, selector: str, duration: timedelta, begin_time: datetime | None, output: OutputType): """ Create a lease @@ -49,6 +50,6 @@ def create_lease(config, selector: str, duration: timedelta, output: OutputType) """ - lease = config.create_lease(selector=selector, duration=duration) + lease = config.create_lease(selector=selector, duration=duration, begin_time=begin_time) model_print(lease, output) diff --git a/packages/jumpstarter-cli/jumpstarter_cli/get.py b/packages/jumpstarter-cli/jumpstarter_cli/get.py index d62e6dee1..f7d1a041b 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/get.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/get.py @@ -41,12 +41,19 @@ def get_exporters(config, selector: str | None, output: OutputType, with_options @opt_config(exporter=False) @opt_selector @opt_output_all +@click.option( + "--all", + "show_all", + is_flag=True, + default=False, + help="Show all leases including expired ones" +) @handle_exceptions_with_reauthentication(relogin_client) -def get_leases(config, selector: str | None, output: OutputType): +def get_leases(config, selector: str | None, output: OutputType, show_all: bool): """ Display one or many leases """ - leases = config.list_leases(filter=selector) + leases = config.list_leases(filter=selector, only_active=not show_all) model_print(leases, output) diff --git a/packages/jumpstarter-cli/jumpstarter_cli/get_test.py b/packages/jumpstarter-cli/jumpstarter_cli/get_test.py index d06a8b837..1713a8866 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/get_test.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/get_test.py @@ -1,10 +1,11 @@ +from datetime import datetime, timedelta from unittest.mock import Mock import click import pytest from jumpstarter_cli_common.opt import parse_comma_separated -from jumpstarter.client.grpc import Exporter, ExporterList, Lease +from jumpstarter.client.grpc import Exporter, ExporterList, Lease, LeaseList from jumpstarter.config.client import ClientConfigV1Alpha1 @@ -239,3 +240,109 @@ def test_exporter_to_exporter_list_flow(self): assert exporter_list.exporters[1].name == "server-001" assert exporter_list.include_online is True assert exporter_list.include_leases is False + + +class TestGetLeasesLogic: + """Tests for get leases command logic (simulating server-side filtering)""" + + def create_test_lease(self, namespace="default", name="lease-1", status="In-Use", + effective_begin_time=None, effective_end_time=None, + duration=timedelta(hours=1)): + """Create a mock lease for testing""" + lease = Mock(spec=Lease) + lease.namespace = namespace + lease.name = name + lease.client = "test-client" + lease.exporter = "test-exporter" + lease.get_status.return_value = status + lease.effective_begin_time = effective_begin_time + lease.effective_end_time = effective_end_time + lease.duration = duration + lease.effective_duration = timedelta(minutes=30) if effective_begin_time else None + lease.begin_time = None + return lease + + def test_only_active_excludes_expired_leases(self): + """Test that server returns only active leases when only_active=True""" + # When only_active=True, server returns only active lease + active_lease = self.create_test_lease( + name="active-lease", + status="In-Use", + effective_begin_time=datetime(2023, 1, 1, 10, 0, 0) + ) + + leases_from_server = LeaseList(leases=[active_lease], next_page_token=None) + + assert len(leases_from_server.leases) == 1 + assert leases_from_server.leases[0].name == "active-lease" + assert leases_from_server.leases[0].get_status() == "In-Use" + + def test_show_all_includes_expired_leases(self): + """Test that server returns all leases including expired when only_active=False""" + # When only_active=False, server returns both active and expired + active_lease = self.create_test_lease( + name="active-lease", + status="In-Use", + effective_begin_time=datetime(2023, 1, 1, 10, 0, 0) + ) + expired_lease = self.create_test_lease( + name="expired-lease", + status="Expired", + effective_begin_time=datetime(2023, 1, 1, 8, 0, 0), + effective_end_time=datetime(2023, 1, 1, 9, 0, 0) + ) + + leases_from_server = LeaseList(leases=[active_lease, expired_lease], next_page_token=None) + + assert len(leases_from_server.leases) == 2 + assert leases_from_server.leases[0].name == "active-lease" + assert leases_from_server.leases[1].name == "expired-lease" + + def test_multiple_active_leases_returned(self): + """Test that server returns all active leases when only_active=True""" + # Server returns multiple active leases (different statuses but all non-expired) + lease1 = self.create_test_lease( + name="lease-1", + status="In-Use", + effective_begin_time=datetime(2023, 1, 1, 10, 0, 0) + ) + lease2 = self.create_test_lease( + name="lease-2", + status="Waiting", + effective_begin_time=datetime(2023, 1, 1, 11, 0, 0) + ) + lease3 = self.create_test_lease( + name="lease-3", + status="In-Use", + effective_begin_time=datetime(2023, 1, 1, 12, 0, 0) + ) + + leases_from_server = LeaseList(leases=[lease1, lease2, lease3], next_page_token=None) + + assert len(leases_from_server.leases) == 3 + assert all(lease.get_status() != "Expired" for lease in leases_from_server.leases) + + def test_all_expired_when_show_all(self): + """Test that server can return only expired leases when only_active=False""" + # When only_active=False and all leases happen to be expired + expired1 = self.create_test_lease( + name="expired-1", + status="Expired", + effective_end_time=datetime(2023, 1, 1, 8, 0, 0) + ) + expired2 = self.create_test_lease( + name="expired-2", + status="Expired", + effective_end_time=datetime(2023, 1, 1, 9, 0, 0) + ) + + leases_from_server = LeaseList(leases=[expired1, expired2], next_page_token=None) + + assert len(leases_from_server.leases) == 2 + assert all(lease.get_status() == "Expired" for lease in leases_from_server.leases) + + def test_empty_lease_list(self): + """Test that server can return empty lease list""" + leases_from_server = LeaseList(leases=[], next_page_token=None) + + assert len(leases_from_server.leases) == 0 diff --git a/packages/jumpstarter-cli/jumpstarter_cli/update.py b/packages/jumpstarter-cli/jumpstarter_cli/update.py index d753a91ae..7b933f792 100644 --- a/packages/jumpstarter-cli/jumpstarter_cli/update.py +++ b/packages/jumpstarter-cli/jumpstarter_cli/update.py @@ -1,4 +1,4 @@ -from datetime import timedelta +from datetime import datetime, timedelta import click from jumpstarter_cli_common.config import opt_config @@ -6,7 +6,7 @@ from jumpstarter_cli_common.opt import OutputType, opt_output_all from jumpstarter_cli_common.print import model_print -from .common import opt_duration_partial +from .common import opt_begin_time, opt_duration_partial from .login import relogin_client @@ -20,14 +20,22 @@ def update(): @update.command(name="lease") @opt_config(exporter=False) @click.argument("name") -@opt_duration_partial(required=True) +@opt_duration_partial(required=False) +@opt_begin_time @opt_output_all @handle_exceptions_with_reauthentication(relogin_client) -def update_lease(config, name: str, duration: timedelta, output: OutputType): +def update_lease(config, name: str, duration: timedelta | None, begin_time: datetime | None, output: OutputType): """ Update a lease + + Update the duration and/or begin time of an existing lease. + At least one of --duration or --begin-time must be specified. + Updating the begin time of an already active lease is not allowed. """ - lease = config.update_lease(name, duration) + if duration is None and begin_time is None: + raise click.UsageError("At least one of --duration or --begin-time must be specified") + + lease = config.update_lease(name, duration=duration, begin_time=begin_time) model_print(lease, output) diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py index be56f7f16..8cc530733 100644 --- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py +++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/client/v1/client_pb2.py @@ -34,13 +34,14 @@ from ...v1 import common_pb2 as jumpstarter_dot_v1_dot_common__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"jumpstarter/client/v1/client.proto\x12\x15jumpstarter.client.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\x1a\x1bjumpstarter/v1/common.proto\"\xe0\x02\n\x08\x45xporter\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x08R\x04name\x12\x43\n\x06labels\x18\x02 \x03(\x0b\x32+.jumpstarter.client.v1.Exporter.LabelsEntryR\x06labels\x12\x1d\n\x06online\x18\x03 \x01(\x08\x42\x05\x18\x01\xe0\x41\x03R\x06online\x12;\n\x06status\x18\x04 \x01(\x0e\x32\x1e.jumpstarter.v1.ExporterStatusB\x03\xe0\x41\x03R\x06status\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01:_\xea\x41\\\n\x18jumpstarter.dev/Exporter\x12+namespaces/{namespace}/exporters/{exporter}*\texporters2\x08\x65xporter\"\xed\x06\n\x05Lease\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x08R\x04name\x12\"\n\x08selector\x18\x02 \x01(\tB\x06\xe0\x41\x02\xe0\x41\x05R\x08selector\x12:\n\x08\x64uration\x18\x03 \x01(\x0b\x32\x19.google.protobuf.DurationB\x03\xe0\x41\x02R\x08\x64uration\x12M\n\x12\x65\x66\x66\x65\x63tive_duration\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationB\x03\xe0\x41\x03R\x11\x65\x66\x66\x65\x63tiveDuration\x12>\n\nbegin_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\tbeginTime\x88\x01\x01\x12V\n\x14\x65\x66\x66\x65\x63tive_begin_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03H\x01R\x12\x65\x66\x66\x65\x63tiveBeginTime\x88\x01\x01\x12:\n\x08\x65nd_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x02R\x07\x65ndTime\x88\x01\x01\x12R\n\x12\x65\x66\x66\x65\x63tive_end_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03H\x03R\x10\x65\x66\x66\x65\x63tiveEndTime\x88\x01\x01\x12;\n\x06\x63lient\x18\t \x01(\tB\x1e\xe0\x41\x03\xfa\x41\x18\n\x16jumpstarter.dev/ClientH\x04R\x06\x63lient\x88\x01\x01\x12\x41\n\x08\x65xporter\x18\n \x01(\tB \xe0\x41\x03\xfa\x41\x1a\n\x18jumpstarter.dev/ExporterH\x05R\x08\x65xporter\x88\x01\x01\x12>\n\nconditions\x18\x0b \x03(\x0b\x32\x19.jumpstarter.v1.ConditionB\x03\xe0\x41\x03R\nconditions:P\xea\x41M\n\x15jumpstarter.dev/Lease\x12%namespaces/{namespace}/leases/{lease}*\x06leases2\x05leaseB\r\n\x0b_begin_timeB\x17\n\x15_effective_begin_timeB\x0b\n\t_end_timeB\x15\n\x13_effective_end_timeB\t\n\x07_clientB\x0b\n\t_exporter\"J\n\x12GetExporterRequest\x12\x34\n\x04name\x18\x01 \x01(\tB \xe0\x41\x02\xfa\x41\x1a\n\x18jumpstarter.dev/ExporterR\x04name\"\xb3\x01\n\x14ListExportersRequest\x12\x38\n\x06parent\x18\x01 \x01(\tB \xe0\x41\x02\xfa\x41\x1a\x12\x18jumpstarter.dev/ExporterR\x06parent\x12 \n\tpage_size\x18\x02 \x01(\x05\x42\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\"~\n\x15ListExportersResponse\x12=\n\texporters\x18\x01 \x03(\x0b\x32\x1f.jumpstarter.client.v1.ExporterR\texporters\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"D\n\x0fGetLeaseRequest\x12\x31\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15jumpstarter.dev/LeaseR\x04name\"\xad\x01\n\x11ListLeasesRequest\x12\x35\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\x12\x15jumpstarter.dev/LeaseR\x06parent\x12 \n\tpage_size\x18\x02 \x01(\x05\x42\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\"r\n\x12ListLeasesResponse\x12\x34\n\x06leases\x18\x01 \x03(\x0b\x32\x1c.jumpstarter.client.v1.LeaseR\x06leases\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xa4\x01\n\x12\x43reateLeaseRequest\x12\x35\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\x12\x15jumpstarter.dev/LeaseR\x06parent\x12\x1e\n\x08lease_id\x18\x02 \x01(\tB\x03\xe0\x41\x01R\x07leaseId\x12\x37\n\x05lease\x18\x03 \x01(\x0b\x32\x1c.jumpstarter.client.v1.LeaseB\x03\xe0\x41\x02R\x05lease\"\x8f\x01\n\x12UpdateLeaseRequest\x12\x37\n\x05lease\x18\x01 \x01(\x0b\x32\x1c.jumpstarter.client.v1.LeaseB\x03\xe0\x41\x02R\x05lease\x12@\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\x03\xe0\x41\x01R\nupdateMask\"G\n\x12\x44\x65leteLeaseRequest\x12\x31\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15jumpstarter.dev/LeaseR\x04name2\xa7\x08\n\rClientService\x12\x8d\x01\n\x0bGetExporter\x12).jumpstarter.client.v1.GetExporterRequest\x1a\x1f.jumpstarter.client.v1.Exporter\"2\xda\x41\x04name\x82\xd3\xe4\x93\x02%\x12#/v1/{name=namespaces/*/exporters/*}\x12\xa0\x01\n\rListExporters\x12+.jumpstarter.client.v1.ListExportersRequest\x1a,.jumpstarter.client.v1.ListExportersResponse\"4\xda\x41\x06parent\x82\xd3\xe4\x93\x02%\x12#/v1/{parent=namespaces/*}/exporters\x12\x81\x01\n\x08GetLease\x12&.jumpstarter.client.v1.GetLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"/\xda\x41\x04name\x82\xd3\xe4\x93\x02\"\x12 /v1/{name=namespaces/*/leases/*}\x12\x94\x01\n\nListLeases\x12(.jumpstarter.client.v1.ListLeasesRequest\x1a).jumpstarter.client.v1.ListLeasesResponse\"1\xda\x41\x06parent\x82\xd3\xe4\x93\x02\"\x12 /v1/{parent=namespaces/*}/leases\x12\x9f\x01\n\x0b\x43reateLease\x12).jumpstarter.client.v1.CreateLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"G\xda\x41\x15parent,lease,lease_id\x82\xd3\xe4\x93\x02)\" /v1/{parent=namespaces/*}/leases:\x05lease\x12\xa1\x01\n\x0bUpdateLease\x12).jumpstarter.client.v1.UpdateLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"I\xda\x41\x11lease,update_mask\x82\xd3\xe4\x93\x02/2&/v1/{lease.name=namespaces/*/leases/*}:\x05lease\x12\x81\x01\n\x0b\x44\x65leteLease\x12).jumpstarter.client.v1.DeleteLeaseRequest\x1a\x16.google.protobuf.Empty\"/\xda\x41\x04name\x82\xd3\xe4\x93\x02\"* /v1/{name=namespaces/*/leases/*}b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"jumpstarter/client/v1/client.proto\x12\x15jumpstarter.client.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\x1a\x1bjumpstarter/v1/common.proto\"\xe0\x02\n\x08\x45xporter\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x08R\x04name\x12\x43\n\x06labels\x18\x02 \x03(\x0b\x32+.jumpstarter.client.v1.Exporter.LabelsEntryR\x06labels\x12\x1d\n\x06online\x18\x03 \x01(\x08\x42\x05\x18\x01\xe0\x41\x03R\x06online\x12;\n\x06status\x18\x04 \x01(\x0e\x32\x1e.jumpstarter.v1.ExporterStatusB\x03\xe0\x41\x03R\x06status\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01:_\xea\x41\\\n\x18jumpstarter.dev/Exporter\x12+namespaces/{namespace}/exporters/{exporter}*\texporters2\x08\x65xporter\"\xfa\x06\n\x05Lease\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x08R\x04name\x12\"\n\x08selector\x18\x02 \x01(\tB\x06\xe0\x41\x02\xe0\x41\x05R\x08selector\x12:\n\x08\x64uration\x18\x03 \x01(\x0b\x32\x19.google.protobuf.DurationH\x00R\x08\x64uration\x88\x01\x01\x12M\n\x12\x65\x66\x66\x65\x63tive_duration\x18\x04 \x01(\x0b\x32\x19.google.protobuf.DurationB\x03\xe0\x41\x03R\x11\x65\x66\x66\x65\x63tiveDuration\x12>\n\nbegin_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01R\tbeginTime\x88\x01\x01\x12V\n\x14\x65\x66\x66\x65\x63tive_begin_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03H\x02R\x12\x65\x66\x66\x65\x63tiveBeginTime\x88\x01\x01\x12:\n\x08\x65nd_time\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x03R\x07\x65ndTime\x88\x01\x01\x12R\n\x12\x65\x66\x66\x65\x63tive_end_time\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03H\x04R\x10\x65\x66\x66\x65\x63tiveEndTime\x88\x01\x01\x12;\n\x06\x63lient\x18\t \x01(\tB\x1e\xe0\x41\x03\xfa\x41\x18\n\x16jumpstarter.dev/ClientH\x05R\x06\x63lient\x88\x01\x01\x12\x41\n\x08\x65xporter\x18\n \x01(\tB \xe0\x41\x03\xfa\x41\x1a\n\x18jumpstarter.dev/ExporterH\x06R\x08\x65xporter\x88\x01\x01\x12>\n\nconditions\x18\x0b \x03(\x0b\x32\x19.jumpstarter.v1.ConditionB\x03\xe0\x41\x03R\nconditions:P\xea\x41M\n\x15jumpstarter.dev/Lease\x12%namespaces/{namespace}/leases/{lease}*\x06leases2\x05leaseB\x0b\n\t_durationB\r\n\x0b_begin_timeB\x17\n\x15_effective_begin_timeB\x0b\n\t_end_timeB\x15\n\x13_effective_end_timeB\t\n\x07_clientB\x0b\n\t_exporter\"J\n\x12GetExporterRequest\x12\x34\n\x04name\x18\x01 \x01(\tB \xe0\x41\x02\xfa\x41\x1a\n\x18jumpstarter.dev/ExporterR\x04name\"\xb3\x01\n\x14ListExportersRequest\x12\x38\n\x06parent\x18\x01 \x01(\tB \xe0\x41\x02\xfa\x41\x1a\x12\x18jumpstarter.dev/ExporterR\x06parent\x12 \n\tpage_size\x18\x02 \x01(\x05\x42\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\"~\n\x15ListExportersResponse\x12=\n\texporters\x18\x01 \x03(\x0b\x32\x1f.jumpstarter.client.v1.ExporterR\texporters\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"D\n\x0fGetLeaseRequest\x12\x31\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15jumpstarter.dev/LeaseR\x04name\"\xe8\x01\n\x11ListLeasesRequest\x12\x35\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\x12\x15jumpstarter.dev/LeaseR\x06parent\x12 \n\tpage_size\x18\x02 \x01(\x05\x42\x03\xe0\x41\x01R\x08pageSize\x12\"\n\npage_token\x18\x03 \x01(\tB\x03\xe0\x41\x01R\tpageToken\x12\x1b\n\x06\x66ilter\x18\x04 \x01(\tB\x03\xe0\x41\x01R\x06\x66ilter\x12)\n\x0bonly_active\x18\x05 \x01(\x08\x42\x03\xe0\x41\x01H\x00R\nonlyActive\x88\x01\x01\x42\x0e\n\x0c_only_active\"r\n\x12ListLeasesResponse\x12\x34\n\x06leases\x18\x01 \x03(\x0b\x32\x1c.jumpstarter.client.v1.LeaseR\x06leases\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"\xa4\x01\n\x12\x43reateLeaseRequest\x12\x35\n\x06parent\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\x12\x15jumpstarter.dev/LeaseR\x06parent\x12\x1e\n\x08lease_id\x18\x02 \x01(\tB\x03\xe0\x41\x01R\x07leaseId\x12\x37\n\x05lease\x18\x03 \x01(\x0b\x32\x1c.jumpstarter.client.v1.LeaseB\x03\xe0\x41\x02R\x05lease\"\x8f\x01\n\x12UpdateLeaseRequest\x12\x37\n\x05lease\x18\x01 \x01(\x0b\x32\x1c.jumpstarter.client.v1.LeaseB\x03\xe0\x41\x02R\x05lease\x12@\n\x0bupdate_mask\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.FieldMaskB\x03\xe0\x41\x01R\nupdateMask\"G\n\x12\x44\x65leteLeaseRequest\x12\x31\n\x04name\x18\x01 \x01(\tB\x1d\xe0\x41\x02\xfa\x41\x17\n\x15jumpstarter.dev/LeaseR\x04name2\xa7\x08\n\rClientService\x12\x8d\x01\n\x0bGetExporter\x12).jumpstarter.client.v1.GetExporterRequest\x1a\x1f.jumpstarter.client.v1.Exporter\"2\xda\x41\x04name\x82\xd3\xe4\x93\x02%\x12#/v1/{name=namespaces/*/exporters/*}\x12\xa0\x01\n\rListExporters\x12+.jumpstarter.client.v1.ListExportersRequest\x1a,.jumpstarter.client.v1.ListExportersResponse\"4\xda\x41\x06parent\x82\xd3\xe4\x93\x02%\x12#/v1/{parent=namespaces/*}/exporters\x12\x81\x01\n\x08GetLease\x12&.jumpstarter.client.v1.GetLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"/\xda\x41\x04name\x82\xd3\xe4\x93\x02\"\x12 /v1/{name=namespaces/*/leases/*}\x12\x94\x01\n\nListLeases\x12(.jumpstarter.client.v1.ListLeasesRequest\x1a).jumpstarter.client.v1.ListLeasesResponse\"1\xda\x41\x06parent\x82\xd3\xe4\x93\x02\"\x12 /v1/{parent=namespaces/*}/leases\x12\x9f\x01\n\x0b\x43reateLease\x12).jumpstarter.client.v1.CreateLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"G\xda\x41\x15parent,lease,lease_id\x82\xd3\xe4\x93\x02)\" /v1/{parent=namespaces/*}/leases:\x05lease\x12\xa1\x01\n\x0bUpdateLease\x12).jumpstarter.client.v1.UpdateLeaseRequest\x1a\x1c.jumpstarter.client.v1.Lease\"I\xda\x41\x11lease,update_mask\x82\xd3\xe4\x93\x02/2&/v1/{lease.name=namespaces/*/leases/*}:\x05lease\x12\x81\x01\n\x0b\x44\x65leteLease\x12).jumpstarter.client.v1.DeleteLeaseRequest\x1a\x16.google.protobuf.Empty\"/\xda\x41\x04name\x82\xd3\xe4\x93\x02\"* /v1/{name=namespaces/*/leases/*}B\x9e\x01\n\x19\x63om.jumpstarter.client.v1B\x0b\x43lientProtoP\x01\xa2\x02\x03JCX\xaa\x02\x15Jumpstarter.Client.V1\xca\x02\x15Jumpstarter\\Client\\V1\xe2\x02!Jumpstarter\\Client\\V1\\GPBMetadata\xea\x02\x17Jumpstarter::Client::V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.client.v1.client_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\031com.jumpstarter.client.v1B\013ClientProtoP\001\242\002\003JCX\252\002\025Jumpstarter.Client.V1\312\002\025Jumpstarter\\Client\\V1\342\002!Jumpstarter\\Client\\V1\\GPBMetadata\352\002\027Jumpstarter::Client::V1' _globals['_EXPORTER_LABELSENTRY']._loaded_options = None _globals['_EXPORTER_LABELSENTRY']._serialized_options = b'8\001' _globals['_EXPORTER'].fields_by_name['name']._loaded_options = None @@ -55,8 +56,6 @@ _globals['_LEASE'].fields_by_name['name']._serialized_options = b'\340A\010' _globals['_LEASE'].fields_by_name['selector']._loaded_options = None _globals['_LEASE'].fields_by_name['selector']._serialized_options = b'\340A\002\340A\005' - _globals['_LEASE'].fields_by_name['duration']._loaded_options = None - _globals['_LEASE'].fields_by_name['duration']._serialized_options = b'\340A\002' _globals['_LEASE'].fields_by_name['effective_duration']._loaded_options = None _globals['_LEASE'].fields_by_name['effective_duration']._serialized_options = b'\340A\003' _globals['_LEASE'].fields_by_name['effective_begin_time']._loaded_options = None @@ -91,6 +90,8 @@ _globals['_LISTLEASESREQUEST'].fields_by_name['page_token']._serialized_options = b'\340A\001' _globals['_LISTLEASESREQUEST'].fields_by_name['filter']._loaded_options = None _globals['_LISTLEASESREQUEST'].fields_by_name['filter']._serialized_options = b'\340A\001' + _globals['_LISTLEASESREQUEST'].fields_by_name['only_active']._loaded_options = None + _globals['_LISTLEASESREQUEST'].fields_by_name['only_active']._serialized_options = b'\340A\001' _globals['_CREATELEASEREQUEST'].fields_by_name['parent']._loaded_options = None _globals['_CREATELEASEREQUEST'].fields_by_name['parent']._serialized_options = b'\340A\002\372A\027\022\025jumpstarter.dev/Lease' _globals['_CREATELEASEREQUEST'].fields_by_name['lease_id']._loaded_options = None @@ -122,25 +123,25 @@ _globals['_EXPORTER_LABELSENTRY']._serialized_start=565 _globals['_EXPORTER_LABELSENTRY']._serialized_end=622 _globals['_LEASE']._serialized_start=722 - _globals['_LEASE']._serialized_end=1599 - _globals['_GETEXPORTERREQUEST']._serialized_start=1601 - _globals['_GETEXPORTERREQUEST']._serialized_end=1675 - _globals['_LISTEXPORTERSREQUEST']._serialized_start=1678 - _globals['_LISTEXPORTERSREQUEST']._serialized_end=1857 - _globals['_LISTEXPORTERSRESPONSE']._serialized_start=1859 - _globals['_LISTEXPORTERSRESPONSE']._serialized_end=1985 - _globals['_GETLEASEREQUEST']._serialized_start=1987 - _globals['_GETLEASEREQUEST']._serialized_end=2055 - _globals['_LISTLEASESREQUEST']._serialized_start=2058 - _globals['_LISTLEASESREQUEST']._serialized_end=2231 - _globals['_LISTLEASESRESPONSE']._serialized_start=2233 - _globals['_LISTLEASESRESPONSE']._serialized_end=2347 - _globals['_CREATELEASEREQUEST']._serialized_start=2350 - _globals['_CREATELEASEREQUEST']._serialized_end=2514 - _globals['_UPDATELEASEREQUEST']._serialized_start=2517 - _globals['_UPDATELEASEREQUEST']._serialized_end=2660 - _globals['_DELETELEASEREQUEST']._serialized_start=2662 - _globals['_DELETELEASEREQUEST']._serialized_end=2733 - _globals['_CLIENTSERVICE']._serialized_start=2736 - _globals['_CLIENTSERVICE']._serialized_end=3799 + _globals['_LEASE']._serialized_end=1612 + _globals['_GETEXPORTERREQUEST']._serialized_start=1614 + _globals['_GETEXPORTERREQUEST']._serialized_end=1688 + _globals['_LISTEXPORTERSREQUEST']._serialized_start=1691 + _globals['_LISTEXPORTERSREQUEST']._serialized_end=1870 + _globals['_LISTEXPORTERSRESPONSE']._serialized_start=1872 + _globals['_LISTEXPORTERSRESPONSE']._serialized_end=1998 + _globals['_GETLEASEREQUEST']._serialized_start=2000 + _globals['_GETLEASEREQUEST']._serialized_end=2068 + _globals['_LISTLEASESREQUEST']._serialized_start=2071 + _globals['_LISTLEASESREQUEST']._serialized_end=2303 + _globals['_LISTLEASESRESPONSE']._serialized_start=2305 + _globals['_LISTLEASESRESPONSE']._serialized_end=2419 + _globals['_CREATELEASEREQUEST']._serialized_start=2422 + _globals['_CREATELEASEREQUEST']._serialized_end=2586 + _globals['_UPDATELEASEREQUEST']._serialized_start=2589 + _globals['_UPDATELEASEREQUEST']._serialized_end=2732 + _globals['_DELETELEASEREQUEST']._serialized_start=2734 + _globals['_DELETELEASEREQUEST']._serialized_end=2805 + _globals['_CLIENTSERVICE']._serialized_start=2808 + _globals['_CLIENTSERVICE']._serialized_end=3871 # @@protoc_insertion_point(module_scope) diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2.py index 6349b917c..4da3a3146 100644 --- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2.py +++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/common_pb2.py @@ -24,13 +24,14 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bjumpstarter/v1/common.proto\x12\x0ejumpstarter.v1*\xb6\x02\n\x0e\x45xporterStatus\x12\x1f\n\x1b\x45XPORTER_STATUS_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x45XPORTER_STATUS_OFFLINE\x10\x01\x12\x1d\n\x19\x45XPORTER_STATUS_AVAILABLE\x10\x02\x12%\n!EXPORTER_STATUS_BEFORE_LEASE_HOOK\x10\x03\x12\x1f\n\x1b\x45XPORTER_STATUS_LEASE_READY\x10\x04\x12$\n EXPORTER_STATUS_AFTER_LEASE_HOOK\x10\x05\x12,\n(EXPORTER_STATUS_BEFORE_LEASE_HOOK_FAILED\x10\x06\x12+\n\'EXPORTER_STATUS_AFTER_LEASE_HOOK_FAILED\x10\x07*\x98\x01\n\tLogSource\x12\x1a\n\x16LOG_SOURCE_UNSPECIFIED\x10\x00\x12\x15\n\x11LOG_SOURCE_DRIVER\x10\x01\x12 \n\x1cLOG_SOURCE_BEFORE_LEASE_HOOK\x10\x02\x12\x1f\n\x1bLOG_SOURCE_AFTER_LEASE_HOOK\x10\x03\x12\x15\n\x11LOG_SOURCE_SYSTEM\x10\x04\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bjumpstarter/v1/common.proto\x12\x0ejumpstarter.v1*\xb6\x02\n\x0e\x45xporterStatus\x12\x1f\n\x1b\x45XPORTER_STATUS_UNSPECIFIED\x10\x00\x12\x1b\n\x17\x45XPORTER_STATUS_OFFLINE\x10\x01\x12\x1d\n\x19\x45XPORTER_STATUS_AVAILABLE\x10\x02\x12%\n!EXPORTER_STATUS_BEFORE_LEASE_HOOK\x10\x03\x12\x1f\n\x1b\x45XPORTER_STATUS_LEASE_READY\x10\x04\x12$\n EXPORTER_STATUS_AFTER_LEASE_HOOK\x10\x05\x12,\n(EXPORTER_STATUS_BEFORE_LEASE_HOOK_FAILED\x10\x06\x12+\n\'EXPORTER_STATUS_AFTER_LEASE_HOOK_FAILED\x10\x07*\x98\x01\n\tLogSource\x12\x1a\n\x16LOG_SOURCE_UNSPECIFIED\x10\x00\x12\x15\n\x11LOG_SOURCE_DRIVER\x10\x01\x12 \n\x1cLOG_SOURCE_BEFORE_LEASE_HOOK\x10\x02\x12\x1f\n\x1bLOG_SOURCE_AFTER_LEASE_HOOK\x10\x03\x12\x15\n\x11LOG_SOURCE_SYSTEM\x10\x04\x42z\n\x12\x63om.jumpstarter.v1B\x0b\x43ommonProtoP\x01\xa2\x02\x03JXX\xaa\x02\x0eJumpstarter.V1\xca\x02\x0eJumpstarter\\V1\xe2\x02\x1aJumpstarter\\V1\\GPBMetadata\xea\x02\x0fJumpstarter::V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.v1.common_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\022com.jumpstarter.v1B\013CommonProtoP\001\242\002\003JXX\252\002\016Jumpstarter.V1\312\002\016Jumpstarter\\V1\342\002\032Jumpstarter\\V1\\GPBMetadata\352\002\017Jumpstarter::V1' _globals['_EXPORTERSTATUS']._serialized_start=48 _globals['_EXPORTERSTATUS']._serialized_end=358 _globals['_LOGSOURCE']._serialized_start=361 diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.py index 10892f748..b735bcad6 100644 --- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.py +++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/jumpstarter_pb2.py @@ -30,13 +30,14 @@ from . import common_pb2 as jumpstarter_dot_v1_dot_common__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n jumpstarter/v1/jumpstarter.proto\x12\x0ejumpstarter.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\x1a\x1bjumpstarter/v1/common.proto\"\xd1\x01\n\x0fRegisterRequest\x12\x43\n\x06labels\x18\x01 \x03(\x0b\x32+.jumpstarter.v1.RegisterRequest.LabelsEntryR\x06labels\x12>\n\x07reports\x18\x02 \x03(\x0b\x32$.jumpstarter.v1.DriverInstanceReportR\x07reports\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xd2\x03\n\x14\x44riverInstanceReport\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12$\n\x0bparent_uuid\x18\x02 \x01(\tH\x00R\nparentUuid\x88\x01\x01\x12H\n\x06labels\x18\x03 \x03(\x0b\x32\x30.jumpstarter.v1.DriverInstanceReport.LabelsEntryR\x06labels\x12%\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01R\x0b\x64\x65scription\x88\x01\x01\x12m\n\x13methods_description\x18\x05 \x03(\x0b\x32<.jumpstarter.v1.DriverInstanceReport.MethodsDescriptionEntryR\x12methodsDescription\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x1a\x45\n\x17MethodsDescriptionEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x0e\n\x0c_parent_uuidB\x0e\n\x0c_description\"&\n\x10RegisterResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\"+\n\x11UnregisterRequest\x12\x16\n\x06reason\x18\x02 \x01(\tR\x06reason\"\x14\n\x12UnregisterResponse\".\n\rListenRequest\x12\x1d\n\nlease_name\x18\x01 \x01(\tR\tleaseName\"\\\n\x0eListenResponse\x12\'\n\x0frouter_endpoint\x18\x01 \x01(\tR\x0erouterEndpoint\x12!\n\x0crouter_token\x18\x02 \x01(\tR\x0brouterToken\"\x0f\n\rStatusRequest\"\x91\x01\n\x0eStatusResponse\x12\x16\n\x06leased\x18\x01 \x01(\x08R\x06leased\x12\"\n\nlease_name\x18\x02 \x01(\tH\x00R\tleaseName\x88\x01\x01\x12$\n\x0b\x63lient_name\x18\x03 \x01(\tH\x01R\nclientName\x88\x01\x01\x42\r\n\x0b_lease_nameB\x0e\n\x0c_client_name\",\n\x0b\x44ialRequest\x12\x1d\n\nlease_name\x18\x01 \x01(\tR\tleaseName\"Z\n\x0c\x44ialResponse\x12\'\n\x0frouter_endpoint\x18\x01 \x01(\tR\x0erouterEndpoint\x12!\n\x0crouter_token\x18\x02 \x01(\tR\x0brouterToken\"\xa1\x01\n\x12\x41uditStreamRequest\x12#\n\rexporter_uuid\x18\x01 \x01(\tR\x0c\x65xporterUuid\x12\x30\n\x14\x64river_instance_uuid\x18\x02 \x01(\tR\x12\x64riverInstanceUuid\x12\x1a\n\x08severity\x18\x03 \x01(\tR\x08severity\x12\x18\n\x07message\x18\x04 \x01(\tR\x07message\"x\n\x13ReportStatusRequest\x12\x36\n\x06status\x18\x01 \x01(\x0e\x32\x1e.jumpstarter.v1.ExporterStatusR\x06status\x12\x1d\n\x07message\x18\x02 \x01(\tH\x00R\x07message\x88\x01\x01\x42\n\n\x08_message\"\x16\n\x14ReportStatusResponse\"\xb8\x02\n\x11GetReportResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x45\n\x06labels\x18\x02 \x03(\x0b\x32-.jumpstarter.v1.GetReportResponse.LabelsEntryR\x06labels\x12>\n\x07reports\x18\x03 \x03(\x0b\x32$.jumpstarter.v1.DriverInstanceReportR\x07reports\x12M\n\x15\x61lternative_endpoints\x18\x04 \x03(\x0b\x32\x18.jumpstarter.v1.EndpointR\x14\x61lternativeEndpoints\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xa5\x01\n\x08\x45ndpoint\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12 \n\x0b\x63\x65rtificate\x18\x02 \x01(\tR\x0b\x63\x65rtificate\x12-\n\x12\x63lient_certificate\x18\x03 \x01(\tR\x11\x63lientCertificate\x12,\n\x12\x63lient_private_key\x18\x04 \x01(\tR\x10\x63lientPrivateKey\"k\n\x11\x44riverCallRequest\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x16\n\x06method\x18\x02 \x01(\tR\x06method\x12*\n\x04\x61rgs\x18\x03 \x03(\x0b\x32\x16.google.protobuf.ValueR\x04\x61rgs\"X\n\x12\x44riverCallResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12.\n\x06result\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x06result\"t\n\x1aStreamingDriverCallRequest\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x16\n\x06method\x18\x02 \x01(\tR\x06method\x12*\n\x04\x61rgs\x18\x03 \x03(\x0b\x32\x16.google.protobuf.ValueR\x04\x61rgs\"a\n\x1bStreamingDriverCallResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12.\n\x06result\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x06result\"\xa0\x01\n\x11LogStreamResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x1a\n\x08severity\x18\x02 \x01(\tR\x08severity\x12\x18\n\x07message\x18\x03 \x01(\tR\x07message\x12\x36\n\x06source\x18\x04 \x01(\x0e\x32\x19.jumpstarter.v1.LogSourceH\x00R\x06source\x88\x01\x01\x42\t\n\x07_source\"\x0e\n\x0cResetRequest\"\x0f\n\rResetResponse\"%\n\x0fGetLeaseRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\x93\x03\n\x10GetLeaseResponse\x12\x35\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.DurationR\x08\x64uration\x12\x39\n\x08selector\x18\x02 \x01(\x0b\x32\x1d.jumpstarter.v1.LabelSelectorR\x08selector\x12>\n\nbegin_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\tbeginTime\x88\x01\x01\x12:\n\x08\x65nd_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01R\x07\x65ndTime\x88\x01\x01\x12(\n\rexporter_uuid\x18\x05 \x01(\tH\x02R\x0c\x65xporterUuid\x88\x01\x01\x12\x39\n\nconditions\x18\x06 \x03(\x0b\x32\x19.jumpstarter.v1.ConditionR\nconditionsB\r\n\x0b_begin_timeB\x0b\n\t_end_timeB\x10\n\x0e_exporter_uuid\"\x87\x01\n\x13RequestLeaseRequest\x12\x35\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.DurationR\x08\x64uration\x12\x39\n\x08selector\x18\x02 \x01(\x0b\x32\x1d.jumpstarter.v1.LabelSelectorR\x08selector\"*\n\x14RequestLeaseResponse\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\")\n\x13ReleaseLeaseRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\x16\n\x14ReleaseLeaseResponse\"\x13\n\x11ListLeasesRequest\"*\n\x12ListLeasesResponse\x12\x14\n\x05names\x18\x01 \x03(\tR\x05names\"\x12\n\x10GetStatusRequest\"v\n\x11GetStatusResponse\x12\x36\n\x06status\x18\x01 \x01(\x0e\x32\x1e.jumpstarter.v1.ExporterStatusR\x06status\x12\x1d\n\x07message\x18\x02 \x01(\tH\x00R\x07message\x88\x01\x01\x42\n\n\x08_message2\x92\x07\n\x11\x43ontrollerService\x12M\n\x08Register\x12\x1f.jumpstarter.v1.RegisterRequest\x1a .jumpstarter.v1.RegisterResponse\x12S\n\nUnregister\x12!.jumpstarter.v1.UnregisterRequest\x1a\".jumpstarter.v1.UnregisterResponse\x12Y\n\x0cReportStatus\x12#.jumpstarter.v1.ReportStatusRequest\x1a$.jumpstarter.v1.ReportStatusResponse\x12I\n\x06Listen\x12\x1d.jumpstarter.v1.ListenRequest\x1a\x1e.jumpstarter.v1.ListenResponse0\x01\x12I\n\x06Status\x12\x1d.jumpstarter.v1.StatusRequest\x1a\x1e.jumpstarter.v1.StatusResponse0\x01\x12\x41\n\x04\x44ial\x12\x1b.jumpstarter.v1.DialRequest\x1a\x1c.jumpstarter.v1.DialResponse\x12K\n\x0b\x41uditStream\x12\".jumpstarter.v1.AuditStreamRequest\x1a\x16.google.protobuf.Empty(\x01\x12M\n\x08GetLease\x12\x1f.jumpstarter.v1.GetLeaseRequest\x1a .jumpstarter.v1.GetLeaseResponse\x12Y\n\x0cRequestLease\x12#.jumpstarter.v1.RequestLeaseRequest\x1a$.jumpstarter.v1.RequestLeaseResponse\x12Y\n\x0cReleaseLease\x12#.jumpstarter.v1.ReleaseLeaseRequest\x1a$.jumpstarter.v1.ReleaseLeaseResponse\x12S\n\nListLeases\x12!.jumpstarter.v1.ListLeasesRequest\x1a\".jumpstarter.v1.ListLeasesResponse2\x82\x04\n\x0f\x45xporterService\x12\x46\n\tGetReport\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.GetReportResponse\x12S\n\nDriverCall\x12!.jumpstarter.v1.DriverCallRequest\x1a\".jumpstarter.v1.DriverCallResponse\x12p\n\x13StreamingDriverCall\x12*.jumpstarter.v1.StreamingDriverCallRequest\x1a+.jumpstarter.v1.StreamingDriverCallResponse0\x01\x12H\n\tLogStream\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.LogStreamResponse0\x01\x12\x44\n\x05Reset\x12\x1c.jumpstarter.v1.ResetRequest\x1a\x1d.jumpstarter.v1.ResetResponse\x12P\n\tGetStatus\x12 .jumpstarter.v1.GetStatusRequest\x1a!.jumpstarter.v1.GetStatusResponseb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n jumpstarter/v1/jumpstarter.proto\x12\x0ejumpstarter.v1\x1a\x1egoogle/protobuf/duration.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1fjumpstarter/v1/kubernetes.proto\x1a\x1bjumpstarter/v1/common.proto\"\xd1\x01\n\x0fRegisterRequest\x12\x43\n\x06labels\x18\x01 \x03(\x0b\x32+.jumpstarter.v1.RegisterRequest.LabelsEntryR\x06labels\x12>\n\x07reports\x18\x02 \x03(\x0b\x32$.jumpstarter.v1.DriverInstanceReportR\x07reports\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xd2\x03\n\x14\x44riverInstanceReport\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12$\n\x0bparent_uuid\x18\x02 \x01(\tH\x00R\nparentUuid\x88\x01\x01\x12H\n\x06labels\x18\x03 \x03(\x0b\x32\x30.jumpstarter.v1.DriverInstanceReport.LabelsEntryR\x06labels\x12%\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01R\x0b\x64\x65scription\x88\x01\x01\x12m\n\x13methods_description\x18\x05 \x03(\x0b\x32<.jumpstarter.v1.DriverInstanceReport.MethodsDescriptionEntryR\x12methodsDescription\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x1a\x45\n\x17MethodsDescriptionEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\x42\x0e\n\x0c_parent_uuidB\x0e\n\x0c_description\"&\n\x10RegisterResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\"+\n\x11UnregisterRequest\x12\x16\n\x06reason\x18\x02 \x01(\tR\x06reason\"\x14\n\x12UnregisterResponse\".\n\rListenRequest\x12\x1d\n\nlease_name\x18\x01 \x01(\tR\tleaseName\"\\\n\x0eListenResponse\x12\'\n\x0frouter_endpoint\x18\x01 \x01(\tR\x0erouterEndpoint\x12!\n\x0crouter_token\x18\x02 \x01(\tR\x0brouterToken\"\x0f\n\rStatusRequest\"\x91\x01\n\x0eStatusResponse\x12\x16\n\x06leased\x18\x01 \x01(\x08R\x06leased\x12\"\n\nlease_name\x18\x02 \x01(\tH\x00R\tleaseName\x88\x01\x01\x12$\n\x0b\x63lient_name\x18\x03 \x01(\tH\x01R\nclientName\x88\x01\x01\x42\r\n\x0b_lease_nameB\x0e\n\x0c_client_name\",\n\x0b\x44ialRequest\x12\x1d\n\nlease_name\x18\x01 \x01(\tR\tleaseName\"Z\n\x0c\x44ialResponse\x12\'\n\x0frouter_endpoint\x18\x01 \x01(\tR\x0erouterEndpoint\x12!\n\x0crouter_token\x18\x02 \x01(\tR\x0brouterToken\"\xa1\x01\n\x12\x41uditStreamRequest\x12#\n\rexporter_uuid\x18\x01 \x01(\tR\x0c\x65xporterUuid\x12\x30\n\x14\x64river_instance_uuid\x18\x02 \x01(\tR\x12\x64riverInstanceUuid\x12\x1a\n\x08severity\x18\x03 \x01(\tR\x08severity\x12\x18\n\x07message\x18\x04 \x01(\tR\x07message\"x\n\x13ReportStatusRequest\x12\x36\n\x06status\x18\x01 \x01(\x0e\x32\x1e.jumpstarter.v1.ExporterStatusR\x06status\x12\x1d\n\x07message\x18\x02 \x01(\tH\x00R\x07message\x88\x01\x01\x42\n\n\x08_message\"\x16\n\x14ReportStatusResponse\"\xb8\x02\n\x11GetReportResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x45\n\x06labels\x18\x02 \x03(\x0b\x32-.jumpstarter.v1.GetReportResponse.LabelsEntryR\x06labels\x12>\n\x07reports\x18\x03 \x03(\x0b\x32$.jumpstarter.v1.DriverInstanceReportR\x07reports\x12M\n\x15\x61lternative_endpoints\x18\x04 \x03(\x0b\x32\x18.jumpstarter.v1.EndpointR\x14\x61lternativeEndpoints\x1a\x39\n\x0bLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xa5\x01\n\x08\x45ndpoint\x12\x1a\n\x08\x65ndpoint\x18\x01 \x01(\tR\x08\x65ndpoint\x12 \n\x0b\x63\x65rtificate\x18\x02 \x01(\tR\x0b\x63\x65rtificate\x12-\n\x12\x63lient_certificate\x18\x03 \x01(\tR\x11\x63lientCertificate\x12,\n\x12\x63lient_private_key\x18\x04 \x01(\tR\x10\x63lientPrivateKey\"k\n\x11\x44riverCallRequest\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x16\n\x06method\x18\x02 \x01(\tR\x06method\x12*\n\x04\x61rgs\x18\x03 \x03(\x0b\x32\x16.google.protobuf.ValueR\x04\x61rgs\"X\n\x12\x44riverCallResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12.\n\x06result\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x06result\"t\n\x1aStreamingDriverCallRequest\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x16\n\x06method\x18\x02 \x01(\tR\x06method\x12*\n\x04\x61rgs\x18\x03 \x03(\x0b\x32\x16.google.protobuf.ValueR\x04\x61rgs\"a\n\x1bStreamingDriverCallResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12.\n\x06result\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x06result\"\xa0\x01\n\x11LogStreamResponse\x12\x12\n\x04uuid\x18\x01 \x01(\tR\x04uuid\x12\x1a\n\x08severity\x18\x02 \x01(\tR\x08severity\x12\x18\n\x07message\x18\x03 \x01(\tR\x07message\x12\x36\n\x06source\x18\x04 \x01(\x0e\x32\x19.jumpstarter.v1.LogSourceH\x00R\x06source\x88\x01\x01\x42\t\n\x07_source\"\x0e\n\x0cResetRequest\"\x0f\n\rResetResponse\"%\n\x0fGetLeaseRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\x93\x03\n\x10GetLeaseResponse\x12\x35\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.DurationR\x08\x64uration\x12\x39\n\x08selector\x18\x02 \x01(\x0b\x32\x1d.jumpstarter.v1.LabelSelectorR\x08selector\x12>\n\nbegin_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00R\tbeginTime\x88\x01\x01\x12:\n\x08\x65nd_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01R\x07\x65ndTime\x88\x01\x01\x12(\n\rexporter_uuid\x18\x05 \x01(\tH\x02R\x0c\x65xporterUuid\x88\x01\x01\x12\x39\n\nconditions\x18\x06 \x03(\x0b\x32\x19.jumpstarter.v1.ConditionR\nconditionsB\r\n\x0b_begin_timeB\x0b\n\t_end_timeB\x10\n\x0e_exporter_uuid\"\x87\x01\n\x13RequestLeaseRequest\x12\x35\n\x08\x64uration\x18\x01 \x01(\x0b\x32\x19.google.protobuf.DurationR\x08\x64uration\x12\x39\n\x08selector\x18\x02 \x01(\x0b\x32\x1d.jumpstarter.v1.LabelSelectorR\x08selector\"*\n\x14RequestLeaseResponse\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\")\n\x13ReleaseLeaseRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\x16\n\x14ReleaseLeaseResponse\"\x13\n\x11ListLeasesRequest\"*\n\x12ListLeasesResponse\x12\x14\n\x05names\x18\x01 \x03(\tR\x05names\"\x12\n\x10GetStatusRequest\"v\n\x11GetStatusResponse\x12\x36\n\x06status\x18\x01 \x01(\x0e\x32\x1e.jumpstarter.v1.ExporterStatusR\x06status\x12\x1d\n\x07message\x18\x02 \x01(\tH\x00R\x07message\x88\x01\x01\x42\n\n\x08_message2\x92\x07\n\x11\x43ontrollerService\x12M\n\x08Register\x12\x1f.jumpstarter.v1.RegisterRequest\x1a .jumpstarter.v1.RegisterResponse\x12S\n\nUnregister\x12!.jumpstarter.v1.UnregisterRequest\x1a\".jumpstarter.v1.UnregisterResponse\x12Y\n\x0cReportStatus\x12#.jumpstarter.v1.ReportStatusRequest\x1a$.jumpstarter.v1.ReportStatusResponse\x12I\n\x06Listen\x12\x1d.jumpstarter.v1.ListenRequest\x1a\x1e.jumpstarter.v1.ListenResponse0\x01\x12I\n\x06Status\x12\x1d.jumpstarter.v1.StatusRequest\x1a\x1e.jumpstarter.v1.StatusResponse0\x01\x12\x41\n\x04\x44ial\x12\x1b.jumpstarter.v1.DialRequest\x1a\x1c.jumpstarter.v1.DialResponse\x12K\n\x0b\x41uditStream\x12\".jumpstarter.v1.AuditStreamRequest\x1a\x16.google.protobuf.Empty(\x01\x12M\n\x08GetLease\x12\x1f.jumpstarter.v1.GetLeaseRequest\x1a .jumpstarter.v1.GetLeaseResponse\x12Y\n\x0cRequestLease\x12#.jumpstarter.v1.RequestLeaseRequest\x1a$.jumpstarter.v1.RequestLeaseResponse\x12Y\n\x0cReleaseLease\x12#.jumpstarter.v1.ReleaseLeaseRequest\x1a$.jumpstarter.v1.ReleaseLeaseResponse\x12S\n\nListLeases\x12!.jumpstarter.v1.ListLeasesRequest\x1a\".jumpstarter.v1.ListLeasesResponse2\x82\x04\n\x0f\x45xporterService\x12\x46\n\tGetReport\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.GetReportResponse\x12S\n\nDriverCall\x12!.jumpstarter.v1.DriverCallRequest\x1a\".jumpstarter.v1.DriverCallResponse\x12p\n\x13StreamingDriverCall\x12*.jumpstarter.v1.StreamingDriverCallRequest\x1a+.jumpstarter.v1.StreamingDriverCallResponse0\x01\x12H\n\tLogStream\x12\x16.google.protobuf.Empty\x1a!.jumpstarter.v1.LogStreamResponse0\x01\x12\x44\n\x05Reset\x12\x1c.jumpstarter.v1.ResetRequest\x1a\x1d.jumpstarter.v1.ResetResponse\x12P\n\tGetStatus\x12 .jumpstarter.v1.GetStatusRequest\x1a!.jumpstarter.v1.GetStatusResponseB\x7f\n\x12\x63om.jumpstarter.v1B\x10JumpstarterProtoP\x01\xa2\x02\x03JXX\xaa\x02\x0eJumpstarter.V1\xca\x02\x0eJumpstarter\\V1\xe2\x02\x1aJumpstarter\\V1\\GPBMetadata\xea\x02\x0fJumpstarter::V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.v1.jumpstarter_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\022com.jumpstarter.v1B\020JumpstarterProtoP\001\242\002\003JXX\252\002\016Jumpstarter.V1\312\002\016Jumpstarter\\V1\342\002\032Jumpstarter\\V1\\GPBMetadata\352\002\017Jumpstarter::V1' _globals['_REGISTERREQUEST_LABELSENTRY']._loaded_options = None _globals['_REGISTERREQUEST_LABELSENTRY']._serialized_options = b'8\001' _globals['_DRIVERINSTANCEREPORT_LABELSENTRY']._loaded_options = None diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/kubernetes_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/kubernetes_pb2.py index fc7fcc649..d7e6db9b4 100644 --- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/kubernetes_pb2.py +++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/kubernetes_pb2.py @@ -24,13 +24,14 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fjumpstarter/v1/kubernetes.proto\x12\x0ejumpstarter.v1\"`\n\x18LabelSelectorRequirement\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x1a\n\x08operator\x18\x02 \x01(\tR\x08operator\x12\x16\n\x06values\x18\x03 \x03(\tR\x06values\"\xf9\x01\n\rLabelSelector\x12U\n\x11match_expressions\x18\x01 \x03(\x0b\x32(.jumpstarter.v1.LabelSelectorRequirementR\x10matchExpressions\x12Q\n\x0cmatch_labels\x18\x02 \x03(\x0b\x32..jumpstarter.v1.LabelSelector.MatchLabelsEntryR\x0bmatchLabels\x1a>\n\x10MatchLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"V\n\x04Time\x12\x1d\n\x07seconds\x18\x01 \x01(\x03H\x00R\x07seconds\x88\x01\x01\x12\x19\n\x05nanos\x18\x02 \x01(\x05H\x01R\x05nanos\x88\x01\x01\x42\n\n\x08_secondsB\x08\n\x06_nanos\"\xd6\x02\n\tCondition\x12\x17\n\x04type\x18\x01 \x01(\tH\x00R\x04type\x88\x01\x01\x12\x1b\n\x06status\x18\x02 \x01(\tH\x01R\x06status\x88\x01\x01\x12\x33\n\x12observedGeneration\x18\x03 \x01(\x03H\x02R\x12observedGeneration\x88\x01\x01\x12I\n\x12lastTransitionTime\x18\x04 \x01(\x0b\x32\x14.jumpstarter.v1.TimeH\x03R\x12lastTransitionTime\x88\x01\x01\x12\x1b\n\x06reason\x18\x05 \x01(\tH\x04R\x06reason\x88\x01\x01\x12\x1d\n\x07message\x18\x06 \x01(\tH\x05R\x07message\x88\x01\x01\x42\x07\n\x05_typeB\t\n\x07_statusB\x15\n\x13_observedGenerationB\x15\n\x13_lastTransitionTimeB\t\n\x07_reasonB\n\n\x08_messageb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fjumpstarter/v1/kubernetes.proto\x12\x0ejumpstarter.v1\"`\n\x18LabelSelectorRequirement\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x1a\n\x08operator\x18\x02 \x01(\tR\x08operator\x12\x16\n\x06values\x18\x03 \x03(\tR\x06values\"\xf9\x01\n\rLabelSelector\x12U\n\x11match_expressions\x18\x01 \x03(\x0b\x32(.jumpstarter.v1.LabelSelectorRequirementR\x10matchExpressions\x12Q\n\x0cmatch_labels\x18\x02 \x03(\x0b\x32..jumpstarter.v1.LabelSelector.MatchLabelsEntryR\x0bmatchLabels\x1a>\n\x10MatchLabelsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"V\n\x04Time\x12\x1d\n\x07seconds\x18\x01 \x01(\x03H\x00R\x07seconds\x88\x01\x01\x12\x19\n\x05nanos\x18\x02 \x01(\x05H\x01R\x05nanos\x88\x01\x01\x42\n\n\x08_secondsB\x08\n\x06_nanos\"\xd6\x02\n\tCondition\x12\x17\n\x04type\x18\x01 \x01(\tH\x00R\x04type\x88\x01\x01\x12\x1b\n\x06status\x18\x02 \x01(\tH\x01R\x06status\x88\x01\x01\x12\x33\n\x12observedGeneration\x18\x03 \x01(\x03H\x02R\x12observedGeneration\x88\x01\x01\x12I\n\x12lastTransitionTime\x18\x04 \x01(\x0b\x32\x14.jumpstarter.v1.TimeH\x03R\x12lastTransitionTime\x88\x01\x01\x12\x1b\n\x06reason\x18\x05 \x01(\tH\x04R\x06reason\x88\x01\x01\x12\x1d\n\x07message\x18\x06 \x01(\tH\x05R\x07message\x88\x01\x01\x42\x07\n\x05_typeB\t\n\x07_statusB\x15\n\x13_observedGenerationB\x15\n\x13_lastTransitionTimeB\t\n\x07_reasonB\n\n\x08_messageB~\n\x12\x63om.jumpstarter.v1B\x0fKubernetesProtoP\x01\xa2\x02\x03JXX\xaa\x02\x0eJumpstarter.V1\xca\x02\x0eJumpstarter\\V1\xe2\x02\x1aJumpstarter\\V1\\GPBMetadata\xea\x02\x0fJumpstarter::V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.v1.kubernetes_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\022com.jumpstarter.v1B\017KubernetesProtoP\001\242\002\003JXX\252\002\016Jumpstarter.V1\312\002\016Jumpstarter\\V1\342\002\032Jumpstarter\\V1\\GPBMetadata\352\002\017Jumpstarter::V1' _globals['_LABELSELECTOR_MATCHLABELSENTRY']._loaded_options = None _globals['_LABELSELECTOR_MATCHLABELSENTRY']._serialized_options = b'8\001' _globals['_LABELSELECTORREQUIREMENT']._serialized_start=51 diff --git a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/router_pb2.py b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/router_pb2.py index 030a29cbb..0d86b315a 100644 --- a/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/router_pb2.py +++ b/packages/jumpstarter-protocol/jumpstarter_protocol/jumpstarter/v1/router_pb2.py @@ -24,13 +24,14 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bjumpstarter/v1/router.proto\x12\x0ejumpstarter.v1\"c\n\rStreamRequest\x12\x18\n\x07payload\x18\x01 \x01(\x0cR\x07payload\x12\x38\n\nframe_type\x18\x02 \x01(\x0e\x32\x19.jumpstarter.v1.FrameTypeR\tframeType\"d\n\x0eStreamResponse\x12\x18\n\x07payload\x18\x01 \x01(\x0cR\x07payload\x12\x38\n\nframe_type\x18\x02 \x01(\x0e\x32\x19.jumpstarter.v1.FrameTypeR\tframeType*g\n\tFrameType\x12\x13\n\x0f\x46RAME_TYPE_DATA\x10\x00\x12\x19\n\x15\x46RAME_TYPE_RST_STREAM\x10\x03\x12\x13\n\x0f\x46RAME_TYPE_PING\x10\x06\x12\x15\n\x11\x46RAME_TYPE_GOAWAY\x10\x07\x32\\\n\rRouterService\x12K\n\x06Stream\x12\x1d.jumpstarter.v1.StreamRequest\x1a\x1e.jumpstarter.v1.StreamResponse(\x01\x30\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bjumpstarter/v1/router.proto\x12\x0ejumpstarter.v1\"c\n\rStreamRequest\x12\x18\n\x07payload\x18\x01 \x01(\x0cR\x07payload\x12\x38\n\nframe_type\x18\x02 \x01(\x0e\x32\x19.jumpstarter.v1.FrameTypeR\tframeType\"d\n\x0eStreamResponse\x12\x18\n\x07payload\x18\x01 \x01(\x0cR\x07payload\x12\x38\n\nframe_type\x18\x02 \x01(\x0e\x32\x19.jumpstarter.v1.FrameTypeR\tframeType*g\n\tFrameType\x12\x13\n\x0f\x46RAME_TYPE_DATA\x10\x00\x12\x19\n\x15\x46RAME_TYPE_RST_STREAM\x10\x03\x12\x13\n\x0f\x46RAME_TYPE_PING\x10\x06\x12\x15\n\x11\x46RAME_TYPE_GOAWAY\x10\x07\x32\\\n\rRouterService\x12K\n\x06Stream\x12\x1d.jumpstarter.v1.StreamRequest\x1a\x1e.jumpstarter.v1.StreamResponse(\x01\x30\x01\x42z\n\x12\x63om.jumpstarter.v1B\x0bRouterProtoP\x01\xa2\x02\x03JXX\xaa\x02\x0eJumpstarter.V1\xca\x02\x0eJumpstarter\\V1\xe2\x02\x1aJumpstarter\\V1\\GPBMetadata\xea\x02\x0fJumpstarter::V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'jumpstarter.v1.router_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\022com.jumpstarter.v1B\013RouterProtoP\001\242\002\003JXX\252\002\016Jumpstarter.V1\312\002\016Jumpstarter\\V1\342\002\032Jumpstarter\\V1\\GPBMetadata\352\002\017Jumpstarter::V1' _globals['_FRAMETYPE']._serialized_start=250 _globals['_FRAMETYPE']._serialized_end=353 _globals['_STREAMREQUEST']._serialized_start=47 diff --git a/packages/jumpstarter/jumpstarter/client/grpc.py b/packages/jumpstarter/jumpstarter/client/grpc.py index 97763e2f8..fc6f52527 100644 --- a/packages/jumpstarter/jumpstarter/client/grpc.py +++ b/packages/jumpstarter/jumpstarter/client/grpc.py @@ -7,7 +7,7 @@ from types import SimpleNamespace from typing import Any -from google.protobuf import duration_pb2, field_mask_pb2, json_format +from google.protobuf import duration_pb2, field_mask_pb2, json_format, timestamp_pb2 from grpc import ChannelConnectivity from grpc.aio import Channel from jumpstarter_protocol import client_pb2, client_pb2_grpc, jumpstarter_pb2_grpc, kubernetes_pb2, router_pb2_grpc @@ -32,7 +32,7 @@ def add_display_columns(table, options: WithOptions = None): if options.show_leases: table.add_column("LEASED BY") table.add_column("LEASE STATUS") - table.add_column("START TIME") + table.add_column("RELEASE TIME") def add_exporter_row(table, exporter, options: WithOptions = None, lease_info: tuple[str, str, str] | None = None): @@ -45,10 +45,10 @@ def add_exporter_row(table, exporter, options: WithOptions = None, lease_info: t row_data.append(",".join(("{}={}".format(k, v) for k, v in sorted(exporter.labels.items())))) if options.show_leases: if lease_info: - lease_client, lease_status, start_time = lease_info + lease_client, lease_status, expected_release = lease_info else: - lease_client, lease_status, start_time = "", "Available", "" - row_data.extend([lease_client, lease_status, start_time]) + lease_client, lease_status, expected_release = "", "Available", "" + row_data.extend([lease_client, lease_status, expected_release]) table.add_row(*row_data) @@ -97,10 +97,19 @@ def rich_add_rows(self, table, options: WithOptions = None): if options and options.show_leases and self.lease: lease_client = self.lease.client lease_status = self.lease.get_status() - start_time = "" - if self.lease.effective_begin_time: - start_time = self.lease.effective_begin_time.strftime("%Y-%m-%d %H:%M:%S") - lease_info = (lease_client, lease_status, start_time) + release_time = "" + if self.lease.effective_end_time: + # Ended: use actual end time + release_time = self.lease.effective_end_time.strftime("%Y-%m-%d %H:%M:%S") + elif self.lease.effective_begin_time: + # Active: calculate expected end + release_time = self.lease.effective_begin_time + self.lease.duration + release_time = release_time.strftime("%Y-%m-%d %H:%M:%S") + elif self.lease.begin_time: + # Scheduled: calculate expected end + release_time = self.lease.begin_time + self.lease.duration + release_time = release_time.strftime("%Y-%m-%d %H:%M:%S") + lease_info = (lease_client, lease_status, release_time) elif options and options.show_leases: lease_info = ("", "Available", "") add_exporter_row(table, self, options, lease_info) @@ -114,10 +123,13 @@ class Lease(BaseModel): name: str selector: str duration: timedelta + effective_duration: timedelta | None = None + begin_time: datetime | None = None client: str exporter: str conditions: list[kubernetes_pb2.Condition] effective_begin_time: datetime | None = None + effective_end_time: datetime | None = None model_config = ConfigDict( arbitrary_types_allowed=True, @@ -138,20 +150,39 @@ def from_protobuf(cls, data: client_pb2.Lease) -> Lease: else: exporter = "" + effective_duration = None + if data.HasField("effective_duration"): + effective_duration = data.effective_duration.ToTimedelta() + + begin_time = None + if data.HasField("begin_time"): + begin_time = data.begin_time.ToDatetime( + tzinfo=datetime.now().astimezone().tzinfo, + ) + effective_begin_time = None - if data.effective_begin_time: + if data.HasField("effective_begin_time"): effective_begin_time = data.effective_begin_time.ToDatetime( tzinfo=datetime.now().astimezone().tzinfo, ) + effective_end_time = None + if data.HasField("effective_end_time"): + effective_end_time = data.effective_end_time.ToDatetime( + tzinfo=datetime.now().astimezone().tzinfo, + ) + return cls( namespace=namespace, name=name, selector=data.selector, duration=data.duration.ToTimedelta(), + effective_duration=effective_duration, + begin_time=begin_time, client=client, exporter=exporter, effective_begin_time=effective_begin_time, + effective_end_time=effective_end_time, conditions=data.conditions, ) @@ -159,15 +190,27 @@ def from_protobuf(cls, data: client_pb2.Lease) -> Lease: def rich_add_columns(cls, table): table.add_column("NAME", no_wrap=True) table.add_column("SELECTOR") + table.add_column("BEGIN TIME") table.add_column("DURATION") table.add_column("CLIENT") table.add_column("EXPORTER") def rich_add_rows(self, table): + # Show effective_begin_time if active, otherwise show scheduled begin_time + begin_time = "" + if self.effective_begin_time: + begin_time = self.effective_begin_time.strftime("%Y-%m-%d %H:%M:%S") + elif self.begin_time: + begin_time = self.begin_time.strftime("%Y-%m-%d %H:%M:%S") + + # Show actual duration for ended leases, requested duration otherwise + duration = str(self.effective_duration if self.effective_end_time else self.duration or "") + table.add_row( self.name, self.selector, - str(self.duration), + begin_time, + duration, self.client, self.exporter, ) @@ -177,6 +220,10 @@ def rich_add_names(self, names): def get_status(self) -> str: """Get the lease status based on conditions""" + # Check if lease has ended (effective_end_time is set) + if self.effective_end_time: + return "Ended" + if not self.conditions: return "Unknown" @@ -323,6 +370,7 @@ async def ListLeases( page_size: int | None = None, page_token: str | None = None, filter: str | None = None, + only_active: bool = True, ): with translate_grpc_exceptions(): leases = await self.stub.ListLeases( @@ -331,6 +379,7 @@ async def ListLeases( page_size=page_size, page_token=page_token, filter=filter, + only_active=only_active, ) ) return LeaseList.from_protobuf(leases) @@ -340,18 +389,26 @@ async def CreateLease( *, selector: str, duration: timedelta, + begin_time: datetime | None = None, ): duration_pb = duration_pb2.Duration() duration_pb.FromTimedelta(duration) + lease_pb = client_pb2.Lease( + duration=duration_pb, + selector=selector, + ) + + if begin_time: + timestamp_pb = timestamp_pb2.Timestamp() + timestamp_pb.FromDatetime(begin_time) + lease_pb.begin_time.CopyFrom(timestamp_pb) + with translate_grpc_exceptions(): lease = await self.stub.CreateLease( client_pb2.CreateLeaseRequest( parent="namespaces/{}".format(self.namespace), - lease=client_pb2.Lease( - duration=duration_pb, - selector=selector, - ), + lease=lease_pb, ) ) return Lease.from_protobuf(lease) @@ -360,21 +417,37 @@ async def UpdateLease( self, *, name: str, - duration: timedelta, + duration: timedelta | None = None, + begin_time: datetime | None = None, ): - duration_pb = duration_pb2.Duration() - duration_pb.FromTimedelta(duration) + lease_pb = client_pb2.Lease( + name="namespaces/{}/leases/{}".format(self.namespace, name), + ) + + update_fields = [] + + if duration is not None: + duration_pb = duration_pb2.Duration() + duration_pb.FromTimedelta(duration) + lease_pb.duration.CopyFrom(duration_pb) + update_fields.append("duration") + + if begin_time is not None: + timestamp_pb = timestamp_pb2.Timestamp() + timestamp_pb.FromDatetime(begin_time) + lease_pb.begin_time.CopyFrom(timestamp_pb) + update_fields.append("begin_time") + + if not update_fields: + raise ValueError("At least one of duration or begin_time must be provided") update_mask = field_mask_pb2.FieldMask() - update_mask.FromJsonString("duration") + update_mask.FromJsonString(",".join(update_fields)) with translate_grpc_exceptions(): lease = await self.stub.UpdateLease( client_pb2.UpdateLeaseRequest( - lease=client_pb2.Lease( - name="namespaces/{}/leases/{}".format(self.namespace, name), - duration=duration_pb, - ), + lease=lease_pb, update_mask=update_mask, ) ) diff --git a/packages/jumpstarter/jumpstarter/client/grpc_test.py b/packages/jumpstarter/jumpstarter/client/grpc_test.py index f5f069ad9..8146c4933 100644 --- a/packages/jumpstarter/jumpstarter/client/grpc_test.py +++ b/packages/jumpstarter/jumpstarter/client/grpc_test.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import datetime, timedelta from io import StringIO from unittest.mock import Mock @@ -48,7 +48,7 @@ def test_with_leases_columns(self): add_display_columns(table, options) columns = [col.header for col in table.columns] - assert columns == ["NAME", "LABELS", "LEASED BY", "LEASE STATUS", "START TIME"] + assert columns == ["NAME", "LABELS", "LEASED BY", "LEASE STATUS", "RELEASE TIME"] def test_with_all_columns(self): table = Table() @@ -56,7 +56,7 @@ def test_with_all_columns(self): add_display_columns(table, options) columns = [col.header for col in table.columns] - assert columns == ["NAME", "ONLINE", "LABELS", "LEASED BY", "LEASE STATUS", "START TIME"] + assert columns == ["NAME", "ONLINE", "LABELS", "LEASED BY", "LEASE STATUS", "RELEASE TIME"] class TestAddExporterRow: @@ -91,7 +91,7 @@ def test_row_with_lease_info(self): add_exporter_row(table, exporter, options, lease_info) assert len(table.rows) == 1 - assert len(table.columns) == 5 # NAME, LABELS, LEASED BY, LEASE STATUS, START TIME + assert len(table.columns) == 5 # NAME, LABELS, LEASED BY, LEASE STATUS, RELEASE TIME def test_row_with_lease_info_available(self): table = Table() @@ -115,15 +115,23 @@ def test_row_with_all_options(self): add_exporter_row(table, exporter, options, lease_info) assert len(table.rows) == 1 - assert len(table.columns) == 6 # NAME, ONLINE, LABELS, LEASED BY, LEASE STATUS, START TIME + assert len(table.columns) == 6 # NAME, ONLINE, LABELS, LEASED BY, LEASE STATUS, RELEASE TIME class TestExporterList: - def create_test_lease(self, client="test-client", status="Active"): + def create_test_lease(self, client="test-client", status="Active", + effective_begin_time=datetime(2023, 1, 1, 10, 0, 0), + effective_duration=timedelta(hours=1), + begin_time=None, duration=timedelta(hours=1), + effective_end_time=None): lease = Mock(spec=Lease) lease.client = client lease.get_status.return_value = status - lease.effective_begin_time = datetime(2023, 1, 1, 10, 0, 0) + lease.effective_begin_time = effective_begin_time + lease.effective_duration = effective_duration + lease.begin_time = begin_time + lease.duration = duration + lease.effective_end_time = effective_end_time return lease def test_exporter_without_lease(self): @@ -175,7 +183,7 @@ def test_exporter_with_lease_display(self): exporter.rich_add_rows(table, options) assert len(table.rows) == 1 - assert len(table.columns) == 5 # NAME, LABELS, LEASED BY, LEASE STATUS, START TIME + assert len(table.columns) == 5 # NAME, LABELS, LEASED BY, LEASE STATUS, RELEASE TIME # Test actual table content by rendering it console = Console(file=StringIO(), width=120) @@ -187,7 +195,7 @@ def test_exporter_with_lease_display(self): assert "type=device" in output assert "test-client" in output assert "Active" in output - assert "2023-01-01 10:00:00" in output + assert "2023-01-01 11:00:00" in output # Expected release: begin_time (10:00:00) + duration (1h) def test_exporter_without_lease_but_show_leases(self): exporter = Exporter( @@ -203,7 +211,7 @@ def test_exporter_without_lease_but_show_leases(self): exporter.rich_add_rows(table, options) assert len(table.rows) == 1 - assert len(table.columns) == 5 # NAME, LABELS, LEASED BY, LEASE STATUS, START TIME + assert len(table.columns) == 5 # NAME, LABELS, LEASED BY, LEASE STATUS, RELEASE TIME # Test actual table content by rendering it console = Console(file=StringIO(), width=120) @@ -285,7 +293,7 @@ def test_exporter_all_features_display(self): exporter_offline_no_lease.rich_add_rows(table, options) assert len(table.rows) == 2 - assert len(table.columns) == 6 # NAME, ONLINE, LABELS, LEASED BY, LEASE STATUS, START TIME + assert len(table.columns) == 6 # NAME, ONLINE, LABELS, LEASED BY, LEASE STATUS, RELEASE TIME # Test actual table content by rendering it console = Console(file=StringIO(), width=150) @@ -302,11 +310,15 @@ def test_exporter_all_features_display(self): assert "full-test-client" in output # Lease client assert "Active" in output # Lease status assert "Available" in output # Available status for no lease - assert "2023-01-01 10:00:00" in output # Lease start time + assert "2023-01-01 11:00:00" in output # Expected release time (begin_time + duration) def test_exporter_lease_info_extraction(self): """Test that lease information is correctly extracted from lease objects""" - lease = self.create_test_lease(client="my-client", status="Expired") + lease = self.create_test_lease( + client="my-client", + status="Expired", + effective_end_time=datetime(2023, 1, 1, 11, 0, 0) # Ended after 1 hour + ) exporter = Exporter( namespace="default", name="test-exporter", @@ -325,10 +337,21 @@ def test_exporter_lease_info_extraction(self): if options.show_leases and exporter.lease: lease_client = exporter.lease.client lease_status = exporter.lease.get_status() - start_time = exporter.lease.effective_begin_time.strftime("%Y-%m-%d %H:%M:%S") - lease_info = (lease_client, lease_status, start_time) - - assert lease_info == ("my-client", "Expired", "2023-01-01 10:00:00") + expected_release = "" + if exporter.lease.effective_end_time: + # Ended: use actual end time + expected_release = exporter.lease.effective_end_time.strftime("%Y-%m-%d %H:%M:%S") + elif exporter.lease.effective_begin_time: + # Active: calculate expected end + release_time = exporter.lease.effective_begin_time + exporter.lease.duration + expected_release = release_time.strftime("%Y-%m-%d %H:%M:%S") + elif exporter.lease.begin_time: + # Scheduled: calculate expected end + release_time = exporter.lease.begin_time + exporter.lease.duration + expected_release = release_time.strftime("%Y-%m-%d %H:%M:%S") + lease_info = (lease_client, lease_status, expected_release) + + assert lease_info == ("my-client", "Expired", "2023-01-01 11:00:00") def test_exporter_no_lease_info_extraction(self): """Test that default lease information is used when no lease exists""" @@ -351,4 +374,43 @@ def test_exporter_no_lease_info_extraction(self): lease_info = ("", "Available", "") assert lease_info == ("", "Available", "") + def test_exporter_scheduled_lease_expected_release(self): + """Test that scheduled leases show expected release time""" + lease = self.create_test_lease( + client="my-client", + status="Scheduled", + effective_begin_time=None, # Not started yet + effective_duration=None, # Not started yet + begin_time=datetime(2023, 1, 1, 10, 0, 0), + duration=timedelta(hours=1) + ) + exporter = Exporter( + namespace="default", + name="test-exporter", + labels={"type": "device"}, + online=True, + lease=lease + ) + + # Test the table display with scheduled lease + table = Table() + options = WithOptions(show_leases=True) + Exporter.rich_add_columns(table, options) + exporter.rich_add_rows(table, options) + + # Should have 5 columns: NAME, LABELS, LEASED BY, LEASE STATUS, RELEASE TIME + assert len(table.columns) == 5 + assert len(table.rows) == 1 + + # Test actual table content by rendering it + console = Console(file=StringIO(), width=120) + console.print(table) + output = console.file.getvalue() + + # Verify the scheduled lease displays expected release time + assert "test-exporter" in output + assert "my-client" in output + assert "Scheduled" in output + assert "2023-01-01 11:00:00" in output # begin_time (10:00) + duration (1h) + diff --git a/packages/jumpstarter/jumpstarter/client/lease.py b/packages/jumpstarter/jumpstarter/client/lease.py index 835019887..32c81fc6d 100644 --- a/packages/jumpstarter/jumpstarter/client/lease.py +++ b/packages/jumpstarter/jumpstarter/client/lease.py @@ -248,23 +248,24 @@ async def _wait_for_ready_connection(self, path: str): @asynccontextmanager async def monitor_async(self, threshold: timedelta = timedelta(minutes=5)): async def _monitor(): + check_interval = 30 # seconds - check periodically for external lease changes while True: lease = await self.get() - # TODO: use effective_end_time as the authoritative source for lease end time - if lease.effective_begin_time: - end_time = lease.effective_begin_time + lease.duration - remain = end_time - datetime.now(tz=datetime.now().astimezone().tzinfo) + if lease.effective_begin_time and lease.effective_duration: + if lease.effective_end_time: # already ended + end_time = lease.effective_end_time + else: + end_time = lease.effective_begin_time + lease.duration + remain = end_time - datetime.now().astimezone() if remain < timedelta(0): # lease already expired, stopping monitor logger.info("Lease {} ended at {}".format(self.name, end_time)) break - elif remain < threshold: - # lease expiring soon, check again on expected expiration time in case it's extended - logger.info("Lease {} ending soon in {} at {}".format(self.name, remain, end_time)) - await sleep(threshold.total_seconds()) - else: - # lease still active, check again in 5 seconds - await sleep(5) + # Log once when entering the threshold window + if threshold - timedelta(seconds=check_interval) <= remain < threshold: + logger.info("Lease {} ending in {} minutes at {}".format( + self.name, int((remain.total_seconds() + 30) // 60), end_time)) + await sleep(min(remain.total_seconds(), check_interval)) else: await sleep(1) diff --git a/packages/jumpstarter/jumpstarter/config/client.py b/packages/jumpstarter/jumpstarter/config/client.py index fc4c3d1c2..8cb24c504 100644 --- a/packages/jumpstarter/jumpstarter/config/client.py +++ b/packages/jumpstarter/jumpstarter/config/client.py @@ -3,7 +3,7 @@ import asyncio import os from contextlib import asynccontextmanager, contextmanager -from datetime import timedelta +from datetime import datetime, timedelta from functools import wraps from pathlib import Path from typing import Annotated, ClassVar, Literal, Optional, Self @@ -200,11 +200,13 @@ async def create_lease( self, selector: str, duration: timedelta, + begin_time: datetime | None = None, ): svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace) return await svc.CreateLease( selector=selector, duration=duration, + begin_time=begin_time, ) @_blocking_compat @@ -225,23 +227,26 @@ async def list_leases( page_size: int | None = None, page_token: str | None = None, filter: str | None = None, + only_active: bool = True, ): svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace) return await svc.ListLeases( page_size=page_size, page_token=page_token, filter=filter, + only_active=only_active, ) @_blocking_compat @_handle_connection_error async def update_lease( self, - name, - duration: timedelta, + name: str, + duration: timedelta | None = None, + begin_time: datetime | None = None, ): svc = ClientService(channel=await self.channel(), namespace=self.metadata.namespace) - return await svc.UpdateLease(name=name, duration=duration) + return await svc.UpdateLease(name=name, duration=duration, begin_time=begin_time) @asynccontextmanager async def lease_async(