import collections import dataclasses import json import logging from analyst.libs import settings from analyst.libs import util from analyst.libs.consts import * from analyst.libs.types import ValueImmutableDict from analyst.libs.util import get_infra_cr_namespace_prefix logger = logging.getLogger('analysis_resource_leak') def build_network_key(project_id: str, network_name: str) -> str: return f'{project_id}/{network_name}' def build_shoot_key(env: str, shoot_id: str) -> str: return f'{env}/{shoot_id}' def build_pod_key(k8s_cluster: str, ns: str, pod: str) -> str: return f'{k8s_cluster}/{ns}/{pod}' def build_node_key(k8s_cluster: str, node: str) -> str: return f'{k8s_cluster}/{node}' def format_datetime(timestamp: int) -> str: return datetime.datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") @dataclasses.dataclass class Shoot: env: str # 'dev provider: str # 'aws' region: str # 'asia-northeast1 network_namespace: str # 'nightly-ms-43562' network_name: str # 'aws-us-west-2 name: str # 'a35a8b14' tenant: str # '123 project: str # '123 @dataclasses.dataclass class Network: # template # key values: name: str # 'eks-us-east-1' namespace: str # 'nightly-ms-7777' provider: str # 'eks' region: str # 'us-east-1' vpc: str # 'vpc-xxxxxx' tenant_id: str # '12345' project_id: str # '7777' or 'nightly-ms-7777'(gardener) status: str # 'Hibernated' shoot_name: str = '' # 'xxxx'(gardener only) dataplane: str = '' # "nightly-ms-7777-eks-us-east-1-xxxx" (cloud-native only) env: str = '' # new for 3-layer network CR status vpc_cr_status: str = '' internet_cr_status: str = '' appbox_cr_status: str = '' approute_cr_status: dict = dataclasses.field(default_factory=dict) @dataclasses.dataclass class Pod: pod: str # 'o11y-vmagent-b864f5bb4-dfg8k', 'db-tidb-0' namespace: str # 'monitoring', 'tidb1234' cluster: str # 'prod-1222222222-gke-us-xxxxx' # the k8s cluster name # "aws:///ap-northeast-1d/i-012845f2a2660d2f4", # "gce://gcp-qa-system-testing-dev/us-west1-a/gke-staging-13698475-tidbcloud-stagin-ee308fcf-nbcs" node_instance_id: str node: str @dataclasses.dataclass class Metadata: clusters: list[dict] projects: list[dict] networks: list[Network] shoots: list[Shoot] pods: list[Pod] def __post_init__(self): self.id2cluster = {each['name']: each for each in self.clusters} self.id2project = {each['id']: each for each in self.projects} self.key2network = ValueImmutableDict({}) self.namespaced_name2network = ValueImmutableDict({}) self.vpc2network: dict[str:Network] = ValueImmutableDict({}) self.approute2network: dict[str:Network] = ValueImmutableDict({}) self.k8s2network = ValueImmutableDict({}) # k8s name to network self.shoot_key2network = ValueImmutableDict({}) for each in self.networks: project_id = each.project_id or 'controlplane' self.key2network[build_network_key(project_id, each.name)] = each self.namespaced_name2network[f'{each.namespace}/{each.name}'] = each if each.vpc: # some gardner networks share the same VPC with the migrated cloud-native network old = self.vpc2network.get(each.vpc) if old and old.project_id == each.project_id and old.tenant_id == each.tenant_id: continue self.vpc2network[each.vpc] = each if each.dataplane: self.k8s2network[each.dataplane] = each if each.shoot_name: self.shoot_key2network[build_shoot_key(each.env, each.shoot_name)] = each # new approute_cr_status if each.approute_cr_status: for approute in each.approute_cr_status.keys(): self.approute2network[approute] = each self.key2shoot: dict[str, Shoot] = ValueImmutableDict({}) for each in self.shoots: env = each.env name = each.name self.key2shoot[build_shoot_key(env, name)] = each self._key2pod: dict[str, Pod] = {} self._node_key2pods: dict[str, list[Pod]] = collections.defaultdict(list) for each in self.pods: self._key2pod[build_pod_key(each.cluster, each.namespace, each.pod)] = each self._node_key2pods[build_node_key(each.cluster, each.node)].append(each) def get_cluster(self, id: str): return self.id2cluster.get(id) def get_project(self, id: str): return self.id2project.get(id) def get_network(self, project_id: str, network_name: str) -> Network: return self.key2network.get(build_network_key(project_id, network_name)) def get_network_by_vpc_name(self, vpc_name: str) -> Network | None: return self.vpc2network.get(vpc_name) def get_network_by_approute_name(self, approute_name: str) -> Network | None: return self.approute2network.get(approute_name) def get_network_by_k8s_name(self, k8s_name: str) -> Network | None: return self.k8s2network.get(k8s_name) def get_network_by_shoot_name(self, env: str, shoot_name: str) -> Network | None: return self.shoot_key2network.get(build_shoot_key(env, shoot_name)) def get_network_by_ns_and_name(self, namespace: str, name: str) -> Network | None: return self.namespaced_name2network.get(f'{namespace}/{name}') def get_shoot(self, env: str, shoot_id: str) -> Shoot: return self.key2shoot.get(build_shoot_key(env, shoot_id)) def get_pods_by_node(self, k8s_name: str, node_name: str) -> list[Pod]: if not k8s_name or not node_name: return [] return self._node_key2pods.get(build_node_key(k8s_name, node_name)) def approute_alive(self, id: str) -> (bool, str): if util.is_empty(id, extra_empty_values={'unknown', }): raise ValueError('cluster_id is empty, cannot check approute alive') network = self.get_network_by_approute_name(id) if network is None: return False, f'approute({id}) is not found in network' if network.vpc_cr_status != CRSatatus.Active: return False, f"vpc CR phase is {network.vpc_cr_status}" if network.internet_cr_status != CRSatatus.Active: return False, f"Internet CR phase is {network.internet_cr_status}" if network.appbox_cr_status != CRSatatus.Active: return False, f"Appbox CR phase is {network.appbox_cr_status}" approute_status = network.approute_cr_status.get(id, '') if approute_status == '': return False, f"Approute CR not found" else: return True, f'Approute CR phase: {approute_status}' def cluster_alive(self, id: str) -> (bool, str): if util.is_empty(id, extra_empty_values={'unknown', }): raise ValueError('cluster_id is empty') cluster = self.get_cluster(id) if cluster is None: return False, f'cluster({id}) is not found in api-server' # if cluster['status'] in {'paused'}: # return False if cluster['status'].lower() in {'deleting', }: return True, f"cluster({id}) status is {cluster['status']}" return True, f'cluster CR phase: {cluster["status"]}' def cluster_suspend(self, id: str) -> (bool, str): cluster = self.get_cluster(id) if cluster is None: return False, f'cluster({id}) is not found in api-server' if cluster['status'].lower() in {'suspend', }: return True, f"cluster({id}) is suspend" return False, f"cluster({id}) status is {cluster['status']}" def network_alive(self, project_id: str, network_name: str, provider: str,product: str) -> (bool, str): if util.is_empty(project_id, extra_empty_values={'unknown', }): raise ValueError('project_id is empty') if util.is_empty(network_name, extra_empty_values={'unknown', }): raise ValueError('network_name is empty') reasons = [] # todo(chenfei): to check cloud-native network if the network is gardener-based network_alive, reason = self._network_alive(project_id, network_name, provider, product) reasons.append(reason) if self.is_gardener(network_name): cloud_native_network_name = network_name.replace('aws', 'eks').replace('gcp', 'gke') cloud_native_network_alive, cloud_native_reason = self._network_alive(project_id, cloud_native_network_name, provider, product) network_alive = any([network_alive, cloud_native_network_alive]) reasons.append(cloud_native_reason) elif self.is_cloud_native(network_name): gardener_network_name = network_name.replace('eks', 'aws').replace('gke', 'gcp') gardener_network_alive, gardener_reason = self._network_alive(project_id, gardener_network_name, provider, product) network_alive = any([network_alive, gardener_network_alive]) reasons.append(gardener_reason) project_alive = True # project_alive, project_alive_reason = self._project_alive(project_id) # reasons.append(project_alive_reason) reasons_str = f'{", ".join(reasons)}' if project_alive and network_alive: return True, reasons_str else: return False, reasons_str # add judgement for spetific product def _network_alive(self, project_id: str, network_name: str, provider: str,product: str) -> (bool, str): network = self.get_network(project_id, network_name) if not network: # TODO(chenfei): network was permanently deleted from meta return False, f'network({project_id}/{network_name}) is not found in api-server' else: if network.status == 'Hibernated': return False, f'network({project_id}/{network_name}) was "hibernated"' # New check for specific product status if provider == Provider.AWS: return self.check_aws_product_cr_status(network, product) elif provider == Provider.GCP: return self.check_gcp_product_cr_status(network, product) else: return False, f"unknwonw provider: {provider}" def _project_alive(self, project_id: str) -> (bool, str): project = self.get_project(project_id) if project is None: logger.warning(f'project({project_id}) is not found in meta') return True, f'project{project_id} is not found in meta' else: if project['deleted_time']: return False, f'project was deleted at {format_datetime(project["deleted_time"] / 1000)}' return True, '' @staticmethod def is_gardener(network_name: str) -> bool: return 'aws' in network_name or 'gcp' in network_name @staticmethod def is_cloud_native(network_name: str) -> bool: return ('eks' in network_name) or ('gke' in network_name) @staticmethod def check_aws_product_cr_status(network: Network,product: str): if product in [AWSProduct.VPC, AWSProduct.SG]: return True, f"vpc CR phase is {network.vpc_cr_status}" elif product in [AWSProduct.NAT]: if network.vpc_cr_status == CRSatatus.Active: return True, f"Internet CR phase is {network.internet_cr_status}" else: return False, f"vpc CR is not Active, phase:{network.vpc_cr_status}" else: if network.vpc_cr_status != CRSatatus.Active: return False, f"vpc CR is not Active, phase:{network.vpc_cr_status}" if network.internet_cr_status != CRSatatus.Active: return False, f"Internet CR is not Active, phase:{network.internet_cr_status}" return True, f"Appbox CR phase is {network.appbox_cr_status}" @staticmethod def check_gcp_product_cr_status(network: Network, product: str): if product in [GCPProduct.VPC]: return True, f"vpc CR phase is {network.vpc_cr_status}" elif product in [GCPProduct.NAT]: if network.vpc_cr_status == CRSatatus.Active: return True, f"Internet CR phase is {network.internet_cr_status}" else: return False, f"vpc CR is not Active, phase:{network.vpc_cr_status}" else: if network.vpc_cr_status != CRSatatus.Active: return False, f"vpc CR is not Active, phase:{network.vpc_cr_status}" if network.internet_cr_status != CRSatatus.Active: return False, f"Internet CR not Active, phase:{network.internet_cr_status}" return True, f"Appbox CR phase is {network.appbox_cr_status}" def load_clusters() -> list[dict]: # nightly-ms-3198830 PROJECT_ID_RE = re.compile('.*-(\d+)$') # template # key values: # exported_namespace: 'nightly-ms-7777' # provider: 'eks', 'gke', 'aws', 'gcp' # region: 'us-east-1' # name: '10169523820493954521' # version: 'v7.5.2' # project_id: '7777' or 'nightly-ms-7777'(gardener) # status: 'Running' cluster_data = json.load(open(settings.INFRA_EXPORTER_CLUSTER_SNAPSHOT_FILE)) #clusters = [each['metric'] for each in cluster_data['data']['result']] clusters = [ each['metric'] for each in cluster_data['data']['result'] if each['metric'].get('cluster', '') != "base/eks/us-west-2" ] # gardener_cluster_data = json.load(open(settings.INFRA_API_RESOURCES_CLUSTER_SNAPSHOT_FILE)) # gardener_cluster_metrics = [each['metric'] for each in gardener_cluster_data['data']['result']] # for each in gardener_cluster_metrics: # mr = PROJECT_ID_RE.match(each.get('project_id', '')) # if mr: # each['project_id'] = mr.groups()[0] # # network_name = each['network_name'] # 'gcp-asia-northeast1' # provider, region = network_name.split('-', 1) # each['provider'] = provider # each['region'] = region # # clusters.extend(gardener_cluster_metrics) return clusters def load_networks() -> list[Network]: # nightly-ms-3198830 PROJECT_ID_RE = re.compile('.*-(\d+)$') network_data = json.load(open(settings.INFRA_EXPORTER_NETWORK_SNAPSHOT_FILE)) key2network: dict[str, Network] = {} for each in network_data: data = each['metric'] approutes = data.get('approutes') approutes = approutes if isinstance(approutes, dict) else {} network = Network( name=data['name'], namespace=data['exported_namespace'], provider=data['exported_provider'], region=data['exported_region'], vpc=data.get('vpc', ''), tenant_id=data.get('tenant_id', ''), project_id=data.get('project_id', ''), status=data.get('status', ''), shoot_name='', dataplane=data.get('dataplane', ''), env=util.get_env_by_infra_cr_namespace(data['exported_namespace']), vpc_cr_status=data.get('vpc_status', ''), internet_cr_status=data.get('internet_status', ''), appbox_cr_status=data.get('appbox_status', ''), approute_cr_status = approutes ) key2network[build_network_key(network.project_id, network.name)] = network # gardener_network_data = json.load(open(settings.INFRA_API_RESOURCES_NETWORK_SNAPSHOT_FILE)) # gardener_network_metrics = [each['metric'] for each in gardener_network_data['data']['result']] # for each in gardener_network_metrics: # mr = PROJECT_ID_RE.match(each.get('project_id', '')) # if mr: # each['project_id'] = mr.groups()[0] # # for metrics in gardener_network_metrics: # network = Network( # name=metrics['name'], namespace=metrics['exported_namespace'], # provider=metrics['exported_provider'], region=metrics['exported_region'], # vpc=metrics.get('vpc', ''), # tenant_id=metrics.get('tenant_id', ''), project_id=metrics.get('project_id', ''), # status=metrics.get('status', ''), # shoot_name=metrics['shoot_name'], # env=util.get_env_by_infra_cr_namespace(metrics['exported_namespace']), # ) # if not network.project_id or not network.name: # logger.warning(f'project_id or name is empty: {network}') # continue # # key = build_network_key(network.project_id, network.name) # old = key2network.get(key) # if old: # merge if this gardener network already exists in metrics `infra_export_network` # old.shoot_name = network.shoot_name # if util.is_empty(old.vpc): # old.vpc = network.vpc # else: # key2network[key] = network return list(key2network.values()) def load_shoots() -> list[Shoot]: shoots_data = json.load(open(settings.INFRA_GARDENER_RESOURCES_SHOOT_SNAPSHOT_FILE)) shoots: list[Shoot] = [] for each in shoots_data['data']['result']: data = each['metric'] ns = data.get('exported_namespace') if ns is None: logger.warning(f'invalid shoot: {data}') continue if data.get('project') is None: logger.warning(f'miss project, shoot: {data}') # set env env = '' if ns == 'garden-nightly': env = Env.Dev elif ns == 'garden-staging': env = Env.Staging elif ns == 'garden-prod': env = Env.Prod else: logger.warning(f'invalid ns: {ns}') continue provider = data['exported_provider'] region = data['exported_region'] network_namespace = f'{get_infra_cr_namespace_prefix(env)}-{data.get("project", "")}' network_name = f"{provider}-{region}" shoot = Shoot( env=env, provider=provider, region=region, network_namespace=network_namespace, network_name=network_name, name=data['name'], tenant=data.get('tenant', 'unknown'), project=data.get('project', 'unknown'), ) if shoot.name == 'f14fe37': logger.error(f'####: {shoot}') shoots.append(shoot) return shoots def load_pods(): pods: list[Pod] = [] pods_data = json.load(open(settings.POD_NODE_SCHEDULED_INFO_FILE)) for pod in pods_data: pods.append(Pod( pod=pod['pod'], namespace=pod['namespace'], cluster=pod['cluster'], node_instance_id=pod['node_instance_id'], node=pod['node'] )) return pods def build_metadata() -> Metadata: # clusters = json.load(open('dwm_dbaas_clusters.json')) # projects = json.load(open('dwm_dbaas_projects.json')) projects = [] networks = load_networks() clusters = load_clusters() shoots = [] pods = load_pods() #shoots = load_shoots() md = Metadata(clusters=clusters, projects=projects, networks=networks, shoots=shoots, pods=pods) print( f'len clusters={len(md.clusters)}, len projects={len(md.projects)}, len networks={len(md.networks)}' f', len shoots={len(md.shoots)}' ) return md