xpk 0.6.0__py3-none-any.whl → 0.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xpk/api/__init__.py +15 -0
- xpk/api/storage_crd.yaml +52 -0
- xpk/commands/batch.py +27 -5
- xpk/commands/cluster.py +104 -80
- xpk/commands/cluster_gcluster.py +94 -10
- xpk/commands/common.py +44 -0
- xpk/commands/config.py +29 -0
- xpk/commands/info.py +8 -10
- xpk/commands/inspector.py +5 -11
- xpk/commands/job.py +9 -7
- xpk/commands/kind.py +34 -4
- xpk/commands/kjob_common.py +44 -0
- xpk/commands/run.py +128 -0
- xpk/commands/shell.py +27 -7
- xpk/commands/storage.py +280 -0
- xpk/commands/version.py +6 -18
- xpk/commands/workload.py +381 -184
- xpk/core/blueprint/blueprint_definitions.py +1 -0
- xpk/core/blueprint/blueprint_generator.py +132 -76
- xpk/core/capacity.py +185 -0
- xpk/core/cluster.py +564 -0
- xpk/core/cluster_private.py +6 -3
- xpk/core/commands.py +18 -14
- xpk/core/config.py +179 -0
- xpk/core/docker_container.py +225 -0
- xpk/core/docker_image.py +210 -0
- xpk/core/docker_resources.py +350 -0
- xpk/core/filestore.py +251 -0
- xpk/core/gcloud_context.py +196 -0
- xpk/core/gcluster_manager.py +20 -2
- xpk/core/gcsfuse.py +50 -0
- xpk/core/kjob.py +257 -18
- xpk/core/kueue.py +12 -6
- xpk/core/monitoring.py +134 -0
- xpk/core/nap.py +32 -20
- xpk/core/network.py +377 -0
- xpk/core/nodepool.py +581 -0
- xpk/core/pathways.py +124 -45
- xpk/core/remote_state/__init__.py +15 -0
- xpk/core/remote_state/fuse_remote_state.py +99 -0
- xpk/core/remote_state/remote_state_client.py +38 -0
- xpk/core/resources.py +238 -0
- xpk/core/scheduling.py +253 -0
- xpk/core/storage.py +581 -0
- xpk/core/system_characteristics.py +38 -1
- xpk/core/vertex.py +105 -0
- xpk/core/workload.py +209 -1
- xpk/core/workload_decorators/rdma_decorator.py +25 -5
- xpk/core/workload_decorators/storage_decorator.py +52 -0
- xpk/core/workload_decorators/tcpxo_decorator.py +70 -37
- xpk/main.py +3 -1
- xpk/parser/batch.py +10 -151
- xpk/parser/cluster.py +49 -8
- xpk/parser/common.py +189 -1
- xpk/parser/config.py +49 -0
- xpk/parser/core.py +27 -1
- xpk/parser/info.py +2 -1
- xpk/parser/inspector.py +3 -3
- xpk/parser/job.py +25 -4
- xpk/parser/kind.py +3 -2
- xpk/parser/run.py +47 -0
- xpk/parser/shell.py +10 -1
- xpk/parser/storage.py +326 -0
- xpk/parser/validators.py +3 -3
- xpk/parser/workload.py +118 -76
- xpk/templates/__init__.py +15 -0
- xpk/templates/storage.yaml +13 -0
- xpk/utils/gcs_utils.py +125 -0
- xpk/utils/kubectl.py +57 -0
- xpk/utils/objects.py +8 -5
- xpk/utils/templates.py +28 -0
- xpk/utils/validation.py +80 -0
- {xpk-0.6.0.dist-info → xpk-0.7.1.dist-info}/METADATA +169 -15
- xpk-0.7.1.dist-info/RECORD +92 -0
- {xpk-0.6.0.dist-info → xpk-0.7.1.dist-info}/WHEEL +1 -1
- xpk/core/core.py +0 -2824
- xpk-0.6.0.dist-info/RECORD +0 -57
- {xpk-0.6.0.dist-info → xpk-0.7.1.dist-info}/entry_points.txt +0 -0
- {xpk-0.6.0.dist-info → xpk-0.7.1.dist-info/licenses}/LICENSE +0 -0
- {xpk-0.6.0.dist-info → xpk-0.7.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,350 @@
|
|
|
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
|
+
from .capacity import H100_DEVICE_TYPE, H100_MEGA_DEVICE_TYPE, H200_DEVICE_TYPE
|
|
18
|
+
from .cluster import setup_k8s_env
|
|
19
|
+
from .storage import GCS_FUSE_TYPE, GCP_FILESTORE_TYPE, Storage, get_storages_to_mount
|
|
20
|
+
from .system_characteristics import AcceleratorType, SystemCharacteristics
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_main_container_resources(
|
|
24
|
+
args, system: SystemCharacteristics, resource_type
|
|
25
|
+
) -> str:
|
|
26
|
+
"""Resources for the main container.
|
|
27
|
+
Args:
|
|
28
|
+
args: user provided args.
|
|
29
|
+
system: system characteristics.
|
|
30
|
+
resource_type: TPU / GPU / CPU
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str:
|
|
34
|
+
Workload resources port as a YAML string
|
|
35
|
+
"""
|
|
36
|
+
# Resources requirements for Pathways workload containers are known.
|
|
37
|
+
resources_yaml = """cpu: "24"
|
|
38
|
+
memory: 100G"""
|
|
39
|
+
if args.use_pathways:
|
|
40
|
+
return resources_yaml
|
|
41
|
+
|
|
42
|
+
gpu_resources_yaml = """nvidia.com/gpu: {system.chips_per_vm}"""
|
|
43
|
+
if system.accelerator_type == AcceleratorType['GPU']:
|
|
44
|
+
return gpu_resources_yaml.format(system=system)
|
|
45
|
+
|
|
46
|
+
if system.accelerator_type == AcceleratorType['CPU']:
|
|
47
|
+
# CPUs don't have chips, but have a subresource called vCPUs.
|
|
48
|
+
# system.chips_per_vm is used as a proxy for vCPUs.
|
|
49
|
+
# Some vCPUs get used in hosting system pods of the workloads,
|
|
50
|
+
# hence an offset of 0.95 is introduced.
|
|
51
|
+
offset_vCPUs = int(system.chips_per_vm) * 0.95
|
|
52
|
+
return f'{resource_type}: {offset_vCPUs}'
|
|
53
|
+
|
|
54
|
+
return f'{resource_type}: {system.chips_per_vm}'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_env_container(args, system: SystemCharacteristics) -> str:
|
|
58
|
+
"""Environment configuration for the main container.
|
|
59
|
+
Args:
|
|
60
|
+
args: user provided args.
|
|
61
|
+
system: system characteristics.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
str:
|
|
65
|
+
YAML with the env config for the main container, as a YAML string.
|
|
66
|
+
"""
|
|
67
|
+
pw_env_yaml = """
|
|
68
|
+
- name: XCLOUD_ENVIRONMENT
|
|
69
|
+
value: GCP
|
|
70
|
+
- name: JAX_PLATFORMS
|
|
71
|
+
value: proxy
|
|
72
|
+
- name: JAX_BACKEND_TARGET
|
|
73
|
+
value: {proxy_address}
|
|
74
|
+
- name: JOBSET_NAME
|
|
75
|
+
valueFrom:
|
|
76
|
+
fieldRef:
|
|
77
|
+
fieldPath: metadata.annotations['jobset.sigs.k8s.io/jobset-name']"""
|
|
78
|
+
if args.use_pathways:
|
|
79
|
+
return pw_env_yaml.format(
|
|
80
|
+
args=args, proxy_address=args.pathways_proxy_address
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
gpu_env_yaml = """
|
|
84
|
+
- name: REPLICATED_JOB_NAME
|
|
85
|
+
valueFrom:
|
|
86
|
+
fieldRef:
|
|
87
|
+
fieldPath: metadata.annotations['jobset.sigs.k8s.io/replicatedjob-name']
|
|
88
|
+
- name: JOBSET_NAME
|
|
89
|
+
valueFrom:
|
|
90
|
+
fieldRef:
|
|
91
|
+
fieldPath: metadata.annotations['jobset.sigs.k8s.io/jobset-name']
|
|
92
|
+
- name: JAX_COORDINATOR_ADDRESS
|
|
93
|
+
value: "$(JOBSET_NAME)-$(REPLICATED_JOB_NAME)-0-0.$(JOBSET_NAME)"
|
|
94
|
+
- name: NNODES
|
|
95
|
+
value: "{args.num_nodes}"
|
|
96
|
+
- name: NODE_RANK
|
|
97
|
+
valueFrom:
|
|
98
|
+
fieldRef:
|
|
99
|
+
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
|
|
100
|
+
- name: USE_GPUDIRECT
|
|
101
|
+
value: {gpu_direct_name}
|
|
102
|
+
- name: GPUS_PER_NODE
|
|
103
|
+
value: "{system.chips_per_vm}"
|
|
104
|
+
- name: JAX_COORDINATOR_PORT
|
|
105
|
+
value: "6002"
|
|
106
|
+
- name: COMMAND
|
|
107
|
+
value: "{args.command}"
|
|
108
|
+
{args.env}"""
|
|
109
|
+
|
|
110
|
+
if system.accelerator_type == AcceleratorType['GPU']:
|
|
111
|
+
gpu_direct_name = 'fastrak'
|
|
112
|
+
if args.device_type == H100_DEVICE_TYPE:
|
|
113
|
+
gpu_direct_name = 'tcpx'
|
|
114
|
+
gpu_env_yaml += """
|
|
115
|
+
- name: LD_LIBRARY_PATH
|
|
116
|
+
value: /usr/local/nvidia/lib64
|
|
117
|
+
"""
|
|
118
|
+
elif args.device_type == H100_MEGA_DEVICE_TYPE:
|
|
119
|
+
gpu_direct_name = 'tcpxo'
|
|
120
|
+
elif args.device_type == H200_DEVICE_TYPE:
|
|
121
|
+
gpu_direct_name = 'rdma'
|
|
122
|
+
return gpu_env_yaml.format(
|
|
123
|
+
args=args, system=system, gpu_direct_name=gpu_direct_name
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if system.accelerator_type == AcceleratorType['CPU']:
|
|
127
|
+
return get_cpu_env(args.num_slices, args.env, system)
|
|
128
|
+
|
|
129
|
+
return args.env # pytype: disable=bad-return-type
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_cpu_env(num_slices, env_vars, system) -> str:
|
|
133
|
+
"""Generate environment variables for CPU nodepools
|
|
134
|
+
Args:
|
|
135
|
+
num_slices: Number of slices to be used in the workload.
|
|
136
|
+
env_vars: Environment variables, processed from user args.
|
|
137
|
+
system: system characteristics
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
str: yaml containing env variables
|
|
141
|
+
"""
|
|
142
|
+
yaml = """
|
|
143
|
+
- name: REPLICATED_JOB_NAME
|
|
144
|
+
valueFrom:
|
|
145
|
+
fieldRef:
|
|
146
|
+
fieldPath: metadata.annotations['jobset.sigs.k8s.io/replicatedjob-name']
|
|
147
|
+
- name: JOB_INDEX
|
|
148
|
+
valueFrom:
|
|
149
|
+
fieldRef:
|
|
150
|
+
fieldPath: metadata.annotations['jobset.sigs.k8s.io/job-index']
|
|
151
|
+
- name: JOB_COMPLETION_INDEX
|
|
152
|
+
valueFrom:
|
|
153
|
+
fieldRef:
|
|
154
|
+
fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
|
|
155
|
+
- name: PROCESSES_IN_JOB
|
|
156
|
+
value: "{processes_in_job}"
|
|
157
|
+
- name: JAX_PROCESS_COUNT
|
|
158
|
+
value: "{process_count}"
|
|
159
|
+
{env_vars}
|
|
160
|
+
- name: JAX_COORDINATOR_ADDRESS
|
|
161
|
+
value: "$(JOBSET_NAME)-$(REPLICATED_JOB_NAME)-0-0.$(JOBSET_NAME)"
|
|
162
|
+
"""
|
|
163
|
+
return yaml.format(
|
|
164
|
+
processes_in_job=system.vms_per_slice,
|
|
165
|
+
process_count=calculate_process_count(num_slices, system.vms_per_slice),
|
|
166
|
+
env_vars=env_vars,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_volumes(args, system: SystemCharacteristics) -> str:
|
|
171
|
+
"""Get volumes accessible to the containers in the pod.
|
|
172
|
+
Args:
|
|
173
|
+
args: user provided args.
|
|
174
|
+
system: system characteristics.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
str:
|
|
178
|
+
YAML for the volumes.
|
|
179
|
+
"""
|
|
180
|
+
volumes = """- emptyDir:
|
|
181
|
+
medium: Memory
|
|
182
|
+
name: dshm-2
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
if args.ramdisk_directory != '':
|
|
186
|
+
volumes += """
|
|
187
|
+
- name: cache
|
|
188
|
+
csi:
|
|
189
|
+
driver: phase1-checkpoint.csi.storage.gke.io"""
|
|
190
|
+
|
|
191
|
+
if (
|
|
192
|
+
system.accelerator_type == AcceleratorType['TPU']
|
|
193
|
+
and args.deploy_stacktrace_sidecar
|
|
194
|
+
):
|
|
195
|
+
volumes += """
|
|
196
|
+
- name: tpu-stack-trace
|
|
197
|
+
- name: shared-data
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
storages: list[Storage] = get_storages_to_mount(
|
|
201
|
+
setup_k8s_env(args), args.storage
|
|
202
|
+
)
|
|
203
|
+
for storage in storages:
|
|
204
|
+
if storage.type == GCS_FUSE_TYPE:
|
|
205
|
+
volumes += f"""- name: {storage.pv}
|
|
206
|
+
persistentVolumeClaim:
|
|
207
|
+
claimName: {storage.pvc}
|
|
208
|
+
readOnly: {storage.readonly}
|
|
209
|
+
"""
|
|
210
|
+
if storage.type == GCP_FILESTORE_TYPE:
|
|
211
|
+
volumes += f"""- name: {storage.pv}
|
|
212
|
+
persistentVolumeClaim:
|
|
213
|
+
claimName: {storage.pvc}
|
|
214
|
+
readOnly: {storage.readonly}
|
|
215
|
+
"""
|
|
216
|
+
return volumes
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_volume_mounts(args, system: SystemCharacteristics) -> str:
|
|
220
|
+
"""Resources for the main container.
|
|
221
|
+
Args:
|
|
222
|
+
args: user provided args.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
str:
|
|
226
|
+
YAML for the volumes mounted within a Pathways container or GPU container as a YAML string.
|
|
227
|
+
"""
|
|
228
|
+
volume_mount_yaml = """- mountPath: /dev/shm
|
|
229
|
+
name: dshm-2
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
if args.ramdisk_directory != '':
|
|
233
|
+
volume_mount_yaml += f"""
|
|
234
|
+
- mountPath: /{args.ramdisk_directory}
|
|
235
|
+
name: cache"""
|
|
236
|
+
|
|
237
|
+
if args.use_pathways:
|
|
238
|
+
volume_mount_yaml = """- mountPath: /tmp
|
|
239
|
+
name: shared-tmp
|
|
240
|
+
"""
|
|
241
|
+
elif (
|
|
242
|
+
system.accelerator_type == AcceleratorType['TPU']
|
|
243
|
+
and args.deploy_stacktrace_sidecar
|
|
244
|
+
):
|
|
245
|
+
volume_mount_yaml += """- name: tpu-stack-trace
|
|
246
|
+
mountPath: /tmp/debugging
|
|
247
|
+
- name: shared-data
|
|
248
|
+
mountPath: /shared-volume
|
|
249
|
+
"""
|
|
250
|
+
elif system.accelerator_type == AcceleratorType['GPU']:
|
|
251
|
+
if system.device_type == H100_DEVICE_TYPE:
|
|
252
|
+
volume_mount_yaml = """- name: nvidia-install-dir-host
|
|
253
|
+
mountPath: /usr/local/nvidia/lib64
|
|
254
|
+
- name: tcpx-nccl-plugin-volume
|
|
255
|
+
mountPath: /usr/local/tcpx
|
|
256
|
+
- name: tcpd-socket
|
|
257
|
+
mountPath: /tmp
|
|
258
|
+
- name: shared-memory
|
|
259
|
+
mountPath: /dev/shm
|
|
260
|
+
- name: workload-terminated-volume
|
|
261
|
+
mountPath: /usr/share/workload"""
|
|
262
|
+
elif (
|
|
263
|
+
system.device_type == H100_MEGA_DEVICE_TYPE
|
|
264
|
+
or system.device_type == H200_DEVICE_TYPE
|
|
265
|
+
):
|
|
266
|
+
volume_mount_yaml = ''
|
|
267
|
+
|
|
268
|
+
storages: list[Storage] = get_storages_to_mount(
|
|
269
|
+
setup_k8s_env(args), args.storage
|
|
270
|
+
)
|
|
271
|
+
for storage in storages:
|
|
272
|
+
if storage.type == GCS_FUSE_TYPE:
|
|
273
|
+
volume_mount_yaml += f"""- name: {storage.pv}
|
|
274
|
+
mountPath: {storage.mount_point}
|
|
275
|
+
readOnly: {storage.readonly}
|
|
276
|
+
"""
|
|
277
|
+
if storage.type == GCP_FILESTORE_TYPE:
|
|
278
|
+
volume_mount_yaml += f"""- name: {storage.pv}
|
|
279
|
+
mountPath: {storage.mount_point}
|
|
280
|
+
readOnly: {storage.readonly}
|
|
281
|
+
"""
|
|
282
|
+
return volume_mount_yaml
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def calculate_process_count(num_slices, vms_per_slice) -> str:
|
|
286
|
+
"""Calculates the total number of processes in the workload.
|
|
287
|
+
Args:
|
|
288
|
+
num_slices: Number of slices to be used in the workload.
|
|
289
|
+
vms_per_slice: number of VMs in each slice.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
str: total number of processes.
|
|
293
|
+
"""
|
|
294
|
+
num_processes = int(num_slices) * int(vms_per_slice)
|
|
295
|
+
|
|
296
|
+
return f'{num_processes}'
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def add_container_ports(args, system: SystemCharacteristics) -> str:
|
|
300
|
+
"""Add slice builder and megascale container ports,
|
|
301
|
+
for non-pathways workloads.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
args: user provided args.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
str:
|
|
308
|
+
Pathways server port as a YAML string
|
|
309
|
+
"""
|
|
310
|
+
port_yaml = """- containerPort: 8471
|
|
311
|
+
- containerPort: 8080"""
|
|
312
|
+
if args.use_pathways:
|
|
313
|
+
return ''
|
|
314
|
+
|
|
315
|
+
gpu_port_yaml = """- containerPort: 6002"""
|
|
316
|
+
if system.accelerator_type == AcceleratorType['GPU']:
|
|
317
|
+
return gpu_port_yaml
|
|
318
|
+
return port_yaml
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def add_jax_coordinator_port(system) -> str:
|
|
322
|
+
"""Add jax coordinator port only for CPUs
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
system: system characteristics.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
str:
|
|
329
|
+
jax coordinator port as a YAML string
|
|
330
|
+
"""
|
|
331
|
+
if system.accelerator_type == AcceleratorType['CPU']:
|
|
332
|
+
return '- containerPort: 1234'
|
|
333
|
+
return ''
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def add_image_pull_policy_for_pw_or_gpu(args, system: SystemCharacteristics):
|
|
337
|
+
"""Add image pull policy only for Pathways containers.
|
|
338
|
+
Args:
|
|
339
|
+
args: user provided args.
|
|
340
|
+
system: system characteristics
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
str:
|
|
344
|
+
YAML stating that the image will be pulled fro GCR every time.
|
|
345
|
+
"""
|
|
346
|
+
yaml = """imagePullPolicy: Always"""
|
|
347
|
+
|
|
348
|
+
if args.use_pathways or system.accelerator_type == AcceleratorType['GPU']:
|
|
349
|
+
return yaml.format(args=args)
|
|
350
|
+
return ''
|
xpk/core/filestore.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
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
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
from google.cloud import filestore_v1
|
|
20
|
+
from google.cloud.exceptions import GoogleCloudError
|
|
21
|
+
from google.cloud.filestore_v1.types import (
|
|
22
|
+
FileShareConfig,
|
|
23
|
+
Instance,
|
|
24
|
+
NetworkConfig,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from ..utils import templates
|
|
28
|
+
from ..utils.console import xpk_exit, xpk_print
|
|
29
|
+
from .cluster import zone_to_region
|
|
30
|
+
|
|
31
|
+
FS_PV_PATH = "/../templates/filestore-pv.yaml"
|
|
32
|
+
FS_PVC_PATH = "/../templates/filestore-pvc.yaml"
|
|
33
|
+
FS_SC_PATH = "/../templates/filestore-sc.yaml"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Availability(Enum):
|
|
37
|
+
ZONAL = "Zonal"
|
|
38
|
+
REGIONAL = "Regional"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
TIERS = {
|
|
42
|
+
"BASIC_HDD": Availability.ZONAL,
|
|
43
|
+
"BASIC_SSD": Availability.ZONAL,
|
|
44
|
+
"ZONAL": Availability.ZONAL,
|
|
45
|
+
"REGIONAL": Availability.REGIONAL,
|
|
46
|
+
"ENTERPRISE": Availability.REGIONAL,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_storage_class_name(storage_name: str) -> str:
|
|
51
|
+
return f"{storage_name}-sc"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_pv_name(storage_name: str) -> str:
|
|
55
|
+
return f"{storage_name}-pv"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_pvc_name(storage_name: str) -> str:
|
|
59
|
+
return f"{storage_name}-pvc"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FilestoreClient:
|
|
63
|
+
"""FilestoreClient is a class for interacting with GCP filestore instances."""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
zone: str,
|
|
68
|
+
name: str,
|
|
69
|
+
project: str,
|
|
70
|
+
) -> None:
|
|
71
|
+
self.zone = zone
|
|
72
|
+
self.region = zone_to_region(zone)
|
|
73
|
+
self.name = name
|
|
74
|
+
self.project = project
|
|
75
|
+
self._client = filestore_v1.CloudFilestoreManagerClient()
|
|
76
|
+
self.instance: Instance | None = None
|
|
77
|
+
|
|
78
|
+
def get_instance(self) -> Instance | None:
|
|
79
|
+
"""Get existing Filestore instance"""
|
|
80
|
+
parentZonal = self.get_parent(self.zone)
|
|
81
|
+
parentRegional = self.get_parent(self.region)
|
|
82
|
+
reqZonal = filestore_v1.ListInstancesRequest(parent=parentZonal)
|
|
83
|
+
reqRegional = filestore_v1.ListInstancesRequest(parent=parentRegional)
|
|
84
|
+
try:
|
|
85
|
+
instancesZonal = self._client.list_instances(reqZonal)
|
|
86
|
+
instancesRegional = self._client.list_instances(reqRegional)
|
|
87
|
+
except GoogleCloudError as e:
|
|
88
|
+
xpk_print(f"Exception while trying to list instances {e}")
|
|
89
|
+
xpk_exit(1)
|
|
90
|
+
|
|
91
|
+
fullname_zonal = self.get_instance_fullname(self.zone)
|
|
92
|
+
fullname_regional = self.get_instance_fullname(self.region)
|
|
93
|
+
|
|
94
|
+
for instance in instancesZonal:
|
|
95
|
+
if instance.name == fullname_zonal:
|
|
96
|
+
return instance # pytype: disable=bad-return-type
|
|
97
|
+
|
|
98
|
+
for instance in instancesRegional:
|
|
99
|
+
if instance.name == fullname_regional:
|
|
100
|
+
return instance # pytype: disable=bad-return-type
|
|
101
|
+
|
|
102
|
+
def check_instance_exists(self) -> bool:
|
|
103
|
+
"""Check if Filestore instance exists"""
|
|
104
|
+
instance = self.get_instance()
|
|
105
|
+
return instance is not None
|
|
106
|
+
|
|
107
|
+
def load_instance(self) -> None:
|
|
108
|
+
if self.instance is None:
|
|
109
|
+
self.instance = self.get_instance()
|
|
110
|
+
|
|
111
|
+
def get_instance_location(self) -> str:
|
|
112
|
+
"""Get Filestore instance's location"""
|
|
113
|
+
self.load_instance()
|
|
114
|
+
return str(self.instance.name.split("/")[3])
|
|
115
|
+
|
|
116
|
+
def create_instance(
|
|
117
|
+
self,
|
|
118
|
+
vol: str,
|
|
119
|
+
size: int,
|
|
120
|
+
tier: str,
|
|
121
|
+
connect_mode=None,
|
|
122
|
+
reserved_ip_range=None,
|
|
123
|
+
network: str = "default",
|
|
124
|
+
description: str = "XPK created filestore instance",
|
|
125
|
+
kms_key_name=None,
|
|
126
|
+
source_backup=None,
|
|
127
|
+
nfs_export_options=None,
|
|
128
|
+
modes=None,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Create new Filestore instance"""
|
|
131
|
+
|
|
132
|
+
location = (
|
|
133
|
+
self.zone
|
|
134
|
+
if TIERS[tier].value == Availability.ZONAL.value
|
|
135
|
+
else self.region
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
file_shares = [
|
|
139
|
+
FileShareConfig(
|
|
140
|
+
name=vol,
|
|
141
|
+
capacity_gb=size,
|
|
142
|
+
source_backup=source_backup,
|
|
143
|
+
nfs_export_options=nfs_export_options,
|
|
144
|
+
)
|
|
145
|
+
]
|
|
146
|
+
networks = [
|
|
147
|
+
NetworkConfig(
|
|
148
|
+
network=network,
|
|
149
|
+
modes=modes,
|
|
150
|
+
reserved_ip_range=reserved_ip_range,
|
|
151
|
+
connect_mode=connect_mode,
|
|
152
|
+
)
|
|
153
|
+
]
|
|
154
|
+
request = filestore_v1.CreateInstanceRequest(
|
|
155
|
+
parent=self.get_parent(location),
|
|
156
|
+
instance_id=self.name,
|
|
157
|
+
instance=Instance(
|
|
158
|
+
description=description,
|
|
159
|
+
tier=tier,
|
|
160
|
+
kms_key_name=kms_key_name,
|
|
161
|
+
file_shares=file_shares,
|
|
162
|
+
networks=networks,
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
# Make the request
|
|
166
|
+
operation = self._client.create_instance(request=request)
|
|
167
|
+
xpk_print("Waiting for filestore creation to complete...")
|
|
168
|
+
self.instance = None
|
|
169
|
+
try:
|
|
170
|
+
self.instance = operation.result()
|
|
171
|
+
except GoogleCloudError as e:
|
|
172
|
+
xpk_print(f"Error while creating Filestore instance: {e}")
|
|
173
|
+
xpk_exit(1)
|
|
174
|
+
xpk_print(
|
|
175
|
+
f"Filestore instance {self.get_instance_fullname(location)} created"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def delete_filestore_instance(self):
|
|
179
|
+
# Initialize request
|
|
180
|
+
name = self.get_instance_fullname()
|
|
181
|
+
request = filestore_v1.DeleteInstanceRequest(name=name)
|
|
182
|
+
|
|
183
|
+
# Make the request
|
|
184
|
+
operation = self._client.delete_instance(request)
|
|
185
|
+
xpk_print("Waiting for filestore deletion to complete...")
|
|
186
|
+
try:
|
|
187
|
+
operation.result()
|
|
188
|
+
except GoogleCloudError as e:
|
|
189
|
+
xpk_print(f"Error while deleting Filestore instance: {e}")
|
|
190
|
+
xpk_exit(1)
|
|
191
|
+
xpk_print(f"Filestore instance {name} deleted")
|
|
192
|
+
|
|
193
|
+
def create_sc(self, name: str, network: str) -> dict:
|
|
194
|
+
"""Create a yaml representing filestore StorageClass."""
|
|
195
|
+
data = templates.load(FS_SC_PATH)
|
|
196
|
+
data["metadata"]["name"] = get_storage_class_name(name)
|
|
197
|
+
data["parameters"]["tier"] = self.instance.tier.name
|
|
198
|
+
data["parameters"][
|
|
199
|
+
"network"
|
|
200
|
+
] = f"projects/{self.project}/global/networks/{network}"
|
|
201
|
+
return data
|
|
202
|
+
|
|
203
|
+
def create_pv(self, name: str, vol: str, access_mode: str) -> dict:
|
|
204
|
+
"""Create a yaml representing filestore PersistentVolume."""
|
|
205
|
+
data = templates.load(FS_PV_PATH)
|
|
206
|
+
data["metadata"]["name"] = get_pv_name(name)
|
|
207
|
+
data["spec"]["storageClassName"] = get_storage_class_name(name)
|
|
208
|
+
data["spec"]["capacity"]["storage"] = self.instance.file_shares[
|
|
209
|
+
0
|
|
210
|
+
].capacity_gb
|
|
211
|
+
data["spec"]["accessModes"] = [access_mode]
|
|
212
|
+
volumeHandle = f"{self.get_instance_fullname()}/volumes/{vol}"
|
|
213
|
+
data["spec"]["csi"]["volumeHandle"] = volumeHandle
|
|
214
|
+
data["spec"]["csi"]["volumeAttributes"]["ip"] = self.instance.networks[
|
|
215
|
+
0
|
|
216
|
+
].ip_addresses[0]
|
|
217
|
+
data["spec"]["csi"]["volumeAttributes"]["volume"] = vol
|
|
218
|
+
return data
|
|
219
|
+
|
|
220
|
+
def create_pvc(self, name: str, access_mode: str) -> dict:
|
|
221
|
+
"""Create a yaml representing filestore PersistentVolumeClaim."""
|
|
222
|
+
data = templates.load(FS_PVC_PATH)
|
|
223
|
+
data["metadata"]["name"] = get_pvc_name(name)
|
|
224
|
+
data["spec"]["accessModes"] = [access_mode]
|
|
225
|
+
data["spec"]["storageClassName"] = get_storage_class_name(name)
|
|
226
|
+
data["spec"]["volumeName"] = get_pv_name(name)
|
|
227
|
+
data["spec"]["resources"]["requests"]["storage"] = (
|
|
228
|
+
self.instance.file_shares[0].capacity_gb
|
|
229
|
+
)
|
|
230
|
+
return data
|
|
231
|
+
|
|
232
|
+
def manifest(
|
|
233
|
+
self, name: str, vol: str, access_mode: str, network: str
|
|
234
|
+
) -> list[dict]:
|
|
235
|
+
self.load_instance()
|
|
236
|
+
pv = self.create_pv(name, vol, access_mode)
|
|
237
|
+
pvc = self.create_pvc(name, access_mode)
|
|
238
|
+
sc = self.create_sc(name, network)
|
|
239
|
+
return [pv, pvc, sc]
|
|
240
|
+
|
|
241
|
+
def get_parent(self, location: str | None = None) -> str:
|
|
242
|
+
"""Get the Filestore's parent's name"""
|
|
243
|
+
if location is None:
|
|
244
|
+
location = self.get_instance_location()
|
|
245
|
+
return f"projects/{self.project}/locations/{location}"
|
|
246
|
+
|
|
247
|
+
def get_instance_fullname(self, location: str | None = None) -> str:
|
|
248
|
+
"""Get the Filestore's full name"""
|
|
249
|
+
if location is None:
|
|
250
|
+
location = self.get_instance_location()
|
|
251
|
+
return f"projects/{self.project}/locations/{location}/instances/{self.name}"
|