# -*- coding: utf-8 -*- # # Copyright 2016 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. """Updates the settings of a Cloud SQL instance.""" from __future__ import absolute_import from __future__ import annotations from __future__ import division from __future__ import unicode_literals import copy import datetime from typing import Optional from apitools.base.protorpclite import messages from apitools.base.py import encoding from googlecloudsdk.api_lib.sql import api_util as common_api_util from googlecloudsdk.api_lib.sql import exceptions from googlecloudsdk.api_lib.sql import instances as api_util from googlecloudsdk.api_lib.sql import operations from googlecloudsdk.api_lib.sql import validate from googlecloudsdk.calliope import arg_parsers from googlecloudsdk.calliope import base from googlecloudsdk.command_lib.sql import flags from googlecloudsdk.command_lib.sql import instances as command_util from googlecloudsdk.command_lib.util.args import labels_util from googlecloudsdk.core import log from googlecloudsdk.core import properties from googlecloudsdk.core.console import console_io _NINE_MONTHS_IN_DAYS = 270 _TWELVE_MONTHS_IN_DAYS = 365 class _Result(object): """Run() method result object.""" def __init__(self, new, old): self.new = new self.old = old def _PrintAndConfirmWarningMessage(args, database_version): """Print and confirm warning indicating the effect of applying the patch.""" continue_msg = None insights_query_length_changed = ( 'insights_config_query_string_length' in args and args.insights_config_query_string_length is not None ) active_directory_config_changed = any([ args.active_directory_domain is not None, args.clear_active_directory is not None, args.clear_active_directory_dns_servers is not None, args.active_directory_dns_servers is not None, args.active_directory_secret_manager_key is not None, args.active_directory_organizational_unit is not None, args.active_directory_mode is not None, ]) if any([ args.tier, args.enable_database_replication is not None, active_directory_config_changed, insights_query_length_changed, ]): continue_msg = ('WARNING: This patch modifies a value that requires ' 'your instance to be restarted. Submitting this patch ' 'will immediately restart your instance if it\'s running.') elif any([args.database_flags, args.clear_database_flags]): database_type_fragment = 'mysql' if api_util.InstancesV1Beta4.IsPostgresDatabaseVersion(database_version): database_type_fragment = 'postgres' elif api_util.InstancesV1Beta4.IsSqlServerDatabaseVersion(database_version): database_type_fragment = 'sqlserver' flag_docs_url = 'https://cloud.google.com/sql/docs/{}/flags'.format( database_type_fragment) if args.database_flags is not None and any([ 'sync_binlog' in args.database_flags, 'innodb_flush_log_at_trx_commit' in args.database_flags, ]): log.warning( 'Changing innodb_flush_log_at_trx_commit ' 'or sync_binlog may cause data loss. Check {}' ' for more details.'.format(flag_docs_url) ) continue_msg = ( 'WARNING: This patch modifies database flag values, which may require ' 'your instance to be restarted. Check the list of supported flags - ' '{} - to see if your instance will be restarted when this patch ' 'is submitted.'.format(flag_docs_url) ) else: if any([args.follow_gae_app, args.gce_zone]): continue_msg = ( 'WARNING: This patch modifies the zone your instance ' 'is set to run in, which may require it to be moved. ' 'Submitting this patch will restart your instance ' 'if it is running in a different zone.' ) if 'time_zone' in args and args.time_zone is not None: time_zone_warning_msg = ( 'WARNING: This patch modifies the time zone for your instance which may' ' cause inconsistencies in your data.' ) log.warning( 'This patch modifies the time zone for your instance which may cause' ' inconsistencies in your data.' ) if continue_msg: continue_msg = continue_msg + '\n' + time_zone_warning_msg else: continue_msg = time_zone_warning_msg if continue_msg and not console_io.PromptContinue(continue_msg): raise exceptions.CancelledError('canceled by the user.') def WithoutKind(message, inline=False): """Remove the kind field from a proto message.""" result = message if inline else copy.deepcopy(message) for field in result.all_fields(): if field.name == 'kind': result.kind = None elif isinstance(field, messages.MessageField): value = getattr(result, field.name) if value is not None: if isinstance(value, list): setattr(result, field.name, [WithoutKind(item, True) for item in value]) else: setattr(result, field.name, WithoutKind(value, True)) return result def _GetConfirmedClearedFields(args, patch_instance, original_instance): """Clear fields according to args and confirm with user.""" cleared_fields = [] if args.clear_gae_apps: cleared_fields.append('settings.authorizedGaeApplications') if args.clear_authorized_networks: cleared_fields.append('settings.ipConfiguration.authorizedNetworks') if args.clear_database_flags: cleared_fields.append('settings.databaseFlags') if args.remove_deny_maintenance_period: cleared_fields.append('settings.denyMaintenancePeriods') if args.clear_password_policy: cleared_fields.append('settings.passwordValidationPolicy') if args.IsKnownAndSpecified('clear_allowed_psc_projects'): cleared_fields.append( 'settings.ipConfiguration.pscConfig.allowedConsumerProjects' ) if args.IsKnownAndSpecified('clear_psc_auto_connections'): cleared_fields.append( 'settings.ipConfiguration.pscConfig.pscAutoConnections' ) if args.IsKnownAndSpecified('clear_custom_subject_alternative_names'): cleared_fields.append( 'settings.ipConfiguration.customSubjectAlternativeNames' ) if args.IsKnownAndSpecified('clear_connection_pool_flags'): cleared_fields.append('settings.connectionPoolConfig.flags') if args.IsKnownAndSpecified('clear_psc_network_attachment_uri'): cleared_fields.append( 'settings.ipConfiguration.pscConfig.networkAttachmentUri' ) if args.IsKnownAndSpecified('clear_unc_mappings'): cleared_fields.append('settings.uncMappings') if args.clear_active_directory_dns_servers: cleared_fields.append('settings.activeDirectoryConfig.dnsServers') if args.clear_active_directory: cleared_fields.append('settings.activeDirectoryConfig') log.status.write( 'The following message will be used for the patch API method.\n' ) log.status.write( encoding.MessageToJson( WithoutKind(patch_instance), include_fields=cleared_fields ) + '\n' ) _PrintAndConfirmWarningMessage(args, original_instance.databaseVersion) return cleared_fields def AddBaseArgs(parser): """Adds base args and flags to the parser.""" # TODO(b/35705305): move common flags to command_lib.sql.flags flags.AddActivationPolicy(parser) flags.AddActiveDirectoryDomain(parser) flags.AddAssignIp(parser) base.ASYNC_FLAG.AddToParser(parser) gae_apps_group = parser.add_mutually_exclusive_group() flags.AddAuthorizedGAEApps(gae_apps_group, update=True) gae_apps_group.add_argument( '--clear-gae-apps', required=False, action='store_true', help=('Specified to clear the list of App Engine apps that can access ' 'this instance.')) networks_group = parser.add_mutually_exclusive_group() flags.AddAuthorizedNetworks(networks_group, update=True) networks_group.add_argument( '--clear-authorized-networks', required=False, action='store_true', help=('Clear the list of external networks that are allowed to connect ' 'to the instance.')) flags.AddAvailabilityType(parser) backups_group = parser.add_mutually_exclusive_group() backups_enabled_group = backups_group.add_group() flags.AddBackupStartTime(backups_enabled_group) flags.AddBackupLocation(backups_enabled_group, allow_empty=True) flags.AddRetainedBackupsCount(backups_enabled_group) flags.AddRetainedTransactionLogDays(backups_enabled_group) backups_group.add_argument( '--no-backup', required=False, action='store_true', help='Specified if daily backup should be disabled.') database_flags_group = parser.add_mutually_exclusive_group() flags.AddDatabaseFlags(database_flags_group) database_flags_group.add_argument( '--clear-database-flags', required=False, action='store_true', help=('Clear the database flags set on the instance. ' 'WARNING: Instance will be restarted.')) flags.AddCPU(parser) parser.add_argument( '--diff', action='store_true', help='Show what changed as a result of the update.') flags.AddEnableBinLog(parser, show_negated_in_help=True) parser.add_argument( '--enable-database-replication', action=arg_parsers.StoreTrueFalseAction, help=('Enable database replication. Applicable only for read replica ' 'instance(s). WARNING: Instance will be restarted.')) parser.add_argument( '--follow-gae-app', required=False, help=('First Generation instances only. The App Engine app ' 'this instance should follow. It must be in the same region as ' 'the instance. WARNING: Instance may be restarted.')) parser.add_argument( 'instance', completer=flags.InstanceCompleter, help='Cloud SQL instance ID.') flags.AddMaintenanceReleaseChannel(parser) parser.add_argument( '--maintenance-window-any', action='store_true', help='Removes the user-specified maintenance window.') flags.AddMaintenanceWindowDay(parser) flags.AddMaintenanceWindowHour(parser) flags.AddDenyMaintenancePeriodStartDate(parser) flags.AddDenyMaintenancePeriodEndDate(parser) flags.AddDenyMaintenancePeriodTime(parser) parser.add_argument( '--remove-deny-maintenance-period', action='store_true', help='Removes the user-specified deny maintenance period.') flags.AddInsightsConfigQueryInsightsEnabled(parser, show_negated_in_help=True) flags.AddInsightsConfigQueryStringLength(parser) flags.AddInsightsConfigRecordApplicationTags( parser, show_negated_in_help=True) flags.AddInsightsConfigRecordClientAddress(parser, show_negated_in_help=True) flags.AddInsightsConfigQueryPlansPerMinute(parser) flags.AddMemory(parser) flags.AddPasswordPolicyMinLength(parser) flags.AddPasswordPolicyComplexity(parser) flags.AddPasswordPolicyReuseInterval(parser) flags.AddPasswordPolicyDisallowUsernameSubstring(parser) flags.AddPasswordPolicyPasswordChangeInterval(parser) flags.AddPasswordPolicyEnablePasswordPolicy(parser) flags.AddPasswordPolicyClearPasswordPolicy(parser) parser.add_argument( '--pricing-plan', '-p', required=False, choices=['PER_USE', 'PACKAGE'], help=('First Generation instances only. The pricing plan for this ' 'instance.')) flags.AddReplication(parser) parser.add_argument( '--require-ssl', action=arg_parsers.StoreTrueFalseAction, help=('mysqld should default to \'REQUIRE X509\' for users connecting ' 'over IP.')) flags.AddStorageAutoIncrease(parser) flags.AddStorageSize(parser) flags.AddStorageType(parser) flags.AddTier(parser, is_patch=True) flags.AddEdition(parser) flags.AddEnablePointInTimeRecovery(parser) flags.AddNetwork(parser) flags.AddMaintenanceVersion(parser) flags.AddSqlServerAudit(parser) flags.AddSqlServerTimeZone(parser) flags.AddDeletionProtection(parser) flags.AddConnectorEnforcement(parser) flags.AddEnableGooglePrivatePath(parser, show_negated_in_help=True) flags.AddThreadsPerCore(parser) flags.AddEnableDataCache(parser) flags.AddEnableAutoUpgrade(parser) flags.AddRecreateReplicasOnPrimaryCrash(parser) psc_update_group = parser.add_mutually_exclusive_group() flags.AddAllowedPscProjects(psc_update_group) flags.AddClearAllowedPscProjects(psc_update_group) ip_update_custom_sans_group = parser.add_mutually_exclusive_group() flags.AddCustomSubjectAlternativeNames(ip_update_custom_sans_group) flags.AddClearCustomSubjectAlternativeNames(ip_update_custom_sans_group) flags.AddSslMode(parser) flags.AddEnableGoogleMLIntegration(parser) flags.AddEnableDataplexIntegration(parser) flags.AddUpgradeSqlNetworkArchitecture(parser) flags.AddForceSqlNetworkArchitecture(parser) flags.AddSimulateMaintenanceEvent(parser) flags.AddSwitchTransactionLogsToCloudStorage(parser) flags.AddFailoverDrReplicaName(parser) flags.AddClearFailoverDrReplicaName(parser) flags.AddIncludeReplicasForMajorVersionUpgrade(parser) flags.AddRetainBackupsOnDelete(parser) flags.AddStorageProvisionedIops(parser) flags.AddStorageProvisionedThroughput(parser) flags.AddEnablePrivateServiceConnect(parser, show_negated_in_help=True) psc_na_uri_update_group = parser.add_mutually_exclusive_group() flags.AddPSCNetworkAttachmentUri(psc_na_uri_update_group) flags.AddClearPSCNetworkAttachmentUri(psc_na_uri_update_group) flags.AddInstanceType(parser) flags.AddNodeCount(parser) flags.AddActiveDirectoryMode(parser) flags.AddActiveDirectorySecretManagerKey(parser) flags.AddActiveDirectoryOrganizationalUnit(parser) flags.AddActiveDirectoryDNSServers(parser) flags.ClearActiveDirectoryDNSServers(parser) flags.AddClearActiveDirectory(parser) flags.AddFinalBackup(parser) flags.AddFinalbackupRetentionDays(parser) flags.AddEnableConnectionPooling(parser) connection_pool_flags_group = parser.add_mutually_exclusive_group() flags.AddConnectionPoolFlags(connection_pool_flags_group) flags.AddClearConnectionPoolFlags(connection_pool_flags_group) psc_update_auto_connections_group = parser.add_mutually_exclusive_group() flags.AddPscAutoConnections(psc_update_auto_connections_group) flags.AddClearPscAutoConnections(psc_update_auto_connections_group) flags.AddServerCaMode(parser) flags.AddServerCaPool(parser) flags.AddReadPoolAutoScaleConfig(parser) def AddBetaArgs(parser): """Adds beta args and flags to the parser.""" flags.AddInstanceResizeLimit(parser) flags.AddAllocatedIpRangeName(parser) labels_util.AddUpdateLabelsFlags(parser, enable_clear=True) flags.AddReplicationLagMaxSecondsForRecreate(parser) flags.AddReconcilePsaNetworking(parser) flags.AddEnableAcceleratedReplicaMode(parser) unc_mappings_group = parser.add_mutually_exclusive_group(hidden=True) flags.AddUncMappings(unc_mappings_group) flags.AddClearUncMappings(unc_mappings_group) flags.AddDataApiAccess(parser) flags.AddServerCertificateRotationMode(parser) flags.AddPerformanceCaptureConfig(parser, hidden=False) flags.AddSqlServerEntraId(parser) flags.AddClearEntraIdConfig(parser) def AddAlphaArgs(unused_parser): """Adds alpha args and flags to the parser.""" pass def IsBetaOrNewer(release_track): """Returns true if the release track is beta or newer.""" return ( release_track == base.ReleaseTrack.BETA or release_track == base.ReleaseTrack.ALPHA ) def RunBasePatchCommand(args, release_track): """Updates settings of a Cloud SQL instance using the patch api method. Args: args: argparse.Namespace, The arguments that this command was invoked with. release_track: base.ReleaseTrack, the release track that this was run under. Returns: A dict object representing the operations resource describing the patch operation if the patch was successful. Raises: CancelledError: The user chose not to continue. """ if args.diff and not args.IsSpecified('format'): args.format = 'diff(old, new)' client = common_api_util.SqlClient(common_api_util.API_VERSION_DEFAULT) sql_client = client.sql_client sql_messages = client.sql_messages validate.ValidateInstanceName(args.instance) validate.ValidateInstanceLocation(args) instance_ref = client.resource_parser.Parse( args.instance, params={'project': properties.VALUES.core.project.GetOrFail}, collection='sql.instances') # If the flag to simulate a maintenance event is supplied along with other # flags thrown an error. if args.IsSpecified( 'simulate_maintenance_event' ): for key in args.GetSpecifiedArgsDict(): # positional argument does not have a flag argument if key == 'instance': continue if key == 'simulate_maintenance_event': continue if not args.GetFlagArgument(key).is_global: raise exceptions.ArgumentError( '`--simulate-maintenance-event` cannot be specified with other' ' arguments excluding gCloud wide flags' ) if args.IsSpecified('no_backup'): if args.IsSpecified('enable_bin_log'): raise exceptions.ArgumentError( '`--enable-bin-log` cannot be specified when --no-backup is ' 'specified') elif args.IsSpecified('enable_point_in_time_recovery'): raise exceptions.ArgumentError( '`--enable-point-in-time-recovery` cannot be specified when ' '--no-backup is specified') if args.IsKnownAndSpecified('failover_dr_replica_name'): if args.IsKnownAndSpecified('clear_failover_dr_replica_name'): raise exceptions.ArgumentError( '`--failover-dr-replica-name` cannot be specified when ' '--clear-failover-dr-replica-name is specified') # If --authorized-networks is used, confirm that the user knows the networks # will get overwritten. if args.authorized_networks: api_util.InstancesV1Beta4.PrintAndConfirmAuthorizedNetworksOverwrite() original_instance_resource = sql_client.instances.Get( sql_messages.SqlInstancesGetRequest( project=instance_ref.project, instance=instance_ref.instance)) if args.IsKnownAndSpecified( 'performance_capture_config' ) and not original_instance_resource.databaseVersion.name.startswith('MYSQL'): raise exceptions.ArgumentError( '`--performance-capture-config` is only supported for MySQL instances.' ) if ( args.IsSpecified('deny_maintenance_period_start_date') or args.IsSpecified('deny_maintenance_period_end_date') or args.IsSpecified('deny_maintenance_period_time') ): maintenance_version = original_instance_resource.maintenanceVersion if maintenance_version: maintenance_date = _ParseDateFromMaintenanceVersion(maintenance_version) if maintenance_date: today = datetime.date.today() delta = today - maintenance_date # 9 months ~ 270 days, 12 months ~ 365 days. if _NINE_MONTHS_IN_DAYS <= delta.days < _TWELVE_MONTHS_IN_DAYS: log.warning( 'Your instance has NOT undergone maintenance for at least 9' ' months. It is highly recommended to perform it soon. While you' ' can still set a deny maintenance period now, please be aware' ' that once your instance is on a maintenance version that is at' ' least 12 months old, you will no longer be able to set a deny' ' period. Maintenance is crucial for important updates, security' ' patches, and bug fixes, and skipping them can leave your' ' instance vulnerable. You can learn more about how to perform' ' maintenance here:' ' https://cloud.google.com/sql/docs/mysql/maintenance' ) if IsBetaOrNewer(release_track) and args.IsSpecified( 'reconcile_psa_networking' ): if ( not original_instance_resource.settings.ipConfiguration or not original_instance_resource.settings.ipConfiguration.privateNetwork ): raise exceptions.ArgumentError( 'argument --reconcile-psa-networking can be used only with instances' ' that have a private network' ) # Do not allow reconcile-psa-networking flag to be specified with other # arguments. for key in args.GetSpecifiedArgsDict(): # positional argument does not have a flag argument if key == 'instance': continue if key == 'reconcile_psa_networking': continue if not args.GetFlagArgument(key).is_global: raise exceptions.ArgumentError( 'argument --reconcile-psa-networking cannot be specified with other' ' arguments excluding gcloud wide flags' ) if args.IsKnownAndSpecified('enable_accelerated_replica_mode'): if not api_util.InstancesV1Beta4.IsMysqlDatabaseVersion( original_instance_resource.databaseVersion ): raise exceptions.ArgumentError( '--enable-accelerated-replica-mode is only supported for MySQL.' ) patch_instance = command_util.InstancesV1Beta4.ConstructPatchInstanceFromArgs( sql_messages, args, original=original_instance_resource, release_track=release_track) patch_instance.project = instance_ref.project patch_instance.name = instance_ref.instance cleared_fields = _GetConfirmedClearedFields(args, patch_instance, original_instance_resource) # beta only if args.maintenance_window_any: cleared_fields.append('settings.maintenanceWindow') if args.IsKnownAndSpecified('clear_failover_dr_replica_name'): cleared_fields.append('replicationCluster') with sql_client.IncludeFields(cleared_fields): result_operation = sql_client.instances.Patch( sql_messages.SqlInstancesPatchRequest( databaseInstance=patch_instance, project=instance_ref.project, instance=instance_ref.instance)) operation_ref = client.resource_parser.Create( 'sql.operations', operation=result_operation.name, project=instance_ref.project) if args.async_: return sql_client.operations.Get( sql_messages.SqlOperationsGetRequest( project=operation_ref.project, operation=operation_ref.operation)) operations.OperationsV1Beta4.WaitForOperation(sql_client, operation_ref, 'Patching Cloud SQL instance') log.UpdatedResource(instance_ref) changed_instance_resource = sql_client.instances.Get( sql_messages.SqlInstancesGetRequest( project=instance_ref.project, instance=instance_ref.instance)) return _Result(changed_instance_resource, original_instance_resource) @base.DefaultUniverseOnly @base.ReleaseTracks(base.ReleaseTrack.GA) class Patch(base.UpdateCommand): """Updates the settings of a Cloud SQL instance.""" def Run(self, args): return RunBasePatchCommand(args, self.ReleaseTrack()) @staticmethod def Args(parser): """Args is called by calliope to gather arguments for this command.""" AddBaseArgs(parser) flags.AddZone( parser, help_text=('Preferred Compute Engine zone (e.g. us-central1-a, ' 'us-central1-b, etc.). WARNING: Instance may be restarted.')) flags.AddDatabaseVersion(parser, support_default_version=False) @base.DefaultUniverseOnly @base.ReleaseTracks(base.ReleaseTrack.BETA) class PatchBeta(base.UpdateCommand): """Updates the settings of a Cloud SQL instance.""" def Run(self, args): return RunBasePatchCommand(args, self.ReleaseTrack()) @staticmethod def Args(parser): """Args is called by calliope to gather arguments for this command.""" AddBaseArgs(parser) flags.AddZone( parser, help_text=('Preferred Compute Engine zone (e.g. us-central1-a, ' 'us-central1-b, etc.). WARNING: Instance may be restarted.')) AddBetaArgs(parser) flags.AddDatabaseVersion( parser, restrict_choices=False, support_default_version=False) @base.DefaultUniverseOnly @base.ReleaseTracks(base.ReleaseTrack.ALPHA) class PatchAlpha(base.UpdateCommand): """Updates the settings of a Cloud SQL instance.""" def Run(self, args): return RunBasePatchCommand(args, self.ReleaseTrack()) @staticmethod def Args(parser): """Args is called by calliope to gather arguments for this command.""" AddBaseArgs(parser) flags.AddZone( parser, help_text=('Preferred Compute Engine zone (e.g. us-central1-a, ' 'us-central1-b, etc.). WARNING: Instance may be restarted.')) AddBetaArgs(parser) AddAlphaArgs(parser) flags.AddDatabaseVersion( parser, restrict_choices=False, support_default_version=False) def _ParseDateFromMaintenanceVersion( maintenance_version: str, ) -> Optional[datetime.date]: """Parses the date from a maintenance version string. Args: maintenance_version: The maintenance version string in a format like 'MYSQL_5_7_44.R20240915.01_02'. Returns: A datetime.date object if a valid date is found, otherwise None. """ for part in maintenance_version.replace('_', '.').split('.'): if part.startswith('R'): maybe_date_str = part[1:] if len(maybe_date_str) == 8 and maybe_date_str.isdigit(): try: return datetime.datetime.strptime(maybe_date_str, '%Y%m%d').date() except ValueError: # Continue searching for a valid date part. pass return None