import os import json import time from datetime import datetime, timedelta, timezone from alibabacloud_ecs20140526.client import Client as EcsClient from alibabacloud_tea_openapi import models as open_api_models from alibabacloud_ecs20140526 import models as ecs_models from alibabacloud_credentials.client import Client as CredentialClient INSTANCE_STATUS_CHECK_INTERVAL_MILLISECOND = 3000 INSTANCE_STATUS_TOTAL_CHECK_TIME_ELAPSE_MILLISECOND = 60000 * 3 def init_ecs_client() -> EcsClient: """ 初始化 ECS 客户端 参数:无 返回:EcsClient —— 使用环境变量中的 AK/SK 与 region 初始化的客户端 用途:用于后续调用阿里云 ECS 接口 """ region_id = os.getenv("ALIYUN_REGION_ID", "cn-shanghai") credential = CredentialClient() config = open_api_models.Config( credential=credential, region_id=region_id, ) config.endpoint = f"ecs.{region_id}.aliyuncs.com" return EcsClient(config) def compute_auto_release_time(hours_default=6, minutes_default=0) -> str: """ 计算自动释放时间(UTC),确保至少晚于当前时间 30 分钟 参数:默认增加 6 小时;支持环境变量 AUTO_RELEASE_HOURS/AUTO_RELEASE_MINUTES 返回:str —— ISO8601 格式时间,如 2025-11-21T08:00:00Z 用途:满足 Aliyun 对 AutoReleaseTime 的格式与时间窗口要求 """ hours = int(os.getenv("AUTO_RELEASE_HOURS") or hours_default) minutes = int(os.getenv("AUTO_RELEASE_MINUTES") or minutes_default) delta = timedelta(hours=hours, minutes=minutes) if delta < timedelta(minutes=30): delta = timedelta(minutes=30) target = datetime.now(timezone.utc) + delta return target.strftime("%Y-%m-%dT%H:%M:%SZ") def compose_run_instances_request() -> ecs_models.RunInstancesRequest: """ 组装创建实例请求参数 参数:无(从环境变量读取可选覆盖项) 返回:RunInstancesRequest —— 包含计费、地域/可用区、规格、镜像、磁盘、网络、数量等参数 用途:用于调用 RunInstances 创建 ECS 实例 """ region_id = os.getenv("ALIYUN_REGION_ID", "cn-shanghai") return ecs_models.RunInstancesRequest( instance_charge_type=os.getenv("ALIYUN_CHARGE_TYPE", "PostPaid"), region_id=region_id, zone_id=os.getenv("ALIYUN_ZONE_ID", "cn-shanghai-b"), instance_type=os.getenv("ALIYUN_INSTANCE_TYPE", "ecs.t5-lc1m1.small"), io_optimized=os.getenv("ALIYUN_IO_OPTIMIZED", "optimized"), spot_strategy=os.getenv("ALIYUN_SPOT_STRATEGY", "SpotAsPriceGo"), spot_interruption_behavior=os.getenv("ALIYUN_SPOT_BEHAVIOR", "Terminate"), image_id=os.getenv("ALIYUN_IMAGE_ID", "ubuntu_24_04_x64_20G_alibase_20251102.vhd"), security_enhancement_strategy=os.getenv("ALIYUN_SECURITY_ENHANCE", "Active"), system_disk=ecs_models.RunInstancesRequestSystemDisk( size=int(os.getenv("ALIYUN_SYSTEM_DISK_SIZE", "40")), category=os.getenv("ALIYUN_SYSTEM_DISK_CATEGORY", "cloud_efficiency"), ), internet_charge_type=os.getenv("ALIYUN_INTERNET_CHARGE_TYPE", "PayByBandwidth"), internet_max_bandwidth_out=int(os.getenv("ALIYUN_MAX_BW_OUT", "1")), v_switch_id=os.getenv("ALIYUN_VSWITCH_ID"), security_group_id=os.getenv("ALIYUN_SECURITY_GROUP_ID"), image_options=ecs_models.RunInstancesRequestImageOptions(login_as_non_root=False), instance_name=os.getenv("ALIYUN_INSTANCE_NAME", "launch-advisor-20251121"), private_dns_name_options=ecs_models.RunInstancesRequestPrivateDnsNameOptions(hostname_type="Custom"), unique_suffix=False, http_tokens=os.getenv("ALIYUN_HTTP_TOKENS", "optional"), tenancy="default", affinity="default", amount=int(os.getenv("ALIYUN_AMOUNT", "20")), min_amount=int(os.getenv("ALIYUN_MIN_AMOUNT", "20")), auto_release_time=compute_auto_release_time(), ) def call_run_instances_api(ecs_client: EcsClient): """ 调用创建实例 API 参数:ecs_client 返回:RunInstancesResponse 或 None 用途:提交实例创建请求并获取实例 ID 列表 """ request = compose_run_instances_request() try: return ecs_client.run_instances(request) except Exception as error: print(getattr(error, "code", str(type(error)))) print(getattr(error, "message", str(error))) data = getattr(error, "data", None) if isinstance(data, dict) and data.get("Recommend") is not None: print(data.get("Recommend")) return None def call_to_describe_instances(ecs_client: EcsClient, instance_ids): """ 轮询检查实例状态直至成功或超时 参数:ecs_client、instance_ids 返回:None 用途:每 3 秒检查一次,累计超过设定时间则判定为超时 """ start_time = int(time.time() * 1000) pending = list(instance_ids) region_id = os.getenv("ALIYUN_REGION_ID", "cn-shanghai") while True: time.sleep(INSTANCE_STATUS_CHECK_INTERVAL_MILLISECOND / 1000.0) req = ecs_models.DescribeInstancesRequest(region_id=region_id, instance_ids=json.dumps(pending)) try: resp = ecs_client.describe_instances(req) except Exception as error: print(getattr(error, "message", str(error))) continue instances = resp.body.instances.instance if hasattr(resp, "body") else resp.instances.instance for inst in instances: if getattr(inst, "status", None) == "Running": iid = getattr(inst, "instance_id", None) or getattr(inst, "instanceId", None) if iid in pending: pending.remove(iid) print(f"Instance boot successfully: {iid}") if not pending: print("Instances all boot successfully.") return if int(time.time() * 1000) - start_time > INSTANCE_STATUS_TOTAL_CHECK_TIME_ELAPSE_MILLISECOND: print(f"Instances boot failed within {int(INSTANCE_STATUS_TOTAL_CHECK_TIME_ELAPSE_MILLISECOND/60000)} mins: {json.dumps(pending)}") return def list_all_instance_ids(ecs_client: EcsClient) -> list: """ 列出当前地域所有实例 ID 参数:ecs_client 返回:List[str] 用途:用于批量清理现有实例 """ region_id = os.getenv("ALIYUN_REGION_ID", "cn-shanghai") ids = [] page = 1 while True: req = ecs_models.DescribeInstancesRequest(region_id=region_id, page_size=100, page_number=page) try: resp = ecs_client.describe_instances(req) except Exception: break body = resp.body if hasattr(resp, "body") else resp items = body.instances.instance if hasattr(body, "instances") else [] if not items: break for it in items: iid = getattr(it, "instance_id", None) or getattr(it, "instanceId", None) if iid: ids.append(iid) page += 1 return ids def wait_instances_status(ecs_client: EcsClient, instance_ids, target_status, timeout_seconds=600) -> bool: """ 等待一组实例达到指定状态 参数:ecs_client、instance_ids、target_status、timeout_seconds 返回:bool 用途:用于重启后等待恢复 Running """ region_id = os.getenv("ALIYUN_REGION_ID", "cn-shanghai") deadline = time.time() + timeout_seconds pending = set(instance_ids) while time.time() < deadline and pending: time.sleep(3) req = ecs_models.DescribeInstancesRequest(region_id=region_id, instance_ids=json.dumps(list(pending))) try: resp = ecs_client.describe_instances(req) except Exception: continue body = resp.body if hasattr(resp, "body") else resp items = body.instances.instance if hasattr(body, "instances") else [] for it in items: st = getattr(it, "status", None) iid = getattr(it, "instance_id", None) or getattr(it, "instanceId", None) if st == target_status and iid in pending: pending.remove(iid) return not pending def install_cloud_assistant_and_reboot(ecs_client: EcsClient, instance_ids) -> bool: """ 为所有实例主动安装云助手并重启实例 参数:ecs_client、instance_ids 返回:bool 用途:确保云助手安装需要的重启操作已完成 """ region_id = os.getenv("ALIYUN_REGION_ID", "cn-shanghai") if not instance_ids: return False try: install_req = ecs_models.InstallCloudAssistantRequest(region_id=region_id, instance_id=instance_ids) ecs_client.install_cloud_assistant(install_req) except Exception as e: print(f"InstallCloudAssistant 调用失败:{e}") return False try: reboot_req = ecs_models.RebootInstancesRequest(region_id=region_id, instance_id=instance_ids) ecs_client.reboot_instances(reboot_req) except Exception as e: print(f"RebootInstances 调用失败:{e}") return False ok = wait_instances_status(ecs_client, instance_ids, target_status="Running", timeout_seconds=600) return ok def ensure_cloud_assistant_ready(ecs_client: EcsClient, instance_ids) -> bool: """ 等待并确保 Cloud Assistant 就绪 参数:ecs_client、instance_ids 返回:bool 用途:避免实例刚启动时命令执行失败 """ region_id = os.getenv("ALIYUN_REGION_ID", "cn-shanghai") deadline = time.time() + int(os.getenv("CLOUD_ASSISTANT_READY_TIMEOUT_SECONDS", "600")) while time.time() < deadline: try: req = ecs_models.DescribeCloudAssistantStatusRequest(region_id=region_id, instance_id=instance_ids) resp = ecs_client.describe_cloud_assistant_status(req) except Exception: time.sleep(5) continue body = resp.body if hasattr(resp, "body") else resp statuses = getattr(body, "instance_cloud_assistant_status", None) if statuses is None and hasattr(body, "cloud_assistant"): statuses = body.cloud_assistant.instance_cloud_assistant_status ready_count = 0 for s in statuses or []: st = getattr(s, "status", None) or getattr(s, "Status", None) is_ready = (st is True) or (isinstance(st, str) and st.lower() in ("true", "enabled", "running")) if is_ready: ready_count += 1 if ready_count == len(instance_ids): return True time.sleep(5) return False def clear_all_instances(ecs_client: EcsClient): """ 清空当前地域的所有 ECS 实例 参数:ecs_client 返回:None 用途:在批量创建前保持干净环境 """ ids = list_all_instance_ids(ecs_client) region_id = os.getenv("ALIYUN_REGION_ID", "cn-shanghai") if not ids: print("当前地域无实例,无需清理") return print(f"准备清理 {len(ids)} 台实例:{json.dumps(ids)}") try: stop_req = ecs_models.StopInstancesRequest(instance_id=ids, region_id=region_id) ecs_client.stop_instances(stop_req) except Exception as e: print(f"停止实例失败:{e}") wait_instances_status(ecs_client, ids, target_status="Stopped", timeout_seconds=600) try: del_req = ecs_models.DeleteInstancesRequest(instance_id=ids, force=True, region_id=region_id) ecs_client.delete_instances(del_req) except Exception: for iid in ids: try: one = ecs_models.DeleteInstanceRequest(instance_id=iid, force=True, region_id=region_id) ecs_client.delete_instance(one) except Exception as e: print(f"删除实例 {iid} 失败:{e}") time.sleep(5) left = list_all_instance_ids(ecs_client) if not left: print("实例已全部清理完毕") else: print(f"仍有实例未删除:{json.dumps(left)}") def main(): """ 脚本入口:清理旧实例 → 创建新实例 → 安装云助手并重启 → 确认就绪 参数:无 返回:None(打印创建的实例 ID JSON) 用途:为后续下发云助手命令准备环境 """ ecs_client = init_ecs_client() if os.getenv("NO_CLEAR") != "1": clear_all_instances(ecs_client) resp = call_run_instances_api(ecs_client) if resp is None: return body = resp.body if hasattr(resp, "body") else resp instance_ids = body.instance_id_sets.instance_id_set print(f"Success. Instance creation succeed. InstanceIds: {json.dumps(instance_ids)}") call_to_describe_instances(ecs_client, instance_ids) installed = install_cloud_assistant_and_reboot(ecs_client, instance_ids) if not installed: print("Cloud Assistant 安装或重启失败,终止") return ready = ensure_cloud_assistant_ready(ecs_client, instance_ids) if not ready: print("Cloud Assistant 未全部就绪,终止") return print(json.dumps(instance_ids)) if __name__ == "__main__": main()