# -*- coding: utf-8 -*- # # Copyright 2023 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. """Utils for VMware Engine private-clouds clusters commands. Provides helpers for parsing the autoscaling settings and node type configs and for combining settings from many sources together. """ from __future__ import absolute_import from __future__ import annotations from __future__ import division from __future__ import unicode_literals import collections import dataclasses from typing import Any, Dict, List, Union from googlecloudsdk.core import exceptions @dataclasses.dataclass(frozen=True) class ScalingThresholds: """Scaling thresholds for a single condition. Uses None for empty values. Attributes: scale_in: The threshold for scaling in. scale_out: The threshold for scaling out. """ scale_in: int scale_out: int def _MergeFields(left, right): """Merges two fields, favoring right one. Args: left: First field. right: Second field. Returns: Merged field. """ return right if right is not None else left def _MergeScalingThresholds( left: ScalingThresholds | None, right: ScalingThresholds | None ) -> ScalingThresholds | None: """Merges two ScalingThresholds objects, favoring right one. Args: left: First ScalingThresholds object. right: Second ScalingThresholds object. Returns: Merged ScalingThresholds - It contains the updated scale_in and scale_out values, favoring the right one. None - It indicates removal of threshold from autoscaling policy, favoring right one. Therefore, if right is None, return None. """ if left is None: return right if right is None: return None return ScalingThresholds( scale_in=_MergeFields(left.scale_in, right.scale_in), scale_out=_MergeFields(left.scale_out, right.scale_out), ) @dataclasses.dataclass(frozen=True) class AutoscalingPolicy: """Represents the autoscaling policy for a single node type. Uses None for empty settings. Attributes: node_type_id: The node type id. scale_out_size: The size of a single scale out operation. min_node_count: The minimum number of nodes of this type in the cluster. max_node_count: The maximum number of nodes of this type in the cluster. cpu_thresholds: The CPU thresholds. granted_memory_thresholds: The granted memory thresholds. consumed_memory_thresholds: The consumed memory thresholds. storage_thresholds: The storage thresholds. """ node_type_id: str scale_out_size: int min_node_count: int max_node_count: int cpu_thresholds: ScalingThresholds granted_memory_thresholds: ScalingThresholds consumed_memory_thresholds: ScalingThresholds storage_thresholds: ScalingThresholds def _MergeAutoscalingPolicies( left: AutoscalingPolicy, right: AutoscalingPolicy, ) -> AutoscalingPolicy: """Merges two AutoscalingPolicy objects, favoring right one. Args: left: First AutoscalingPolicy object. right: Second AutoscalingPolicy object. Returns: Merged AutoscalingPolicy. """ if left is None: return right if right is None: return left return AutoscalingPolicy( node_type_id=_MergeFields(left.node_type_id, right.node_type_id), scale_out_size=_MergeFields(left.scale_out_size, right.scale_out_size), min_node_count=_MergeFields(left.min_node_count, right.min_node_count), max_node_count=_MergeFields(left.max_node_count, right.max_node_count), cpu_thresholds=_MergeScalingThresholds( left.cpu_thresholds, right.cpu_thresholds ), granted_memory_thresholds=_MergeScalingThresholds( left.granted_memory_thresholds, right.granted_memory_thresholds ), consumed_memory_thresholds=_MergeScalingThresholds( left.consumed_memory_thresholds, right.consumed_memory_thresholds ), storage_thresholds=_MergeScalingThresholds( left.storage_thresholds, right.storage_thresholds ), ) @dataclasses.dataclass(frozen=True) class AutoscalingSettings: """Represents the autoscaling settings for a private-cloud cluster. Uses None for empty settings. Attributes: min_cluster_node_count: The minimum number of nodes in the cluster. max_cluster_node_count: The maximum number of nodes in the cluster. cool_down_period: The cool down period for autoscaling. autoscaling_policies: The autoscaling policies for each node type. """ min_cluster_node_count: int max_cluster_node_count: int cool_down_period: str autoscaling_policies: Dict[str, AutoscalingPolicy] def MergeAutoscalingSettings( left: AutoscalingSettings, right: AutoscalingSettings ) -> AutoscalingSettings: """Merges two AutoscalingSettings objects, favoring right one. Args: left: First AutoscalingSettings object. right: Second AutoscalingSettings object. Returns: Merged AutoscalingSettings. """ if left is None: return right if right is None: return left policies = {} for policy_name, policy in left.autoscaling_policies.items(): if policy_name in right.autoscaling_policies: policies[policy_name] = _MergeAutoscalingPolicies( policy, right.autoscaling_policies[policy_name] ) else: policies[policy_name] = policy for policy_name, policy in right.autoscaling_policies.items(): if policy_name not in left.autoscaling_policies: policies[policy_name] = policy return AutoscalingSettings( min_cluster_node_count=_MergeFields( left.min_cluster_node_count, right.min_cluster_node_count ), max_cluster_node_count=_MergeFields( left.max_cluster_node_count, right.max_cluster_node_count ), cool_down_period=_MergeFields( left.cool_down_period, right.cool_down_period ), autoscaling_policies=policies, ) class InvalidNodeConfigsProvidedError(exceptions.Error): def __init__(self, details): super(InvalidNodeConfigsProvidedError, self).__init__( f'INVALID_ARGUMENT: {details}' ) class InvalidAutoscalingSettingsProvidedError(exceptions.Error): def __init__(self, details): super(InvalidAutoscalingSettingsProvidedError, self).__init__( f'INVALID_ARGUMENT: {details}' ) NodeTypeConfig = collections.namedtuple( typename='NodeTypeConfig', field_names=['type', 'count', 'custom_core_count'], ) def FindDuplicatedTypes(types): type_counts = collections.Counter(types) return [node_type for node_type, count in type_counts.items() if count > 1] def ParseNodesConfigsParameters(nodes_configs): requested_node_types = [config['type'] for config in nodes_configs] duplicated_types = FindDuplicatedTypes(requested_node_types) if duplicated_types: raise InvalidNodeConfigsProvidedError( 'types: {} provided more than once.'.format(duplicated_types) ) return [ NodeTypeConfig( config['type'], config['count'], config.get('custom-core-count', 0) ) for config in nodes_configs ] def ParseAutoscalingSettingsFromInlinedFormat( min_cluster_node_count: int, max_cluster_node_count: int, cool_down_period: str, autoscaling_policies: List[Dict[str, Union[str, int]]], ) -> AutoscalingSettings: """Parses inlined autoscaling settings (passed as CLI arguments). The resulting object can later be passed to googlecloudsdk.api_lib.vmware.util.ConstructAutoscalingSettingsMessage. Args: min_cluster_node_count: autoscaling-min-cluster-node-count CLI argument. max_cluster_node_count: autoscaling-max-cluster-node-count CLI argument. cool_down_period: autoscaling-cool-down-period CLI argument. autoscaling_policies: list of update-autoscaling-policy CLI arguments. Returns: Equivalent AutoscalingSettings instance. """ parsed_settings = AutoscalingSettings( min_cluster_node_count=min_cluster_node_count, max_cluster_node_count=max_cluster_node_count, cool_down_period=cool_down_period, autoscaling_policies={}, ) for policy in autoscaling_policies: parsed_policy = AutoscalingPolicy( node_type_id=policy.get('node-type-id'), scale_out_size=policy.get('scale-out-size'), min_node_count=policy.get('min-node-count'), max_node_count=policy.get('max-node-count'), cpu_thresholds=_AutoscalingThresholdsFromPolicy( policy, 'cpu-thresholds' ), granted_memory_thresholds=_AutoscalingThresholdsFromPolicy( policy, 'granted-memory-thresholds' ), consumed_memory_thresholds=_AutoscalingThresholdsFromPolicy( policy, 'consumed-memory-thresholds' ), storage_thresholds=_AutoscalingThresholdsFromPolicy( policy, 'storage-thresholds' ), ) parsed_settings.autoscaling_policies[policy['name']] = parsed_policy return parsed_settings def _AutoscalingThresholdsFromPolicy( policy: Dict[str, Union[str, int]], threshold: str ) -> ScalingThresholds: scale_in = policy.get(f'{threshold}-scale-in') scale_out = policy.get(f'{threshold}-scale-out') if scale_in is None and scale_out is None: return None return ScalingThresholds(scale_in=scale_in, scale_out=scale_out) def _ValidateIfOnlySupportedKeysArePassed( keys: List[str], supported_keys: List[str] ): for key in keys: if key not in supported_keys: raise InvalidAutoscalingSettingsProvidedError( 'unsupported key: {key}, supported keys are: {supported_keys}'.format( key=key, supported_keys=supported_keys ) ) def ParseAutoscalingSettingsFromFileFormat( cluster: Dict[str, Any] ) -> AutoscalingSettings: """Parses the autoscaling settings from the format returned by the describe command. The resulting object can later be passed to googlecloudsdk.api_lib.vmware.util.ConstructAutoscalingSettingsMessage. Args: cluster: dictionary with the settings. Parsed from a file provided by user. Returns: Equivalent AutoscalingSettings instance. Raises: InvalidAutoscalingSettingsProvidedError: if the file format was wrong. """ def _ParseThresholds(thresholds_dict): if thresholds_dict is None: return None _ValidateIfOnlySupportedKeysArePassed( thresholds_dict.keys(), ['scaleIn', 'scaleOut'] ) return ScalingThresholds( scale_in=thresholds_dict.get('scaleIn'), scale_out=thresholds_dict.get('scaleOut'), ) _ValidateIfOnlySupportedKeysArePassed(cluster.keys(), ['autoscalingSettings']) if 'autoscalingSettings' not in cluster: raise InvalidAutoscalingSettingsProvidedError( 'autoscalingSettings not provided in the file' ) autoscaling_settings = cluster['autoscalingSettings'] _ValidateIfOnlySupportedKeysArePassed( autoscaling_settings.keys(), [ 'minClusterNodeCount', 'maxClusterNodeCount', 'coolDownPeriod', 'autoscalingPolicies', ], ) parsed_settings = AutoscalingSettings( min_cluster_node_count=autoscaling_settings.get('minClusterNodeCount'), max_cluster_node_count=autoscaling_settings.get('maxClusterNodeCount'), cool_down_period=autoscaling_settings.get('coolDownPeriod'), autoscaling_policies={}, ) if 'autoscalingPolicies' not in autoscaling_settings: return parsed_settings for policy_name, policy_settings in autoscaling_settings[ 'autoscalingPolicies' ].items(): _ValidateIfOnlySupportedKeysArePassed( policy_settings.keys(), [ 'nodeTypeId', 'scaleOutSize', 'minNodeCount', 'maxNodeCount', 'cpuThresholds', 'grantedMemoryThresholds', 'consumedMemoryThresholds', 'storageThresholds', ], ) parsed_policy = AutoscalingPolicy( node_type_id=policy_settings.get('nodeTypeId'), scale_out_size=policy_settings.get('scaleOutSize'), min_node_count=policy_settings.get('minNodeCount'), max_node_count=policy_settings.get('maxNodeCount'), cpu_thresholds=_ParseThresholds(policy_settings.get('cpuThresholds')), granted_memory_thresholds=_ParseThresholds( policy_settings.get('grantedMemoryThresholds') ), consumed_memory_thresholds=_ParseThresholds( policy_settings.get('consumedMemoryThresholds') ), storage_thresholds=_ParseThresholds( policy_settings.get('storageThresholds') ), ) parsed_settings.autoscaling_policies[policy_name] = parsed_policy return parsed_settings def ParseAutoscalingSettingsFromApiFormat( cluster_message, ) -> AutoscalingSettings: """Parses the autoscaling settings from the format returned by the describe command. The resulting object can later be passed to googlecloudsdk.api_lib.vmware.util.ConstructAutoscalingSettingsMessage. Args: cluster_message: cluster object with the autoscaling settings. Returns: Equivalent AutoscalingSettings istance. """ if cluster_message.autoscalingSettings is None: return None autoscaling_settings = cluster_message.autoscalingSettings parsed_settings = AutoscalingSettings( min_cluster_node_count=autoscaling_settings.minClusterNodeCount, max_cluster_node_count=autoscaling_settings.maxClusterNodeCount, cool_down_period=autoscaling_settings.coolDownPeriod, autoscaling_policies={}, ) for item in autoscaling_settings.autoscalingPolicies.additionalProperties: policy_name, policy_settings = item.key, item.value def _ParseThresholds(thresholds): if thresholds is None: return None return ScalingThresholds( scale_in=thresholds.scaleIn, scale_out=thresholds.scaleOut, ) parsed_policy = AutoscalingPolicy( node_type_id=policy_settings.nodeTypeId, scale_out_size=policy_settings.scaleOutSize, min_node_count=policy_settings.minNodeCount, max_node_count=policy_settings.maxNodeCount, cpu_thresholds=_ParseThresholds(policy_settings.cpuThresholds), granted_memory_thresholds=_ParseThresholds( policy_settings.grantedMemoryThresholds ), consumed_memory_thresholds=_ParseThresholds( policy_settings.consumedMemoryThresholds ), storage_thresholds=_ParseThresholds(policy_settings.storageThresholds), ) parsed_settings.autoscaling_policies[policy_name] = parsed_policy return parsed_settings