Skip to content

Commit 8d8ff1d

Browse files
authored
Subslicing reservation validation (#738)
* feat: method for checking reservation deployment type * feat: validate deployment type for subslicing * fix: address peer feedback
1 parent 34d0428 commit 8d8ff1d

File tree

4 files changed

+167
-12
lines changed

4 files changed

+167
-12
lines changed

src/xpk/commands/cluster.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from tabulate import tabulate
1818

1919
from ..utils.feature_flags import FeatureFlags
20-
from ..core.capacity import H100_DEVICE_TYPE, H200_DEVICE_TYPE, B200_DEVICE_TYPE
20+
from ..core.capacity import H100_DEVICE_TYPE, H200_DEVICE_TYPE, B200_DEVICE_TYPE, get_reservation_deployment_type
2121
from ..core.cluster import (
2222
get_all_clusters_programmatic,
2323
get_cluster_credentials,
@@ -204,6 +204,38 @@ def cluster_adapt(args) -> None:
204204
def _validate_cluster_create_args(args, system: SystemCharacteristics):
205205
if FeatureFlags.SUB_SLICING_ENABLED and args.sub_slicing:
206206
validate_sub_slicing_system(system)
207+
_validate_sub_slicing_reservation(args)
208+
209+
210+
def _validate_sub_slicing_reservation(args):
211+
if args.reservation is None:
212+
xpk_print(
213+
'Error: Validation failed: Sub-slicing cluster creation requires'
214+
' Cluster Director reservation to be specified.'
215+
)
216+
xpk_exit(1)
217+
218+
deployment_type = get_reservation_deployment_type(
219+
reservation=args.reservation, project=args.project, zone=args.zone
220+
)
221+
if deployment_type != 'DENSE':
222+
xpk_print(
223+
'Error: Validation failed: The specified reservation'
224+
f' "{args.reservation}" is not a Cluster Director reservation.'
225+
)
226+
xpk_print(
227+
'Please provide a reservation created for Cluster Director to proceed.'
228+
)
229+
xpk_print('To list valid Cluster Director reservations, run:')
230+
xpk_print(
231+
' gcloud compute reservations list --filter="deploymentType=DENSE"'
232+
)
233+
xpk_print(
234+
'Refer to the documentation for more information on creating Cluster'
235+
' Director reservations:'
236+
' https://cloud.google.com/cluster-director/docs/reserve-capacity'
237+
)
238+
xpk_exit(1)
207239

208240

209241
def cluster_create(args) -> None:

src/xpk/commands/cluster_test.py

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
@dataclass
2828
class _Mocks:
2929
common_print_mock: MagicMock
30-
common_exit_mock: MagicMock
30+
commands_print_mock: MagicMock
31+
commands_get_reservation_deployment_type: MagicMock
3132

3233

3334
@pytest.fixture
@@ -36,12 +37,17 @@ def mock_common_print_and_exit(mocker):
3637
'xpk.commands.common.xpk_print',
3738
return_value=None,
3839
)
39-
common_exit_mock = mocker.patch(
40-
'xpk.commands.common.xpk_exit',
41-
return_value=None,
40+
commands_print_mock = mocker.patch(
41+
'xpk.commands.cluster.xpk_print', return_value=None
42+
)
43+
commands_get_reservation_deployment_type = mocker.patch(
44+
'xpk.commands.cluster.get_reservation_deployment_type',
45+
return_value='DENSE',
4246
)
4347
return _Mocks(
44-
common_print_mock=common_print_mock, common_exit_mock=common_exit_mock
48+
common_print_mock=common_print_mock,
49+
commands_get_reservation_deployment_type=commands_get_reservation_deployment_type,
50+
commands_print_mock=commands_print_mock,
4551
)
4652

4753

@@ -61,32 +67,82 @@ def test_validate_cluster_create_args_for_correct_args_pass(
6167
_validate_cluster_create_args(args, DEFAULT_TEST_SYSTEM)
6268

6369
assert mock_common_print_and_exit.common_print_mock.call_count == 0
64-
assert mock_common_print_and_exit.common_exit_mock.call_count == 0
6570

6671

6772
def test_validate_cluster_create_args_for_correct_sub_slicing_args_pass(
6873
mock_common_print_and_exit: _Mocks,
6974
):
7075
FeatureFlags.SUB_SLICING_ENABLED = True
71-
args = Namespace(sub_slicing=True)
76+
args = Namespace(
77+
sub_slicing=True,
78+
reservation='test-reservation',
79+
project='project',
80+
zone='zone',
81+
)
7282

7383
_validate_cluster_create_args(args, SUB_SLICING_SYSTEM)
7484

7585
assert mock_common_print_and_exit.common_print_mock.call_count == 0
76-
assert mock_common_print_and_exit.common_exit_mock.call_count == 0
7786

7887

7988
def test_validate_cluster_create_args_for_not_supported_system_throws(
8089
mock_common_print_and_exit: _Mocks,
8190
):
8291
FeatureFlags.SUB_SLICING_ENABLED = True
83-
args = Namespace(sub_slicing=True)
92+
args = Namespace(
93+
sub_slicing=True,
94+
reservation='test-reservation',
95+
project='project',
96+
zone='zone',
97+
)
8498

85-
_validate_cluster_create_args(args, DEFAULT_TEST_SYSTEM)
99+
with pytest.raises(SystemExit):
100+
_validate_cluster_create_args(args, DEFAULT_TEST_SYSTEM)
86101

87102
assert mock_common_print_and_exit.common_print_mock.call_count == 1
88103
assert (
89104
mock_common_print_and_exit.common_print_mock.call_args[0][0]
90105
== 'Error: l4-1 does not support Sub-slicing.'
91106
)
92-
assert mock_common_print_and_exit.common_exit_mock.call_count == 1
107+
108+
109+
def test_validate_cluster_create_args_for_missing_reservation(
110+
mock_common_print_and_exit: _Mocks,
111+
):
112+
FeatureFlags.SUB_SLICING_ENABLED = True
113+
args = Namespace(
114+
sub_slicing=True, project='project', zone='zone', reservation=None
115+
)
116+
117+
with pytest.raises(SystemExit):
118+
_validate_cluster_create_args(args, SUB_SLICING_SYSTEM)
119+
120+
assert mock_common_print_and_exit.commands_print_mock.call_count == 1
121+
assert (
122+
'Validation failed: Sub-slicing cluster creation requires'
123+
in mock_common_print_and_exit.commands_print_mock.call_args[0][0]
124+
)
125+
126+
127+
def test_validate_cluster_create_args_for_invalid_reservation(
128+
mock_common_print_and_exit: _Mocks,
129+
):
130+
FeatureFlags.SUB_SLICING_ENABLED = True
131+
args = Namespace(
132+
sub_slicing=True,
133+
project='project',
134+
zone='zone',
135+
reservation='test-reservation',
136+
)
137+
mock_common_print_and_exit.commands_get_reservation_deployment_type.return_value = (
138+
'SPARSE'
139+
)
140+
141+
with pytest.raises(SystemExit):
142+
_validate_cluster_create_args(args, SUB_SLICING_SYSTEM)
143+
144+
assert mock_common_print_and_exit.commands_print_mock.call_count == 5
145+
assert (
146+
'Refer to the documentation for more information on creating Cluster'
147+
in mock_common_print_and_exit.commands_print_mock.call_args[0][0]
148+
)

src/xpk/core/capacity.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,23 @@ def get_reservation_placement_policy(
152152
return output.strip()
153153

154154

155+
def get_reservation_deployment_type(
156+
reservation: str, zone: str, project: str
157+
) -> str:
158+
"""Get reservation deployment type."""
159+
command = (
160+
f'gcloud beta compute reservations describe {reservation}'
161+
f' --project={project} --zone={zone} --format="value(deploymentType)"'
162+
)
163+
return_code, output = run_command_for_value(
164+
command, 'Get reservation deployment type', dry_run_return_val='DENSE'
165+
)
166+
if return_code != 0:
167+
xpk_print(f'Get reservation deployment type ERROR {return_code}')
168+
xpk_exit(1)
169+
return output.strip()
170+
171+
155172
def verify_reservation_exists(args) -> int:
156173
"""Verify the reservation exists.
157174

src/xpk/core/capacity_test.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
import pytest
18+
from unittest.mock import MagicMock, patch
19+
from .capacity import get_reservation_deployment_type
20+
21+
22+
@patch('xpk.core.capacity.xpk_print')
23+
def test_get_reservation_deployment_type_exits_with_command_fails(
24+
xpk_print: MagicMock, mocker
25+
):
26+
mocker.patch(
27+
target='xpk.core.capacity.run_command_for_value', return_value=(1, '')
28+
)
29+
with pytest.raises(SystemExit):
30+
get_reservation_deployment_type(
31+
reservation='reservation', zone='zone', project='project'
32+
)
33+
34+
assert (
35+
'Get reservation deployment type ERROR 1'
36+
in xpk_print.mock_calls[0].args[0]
37+
)
38+
39+
40+
def test_get_reservation_deployment_type_returns_deployment_type_when_command_succeeds(
41+
mocker,
42+
):
43+
mocker.patch(
44+
target='xpk.core.capacity.run_command_for_value',
45+
return_value=(0, 'DENSE'),
46+
)
47+
result = get_reservation_deployment_type(
48+
reservation='reservation', zone='zone', project='project'
49+
)
50+
assert result == 'DENSE'

0 commit comments

Comments
 (0)