|
1 | 1 | import boto3
|
2 | 2 | import time
|
| 3 | +from typing import Union, Tuple |
3 | 4 | from emd.utils.logger_utils import get_logger
|
4 | 5 | from .status import get_destroy_status
|
5 | 6 | from emd.constants import (
|
|
18 | 19 | from emd.models.utils.constants import ServiceType
|
19 | 20 | from emd.models import Model
|
20 | 21 | from emd.utils.aws_service_utils import get_current_region
|
| 22 | + |
21 | 23 | logger = get_logger(__name__)
|
22 | 24 |
|
23 | 25 |
|
| 26 | +def parse_model_identifier(model_identifier: str) -> Tuple[str, str]: |
| 27 | + """ |
| 28 | + Parse model identifier in format 'model_id/model_tag' |
| 29 | +
|
| 30 | + Args: |
| 31 | + model_identifier: String in format 'model_id/model_tag' or just 'model_id' |
| 32 | +
|
| 33 | + Returns: |
| 34 | + Tuple of (model_id, model_tag) |
| 35 | +
|
| 36 | + Raises: |
| 37 | + ValueError: If format is invalid |
| 38 | +
|
| 39 | + Examples: |
| 40 | + parse_model_identifier('Qwen2.5-0.5B-Instruct/d2') -> ('Qwen2.5-0.5B-Instruct', 'd2') |
| 41 | + parse_model_identifier('Qwen2.5-0.5B-Instruct') -> ('Qwen2.5-0.5B-Instruct', 'dev') |
| 42 | + """ |
| 43 | + if not model_identifier or not model_identifier.strip(): |
| 44 | + raise ValueError("Model identifier cannot be empty") |
| 45 | + |
| 46 | + model_identifier = model_identifier.strip() |
| 47 | + |
| 48 | + if '/' not in model_identifier: |
| 49 | + # Backward compatibility: treat as model_id with default tag |
| 50 | + return model_identifier, MODEL_DEFAULT_TAG |
| 51 | + |
| 52 | + parts = model_identifier.split('/') |
| 53 | + if len(parts) != 2: |
| 54 | + raise ValueError( |
| 55 | + f"Invalid format: '{model_identifier}'. " |
| 56 | + f"Expected format: 'model_id/model_tag' (e.g., 'Qwen2.5-0.5B-Instruct/d2')" |
| 57 | + ) |
| 58 | + |
| 59 | + model_id, model_tag = parts |
| 60 | + if not model_id.strip(): |
| 61 | + raise ValueError("Model ID cannot be empty") |
| 62 | + if not model_tag.strip(): |
| 63 | + raise ValueError("Model tag cannot be empty") |
| 64 | + |
| 65 | + return model_id.strip(), model_tag.strip() |
| 66 | + |
| 67 | + |
24 | 68 | def stop_pipeline_execution(
|
25 |
| - model_id:str, |
26 |
| - model_tag:str, |
27 |
| - pipeline_name=CODEPIPELINE_NAME, |
28 |
| - waiting_until_complete=True |
| 69 | + model_id: str, |
| 70 | + model_tag: str, |
| 71 | + pipeline_name: str = CODEPIPELINE_NAME, |
| 72 | + waiting_until_complete: bool = True |
29 | 73 | ):
|
| 74 | + """ |
| 75 | + Stop an active pipeline execution for a model. |
| 76 | +
|
| 77 | + Args: |
| 78 | + model_id: Model ID |
| 79 | + model_tag: Model tag |
| 80 | + pipeline_name: Name of the CodePipeline |
| 81 | + waiting_until_complete: Whether to wait for the stop to complete |
| 82 | + """ |
| 83 | + logger.info(f"Checking for active pipeline executions for model: {model_id}, tag: {model_tag}") |
| 84 | + |
30 | 85 | active_executuion_infos = get_pipeline_active_executions(
|
31 | 86 | pipeline_name=pipeline_name
|
32 | 87 | )
|
33 | 88 | active_executuion_infos_d = {
|
34 |
| - Model.get_model_stack_name_prefix(d['model_id'],d['model_tag']):d for d in active_executuion_infos |
| 89 | + Model.get_model_stack_name_prefix(d['model_id'], d['model_tag']): d |
| 90 | + for d in active_executuion_infos |
35 | 91 | }
|
36 |
| - cur_uuid = Model.get_model_stack_name_prefix(model_id,model_tag) |
| 92 | + |
| 93 | + cur_uuid = Model.get_model_stack_name_prefix(model_id, model_tag) |
| 94 | + logger.info(f"Looking for pipeline execution with key: {cur_uuid}") |
| 95 | + |
37 | 96 | if cur_uuid in active_executuion_infos_d:
|
38 |
| - pipeline_execution_id = active_executuion_infos_d[cur_uuid]['pipeline_execution_id'] |
| 97 | + execution_info = active_executuion_infos_d[cur_uuid] |
| 98 | + pipeline_execution_id = execution_info['pipeline_execution_id'] |
| 99 | + |
| 100 | + logger.info(f"Found active pipeline execution: {pipeline_execution_id}") |
| 101 | + logger.info(f"Current status: {execution_info.get('status', 'Unknown')}") |
| 102 | + |
39 | 103 | client = boto3.client('codepipeline', region_name=get_current_region())
|
40 | 104 | try:
|
41 | 105 | client.stop_pipeline_execution(
|
42 | 106 | pipelineName=pipeline_name,
|
43 | 107 | pipelineExecutionId=pipeline_execution_id
|
44 | 108 | )
|
| 109 | + logger.info(f"Stop request sent for pipeline execution: {pipeline_execution_id}") |
45 | 110 | except client.exceptions.DuplicatedStopRequestException as e:
|
46 |
| - logger.error(e) |
| 111 | + logger.warning(f"Stop request already sent for execution {pipeline_execution_id}: {e}") |
| 112 | + except Exception as e: |
| 113 | + logger.error(f"Failed to stop pipeline execution {pipeline_execution_id}: {e}") |
| 114 | + raise |
| 115 | + |
47 | 116 | if waiting_until_complete:
|
| 117 | + logger.info("Waiting for pipeline execution to stop...") |
48 | 118 | while True:
|
49 | 119 | execution_info = get_pipeline_execution_info(
|
50 | 120 | pipeline_name=pipeline_name,
|
51 | 121 | pipeline_execution_id=pipeline_execution_id,
|
52 | 122 | )
|
53 |
| - logger.info(f"pipeline execution status: {execution_info['status']}") |
54 |
| - if execution_info['status'] == 'Stopped': |
| 123 | + current_status = execution_info['status'] |
| 124 | + logger.info(f"Pipeline execution status: {current_status}") |
| 125 | + |
| 126 | + if current_status == 'Stopped': |
| 127 | + logger.info("Pipeline execution stopped successfully") |
| 128 | + break |
| 129 | + elif current_status in ['Succeeded', 'Failed', 'Cancelled']: |
| 130 | + logger.info(f"Pipeline execution completed with status: {current_status}") |
55 | 131 | break
|
| 132 | + |
56 | 133 | time.sleep(5)
|
57 | 134 | else:
|
58 |
| - logger.error(f"model: {model_id}, model_tag: {model_tag} not found in pipeline executions.") |
| 135 | + logger.warning(f"No active pipeline execution found for model: {model_id}, tag: {model_tag}") |
| 136 | + logger.info(f"Available active executions: {list(active_executuion_infos_d.keys())}") |
59 | 137 |
|
60 | 138 |
|
61 | 139 | def destroy_ecs(model_id,model_tag,stack_name):
|
62 | 140 | cf_client = boto3.client('cloudformation', region_name=get_current_region())
|
63 | 141 | cf_client.delete_stack(StackName=stack_name)
|
64 | 142 |
|
65 |
| -def destroy(model_id:str,model_tag=MODEL_DEFAULT_TAG,waiting_until_complete=True): |
| 143 | +def destroy( |
| 144 | + model_id: Union[str, None] = None, |
| 145 | + model_tag: str = MODEL_DEFAULT_TAG, |
| 146 | + model_identifier: Union[str, None] = None, |
| 147 | + waiting_until_complete: bool = True |
| 148 | +): |
| 149 | + """ |
| 150 | + Destroy a model deployment. |
| 151 | +
|
| 152 | + Args: |
| 153 | + model_id: Model ID (legacy format) |
| 154 | + model_tag: Model tag (legacy format) |
| 155 | + model_identifier: Model identifier in 'model_id/model_tag' format (new format) |
| 156 | + waiting_until_complete: Whether to wait for deletion to complete |
| 157 | +
|
| 158 | + Examples: |
| 159 | + # New format (recommended) |
| 160 | + destroy(model_identifier='Qwen2.5-0.5B-Instruct/d2') |
| 161 | +
|
| 162 | + # Legacy format (still supported) |
| 163 | + destroy(model_id='Qwen2.5-0.5B-Instruct', model_tag='d2') |
| 164 | +
|
| 165 | + Raises: |
| 166 | + ValueError: If neither format is provided or format is invalid |
| 167 | + """ |
66 | 168 | check_env_stack_exist_and_complete()
|
67 |
| - stack_name = Model.get_model_stack_name_prefix(model_id,model_tag=model_tag) |
| 169 | + |
| 170 | + # Handle different input formats |
| 171 | + if model_identifier is not None: |
| 172 | + if model_id is not None: |
| 173 | + raise ValueError("Cannot specify both model_identifier and model_id. Use either the new format (model_identifier='model_id/model_tag') or legacy format (model_id='model_id', model_tag='model_tag')") |
| 174 | + |
| 175 | + # Parse new format |
| 176 | + try: |
| 177 | + model_id, model_tag = parse_model_identifier(model_identifier) |
| 178 | + logger.info(f"Parsed model identifier '{model_identifier}' -> model_id='{model_id}', model_tag='{model_tag}'") |
| 179 | + except ValueError as e: |
| 180 | + logger.error(f"Invalid model identifier format: {e}") |
| 181 | + raise |
| 182 | + |
| 183 | + elif model_id is not None: |
| 184 | + # Legacy format |
| 185 | + logger.info(f"Using legacy format -> model_id='{model_id}', model_tag='{model_tag}'") |
| 186 | + else: |
| 187 | + raise ValueError("Must specify either model_identifier (new format) or model_id (legacy format)") |
| 188 | + |
| 189 | + stack_name = Model.get_model_stack_name_prefix(model_id, model_tag=model_tag) |
| 190 | + logger.info(f"Target stack name: {stack_name}") |
| 191 | + |
68 | 192 | if not check_stack_exists(stack_name):
|
69 |
| - stop_pipeline_execution(model_id,model_tag,waiting_until_complete=waiting_until_complete) |
| 193 | + logger.info(f"Stack {stack_name} does not exist, checking for active pipeline executions...") |
| 194 | + stop_pipeline_execution(model_id, model_tag, waiting_until_complete=waiting_until_complete) |
70 | 195 | return
|
71 | 196 |
|
72 | 197 | stack_info = get_stack_info(stack_name)
|
73 | 198 | parameters = stack_info['parameters']
|
74 | 199 | if parameters['ServiceType'] == ServiceType.ECS:
|
75 |
| - return destroy_ecs(model_id, model_tag,stack_name) |
| 200 | + logger.info(f"Destroying ECS service for stack: {stack_name}") |
| 201 | + return destroy_ecs(model_id, model_tag, stack_name) |
76 | 202 |
|
77 | 203 | cf_client = boto3.client('cloudformation', region_name=get_current_region())
|
78 | 204 | cf_client.delete_stack(StackName=stack_name)
|
79 | 205 |
|
80 |
| - logger.info(f"Delete stack initiated: {stack_name}") |
| 206 | + logger.info(f"CloudFormation stack deletion started: {stack_name}") |
| 207 | + logger.info("Deleting model infrastructure (compute instances, load balancers, security groups, etc.)") |
| 208 | + |
81 | 209 | # check delete status
|
82 | 210 | if waiting_until_complete:
|
| 211 | + logger.info("Waiting for stack deletion to complete...") |
83 | 212 | while True:
|
84 | 213 | status_info = get_destroy_status(stack_name)
|
85 | 214 | status = status_info['status']
|
86 | 215 | status_code = status_info['status_code']
|
87 | 216 | if status_code == 0:
|
88 | 217 | break
|
89 |
| - logger.info(f'stack delete status: {status}') |
| 218 | + logger.info(f'Stack deletion progress: {status}') |
90 | 219 | time.sleep(5)
|
| 220 | + |
91 | 221 | if status == EMD_STACK_NOT_EXISTS_STATUS:
|
92 | 222 | status = "DELETE_COMPLETED"
|
93 |
| - logger.info(f'stack delete status: {status}') |
| 223 | + logger.info("✅ Model deployment successfully deleted - all resources have been removed") |
| 224 | + else: |
| 225 | + logger.info(f'Stack deletion completed with status: {status}') |
0 commit comments