awslabs.elasticache-mcp-server 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- awslabs/__init__.py +16 -0
- awslabs/elasticache_mcp_server/__init__.py +17 -0
- awslabs/elasticache_mcp_server/common/__init__.py +15 -0
- awslabs/elasticache_mcp_server/common/connection.py +117 -0
- awslabs/elasticache_mcp_server/common/decorators.py +41 -0
- awslabs/elasticache_mcp_server/common/server.py +30 -0
- awslabs/elasticache_mcp_server/context.py +39 -0
- awslabs/elasticache_mcp_server/main.py +52 -0
- awslabs/elasticache_mcp_server/tools/__init__.py +15 -0
- awslabs/elasticache_mcp_server/tools/cc/__init__.py +31 -0
- awslabs/elasticache_mcp_server/tools/cc/connect.py +444 -0
- awslabs/elasticache_mcp_server/tools/cc/create.py +212 -0
- awslabs/elasticache_mcp_server/tools/cc/delete.py +65 -0
- awslabs/elasticache_mcp_server/tools/cc/describe.py +80 -0
- awslabs/elasticache_mcp_server/tools/cc/modify.py +159 -0
- awslabs/elasticache_mcp_server/tools/cc/parsers.py +78 -0
- awslabs/elasticache_mcp_server/tools/cc/processors.py +74 -0
- awslabs/elasticache_mcp_server/tools/ce/__init__.py +19 -0
- awslabs/elasticache_mcp_server/tools/ce/get_cost_and_usage.py +76 -0
- awslabs/elasticache_mcp_server/tools/cw/__init__.py +19 -0
- awslabs/elasticache_mcp_server/tools/cw/get_metric_statistics.py +85 -0
- awslabs/elasticache_mcp_server/tools/cwlogs/__init__.py +29 -0
- awslabs/elasticache_mcp_server/tools/cwlogs/create_log_group.py +68 -0
- awslabs/elasticache_mcp_server/tools/cwlogs/describe_log_groups.py +123 -0
- awslabs/elasticache_mcp_server/tools/cwlogs/describe_log_streams.py +120 -0
- awslabs/elasticache_mcp_server/tools/cwlogs/filter_log_events.py +122 -0
- awslabs/elasticache_mcp_server/tools/cwlogs/get_log_events.py +99 -0
- awslabs/elasticache_mcp_server/tools/firehose/__init__.py +19 -0
- awslabs/elasticache_mcp_server/tools/firehose/list_delivery_streams.py +63 -0
- awslabs/elasticache_mcp_server/tools/misc/__init__.py +31 -0
- awslabs/elasticache_mcp_server/tools/misc/batch_apply_update_action.py +62 -0
- awslabs/elasticache_mcp_server/tools/misc/batch_stop_update_action.py +62 -0
- awslabs/elasticache_mcp_server/tools/misc/describe_cache_engine_versions.py +79 -0
- awslabs/elasticache_mcp_server/tools/misc/describe_engine_default_parameters.py +64 -0
- awslabs/elasticache_mcp_server/tools/misc/describe_events.py +86 -0
- awslabs/elasticache_mcp_server/tools/misc/describe_service_updates.py +71 -0
- awslabs/elasticache_mcp_server/tools/rg/__init__.py +54 -0
- awslabs/elasticache_mcp_server/tools/rg/complete_migration.py +94 -0
- awslabs/elasticache_mcp_server/tools/rg/connect.py +537 -0
- awslabs/elasticache_mcp_server/tools/rg/create.py +318 -0
- awslabs/elasticache_mcp_server/tools/rg/delete.py +68 -0
- awslabs/elasticache_mcp_server/tools/rg/describe.py +68 -0
- awslabs/elasticache_mcp_server/tools/rg/modify.py +236 -0
- awslabs/elasticache_mcp_server/tools/rg/parsers.py +268 -0
- awslabs/elasticache_mcp_server/tools/rg/processors.py +227 -0
- awslabs/elasticache_mcp_server/tools/rg/start_migration.py +151 -0
- awslabs/elasticache_mcp_server/tools/rg/test_migration.py +139 -0
- awslabs/elasticache_mcp_server/tools/serverless/__init__.py +37 -0
- awslabs/elasticache_mcp_server/tools/serverless/connect.py +451 -0
- awslabs/elasticache_mcp_server/tools/serverless/create.py +174 -0
- awslabs/elasticache_mcp_server/tools/serverless/delete.py +49 -0
- awslabs/elasticache_mcp_server/tools/serverless/describe.py +69 -0
- awslabs/elasticache_mcp_server/tools/serverless/models.py +160 -0
- awslabs/elasticache_mcp_server/tools/serverless/modify.py +95 -0
- awslabs_elasticache_mcp_server-0.1.1.dist-info/METADATA +257 -0
- awslabs_elasticache_mcp_server-0.1.1.dist-info/RECORD +60 -0
- awslabs_elasticache_mcp_server-0.1.1.dist-info/WHEEL +4 -0
- awslabs_elasticache_mcp_server-0.1.1.dist-info/entry_points.txt +2 -0
- awslabs_elasticache_mcp_server-0.1.1.dist-info/licenses/LICENSE +175 -0
- awslabs_elasticache_mcp_server-0.1.1.dist-info/licenses/NOTICE +2 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Parser functions for ElastiCache replication group tools."""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from typing import Any, Dict
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_shorthand_resharding(config: str) -> Dict[str, Any]:
|
|
22
|
+
"""Parse a single resharding configuration from shorthand syntax.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
config: Shorthand syntax string for resharding configuration
|
|
26
|
+
Format: NodeGroupId=string,NewShardConfiguration={NewReplicaCount=integer,PreferredAvailabilityZones=string1,string2}
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dictionary containing the parsed resharding configuration
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: If the syntax is invalid
|
|
33
|
+
"""
|
|
34
|
+
if not config:
|
|
35
|
+
raise ValueError('Empty resharding configuration')
|
|
36
|
+
|
|
37
|
+
result = {}
|
|
38
|
+
pairs = config.split(',')
|
|
39
|
+
|
|
40
|
+
# Define valid keys and their processors
|
|
41
|
+
key_processors = {
|
|
42
|
+
'NodeGroupId': str,
|
|
43
|
+
'NewShardConfiguration': lambda x: parse_new_shard_config(x),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for pair in pairs:
|
|
47
|
+
if '=' not in pair:
|
|
48
|
+
raise ValueError(f'Invalid format. Each parameter must be in key=value format: {pair}')
|
|
49
|
+
|
|
50
|
+
key, value = pair.split('=', 1)
|
|
51
|
+
if not key or not value:
|
|
52
|
+
raise ValueError(f'Empty key or value: {pair}')
|
|
53
|
+
|
|
54
|
+
if key not in key_processors:
|
|
55
|
+
raise ValueError(f'Invalid parameter: {key}')
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
result[key] = key_processors[key](value)
|
|
59
|
+
except ValueError as e:
|
|
60
|
+
raise ValueError(f'Invalid value for {key}: {value}') from e
|
|
61
|
+
|
|
62
|
+
# Validate required fields
|
|
63
|
+
if 'NodeGroupId' not in result:
|
|
64
|
+
raise ValueError('Missing required field: NodeGroupId')
|
|
65
|
+
if 'NewShardConfiguration' not in result:
|
|
66
|
+
raise ValueError('Missing required field: NewShardConfiguration')
|
|
67
|
+
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_new_shard_config(config: str) -> Dict[str, Any]:
|
|
72
|
+
"""Parse the NewShardConfiguration portion of resharding configuration.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
config: String containing new shard configuration
|
|
76
|
+
Format: {NewReplicaCount=integer,PreferredAvailabilityZones=string1,string2}
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary containing the parsed new shard configuration
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ValueError: If the syntax is invalid
|
|
83
|
+
"""
|
|
84
|
+
if not config.startswith('{') or not config.endswith('}'):
|
|
85
|
+
raise ValueError('NewShardConfiguration must be enclosed in curly braces')
|
|
86
|
+
|
|
87
|
+
# Remove curly braces
|
|
88
|
+
config = config[1:-1]
|
|
89
|
+
|
|
90
|
+
result = {}
|
|
91
|
+
pairs = config.split(',')
|
|
92
|
+
|
|
93
|
+
# Define valid keys and their processors
|
|
94
|
+
key_processors = {
|
|
95
|
+
'NewReplicaCount': int,
|
|
96
|
+
'PreferredAvailabilityZones': lambda x: x.split(','),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for pair in pairs:
|
|
100
|
+
if '=' not in pair:
|
|
101
|
+
raise ValueError(f'Invalid format. Each parameter must be in key=value format: {pair}')
|
|
102
|
+
|
|
103
|
+
key, value = pair.split('=', 1)
|
|
104
|
+
if not key or not value:
|
|
105
|
+
raise ValueError(f'Empty key or value: {pair}')
|
|
106
|
+
|
|
107
|
+
if key not in key_processors:
|
|
108
|
+
raise ValueError(f'Invalid parameter: {key}')
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
result[key] = key_processors[key](value)
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
raise ValueError(f'Invalid value for {key}: {value}') from e
|
|
114
|
+
|
|
115
|
+
# Validate required fields
|
|
116
|
+
if 'NewReplicaCount' not in result:
|
|
117
|
+
raise ValueError('Missing required field: NewReplicaCount')
|
|
118
|
+
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_shorthand_nodegroup(group: str) -> Dict[str, Any]:
|
|
123
|
+
"""Parse a single nodegroup from shorthand syntax.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
group: Shorthand syntax string for a nodegroup
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dictionary containing the parsed nodegroup configuration
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If the syntax is invalid
|
|
133
|
+
"""
|
|
134
|
+
if not group:
|
|
135
|
+
raise ValueError('Empty nodegroup configuration')
|
|
136
|
+
|
|
137
|
+
config = {}
|
|
138
|
+
|
|
139
|
+
# Define valid keys
|
|
140
|
+
valid_keys = {
|
|
141
|
+
'NodeGroupId',
|
|
142
|
+
'Slots',
|
|
143
|
+
'ReplicaCount',
|
|
144
|
+
'PrimaryAvailabilityZone',
|
|
145
|
+
'ReplicaAvailabilityZones',
|
|
146
|
+
'PrimaryOutpostArn',
|
|
147
|
+
'ReplicaOutpostArns',
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Define keys that should be treated as arrays
|
|
151
|
+
array_keys = {'ReplicaAvailabilityZones', 'ReplicaOutpostArns'}
|
|
152
|
+
|
|
153
|
+
# Split into key-value pairs
|
|
154
|
+
pairs = group.split(',')
|
|
155
|
+
current_key = None
|
|
156
|
+
current_values = []
|
|
157
|
+
|
|
158
|
+
for pair in pairs:
|
|
159
|
+
pair = pair.strip()
|
|
160
|
+
|
|
161
|
+
# If this part contains an equals sign, it's a new key-value pair
|
|
162
|
+
if '=' in pair:
|
|
163
|
+
# Save any previous array values
|
|
164
|
+
if current_key in array_keys and current_values:
|
|
165
|
+
config[current_key] = current_values
|
|
166
|
+
current_values = []
|
|
167
|
+
|
|
168
|
+
key, value = pair.split('=', 1)
|
|
169
|
+
key = key.strip()
|
|
170
|
+
value = value.strip()
|
|
171
|
+
|
|
172
|
+
if not key or not value:
|
|
173
|
+
raise ValueError(f'Empty key or value: {pair}')
|
|
174
|
+
|
|
175
|
+
if key not in valid_keys:
|
|
176
|
+
raise ValueError(f'Invalid parameter: {key}')
|
|
177
|
+
|
|
178
|
+
current_key = key
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
if key == 'ReplicaCount':
|
|
182
|
+
config[key] = int(value)
|
|
183
|
+
elif key in array_keys:
|
|
184
|
+
current_values = [value]
|
|
185
|
+
else:
|
|
186
|
+
config[key] = value
|
|
187
|
+
except ValueError as e:
|
|
188
|
+
raise ValueError(f'Invalid value for {key}: {value}') from e
|
|
189
|
+
|
|
190
|
+
# If no equals sign and we're in an array key context, treat as array value
|
|
191
|
+
elif current_key in array_keys:
|
|
192
|
+
current_values.append(pair)
|
|
193
|
+
else:
|
|
194
|
+
raise ValueError(f'Invalid format. Each parameter must be in key=value format: {pair}')
|
|
195
|
+
|
|
196
|
+
# Handle any remaining array values
|
|
197
|
+
if current_key in array_keys and current_values:
|
|
198
|
+
config[current_key] = current_values
|
|
199
|
+
|
|
200
|
+
# Validate required fields
|
|
201
|
+
if 'NodeGroupId' not in config:
|
|
202
|
+
raise ValueError('Missing required field: NodeGroupId')
|
|
203
|
+
|
|
204
|
+
return config
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def parse_shorthand_log_delivery(config: str) -> Dict[str, Any]:
|
|
208
|
+
"""Parse a single log delivery configuration from shorthand syntax.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
config: Shorthand syntax string for log delivery configuration
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dictionary containing the parsed log delivery configuration
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
ValueError: If the syntax is invalid
|
|
218
|
+
"""
|
|
219
|
+
if not config:
|
|
220
|
+
raise ValueError('Empty log delivery configuration')
|
|
221
|
+
|
|
222
|
+
result = {}
|
|
223
|
+
pairs = config.split(',')
|
|
224
|
+
|
|
225
|
+
# Define valid keys and their processors
|
|
226
|
+
key_processors = {
|
|
227
|
+
'LogType': str,
|
|
228
|
+
'DestinationType': str,
|
|
229
|
+
'DestinationDetails': lambda x: json.loads(x.replace("'", '"')),
|
|
230
|
+
'LogFormat': str,
|
|
231
|
+
'Enabled': lambda x: x.lower() == 'true',
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for pair in pairs:
|
|
235
|
+
if '=' not in pair:
|
|
236
|
+
raise ValueError(f'Invalid format. Each parameter must be in key=value format: {pair}')
|
|
237
|
+
|
|
238
|
+
key, value = pair.split('=', 1)
|
|
239
|
+
if not key or not value:
|
|
240
|
+
raise ValueError(f'Empty key or value: {pair}')
|
|
241
|
+
|
|
242
|
+
if key not in key_processors:
|
|
243
|
+
raise ValueError(f'Invalid parameter: {key}')
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
result[key] = key_processors[key](value)
|
|
247
|
+
except ValueError as e:
|
|
248
|
+
raise ValueError(f'Invalid value for {key}: {value}') from e
|
|
249
|
+
|
|
250
|
+
# Validate required fields
|
|
251
|
+
required_fields = ['LogType', 'DestinationType', 'DestinationDetails', 'LogFormat', 'Enabled']
|
|
252
|
+
missing_fields = [field for field in required_fields if field not in result]
|
|
253
|
+
if missing_fields:
|
|
254
|
+
raise ValueError(f'Missing required fields: {", ".join(missing_fields)}')
|
|
255
|
+
|
|
256
|
+
# Validate LogType
|
|
257
|
+
if result['LogType'] not in ['slow-log', 'engine-log']:
|
|
258
|
+
raise ValueError("LogType must be either 'slow-log' or 'engine-log'")
|
|
259
|
+
|
|
260
|
+
# Validate DestinationType
|
|
261
|
+
if result['DestinationType'] not in ['cloudwatch-logs', 'kinesis-firehose']:
|
|
262
|
+
raise ValueError("DestinationType must be either 'cloudwatch-logs' or 'kinesis-firehose'")
|
|
263
|
+
|
|
264
|
+
# Validate LogFormat
|
|
265
|
+
if result['LogFormat'] not in ['text', 'json']:
|
|
266
|
+
raise ValueError("LogFormat must be either 'text' or 'json'")
|
|
267
|
+
|
|
268
|
+
return result
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Processor functions for ElastiCache replication group tools."""
|
|
16
|
+
|
|
17
|
+
from .parsers import (
|
|
18
|
+
parse_shorthand_log_delivery,
|
|
19
|
+
parse_shorthand_nodegroup,
|
|
20
|
+
parse_shorthand_resharding,
|
|
21
|
+
)
|
|
22
|
+
from typing import Dict, List, Union
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def process_resharding_configuration(
|
|
26
|
+
resharding_configuration: Union[str, List[Dict]],
|
|
27
|
+
) -> List[Dict]:
|
|
28
|
+
"""Process resharding configuration in either shorthand or JSON format.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
resharding_configuration: Resharding configuration in either format
|
|
32
|
+
Shorthand format: "NodeGroupId=string,NewShardConfiguration={NewReplicaCount=integer,PreferredAvailabilityZones=string1,string2}"
|
|
33
|
+
Multiple configurations can be separated by spaces.
|
|
34
|
+
JSON format: List of dictionaries with required fields:
|
|
35
|
+
- NodeGroupId: string
|
|
36
|
+
- NewShardConfiguration:
|
|
37
|
+
- NewReplicaCount: integer
|
|
38
|
+
- PreferredAvailabilityZones: list of strings (optional)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of processed resharding configurations
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If the configuration is invalid
|
|
45
|
+
"""
|
|
46
|
+
processed_configs = []
|
|
47
|
+
|
|
48
|
+
if isinstance(resharding_configuration, str):
|
|
49
|
+
# Parse shorthand syntax
|
|
50
|
+
configs = resharding_configuration.split(' ')
|
|
51
|
+
for config in configs:
|
|
52
|
+
if not config:
|
|
53
|
+
continue
|
|
54
|
+
try:
|
|
55
|
+
parsed_config = parse_shorthand_resharding(config)
|
|
56
|
+
processed_configs.append(parsed_config)
|
|
57
|
+
except ValueError as e:
|
|
58
|
+
raise ValueError(f'Invalid resharding shorthand syntax: {str(e)}')
|
|
59
|
+
else:
|
|
60
|
+
# Handle JSON format
|
|
61
|
+
if not isinstance(resharding_configuration, list):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
'Resharding configuration must be a list of dictionaries or a shorthand string'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
for config in resharding_configuration:
|
|
67
|
+
if not isinstance(config, dict):
|
|
68
|
+
raise ValueError('Each resharding configuration must be a dictionary')
|
|
69
|
+
|
|
70
|
+
# Validate required fields
|
|
71
|
+
if 'NodeGroupId' not in config:
|
|
72
|
+
raise ValueError('Missing required field: NodeGroupId')
|
|
73
|
+
if 'NewShardConfiguration' not in config:
|
|
74
|
+
raise ValueError('Missing required field: NewShardConfiguration')
|
|
75
|
+
|
|
76
|
+
# Validate NewShardConfiguration
|
|
77
|
+
new_shard = config['NewShardConfiguration']
|
|
78
|
+
if not isinstance(new_shard, dict):
|
|
79
|
+
raise ValueError('NewShardConfiguration must be a dictionary')
|
|
80
|
+
if 'NewReplicaCount' not in new_shard:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
'Missing required field: NewReplicaCount in NewShardConfiguration'
|
|
83
|
+
)
|
|
84
|
+
if not isinstance(new_shard['NewReplicaCount'], int):
|
|
85
|
+
raise ValueError('NewReplicaCount must be an integer')
|
|
86
|
+
|
|
87
|
+
# Validate PreferredAvailabilityZones if present
|
|
88
|
+
if 'PreferredAvailabilityZones' in new_shard:
|
|
89
|
+
if not isinstance(new_shard['PreferredAvailabilityZones'], list):
|
|
90
|
+
raise ValueError('PreferredAvailabilityZones must be a list of strings')
|
|
91
|
+
for zone in new_shard['PreferredAvailabilityZones']:
|
|
92
|
+
if not isinstance(zone, str):
|
|
93
|
+
raise ValueError('Each availability zone must be a string')
|
|
94
|
+
|
|
95
|
+
processed_configs.append(config)
|
|
96
|
+
|
|
97
|
+
return processed_configs
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def process_log_delivery_configurations(
|
|
101
|
+
log_delivery_configurations: Union[str, List[Dict]],
|
|
102
|
+
) -> List[Dict]:
|
|
103
|
+
"""Process log delivery configurations in either shorthand or JSON format.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
log_delivery_configurations: Log delivery configurations in either format
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
List of processed log delivery configurations
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ValueError: If the configuration is invalid
|
|
113
|
+
"""
|
|
114
|
+
processed_configs = []
|
|
115
|
+
|
|
116
|
+
if isinstance(log_delivery_configurations, str):
|
|
117
|
+
# Parse shorthand syntax
|
|
118
|
+
configs = log_delivery_configurations.split(' ')
|
|
119
|
+
for config in configs:
|
|
120
|
+
if not config:
|
|
121
|
+
continue
|
|
122
|
+
try:
|
|
123
|
+
parsed_config = parse_shorthand_log_delivery(config)
|
|
124
|
+
processed_configs.append(parsed_config)
|
|
125
|
+
except ValueError as e:
|
|
126
|
+
raise ValueError(f'Invalid log delivery shorthand syntax: {str(e)}')
|
|
127
|
+
else:
|
|
128
|
+
# Handle JSON format
|
|
129
|
+
if not isinstance(log_delivery_configurations, list):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
'Log delivery configurations must be a list of dictionaries or a shorthand string'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
for config in log_delivery_configurations:
|
|
135
|
+
if not isinstance(config, dict):
|
|
136
|
+
raise ValueError('Each log delivery configuration must be a dictionary')
|
|
137
|
+
|
|
138
|
+
# Validate required fields and types
|
|
139
|
+
required_fields = {
|
|
140
|
+
'LogType': ['slow-log', 'engine-log'],
|
|
141
|
+
'DestinationType': ['cloudwatch-logs', 'kinesis-firehose'],
|
|
142
|
+
'DestinationDetails': dict,
|
|
143
|
+
'LogFormat': ['text', 'json'],
|
|
144
|
+
'Enabled': bool,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for field, valid_values in required_fields.items():
|
|
148
|
+
if field not in config:
|
|
149
|
+
raise ValueError(f'Missing required field: {field}')
|
|
150
|
+
|
|
151
|
+
if field in ['LogType', 'DestinationType', 'LogFormat']:
|
|
152
|
+
if config[field] not in valid_values:
|
|
153
|
+
raise ValueError(f'{field} must be one of {valid_values}')
|
|
154
|
+
elif field == 'DestinationDetails':
|
|
155
|
+
if not isinstance(config[field], valid_values):
|
|
156
|
+
raise ValueError(f'{field} must be a dictionary')
|
|
157
|
+
elif field == 'Enabled':
|
|
158
|
+
if not isinstance(config[field], valid_values):
|
|
159
|
+
raise ValueError(f'{field} must be a boolean')
|
|
160
|
+
|
|
161
|
+
processed_configs.append(config)
|
|
162
|
+
|
|
163
|
+
return processed_configs
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def process_nodegroup_configuration(
|
|
167
|
+
node_group_configuration: Union[str, List[Dict]],
|
|
168
|
+
) -> List[Dict]:
|
|
169
|
+
"""Process nodegroup configuration in either shorthand or JSON format.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
node_group_configuration: Nodegroup configuration in either format
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
List of processed nodegroup configurations
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
ValueError: If the configuration is invalid
|
|
179
|
+
"""
|
|
180
|
+
processed_config = []
|
|
181
|
+
|
|
182
|
+
if isinstance(node_group_configuration, str):
|
|
183
|
+
# Parse shorthand syntax
|
|
184
|
+
groups = node_group_configuration.split(' ')
|
|
185
|
+
for group in groups:
|
|
186
|
+
if not group:
|
|
187
|
+
continue
|
|
188
|
+
try:
|
|
189
|
+
config = parse_shorthand_nodegroup(group)
|
|
190
|
+
processed_config.append(config)
|
|
191
|
+
except ValueError as e:
|
|
192
|
+
raise ValueError(f'Invalid nodegroup shorthand syntax: {str(e)}')
|
|
193
|
+
else:
|
|
194
|
+
# Handle JSON format
|
|
195
|
+
if not isinstance(node_group_configuration, list):
|
|
196
|
+
raise ValueError(
|
|
197
|
+
'Node group configuration must be a list of dictionaries or a shorthand string'
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
for config in node_group_configuration:
|
|
201
|
+
if not isinstance(config, dict):
|
|
202
|
+
raise ValueError('Each node group configuration must be a dictionary')
|
|
203
|
+
|
|
204
|
+
# Validate required fields
|
|
205
|
+
if 'NodeGroupId' not in config:
|
|
206
|
+
raise ValueError('Missing required field: NodeGroupId')
|
|
207
|
+
|
|
208
|
+
# Process the configuration
|
|
209
|
+
processed_item = {}
|
|
210
|
+
for k, v in config.items():
|
|
211
|
+
if k == 'ReplicaCount':
|
|
212
|
+
try:
|
|
213
|
+
processed_item[k] = int(v)
|
|
214
|
+
except (ValueError, TypeError):
|
|
215
|
+
raise ValueError(f'ReplicaCount must be an integer: {v}')
|
|
216
|
+
elif k in ['ReplicaAvailabilityZones', 'ReplicaOutpostArns']:
|
|
217
|
+
if isinstance(v, str):
|
|
218
|
+
processed_item[k] = v.split(',')
|
|
219
|
+
elif isinstance(v, list):
|
|
220
|
+
processed_item[k] = v
|
|
221
|
+
else:
|
|
222
|
+
raise ValueError(f'{k} must be a string or list of strings')
|
|
223
|
+
else:
|
|
224
|
+
processed_item[k] = v
|
|
225
|
+
processed_config.append(processed_item)
|
|
226
|
+
|
|
227
|
+
return processed_config
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Start migration tool for ElastiCache MCP server."""
|
|
16
|
+
|
|
17
|
+
from ...common.connection import ElastiCacheConnectionManager
|
|
18
|
+
from ...common.decorators import handle_exceptions
|
|
19
|
+
from ...common.server import mcp
|
|
20
|
+
from ...context import Context
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
22
|
+
from typing import Any, Dict, List, Union
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CustomerNodeEndpoint(BaseModel):
|
|
26
|
+
"""Customer node endpoint model."""
|
|
27
|
+
|
|
28
|
+
Address: str = Field(..., description='The address of the node endpoint')
|
|
29
|
+
Port: int = Field(..., description='The port of the node endpoint')
|
|
30
|
+
model_config = ConfigDict(validate_by_name=True, arbitrary_types_allowed=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StartMigrationRequest(BaseModel):
|
|
34
|
+
"""Request model for starting migration to an ElastiCache replication group."""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(validate_by_name=True, arbitrary_types_allowed=True)
|
|
37
|
+
|
|
38
|
+
replication_group_id: str = Field(
|
|
39
|
+
..., description='The ID of the replication group to which data should be migrated'
|
|
40
|
+
)
|
|
41
|
+
customer_node_endpoint_list: Union[str, List[CustomerNodeEndpoint]] = Field(
|
|
42
|
+
...,
|
|
43
|
+
description='List of endpoints from which data should be migrated. For Valkey or Redis OSS (cluster mode disabled), the list should have only one element.',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def prepare_request_dict(request: StartMigrationRequest) -> Dict[str, Any]:
|
|
48
|
+
"""Prepare the request dictionary for the AWS API.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
request: The StartMigrationRequest object
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dict containing the properly formatted request parameters
|
|
55
|
+
"""
|
|
56
|
+
# Start with required parameters
|
|
57
|
+
start_migration_request: Dict[str, Any] = {
|
|
58
|
+
'ReplicationGroupId': request.replication_group_id,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Process customer node endpoint list
|
|
62
|
+
if isinstance(request.customer_node_endpoint_list, str):
|
|
63
|
+
# Parse shorthand syntax: Address=string,Port=integer
|
|
64
|
+
try:
|
|
65
|
+
pairs = [
|
|
66
|
+
p.strip() for p in request.customer_node_endpoint_list.split(',') if p.strip()
|
|
67
|
+
]
|
|
68
|
+
endpoint = {}
|
|
69
|
+
for pair in pairs:
|
|
70
|
+
if '=' not in pair:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
'Invalid endpoint format. Each parameter must be in key=value format'
|
|
73
|
+
)
|
|
74
|
+
key, value = pair.split('=', 1)
|
|
75
|
+
key = key.strip()
|
|
76
|
+
value = value.strip()
|
|
77
|
+
if not key or not value:
|
|
78
|
+
raise ValueError('Key or value cannot be empty')
|
|
79
|
+
|
|
80
|
+
if key == 'Address':
|
|
81
|
+
endpoint['Address'] = value
|
|
82
|
+
elif key == 'Port':
|
|
83
|
+
try:
|
|
84
|
+
endpoint['Port'] = int(value)
|
|
85
|
+
except ValueError:
|
|
86
|
+
raise ValueError(f'Port must be an integer: {value}')
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError(f'Invalid parameter: {key}')
|
|
89
|
+
|
|
90
|
+
# Validate required fields
|
|
91
|
+
if 'Address' not in endpoint:
|
|
92
|
+
raise ValueError('Missing required field: Address')
|
|
93
|
+
if 'Port' not in endpoint:
|
|
94
|
+
raise ValueError('Missing required field: Port')
|
|
95
|
+
|
|
96
|
+
start_migration_request['CustomerNodeEndpointList'] = [endpoint]
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f'Invalid endpoint shorthand syntax. Expected format: Address=string,Port=integer. Error: {str(e)}'
|
|
100
|
+
)
|
|
101
|
+
elif isinstance(request.customer_node_endpoint_list, list):
|
|
102
|
+
# Handle list format
|
|
103
|
+
if len(request.customer_node_endpoint_list) < 1:
|
|
104
|
+
raise ValueError('CustomerNodeEndpointList should have at least one element')
|
|
105
|
+
|
|
106
|
+
endpoints = [
|
|
107
|
+
endpoint.model_dump(exclude_none=True)
|
|
108
|
+
for endpoint in request.customer_node_endpoint_list
|
|
109
|
+
]
|
|
110
|
+
start_migration_request['CustomerNodeEndpointList'] = endpoints
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
'CustomerNodeEndpointList must be a string or a list with at least one element'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return start_migration_request
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@mcp.tool(name='start-migration')
|
|
120
|
+
@handle_exceptions
|
|
121
|
+
async def start_migration(request: StartMigrationRequest) -> Dict:
|
|
122
|
+
"""Start migration to an Amazon ElastiCache replication group.
|
|
123
|
+
|
|
124
|
+
This tool starts migration from a Redis instance to an ElastiCache replication group.
|
|
125
|
+
It initiates the data migration process from the specified endpoint(s) to
|
|
126
|
+
the target replication group.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
request: The StartMigrationRequest object containing:
|
|
130
|
+
- replication_group_id: The ID of the replication group to which data should be migrated
|
|
131
|
+
- customer_node_endpoint_list: List of endpoints from which data should be migrated.
|
|
132
|
+
For Valkey or Redis OSS (cluster mode disabled), the list should have only one element.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Dict containing information about the migration start result.
|
|
136
|
+
"""
|
|
137
|
+
# Check if readonly mode is enabled
|
|
138
|
+
if Context.readonly_mode():
|
|
139
|
+
raise ValueError(
|
|
140
|
+
'You have configured this tool in readonly mode. To make this change you will have to update your configuration.'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Get ElastiCache client
|
|
144
|
+
elasticache_client = ElastiCacheConnectionManager.get_connection()
|
|
145
|
+
|
|
146
|
+
# Prepare request dictionary
|
|
147
|
+
start_request = prepare_request_dict(request)
|
|
148
|
+
|
|
149
|
+
# Start the migration
|
|
150
|
+
response = elasticache_client.start_migration(**start_request)
|
|
151
|
+
return response
|