gcpdiag.queries.gce

Queries related to GCP Compute Engine.
POSITIVE_BOOL_VALUES = {'YES', '1', 'Y', 'TRUE'}
DATAPROC_LABEL = 'goog-dataproc-cluster-name'
GKE_LABEL = 'goog-gke-node'
class InstanceTemplate(gcpdiag.models.Resource):
 40class InstanceTemplate(models.Resource):
 41  """Represents a GCE Instance Template."""
 42
 43  _resource_data: dict
 44
 45  def __init__(self, project_id, resource_data):
 46    super().__init__(project_id=project_id)
 47    self._resource_data = resource_data
 48
 49  @property
 50  def self_link(self) -> str:
 51    return self._resource_data['selfLink']
 52
 53  @property
 54  def full_path(self) -> str:
 55    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
 56    if result:
 57      return result.group(1)
 58    else:
 59      return f'>> {self.self_link}'
 60
 61  @property
 62  def short_path(self) -> str:
 63    path = self.project_id + '/' + self.name
 64    return path
 65
 66  @property
 67  def name(self) -> str:
 68    return self._resource_data['name']
 69
 70  @property
 71  def tags(self) -> List[str]:
 72    return self._resource_data['properties'].get('tags', {}).get('items', [])
 73
 74  @property
 75  def service_account(self) -> Optional[str]:
 76    sa_list = self._resource_data['properties'].get('serviceAccounts', [])
 77    if not sa_list:
 78      return None
 79    email = sa_list[0]['email']
 80    if email == 'default':
 81      project_nr = crm.get_project(self._project_id).number
 82      return f'{project_nr}-compute@developer.gserviceaccount.com'
 83    return email
 84
 85  @property
 86  def network(self) -> network_q.Network:
 87    return network_q.get_network_from_url(
 88      self._resource_data['properties']['networkInterfaces'][0]['network']
 89    )
 90
 91  @property
 92  def subnetwork(self) -> network_q.Subnetwork:
 93    subnet_url = self._resource_data['properties']['networkInterfaces'][0]['subnetwork']
 94    return self.network.subnetworks[subnet_url]
 95
 96  def get_metadata(self, key: str) -> str:
 97    for item in self._resource_data['properties']['metadata']['items']:
 98      if item['key'] == key:
 99        return item['value']
100    return ''

Represents a GCE Instance Template.

InstanceTemplate(project_id, resource_data)
45  def __init__(self, project_id, resource_data):
46    super().__init__(project_id=project_id)
47    self._resource_data = resource_data
full_path: str
53  @property
54  def full_path(self) -> str:
55    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
56    if result:
57      return result.group(1)
58    else:
59      return f'>> {self.self_link}'

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

short_path: str
61  @property
62  def short_path(self) -> str:
63    path = self.project_id + '/' + self.name
64    return path

Returns the short name for this resource.

Note that it isn't clear from this name what kind of resource it is.

Example: 'gke1'

name: str
66  @property
67  def name(self) -> str:
68    return self._resource_data['name']
tags: List[str]
70  @property
71  def tags(self) -> List[str]:
72    return self._resource_data['properties'].get('tags', {}).get('items', [])
service_account: Optional[str]
74  @property
75  def service_account(self) -> Optional[str]:
76    sa_list = self._resource_data['properties'].get('serviceAccounts', [])
77    if not sa_list:
78      return None
79    email = sa_list[0]['email']
80    if email == 'default':
81      project_nr = crm.get_project(self._project_id).number
82      return f'{project_nr}-compute@developer.gserviceaccount.com'
83    return email
network: gcpdiag.queries.network.Network
85  @property
86  def network(self) -> network_q.Network:
87    return network_q.get_network_from_url(
88      self._resource_data['properties']['networkInterfaces'][0]['network']
89    )
subnetwork: gcpdiag.queries.network.Subnetwork
91  @property
92  def subnetwork(self) -> network_q.Subnetwork:
93    subnet_url = self._resource_data['properties']['networkInterfaces'][0]['subnetwork']
94    return self.network.subnetworks[subnet_url]
def get_metadata(self, key: str) -> str:
 96  def get_metadata(self, key: str) -> str:
 97    for item in self._resource_data['properties']['metadata']['items']:
 98      if item['key'] == key:
 99        return item['value']
100    return ''
class InstanceGroup(gcpdiag.models.Resource):
103class InstanceGroup(models.Resource):
104  """Represents a GCE instance group."""
105
106  _resource_data: dict
107
108  def __init__(self, project_id, resource_data):
109    super().__init__(project_id=project_id)
110    self._resource_data = resource_data
111
112  @property
113  def full_path(self) -> str:
114    result = re.match(
115      r'https://www.googleapis.com/compute/v1/(.*)',
116      self._resource_data['selfLink'],
117    )
118    if result:
119      return result.group(1)
120    else:
121      return '>> ' + self._resource_data['selfLink']
122
123  @property
124  def short_path(self) -> str:
125    path = self.project_id + '/' + self.name
126    return path
127
128  @property
129  def self_link(self) -> str:
130    return self._resource_data['selfLink']
131
132  @property
133  def name(self) -> str:
134    return self._resource_data['name']
135
136  @property
137  def named_ports(self) -> List[dict]:
138    if 'namedPorts' in self._resource_data:
139      return self._resource_data['namedPorts']
140    return []
141
142  def has_named_ports(self) -> bool:
143    if 'namedPorts' in self._resource_data:
144      return True
145    return False

Represents a GCE instance group.

InstanceGroup(project_id, resource_data)
108  def __init__(self, project_id, resource_data):
109    super().__init__(project_id=project_id)
110    self._resource_data = resource_data
full_path: str
112  @property
113  def full_path(self) -> str:
114    result = re.match(
115      r'https://www.googleapis.com/compute/v1/(.*)',
116      self._resource_data['selfLink'],
117    )
118    if result:
119      return result.group(1)
120    else:
121      return '>> ' + self._resource_data['selfLink']

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

short_path: str
123  @property
124  def short_path(self) -> str:
125    path = self.project_id + '/' + self.name
126    return path

Returns the short name for this resource.

Note that it isn't clear from this name what kind of resource it is.

Example: 'gke1'

name: str
132  @property
133  def name(self) -> str:
134    return self._resource_data['name']
named_ports: List[dict]
136  @property
137  def named_ports(self) -> List[dict]:
138    if 'namedPorts' in self._resource_data:
139      return self._resource_data['namedPorts']
140    return []
def has_named_ports(self) -> bool:
142  def has_named_ports(self) -> bool:
143    if 'namedPorts' in self._resource_data:
144      return True
145    return False
class ManagedInstanceGroup(gcpdiag.models.Resource):
148class ManagedInstanceGroup(models.Resource):
149  """Represents a GCE managed instance group."""
150
151  _resource_data: dict
152  _region: Optional[str]
153
154  def __init__(self, project_id, resource_data):
155    super().__init__(project_id=project_id)
156    self._resource_data = resource_data
157    self._region = None
158
159  @property
160  def full_path(self) -> str:
161    result = re.match(
162      r'https://www.googleapis.com/compute/v1/(.*)',
163      self._resource_data['selfLink'],
164    )
165    if result:
166      return result.group(1)
167    else:
168      return '>> ' + self._resource_data['selfLink']
169
170  @property
171  def short_path(self) -> str:
172    path = self.project_id + '/' + self.name
173    return path
174
175  def is_gke(self) -> bool:
176    """Is this managed instance group part of a GKE cluster?
177
178    Note that the results are based on heuristics (the mig name),
179    which is not ideal.
180
181    Returns:
182        bool: True if this managed instance group is part of a GKE cluster.
183    """
184
185    # gke- is normal GKE, gk3- is GKE autopilot
186    return self.name.startswith('gke-') or self.name.startswith('gk3-')
187
188  @property
189  def self_link(self) -> str:
190    return self._resource_data['selfLink']
191
192  @property
193  def name(self) -> str:
194    return self._resource_data['name']
195
196  @property
197  def region(self) -> str:
198    if self._region is None:
199      if 'region' in self._resource_data:
200        m = re.search(r'/regions/([^/]+)$', self._resource_data['region'])
201        if not m:
202          raise RuntimeError(
203            "can't determine region of mig {} ({})".format(self.name, self._resource_data['region'])
204          )
205        self._region = m.group(1)
206      elif 'zone' in self._resource_data:
207        m = re.search(r'/zones/([^/]+)$', self._resource_data['zone'])
208        if not m:
209          raise RuntimeError(
210            "can't determine region of mig {} ({})".format(self.name, self._resource_data['region'])
211          )
212        zone = m.group(1)
213        self._region = utils.zone_region(zone)
214      else:
215        raise RuntimeError(
216          f"can't determine region of mig {self.name}, both region and zone aren't set!"
217        )
218    return self._region
219
220  @property
221  def zone(self) -> Optional[str]:
222    if 'zone' in self._resource_data:
223      m = re.search(r'/zones/([^/]+)$', self._resource_data['zone'])
224      if not m:
225        raise RuntimeError(
226          "can't determine zone of mig {} ({})".format(self.name, self._resource_data['zone'])
227        )
228      return m.group(1)
229    return None
230
231  def count_no_action_instances(self) -> int:
232    """number of instances in the mig that are running and have no scheduled actions."""
233    return self._resource_data['currentActions']['none']
234
235  def is_instance_member(self, project_id: str, region: str, instance_name: str):
236    """Given the project_id, region and instance name, is it a member of this MIG?"""
237    return (
238      self.project_id == project_id
239      and self.region == region
240      and instance_name.startswith(self._resource_data['baseInstanceName'])
241    )
242
243  @property
244  def template(self) -> InstanceTemplate:
245    if 'instanceTemplate' not in self._resource_data:
246      raise RuntimeError('instanceTemplate not set for MIG {self.name}')
247
248    m = re.match(
249      r'https://www.googleapis.com/compute/v1/(.*)',
250      self._resource_data['instanceTemplate'],
251    )
252
253    if not m:
254      raise RuntimeError(
255        "can't parse instanceTemplate: %s" % self._resource_data['instanceTemplate']
256      )
257    template_self_link = m.group(1)
258    templates = get_instance_templates(self.project_id)
259    if template_self_link not in templates:
260      raise RuntimeError(f'instanceTemplate {template_self_link} for MIG {self.name} not found')
261    return templates[template_self_link]
262
263  @property
264  def version_target_reached(self) -> bool:
265    return get_path(self._resource_data, ('status', 'versionTarget', 'isReached'))
266
267  def get(self, path: str, default: Any = None) -> Any:
268    """Gets a value from resource_data using a dot-separated path."""
269    return get_path(self._resource_data, tuple(path.split('.')), default=default)

Represents a GCE managed instance group.

ManagedInstanceGroup(project_id, resource_data)
154  def __init__(self, project_id, resource_data):
155    super().__init__(project_id=project_id)
156    self._resource_data = resource_data
157    self._region = None
full_path: str
159  @property
160  def full_path(self) -> str:
161    result = re.match(
162      r'https://www.googleapis.com/compute/v1/(.*)',
163      self._resource_data['selfLink'],
164    )
165    if result:
166      return result.group(1)
167    else:
168      return '>> ' + self._resource_data['selfLink']

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

short_path: str
170  @property
171  def short_path(self) -> str:
172    path = self.project_id + '/' + self.name
173    return path

Returns the short name for this resource.

Note that it isn't clear from this name what kind of resource it is.

Example: 'gke1'

def is_gke(self) -> bool:
175  def is_gke(self) -> bool:
176    """Is this managed instance group part of a GKE cluster?
177
178    Note that the results are based on heuristics (the mig name),
179    which is not ideal.
180
181    Returns:
182        bool: True if this managed instance group is part of a GKE cluster.
183    """
184
185    # gke- is normal GKE, gk3- is GKE autopilot
186    return self.name.startswith('gke-') or self.name.startswith('gk3-')

Is this managed instance group part of a GKE cluster?

Note that the results are based on heuristics (the mig name), which is not ideal.

Returns:

bool: True if this managed instance group is part of a GKE cluster.

name: str
192  @property
193  def name(self) -> str:
194    return self._resource_data['name']
region: str
196  @property
197  def region(self) -> str:
198    if self._region is None:
199      if 'region' in self._resource_data:
200        m = re.search(r'/regions/([^/]+)$', self._resource_data['region'])
201        if not m:
202          raise RuntimeError(
203            "can't determine region of mig {} ({})".format(self.name, self._resource_data['region'])
204          )
205        self._region = m.group(1)
206      elif 'zone' in self._resource_data:
207        m = re.search(r'/zones/([^/]+)$', self._resource_data['zone'])
208        if not m:
209          raise RuntimeError(
210            "can't determine region of mig {} ({})".format(self.name, self._resource_data['region'])
211          )
212        zone = m.group(1)
213        self._region = utils.zone_region(zone)
214      else:
215        raise RuntimeError(
216          f"can't determine region of mig {self.name}, both region and zone aren't set!"
217        )
218    return self._region
zone: Optional[str]
220  @property
221  def zone(self) -> Optional[str]:
222    if 'zone' in self._resource_data:
223      m = re.search(r'/zones/([^/]+)$', self._resource_data['zone'])
224      if not m:
225        raise RuntimeError(
226          "can't determine zone of mig {} ({})".format(self.name, self._resource_data['zone'])
227        )
228      return m.group(1)
229    return None
def count_no_action_instances(self) -> int:
231  def count_no_action_instances(self) -> int:
232    """number of instances in the mig that are running and have no scheduled actions."""
233    return self._resource_data['currentActions']['none']

number of instances in the mig that are running and have no scheduled actions.

def is_instance_member(self, project_id: str, region: str, instance_name: str):
235  def is_instance_member(self, project_id: str, region: str, instance_name: str):
236    """Given the project_id, region and instance name, is it a member of this MIG?"""
237    return (
238      self.project_id == project_id
239      and self.region == region
240      and instance_name.startswith(self._resource_data['baseInstanceName'])
241    )

Given the project_id, region and instance name, is it a member of this MIG?

template: InstanceTemplate
243  @property
244  def template(self) -> InstanceTemplate:
245    if 'instanceTemplate' not in self._resource_data:
246      raise RuntimeError('instanceTemplate not set for MIG {self.name}')
247
248    m = re.match(
249      r'https://www.googleapis.com/compute/v1/(.*)',
250      self._resource_data['instanceTemplate'],
251    )
252
253    if not m:
254      raise RuntimeError(
255        "can't parse instanceTemplate: %s" % self._resource_data['instanceTemplate']
256      )
257    template_self_link = m.group(1)
258    templates = get_instance_templates(self.project_id)
259    if template_self_link not in templates:
260      raise RuntimeError(f'instanceTemplate {template_self_link} for MIG {self.name} not found')
261    return templates[template_self_link]
version_target_reached: bool
263  @property
264  def version_target_reached(self) -> bool:
265    return get_path(self._resource_data, ('status', 'versionTarget', 'isReached'))
def get(self, path: str, default: Any = None) -> Any:
267  def get(self, path: str, default: Any = None) -> Any:
268    """Gets a value from resource_data using a dot-separated path."""
269    return get_path(self._resource_data, tuple(path.split('.')), default=default)

Gets a value from resource_data using a dot-separated path.

class Autoscaler(gcpdiag.models.Resource):
272class Autoscaler(models.Resource):
273  """Represents a GCE Autoscaler."""
274
275  _resource_data: dict
276
277  def __init__(self, project_id, resource_data):
278    super().__init__(project_id=project_id)
279    self._resource_data = resource_data
280
281  @property
282  def self_link(self) -> str:
283    return self._resource_data['selfLink']
284
285  @property
286  def full_path(self) -> str:
287    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
288    if result:
289      return result.group(1)
290    else:
291      return f'>> {self.self_link}'
292
293  @property
294  def name(self) -> str:
295    return self._resource_data['name']
296
297  def get(self, path: str, default: Any = None) -> Any:
298    """Gets a value from resource_data using a dot-separated path."""
299    return get_path(self._resource_data, tuple(path.split('.')), default=default)

Represents a GCE Autoscaler.

Autoscaler(project_id, resource_data)
277  def __init__(self, project_id, resource_data):
278    super().__init__(project_id=project_id)
279    self._resource_data = resource_data
full_path: str
285  @property
286  def full_path(self) -> str:
287    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
288    if result:
289      return result.group(1)
290    else:
291      return f'>> {self.self_link}'

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

name: str
293  @property
294  def name(self) -> str:
295    return self._resource_data['name']
def get(self, path: str, default: Any = None) -> Any:
297  def get(self, path: str, default: Any = None) -> Any:
298    """Gets a value from resource_data using a dot-separated path."""
299    return get_path(self._resource_data, tuple(path.split('.')), default=default)

Gets a value from resource_data using a dot-separated path.

class SerialPortOutput:
302class SerialPortOutput:
303  """Represents the full Serial Port Output (/dev/ttyS0 or COM1) of an instance.
304
305  contents is the full 1MB of the instance.
306  """
307
308  _project_id: str
309  _instance_id: str
310  _contents: List[str]
311
312  def __init__(self, project_id, instance_id, contents):
313    self._project_id = project_id
314    self._instance_id = instance_id
315    self._contents = contents
316
317  @property
318  def contents(self) -> List[str]:
319    return self._contents
320
321  @property
322  def instance_id(self) -> str:
323    return self._instance_id

Represents the full Serial Port Output (/dev/ttyS0 or COM1) of an instance.

contents is the full 1MB of the instance.

SerialPortOutput(project_id, instance_id, contents)
312  def __init__(self, project_id, instance_id, contents):
313    self._project_id = project_id
314    self._instance_id = instance_id
315    self._contents = contents
contents: List[str]
317  @property
318  def contents(self) -> List[str]:
319    return self._contents
instance_id: str
321  @property
322  def instance_id(self) -> str:
323    return self._instance_id
class Instance(gcpdiag.models.Resource):
326class Instance(models.Resource):
327  """Represents a GCE instance."""
328
329  _resource_data: dict
330  _region: Optional[str]
331
332  def __init__(self, project_id, resource_data):
333    super().__init__(project_id=project_id)
334    self._resource_data = resource_data
335    self._metadata_dict = None
336    self._region = None
337
338  @property
339  def id(self) -> str:
340    return self._resource_data['id']
341
342  @property
343  def name(self) -> str:
344    return self._resource_data['name']
345
346  @property
347  def full_path(self) -> str:
348    result = re.match(
349      r'https://www.googleapis.com/compute/v1/(.*)',
350      self._resource_data['selfLink'],
351    )
352    if result:
353      return result.group(1)
354    else:
355      return '>> ' + self._resource_data['selfLink']
356
357  @property
358  def short_path(self) -> str:
359    # Note: instance names must be unique per project,
360    # so no need to add the zone.
361    path = self.project_id + '/' + self.name
362    return path
363
364  @property
365  def creation_timestamp(self) -> datetime:
366    """VM creation time, as a *naive* `datetime` object."""
367    return (
368      datetime.fromisoformat(self._resource_data['creationTimestamp'])
369      .astimezone(timezone.utc)
370      .replace(tzinfo=None)
371    )
372
373  @property
374  def region(self) -> str:
375    if self._region is None:
376      if 'zone' in self._resource_data:
377        m = re.search(r'/zones/([^/]+)$', self._resource_data['zone'])
378        if not m:
379          raise RuntimeError(
380            "can't determine region of instance %s (%s)"
381            % (self.name, self._resource_data['region'])
382          )
383        zone = m.group(1)
384        self._region = utils.zone_region(zone)
385      else:
386        raise RuntimeError(f"can't determine region of instance {self.name}, zone isn't set!")
387    return self._region
388
389  @property
390  def zone(self) -> str:
391    zone_uri = self._resource_data['zone']
392    m = re.search(r'/zones/([^/]+)$', zone_uri)
393    if m:
394      return m.group(1)
395    else:
396      raise RuntimeError(f"can't determine zone of instance {self.name}")
397
398  @property
399  def disks(self) -> List[dict]:
400    if 'disks' in self._resource_data:
401      return self._resource_data['disks']
402    return []
403
404  @property
405  def boot_disk_licenses(self) -> List[str]:
406    """Returns license names associated with boot disk."""
407    for disk in self.disks:
408      if disk.get('boot'):
409        return [
410          license_str.partition('/global/licenses/')[2] for license_str in disk.get('licenses', [])
411        ]
412    return []
413
414  @property
415  def guest_os_features(self) -> List[str]:
416    """Returns guestOsFeatures types associated with boot disk."""
417    for disk in self.disks:
418      if disk.get('boot'):
419        return [f['type'] for f in disk.get('guestOsFeatures', [])]
420    return []
421
422  @property
423  def startrestricted(self) -> bool:
424    return self._resource_data['startRestricted']
425
426  def laststarttimestamp(self) -> str:
427    return self._resource_data['lastStartTimestamp']
428
429  def laststoptimestamp(self) -> str:
430    if 'lastStopTimestamp' in self._resource_data:
431      return self._resource_data['lastStopTimestamp']
432    return ''
433
434  def is_serial_port_logging_enabled(self) -> bool:
435    value = self.get_metadata('serial-port-logging-enable')
436    return bool(value and value.upper() in POSITIVE_BOOL_VALUES)
437
438  def is_oslogin_enabled(self) -> bool:
439    value = self.get_metadata('enable-oslogin')
440    return bool(value and value.upper() in POSITIVE_BOOL_VALUES)
441
442  def is_metadata_enabled(self, metadata_name) -> bool:
443    """Use to check for common boolean metadata value"""
444    value = self.get_metadata(metadata_name)
445    return bool(value and value.upper() in POSITIVE_BOOL_VALUES)
446
447  def has_label(self, label) -> bool:
448    return label in self.labels
449
450  def is_dataproc_instance(self) -> bool:
451    return self.has_label(DATAPROC_LABEL)
452
453  def is_gke_node(self) -> bool:
454    return self.has_label(GKE_LABEL)
455
456  @property
457  def is_preemptible_vm(self) -> bool:
458    return (
459      'scheduling' in self._resource_data
460      and 'preemptible' in self._resource_data['scheduling']
461      and self._resource_data['scheduling']['preemptible']
462    )
463
464  def min_cpu_platform(self) -> str:
465    if 'minCpuPlatform' in self._resource_data:
466      return self._resource_data['minCpuPlatform']
467    return 'None'
468
469  @property
470  def created_by_mig(self) -> bool:
471    """Return bool indicating if the instance part of a mig.
472
473    MIG which were part of MIG however have been removed or terminated will
474    return True.
475    """
476    created_by = self.get_metadata('created-by')
477    if created_by is None:
478      return False
479
480    created_by_match = re.match(
481      r'projects/([^/]+)/((?:regions|zones)/[^/]+/instanceGroupManagers/[^/]+)$',
482      created_by,
483    )
484    if not created_by_match:
485      return False
486    return True
487
488  def is_windows_machine(self) -> bool:
489    if 'disks' in self._resource_data:
490      disks = next(iter(self._resource_data['disks']))
491      if 'guestOsFeatures' in disks:
492        if 'WINDOWS' in [t['type'] for t in iter(disks['guestOsFeatures'])]:
493          return True
494    return False
495
496  def is_public_machine(self) -> bool:
497    if 'networkInterfaces' in self._resource_data:
498      return 'natIP' in str(self._resource_data['networkInterfaces'])
499    return False
500
501  def machine_type(self):
502    if 'machineType' in self._resource_data:
503      # return self._resource_data['machineType']
504      machine_type_uri = self._resource_data['machineType']
505      mt = re.search(r'/machineTypes/([^/]+)$', machine_type_uri)
506      if mt:
507        return mt.group(1)
508      else:
509        raise RuntimeError(f"can't determine machineType of instance {self.name}")
510    return None
511
512  def check_license(self, licenses: List[str]) -> bool:
513    """Checks that a license is contained in a given license list."""
514    if 'disks' in self._resource_data:
515      for disk in self._resource_data['disks']:
516        if 'license' in str(disk):
517          for license_ in licenses:
518            for attached_license in disk['licenses']:
519              if license_ == attached_license.partition('/global/licenses/')[2]:
520                return True
521    return False
522
523  def get_boot_disk_image(self) -> str:
524    """Get VM's boot disk image."""
525    boot_disk_image: str = ''
526    for disk in self.disks:
527      if disk.get('boot', False):
528        disk_source = disk.get('source', '')
529        m = re.search(r'/disks/([^/]+)$', disk_source)
530        if not m:
531          raise RuntimeError(f"can't determine name of boot disk {disk_source}")
532        disk_name = m.group(1)
533        gce_disk: Disk = get_disk(self.project_id, zone=self.zone, disk_name=disk_name)
534        return gce_disk.source_image
535    return boot_disk_image
536
537  @property
538  def is_sole_tenant_vm(self) -> bool:
539    return bool('nodeAffinities' in self._resource_data['scheduling'])
540
541  @property
542  def network(self) -> network_q.Network:
543    # 'https://www.googleapis.com/compute/v1/projects/gcpdiag-gce1-aaaa/global/networks/default'
544    network_string = self._resource_data['networkInterfaces'][0]['network']
545    m = re.match(r'^.+/projects/([^/]+)/global/networks/([^/]+)$', network_string)
546    if not m:
547      raise RuntimeError("can't parse network string: %s" % network_string)
548    return network_q.get_network(
549      m.group(1), m.group(2), context=models.Context(project_id=m.group(1))
550    )
551
552  @property
553  def network_ips(self) -> List[network_q.IPv4AddrOrIPv6Addr]:
554    return [
555      ipaddress.ip_address(nic['networkIP']) for nic in self._resource_data['networkInterfaces']
556    ]
557
558  @property
559  def get_network_interfaces(self):
560    return self._resource_data['networkInterfaces']
561
562  @property
563  def subnetworks(self) -> List[network_q.Subnetwork]:
564    subnetworks = []
565    for nic in self._resource_data['networkInterfaces']:
566      subnetworks.append(network_q.get_subnetwork_from_url(nic['subnetwork']))
567    return subnetworks
568
569  @property
570  def routes(self) -> List[network_q.Route]:
571    routes = []
572    for nic in self._resource_data['networkInterfaces']:
573      for route in network_q.get_routes(self.project_id):
574        if nic['network'] == route.network:
575          if route.tags == []:
576            routes.append(route)
577            continue
578          else:
579            temp = [x for x in self.tags if x in route.tags]
580            if len(temp) > 0:
581              routes.append(route)
582    return routes
583
584  def get_network_ip_for_instance_interface(
585    self, network: str
586  ) -> Optional[network_q.IPv4NetOrIPv6Net]:
587    """Get the network ip for a nic given a network name."""
588    for nic in self._resource_data['networkInterfaces']:
589      if nic.get('network') == network:
590        return ipaddress.ip_network(nic.get('networkIP'))
591    return None
592
593  def secure_boot_enabled(self) -> bool:
594    if 'shieldedInstanceConfig' in self._resource_data:
595      return self._resource_data['shieldedInstanceConfig']['enableSecureBoot']
596    return False
597
598  @property
599  def access_scopes(self) -> List[str]:
600    if 'serviceAccounts' in self._resource_data:
601      saccts = self._resource_data['serviceAccounts']
602      if isinstance(saccts, list) and len(saccts) >= 1:
603        return saccts[0].get('scopes', [])
604    return []
605
606  @property
607  def service_account(self) -> Optional[str]:
608    if 'serviceAccounts' in self._resource_data:
609      saccts = self._resource_data['serviceAccounts']
610      if isinstance(saccts, list) and len(saccts) >= 1:
611        return saccts[0]['email']
612    return None
613
614  @property
615  def tags(self) -> List[str]:
616    if 'tags' in self._resource_data:
617      if 'items' in self._resource_data['tags']:
618        return self._resource_data['tags']['items']
619    return []
620
621  def get_metadata(self, key: str) -> str:
622    if not self._metadata_dict:
623      self._metadata_dict = {}
624      if 'metadata' in self._resource_data and 'items' in self._resource_data['metadata']:
625        for item in self._resource_data['metadata']['items']:
626          if 'key' in item and 'value' in item:
627            self._metadata_dict[item['key']] = item['value']
628    project_metadata = get_project_metadata(self.project_id)
629    return self._metadata_dict.get(key, project_metadata.get(key))
630
631  @property
632  def status(self) -> str:
633    """VM Status."""
634    return self._resource_data.get('status', None)
635
636  @property
637  def is_running(self) -> bool:
638    """VM Status is indicated as running."""
639    return self._resource_data.get('status', False) == 'RUNNING'
640
641  @property
642  def network_interface_count(self) -> int:
643    """Returns the number of network interfaces attached to the instance."""
644    return len(self._resource_data.get('networkInterfaces', []))
645
646  @property  # type: ignore
647  @caching.cached_api_call(in_memory=True)
648  def mig(self) -> ManagedInstanceGroup:
649    """Return ManagedInstanceGroup that owns this instance.
650
651    Throws AttributeError in case it isn't MIG-managed.
652    """
653
654    created_by = self.get_metadata('created-by')
655    if created_by is None:
656      raise AttributeError(f'instance {self.id} is not managed by a mig')
657
658    # Example created-by:
659    # "projects/12340002/zones/europe-west4-a/instanceGroupManagers/gke-gke1-default-pool-e5e20a34-grp"
660    # (note how it uses a project number and not a project id...)
661    created_by_match = re.match(
662      r'projects/([^/]+)/((?:regions|zones)/[^/]+/instanceGroupManagers/[^/]+)$',
663      created_by,
664    )
665    if not created_by_match:
666      raise AttributeError(f'instance {self.id} is not managed by a mig (created-by={created_by})')
667    project = crm.get_project(created_by_match.group(1))
668
669    mig_self_link = (
670      f'https://www.googleapis.com/compute/v1/projects/{project.id}/{created_by_match.group(2)}'
671    )
672
673    # Try to find a matching mig.
674    context = models.Context(project_id=self.project_id)
675    all_migs = list(get_managed_instance_groups(context).values()) + list(
676      get_region_managed_instance_groups(context).values()
677    )
678
679    for mig in all_migs:
680      if mig.self_link == mig_self_link:
681        return mig
682
683    raise AttributeError(f'MIG not found for instance {self.id}. Created by: {created_by}')
684
685  @property
686  def labels(self) -> dict:
687    return self._resource_data.get('labels', {})

Represents a GCE instance.

Instance(project_id, resource_data)
332  def __init__(self, project_id, resource_data):
333    super().__init__(project_id=project_id)
334    self._resource_data = resource_data
335    self._metadata_dict = None
336    self._region = None
id: str
338  @property
339  def id(self) -> str:
340    return self._resource_data['id']
name: str
342  @property
343  def name(self) -> str:
344    return self._resource_data['name']
full_path: str
346  @property
347  def full_path(self) -> str:
348    result = re.match(
349      r'https://www.googleapis.com/compute/v1/(.*)',
350      self._resource_data['selfLink'],
351    )
352    if result:
353      return result.group(1)
354    else:
355      return '>> ' + self._resource_data['selfLink']

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

short_path: str
357  @property
358  def short_path(self) -> str:
359    # Note: instance names must be unique per project,
360    # so no need to add the zone.
361    path = self.project_id + '/' + self.name
362    return path

Returns the short name for this resource.

Note that it isn't clear from this name what kind of resource it is.

Example: 'gke1'

creation_timestamp: datetime.datetime
364  @property
365  def creation_timestamp(self) -> datetime:
366    """VM creation time, as a *naive* `datetime` object."""
367    return (
368      datetime.fromisoformat(self._resource_data['creationTimestamp'])
369      .astimezone(timezone.utc)
370      .replace(tzinfo=None)
371    )

VM creation time, as a naive datetime object.

region: str
373  @property
374  def region(self) -> str:
375    if self._region is None:
376      if 'zone' in self._resource_data:
377        m = re.search(r'/zones/([^/]+)$', self._resource_data['zone'])
378        if not m:
379          raise RuntimeError(
380            "can't determine region of instance %s (%s)"
381            % (self.name, self._resource_data['region'])
382          )
383        zone = m.group(1)
384        self._region = utils.zone_region(zone)
385      else:
386        raise RuntimeError(f"can't determine region of instance {self.name}, zone isn't set!")
387    return self._region
zone: str
389  @property
390  def zone(self) -> str:
391    zone_uri = self._resource_data['zone']
392    m = re.search(r'/zones/([^/]+)$', zone_uri)
393    if m:
394      return m.group(1)
395    else:
396      raise RuntimeError(f"can't determine zone of instance {self.name}")
disks: List[dict]
398  @property
399  def disks(self) -> List[dict]:
400    if 'disks' in self._resource_data:
401      return self._resource_data['disks']
402    return []
boot_disk_licenses: List[str]
404  @property
405  def boot_disk_licenses(self) -> List[str]:
406    """Returns license names associated with boot disk."""
407    for disk in self.disks:
408      if disk.get('boot'):
409        return [
410          license_str.partition('/global/licenses/')[2] for license_str in disk.get('licenses', [])
411        ]
412    return []

Returns license names associated with boot disk.

guest_os_features: List[str]
414  @property
415  def guest_os_features(self) -> List[str]:
416    """Returns guestOsFeatures types associated with boot disk."""
417    for disk in self.disks:
418      if disk.get('boot'):
419        return [f['type'] for f in disk.get('guestOsFeatures', [])]
420    return []

Returns guestOsFeatures types associated with boot disk.

startrestricted: bool
422  @property
423  def startrestricted(self) -> bool:
424    return self._resource_data['startRestricted']
def laststarttimestamp(self) -> str:
426  def laststarttimestamp(self) -> str:
427    return self._resource_data['lastStartTimestamp']
def laststoptimestamp(self) -> str:
429  def laststoptimestamp(self) -> str:
430    if 'lastStopTimestamp' in self._resource_data:
431      return self._resource_data['lastStopTimestamp']
432    return ''
def is_serial_port_logging_enabled(self) -> bool:
434  def is_serial_port_logging_enabled(self) -> bool:
435    value = self.get_metadata('serial-port-logging-enable')
436    return bool(value and value.upper() in POSITIVE_BOOL_VALUES)
def is_oslogin_enabled(self) -> bool:
438  def is_oslogin_enabled(self) -> bool:
439    value = self.get_metadata('enable-oslogin')
440    return bool(value and value.upper() in POSITIVE_BOOL_VALUES)
def is_metadata_enabled(self, metadata_name) -> bool:
442  def is_metadata_enabled(self, metadata_name) -> bool:
443    """Use to check for common boolean metadata value"""
444    value = self.get_metadata(metadata_name)
445    return bool(value and value.upper() in POSITIVE_BOOL_VALUES)

Use to check for common boolean metadata value

def has_label(self, label) -> bool:
447  def has_label(self, label) -> bool:
448    return label in self.labels
def is_dataproc_instance(self) -> bool:
450  def is_dataproc_instance(self) -> bool:
451    return self.has_label(DATAPROC_LABEL)
def is_gke_node(self) -> bool:
453  def is_gke_node(self) -> bool:
454    return self.has_label(GKE_LABEL)
is_preemptible_vm: bool
456  @property
457  def is_preemptible_vm(self) -> bool:
458    return (
459      'scheduling' in self._resource_data
460      and 'preemptible' in self._resource_data['scheduling']
461      and self._resource_data['scheduling']['preemptible']
462    )
def min_cpu_platform(self) -> str:
464  def min_cpu_platform(self) -> str:
465    if 'minCpuPlatform' in self._resource_data:
466      return self._resource_data['minCpuPlatform']
467    return 'None'
created_by_mig: bool
469  @property
470  def created_by_mig(self) -> bool:
471    """Return bool indicating if the instance part of a mig.
472
473    MIG which were part of MIG however have been removed or terminated will
474    return True.
475    """
476    created_by = self.get_metadata('created-by')
477    if created_by is None:
478      return False
479
480    created_by_match = re.match(
481      r'projects/([^/]+)/((?:regions|zones)/[^/]+/instanceGroupManagers/[^/]+)$',
482      created_by,
483    )
484    if not created_by_match:
485      return False
486    return True

Return bool indicating if the instance part of a mig.

MIG which were part of MIG however have been removed or terminated will return True.

def is_windows_machine(self) -> bool:
488  def is_windows_machine(self) -> bool:
489    if 'disks' in self._resource_data:
490      disks = next(iter(self._resource_data['disks']))
491      if 'guestOsFeatures' in disks:
492        if 'WINDOWS' in [t['type'] for t in iter(disks['guestOsFeatures'])]:
493          return True
494    return False
def is_public_machine(self) -> bool:
496  def is_public_machine(self) -> bool:
497    if 'networkInterfaces' in self._resource_data:
498      return 'natIP' in str(self._resource_data['networkInterfaces'])
499    return False
def machine_type(self):
501  def machine_type(self):
502    if 'machineType' in self._resource_data:
503      # return self._resource_data['machineType']
504      machine_type_uri = self._resource_data['machineType']
505      mt = re.search(r'/machineTypes/([^/]+)$', machine_type_uri)
506      if mt:
507        return mt.group(1)
508      else:
509        raise RuntimeError(f"can't determine machineType of instance {self.name}")
510    return None
def check_license(self, licenses: List[str]) -> bool:
512  def check_license(self, licenses: List[str]) -> bool:
513    """Checks that a license is contained in a given license list."""
514    if 'disks' in self._resource_data:
515      for disk in self._resource_data['disks']:
516        if 'license' in str(disk):
517          for license_ in licenses:
518            for attached_license in disk['licenses']:
519              if license_ == attached_license.partition('/global/licenses/')[2]:
520                return True
521    return False

Checks that a license is contained in a given license list.

def get_boot_disk_image(self) -> str:
523  def get_boot_disk_image(self) -> str:
524    """Get VM's boot disk image."""
525    boot_disk_image: str = ''
526    for disk in self.disks:
527      if disk.get('boot', False):
528        disk_source = disk.get('source', '')
529        m = re.search(r'/disks/([^/]+)$', disk_source)
530        if not m:
531          raise RuntimeError(f"can't determine name of boot disk {disk_source}")
532        disk_name = m.group(1)
533        gce_disk: Disk = get_disk(self.project_id, zone=self.zone, disk_name=disk_name)
534        return gce_disk.source_image
535    return boot_disk_image

Get VM's boot disk image.

is_sole_tenant_vm: bool
537  @property
538  def is_sole_tenant_vm(self) -> bool:
539    return bool('nodeAffinities' in self._resource_data['scheduling'])
network: gcpdiag.queries.network.Network
541  @property
542  def network(self) -> network_q.Network:
543    # 'https://www.googleapis.com/compute/v1/projects/gcpdiag-gce1-aaaa/global/networks/default'
544    network_string = self._resource_data['networkInterfaces'][0]['network']
545    m = re.match(r'^.+/projects/([^/]+)/global/networks/([^/]+)$', network_string)
546    if not m:
547      raise RuntimeError("can't parse network string: %s" % network_string)
548    return network_q.get_network(
549      m.group(1), m.group(2), context=models.Context(project_id=m.group(1))
550    )
network_ips: List[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]
552  @property
553  def network_ips(self) -> List[network_q.IPv4AddrOrIPv6Addr]:
554    return [
555      ipaddress.ip_address(nic['networkIP']) for nic in self._resource_data['networkInterfaces']
556    ]
get_network_interfaces
558  @property
559  def get_network_interfaces(self):
560    return self._resource_data['networkInterfaces']
subnetworks: List[gcpdiag.queries.network.Subnetwork]
562  @property
563  def subnetworks(self) -> List[network_q.Subnetwork]:
564    subnetworks = []
565    for nic in self._resource_data['networkInterfaces']:
566      subnetworks.append(network_q.get_subnetwork_from_url(nic['subnetwork']))
567    return subnetworks
routes: List[gcpdiag.queries.network.Route]
569  @property
570  def routes(self) -> List[network_q.Route]:
571    routes = []
572    for nic in self._resource_data['networkInterfaces']:
573      for route in network_q.get_routes(self.project_id):
574        if nic['network'] == route.network:
575          if route.tags == []:
576            routes.append(route)
577            continue
578          else:
579            temp = [x for x in self.tags if x in route.tags]
580            if len(temp) > 0:
581              routes.append(route)
582    return routes
def get_network_ip_for_instance_interface( self, network: str) -> Union[ipaddress.IPv4Network, ipaddress.IPv6Network, NoneType]:
584  def get_network_ip_for_instance_interface(
585    self, network: str
586  ) -> Optional[network_q.IPv4NetOrIPv6Net]:
587    """Get the network ip for a nic given a network name."""
588    for nic in self._resource_data['networkInterfaces']:
589      if nic.get('network') == network:
590        return ipaddress.ip_network(nic.get('networkIP'))
591    return None

Get the network ip for a nic given a network name.

def secure_boot_enabled(self) -> bool:
593  def secure_boot_enabled(self) -> bool:
594    if 'shieldedInstanceConfig' in self._resource_data:
595      return self._resource_data['shieldedInstanceConfig']['enableSecureBoot']
596    return False
access_scopes: List[str]
598  @property
599  def access_scopes(self) -> List[str]:
600    if 'serviceAccounts' in self._resource_data:
601      saccts = self._resource_data['serviceAccounts']
602      if isinstance(saccts, list) and len(saccts) >= 1:
603        return saccts[0].get('scopes', [])
604    return []
service_account: Optional[str]
606  @property
607  def service_account(self) -> Optional[str]:
608    if 'serviceAccounts' in self._resource_data:
609      saccts = self._resource_data['serviceAccounts']
610      if isinstance(saccts, list) and len(saccts) >= 1:
611        return saccts[0]['email']
612    return None
tags: List[str]
614  @property
615  def tags(self) -> List[str]:
616    if 'tags' in self._resource_data:
617      if 'items' in self._resource_data['tags']:
618        return self._resource_data['tags']['items']
619    return []
def get_metadata(self, key: str) -> str:
621  def get_metadata(self, key: str) -> str:
622    if not self._metadata_dict:
623      self._metadata_dict = {}
624      if 'metadata' in self._resource_data and 'items' in self._resource_data['metadata']:
625        for item in self._resource_data['metadata']['items']:
626          if 'key' in item and 'value' in item:
627            self._metadata_dict[item['key']] = item['value']
628    project_metadata = get_project_metadata(self.project_id)
629    return self._metadata_dict.get(key, project_metadata.get(key))
status: str
631  @property
632  def status(self) -> str:
633    """VM Status."""
634    return self._resource_data.get('status', None)

VM Status.

is_running: bool
636  @property
637  def is_running(self) -> bool:
638    """VM Status is indicated as running."""
639    return self._resource_data.get('status', False) == 'RUNNING'

VM Status is indicated as running.

network_interface_count: int
641  @property
642  def network_interface_count(self) -> int:
643    """Returns the number of network interfaces attached to the instance."""
644    return len(self._resource_data.get('networkInterfaces', []))

Returns the number of network interfaces attached to the instance.

mig: ManagedInstanceGroup
646  @property  # type: ignore
647  @caching.cached_api_call(in_memory=True)
648  def mig(self) -> ManagedInstanceGroup:
649    """Return ManagedInstanceGroup that owns this instance.
650
651    Throws AttributeError in case it isn't MIG-managed.
652    """
653
654    created_by = self.get_metadata('created-by')
655    if created_by is None:
656      raise AttributeError(f'instance {self.id} is not managed by a mig')
657
658    # Example created-by:
659    # "projects/12340002/zones/europe-west4-a/instanceGroupManagers/gke-gke1-default-pool-e5e20a34-grp"
660    # (note how it uses a project number and not a project id...)
661    created_by_match = re.match(
662      r'projects/([^/]+)/((?:regions|zones)/[^/]+/instanceGroupManagers/[^/]+)$',
663      created_by,
664    )
665    if not created_by_match:
666      raise AttributeError(f'instance {self.id} is not managed by a mig (created-by={created_by})')
667    project = crm.get_project(created_by_match.group(1))
668
669    mig_self_link = (
670      f'https://www.googleapis.com/compute/v1/projects/{project.id}/{created_by_match.group(2)}'
671    )
672
673    # Try to find a matching mig.
674    context = models.Context(project_id=self.project_id)
675    all_migs = list(get_managed_instance_groups(context).values()) + list(
676      get_region_managed_instance_groups(context).values()
677    )
678
679    for mig in all_migs:
680      if mig.self_link == mig_self_link:
681        return mig
682
683    raise AttributeError(f'MIG not found for instance {self.id}. Created by: {created_by}')

Return ManagedInstanceGroup that owns this instance.

Throws AttributeError in case it isn't MIG-managed.

labels: dict
685  @property
686  def labels(self) -> dict:
687    return self._resource_data.get('labels', {})
class Disk(gcpdiag.models.Resource):
690class Disk(models.Resource):
691  """Represents a GCE disk."""
692
693  _resource_data: dict
694
695  def __init__(self, project_id, resource_data):
696    super().__init__(project_id=project_id)
697    self._resource_data = resource_data
698
699  @property
700  def id(self) -> str:
701    return self._resource_data['id']
702
703  @property
704  def name(self) -> str:
705    return self._resource_data['name']
706
707  @property
708  def type(self) -> str:
709    disk_type = re.search(r'/diskTypes/([^/]+)$', self._resource_data['type'])
710    if not disk_type:
711      raise RuntimeError(
712        "can't determine type of the disk {} ({})".format(self.name, self._resource_data['type'])
713      )
714    return disk_type.group(1)
715
716  @property
717  def users(self) -> list:
718    pattern = r'/instances/(.+)$'
719    # Extracting the instances
720    instances = []
721    for i in self._resource_data.get('users', []):
722      m = re.search(pattern, i)
723      if m:
724        instances.append(m.group(1))
725    return instances
726
727  @property
728  def zone(self) -> str:
729    m = re.search(r'/zones/([^/]+)$', self._resource_data['zone'])
730    if not m:
731      raise RuntimeError(
732        "can't determine zone of disk {} ({})".format(self.name, self._resource_data['zone'])
733      )
734    return m.group(1)
735
736  @property
737  def source_image(self) -> str:
738    return self._resource_data.get('sourceImage', '')
739
740  @property
741  def full_path(self) -> str:
742    result = re.match(
743      r'https://www.googleapis.com/compute/v1/(.*)',
744      self._resource_data['selfLink'],
745    )
746    if result:
747      return result.group(1)
748    else:
749      return '>> ' + self._resource_data['selfLink']
750
751  @property
752  def short_path(self) -> str:
753    return f'{self.project_id}/{self.name}'
754
755  @property
756  def bootable(self) -> bool:
757    return 'guestOsFeatures' in self._resource_data
758
759  @property
760  def in_use(self) -> bool:
761    return 'users' in self._resource_data
762
763  @property
764  def size(self) -> int:
765    return self._resource_data['sizeGb']
766
767  @property
768  def provisionediops(self) -> Optional[int]:
769    return self._resource_data.get('provisionedIops')
770
771  @property
772  def has_snapshot_schedule(self) -> bool:
773    return 'resourcePolicies' in self._resource_data

Represents a GCE disk.

Disk(project_id, resource_data)
695  def __init__(self, project_id, resource_data):
696    super().__init__(project_id=project_id)
697    self._resource_data = resource_data
id: str
699  @property
700  def id(self) -> str:
701    return self._resource_data['id']
name: str
703  @property
704  def name(self) -> str:
705    return self._resource_data['name']
type: str
707  @property
708  def type(self) -> str:
709    disk_type = re.search(r'/diskTypes/([^/]+)$', self._resource_data['type'])
710    if not disk_type:
711      raise RuntimeError(
712        "can't determine type of the disk {} ({})".format(self.name, self._resource_data['type'])
713      )
714    return disk_type.group(1)
users: list
716  @property
717  def users(self) -> list:
718    pattern = r'/instances/(.+)$'
719    # Extracting the instances
720    instances = []
721    for i in self._resource_data.get('users', []):
722      m = re.search(pattern, i)
723      if m:
724        instances.append(m.group(1))
725    return instances
zone: str
727  @property
728  def zone(self) -> str:
729    m = re.search(r'/zones/([^/]+)$', self._resource_data['zone'])
730    if not m:
731      raise RuntimeError(
732        "can't determine zone of disk {} ({})".format(self.name, self._resource_data['zone'])
733      )
734    return m.group(1)
source_image: str
736  @property
737  def source_image(self) -> str:
738    return self._resource_data.get('sourceImage', '')
full_path: str
740  @property
741  def full_path(self) -> str:
742    result = re.match(
743      r'https://www.googleapis.com/compute/v1/(.*)',
744      self._resource_data['selfLink'],
745    )
746    if result:
747      return result.group(1)
748    else:
749      return '>> ' + self._resource_data['selfLink']

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

short_path: str
751  @property
752  def short_path(self) -> str:
753    return f'{self.project_id}/{self.name}'

Returns the short name for this resource.

Note that it isn't clear from this name what kind of resource it is.

Example: 'gke1'

bootable: bool
755  @property
756  def bootable(self) -> bool:
757    return 'guestOsFeatures' in self._resource_data
in_use: bool
759  @property
760  def in_use(self) -> bool:
761    return 'users' in self._resource_data
size: int
763  @property
764  def size(self) -> int:
765    return self._resource_data['sizeGb']
provisionediops: Optional[int]
767  @property
768  def provisionediops(self) -> Optional[int]:
769    return self._resource_data.get('provisionedIops')
has_snapshot_schedule: bool
771  @property
772  def has_snapshot_schedule(self) -> bool:
773    return 'resourcePolicies' in self._resource_data
@caching.cached_api_call(in_memory=True)
def get_gce_zones(project_id: str) -> Set[str]:
776@caching.cached_api_call(in_memory=True)
777def get_gce_zones(project_id: str) -> Set[str]:
778  try:
779    gce_api = apis.get_api('compute', 'v1', project_id)
780    logging.debug('listing gce zones of project %s', project_id)
781    request = gce_api.zones().list(project=project_id)
782    response = request.execute(num_retries=config.API_RETRIES)
783    if not response or 'items' not in response:
784      return set()
785    return {item['name'] for item in response['items'] if 'name' in item}
786  except googleapiclient.errors.HttpError as err:
787    raise utils.GcpApiError(err) from err
def get_gce_public_licences(project_id: str) -> List[str]:
790def get_gce_public_licences(project_id: str) -> List[str]:
791  """Returns a list of licenses based on publicly available image project"""
792  licenses = []
793  gce_api = apis.get_api('compute', 'v1', project_id)
794  logging.debug('listing licenses of project %s', project_id)
795  request = gce_api.licenses().list(project=project_id)
796  while request is not None:
797    response = request.execute()
798    for license_ in response['items']:
799      formatted_license = license_['selfLink'].partition('/global/licenses/')[2]
800      licenses.append(formatted_license)
801    request = gce_api.licenses().list_next(previous_request=request, previous_response=response)
802  return licenses

Returns a list of licenses based on publicly available image project

def get_instance( project_id: str, zone: str, instance_name: str) -> Instance:
805def get_instance(project_id: str, zone: str, instance_name: str) -> Instance:
806  """Returns instance object matching instance name and zone"""
807  compute = apis.get_api('compute', 'v1', project_id)
808  request = compute.instances().get(project=project_id, zone=zone, instance=instance_name)
809
810  response = request.execute(num_retries=config.API_RETRIES)
811  return Instance(project_id, resource_data=response)

Returns instance object matching instance name and zone

@caching.cached_api_call(in_memory=True)
def get_instance_by_id( project_id: str, instance_id: str) -> Optional[Instance]:
814@caching.cached_api_call(in_memory=True)
815def get_instance_by_id(project_id: str, instance_id: str) -> Optional[Instance]:
816  """Returns instance object matching instance id in the project.
817
818  Searches all zones.
819
820  Args:
821    project_id: The ID of the GCP project.
822    instance_id: The unique ID of the GCE instance.
823  """
824  if not apis.is_enabled(project_id, 'compute'):
825    return None
826  gce_api = apis.get_api('compute', 'v1', project_id)
827  # Use aggregatedList with filter to efficiently find the instance by ID.
828  request = gce_api.instances().aggregatedList(
829    project=project_id,
830    filter=f'id eq {instance_id}',
831    returnPartialSuccess=True,
832  )
833
834  while request:
835    response = request.execute(num_retries=config.API_RETRIES)
836    items = response.get('items', {})
837    for _, data in items.items():
838      if 'instances' in data:
839        for instance_data in data['instances']:
840          if str(instance_data.get('id')) == str(instance_id):
841            return Instance(project_id, instance_data)
842
843    request = gce_api.instances().aggregatedList_next(
844      previous_request=request, previous_response=response
845    )
846
847  return None

Returns instance object matching instance id in the project.

Searches all zones.

Arguments:
  • project_id: The ID of the GCP project.
  • instance_id: The unique ID of the GCE instance.
@caching.cached_api_call(in_memory=True)
def get_global_operations( project: str, filter_str: Optional[str] = None, order_by: Optional[str] = None, max_results: Optional[int] = None, service_project_number: Optional[int] = None) -> List[Dict[str, Any]]:
850@caching.cached_api_call(in_memory=True)
851def get_global_operations(
852  project: str,
853  filter_str: Optional[str] = None,
854  order_by: Optional[str] = None,
855  max_results: Optional[int] = None,
856  service_project_number: Optional[int] = None,
857) -> List[Dict[str, Any]]:
858  """Returns global operations object matching project id."""
859  compute = apis.get_api('compute', 'v1', project)
860  logging.debug(
861    'searching compute global operationslogs in project %s with filter %s',
862    project,
863    filter_str,
864  )
865  operations: List[Dict[str, Any]] = []
866  request = compute.globalOperations().aggregatedList(
867    project=project,
868    filter=filter_str,
869    orderBy=order_by,
870    maxResults=max_results,
871    serviceProjectNumber=service_project_number,
872    returnPartialSuccess=True,
873  )
874  while request:
875    response = request.execute(num_retries=config.API_RETRIES)
876    operations_by_regions = response.get('items', {})
877    for _, data in operations_by_regions.items():
878      if 'operations' not in data:
879        continue
880      operations.extend(data['operations'])
881    request = compute.globalOperations().aggregatedList_next(
882      previous_request=request, previous_response=response
883    )
884  return operations

Returns global operations object matching project id.

@caching.cached_api_call(in_memory=True)
def get_disk(project_id: str, zone: str, disk_name: str) -> Disk:
887@caching.cached_api_call(in_memory=True)
888def get_disk(project_id: str, zone: str, disk_name: str) -> Disk:
889  """Returns disk object matching disk name and zone."""
890  compute = apis.get_api('compute', 'v1', project_id)
891  request = compute.disks().get(project=project_id, zone=zone, disk=disk_name)
892  response = request.execute(num_retries=config.API_RETRIES)
893  return Disk(project_id, resource_data=response)

Returns disk object matching disk name and zone.

def get_instance_group_manager( project_id: str, zone: str, instance_group_manager_name: str) -> ManagedInstanceGroup:
896def get_instance_group_manager(
897  project_id: str, zone: str, instance_group_manager_name: str
898) -> ManagedInstanceGroup:
899  """Get a zonal ManagedInstanceGroup object by name and zone.
900
901  Args:
902    project_id: The project ID of the instance group manager.
903    zone: The zone of the instance group manager.
904    instance_group_manager_name: The name of the instance group manager.
905
906  Returns:
907    A ManagedInstanceGroup object.
908
909  Raises:
910    utils.GcpApiError: If the API call fails.
911  """
912  compute = apis.get_api('compute', 'v1', project_id)
913  request = compute.instanceGroupManagers().get(
914    project=project_id,
915    zone=zone,
916    instanceGroupManager=instance_group_manager_name,
917  )
918  try:
919    response = request.execute(num_retries=config.API_RETRIES)
920    return ManagedInstanceGroup(project_id, resource_data=response)
921  except googleapiclient.errors.HttpError as err:
922    raise utils.GcpApiError(err) from err

Get a zonal ManagedInstanceGroup object by name and zone.

Arguments:
  • project_id: The project ID of the instance group manager.
  • zone: The zone of the instance group manager.
  • instance_group_manager_name: The name of the instance group manager.
Returns:

A ManagedInstanceGroup object.

Raises:
  • utils.GcpApiError: If the API call fails.
def get_region_instance_group_manager( project_id: str, region: str, instance_group_manager_name: str) -> ManagedInstanceGroup:
925def get_region_instance_group_manager(
926  project_id: str, region: str, instance_group_manager_name: str
927) -> ManagedInstanceGroup:
928  """Get a regional ManagedInstanceGroup object by name and region.
929
930  Args:
931    project_id: The project ID of the instance group manager.
932    region: The region of the instance group manager.
933    instance_group_manager_name: The name of the instance group manager.
934
935  Returns:
936    A ManagedInstanceGroup object.
937
938  Raises:
939    utils.GcpApiError: If the API call fails.
940  """
941  compute = apis.get_api('compute', 'v1', project_id)
942  request = compute.regionInstanceGroupManagers().get(
943    project=project_id,
944    region=region,
945    instanceGroupManager=instance_group_manager_name,
946  )
947  try:
948    response = request.execute(num_retries=config.API_RETRIES)
949    return ManagedInstanceGroup(project_id, resource_data=response)
950  except googleapiclient.errors.HttpError as err:
951    raise utils.GcpApiError(err) from err

Get a regional ManagedInstanceGroup object by name and region.

Arguments:
  • project_id: The project ID of the instance group manager.
  • region: The region of the instance group manager.
  • instance_group_manager_name: The name of the instance group manager.
Returns:

A ManagedInstanceGroup object.

Raises:
  • utils.GcpApiError: If the API call fails.
def get_autoscaler( project_id: str, zone: str, autoscaler_name: str) -> Autoscaler:
954def get_autoscaler(project_id: str, zone: str, autoscaler_name: str) -> Autoscaler:
955  """Get a zonal Autoscaler object by name and zone."""
956  compute = apis.get_api('compute', 'v1', project_id)
957  request = compute.autoscalers().get(project=project_id, zone=zone, autoscaler=autoscaler_name)
958  try:
959    response = request.execute(num_retries=config.API_RETRIES)
960    return Autoscaler(project_id, resource_data=response)
961  except googleapiclient.errors.HttpError as err:
962    raise utils.GcpApiError(err) from err

Get a zonal Autoscaler object by name and zone.

def get_region_autoscaler( project_id: str, region: str, autoscaler_name: str) -> Autoscaler:
965def get_region_autoscaler(project_id: str, region: str, autoscaler_name: str) -> Autoscaler:
966  """Get a regional Autoscaler object by name and region."""
967  compute = apis.get_api('compute', 'v1', project_id)
968  request = compute.regionAutoscalers().get(
969    project=project_id, region=region, autoscaler=autoscaler_name
970  )
971  try:
972    response = request.execute(num_retries=config.API_RETRIES)
973    return Autoscaler(project_id, resource_data=response)
974  except googleapiclient.errors.HttpError as err:
975    raise utils.GcpApiError(err) from err

Get a regional Autoscaler object by name and region.

@caching.cached_api_call(in_memory=True)
def get_instances( context: gcpdiag.models.Context) -> Mapping[str, Instance]:
 978@caching.cached_api_call(in_memory=True)
 979def get_instances(context: models.Context) -> Mapping[str, Instance]:
 980  """Get a list of Instance matching the given context, indexed by instance id."""
 981
 982  instances: Dict[str, Instance] = {}
 983  if not apis.is_enabled(context.project_id, 'compute'):
 984    return instances
 985  gce_api = apis.get_api('compute', 'v1', context.project_id)
 986  request = gce_api.instances().aggregatedList(
 987    project=context.project_id, returnPartialSuccess=True
 988  )
 989  logging.debug('listing gce instances of project %s', context.project_id)
 990  while request:  # Continue as long as there are pages
 991    response = request.execute(num_retries=config.API_RETRIES)
 992    instances_by_zones = response.get('items', {})
 993    for _, data_ in instances_by_zones.items():
 994      if 'instances' not in data_:
 995        continue
 996      for instance in data_['instances']:
 997        result = re.match(
 998          r'https://www.googleapis.com/compute/v1/projects/[^/]+/zones/([^/]+)/',
 999          instance['selfLink'],
1000        )
1001        if not result:
1002          logging.error(
1003            "instance %s selfLink didn't match regexp: %s",
1004            instance['id'],
1005            instance['selfLink'],
1006          )
1007          continue
1008        zone = result.group(1)
1009        labels = instance.get('labels', {})
1010        if not context.match_project_resource(
1011          resource=instance.get('name'), location=zone, labels=labels
1012        ) and not context.match_project_resource(
1013          resource=instance.get('id'), location=zone, labels=labels
1014        ):
1015          continue
1016        instances.update(
1017          {instance['id']: Instance(project_id=context.project_id, resource_data=instance)}
1018        )
1019    request = gce_api.instances().aggregatedList_next(
1020      previous_request=request, previous_response=response
1021    )
1022  return instances

Get a list of Instance matching the given context, indexed by instance id.

@caching.cached_api_call(in_memory=True)
def get_instance_groups( context: gcpdiag.models.Context) -> Mapping[str, InstanceGroup]:
1025@caching.cached_api_call(in_memory=True)
1026def get_instance_groups(context: models.Context) -> Mapping[str, InstanceGroup]:
1027  """Get a list of InstanceGroups matching the given context, indexed by name."""
1028  groups: Dict[str, InstanceGroup] = {}
1029  if not apis.is_enabled(context.project_id, 'compute'):
1030    return groups
1031  gce_api = apis.get_api('compute', 'v1', context.project_id)
1032  request = gce_api.instanceGroups().aggregatedList(
1033    project=context.project_id, returnPartialSuccess=True
1034  )
1035  logging.debug('listing gce instance groups of project %s', context.project_id)
1036  while request:  # Continue as long as there are pages
1037    response = request.execute(num_retries=config.API_RETRIES)
1038    groups_by_zones = response.get('items', {})
1039    for _, data_ in groups_by_zones.items():
1040      if 'instanceGroups' not in data_:
1041        continue
1042      for group in data_['instanceGroups']:
1043        result = re.match(
1044          r'https://www.googleapis.com/compute/v1/projects/[^/]+/(zones|regions)/([^/]+)',
1045          group['selfLink'],
1046        )
1047        if not result:
1048          logging.error(
1049            "instance %s selfLink didn't match regexp: %s",
1050            group['id'],
1051            group['selfLink'],
1052          )
1053          continue
1054        location = result.group(2)
1055        labels = group.get('labels', {})
1056        resource = group.get('name', '')
1057        if not context.match_project_resource(location=location, labels=labels, resource=resource):
1058          continue
1059        instance_group = InstanceGroup(context.project_id, resource_data=group)
1060        groups[instance_group.full_path] = instance_group
1061    request = gce_api.instanceGroups().aggregatedList_next(
1062      previous_request=request, previous_response=response
1063    )
1064  return groups

Get a list of InstanceGroups matching the given context, indexed by name.

@caching.cached_api_call(in_memory=True)
def get_managed_instance_groups( context: gcpdiag.models.Context) -> Mapping[int, ManagedInstanceGroup]:
1067@caching.cached_api_call(in_memory=True)
1068def get_managed_instance_groups(
1069  context: models.Context,
1070) -> Mapping[int, ManagedInstanceGroup]:
1071  """Get a list of zonal ManagedInstanceGroups matching the given context, indexed by mig id."""
1072
1073  migs: Dict[int, ManagedInstanceGroup] = {}
1074  if not apis.is_enabled(context.project_id, 'compute'):
1075    return migs
1076  gce_api = apis.get_api('compute', 'v1', context.project_id)
1077  request = gce_api.instanceGroupManagers().aggregatedList(
1078    project=context.project_id, returnPartialSuccess=True
1079  )
1080  logging.debug('listing zonal managed instance groups of project %s', context.project_id)
1081  while request:  # Continue as long as there are pages
1082    response = request.execute(num_retries=config.API_RETRIES)
1083    migs_by_zones = response.get('items', {})
1084    for _, data_ in migs_by_zones.items():
1085      if 'instanceGroupManagers' not in data_:
1086        continue
1087      for mig in data_['instanceGroupManagers']:
1088        result = re.match(
1089          r'https://www.googleapis.com/compute/v1/projects/[^/]+/(?:regions|zones)/([^/]+)/',
1090          mig['selfLink'],
1091        )
1092        if not result:
1093          logging.error(
1094            "mig %s selfLink didn't match regexp: %s",
1095            mig['name'],
1096            mig['selfLink'],
1097          )
1098          continue
1099        location = result.group(1)
1100        labels = mig.get('labels', {})
1101        resource = mig.get('name', '')
1102        if not context.match_project_resource(location=location, labels=labels, resource=resource):
1103          continue
1104        migs[mig['id']] = ManagedInstanceGroup(project_id=context.project_id, resource_data=mig)
1105    request = gce_api.instanceGroupManagers().aggregatedList_next(
1106      previous_request=request, previous_response=response
1107    )
1108  return migs

Get a list of zonal ManagedInstanceGroups matching the given context, indexed by mig id.

@caching.cached_api_call(in_memory=True)
def get_region_managed_instance_groups( context: gcpdiag.models.Context) -> Mapping[int, ManagedInstanceGroup]:
1111@caching.cached_api_call(in_memory=True)
1112def get_region_managed_instance_groups(
1113  context: models.Context,
1114) -> Mapping[int, ManagedInstanceGroup]:
1115  """Get a list of regional ManagedInstanceGroups matching the given context, indexed by mig id."""
1116
1117  migs: Dict[int, ManagedInstanceGroup] = {}
1118  if not apis.is_enabled(context.project_id, 'compute'):
1119    return migs
1120  gce_api = apis.get_api('compute', 'v1', context.project_id)
1121  requests = [
1122    gce_api.regionInstanceGroupManagers().list(project=context.project_id, region=r.name)
1123    for r in get_all_regions(context.project_id)
1124  ]
1125  logging.debug(
1126    'listing regional managed instance groups of project %s',
1127    context.project_id,
1128  )
1129  items = apis_utils.execute_concurrently_with_pagination(
1130    api=gce_api,
1131    requests=requests,
1132    next_function=gce_api.regionInstanceGroupManagers().list_next,
1133    context=context,
1134    log_text=(f'listing regional managed instance groups of project {context.project_id}'),
1135  )
1136  for i in items:
1137    result = re.match(
1138      r'https://www.googleapis.com/compute/v1/projects/[^/]+/(?:regions)/([^/]+)/',
1139      i['selfLink'],
1140    )
1141    if not result:
1142      logging.error("mig %s selfLink didn't match regexp: %s", i['name'], i['selfLink'])
1143      continue
1144    location = result.group(1)
1145    labels = i.get('labels', {})
1146    name = i.get('name', '')
1147    if not context.match_project_resource(location=location, labels=labels, resource=name):
1148      continue
1149    migs[i['id']] = ManagedInstanceGroup(project_id=context.project_id, resource_data=i)
1150  return migs

Get a list of regional ManagedInstanceGroups matching the given context, indexed by mig id.

@caching.cached_api_call
def get_instance_templates(project_id: str) -> Mapping[str, InstanceTemplate]:
1153@caching.cached_api_call
1154def get_instance_templates(project_id: str) -> Mapping[str, InstanceTemplate]:
1155  logging.info('fetching instance templates')
1156  templates = {}
1157  gce_api = apis.get_api('compute', 'v1', project_id)
1158  request = gce_api.instanceTemplates().list(
1159    project=project_id,
1160    returnPartialSuccess=True,
1161    # Fetch only a subset of the fields to improve performance.
1162    fields=(
1163      'items/name, items/properties/tags,'
1164      ' items/properties/networkInterfaces,'
1165      ' items/properties/serviceAccounts, items/properties/metadata'
1166    ),
1167  )
1168  for t in apis_utils.list_all(request, next_function=gce_api.instanceTemplates().list_next):
1169    instance_template = InstanceTemplate(project_id, t)
1170    templates[instance_template.full_path] = instance_template
1171  return templates
@caching.cached_api_call
def get_project_metadata(project_id) -> Mapping[str, str]:
1174@caching.cached_api_call
1175def get_project_metadata(project_id) -> Mapping[str, str]:
1176  gce_api = apis.get_api('compute', 'v1', project_id)
1177  logging.debug('fetching metadata of project %s\n', project_id)
1178  query = gce_api.projects().get(project=project_id)
1179  try:
1180    response = query.execute(num_retries=config.API_RETRIES)
1181  except googleapiclient.errors.HttpError as err:
1182    raise utils.GcpApiError(err) from err
1183
1184  mapped_metadata: Dict[str, str] = {}
1185  metadata = response.get('commonInstanceMetadata')
1186  if metadata and 'items' in metadata:
1187    for m_item in metadata['items']:
1188      mapped_metadata[m_item.get('key')] = m_item.get('value')
1189  return mapped_metadata
@caching.cached_api_call
def get_instances_serial_port_output(context: gcpdiag.models.Context):
1192@caching.cached_api_call
1193def get_instances_serial_port_output(context: models.Context):
1194  """Get a list of serial port output for instances
1195
1196  which matches the given context, running and is not
1197  exported to cloud logging.
1198  """
1199  # Create temp storage (diskcache.Deque) for output
1200  deque = caching.get_tmp_deque('tmp-gce-serial-output-')
1201  if not apis.is_enabled(context.project_id, 'compute'):
1202    return deque
1203  gce_api = apis.get_api('compute', 'v1', context.project_id)
1204
1205  # Serial port output are rolled over on day 7 and limited to 1MB.
1206  # Fetching serial outputs are very expensive so optimize to fetch.
1207  # Only relevant instances as storage size can grow drastically for
1208  # massive projects. Think 1MB * N where N is some large number.
1209  requests = [
1210    gce_api.instances().getSerialPortOutput(
1211      project=i.project_id,
1212      zone=i.zone,
1213      instance=i.id,
1214      # To get all 1mb output
1215      start=-1000000,
1216    )
1217    for i in get_instances(context).values()
1218    # fetch running instances that do not export to cloud logging
1219    if not i.is_serial_port_logging_enabled() and i.is_running
1220  ]
1221  requests_start_time = datetime.now()
1222  # Note: We are limited to 1000 calls in a single batch request.
1223  # We have to use multiple batch requests in batches of 1000
1224  # https://github.com/googleapis/google-api-python-client/blob/main/docs/batch.md
1225  batch_size = 1000
1226  for i in range(0, len(requests), batch_size):
1227    batch_requests = requests[i : i + batch_size]
1228    for _, response, exception in apis_utils.execute_concurrently(
1229      api=gce_api, requests=batch_requests, context=context
1230    ):
1231      if exception:
1232        if isinstance(exception, googleapiclient.errors.HttpError):
1233          raise utils.GcpApiError(exception) from exception
1234        else:
1235          raise exception
1236
1237      if response:
1238        result = re.match(
1239          r'https://www.googleapis.com/compute/v1/projects/([^/]+)/zones/[^/]+/instances/([^/]+)',
1240          response['selfLink'],
1241        )
1242        if not result:
1243          logging.error("instance selfLink didn't match regexp: %s", response['selfLink'])
1244          return
1245
1246        project_id = result.group(1)
1247        instance_id = result.group(2)
1248        deque.appendleft(
1249          SerialPortOutput(
1250            project_id=project_id,
1251            instance_id=instance_id,
1252            contents=response['contents'].splitlines(),
1253          )
1254        )
1255  requests_end_time = datetime.now()
1256  logging.debug(
1257    'total serial logs processing time: %s, number of instances: %s',
1258    requests_end_time - requests_start_time,
1259    len(requests),
1260  )
1261  return deque

Get a list of serial port output for instances

which matches the given context, running and is not exported to cloud logging.

@caching.cached_api_call
def get_instance_serial_port_output( project_id, zone, instance_name) -> Optional[SerialPortOutput]:
1264@caching.cached_api_call
1265def get_instance_serial_port_output(project_id, zone, instance_name) -> Optional[SerialPortOutput]:
1266  """Get a list of serial port output for instances
1267
1268  which matches the given context, running and is not
1269  exported to cloud logging.
1270  """
1271  # Create temp storage (diskcache.Deque) for output
1272  if not apis.is_enabled(project_id, 'compute'):
1273    return None
1274  gce_api = apis.get_api('compute', 'v1', project_id)
1275
1276  request = gce_api.instances().getSerialPortOutput(
1277    project=project_id,
1278    zone=zone,
1279    instance=instance_name,
1280    # To get all 1mb output
1281    start=-1000000,
1282  )
1283  try:
1284    response = request.execute(num_retries=config.API_RETRIES)
1285  except googleapiclient.errors.HttpError:
1286    return None
1287
1288  if response:
1289    result = re.match(
1290      r'https://www.googleapis.com/compute/v1/projects/([^/]+)/zones/[^/]+/instances/([^/]+)',
1291      response['selfLink'],
1292    )
1293    if not result:
1294      logging.error("instance selfLink didn't match regexp: %s", response['selfLink'])
1295      return None
1296
1297    project_id = result.group(1)
1298    instance_id = result.group(2)
1299    return SerialPortOutput(
1300      project_id,
1301      instance_id=instance_id,
1302      contents=response['contents'].splitlines(),
1303    )
1304  return None

Get a list of serial port output for instances

which matches the given context, running and is not exported to cloud logging.

class Region(gcpdiag.models.Resource):
1307class Region(models.Resource):
1308  """Represents a GCE Region."""
1309
1310  _resource_data: dict
1311
1312  def __init__(self, project_id, resource_data):
1313    super().__init__(project_id=project_id)
1314    self._resource_data = resource_data
1315
1316  @property
1317  def self_link(self) -> str:
1318    return self._resource_data['selfLink']
1319
1320  @property
1321  def full_path(self) -> str:
1322    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
1323    if result:
1324      return result.group(1)
1325    else:
1326      return f'>> {self.self_link}'
1327
1328  @property
1329  def name(self) -> str:
1330    return self._resource_data['name']

Represents a GCE Region.

Region(project_id, resource_data)
1312  def __init__(self, project_id, resource_data):
1313    super().__init__(project_id=project_id)
1314    self._resource_data = resource_data
full_path: str
1320  @property
1321  def full_path(self) -> str:
1322    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
1323    if result:
1324      return result.group(1)
1325    else:
1326      return f'>> {self.self_link}'

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

name: str
1328  @property
1329  def name(self) -> str:
1330    return self._resource_data['name']
@caching.cached_api_call
def get_all_regions(project_id: str) -> Iterable[Region]:
1333@caching.cached_api_call
1334def get_all_regions(project_id: str) -> Iterable[Region]:
1335  """Return list of all regions
1336
1337  Args:
1338      project_id (str): project id for this request
1339
1340  Raises:
1341      utils.GcpApiError: Raises GcpApiError in case of query issues
1342
1343  Returns:
1344      Iterable[Region]: Return list of all regions
1345  """
1346  try:
1347    gce_api = apis.get_api('compute', 'v1', project_id)
1348    request = gce_api.regions().list(project=project_id)
1349    response = request.execute(num_retries=config.API_RETRIES)
1350    if not response or 'items' not in response:
1351      return set()
1352
1353    return {Region(project_id, item) for item in response['items'] if 'name' in item}
1354  except googleapiclient.errors.HttpError as err:
1355    raise utils.GcpApiError(err) from err

Return list of all regions

Arguments:
  • project_id (str): project id for this request
Raises:
  • utils.GcpApiError: Raises GcpApiError in case of query issues
Returns:

Iterable[Region]: Return list of all regions

def get_regions_with_instances(context: gcpdiag.models.Context) -> Iterable[Region]:
1358def get_regions_with_instances(context: models.Context) -> Iterable[Region]:
1359  """Return list of regions with instances
1360
1361  Args:
1362      context (models.Context): context for this request
1363
1364  Returns:
1365      Iterable[Region]: Return list of regions which contains instances
1366  """
1367
1368  regions_of_instances = {i.region for i in get_instances(context).values()}
1369
1370  all_regions = get_all_regions(context.project_id)
1371  if not all_regions:
1372    return set()
1373
1374  return {r for r in all_regions if r.name in regions_of_instances}

Return list of regions with instances

Arguments:
  • context (models.Context): context for this request
Returns:

Iterable[Region]: Return list of regions which contains instances

@caching.cached_api_call
def get_all_disks(context: gcpdiag.models.Context) -> Iterable[Disk]:
1377@caching.cached_api_call
1378def get_all_disks(context: models.Context) -> Iterable[Disk]:
1379  """Get all disks in a project, matching the context.
1380
1381  Args:
1382    context: The project context.
1383
1384  Returns:
1385    An iterable of Disk objects.
1386  """
1387  project_id = context.project_id
1388  # Fetching only Zonal Disks(Regional disks exempted)
1389  try:
1390    gce_api = apis.get_api('compute', 'v1', project_id)
1391    requests = [
1392      gce_api.disks().list(project=project_id, zone=zone) for zone in get_gce_zones(project_id)
1393    ]
1394
1395    logging.debug('listing gce disks of project %s', project_id)
1396
1397    items = apis_utils.execute_concurrently_with_pagination(
1398      api=gce_api,
1399      requests=requests,
1400      next_function=gce_api.disks().list_next,
1401      context=context,
1402      log_text=f'listing GCE disks of project {project_id}',
1403    )
1404
1405    return {Disk(project_id, item) for item in items}
1406
1407  except googleapiclient.errors.HttpError as err:
1408    raise utils.GcpApiError(err) from err

Get all disks in a project, matching the context.

Arguments:
  • context: The project context.
Returns:

An iterable of Disk objects.

@caching.cached_api_call
def get_all_disks_of_instance(context: gcpdiag.models.Context, zone: str, instance_name: str) -> dict:
1411@caching.cached_api_call
1412def get_all_disks_of_instance(context: models.Context, zone: str, instance_name: str) -> dict:
1413  """Get all disks of a given instance.
1414
1415  Args:
1416    context: The project context.
1417    zone: The zone of the instance.
1418    instance_name: The name of the instance.
1419
1420  Returns:
1421    A dict of Disk objects keyed by disk name.
1422  """
1423  project_id = context.project_id
1424  # Fetching only Zonal Disks(Regional disks exempted) attached to an instance
1425  try:
1426    gce_api = apis.get_api('compute', 'v1', project_id)
1427    requests = [gce_api.disks().list(project=project_id, zone=zone)]
1428    logging.debug(
1429      'listing gce disks attached to instance %s in project %s',
1430      instance_name,
1431      project_id,
1432    )
1433
1434    items = apis_utils.execute_concurrently_with_pagination(
1435      api=gce_api,
1436      requests=requests,
1437      next_function=gce_api.disks().list_next,
1438      context=context,
1439      log_text=(f'listing gce disks attached to instance {instance_name} in project {project_id}'),
1440    )
1441    all_disk_list = {Disk(project_id, item) for item in items}
1442    disk_list = {}
1443    for disk in all_disk_list:
1444      if disk.users == [instance_name]:
1445        disk_list[disk.name] = disk
1446    return disk_list
1447
1448  except googleapiclient.errors.HttpError as err:
1449    raise utils.GcpApiError(err) from err

Get all disks of a given instance.

Arguments:
  • context: The project context.
  • zone: The zone of the instance.
  • instance_name: The name of the instance.
Returns:

A dict of Disk objects keyed by disk name.

class InstanceEffectiveFirewalls(gcpdiag.queries.network.EffectiveFirewalls):
1452class InstanceEffectiveFirewalls(network_q.EffectiveFirewalls):
1453  """Effective firewall rules for a network interface on a VM instance.
1454
1455  Includes org/folder firewall policies).
1456  """
1457
1458  _instance: Instance
1459  _nic: str
1460
1461  def __init__(self, instance, nic, resource_data):
1462    super().__init__(resource_data)
1463    self._instance = instance
1464    self._nic = nic

Effective firewall rules for a network interface on a VM instance.

Includes org/folder firewall policies).

InstanceEffectiveFirewalls(instance, nic, resource_data)
1461  def __init__(self, instance, nic, resource_data):
1462    super().__init__(resource_data)
1463    self._instance = instance
1464    self._nic = nic
@caching.cached_api_call(in_memory=True)
def get_instance_interface_effective_firewalls( instance: Instance, nic: str) -> InstanceEffectiveFirewalls:
1467@caching.cached_api_call(in_memory=True)
1468def get_instance_interface_effective_firewalls(
1469  instance: Instance, nic: str
1470) -> InstanceEffectiveFirewalls:
1471  """Return effective firewalls for a network interface on the instance."""
1472  compute = apis.get_api('compute', 'v1', instance.project_id)
1473  request = compute.instances().getEffectiveFirewalls(
1474    project=instance.project_id,
1475    zone=instance.zone,
1476    instance=instance.name,
1477    networkInterface=nic,
1478  )
1479  response = request.execute(num_retries=config.API_RETRIES)
1480  return InstanceEffectiveFirewalls(Instance, nic, response)

Return effective firewalls for a network interface on the instance.

def is_project_serial_port_logging_enabled(project_id: str) -> bool:
1483def is_project_serial_port_logging_enabled(project_id: str) -> bool:
1484  if not apis.is_enabled(project_id, 'compute'):
1485    return False
1486
1487  value = get_project_metadata(project_id=project_id).get('serial-port-logging-enable')
1488  return bool(value and value.upper() in POSITIVE_BOOL_VALUES)
def is_serial_port_buffer_enabled():
1491def is_serial_port_buffer_enabled():
1492  return config.get('enable_gce_serial_buffer')
class SerialOutputQuery:
1507class SerialOutputQuery:
1508  """A serial output job that was started with prefetch_logs()."""
1509
1510  job: _SerialOutputJob
1511
1512  def __init__(self, job):
1513    self.job = job
1514
1515  @property
1516  def entries(self) -> Sequence:
1517    if not self.job.future:
1518      raise RuntimeError(
1519        "Fetching serial logs wasn't executed. did you call execute_get_serial_port_output()?"
1520      )
1521    elif self.job.future.running():
1522      logging.debug(
1523        'waiting for serial output results for project: %s',
1524        self.job.context.project_id,
1525      )
1526    return self.job.future.result()

A serial output job that was started with prefetch_logs().

SerialOutputQuery(job)
1512  def __init__(self, job):
1513    self.job = job
job: gcpdiag.queries.gce._SerialOutputJob
entries: Sequence
1515  @property
1516  def entries(self) -> Sequence:
1517    if not self.job.future:
1518      raise RuntimeError(
1519        "Fetching serial logs wasn't executed. did you call execute_get_serial_port_output()?"
1520      )
1521    elif self.job.future.running():
1522      logging.debug(
1523        'waiting for serial output results for project: %s',
1524        self.job.context.project_id,
1525      )
1526    return self.job.future.result()
jobs_todo: Dict[gcpdiag.models.Context, gcpdiag.queries.gce._SerialOutputJob] = {}
def execute_fetch_serial_port_outputs(query_executor: gcpdiag.executor.ContextAwareExecutor):
1532def execute_fetch_serial_port_outputs(
1533  query_executor: executor.ContextAwareExecutor,
1534):
1535  # start a thread to fetch serial log; processing logs can be large
1536  # depending on he number of instances in the project which aren't
1537  # logging to cloud logging. currently expects only one job but
1538  # implementing it so support for multiple projects is possible.
1539  global jobs_todo
1540  jobs_executing = jobs_todo
1541  jobs_todo = {}
1542  # query_executor = get_executor(context)
1543  for job in jobs_executing.values():
1544    job.future = query_executor.submit(get_instances_serial_port_output, job.context)
def fetch_serial_port_outputs(context: gcpdiag.models.Context) -> SerialOutputQuery:
1547def fetch_serial_port_outputs(context: models.Context) -> SerialOutputQuery:
1548  # Aggregate by context
1549  job = jobs_todo.setdefault(context, _SerialOutputJob(context=context))
1550  return SerialOutputQuery(job=job)
class HealthCheck(gcpdiag.models.Resource):
1554class HealthCheck(models.Resource):
1555  """A Health Check resource."""
1556
1557  _resource_data: dict
1558  _type: str
1559
1560  def __init__(self, project_id, resource_data):
1561    super().__init__(project_id=project_id)
1562    self._resource_data = resource_data
1563
1564  @property
1565  def name(self) -> str:
1566    return self._resource_data['name']
1567
1568  @property
1569  def full_path(self) -> str:
1570    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
1571    if result:
1572      return result.group(1)
1573    else:
1574      return f'>> {self.self_link}'
1575
1576  @property
1577  def short_path(self) -> str:
1578    path = self.project_id + '/' + self.name
1579    return path
1580
1581  @property
1582  def self_link(self) -> str:
1583    return self._resource_data['selfLink']
1584
1585  @property
1586  def is_log_enabled(self) -> bool:
1587    try:
1588      log_config = self._resource_data.get('logConfig', False)
1589      if log_config and log_config['enable']:
1590        return True
1591    except KeyError:
1592      return False
1593    return False
1594
1595  @property
1596  def region(self):
1597    url = self._resource_data.get('region', '')
1598    match = re.search(r'/([^/]+)/?$', url)
1599    if match:
1600      region = match.group(1)
1601      return region
1602    return None
1603
1604  @property
1605  def type(self) -> str:
1606    return self._resource_data['type']
1607
1608  @property
1609  def request_path(self) -> str:
1610    return self.get_health_check_property('requestPath', '/')
1611
1612  @property
1613  def request(self) -> str:
1614    return self.get_health_check_property('request')
1615
1616  @property
1617  def response(self) -> str:
1618    return self.get_health_check_property('response')
1619
1620  @property
1621  def port(self) -> int:
1622    return self.get_health_check_property('port')
1623
1624  @property
1625  def port_specification(self) -> str:
1626    return self.get_health_check_property('portSpecification', 'USE_FIXED_PORT')
1627
1628  @property
1629  def timeout_sec(self) -> int:
1630    return self._resource_data.get('timeoutSec', 5)
1631
1632  @property
1633  def check_interval_sec(self) -> int:
1634    return self._resource_data.get('checkIntervalSec', 5)
1635
1636  @property
1637  def unhealthy_threshold(self) -> int:
1638    return self._resource_data.get('unhealthyThreshold', 2)
1639
1640  @property
1641  def healthy_threshold(self) -> int:
1642    return self._resource_data.get('healthyThreshold', 2)
1643
1644  def get_health_check_property(self, property_name: str, default_value=None):
1645    health_check_types = {
1646      'HTTP': 'httpHealthCheck',
1647      'HTTPS': 'httpsHealthCheck',
1648      'HTTP2': 'http2HealthCheck',
1649      'TCP': 'tcpHealthCheck',
1650      'SSL': 'sslHealthCheck',
1651      'GRPC': 'grpcHealthCheck',
1652    }
1653    if self.type in health_check_types:
1654      health_check_data = self._resource_data.get(health_check_types[self.type])
1655      if health_check_data:
1656        return health_check_data.get(property_name) or default_value
1657    return default_value

A Health Check resource.

HealthCheck(project_id, resource_data)
1560  def __init__(self, project_id, resource_data):
1561    super().__init__(project_id=project_id)
1562    self._resource_data = resource_data
name: str
1564  @property
1565  def name(self) -> str:
1566    return self._resource_data['name']
full_path: str
1568  @property
1569  def full_path(self) -> str:
1570    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
1571    if result:
1572      return result.group(1)
1573    else:
1574      return f'>> {self.self_link}'

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

short_path: str
1576  @property
1577  def short_path(self) -> str:
1578    path = self.project_id + '/' + self.name
1579    return path

Returns the short name for this resource.

Note that it isn't clear from this name what kind of resource it is.

Example: 'gke1'

is_log_enabled: bool
1585  @property
1586  def is_log_enabled(self) -> bool:
1587    try:
1588      log_config = self._resource_data.get('logConfig', False)
1589      if log_config and log_config['enable']:
1590        return True
1591    except KeyError:
1592      return False
1593    return False
region
1595  @property
1596  def region(self):
1597    url = self._resource_data.get('region', '')
1598    match = re.search(r'/([^/]+)/?$', url)
1599    if match:
1600      region = match.group(1)
1601      return region
1602    return None
type: str
1604  @property
1605  def type(self) -> str:
1606    return self._resource_data['type']
request_path: str
1608  @property
1609  def request_path(self) -> str:
1610    return self.get_health_check_property('requestPath', '/')
request: str
1612  @property
1613  def request(self) -> str:
1614    return self.get_health_check_property('request')
response: str
1616  @property
1617  def response(self) -> str:
1618    return self.get_health_check_property('response')
port: int
1620  @property
1621  def port(self) -> int:
1622    return self.get_health_check_property('port')
port_specification: str
1624  @property
1625  def port_specification(self) -> str:
1626    return self.get_health_check_property('portSpecification', 'USE_FIXED_PORT')
timeout_sec: int
1628  @property
1629  def timeout_sec(self) -> int:
1630    return self._resource_data.get('timeoutSec', 5)
check_interval_sec: int
1632  @property
1633  def check_interval_sec(self) -> int:
1634    return self._resource_data.get('checkIntervalSec', 5)
unhealthy_threshold: int
1636  @property
1637  def unhealthy_threshold(self) -> int:
1638    return self._resource_data.get('unhealthyThreshold', 2)
healthy_threshold: int
1640  @property
1641  def healthy_threshold(self) -> int:
1642    return self._resource_data.get('healthyThreshold', 2)
def get_health_check_property(self, property_name: str, default_value=None):
1644  def get_health_check_property(self, property_name: str, default_value=None):
1645    health_check_types = {
1646      'HTTP': 'httpHealthCheck',
1647      'HTTPS': 'httpsHealthCheck',
1648      'HTTP2': 'http2HealthCheck',
1649      'TCP': 'tcpHealthCheck',
1650      'SSL': 'sslHealthCheck',
1651      'GRPC': 'grpcHealthCheck',
1652    }
1653    if self.type in health_check_types:
1654      health_check_data = self._resource_data.get(health_check_types[self.type])
1655      if health_check_data:
1656        return health_check_data.get(property_name) or default_value
1657    return default_value
@caching.cached_api_call(in_memory=True)
def get_health_check(project_id: str, health_check: str, region: str = None) -> object:
1660@caching.cached_api_call(in_memory=True)
1661def get_health_check(project_id: str, health_check: str, region: str = None) -> object:
1662  compute = apis.get_api('compute', 'v1', project_id)
1663  if not region or region == 'global':
1664    request = compute.healthChecks().get(project=project_id, healthCheck=health_check)
1665  else:
1666    request = compute.regionHealthChecks().get(
1667      project=project_id, healthCheck=health_check, region=region
1668    )
1669  response = request.execute(num_retries=config.API_RETRIES)
1670  return HealthCheck(project_id, response)
class NetworkEndpointGroup(gcpdiag.models.Resource):
1673class NetworkEndpointGroup(models.Resource):
1674  """A Network Endpoint Group resource."""
1675
1676  _resource_data: dict
1677  _type: str
1678
1679  def __init__(self, project_id, resource_data):
1680    super().__init__(project_id=project_id)
1681    self._resource_data = resource_data
1682
1683  @property
1684  def name(self) -> str:
1685    return self._resource_data['name']
1686
1687  @property
1688  def id(self) -> str:
1689    return self._resource_data['id']
1690
1691  @property
1692  def full_path(self) -> str:
1693    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
1694    if result:
1695      return result.group(1)
1696    else:
1697      return f'>> {self.self_link}'
1698
1699  @property
1700  def short_path(self) -> str:
1701    path = self.project_id + '/' + self.name
1702    return path
1703
1704  @property
1705  def self_link(self) -> str:
1706    return self._resource_data['selfLink']

A Network Endpoint Group resource.

NetworkEndpointGroup(project_id, resource_data)
1679  def __init__(self, project_id, resource_data):
1680    super().__init__(project_id=project_id)
1681    self._resource_data = resource_data
name: str
1683  @property
1684  def name(self) -> str:
1685    return self._resource_data['name']
id: str
1687  @property
1688  def id(self) -> str:
1689    return self._resource_data['id']
full_path: str
1691  @property
1692  def full_path(self) -> str:
1693    result = re.match(r'https://www.googleapis.com/compute/v1/(.*)', self.self_link)
1694    if result:
1695      return result.group(1)
1696    else:
1697      return f'>> {self.self_link}'

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

short_path: str
1699  @property
1700  def short_path(self) -> str:
1701    path = self.project_id + '/' + self.name
1702    return path

Returns the short name for this resource.

Note that it isn't clear from this name what kind of resource it is.

Example: 'gke1'

@caching.cached_api_call(in_memory=True)
def get_zonal_network_endpoint_groups( context: gcpdiag.models.Context) -> Mapping[str, NetworkEndpointGroup]:
1709@caching.cached_api_call(in_memory=True)
1710def get_zonal_network_endpoint_groups(
1711  context: models.Context,
1712) -> Mapping[str, NetworkEndpointGroup]:
1713  """Returns a list of Network Endpoint Groups in the project."""
1714  groups: Dict[str, NetworkEndpointGroup] = {}
1715  if not apis.is_enabled(context.project_id, 'compute'):
1716    return groups
1717  gce_api = apis.get_api('compute', 'v1', context.project_id)
1718  requests = [
1719    gce_api.networkEndpointGroups().list(project=context.project_id, zone=zone)
1720    for zone in get_gce_zones(context.project_id)
1721  ]
1722  logging.debug('listing gce networkEndpointGroups of project %s', context.project_id)
1723  items = apis_utils.execute_concurrently_with_pagination(
1724    api=gce_api,
1725    requests=requests,
1726    next_function=gce_api.networkEndpointGroups().list_next,
1727    context=context,
1728    log_text=(f'listing gce networkEndpointGroups of project {context.project_id}'),
1729  )
1730
1731  for i in items:
1732    result = re.match(
1733      r'https://www.googleapis.com/compute/v1/projects/[^/]+/zones/([^/]+)',
1734      i['selfLink'],
1735    )
1736    if not result:
1737      logging.error("instance %s selfLink didn't match regexp: %s", i['id'], i['selfLink'])
1738      continue
1739    zone = result.group(1)
1740    labels = i.get('labels', {})
1741    resource = i.get('name', '')
1742    if not context.match_project_resource(location=zone, labels=labels, resource=resource):
1743      continue
1744    data = NetworkEndpointGroup(context.project_id, i)
1745    groups[data.full_path] = data
1746  return groups

Returns a list of Network Endpoint Groups in the project.

class TargetVpnGateway(gcpdiag.models.Resource):
1749class TargetVpnGateway(models.Resource):
1750  """Represents a GCE Target VPN Gateway (Classic VPN)."""
1751
1752  _resource_data: dict
1753
1754  def __init__(self, project_id, resource_data):
1755    super().__init__(project_id=project_id)
1756    self._resource_data = resource_data
1757
1758  @property
1759  def id(self) -> str:
1760    return self._resource_data['id']
1761
1762  @property
1763  def name(self) -> str:
1764    return self._resource_data['name']
1765
1766  @property
1767  def self_link(self) -> str:
1768    return self._resource_data['selfLink']
1769
1770  @property
1771  def full_path(self) -> str:
1772    result = re.match(
1773      r'https://www.googleapis.com/compute/v1/(.*)',
1774      self._resource_data['selfLink'],
1775    )
1776    if result:
1777      return result.group(1)
1778    else:
1779      return '>> ' + self._resource_data['selfLink']
1780
1781  @property
1782  def short_path(self) -> str:
1783    return self.project_id + '/' + self.name
1784
1785  @property
1786  def region(self) -> str:
1787    m = re.search(r'/regions/([^/]+)$', self._resource_data['region'])
1788    if not m:
1789      raise RuntimeError(
1790        "can't determine region of target VPN gateway %s (%s)"
1791        % (self.name, self._resource_data['region'])
1792      )
1793    return m.group(1)

Represents a GCE Target VPN Gateway (Classic VPN).

TargetVpnGateway(project_id, resource_data)
1754  def __init__(self, project_id, resource_data):
1755    super().__init__(project_id=project_id)
1756    self._resource_data = resource_data
id: str
1758  @property
1759  def id(self) -> str:
1760    return self._resource_data['id']
name: str
1762  @property
1763  def name(self) -> str:
1764    return self._resource_data['name']
full_path: str
1770  @property
1771  def full_path(self) -> str:
1772    result = re.match(
1773      r'https://www.googleapis.com/compute/v1/(.*)',
1774      self._resource_data['selfLink'],
1775    )
1776    if result:
1777      return result.group(1)
1778    else:
1779      return '>> ' + self._resource_data['selfLink']

Returns the full path of this resource.

Example: 'projects/gcpdiag-gke-1-9b90/zones/europe-west4-a/clusters/gke1'

short_path: str
1781  @property
1782  def short_path(self) -> str:
1783    return self.project_id + '/' + self.name

Returns the short name for this resource.

Note that it isn't clear from this name what kind of resource it is.

Example: 'gke1'

region: str
1785  @property
1786  def region(self) -> str:
1787    m = re.search(r'/regions/([^/]+)$', self._resource_data['region'])
1788    if not m:
1789      raise RuntimeError(
1790        "can't determine region of target VPN gateway %s (%s)"
1791        % (self.name, self._resource_data['region'])
1792      )
1793    return m.group(1)
@caching.cached_api_call(in_memory=True)
def get_target_vpn_gateways( context: gcpdiag.models.Context) -> Mapping[str, TargetVpnGateway]:
1796@caching.cached_api_call(in_memory=True)
1797def get_target_vpn_gateways(
1798  context: models.Context,
1799) -> Mapping[str, TargetVpnGateway]:
1800  """Get a list of Classic VPN Target VPN Gateways matching the given context."""
1801
1802  gateways: Dict[str, TargetVpnGateway] = {}
1803  if not apis.is_enabled(context.project_id, 'compute'):
1804    return gateways
1805  gce_api = apis.get_api('compute', 'v1', context.project_id)
1806  request = gce_api.targetVpnGateways().aggregatedList(
1807    project=context.project_id, returnPartialSuccess=True
1808  )
1809  logging.debug('listing GCE Target VPN Gateways of project %s', context.project_id)
1810  while request:  # Continue as long as there are pages
1811    try:
1812      response = request.execute(num_retries=config.API_RETRIES)
1813      gateways_by_regions = response.get('items', {})
1814      for _, data_ in gateways_by_regions.items():
1815        if 'targetVpnGateways' not in data_:
1816          continue
1817        for gateway in data_['targetVpnGateways']:
1818          m = re.search(r'/regions/([^/]+)$', gateway['region'])
1819          if not m:
1820            continue
1821          region = m.group(1)
1822          if not context.match_project_resource(
1823            resource=gateway.get('name'), location=region
1824          ) and not context.match_project_resource(resource=gateway.get('id'), location=region):
1825            continue
1826          gateways[gateway['selfLink']] = TargetVpnGateway(context.project_id, gateway)
1827      request = gce_api.targetVpnGateways().aggregatedList_next(
1828        previous_request=request, previous_response=response
1829      )
1830    except googleapiclient.errors.HttpError as err:
1831      raise utils.GcpApiError(err) from err
1832  return gateways

Get a list of Classic VPN Target VPN Gateways matching the given context.