# -*- 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. """'vmware clusters update' command.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals from typing import List from googlecloudsdk.api_lib.vmware import clusters from googlecloudsdk.calliope import actions from googlecloudsdk.calliope import arg_parsers from googlecloudsdk.calliope import base from googlecloudsdk.command_lib.vmware import flags from googlecloudsdk.command_lib.vmware.clusters import util from googlecloudsdk.core import log DETAILED_HELP = { 'DESCRIPTION': """ Adjust the number of nodes in the VMware Engine cluster. Successful addition or removal of a node results in a cluster in READY state. Check the progress of a cluster using `{parent_command} list`. """, 'EXAMPLES': """ To resize a cluster called `my-cluster` in private cloud `my-private-cloud` and zone `us-west2-a` to have `3` nodes of type `standard-72`, run: $ {command} my-cluster --location=us-west2-a --project=my-project --private-cloud=my-private-cloud --update-nodes-config=type=standard-72,count=3 Or: $ {command} my-cluster --private-cloud=my-private-cloud --update-nodes-config=type=standard-72,count=3 In the second example, the project and location are taken from gcloud properties core/project and compute/zone. To enable autoscale in a cluster called `my-cluster` in private cloud `my-private-cloud` and zone `us-west2-a`, run: $ {command} my-cluster --location=us-west2-a --project=my-project --private-cloud=my-private-cloud --autoscaling-min-cluster-node-count=3 --autoscaling-max-cluster-node-count=5 --update-autoscaling-policy=name=custom-policy,node-type-id=standard-72,scale-out-size=1,storage-thresholds-scale-in=10,storage-thresholds-scale-out=80 """, } _NODE_TYPE_CONFIG_HELP = """ Information about the type and number of nodes associated with the cluster. type (required): canonical identifier of the node type. count (required): number of nodes of this type in the cluster. """ _OLD_NODE_TYPE_CONFIG_HELP = _NODE_TYPE_CONFIG_HELP + """ custom_core_count: can be passed, but the value will be ignored. Updating custom core count is not supported. """ def _ParseOldNodesConfigsParameters(existing_cluster, nodes_configs): """Parses the node configs parameters passed in the old format. In the old format, the nodes configs are passed in a way that specifies what exact node configs should be attached to the cluster after the operation. It's not possible to remove existing node types. Even unchanged nodes configs have to be specified in the parameters. Args: existing_cluster: cluster whose nodes configs should be updated nodes_configs: nodes configs to be attached to the cluster Returns: list of NodeTypeConfig objects prepared for further processing Raises: InvalidNodeConfigsProvidedError: if duplicate node types were specified or if a config for an existing node type is not specified """ current_node_types = [ prop.key for prop in existing_cluster.nodeTypeConfigs.additionalProperties ] requested_node_types = [config['type'] for config in nodes_configs] duplicated_types = util.FindDuplicatedTypes(requested_node_types) if duplicated_types: raise util.InvalidNodeConfigsProvidedError( f'types: {duplicated_types} provided more than once.' ) unspecified_types = set(current_node_types) - set(requested_node_types) if unspecified_types: raise util.InvalidNodeConfigsProvidedError( 'when using `--node-type-config` parameters you need to specify node' ' counts for all node types present in the cluster. Missing node' f' types: {list(unspecified_types)}.' ) return [ util.NodeTypeConfig( type=config['type'], count=config['count'], custom_core_count=0 ) for config in nodes_configs ] def _ParseNewNodesConfigsParameters( existing_cluster, updated_nodes_configs, removed_types ): """Parses the node configs parameters passed in the new format. In the new format, the nodes configs are passed using two parameters. One of them specifies which configs should be updated or created (unchanged configs don't have to be specified at all). The other lists the configs to be removed. This format is more flexible than the old one because it allows for config removal and doesn't require re-specifying unchanged configs. Args: existing_cluster: cluster whose nodes configs should be updated updated_nodes_configs: list of nodes configs to update or create removed_types: list of node types for which nodes configs should be removed Returns: list of NodeTypeConfig objects prepared for further processing Raises: InvalidNodeConfigsProvidedError: if duplicate node types were specified """ requested_node_types = [ config['type'] for config in updated_nodes_configs ] + removed_types duplicated_types = util.FindDuplicatedTypes(requested_node_types) if duplicated_types: raise util.InvalidNodeConfigsProvidedError( f'types: {duplicated_types} provided more than once.' ) node_count = {} for prop in existing_cluster.nodeTypeConfigs.additionalProperties: node_count[prop.key] = prop.value.nodeCount for config in updated_nodes_configs: node_count[config['type']] = config['count'] for node_type in removed_types: node_count[node_type] = 0 return [ util.NodeTypeConfig(type=node_type, count=count, custom_core_count=0) for node_type, count in node_count.items() ] def _ValidatePoliciesToRemove( existing_cluster, updated_settings, policies_to_remove ): """Checks if the policies specified for removal actually exist and that they are not updated in the same call. Args: existing_cluster: cluster before the update updated_settings: updated autoscale settings policies_to_remove: list of policy names to remove Raises: InvalidAutoscalingSettingsProvidedError: if the validation fails. """ if not policies_to_remove: return if updated_settings and updated_settings.autoscaling_policies: for name in updated_settings.autoscaling_policies: if name in policies_to_remove: raise util.InvalidAutoscalingSettingsProvidedError( f"policy '{name}' specified both for update and removal" ) if not existing_cluster.autoscalingSettings: raise util.InvalidAutoscalingSettingsProvidedError( f"nonexistent policies '{policies_to_remove}' specified for removal" ) existing_policies = { p.key for p in existing_cluster.autoscalingSettings.autoscalingPolicies.additionalProperties } for name in policies_to_remove: if name not in existing_policies: raise util.InvalidAutoscalingSettingsProvidedError( f"nonexistent policies '{policies_to_remove}' specified for removal" ) def _RemoveAutoscalingPolicies( autoscaling_settings: util.AutoscalingSettings, policies_to_remove: List[str], ) -> util.AutoscalingSettings: if not policies_to_remove: return autoscaling_settings for policy in policies_to_remove: del autoscaling_settings.autoscaling_policies[policy] return autoscaling_settings @base.ReleaseTracks(base.ReleaseTrack.GA) @base.DefaultUniverseOnly class Update(base.UpdateCommand): """Update a Google Cloud VMware Engine cluster.""" detailed_help = DETAILED_HELP @staticmethod def Args(parser): """Register flags for this command.""" flags.AddClusterArgToParser(parser, positional=True) base.ASYNC_FLAG.AddToParser(parser) base.ASYNC_FLAG.SetDefault(parser, True) parser.display_info.AddFormat('yaml') parser.add_argument( '--node-type-config', required=False, type=arg_parsers.ArgDict( spec={'type': str, 'count': int, 'custom-core-count': int}, required_keys=('type', 'count'), ), action=actions.DeprecationAction( '--node-type-config', warn=( 'The {flag_name} option is deprecated; please use' ' --update-nodes-config and --remove-nodes-config instead.' ), removed=False, action='append', ), metavar='[count=COUNT],[type=TYPE]', help=_OLD_NODE_TYPE_CONFIG_HELP, ) parser.add_argument( '--update-nodes-config', required=False, default=list(), type=arg_parsers.ArgDict( spec={'type': str, 'count': int}, required_keys=('type', 'count'), ), action='append', help=_NODE_TYPE_CONFIG_HELP, ) parser.add_argument( '--remove-nodes-config', required=False, metavar='TYPE', default=list(), type=str, action='append', help='Type of node that should be removed from the cluster', ) autoscaling_settings_group = parser.add_mutually_exclusive_group( required=False ) inlined_autoscaling_settings_group = autoscaling_settings_group.add_group() inlined_autoscaling_settings_group.add_argument( '--autoscaling-min-cluster-node-count', type=int, help='Minimum number of nodes in the cluster', ) inlined_autoscaling_settings_group.add_argument( '--autoscaling-max-cluster-node-count', type=int, help='Maximum number of nodes in the cluster', ) inlined_autoscaling_settings_group.add_argument( '--autoscaling-cool-down-period', type=str, help=( 'Cool down period (in minutes) between consecutive cluster' ' expansions/contractions' ), ) inlined_autoscaling_settings_group.add_argument( '--update-autoscaling-policy', type=arg_parsers.ArgDict( spec={ 'name': str, 'node-type-id': str, 'scale-out-size': int, 'min-node-count': int, 'max-node-count': int, 'cpu-thresholds-scale-in': int, 'cpu-thresholds-scale-out': int, 'granted-memory-thresholds-scale-in': int, 'granted-memory-thresholds-scale-out': int, 'consumed-memory-thresholds-scale-in': int, 'consumed-memory-thresholds-scale-out': int, 'storage-thresholds-scale-in': int, 'storage-thresholds-scale-out': int, }, required_keys=['name'], ), action='append', default=list(), help='Autoscaling policy to be applied to the cluster', ) autoscaling_settings_group.add_argument( '--autoscaling-settings-from-file', type=arg_parsers.YAMLFileContents(), help=( 'A YAML file containing the autoscaling settings to be applied to' ' the cluster' ), ) parser.add_argument( '--remove-autoscaling-policy', required=False, metavar='NAME', default=list(), type=str, action='append', help=( 'Names of autoscaling policies that should be removed from the' ' cluster' ), ) def Run(self, args): cluster = args.CONCEPTS.cluster.Parse() client = clusters.ClustersClient() if args.node_type_config and ( args.update_nodes_config or args.remove_nodes_config ): raise util.InvalidNodeConfigsProvidedError( 'flag `--node-type-config` is mutually exclusive with' ' `--update-nodes-config` and `--remove-nodes-config` flags.' ) existing_cluster = client.Get(cluster) if args.node_type_config: configs = _ParseOldNodesConfigsParameters( existing_cluster, args.node_type_config ) elif args.update_nodes_config or args.remove_nodes_config: configs = _ParseNewNodesConfigsParameters( existing_cluster, args.update_nodes_config, args.remove_nodes_config ) else: configs = None if args.autoscaling_settings_from_file: updated_settings = util.ParseAutoscalingSettingsFromFileFormat( args.autoscaling_settings_from_file ) elif ( args.autoscaling_min_cluster_node_count or args.autoscaling_max_cluster_node_count or args.autoscaling_cool_down_period or args.update_autoscaling_policy ): updated_settings = util.ParseAutoscalingSettingsFromInlinedFormat( args.autoscaling_min_cluster_node_count, args.autoscaling_max_cluster_node_count, args.autoscaling_cool_down_period, args.update_autoscaling_policy, ) else: updated_settings = None _ValidatePoliciesToRemove( existing_cluster, updated_settings, args.remove_autoscaling_policy ) autoscaling_settings = None if updated_settings is not None or args.remove_autoscaling_policy: old_settings = util.ParseAutoscalingSettingsFromApiFormat( existing_cluster ) autoscaling_settings = util.MergeAutoscalingSettings( old_settings, updated_settings ) autoscaling_settings = _RemoveAutoscalingPolicies( autoscaling_settings, args.remove_autoscaling_policy ) operation = client.Update(cluster, configs, autoscaling_settings) is_async = args.async_ if is_async: log.UpdatedResource(operation.name, kind='cluster', is_async=True) return resource = client.WaitForOperation( operation_ref=client.GetOperationRef(operation), message='waiting for cluster [{}] to be updated'.format( cluster.RelativeName() ), ) log.UpdatedResource(cluster.RelativeName(), kind='cluster') return resource