feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Flag utilities for `gcloud redis clusters`."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from apitools.base.py import encoding
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
def ClusterRedisConfigArgType(value):
return arg_parsers.ArgDict()(value)
def ClusterUpdateRedisConfigFlag():
return base.Argument(
'--update-redis-config',
metavar='KEY=VALUE',
type=ClusterRedisConfigArgType,
action=arg_parsers.UpdateAction,
help="""\
A list of Redis Cluster config KEY=VALUE pairs to update. If a
config parameter is already set, its value is modified; otherwise a
new Redis config parameter is added.
""",
)
def ClusterRemoveRedisConfigFlag():
return base.Argument(
'--remove-redis-config',
metavar='KEY',
type=arg_parsers.ArgList(),
action=arg_parsers.UpdateAction,
help="""\
A list of Redis Cluster config parameters to remove. Removing a non-existent
config parameter is silently ignored.""",
)
def AdditionalClusterUpdateArguments():
return [ClusterUpdateRedisConfigFlag(), ClusterRemoveRedisConfigFlag()]
def PackageClusterRedisConfig(config, messages):
return encoding.DictToAdditionalPropertyMessage(
config, messages.Cluster.RedisConfigsValue, sort_items=True
)
def ParseTimeOfDayAlpha(start_time):
return ParseTimeOfDay(start_time, 'v1alpha1')
def ParseTimeOfDayBeta(start_time):
return ParseTimeOfDay(start_time, 'v1beta1')
def ParseTimeOfDayGa(start_time):
return ParseTimeOfDay(start_time, 'v1')
def ParseTimeOfDay(start_time, api_version):
m = re.match(r'^(\d?\d):00$', start_time)
if m:
message = apis.GetMessagesModule('redis', api_version)
hour = int(m.group(1))
if hour <= 23 and hour >= 0:
return message.TimeOfDay(hours=hour)
raise arg_parsers.ArgumentTypeError(
'Failed to parse time of day: {0}, expected format: HH:00.'.format(
start_time
)
)

View File

@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Utility for Memorystore Redis clusters Cross Cluster Replication."""
from googlecloudsdk.command_lib.redis import util
from googlecloudsdk.core import exceptions
class DetachNotSupportedException(exceptions.Error):
"""Exception for when detach is not supported."""
class SwitchoverNotSupportedException(exceptions.Error):
"""Exception for when switchover is not supported."""
class DetachSecondariesNotSupportedException(exceptions.Error):
"""Exception for when detach-secondaries is not supported."""
def _GetCluster(cluster_ref, cluster_name):
client = util.GetClientForResource(cluster_ref)
messages = util.GetMessagesForResource(cluster_ref)
return client.projects_locations_clusters.Get(
messages.RedisProjectsLocationsClustersGetRequest(name=cluster_name)
)
def Switchover(cluster_ref, args, patch_request):
"""Hook to trigger the switchover to the secondary cluster."""
del args
cluster = _GetCluster(cluster_ref, patch_request.name)
messages = util.GetMessagesForResource(cluster_ref)
if (
cluster.crossClusterReplicationConfig is None
or cluster.crossClusterReplicationConfig.clusterRole
!= (
messages.CrossClusterReplicationConfig.ClusterRoleValueValuesEnum.SECONDARY
)
):
raise SwitchoverNotSupportedException(
'Cluster {} is not a secondary cluster. Please run switchover on a'
' secondary cluster.'.format(cluster.name)
)
# The current primary cluster will become a secondary cluster.
new_secondary_clusters = [
messages.RemoteCluster(
cluster=cluster.crossClusterReplicationConfig.primaryCluster.cluster
)
]
# Add the rest of the secondary clusters to the new secondary clusters list.
for (
curr_sec_cluster
) in cluster.crossClusterReplicationConfig.membership.secondaryClusters:
# Filter out the current cluster from the secondary clusters list.
if curr_sec_cluster.cluster != cluster.name:
new_secondary_clusters.append(
messages.RemoteCluster(cluster=curr_sec_cluster.cluster)
)
new_ccr_config = messages.CrossClusterReplicationConfig(
clusterRole=messages.CrossClusterReplicationConfig.ClusterRoleValueValuesEnum.PRIMARY,
secondaryClusters=new_secondary_clusters,
)
patch_request.updateMask = 'cross_cluster_replication_config'
patch_request.cluster = messages.Cluster(
crossClusterReplicationConfig=new_ccr_config
)
return patch_request
def Detach(cluster_ref, args, patch_request):
"""Hook to detach the secondary cluster from the primary cluster."""
del args
cluster = _GetCluster(cluster_ref, patch_request.name)
messages = util.GetMessagesForResource(cluster_ref)
if (
cluster.crossClusterReplicationConfig is None
or cluster.crossClusterReplicationConfig.clusterRole
!= (
messages.CrossClusterReplicationConfig.ClusterRoleValueValuesEnum.SECONDARY
)
):
raise DetachNotSupportedException(
'Cluster {} is not a secondary cluster. Please run detach on a'
' secondary cluster.'.format(cluster.name)
)
new_ccr_config = messages.CrossClusterReplicationConfig(
clusterRole=messages.CrossClusterReplicationConfig.ClusterRoleValueValuesEnum.NONE
)
patch_request.updateMask = 'cross_cluster_replication_config'
patch_request.cluster = messages.Cluster(
crossClusterReplicationConfig=new_ccr_config
)
return patch_request
def DetachSecondaries(cluster_ref, args, patch_request):
"""Hook to detach the given secondary clusters from the primary cluster."""
cluster = _GetCluster(cluster_ref, patch_request.name)
messages = util.GetMessagesForResource(cluster_ref)
if (
cluster.crossClusterReplicationConfig is None
or cluster.crossClusterReplicationConfig.clusterRole
!= (
messages.CrossClusterReplicationConfig.ClusterRoleValueValuesEnum.PRIMARY
)
):
raise DetachSecondariesNotSupportedException(
'Cluster {} is not a primary cluster. Please run detach-secondaries on'
' a primary cluster.'.format(cluster.name)
)
current_secondary_clusters = (
cluster.crossClusterReplicationConfig.secondaryClusters
)
new_secondary_clusters = []
for secondary_cluster in current_secondary_clusters:
if secondary_cluster.cluster not in args.clusters_to_detach:
new_secondary_clusters.append(secondary_cluster)
new_ccr_config = messages.CrossClusterReplicationConfig()
if not new_secondary_clusters:
new_ccr_config.clusterRole = (
messages.CrossClusterReplicationConfig.ClusterRoleValueValuesEnum.NONE
)
else:
new_ccr_config.clusterRole = (
messages.CrossClusterReplicationConfig.ClusterRoleValueValuesEnum.PRIMARY
)
new_ccr_config.secondaryClusters = new_secondary_clusters
patch_request.updateMask = 'cross_cluster_replication_config'
patch_request.cluster = messages.Cluster(
crossClusterReplicationConfig=new_ccr_config
)
return patch_request

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""Utilities for reschedule Redis cluster maintenance window."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
class Error(Exception):
"""Exceptions for this module."""
class NoScheduleTimeSpecifiedError(Error):
"""Error for calling update command with no args that represent fields."""
def CheckSpecificTimeField(unused_instance_ref, args, patch_request):
"""Hook to check specific time field of the request."""
if args.IsSpecified('reschedule_type'):
if args.reschedule_type.lower() == 'specific-time':
if args.IsSpecified('schedule_time'):
return patch_request
else:
raise NoScheduleTimeSpecifiedError('Must specify schedule time')
return patch_request

View File

@@ -0,0 +1,194 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 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.
"""Utility for updating Memorystore Redis clusters."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import encoding
from googlecloudsdk.command_lib.redis import cluster_util
from googlecloudsdk.command_lib.redis import util
class Error(Exception):
"""Exceptions for this module."""
class InvalidTimeOfDayError(Error):
"""Error for passing invalid time of day."""
def AddFieldToUpdateMask(field, patch_request):
update_mask = patch_request.updateMask
if update_mask:
if update_mask.count(field) == 0:
patch_request.updateMask = update_mask + ',' + field
else:
patch_request.updateMask = field
return patch_request
def AddNewRedisClusterConfigs(cluster_ref, redis_configs_dict, patch_request):
messages = util.GetMessagesForResource(cluster_ref)
new_redis_configs = cluster_util.PackageClusterRedisConfig(
redis_configs_dict, messages
)
patch_request.cluster.redisConfigs = new_redis_configs
patch_request = AddFieldToUpdateMask('redis_configs', patch_request)
return patch_request
def UpdateReplicaCount(unused_cluster_ref, args, patch_request):
"""Hook to add replica count to the redis cluster update request."""
if args.IsSpecified('replica_count'):
patch_request.cluster.replicaCount = args.replica_count
patch_request = AddFieldToUpdateMask('replica_count', patch_request)
return patch_request
def UpdateMaintenanceWindowPolicy(unused_cluster_ref, args, patch_request):
"""Hook to update maintenance window policy to the update mask of the request."""
if (
args.IsSpecified('maintenance_window_day')
or args.IsSpecified('maintenance_window_hour')
):
patch_request = AddFieldToUpdateMask('maintenance_window', patch_request)
return patch_request
def UpdateMaintenanceWindowAny(unused_cluster_ref, args, patch_request):
"""Hook to remove maintenance policy."""
if args.IsSpecified('maintenance_window_any'):
patch_request.cluster.maintenancePolicy = None
patch_request = AddFieldToUpdateMask('maintenance_window', patch_request)
return patch_request
def UpdateSimulateMaintenanceEvent(unused_cluster_ref, args, patch_request):
"""Hook to update simulate maintenance event to the update mask of the request."""
if args.IsSpecified('simulate_maintenance_event'):
patch_request.cluster.simulateMaintenanceEvent = (
args.simulate_maintenance_event
)
patch_request = AddFieldToUpdateMask(
'simulate_maintenance_event', patch_request
)
return patch_request
def UpdateShardCount(unused_cluster_ref, args, patch_request):
"""Hook to add shard count to the redis cluster update request."""
if args.IsSpecified('shard_count'):
patch_request.cluster.shardCount = args.shard_count
patch_request = AddFieldToUpdateMask('shard_count', patch_request)
return patch_request
def UpdateMaintenanceVersion(unused_cluster_ref, args, patch_request):
"""Hook to add maintenance version to the redis cluster update request."""
if args.IsSpecified('maintenance_version'):
patch_request.cluster.maintenanceVersion = args.maintenance_version
patch_request = AddFieldToUpdateMask('maintenance_version', patch_request)
return patch_request
def UpdateDeletionProtection(unused_cluster_ref, args, patch_request):
"""Hook to add delete protection to the redis cluster update request."""
if args.IsSpecified('deletion_protection'):
patch_request.cluster.deletionProtectionEnabled = args.deletion_protection
patch_request = AddFieldToUpdateMask(
'deletion_protection_enabled', patch_request
)
return patch_request
def UpdateRedisConfigs(cluster_ref, args, patch_request):
"""Hook to update redis configs to the redis cluster update request."""
if args.IsSpecified('update_redis_config'):
config_dict = {}
if getattr(patch_request.cluster, 'redisConfigs', None):
config_dict = encoding.MessageToDict(patch_request.cluster.redisConfigs)
config_dict.update(args.update_redis_config)
patch_request = AddNewRedisClusterConfigs(
cluster_ref, config_dict, patch_request
)
return patch_request
def RemoveRedisConfigs(cluster_ref, args, patch_request):
"""Hook to remove redis configs to the redis cluster update request."""
if not getattr(patch_request.cluster, 'redisConfigs', None):
return patch_request
if args.IsSpecified('remove_redis_config'):
config_dict = encoding.MessageToDict(patch_request.cluster.redisConfigs)
for removed_key in args.remove_redis_config:
config_dict.pop(removed_key, None)
patch_request = AddNewRedisClusterConfigs(
cluster_ref, config_dict, patch_request
)
return patch_request
def UpdatePersistenceConfig(unused_cluster_ref, args, patch_request):
"""Hook to add persistence config to the redis cluster update request."""
if (
args.IsSpecified('persistence_mode')
or args.IsSpecified('rdb_snapshot_period')
or args.IsSpecified('rdb_snapshot_start_time')
or args.IsSpecified('aof_append_fsync')
):
patch_request = AddFieldToUpdateMask('persistence_config', patch_request)
# Before update, gcloud will `get` the cluster and overrides the existing
# persistence config with the input.
# We can't send both RDB & AOF config to the backend. Explicitly zero out
# the non selected config so only one mode is sent to backend.
if patch_request.cluster.persistenceConfig:
if not args.IsSpecified('rdb_snapshot_period') and not args.IsSpecified(
'rdb_snapshot_start_time'
):
patch_request.cluster.persistenceConfig.rdbConfig = None
if not args.IsSpecified('aof_append_fsync'):
patch_request.cluster.persistenceConfig.aofConfig = None
return patch_request
def UpdateNodeType(unused_cluster_ref, args, patch_request):
"""Hook to add node type to the redis cluster update request."""
if args.IsSpecified('node_type'):
patch_request = AddFieldToUpdateMask('node_type', patch_request)
return patch_request
def UpdateAutomatedBackupConfig(unused_cluster_ref, args, patch_request):
"""Hook to add automated backup config to the redis cluster update request."""
if (
args.IsSpecified('automated_backup_ttl')
or args.IsSpecified('automated_backup_start_time')
or args.IsSpecified('automated_backup_mode')
):
patch_request = AddFieldToUpdateMask(
'automated_backup_config', patch_request
)
return patch_request
def CheckMaintenanceWindowStartTimeField(maintenance_window_start_time):
if maintenance_window_start_time < 0 or maintenance_window_start_time > 23:
raise InvalidTimeOfDayError(
'A valid time of day must be specified (0, 23) hours.'
)
return maintenance_window_start_time

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Instances utilities for `gcloud redis` commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from apitools.base.py import encoding
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
NETWORK_REGEX = "^projects/(.*)/global/networks/(.*)$"
def ParseInstanceNetworkArg(network):
if re.search(NETWORK_REGEX, network):
# return network if it is a valid full network path
return network
project = properties.VALUES.core.project.GetOrFail()
network_ref = resources.REGISTRY.Create(
"compute.networks", project=project, network=network)
return network_ref.RelativeName()
def PackageInstanceLabels(labels, messages):
return encoding.DictToAdditionalPropertyMessage(
labels, messages.Instance.LabelsValue, sort_items=True)
def AddDefaultReplicaCount(unused_instance_ref, args, post_request):
"""Hook to update default replica count."""
if args.IsSpecified("replica_count"):
return post_request
if args.read_replicas_mode == "read-replicas-enabled":
post_request.instance.replicaCount = 2
return post_request

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""Utilities for describe Memorystore Redis instances."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
def FormatResponse(response, _):
"""Hook to convert seconds into minutes for duration field."""
modified_response = {}
if response.authorizedNetwork:
modified_response['authorizedNetwork'] = response.authorizedNetwork
if response.availableMaintenanceVersions:
modified_response[
'availableMaintenanceVersions'] = response.availableMaintenanceVersions
if response.connectMode:
modified_response['connectMode'] = response.connectMode
if response.createTime:
modified_response['createTime'] = response.createTime
if response.currentLocationId:
modified_response['currentLocationId'] = response.currentLocationId
if response.host:
modified_response['host'] = response.host
if response.locationId:
modified_response['locationId'] = response.locationId
if response.maintenanceSchedule:
modified_response['maintenanceSchedule'] = response.maintenanceSchedule
if response.maintenanceVersion:
modified_response['maintenanceVersion'] = response.maintenanceVersion
if response.memorySizeGb:
modified_response['memorySizeGb'] = response.memorySizeGb
if response.name:
modified_response['name'] = response.name
if response.persistenceIamIdentity:
modified_response[
'persistenceIamIdentity'] = response.persistenceIamIdentity
if response.port:
modified_response['port'] = response.port
if response.readEndpoint:
modified_response['readEndpoint'] = response.readEndpoint
if response.readEndpointPort:
modified_response['readEndpointPort'] = response.readEndpointPort
if response.readReplicasMode:
modified_response['readReplicasMode'] = response.readReplicasMode
if response.redisVersion:
modified_response['redisVersion'] = response.redisVersion
if response.replicaCount:
modified_response['replicaCount'] = response.replicaCount
if response.reservedIpRange:
modified_response['reservedIpRange'] = response.reservedIpRange
if response.secondaryIpRange:
modified_response['secondaryIpRange'] = response.secondaryIpRange
if response.state:
modified_response['state'] = response.state
if response.tier:
modified_response['tier'] = response.tier
if response.transitEncryptionMode:
modified_response['transitEncryptionMode'] = response.transitEncryptionMode
if response.persistenceConfig:
modified_response['persistenceConfig'] = response.persistenceConfig
if response.maintenancePolicy:
modified_mw_policy = {}
modified_mw_policy['createTime'] = response.maintenancePolicy.createTime
modified_mw_policy['updateTime'] = response.maintenancePolicy.updateTime
modified_mwlist = []
for mw in response.maintenancePolicy.weeklyMaintenanceWindow:
item = {}
# convert seconds to minutes
duration_secs = int(mw.duration[:-1])
duration_mins = int(duration_secs/60)
item['day'] = mw.day
item['hour'] = mw.startTime.hours
item['duration'] = str(duration_mins) + ' minutes'
modified_mwlist.append(item)
modified_mw_policy['maintenanceWindow'] = modified_mwlist
modified_response['maintenancePolicy'] = modified_mw_policy
if response.nodes:
modified_node_list = []
for node in response.nodes:
item = {}
item['id'] = node.id
item['zone'] = node.zone
modified_node_list.append(item)
modified_response['nodes'] = modified_node_list
return modified_response

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""Utilities for reschedule Memorystore Redis instances maintenance window."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
class Error(Exception):
"""Exceptions for this module."""
class NoScheduleTimeSpecifiedError(Error):
"""Error for calling update command with no args that represent fields."""
def CheckSpecificTimeField(unused_instance_ref, args, patch_request):
"""Hook to check specific time field of the request."""
if args.IsSpecified('reschedule_type'):
if args.reschedule_type.lower() == 'specific-time':
if args.IsSpecified('schedule_time'):
return patch_request
else:
raise NoScheduleTimeSpecifiedError('Must specify schedule time')
return patch_request

View File

@@ -0,0 +1,242 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Utility for updating Memorystore Redis instances."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import encoding
from googlecloudsdk.command_lib.redis import util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core.console import console_io
from six.moves import filter # pylint: disable=redefined-builtin
class NoFieldsSpecified(exceptions.Error):
"""Error for calling update command with no args that represent fields."""
def CheckFieldsSpecifiedGA(unused_instance_ref, args, patch_request):
"""Checks if fields to update are registered for GA track."""
additional_update_args = [
'maintenance_version',
]
return CheckFieldsSpecifiedCommon(args, patch_request, additional_update_args)
def CheckFieldsSpecifiedBeta(unused_instance_ref, args, patch_request):
"""Checks if fields to update are registered for BETA track."""
additional_update_args = [
'maintenance_version',
]
return CheckFieldsSpecifiedCommon(args, patch_request, additional_update_args)
def CheckFieldsSpecifiedAlpha(unused_instance_ref, args, patch_request):
"""Checks if fields to update are registered for ALPHA track."""
additional_update_args = [
'maintenance_version',
]
return CheckFieldsSpecifiedCommon(args, patch_request, additional_update_args)
def CheckFieldsSpecifiedCommon(args, patch_request, additional_update_args):
"""Checks fields to update that are registered for all tracks."""
update_args = [
'clear_labels',
'display_name',
'enable_auth',
'remove_labels',
'remove_redis_config',
'size',
'update_labels',
'update_redis_config',
'read_replicas_mode',
'secondary_ip_range',
'replica_count',
'persistence_mode',
'rdb_snapshot_period',
'rdb_snapshot_start_time',
'maintenance_window_day',
'maintenance_window_hour',
'maintenance_window_any',
] + additional_update_args
if list(filter(args.IsSpecified, update_args)):
return patch_request
raise NoFieldsSpecified(
'Must specify at least one valid instance parameter to update')
def AddFieldToUpdateMask(field, patch_request):
update_mask = patch_request.updateMask
if update_mask:
if update_mask.count(field) == 0:
patch_request.updateMask = update_mask + ',' + field
else:
patch_request.updateMask = field
return patch_request
def AddDisplayName(unused_instance_ref, args, patch_request):
if args.IsSpecified('display_name'):
patch_request.instance.displayName = args.display_name
patch_request = AddFieldToUpdateMask('display_name', patch_request)
return patch_request
def _WarnForDestructiveSizeUpdate(instance_ref, instance):
"""Adds prompt that warns about a destructive size update."""
messages = util.GetMessagesForResource(instance_ref)
message = 'Change to instance size requested. '
if instance.tier == messages.Instance.TierValueValuesEnum.BASIC:
message += ('Scaling a Basic Tier instance may result in data loss, '
'and the instance will briefly be unavailable during the '
'operation. ')
elif instance.tier == messages.Instance.TierValueValuesEnum.STANDARD_HA:
message += ('Scaling a Standard Tier instance may result in the loss of '
'unreplicated data, and the instance will be briefly '
'unavailable during failover. ')
else:
# To future proof this against new instance types, add a default message.
message += ('Scaling a redis instance may result in data loss, and the '
'instance will be briefly unavailable during scaling. ')
message += (
'For more information please take a look at '
'https://cloud.google.com/memorystore/docs/redis/scaling-instances')
console_io.PromptContinue(
message=message,
prompt_string='Do you want to proceed with update?',
cancel_on_no=True)
def AddSize(instance_ref, args, patch_request):
"""Python hook to add size update to the redis instance update request."""
if args.IsSpecified('size'):
# Changing size is destructive and users should be warned before proceeding.
_WarnForDestructiveSizeUpdate(instance_ref, patch_request.instance)
patch_request.instance.memorySizeGb = args.size
patch_request = AddFieldToUpdateMask('memory_size_gb', patch_request)
return patch_request
def RemoveRedisConfigs(instance_ref, args, patch_request):
if not getattr(patch_request.instance, 'redisConfigs', None):
return patch_request
if args.IsSpecified('remove_redis_config'):
config_dict = encoding.MessageToDict(patch_request.instance.redisConfigs)
for removed_key in args.remove_redis_config:
config_dict.pop(removed_key, None)
patch_request = AddNewRedisConfigs(instance_ref, config_dict, patch_request)
return patch_request
def UpdateRedisConfigs(instance_ref, args, patch_request):
if args.IsSpecified('update_redis_config'):
config_dict = {}
if getattr(patch_request.instance, 'redisConfigs', None):
config_dict = encoding.MessageToDict(patch_request.instance.redisConfigs)
config_dict.update(args.update_redis_config)
patch_request = AddNewRedisConfigs(instance_ref, config_dict, patch_request)
return patch_request
def AddNewRedisConfigs(instance_ref, redis_configs_dict, patch_request):
messages = util.GetMessagesForResource(instance_ref)
new_redis_configs = util.PackageInstanceRedisConfig(redis_configs_dict,
messages)
patch_request.instance.redisConfigs = new_redis_configs
patch_request = AddFieldToUpdateMask('redis_configs', patch_request)
return patch_request
def UpdateAuthEnabled(unused_instance_ref, args, patch_request):
"""Hook to add auth_enabled to the update mask of the request."""
if args.IsSpecified('enable_auth'):
util.WarnOnAuthEnabled(args.enable_auth)
patch_request = AddFieldToUpdateMask('auth_enabled', patch_request)
return patch_request
def UpdateMaintenanceWindowDay(unused_instance_ref, args, patch_request):
"""Hook to update maintenance window day to the update mask of the request."""
if args.IsSpecified('maintenance_window_day'):
patch_request = AddFieldToUpdateMask('maintenance_policy', patch_request)
return patch_request
def UpdateMaintenanceWindowHour(unused_instance_ref, args, patch_request):
"""Hook to update maintenance window hour to the update mask of the request."""
if args.IsSpecified('maintenance_window_hour'):
patch_request = AddFieldToUpdateMask('maintenance_policy', patch_request)
return patch_request
def UpdateMaintenanceWindowAny(unused_instance_ref, args, patch_request):
"""Hook to remove maintenance window."""
if args.IsSpecified('maintenance_window_any'):
patch_request.instance.maintenancePolicy = None
patch_request = AddFieldToUpdateMask('maintenance_policy', patch_request)
return patch_request
def UpdatePersistenceMode(unused_instance_ref, args, patch_request):
"""Hook to update persistence mode."""
if args.IsSpecified('persistence_mode'):
patch_request = AddFieldToUpdateMask('persistence_config', patch_request)
return patch_request
def UpdateRdbSnapshotPeriod(unused_instance_ref, args, patch_request):
"""Hook to update RDB snapshot period."""
if args.IsSpecified('rdb_snapshot_period'):
patch_request = AddFieldToUpdateMask('persistence_config', patch_request)
return patch_request
def UpdateRdbSnapshotStartTime(unused_instance_ref, args, patch_request):
"""Hook to update RDB snapshot start time."""
if args.IsSpecified('rdb_snapshot_start_time'):
patch_request = AddFieldToUpdateMask('persistence_config', patch_request)
return patch_request
def UpdateReplicaCount(unused_instance_ref, args, patch_request):
"""Hook to update replica count."""
if args.IsSpecified('replica_count'):
patch_request = AddFieldToUpdateMask('replica_count', patch_request)
return patch_request
def UpdateReadReplicasMode(unused_instance_ref, args, patch_request):
"""Hook to update read replicas mode."""
if args.IsSpecified('read_replicas_mode'):
patch_request = AddFieldToUpdateMask('read_replicas_mode', patch_request)
return patch_request
def UpdateSecondaryIpRange(unused_instance_ref, args, patch_request):
"""Hook to update secondary IP range."""
if args.IsSpecified('secondary_ip_range'):
patch_request = AddFieldToUpdateMask('secondary_ip_range', patch_request)
return patch_request
def UpdateMaintenanceVersion(unused_instance_ref, args, patch_request):
"""Hook to update maintenance version to the update mask of the request."""
if args.IsSpecified('maintenance_version'):
patch_request = AddFieldToUpdateMask('maintenance_version', patch_request)
return patch_request

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Utilities for `gcloud redis operations` commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core import log
from six.moves import filter # pylint: disable=redefined-builtin
def _IsPublicVersion(operation):
# TODO(b/65694318): until we move to the CCFE producer API model,
# v1internal1 operations should not be visible to users.
for o in operation.metadata.additionalProperties:
if o.key == 'apiVersion':
return o.value.string_value != 'v1internal1'
return True
def FilterListResponse(response, unused_args):
return list(filter(_IsPublicVersion, response))
def LogCanceledOperation(response, args):
operation = args.CONCEPTS.operation.Parse()
log.status.Print(
'Cancellation in progress for [{}].'.format(operation.Name()))
return response

View File

@@ -0,0 +1,92 @@
# -*- coding: utf-8 -*- #
# Copyright 2024 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.
"""PSC Connection utilities for `gcloud redis clusters`."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.command_lib.redis import util
class Error(Exception):
"""Exceptions for this module."""
class InvalidInputError(Error):
"""Error for parsing cluster endpoint input."""
def _GetCluster(cluster_ref, cluster_name):
client = util.GetClientForResource(cluster_ref)
messages = util.GetMessagesForResource(cluster_ref)
return client.projects_locations_clusters.Get(
messages.RedisProjectsLocationsClustersGetRequest(name=cluster_name)
)
def _ValidateConnectionLength(cluster_endpoint):
if len(cluster_endpoint.connections) != 2:
raise InvalidInputError(
'Each cluster endpoint should have two connections in a pair')
def UpdateClusterEndpoints(cluster_ref, args, patch_request):
"""Hook to update cluster endpoint for a redis cluster."""
cluster = _GetCluster(cluster_ref, patch_request.name)
all_cluster_endpoints = cluster.clusterEndpoints
for cluster_endpoint in args.cluster_endpoint:
_ValidateConnectionLength(cluster_endpoint)
all_cluster_endpoints.append(cluster_endpoint)
patch_request.cluster.clusterEndpoints = all_cluster_endpoints
patch_request.updateMask = 'cluster_endpoints'
return patch_request
def _ExtractAllPSCIDs(endpoint):
return set(
connection.pscConnection.pscConnectionId
for connection in endpoint.connections
if connection.pscConnection is not None
)
def _IsInToBeRemovedList(endpoint, to_be_removed_list):
existing_ids = _ExtractAllPSCIDs(endpoint)
return any(
_ExtractAllPSCIDs(to_be_removed) == existing_ids
for to_be_removed in to_be_removed_list
)
def RemoveClusterEndpoints(cluster_ref, args, patch_request):
"""Hook to remove a cluster endpoint from a redis cluster."""
cluster = _GetCluster(cluster_ref, patch_request.name)
all_cluster_endpoints = cluster.clusterEndpoints
for cluster_endpoint in args.cluster_endpoint:
_ValidateConnectionLength(cluster_endpoint)
new_cluster_endpoints = []
for existing_endpoint in all_cluster_endpoints:
if not _IsInToBeRemovedList(existing_endpoint, args.cluster_endpoint):
new_cluster_endpoints.append(existing_endpoint)
patch_request.cluster.clusterEndpoints = new_cluster_endpoints
patch_request.updateMask = 'cluster_endpoints'
return patch_request

View File

@@ -0,0 +1,89 @@
project:
name: project
collection: redis.projects
attributes:
- parameter_name: projectsId
attribute_name: project
help: |
The project name.
property: core/project
region:
name: region
collection: redis.projects.locations
attributes:
- &region
parameter_name: locationsId
attribute_name: region
help: |
The name of the Redis region of the {resource}. Overrides the default
redis/region property value for this command invocation.
property: redis/region
disable_auto_completers: false
region_without_property:
name: region
collection: redis.projects.locations
attributes:
- parameter_name: locationsId
attribute_name: region
help: |
The name of the Redis region.
disable_auto_completers: false
operation:
name: operation
collection: redis.projects.locations.operations
attributes:
- *region
- parameter_name: operationsId
attribute_name: operation
help: The name of the Redis operation.
disable_auto_completers: false
instance:
name: instance
collection: redis.projects.locations.instances
request_id_field: instanceId
attributes:
- *region
- parameter_name: instancesId
attribute_name: instance
help: The name of the Redis instance.
disable_auto_completers: false
cluster:
name: cluster
collection: redis.projects.locations.clusters
request_id_field: clusterId
attributes:
- *region
- parameter_name: clustersId
attribute_name: cluster
help: The name of the Redis cluster
disable_auto_completers: false
backup_collection:
name: backup collection
collection: redis.projects.locations.backupCollections
request_id_field: backupCollectionId
attributes:
- *region
- parameter_name: backupCollectionsId
attribute_name: backup-collection
help: The name of the Redis cluster backup collection.
disable_auto_completers: false
backup:
name: backup
collection: redis.projects.locations.backupCollections.backups
request_id_field: backupId
attributes:
- *region
- parameter_name: backupCollectionsId
attribute_name: backup-collection
help: The name of the Redis cluster backup collection.
- parameter_name: backupsId
attribute_name: backup
help: The name of the Redis cluster backup.
disable_auto_completers: false

View File

@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Flag utilities for `gcloud redis`."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import encoding
from googlecloudsdk.api_lib import redis
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.util.args import labels_util
from googlecloudsdk.core.console import console_io
import six
# Note that updating config "databases" is not currently supported.
VALID_REDIS_3_2_CONFIG_KEYS = ('maxmemory-policy',
'notify-keyspace-events',
'timeout')
VALID_REDIS_4_0_CONFIG_KEYS = ('activedefrag', 'lfu-decay-time',
'lfu-log-factor', 'maxmemory-gb')
VALID_REDIS_5_0_CONFIG_KEYS = ('stream-node-max-bytes',
'stream-node-max-entries')
VALID_REDIS_7_0_CONFIG_KEYS = (
'maxmemory-clients',
'lazyfree-lazy-eviction',
'lazyfree-lazy-expire',
'lazyfree-lazy-user-del',
'lazyfree-lazy-user-flush',
)
def GetClientForResource(resource_ref):
api_version = resource_ref.GetCollectionInfo().api_version
client = redis.Client(api_version)
return client
def GetMessagesForResource(resource_ref):
api_version = resource_ref.GetCollectionInfo().api_version
messages = redis.Messages(api_version)
return messages
def InstanceRedisConfigArgDictSpec():
valid_redis_config_keys = (
VALID_REDIS_3_2_CONFIG_KEYS + VALID_REDIS_4_0_CONFIG_KEYS +
VALID_REDIS_5_0_CONFIG_KEYS + VALID_REDIS_7_0_CONFIG_KEYS)
return {k: six.text_type for k in valid_redis_config_keys}
def InstanceRedisConfigArgType(value):
return arg_parsers.ArgDict(spec=InstanceRedisConfigArgDictSpec())(value)
def InstanceLabelsArgType(value):
return arg_parsers.ArgDict(
key_type=labels_util.KEY_FORMAT_VALIDATOR,
value_type=labels_util.VALUE_FORMAT_VALIDATOR)(
value)
def AdditionalInstanceUpdateArguments():
return [
InstanceUpdateRedisConfigFlag(),
InstanceRemoveRedisConfigFlag()
]
def InstanceUpdateLabelsFlags():
remove_group = base.ArgumentGroup(mutex=True)
remove_group.AddArgument(labels_util.GetClearLabelsFlag())
remove_group.AddArgument(labels_util.GetRemoveLabelsFlag(''))
return [labels_util.GetUpdateLabelsFlag(''), remove_group]
def InstanceUpdateRedisConfigFlag():
return base.Argument(
'--update-redis-config',
metavar='KEY=VALUE',
type=InstanceRedisConfigArgType,
action=arg_parsers.UpdateAction,
# TODO(b/286342356): add help text for 7.0 config.
help="""\
A list of Redis config KEY=VALUE pairs to update according to
http://cloud.google.com/memorystore/docs/reference/redis-configs. If a config parameter is already set,
its value is modified; otherwise a new Redis config parameter is added.
Currently, the only supported parameters are:\n
Redis version 3.2 and newer: {}.\n
Redis version 4.0 and newer: {}.\n
Redis version 5.0 and newer: {}.\n
Redis version 7.0 and newer: {}.
""".format(
', '.join(VALID_REDIS_3_2_CONFIG_KEYS),
', '.join(VALID_REDIS_4_0_CONFIG_KEYS),
', '.join(VALID_REDIS_5_0_CONFIG_KEYS),
', '.join(VALID_REDIS_7_0_CONFIG_KEYS),
),
)
def InstanceRemoveRedisConfigFlag():
return base.Argument(
'--remove-redis-config',
metavar='KEY',
type=arg_parsers.ArgList(),
action=arg_parsers.UpdateAction,
help="""\
A list of Redis config parameters to remove. Removing a non-existent
config parameter is silently ignored.""")
def PackageInstanceRedisConfig(config, messages):
return encoding.DictToAdditionalPropertyMessage(
config, messages.Instance.RedisConfigsValue, sort_items=True)
def WarnOnAuthEnabled(auth_enabled):
"""Adds prompt that describes lack of security provided by AUTH feature."""
if auth_enabled:
console_io.PromptContinue(
message=('AUTH prevents accidental access to the instance by ' +
'requiring an AUTH string (automatically generated for ' +
'you). AUTH credentials are not confidential when ' +
'transmitted or intended to protect against malicious ' +
'actors.'),
prompt_string='Do you want to proceed?',
cancel_on_no=True)
return auth_enabled
# TODO(b/261183749): Remove modify_request_hook when singleton resource args
# are enabled in declarative.
def UpdateGetCertificateAuthorityRequestPath(unused_ref, unused_args, req):
req.name = req.name + '/certificateAuthority'
return req

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*- #
# Copyright 2018 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.
"""Utilities for `gcloud redis zones` commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
from apitools.base.py import encoding
RedisZone = collections.namedtuple('RedisZone', ['name', 'region'])
def ExtractZonesFromRegionsListResponse(response, args):
for region in response:
if args.IsSpecified('region') and region.locationId != args.region:
continue
if not region.metadata:
continue
metadata = encoding.MessageToDict(region.metadata)
for zone in metadata.get('availableZones', []):
zone = RedisZone(name=zone, region=region.locationId)
yield zone