1# -*- coding: utf-8 -*- # 2# Copyright 2017 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15"""Common utility functions for sql instance commands.""" 16 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import unicode_literals 20 21from googlecloudsdk.api_lib.sql import constants 22from googlecloudsdk.api_lib.sql import instance_prop_reducers as reducers 23from googlecloudsdk.api_lib.sql import validate 24from googlecloudsdk.calliope import base 25from googlecloudsdk.calliope import exceptions 26from googlecloudsdk.command_lib import info_holder 27from googlecloudsdk.command_lib.util.args import labels_util 28from googlecloudsdk.core import execution_utils 29from googlecloudsdk.core import log 30from googlecloudsdk.core import properties 31from googlecloudsdk.core.console import console_io 32 33DEFAULT_RELEASE_TRACK = base.ReleaseTrack.GA 34 35# PD = Persistent Disk. This is prefixed to all storage type payloads. 36STORAGE_TYPE_PREFIX = 'PD_' 37 38 39def GetInstanceRef(args, client): 40 """Validates and returns the instance reference.""" 41 validate.ValidateInstanceName(args.instance) 42 return client.resource_parser.Parse( 43 args.instance, 44 params={'project': properties.VALUES.core.project.GetOrFail}, 45 collection='sql.instances') 46 47 48def GetDatabaseArgs(args, flags): 49 """Gets the args for specifying a database during instance connection.""" 50 command_line_args = [] 51 if args.IsSpecified('database'): 52 try: 53 command_line_args.extend([flags['database'], args.database]) 54 except KeyError: 55 raise exceptions.InvalidArgumentException( 56 '--database', 'This instance does not support the database argument.') 57 return command_line_args 58 59 60def ConnectToInstance(cmd_args, sql_user): 61 """Connects to the instance using the relevant CLI.""" 62 try: 63 log.status.write( 64 'Connecting to database with SQL user [{0}].'.format(sql_user)) 65 execution_utils.Exec(cmd_args) 66 except OSError: 67 log.error('Failed to execute command "{0}"'.format(' '.join(cmd_args))) 68 log.Print(info_holder.InfoHolder()) 69 70 71def _GetAndValidateCmekKeyName(args, is_primary): 72 """Parses the CMEK resource arg, makes sure the key format was correct.""" 73 kms_ref = args.CONCEPTS.kms_key.Parse() 74 if kms_ref: 75 # Since CMEK is required for replicas of CMEK primaries, this prompt is only 76 # actionable for primary instances. 77 if is_primary: 78 _ShowCmekPrompt() 79 return kms_ref.RelativeName() 80 else: 81 # Check for partially specified disk-encryption-key. 82 for keyword in [ 83 'disk-encryption-key', 'disk-encryption-key-keyring', 84 'disk-encryption-key-location', 'disk-encryption-key-project' 85 ]: 86 if getattr(args, keyword.replace('-', '_'), None): 87 raise exceptions.InvalidArgumentException('--disk-encryption-key', 88 'not fully specified.') 89 90 91def _GetZone(args): 92 return args.zone or args.gce_zone 93 94 95def _GetSecondaryZone(args): 96 if 'secondary_zone' in args: 97 return args.secondary_zone 98 99 return None 100 101 102def _IsAlpha(release_track): 103 return release_track == base.ReleaseTrack.ALPHA 104 105 106def _IsBetaOrNewer(release_track): 107 return release_track == base.ReleaseTrack.BETA or _IsAlpha(release_track) 108 109 110def _ParseActivationPolicy(sql_messages, policy): 111 if policy: 112 return sql_messages.Settings.ActivationPolicyValueValuesEnum.lookup_by_name( 113 policy.replace('-', '_').upper()) 114 return None 115 116 117def _ParseAvailabilityType(sql_messages, availability_type): 118 if availability_type: 119 return sql_messages.Settings.AvailabilityTypeValueValuesEnum.lookup_by_name( 120 availability_type.upper()) 121 return None 122 123 124def _ParseDatabaseVersion(sql_messages, database_version): 125 if database_version: 126 return sql_messages.DatabaseInstance.DatabaseVersionValueValuesEnum.lookup_by_name( 127 database_version.upper()) 128 return None 129 130 131def _ParsePricingPlan(sql_messages, pricing_plan): 132 if pricing_plan: 133 return sql_messages.Settings.PricingPlanValueValuesEnum.lookup_by_name( 134 pricing_plan.upper()) 135 return None 136 137 138def _ParseReplicationType(sql_messages, replication): 139 if replication: 140 return sql_messages.Settings.ReplicationTypeValueValuesEnum.lookup_by_name( 141 replication.upper()) 142 return None 143 144 145def _ParseStorageType(sql_messages, storage_type): 146 if storage_type: 147 return sql_messages.Settings.DataDiskTypeValueValuesEnum.lookup_by_name( 148 storage_type.upper()) 149 return None 150 151 152# TODO(b/122660263): Remove when V1 instances are no longer supported. 153def ShowV1DeprecationWarning(plural=False): 154 message = ( 155 'Upgrade your First Generation instance{} to Second Generation before we ' 156 'auto-upgrade {} on March 4, 2020, ahead of the full decommission of ' 157 'First Generation on March 25, 2020.') 158 if plural: 159 log.warning(message.format('s', 'them')) 160 else: 161 log.warning(message.format('', 'it')) 162 163 164def ShowZoneDeprecationWarnings(args): 165 """Show warnings if both region and zone are specified or neither is. 166 167 Args: 168 args: argparse.Namespace, The arguments that the command was invoked with. 169 """ 170 171 region_specified = args.IsSpecified('region') 172 zone_specified = args.IsSpecified('gce_zone') or args.IsSpecified('zone') 173 174 # TODO(b/73362371): Remove this check; user must specify a location flag. 175 if not (region_specified or zone_specified): 176 log.warning('Starting with release 233.0.0, you will need to specify ' 177 'either a region or a zone to create an instance.') 178 179 180def ShowCmekWarning(resource_type_label, instance_type_label=None): 181 if instance_type_label is None: 182 log.warning( 183 'Your {} will be encrypted with a customer-managed key. If anyone ' 184 'destroys this key, all data encrypted with it will be permanently ' 185 'lost.'.format(resource_type_label)) 186 else: 187 log.warning( 188 'Your {} will be encrypted with {}\'s customer-managed encryption key. ' 189 'If anyone destroys this key, all data encrypted with it will be ' 190 'permanently lost.'.format(resource_type_label, instance_type_label)) 191 192 193def _ShowCmekPrompt(): 194 log.warning( 195 'You are creating a Cloud SQL instance encrypted with a customer-managed ' 196 'key. If anyone destroys a customer-managed key, all data encrypted with ' 197 'it will be permanently lost.\n') 198 console_io.PromptContinue(cancel_on_no=True) 199 200 201class _BaseInstances(object): 202 """Common utility functions for sql instance commands."""\ 203 204 @classmethod 205 def _ConstructBaseSettingsFromArgs(cls, 206 sql_messages, 207 args, 208 instance=None, 209 release_track=DEFAULT_RELEASE_TRACK): 210 """Constructs instance settings from the command line arguments. 211 212 Args: 213 sql_messages: module, The messages module that should be used. 214 args: argparse.Namespace, The arguments that this command was invoked 215 with. 216 instance: sql_messages.DatabaseInstance, The original instance, for 217 settings that depend on the previous state. 218 release_track: base.ReleaseTrack, the release track that this was run 219 under. 220 221 Returns: 222 A settings object representing the instance settings. 223 224 Raises: 225 ToolException: An error other than http error occurred while executing the 226 command. 227 """ 228 229 # This code is shared by create and patch, but these args don't exist in 230 # create anymore, so insert them here to avoid regressions below. 231 if 'authorized_gae_apps' not in args: 232 args.authorized_gae_apps = None 233 if 'follow_gae_app' not in args: 234 args.follow_gae_app = None 235 if 'pricing_plan' not in args: 236 args.pricing_plan = 'PER_USE' 237 238 settings = sql_messages.Settings( 239 kind='sql#settings', 240 tier=reducers.MachineType(instance, args.tier, args.memory, args.cpu), 241 pricingPlan=_ParsePricingPlan(sql_messages, args.pricing_plan), 242 replicationType=_ParseReplicationType(sql_messages, args.replication), 243 activationPolicy=_ParseActivationPolicy(sql_messages, 244 args.activation_policy)) 245 246 if args.authorized_gae_apps: 247 settings.authorizedGaeApplications = args.authorized_gae_apps 248 249 if any([ 250 args.assign_ip is not None, args.require_ssl is not None, 251 args.authorized_networks 252 ]): 253 settings.ipConfiguration = sql_messages.IpConfiguration() 254 if args.assign_ip is not None: 255 cls.SetIpConfigurationEnabled(settings, args.assign_ip) 256 257 if args.authorized_networks: 258 cls.SetAuthorizedNetworks(settings, args.authorized_networks, 259 sql_messages.AclEntry) 260 261 if args.require_ssl is not None: 262 settings.ipConfiguration.requireSsl = args.require_ssl 263 264 if any([args.follow_gae_app, _GetZone(args), _GetSecondaryZone(args)]): 265 settings.locationPreference = sql_messages.LocationPreference( 266 kind='sql#locationPreference', 267 followGaeApplication=args.follow_gae_app, 268 zone=_GetZone(args), 269 secondaryZone=_GetSecondaryZone(args)) 270 271 if args.storage_size: 272 settings.dataDiskSizeGb = int(args.storage_size / constants.BYTES_TO_GB) 273 274 if args.storage_auto_increase is not None: 275 settings.storageAutoResize = args.storage_auto_increase 276 277 if args.IsSpecified('availability_type'): 278 settings.availabilityType = _ParseAvailabilityType( 279 sql_messages, args.availability_type) 280 281 # BETA args. 282 if _IsBetaOrNewer(release_track): 283 if args.IsSpecified('storage_auto_increase_limit'): 284 # Resize limit should be settable if the original instance has resize 285 # turned on, or if the instance to be created has resize flag. 286 if (instance and instance.settings.storageAutoResize) or ( 287 args.storage_auto_increase): 288 # If the limit is set to None, we want it to be set to 0. This is a 289 # backend requirement. 290 settings.storageAutoResizeLimit = ( 291 args.storage_auto_increase_limit or 0) 292 else: 293 raise exceptions.RequiredArgumentException( 294 '--storage-auto-increase', 'To set the storage capacity limit ' 295 'using [--storage-auto-increase-limit], ' 296 '[--storage-auto-increase] must be enabled.') 297 298 if args.IsSpecified('network'): 299 if not settings.ipConfiguration: 300 settings.ipConfiguration = sql_messages.IpConfiguration() 301 settings.ipConfiguration.privateNetwork = reducers.PrivateNetworkUrl( 302 args.network) 303 304 return settings 305 306 @classmethod 307 def _ConstructCreateSettingsFromArgs(cls, 308 sql_messages, 309 args, 310 instance=None, 311 release_track=DEFAULT_RELEASE_TRACK): 312 """Constructs create settings object from base settings and args.""" 313 original_settings = instance.settings if instance else None 314 settings = cls._ConstructBaseSettingsFromArgs(sql_messages, args, instance, 315 release_track) 316 317 backup_configuration = ( 318 reducers.BackupConfiguration( 319 sql_messages, 320 instance, 321 backup_enabled=args.backup, 322 backup_location=args.backup_location, 323 backup_start_time=args.backup_start_time, 324 enable_bin_log=args.enable_bin_log, 325 enable_point_in_time_recovery=args.enable_point_in_time_recovery, 326 retained_backups_count=args.retained_backups_count, 327 retained_transaction_log_days=args.retained_transaction_log_days)) 328 if backup_configuration: 329 cls.AddBackupConfigToSettings(settings, backup_configuration) 330 331 settings.databaseFlags = ( 332 reducers.DatabaseFlags( 333 sql_messages, original_settings, 334 database_flags=args.database_flags)) 335 336 settings.maintenanceWindow = ( 337 reducers.MaintenanceWindow( 338 sql_messages, 339 instance, 340 maintenance_release_channel=args.maintenance_release_channel, 341 maintenance_window_day=args.maintenance_window_day, 342 maintenance_window_hour=args.maintenance_window_hour)) 343 344 if args.deny_maintenance_period_start_date and args.deny_maintenance_period_end_date: 345 settings.denyMaintenancePeriods = [] 346 settings.denyMaintenancePeriods.append( 347 reducers.DenyMaintenancePeriod( 348 sql_messages, 349 instance, 350 deny_maintenance_period_start_date=args 351 .deny_maintenance_period_start_date, 352 deny_maintenance_period_end_date=args 353 .deny_maintenance_period_end_date, 354 deny_maintenance_period_time=args.deny_maintenance_period_time)) 355 356 settings.insightsConfig = ( 357 reducers.InsightsConfig( 358 sql_messages, 359 insights_config_query_insights_enabled=args 360 .insights_config_query_insights_enabled, 361 insights_config_query_string_length=args 362 .insights_config_query_string_length, 363 insights_config_record_application_tags=args 364 .insights_config_record_application_tags, 365 insights_config_record_client_address=args 366 .insights_config_record_client_address)) 367 368 if args.storage_type: 369 settings.dataDiskType = _ParseStorageType( 370 sql_messages, STORAGE_TYPE_PREFIX + args.storage_type) 371 372 # BETA args. 373 if _IsBetaOrNewer(release_track): 374 settings.userLabels = labels_util.ParseCreateArgs( 375 args, sql_messages.Settings.UserLabelsValue) 376 377 # ALPHA args. 378 if _IsAlpha(release_track): 379 if args.active_directory_domain is not None: 380 settings.activeDirectoryConfig = ( 381 reducers.ActiveDirectoryConfig(sql_messages, 382 args.active_directory_domain)) 383 384 return settings 385 386 @classmethod 387 def _ConstructPatchSettingsFromArgs(cls, 388 sql_messages, 389 args, 390 instance, 391 release_track=DEFAULT_RELEASE_TRACK): 392 """Constructs patch settings object from base settings and args.""" 393 original_settings = instance.settings 394 settings = cls._ConstructBaseSettingsFromArgs(sql_messages, args, instance, 395 release_track) 396 397 if args.clear_gae_apps: 398 settings.authorizedGaeApplications = [] 399 400 if any([args.follow_gae_app, _GetZone(args), _GetSecondaryZone(args)]): 401 settings.locationPreference = sql_messages.LocationPreference( 402 kind='sql#locationPreference', 403 followGaeApplication=args.follow_gae_app, 404 zone=_GetZone(args), 405 secondaryZone=_GetSecondaryZone(args)) 406 407 if args.clear_authorized_networks: 408 if not settings.ipConfiguration: 409 settings.ipConfiguration = sql_messages.IpConfiguration() 410 settings.ipConfiguration.authorizedNetworks = [] 411 412 if args.enable_database_replication is not None: 413 settings.databaseReplicationEnabled = args.enable_database_replication 414 415 backup_configuration = ( 416 reducers.BackupConfiguration( 417 sql_messages, 418 instance, 419 backup_enabled=not args.no_backup, 420 backup_location=args.backup_location, 421 backup_start_time=args.backup_start_time, 422 enable_bin_log=args.enable_bin_log, 423 enable_point_in_time_recovery=args.enable_point_in_time_recovery, 424 retained_backups_count=args.retained_backups_count, 425 retained_transaction_log_days=args.retained_transaction_log_days)) 426 427 if backup_configuration: 428 cls.AddBackupConfigToSettings(settings, backup_configuration) 429 430 settings.databaseFlags = ( 431 reducers.DatabaseFlags( 432 sql_messages, 433 original_settings, 434 database_flags=args.database_flags, 435 clear_database_flags=args.clear_database_flags)) 436 437 settings.maintenanceWindow = ( 438 reducers.MaintenanceWindow( 439 sql_messages, 440 instance, 441 maintenance_release_channel=args.maintenance_release_channel, 442 maintenance_window_day=args.maintenance_window_day, 443 maintenance_window_hour=args.maintenance_window_hour)) 444 445 if args.remove_deny_maintenance_period: 446 settings.denyMaintenancePeriods = [] 447 448 if (args.deny_maintenance_period_start_date or 449 args.deny_maintenance_period_end_date or 450 args.deny_maintenance_period_time): 451 settings.denyMaintenancePeriods = [] 452 settings.denyMaintenancePeriods.append( 453 reducers.DenyMaintenancePeriod( 454 sql_messages, 455 instance, 456 deny_maintenance_period_start_date=args 457 .deny_maintenance_period_start_date, 458 deny_maintenance_period_end_date=args 459 .deny_maintenance_period_end_date, 460 deny_maintenance_period_time=args.deny_maintenance_period_time)) 461 462 settings.insightsConfig = ( 463 reducers.InsightsConfig( 464 sql_messages, 465 insights_config_query_insights_enabled=args 466 .insights_config_query_insights_enabled, 467 insights_config_query_string_length=args 468 .insights_config_query_string_length, 469 insights_config_record_application_tags=args 470 .insights_config_record_application_tags, 471 insights_config_record_client_address=args 472 .insights_config_record_client_address)) 473 474 # BETA args. 475 if _IsBetaOrNewer(release_track): 476 labels_diff = labels_util.ExplicitNullificationDiff.FromUpdateArgs(args) 477 labels_update = labels_diff.Apply(sql_messages.Settings.UserLabelsValue, 478 instance.settings.userLabels) 479 if labels_update.needs_update: 480 settings.userLabels = labels_update.labels 481 482 # ALPHA args. 483 if _IsAlpha(release_track): 484 if args.active_directory_domain is not None: 485 settings.activeDirectoryConfig = ( 486 reducers.ActiveDirectoryConfig(sql_messages, 487 args.active_directory_domain)) 488 489 return settings 490 491 @classmethod 492 def _ConstructBaseInstanceFromArgs(cls, 493 sql_messages, 494 args, 495 original=None, 496 instance_ref=None, 497 release_track=DEFAULT_RELEASE_TRACK): 498 """Construct a Cloud SQL instance from command line args. 499 500 Args: 501 sql_messages: module, The messages module that should be used. 502 args: argparse.Namespace, The CLI arg namespace. 503 original: sql_messages.DatabaseInstance, The original instance, if some of 504 it might be used to fill fields in the new one. 505 instance_ref: reference to DatabaseInstance object, used to fill project 506 and instance information. 507 release_track: base.ReleaseTrack, the release track that this was run 508 under. 509 510 Returns: 511 sql_messages.DatabaseInstance, The constructed (and possibly partial) 512 database instance. 513 514 Raises: 515 ToolException: An error other than http error occurred while executing the 516 command. 517 """ 518 del args, original, release_track # Currently unused in base function. 519 instance_resource = sql_messages.DatabaseInstance(kind='sql#instance') 520 521 if instance_ref: 522 cls.SetProjectAndInstanceFromRef(instance_resource, instance_ref) 523 524 return instance_resource 525 526 @classmethod 527 def ConstructCreateInstanceFromArgs(cls, 528 sql_messages, 529 args, 530 original=None, 531 instance_ref=None, 532 release_track=DEFAULT_RELEASE_TRACK): 533 """Constructs Instance for create request from base instance and args.""" 534 ShowZoneDeprecationWarnings(args) 535 instance_resource = cls._ConstructBaseInstanceFromArgs( 536 sql_messages, args, original, instance_ref) 537 538 instance_resource.region = reducers.Region(args.region, _GetZone(args), 539 _GetSecondaryZone(args)) 540 instance_resource.databaseVersion = _ParseDatabaseVersion( 541 sql_messages, args.database_version) 542 instance_resource.masterInstanceName = args.master_instance_name 543 instance_resource.rootPassword = args.root_password 544 545 # BETA: Set the host port and return early if external master instance. 546 if _IsBetaOrNewer(release_track) and args.IsSpecified('source_ip_address'): 547 on_premises_configuration = reducers.OnPremisesConfiguration( 548 sql_messages, args.source_ip_address, args.source_port) 549 instance_resource.onPremisesConfiguration = on_premises_configuration 550 return instance_resource 551 552 instance_resource.settings = cls._ConstructCreateSettingsFromArgs( 553 sql_messages, args, original, release_track) 554 555 if args.master_instance_name: 556 replication = sql_messages.Settings.ReplicationTypeValueValuesEnum.ASYNCHRONOUS 557 if args.replica_type == 'FAILOVER': 558 instance_resource.replicaConfiguration = ( 559 sql_messages.ReplicaConfiguration( 560 kind='sql#demoteMasterMysqlReplicaConfiguration', 561 failoverTarget=True)) 562 else: 563 replication = sql_messages.Settings.ReplicationTypeValueValuesEnum.SYNCHRONOUS 564 if not args.replication: 565 instance_resource.settings.replicationType = replication 566 567 if args.failover_replica_name: 568 instance_resource.failoverReplica = ( 569 sql_messages.DatabaseInstance.FailoverReplicaValue( 570 name=args.failover_replica_name)) 571 572 if args.collation: 573 instance_resource.settings.collation = args.collation 574 575 # BETA: Config for creating a replica of an external primary instance. 576 if _IsBetaOrNewer(release_track) and args.IsSpecified('master_username'): 577 # Ensure that the primary instance name is specified. 578 if not args.IsSpecified('master_instance_name'): 579 raise exceptions.RequiredArgumentException( 580 '--master-instance-name', 'To create a read replica of an external ' 581 'master instance, [--master-instance-name] must be specified') 582 583 # TODO(b/78648703): Remove when mutex required status is fixed. 584 # Ensure that the primary replication user password is specified. 585 if not (args.IsSpecified('master_password') or 586 args.IsSpecified('prompt_for_master_password')): 587 raise exceptions.RequiredArgumentException( 588 '--master-password', 'To create a read replica of an external ' 589 'master instance, [--master-password] or ' 590 '[--prompt-for-master-password] must be specified') 591 592 # Get password if not specified on command line. 593 if args.prompt_for_master_password: 594 args.master_password = console_io.PromptPassword( 595 'Master Instance Password: ') 596 597 instance_resource.replicaConfiguration = reducers.ReplicaConfiguration( 598 sql_messages, args.master_username, args.master_password, 599 args.master_dump_file_path, args.master_ca_certificate_path, 600 args.client_certificate_path, args.client_key_path) 601 602 is_primary = instance_resource.masterInstanceName is None 603 key_name = _GetAndValidateCmekKeyName(args, is_primary) 604 if key_name: 605 config = sql_messages.DiskEncryptionConfiguration( 606 kind='sql#diskEncryptionConfiguration', kmsKeyName=key_name) 607 instance_resource.diskEncryptionConfiguration = config 608 609 return instance_resource 610 611 @classmethod 612 def ConstructPatchInstanceFromArgs(cls, 613 sql_messages, 614 args, 615 original, 616 instance_ref=None, 617 release_track=DEFAULT_RELEASE_TRACK): 618 """Constructs Instance for patch request from base instance and args.""" 619 instance_resource = cls._ConstructBaseInstanceFromArgs( 620 sql_messages, args, original, instance_ref) 621 622 instance_resource.settings = cls._ConstructPatchSettingsFromArgs( 623 sql_messages, args, original, release_track) 624 625 return instance_resource 626 627 628class InstancesV1Beta4(_BaseInstances): 629 """Common utility functions for sql instances V1Beta4.""" 630 631 @staticmethod 632 def SetProjectAndInstanceFromRef(instance_resource, instance_ref): 633 instance_resource.project = instance_ref.project 634 instance_resource.name = instance_ref.instance 635 636 @staticmethod 637 def AddBackupConfigToSettings(settings, backup_config): 638 settings.backupConfiguration = backup_config 639 640 @staticmethod 641 def SetIpConfigurationEnabled(settings, assign_ip): 642 settings.ipConfiguration.ipv4Enabled = assign_ip 643 644 @staticmethod 645 def SetAuthorizedNetworks(settings, authorized_networks, acl_entry_value): 646 settings.ipConfiguration.authorizedNetworks = [ 647 acl_entry_value(kind='sql#aclEntry', value=n) 648 for n in authorized_networks 649 ] 650