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