gcpdiag.queries.crm

Queries related to Resource Manager (projects, resources).
class Project(gcpdiag.models.Resource):
29class Project(models.Resource):
30  """Represents a Project resource.
31
32  See also the API documentation:
33  https://cloud.google.com/resource-manager/reference/rest/v3/projects/get
34  """
35
36  _id: str
37  _resource_data: dict
38  _number: int
39
40  def __init__(self, resource_data):
41    super().__init__(project_id=resource_data['projectId'])
42    self._id = resource_data['projectId']
43    self._resource_data = resource_data
44    match = re.match(r'projects/(\d+)$', resource_data['name'])
45    if not match:
46      raise ValueError(f"can't determine project id ({resource_data})")
47    self._number = int(match.group(1))
48
49  @property
50  def number(self) -> int:
51    return self._number
52
53  @property
54  def id(self) -> str:
55    """Project id (not project number)."""
56    return self._id
57
58  @property
59  def name(self) -> str:
60    return self._resource_data['displayName']
61
62  @property
63  def full_path(self) -> str:
64    return f'projects/{self._id}'
65
66  @property
67  def short_path(self) -> str:
68    return self._id
69
70  @property
71  def default_compute_service_account(self) -> str:
72    return f'{self.number}-compute@developer.gserviceaccount.com'
73
74  @property
75  def parent(self) -> str:
76    return self._resource_data['parent']

Represents a Project resource.

See also the API documentation: https://cloud.google.com/resource-manager/reference/rest/v3/projects/get

Project(resource_data)
40  def __init__(self, resource_data):
41    super().__init__(project_id=resource_data['projectId'])
42    self._id = resource_data['projectId']
43    self._resource_data = resource_data
44    match = re.match(r'projects/(\d+)$', resource_data['name'])
45    if not match:
46      raise ValueError(f"can't determine project id ({resource_data})")
47    self._number = int(match.group(1))
number: int
49  @property
50  def number(self) -> int:
51    return self._number
id: str
53  @property
54  def id(self) -> str:
55    """Project id (not project number)."""
56    return self._id

Project id (not project number).

name: str
58  @property
59  def name(self) -> str:
60    return self._resource_data['displayName']
full_path: str
62  @property
63  def full_path(self) -> str:
64    return f'projects/{self._id}'

Returns the full path of this resource.

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

short_path: str
66  @property
67  def short_path(self) -> str:
68    return self._id

Returns the short name for this resource.

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

Example: 'gke1'

default_compute_service_account: str
70  @property
71  def default_compute_service_account(self) -> str:
72    return f'{self.number}-compute@developer.gserviceaccount.com'
parent: str
74  @property
75  def parent(self) -> str:
76    return self._resource_data['parent']
@caching.cached_api_call
def get_project(project_id: str) -> Project:
 79@caching.cached_api_call
 80def get_project(project_id: str) -> Project:
 81  """Attempts to retrieve project details for the supplied project id or number.
 82  If the project is found/accessible, it returns a Project object with the resource data.
 83  If the project cannot be retrieved, the application raises one of the exceptions below.
 84
 85  Args:
 86      project_id (str): The project id or number of
 87      the project (e.g., "123456789", "example-project").
 88
 89  Returns:
 90      Project: An object representing the project's full details.
 91
 92  Raises:
 93      utils.GcpApiError: If there is an issue calling the GCP/HTTP Error API.
 94
 95  Usage:
 96      When using project identifier from gcpdiag.models.Context
 97
 98      project = crm.get_project(context.project_id)
 99
100      An unknown project identifier
101      try:
102        project = crm.get_project("123456789")
103      except:
104        # Handle exception
105      else:
106        # use project data
107  """
108  try:
109    logging.debug('retrieving project %s ', project_id)
110    crm_api = apis.get_api('cloudresourcemanager', 'v3', project_id)
111    request = crm_api.projects().get(name=f'projects/{project_id}')
112    response = request.execute(num_retries=config.API_RETRIES)
113  except googleapiclient.errors.HttpError as e:
114    error = utils.GcpApiError(response=e)
115    if 'IAM_PERMISSION_DENIED' == error.reason:
116      error.message = (
117        "Authenticated account doesn't have access to project details of "
118        f'{project_id}.\nExecute:\n'
119        f'gcloud projects add-iam-policy-binding {project_id} '
120        '--role=roles/viewer '
121        '--member="user|group|serviceAccount:EMAIL_ACCOUNT"'
122      )
123    else:
124      error.message = f"can't access project {project_id}: {error.message}."
125    logging.debug('An Http Error occurred whiles accessing projects.get \n\n%s', e)
126    raise error from e
127  else:
128    return Project(resource_data=response)

Attempts to retrieve project details for the supplied project id or number. If the project is found/accessible, it returns a Project object with the resource data. If the project cannot be retrieved, the application raises one of the exceptions below.

Arguments:
  • project_id (str): The project id or number of
  • the project (e.g., "123456789", "example-project").
Returns:

Project: An object representing the project's full details.

Raises:
  • utils.GcpApiError: If there is an issue calling the GCP/HTTP Error API.
Usage:

When using project identifier from gcpdiag.models.Context

project = crm.get_project(context.project_id)

An unknown project identifier try: project = crm.get_project("123456789") except: # Handle exception else: # use project data

@caching.cached_api_call
def get_all_projects_in_parent(project_id: str) -> List[gcpdiag.queries.billing.ProjectBillingInfo]:
131@caching.cached_api_call
132def get_all_projects_in_parent(project_id: str) -> List[ProjectBillingInfo]:
133  """Get all projects in the Parent Folder that current user has
134  permission to view"""
135  projects: List[ProjectBillingInfo] = []
136  if (not project_id) or (not apis.is_enabled(project_id, 'cloudbilling')):
137    return projects
138  project = get_project(project_id)
139  p_filter = (
140    'parent.type:'
141    + project.parent.split('/')[0][:-1]
142    + ' parent.id:'
143    + project.parent.split('/')[1]
144    if project.parent
145    else ''
146  )
147
148  api = apis.get_api('cloudresourcemanager', 'v3')
149  for p in apis_utils.list_all(
150    request=api.projects().search(query=p_filter),
151    next_function=api.projects().search_next,
152    response_keyword='projects',
153  ):
154    try:
155      crm_api = apis.get_api('cloudresourcemanager', 'v3', p['projectId'])
156      p_name = 'projects/' + p['projectId'] if 'projects/' not in p['projectId'] else p['projectId']
157      request = crm_api.projects().get(name=p_name)
158      response = request.execute(num_retries=config.API_RETRIES)
159      projects.append(get_billing_info(response['projectId']))
160    except (utils.GcpApiError, googleapiclient.errors.HttpError) as error:
161      if isinstance(error, googleapiclient.errors.HttpError):
162        error = utils.GcpApiError(error)
163      if error.reason in ['IAM_PERMISSION_DENIED', 'USER_PROJECT_DENIED', 'SERVICE_DISABLED']:
164        # skip projects that user does not have permissions on
165        continue
166      else:
167        print(
168          f'[ERROR]: An Http Error occurred whiles accessing projects.get \n\n{error}',
169          file=sys.stderr,
170        )
171      raise error from error
172  return projects

Get all projects in the Parent Folder that current user has permission to view

class Organization(gcpdiag.models.Resource):
175class Organization(models.Resource):
176  """Represents an Organization resource.
177
178  See also the API documentation:
179  https://cloud.google.com/resource-manager/reference/rest/v1/organizations/get
180  """
181
182  _resource_data: dict
183
184  def __init__(self, project_id, resource_data):
185    super().__init__(project_id=project_id)
186    self._resource_data = resource_data
187
188  @property
189  def id(self) -> str:
190    """The numeric organization ID."""
191    # Note: organization ID is returned in the format 'organizations/12345'
192    return self._resource_data['name'].split('/')[-1]
193
194  @property
195  def name(self) -> str:
196    """The organization's display name."""
197    return self._resource_data['displayName']
198
199  @property
200  def full_path(self) -> str:
201    return self._resource_data['name']
202
203  @property
204  def short_path(self) -> str:
205    return f'organizations/{self.id}'

Represents an Organization resource.

See also the API documentation: https://cloud.google.com/resource-manager/reference/rest/v1/organizations/get

Organization(project_id, resource_data)
184  def __init__(self, project_id, resource_data):
185    super().__init__(project_id=project_id)
186    self._resource_data = resource_data
id: str
188  @property
189  def id(self) -> str:
190    """The numeric organization ID."""
191    # Note: organization ID is returned in the format 'organizations/12345'
192    return self._resource_data['name'].split('/')[-1]

The numeric organization ID.

name: str
194  @property
195  def name(self) -> str:
196    """The organization's display name."""
197    return self._resource_data['displayName']

The organization's display name.

full_path: str
199  @property
200  def full_path(self) -> str:
201    return self._resource_data['name']

Returns the full path of this resource.

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

short_path: str
203  @property
204  def short_path(self) -> str:
205    return f'organizations/{self.id}'

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
def get_organization( project_id: str, skip_error_print: bool = True) -> Organization | None:
208@caching.cached_api_call
209def get_organization(project_id: str, skip_error_print: bool = True) -> Organization | None:
210  """Retrieves the parent Organization for a given project.
211
212  This function first finds the project's ancestry to identify the
213  organization ID, then fetches the organization's details.
214
215  Args:
216      project_id (str): The ID of the project whose organization is to be fetched.
217
218  Returns:
219      An Organization object if the project belongs to an organization,
220      otherwise None.
221
222  Raises:
223        utils.GcpApiError: If there is an issue calling the GCP/HTTP Error API.
224  """
225  try:
226    logging.debug('retrieving ancestry for project %s', project_id)
227    crm_v1_api = apis.get_api('cloudresourcemanager', 'v1', project_id)
228    ancestry_request = crm_v1_api.projects().getAncestry(projectId=project_id)
229    ancestry_response = ancestry_request.execute(num_retries=config.API_RETRIES)
230
231    org_id = None
232    for ancestor in ancestry_response.get('ancestor', []):
233      if ancestor.get('resourceId', {}).get('type') == 'organization':
234        org_id = ancestor['resourceId']['id']
235        break
236
237    if not org_id:
238      logging.debug('project %s is not part of an organization', project_id)
239      return None
240
241    crm_v1_api = apis.get_api('cloudresourcemanager', 'v1', project_id)
242    org_request = crm_v1_api.organizations().get(name=f'organizations/{org_id}')
243    org_response = org_request.execute(num_retries=config.API_RETRIES)
244
245    return Organization(project_id=project_id, resource_data=org_response)
246
247  except googleapiclient.errors.HttpError as e:
248    error = utils.GcpApiError(response=e)
249    if not skip_error_print:
250      print(
251        f"[ERROR]: can't access organization for project {project_id}: {error.message}.",
252        file=sys.stderr,
253      )
254      print(
255        f'[DEBUG]: An Http Error occurred while accessing organization details \n\n{e}',
256        file=sys.stderr,
257      )
258    raise error from e

Retrieves the parent Organization for a given project.

This function first finds the project's ancestry to identify the organization ID, then fetches the organization's details.

Arguments:
  • project_id (str): The ID of the project whose organization is to be fetched.
Returns:

An Organization object if the project belongs to an organization, otherwise None.

Raises:
  • utils.GcpApiError: If there is an issue calling the GCP/HTTP Error API.