527 lines
15 KiB
Python
527 lines
15 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2014 Google LLC. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""A library that is used to support logging commands."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
from apitools.base.py import encoding
|
|
from apitools.base.py import extra_types
|
|
from googlecloudsdk.api_lib.resource_manager import folders
|
|
from googlecloudsdk.api_lib.util import apis as core_apis
|
|
from googlecloudsdk.calliope import arg_parsers
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.command_lib.resource_manager import completers
|
|
from googlecloudsdk.command_lib.util.apis import arg_utils
|
|
from googlecloudsdk.command_lib.util.args import common_args
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import log as sdk_log
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core import resources
|
|
from googlecloudsdk.core import yaml
|
|
|
|
DEFAULT_API_VERSION = 'v2'
|
|
|
|
|
|
class Error(exceptions.Error):
|
|
"""Base error for this module."""
|
|
|
|
|
|
class InvalidJSONValueError(Error):
|
|
"""Invalid JSON value error."""
|
|
|
|
|
|
def GetClient():
|
|
"""Returns the client for the logging API."""
|
|
return core_apis.GetClientInstance('logging', DEFAULT_API_VERSION)
|
|
|
|
|
|
def GetMessages():
|
|
"""Returns the messages for the logging API."""
|
|
return core_apis.GetMessagesModule('logging', DEFAULT_API_VERSION)
|
|
|
|
|
|
def GetCurrentProjectParent():
|
|
"""Returns the relative resource path to the current project."""
|
|
project = properties.VALUES.core.project.Get(required=True)
|
|
project_ref = resources.REGISTRY.Parse(
|
|
project, collection='cloudresourcemanager.projects'
|
|
)
|
|
return project_ref.RelativeName()
|
|
|
|
|
|
def GetSinkReference(sink_name, args):
|
|
"""Returns the appropriate sink resource based on args."""
|
|
return resources.REGISTRY.Parse(
|
|
sink_name,
|
|
params={GetIdFromArgs(args): GetParentResourceFromArgs(args).Name()},
|
|
collection=GetCollectionFromArgs(args, 'sinks'),
|
|
)
|
|
|
|
|
|
def GetOperationReference(operation_name, args):
|
|
"""Returns the appropriate operation resource based on args."""
|
|
return resources.REGISTRY.Parse(
|
|
operation_name,
|
|
params={
|
|
GetIdFromArgs(args): GetParentResourceFromArgs(args).Name(),
|
|
'locationsId': args.location,
|
|
},
|
|
collection=GetCollectionFromArgs(args, 'locations.operations'),
|
|
)
|
|
|
|
|
|
def FormatTimestamp(timestamp):
|
|
"""Returns a string representing timestamp in RFC3339 format.
|
|
|
|
Args:
|
|
timestamp: A datetime.datetime object.
|
|
|
|
Returns:
|
|
A timestamp string in format, which is accepted by Cloud Logging.
|
|
"""
|
|
return timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
|
|
|
|
|
|
def ConvertToJsonObject(json_string):
|
|
"""Tries to convert the JSON string into JsonObject."""
|
|
try:
|
|
return extra_types.JsonProtoDecoder(json_string)
|
|
except Exception as e:
|
|
raise InvalidJSONValueError('Invalid JSON value: %s' % e)
|
|
|
|
|
|
def AddParentArgs(parser, help_string, exclude_billing_account=False):
|
|
"""Adds arguments for parent of the entities.
|
|
|
|
Args:
|
|
parser: parser to which arguments are added.
|
|
help_string: text that is prepended to help for each argument.
|
|
exclude_billing_account: whether to exclude the billing account argument.
|
|
"""
|
|
entity_group = parser.add_mutually_exclusive_group()
|
|
entity_group.add_argument(
|
|
'--organization',
|
|
required=False,
|
|
metavar='ORGANIZATION_ID',
|
|
completer=completers.OrganizationCompleter,
|
|
help='Organization of the {0}.'.format(help_string),
|
|
)
|
|
|
|
entity_group.add_argument(
|
|
'--folder',
|
|
required=False,
|
|
metavar='FOLDER_ID',
|
|
help='Folder of the {0}.'.format(help_string),
|
|
)
|
|
if not exclude_billing_account:
|
|
entity_group.add_argument(
|
|
'--billing-account',
|
|
required=False,
|
|
metavar='BILLING_ACCOUNT_ID',
|
|
help='Billing account of the {0}.'.format(help_string),
|
|
)
|
|
|
|
common_args.ProjectArgument(
|
|
help_text_to_prepend='Project of the {0}.'.format(help_string)
|
|
).AddToParser(entity_group)
|
|
|
|
|
|
def AddBucketLocationArg(parser, required, help_string):
|
|
"""Adds a location argument.
|
|
|
|
Args:
|
|
parser: parser to which to add args.
|
|
required: whether the arguments is required.
|
|
help_string: the help string.
|
|
"""
|
|
# We validate that the location is non-empty since otherwise the
|
|
# error message from the API can be confusing. We leave the rest of the
|
|
# validation to the API.
|
|
parser.add_argument(
|
|
'--location',
|
|
required=required,
|
|
metavar='LOCATION',
|
|
type=arg_parsers.RegexpValidator(r'.+', 'must be non-empty'),
|
|
help=help_string,
|
|
)
|
|
|
|
|
|
def GetProjectResource(project):
|
|
"""Returns the resource for the current project."""
|
|
return resources.REGISTRY.Parse(
|
|
project or properties.VALUES.core.project.Get(required=True),
|
|
collection='cloudresourcemanager.projects',
|
|
)
|
|
|
|
|
|
def GetOrganizationResource(organization):
|
|
"""Returns the resource for the organization.
|
|
|
|
Args:
|
|
organization: organization.
|
|
|
|
Returns:
|
|
The resource.
|
|
"""
|
|
return resources.REGISTRY.Parse(
|
|
organization, collection='cloudresourcemanager.organizations'
|
|
)
|
|
|
|
|
|
def GetFolderResource(folder):
|
|
"""Returns the resource for the folder.
|
|
|
|
Args:
|
|
folder: folder.
|
|
|
|
Returns:
|
|
The resource.
|
|
"""
|
|
return folders.FoldersRegistry().Parse(
|
|
folder, collection='cloudresourcemanager.folders'
|
|
)
|
|
|
|
|
|
def GetBillingAccountResource(billing_account):
|
|
"""Returns the resource for the billing_account.
|
|
|
|
Args:
|
|
billing_account: billing account.
|
|
|
|
Returns:
|
|
The resource.
|
|
"""
|
|
return resources.REGISTRY.Parse(
|
|
billing_account, collection='cloudbilling.billingAccounts'
|
|
)
|
|
|
|
|
|
def GetParentResourceFromArgs(args, exclude_billing_account=False):
|
|
"""Returns the parent resource derived from the given args.
|
|
|
|
Args:
|
|
args: command line args.
|
|
exclude_billing_account: whether to exclude the billing account argument.
|
|
|
|
Returns:
|
|
The parent resource.
|
|
"""
|
|
if args.organization:
|
|
return GetOrganizationResource(args.organization)
|
|
elif args.folder:
|
|
return GetFolderResource(args.folder)
|
|
elif not exclude_billing_account and args.billing_account:
|
|
return GetBillingAccountResource(args.billing_account)
|
|
else:
|
|
return GetProjectResource(args.project)
|
|
|
|
|
|
def GetParentFromArgs(args, exclude_billing_account=False):
|
|
"""Returns the relative path to the parent from args.
|
|
|
|
Args:
|
|
args: command line args.
|
|
exclude_billing_account: whether to exclude the billing account argument.
|
|
|
|
Returns:
|
|
The relative path. e.g. 'projects/foo', 'folders/1234'.
|
|
"""
|
|
return GetParentResourceFromArgs(args, exclude_billing_account).RelativeName()
|
|
|
|
|
|
def GetBucketLocationFromArgs(args):
|
|
"""Returns the relative path to the bucket location from args.
|
|
|
|
Args:
|
|
args: command line args.
|
|
|
|
Returns:
|
|
The relative path. e.g. 'projects/foo/locations/bar'.
|
|
"""
|
|
if args.location:
|
|
location = args.location
|
|
else:
|
|
location = '-'
|
|
|
|
return CreateResourceName(GetParentFromArgs(args), 'locations', location)
|
|
|
|
|
|
def GetIdFromArgs(args):
|
|
"""Returns the id to be used for constructing resource paths.
|
|
|
|
Args:
|
|
args: command line args.
|
|
|
|
Returns:
|
|
The id to be used..
|
|
"""
|
|
if args.organization:
|
|
return 'organizationsId'
|
|
elif args.folder:
|
|
return 'foldersId'
|
|
elif args.billing_account:
|
|
return 'billingAccountsId'
|
|
else:
|
|
return 'projectsId'
|
|
|
|
|
|
def GetCollectionFromArgs(args, collection_suffix):
|
|
"""Returns the collection derived from args and the suffix.
|
|
|
|
Args:
|
|
args: command line args.
|
|
collection_suffix: suffix of collection
|
|
|
|
Returns:
|
|
The collection.
|
|
"""
|
|
if args.organization:
|
|
prefix = 'logging.organizations'
|
|
elif args.folder:
|
|
prefix = 'logging.folders'
|
|
elif args.billing_account:
|
|
prefix = 'logging.billingAccounts'
|
|
else:
|
|
prefix = 'logging.projects'
|
|
return '{0}.{1}'.format(prefix, collection_suffix)
|
|
|
|
|
|
def CreateResourceName(parent, collection, resource_id):
|
|
"""Creates the full resource name.
|
|
|
|
Args:
|
|
parent: The project or organization id as a resource name, e.g.
|
|
'projects/my-project' or 'organizations/123'.
|
|
collection: The resource collection. e.g. 'logs'
|
|
resource_id: The id within the collection , e.g. 'my-log'.
|
|
|
|
Returns:
|
|
resource, e.g. projects/my-project/logs/my-log.
|
|
"""
|
|
# id needs to be escaped to create a valid resource name - i.e it is a
|
|
# requirement of the Cloud Logging API that each component of a resource
|
|
# name must have no slashes.
|
|
return '{0}/{1}/{2}'.format(
|
|
parent, collection, resource_id.replace('/', '%2F')
|
|
)
|
|
|
|
|
|
def CreateLogResourceName(parent, log_id):
|
|
"""Creates the full log resource name.
|
|
|
|
Args:
|
|
parent: The project or organization id as a resource name, e.g.
|
|
'projects/my-project' or 'organizations/123'.
|
|
log_id: The log id, e.g. 'my-log'. This may already be a resource name, in
|
|
which case parent is ignored and log_id is returned directly, e.g.
|
|
CreateLogResourceName('projects/ignored', 'projects/bar/logs/my-log')
|
|
returns 'projects/bar/logs/my-log'
|
|
|
|
Returns:
|
|
Log resource, e.g. projects/my-project/logs/my-log.
|
|
"""
|
|
if '/logs/' in log_id:
|
|
return log_id
|
|
return CreateResourceName(parent, 'logs', log_id)
|
|
|
|
|
|
def ExtractLogId(log_resource):
|
|
"""Extracts only the log id and restore original slashes.
|
|
|
|
Args:
|
|
log_resource: The full log uri e.g projects/my-projects/logs/my-log.
|
|
|
|
Returns:
|
|
A log id that can be used in other commands.
|
|
"""
|
|
log_id = log_resource.split('/logs/', 1)[1]
|
|
return log_id.replace('%2F', '/')
|
|
|
|
|
|
def IndexTypeToEnum(index_type):
|
|
"""Converts an Index Type String Literal to an Enum.
|
|
|
|
Args:
|
|
index_type: The index type e.g INDEX_TYPE_STRING.
|
|
|
|
Returns:
|
|
A IndexConfig.TypeValueValuesEnum mapped e.g
|
|
TypeValueValuesEnum(INDEX_TYPE_INTEGER, 2) .
|
|
|
|
Will return a Parser error if an incorrect value is provided.
|
|
"""
|
|
return arg_utils.ChoiceToEnum(
|
|
index_type,
|
|
GetMessages().IndexConfig.TypeValueValuesEnum,
|
|
valid_choices=['INDEX_TYPE_STRING', 'INDEX_TYPE_INTEGER'],
|
|
)
|
|
|
|
|
|
def PrintPermissionInstructions(destination, writer_identity):
|
|
"""Prints a message to remind the user to set up permissions for a sink.
|
|
|
|
Args:
|
|
destination: the sink destination (either bigquery or cloud storage).
|
|
writer_identity: identity to which to grant write access.
|
|
"""
|
|
if writer_identity:
|
|
grantee = '`{0}`'.format(writer_identity)
|
|
else:
|
|
grantee = 'the group `cloud-logs@google.com`'
|
|
|
|
if destination.startswith('bigquery'):
|
|
sdk_log.status.Print(
|
|
'Please remember to grant {0} the BigQuery Data '
|
|
'Editor role on the dataset.'.format(grantee)
|
|
)
|
|
elif destination.startswith('storage'):
|
|
sdk_log.status.Print(
|
|
'Please remember to grant {0} the Storage Object '
|
|
'Creator role on the bucket.'.format(grantee)
|
|
)
|
|
elif destination.startswith('pubsub'):
|
|
sdk_log.status.Print(
|
|
'Please remember to grant {0} the Pub/Sub Publisher '
|
|
'role on the topic.'.format(grantee)
|
|
)
|
|
sdk_log.status.Print(
|
|
'More information about sinks can be found at https://'
|
|
'cloud.google.com/logging/docs/export/configure_export'
|
|
)
|
|
|
|
|
|
def CreateLogMetric(
|
|
metric_name, description=None, log_filter=None, bucket_name=None, data=None
|
|
):
|
|
"""Returns a LogMetric message based on a data stream or a description/filter.
|
|
|
|
Args:
|
|
metric_name: str, the name of the metric.
|
|
description: str, a description.
|
|
log_filter: str, the filter for the metric's filter field.
|
|
bucket_name: str, the bucket name which ownes the metric.
|
|
data: str, a stream of data read from a config file.
|
|
|
|
Returns:
|
|
LogMetric, the message representing the new metric.
|
|
"""
|
|
messages = GetMessages()
|
|
if data:
|
|
contents = yaml.load(data)
|
|
metric_msg = encoding.DictToMessage(contents, messages.LogMetric)
|
|
metric_msg.name = metric_name
|
|
else:
|
|
metric_msg = messages.LogMetric(
|
|
name=metric_name,
|
|
description=description,
|
|
filter=log_filter,
|
|
bucketName=bucket_name,
|
|
)
|
|
return metric_msg
|
|
|
|
|
|
def UpdateLogMetric(
|
|
metric, description=None, log_filter=None, bucket_name=None, data=None
|
|
):
|
|
"""Updates a LogMetric message given description, filter, and/or data.
|
|
|
|
Args:
|
|
metric: LogMetric, the original metric.
|
|
description: str, updated description if any.
|
|
log_filter: str, updated filter for the metric's filter field if any.
|
|
bucket_name: str, the bucket name which ownes the metric.
|
|
data: str, a stream of data read from a config file if any.
|
|
|
|
Returns:
|
|
LogMetric, the message representing the updated metric.
|
|
"""
|
|
messages = GetMessages()
|
|
if description:
|
|
metric.description = description
|
|
if log_filter:
|
|
metric.filter = log_filter
|
|
if bucket_name:
|
|
metric.bucketName = bucket_name
|
|
if data:
|
|
# Update the top-level fields only.
|
|
update_data = yaml.load(data)
|
|
metric_diff = encoding.DictToMessage(update_data, messages.LogMetric)
|
|
for field_name in update_data:
|
|
setattr(metric, field_name, getattr(metric_diff, field_name))
|
|
return metric
|
|
|
|
|
|
def GetIamPolicy(view):
|
|
"""Get IAM policy, for a given view."""
|
|
|
|
get_iam_policy_request = (
|
|
GetMessages().LoggingProjectsLocationsBucketsViewsGetIamPolicyRequest(
|
|
resource=view
|
|
)
|
|
)
|
|
return GetClient().projects_locations_buckets_views.GetIamPolicy(
|
|
get_iam_policy_request
|
|
)
|
|
|
|
|
|
def SetIamPolicy(view, policy):
|
|
"""Set IAM policy, for a given view."""
|
|
messages = GetMessages()
|
|
|
|
policy_request = (
|
|
messages.LoggingProjectsLocationsBucketsViewsSetIamPolicyRequest(
|
|
resource=view,
|
|
setIamPolicyRequest=messages.SetIamPolicyRequest(policy=policy),
|
|
)
|
|
)
|
|
return GetClient().projects_locations_buckets_views.SetIamPolicy(
|
|
policy_request
|
|
)
|
|
|
|
|
|
def GetTagsArg():
|
|
"""Makes the base.Argument for --tags flag."""
|
|
help_parts = [
|
|
'List of tags KEY=VALUE pairs to bind.',
|
|
'Each item must be expressed as',
|
|
'`<tag-key-namespaced-name>=<tag-value-short-name>`.\n',
|
|
'Example: `123/environment=production,123/costCenter=marketing`\n',
|
|
]
|
|
return base.Argument(
|
|
'--tags',
|
|
metavar='KEY=VALUE',
|
|
type=arg_parsers.ArgDict(),
|
|
action=arg_parsers.UpdateAction,
|
|
help='\n'.join(help_parts),
|
|
hidden=True,
|
|
)
|
|
|
|
|
|
def GetTagsFromArgs(args, tags_message, tags_arg_name='tags'):
|
|
"""Makes the tags message object."""
|
|
tags = getattr(args, tags_arg_name)
|
|
if not tags:
|
|
return None
|
|
# Sorted for test stability
|
|
return tags_message(
|
|
additionalProperties=[
|
|
tags_message.AdditionalProperty(key=key, value=value)
|
|
for key, value in sorted(tags.items())
|
|
]
|
|
)
|