Files
agent_management/k8s_manager.py
T
2026-01-06 09:26:11 +00:00

565 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Kubernetes管理模块 - 负责与Kubernetes集群交互
"""
from kubernetes import client, config
from kubernetes.client.rest import ApiException
from typing import Dict, List, Optional
import logging
import os
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class K8sManager:
"""Kubernetes资源管理器"""
def __init__(self, namespace: str = "ai-agents", kubeconfig_path: str = None):
"""
初始化Kubernetes管理器
Args:
namespace: AI Agent部署的命名空间
kubeconfig_path: kubeconfig文件路径(可选)
"""
self.namespace = namespace
self._load_kube_config(kubeconfig_path)
self.v1 = client.CoreV1Api()
self.apps_v1 = client.AppsV1Api()
# 确保命名空间存在
self._ensure_namespace()
def _load_kube_config(self, kubeconfig_path: str = None):
"""
加载Kubernetes配置
优先级:
1. 集群内ServiceAccount(推荐用于生产环境)
2. 指定的kubeconfig文件路径
3. 默认的kubeconfig路径 (~/.kube/config)
"""
try:
# 方式1: 尝试加载集群内配置(当服务运行在K8s中且有ServiceAccount时)
config.load_incluster_config()
logger.info("✅ 使用集群内ServiceAccount配置")
except config.ConfigException:
try:
if kubeconfig_path and os.path.exists(kubeconfig_path):
# 方式2: 使用指定的kubeconfig文件
config.load_kube_config(config_file=kubeconfig_path)
logger.info(f"✅ 使用指定的kubeconfig: {kubeconfig_path}")
else:
# 方式3: 使用默认kubeconfig(开发环境)
config.load_kube_config()
logger.info("✅ 使用默认kubeconfig (~/.kube/config)")
except Exception as e:
logger.error(f"❌ 无法加载Kubernetes配置: {e}")
raise Exception(f"Kubernetes配置加载失败: {e}")
def _ensure_namespace(self):
"""确保AI Agent命名空间存在"""
try:
self.v1.read_namespace(name=self.namespace)
logger.info(f"命名空间 {self.namespace} 已存在")
except ApiException as e:
if e.status == 404:
# 创建命名空间
namespace_manifest = client.V1Namespace(
metadata=client.V1ObjectMeta(name=self.namespace)
)
self.v1.create_namespace(body=namespace_manifest)
logger.info(f"创建命名空间 {self.namespace}")
else:
raise
# 模板端口映射
TEMPLATE_PORTS = {
"jina_search_agent": 8080,
}
# 模板所需环境变量说明
TEMPLATE_ENV_INFO = {
"jina_search_agent": {
"required": {
"JINA_API_KEY": "Jina API密钥,从 https://jina.ai/ 获取"
},
"optional": {
"SERVICE_PORT": "HTTP服务端口,默认8080",
"SERVICE_HOST": "HTTP服务监听地址,默认0.0.0.0"
}
},
"mysql_agent": {
"required": {
"MYSQL_HOST": "MySQL数据库主机地址",
"MYSQL_USER": "MySQL用户名",
"MYSQL_PASSWORD": "MySQL密码",
"MYSQL_DATABASE": "MySQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"MYSQL_PORT": "MySQL端口,默认3306"
}
},
"postgresql_agent": {
"required": {
"POSTGRES_HOST": "PostgreSQL数据库主机地址",
"POSTGRES_USER": "PostgreSQL用户名",
"POSTGRES_PASSWORD": "PostgreSQL密码",
"POSTGRES_DATABASE": "PostgreSQL数据库名",
"OPENAI_API_KEY": "OpenAI API密钥"
},
"optional": {
"POSTGRES_PORT": "PostgreSQL端口,默认5432"
}
}
}
def get_template_info(self, template: str) -> Dict:
"""
获取模板信息
Args:
template: 模板类型
Returns:
模板信息(端口、所需环境变量等)
"""
return {
"template": template,
"port": self.TEMPLATE_PORTS.get(template),
"env_info": self.TEMPLATE_ENV_INFO.get(template, {})
}
def create_pod(self, pod_name: str, template: str, config_data: Dict) -> Dict:
"""
创建Pod
Args:
pod_name: Pod名称
template: 模板类型(echo_agent, chat_agent等)
config_data: 配置信息(replicas, resources等)
Returns:
创建的Pod信息,包含访问地址
"""
try:
# 生成Pod规格
pod_manifest = self._generate_pod_manifest(pod_name, template, config_data)
# 创建Pod
response = self.v1.create_namespaced_pod(
namespace=self.namespace,
body=pod_manifest
)
logger.info(f"Pod {pod_name} 创建成功")
# 获取服务端口
service_port = self.TEMPLATE_PORTS.get(template)
result = {
"name": response.metadata.name,
"namespace": response.metadata.namespace,
"status": response.status.phase,
"created_at": response.metadata.creation_timestamp.isoformat() if response.metadata.creation_timestamp else None,
"template": template
}
# 如果是HTTP服务类型的agent,添加访问信息
if service_port:
result["service_port"] = service_port
result["access_info"] = {
"note": "Pod IP将在Pod运行后可用,请通过 /agents/{name}/status 获取",
"port": service_port,
"endpoints": {
"root": f"http://<pod_ip>:{service_port}/",
"health": f"http://<pod_ip>:{service_port}/health"
}
}
return result
except ApiException as e:
logger.error(f"创建Pod失败: {e}")
raise Exception(f"创建Pod失败: {e.reason}")
def _generate_pod_manifest(self, pod_name: str, template: str, config_data: Dict) -> client.V1Pod:
"""生成Pod配置清单"""
# 默认资源配置
replicas = config_data.get("replicas", 1)
cpu_request = config_data.get("cpu_request", "100m")
cpu_limit = config_data.get("cpu_limit", "500m")
memory_request = config_data.get("memory_request", "128Mi")
memory_limit = config_data.get("memory_limit", "512Mi")
# 根据模板类型选择镜像
image_map = {
"echo_agent": "agnettaiji.azurecr.io/ai-agents/echo-agent:latest",
"chat_agent": "agnettaiji.azurecr.io/ai-agents/chat-agent:latest",
"code_agent": "agnettaiji.azurecr.io/ai-agents/code-agent:latest",
"search_agent": "agnettaiji.azurecr.io/ai-agents/search-agent:latest",
"mysql_agent": "agnettaiji.azurecr.io/ai-agents/mysql-agent:latest",
"postgresql_agent": "agnettaiji.azurecr.io/ai-agents/postgresql-agent:latest",
"jina_search_agent": "agnettaiji.azurecr.io/ai-agents/jina-search-agent:latest",
}
image = image_map.get(template, image_map["echo_agent"])
# 构建环境变量列表
env_vars = [
client.V1EnvVar(name="POD_NAME", value=pod_name),
client.V1EnvVar(name="TEMPLATE_TYPE", value=template)
]
# 添加用户自定义环境变量
custom_env = config_data.get("env", {})
for key, value in custom_env.items():
env_vars.append(client.V1EnvVar(name=key, value=str(value)))
logger.info(f"Pod {pod_name} 环境变量数量: {len(env_vars)}")
# 设置容器端口(如果是HTTP服务类型的agent)
container_ports = None
if template in ["jina_search_agent"]:
container_ports = [client.V1ContainerPort(container_port=8080)]
# 创建Pod规格
container = client.V1Container(
name=pod_name,
image=image,
resources=client.V1ResourceRequirements(
requests={"cpu": cpu_request, "memory": memory_request},
limits={"cpu": cpu_limit, "memory": memory_limit}
),
env=env_vars,
ports=container_ports
)
pod_spec = client.V1PodSpec(
containers=[container],
restart_policy="Always",
image_pull_secrets=[client.V1LocalObjectReference(name="acr-secret")]
)
# 构建标签(合并默认标签和用户自定义标签)
labels = {
"app": "ai-agent",
"template": template,
"managed-by": "agent-manager"
}
# 添加用户自定义标签
if "labels" in config_data:
labels.update(config_data["labels"])
pod_manifest = client.V1Pod(
api_version="v1",
kind="Pod",
metadata=client.V1ObjectMeta(
name=pod_name,
labels=labels
),
spec=pod_spec
)
return pod_manifest
def delete_pod(self, pod_name: str) -> Dict:
"""
删除Pod
Args:
pod_name: Pod名称
Returns:
删除结果
"""
try:
self.v1.delete_namespaced_pod(
name=pod_name,
namespace=self.namespace,
body=client.V1DeleteOptions()
)
logger.info(f"Pod {pod_name} 删除成功")
return {"status": "success", "message": f"Pod {pod_name} 已删除"}
except ApiException as e:
if e.status == 404:
return {"status": "not_found", "message": f"Pod {pod_name} 不存在"}
logger.error(f"删除Pod失败: {e}")
raise Exception(f"删除Pod失败: {e.reason}")
def get_pod_status(self, pod_name: str) -> Dict:
"""
获取Pod状态
Args:
pod_name: Pod名称
Returns:
Pod状态信息,包含访问URL和资源使用情况
"""
try:
pod = self.v1.read_namespaced_pod(
name=pod_name,
namespace=self.namespace
)
template = pod.metadata.labels.get("template", "unknown")
pod_ip = pod.status.pod_ip
service_port = self.TEMPLATE_PORTS.get(template)
# 获取资源配额信息
container = pod.spec.containers[0]
resources = container.resources
resource_requests = {
"cpu": resources.requests.get("cpu") if resources.requests else None,
"memory": resources.requests.get("memory") if resources.requests else None
}
resource_limits = {
"cpu": resources.limits.get("cpu") if resources.limits else None,
"memory": resources.limits.get("memory") if resources.limits else None
}
# 尝试获取实际资源使用情况(需要metrics-server)
resource_usage = self._get_pod_resource_usage(pod_name)
# 获取容器实际状态 - 检查是否崩溃或异常
container_statuses = pod.status.container_statuses or []
actual_status = pod.status.phase # 默认使用Pod阶段
health_status = "healthy"
container_info = []
for container_status in container_statuses:
container_state = {}
restart_count = container_status.restart_count
# 检查容器状态
if container_status.state.running:
container_state = {
"state": "running",
"started_at": container_status.state.running.started_at.isoformat() if container_status.state.running.started_at else None
}
elif container_status.state.waiting:
container_state = {
"state": "waiting",
"reason": container_status.state.waiting.reason,
"message": container_status.state.waiting.message
}
# 容器在等待状态,标记为不健康
health_status = "unhealthy"
actual_status = "Waiting"
elif container_status.state.terminated:
container_state = {
"state": "terminated",
"reason": container_status.state.terminated.reason,
"exit_code": container_status.state.terminated.exit_code,
"message": container_status.state.terminated.message,
"finished_at": container_status.state.terminated.finished_at.isoformat() if container_status.state.terminated.finished_at else None
}
# 容器已终止,标记为不健康
health_status = "unhealthy"
actual_status = "Terminated"
# 检查容器是否就绪
if not container_status.ready:
health_status = "unhealthy"
# 如果重启次数过多,也标记为不健康
if restart_count > 5:
health_status = "degraded"
container_info.append({
"name": container_status.name,
"ready": container_status.ready,
"restart_count": restart_count,
**container_state
})
result = {
"name": pod.metadata.name,
"namespace": pod.metadata.namespace,
"status": actual_status,
"health_status": health_status, # 新增:真实健康状态
"template": template,
"created_at": pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
"node": pod.spec.node_name,
"pod_ip": pod_ip,
"containers": container_info, # 新增:容器详细信息
"resources": {
"requests": resource_requests,
"limits": resource_limits,
"usage": resource_usage
},
"conditions": [
{
"type": condition.type,
"status": condition.status,
"reason": condition.reason
}
for condition in (pod.status.conditions or [])
]
}
# 如果是HTTP服务类型的agent且Pod已有IP,添加访问URL
if service_port and pod_ip:
result["service_port"] = service_port
result["access_url"] = f"http://{pod_ip}:{service_port}"
result["endpoints"] = {
"root": f"http://{pod_ip}:{service_port}/",
"health": f"http://{pod_ip}:{service_port}/health"
}
return result
except ApiException as e:
if e.status == 404:
return {"status": "not_found", "message": f"Pod {pod_name} 不存在"}
logger.error(f"获取Pod状态失败: {e}")
raise Exception(f"获取Pod状态失败: {e.reason}")
def _get_pod_resource_usage(self, pod_name: str) -> Dict:
"""
获取Pod实际资源使用情况(需要metrics-server)
Args:
pod_name: Pod名称
Returns:
资源使用信息(cpu、memory)
"""
try:
# 使用CustomObjectsApi调用metrics API
custom_api = client.CustomObjectsApi()
metrics = custom_api.get_namespaced_custom_object(
group="metrics.k8s.io",
version="v1beta1",
namespace=self.namespace,
plural="pods",
name=pod_name
)
# 解析容器资源使用情况
containers = metrics.get("containers", [])
if containers:
container = containers[0]
usage = container.get("usage", {})
return {
"cpu": usage.get("cpu"),
"memory": usage.get("memory"),
"available": True
}
return {"cpu": None, "memory": None, "available": False, "reason": "无容器数据"}
except ApiException as e:
if e.status == 404:
return {"cpu": None, "memory": None, "available": False, "reason": "metrics-server未安装或Pod不存在"}
logger.warning(f"获取Pod资源使用情况失败: {e.reason}")
return {"cpu": None, "memory": None, "available": False, "reason": f"获取失败: {e.reason}"}
except Exception as e:
logger.warning(f"获取Pod资源使用情况异常: {str(e)}")
return {"cpu": None, "memory": None, "available": False, "reason": f"异常: {str(e)}"}
def get_pod_metrics(self, pod_name: str) -> Dict:
"""
获取Pod资源使用情况(CPU、内存)
Args:
pod_name: Pod名称
Returns:
Pod实时资源使用信息和配额信息
"""
try:
# 获取 Pod 配额信息
pod = self.v1.read_namespaced_pod(
name=pod_name,
namespace=self.namespace
)
container = pod.spec.containers[0]
resources = container.resources
result = {
"name": pod_name,
"namespace": self.namespace,
"requests": {
"cpu": resources.requests.get("cpu") if resources.requests else None,
"memory": resources.requests.get("memory") if resources.requests else None
},
"limits": {
"cpu": resources.limits.get("cpu") if resources.limits else None,
"memory": resources.limits.get("memory") if resources.limits else None
}
}
# 尝试获取实时使用情况(需要 metrics-server)
try:
from kubernetes.client import CustomObjectsApi
custom_api = CustomObjectsApi()
# 调用 metrics.k8s.io API
metrics = custom_api.get_namespaced_custom_object(
group="metrics.k8s.io",
version="v1beta1",
namespace=self.namespace,
plural="pods",
name=pod_name
)
# 提取实时使用数据
if metrics and "containers" in metrics:
container_metrics = metrics["containers"][0]
usage = container_metrics.get("usage", {})
result["usage"] = {
"cpu": usage.get("cpu"),
"memory": usage.get("memory")
}
result["timestamp"] = metrics.get("timestamp")
logger.info(f"✅ 获取到实时资源使用: CPU={usage.get('cpu')}, Memory={usage.get('memory')}")
else:
result["usage"] = None
logger.warning(f"⚠️ Metrics 数据格式异常")
except Exception as metrics_error:
logger.warning(f"⚠️ 无法获取实时资源使用(可能未安装 metrics-server): {str(metrics_error)}")
result["usage"] = None
result["metrics_available"] = False
return result
except ApiException as e:
logger.error(f"获取Pod资源信息失败: {e}")
raise Exception(f"获取Pod资源信息失败: {e.reason}")
def list_pods(self, label_selector: Optional[str] = None) -> List[Dict]:
"""
列出所有Pod
Args:
label_selector: 标签选择器(可选)
Returns:
Pod列表
"""
try:
if label_selector is None:
label_selector = "managed-by=agent-manager"
pods = self.v1.list_namespaced_pod(
namespace=self.namespace,
label_selector=label_selector
)
return [
{
"name": pod.metadata.name,
"status": pod.status.phase,
"template": pod.metadata.labels.get("template", "unknown"),
"created_at": pod.metadata.creation_timestamp.isoformat() if pod.metadata.creation_timestamp else None,
"pod_ip": pod.status.pod_ip
}
for pod in pods.items
]
except ApiException as e:
logger.error(f"列出Pod失败: {e}")
raise Exception(f"列出Pod失败: {e.reason}")