2020import subprocess
2121import time
2222from collections import defaultdict
23- from typing import Any , Dict , List , Optional , Tuple , TypeVar , TYPE_CHECKING , Union
23+ from typing import Any , cast , Dict , List , Optional , Tuple , TypeVar , TYPE_CHECKING , Union
2424
2525from googleapiclient .errors import HttpError
2626
@@ -143,6 +143,73 @@ def _FindResourceByName(
143143
144144 return matches .pop ()
145145
146+ def _GetResourceFromComputeApi (
147+ self ,
148+ resource_type : str ,
149+ resource_name : str ,
150+ zone : Optional [str ] = None ,
151+ region : Optional [str ] = None ) -> Optional [Dict [str , Any ]]:
152+ """Helper to get a specific resource from the GCE API.
153+
154+ Args:
155+ resource_type: The GCE resource type (e.g., 'instance' or 'disk').
156+ resource_name: The resource name or ID.
157+ zone: Optional zone to restrict the search.
158+ region: Optional region to restrict the search.
159+
160+ Returns:
161+ Optional[Dict[str, Any]]: The resource metadata if found, None otherwise.
162+ """
163+ client = getattr (self .GceApi (), resource_type )()
164+ if zone or region :
165+ param_name = (resource_type [:- 1 ] if resource_type .endswith ('s' )
166+ else resource_type )
167+ if resource_type == 'instanceGroupManagers' :
168+ param_name = 'instanceGroupManager'
169+ elif resource_type == 'regionDisks' :
170+ param_name = 'disk'
171+
172+ kwargs = {'project' : self .project_id , param_name : resource_name }
173+ if zone :
174+ kwargs ['zone' ] = zone
175+ if region :
176+ kwargs ['region' ] = region
177+
178+ try :
179+ return cast (Dict [str , Any ], client .get (** kwargs ).execute ())
180+ except HttpError as e :
181+ if e .resp .status == 404 :
182+ return None
183+ raise
184+ else :
185+ # Use aggregatedList with filter to avoid listing all resources
186+ filter_str = f'name = "{ resource_name } "'
187+ if re .match (RESOURCE_ID_REGEX , resource_name ):
188+ filter_str = f'id = "{ resource_name } "'
189+
190+ # Regional resources might not support aggregatedList on their own
191+ # client. For example, regionDisks doesn't have aggregatedList.
192+ # We use disks.aggregatedList instead as it returns both zonal
193+ # and regional disks.
194+ if resource_type == 'regionDisks' :
195+ client = self .GceApi ().disks () # pylint: disable=no-member
196+ res_type_in_resp = 'disks'
197+ else :
198+ res_type_in_resp = resource_type
199+
200+ responses = common .ExecuteRequest (
201+ client ,
202+ 'aggregatedList' , {
203+ 'project' : self .project_id , 'filter' : filter_str
204+ })
205+ for response in responses :
206+ for location in response .get ('items' , {}):
207+ items = response ['items' ][location ].get (res_type_in_resp , [])
208+ if items :
209+ return cast (Dict [str , Any ], items [0 ])
210+ return None
211+
212+
146213 def Instances (self ,
147214 refresh : bool = True ) -> Dict [str , 'GoogleComputeInstance' ]:
148215 """Get all instances in the project.
@@ -237,18 +304,28 @@ def ListInstances(self) -> Dict[str, 'GoogleComputeInstance']:
237304
238305 return instances
239306
240- def ListSnapshots (self , filter_string : str | None = None ) -> Dict [str , Any ]:
307+ def ListSnapshots (self ,
308+ filter_string : str | None = None ,
309+ zone : str | None = None ) -> Dict [str , Any ]:
241310 """List snapshots in project.
242311
243312 Args:
244313 filter_string: Filter for the snapshot query.
314+ zone: Optional zone to filter snapshots by.
245315
246316 Returns:
247317 Dict[str, Any]: Dictionary mapping snapshot IDs (str)
248318 to their respective snapshot description.
249319 See:
250320 https://docs.cloud.google.com/compute/docs/reference/rest/v1/snapshots/list
251321 """
322+ if zone :
323+ zone_filter = f'sourceDisk ~ ".*/zones/{ zone } /disks/.*"'
324+ if filter_string :
325+ filter_string = f'({ filter_string } ) ({ zone_filter } )'
326+ else :
327+ filter_string = zone_filter
328+
252329 snapshots = {}
253330 gce_snapshot_client = self .GceApi ().snapshots () # pylint: disable=no-member
254331 responses = common .ExecuteRequest (
@@ -450,16 +527,25 @@ def GetRegionDisk(
450527 Raises:
451528 ResourceNotFoundError: When the specified disk cannot be found in project.
452529 """
453- disks = self .RegionDisks ()
454- if re .match (RESOURCE_ID_REGEX , disk_name ):
455- disk = disks .get (disk_name )
456- else :
457- disk = self ._FindResourceByName (disks , disk_name , region = region )
458- if not disk :
530+ disk_dict = self ._GetResourceFromComputeApi (
531+ 'regionDisks' , disk_name , region = region )
532+
533+ if not disk_dict :
459534 raise errors .ResourceNotFoundError (
460535 f'Disk { disk_name } was not found in project '
461536 f'{ self .project_id } ' , __name__ )
462- return disk
537+
538+ try :
539+ _ , disk_region = disk_dict ['region' ].rsplit ('/' , 1 )
540+ except ValueError as exception :
541+ raise errors .ResourceNotFoundError (
542+ f'Region not found for disk { disk_name } in project '
543+ f'{ self .project_id } ' , __name__ ) from exception
544+
545+ return GoogleRegionComputeDisk (
546+ self .project_id , disk_region , disk_dict ['name' ],
547+ resource_id = disk_dict ['id' ],
548+ labels = disk_dict .get ('labels' ))
463549
464550 def GetInstance (
465551 self ,
@@ -478,19 +564,28 @@ def GetInstance(
478564 Raises:
479565 ResourceNotFoundError: If instance does not exist.
480566 """
567+ instance_dict = self ._GetResourceFromComputeApi (
568+ 'instances' , instance_name , zone = zone )
481569
482- instances = self .Instances ()
483-
484- if re .match (RESOURCE_ID_REGEX , instance_name ):
485- instance = instances .get (instance_name )
486- else :
487- instance = self ._FindResourceByName (instances , instance_name , zone )
488-
489- if not instance :
570+ if not instance_dict :
490571 raise errors .ResourceNotFoundError (
491572 f'Instance { instance_name } was not found in project '
492573 f'{ self .project_id } ' , __name__ )
493- return instance
574+
575+ try :
576+ _ , instance_zone = instance_dict ['zone' ].rsplit ('/' , 1 )
577+ except ValueError as exception :
578+ raise errors .ResourceNotFoundError (
579+ f'Zone not found for instance { instance_name } in project '
580+ f'{ self .project_id } ' , __name__ ) from exception
581+
582+ return GoogleComputeInstance (
583+ self .project_id ,
584+ instance_zone ,
585+ instance_dict ['name' ],
586+ resource_id = instance_dict ['id' ],
587+ labels = instance_dict .get ('labels' ),
588+ deletion_protection = instance_dict .get ('deletionProtection' , False ))
494589
495590 def GetDisk (
496591 self ,
@@ -509,16 +604,26 @@ def GetDisk(
509604 ResourceNotFoundError: When the specified disk cannot be found in project.
510605 """
511606
512- disks = self .Disks ()
513- if re .match (RESOURCE_ID_REGEX , disk_name ):
514- disk = disks .get (disk_name )
515- else :
516- disk = self ._FindResourceByName (disks , disk_name , zone )
517- if not disk :
607+ disk_dict = self ._GetResourceFromComputeApi ('disks' , disk_name , zone = zone )
608+
609+ if not disk_dict :
518610 raise errors .ResourceNotFoundError (
519611 f'Disk { disk_name } was not found in project '
520612 f'{ self .project_id } ' , __name__ )
521- return disk
613+
614+ try :
615+ _ , disk_zone = disk_dict ['zone' ].rsplit ('/' , 1 )
616+ except ValueError as exception :
617+ raise errors .ResourceNotFoundError (
618+ f'Zone not found for disk { disk_name } in project '
619+ f'{ self .project_id } ' , __name__ ) from exception
620+
621+ return GoogleComputeDisk (
622+ self .project_id ,
623+ disk_zone ,
624+ disk_dict ['name' ],
625+ resource_id = disk_dict ['id' ],
626+ labels = disk_dict .get ('labels' ))
522627
523628 def CreateDiskFromSnapshot (
524629 self ,
@@ -1431,7 +1536,8 @@ def GetBootDisk(self) -> 'GoogleComputeDisk':
14311536 self .name ),
14321537 __name__ )
14331538 disk_name = disk ['source' ].split ('/' )[- 1 ]
1434- return GoogleCloudCompute (self .project_id ).GetDisk (disk_name = disk_name )
1539+ return GoogleCloudCompute (self .project_id ).GetDisk (
1540+ disk_name = disk_name , zone = self .zone )
14351541 raise errors .ResourceNotFoundError (
14361542 'Boot disk not found for instance {0:s}' .format (self .name ), __name__ )
14371543
@@ -1451,7 +1557,8 @@ def GetDisk(self, disk_name: str) -> 'GoogleComputeDisk':
14511557
14521558 for disk in self .GetValue ('disks' ):
14531559 if disk .get ('source' , '' ).split ('/' )[- 1 ] == disk_name :
1454- return GoogleCloudCompute (self .project_id ).GetDisk (disk_name = disk_name )
1560+ return GoogleCloudCompute (self .project_id ).GetDisk (
1561+ disk_name = disk_name , zone = self .zone )
14551562 raise errors .ResourceNotFoundError (
14561563 'Disk {0:s} was not found in instance {1:s}' .format (
14571564 disk_name , self .name ),
@@ -1640,7 +1747,8 @@ def Delete(
16401747
16411748 for disk_name in disks_to_delete :
16421749 try :
1643- disk = GoogleCloudCompute (self .project_id ).GetDisk (disk_name = disk_name )
1750+ disk = GoogleCloudCompute (self .project_id ).GetDisk (
1751+ disk_name = disk_name , zone = self .zone )
16441752 disk .Delete ()
16451753 except (errors .ResourceDeletionError , errors .ResourceNotFoundError ):
16461754 logger .info (
@@ -2333,7 +2441,7 @@ def ExportImage(
23332441 build_args .append ('-format={0:s}' .format (image_format ))
23342442 build_body = {
23352443 'timeout' : '86400s' ,
2336- 'steps' : [{
2444+ 'steps' : [{
23372445 'args' : build_args ,
23382446 'name' : 'gcr.io/compute-image-tools/gce_vm_image_export:release' ,
23392447 'env' : []
0 commit comments