ocboot.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827
  1. # encoding: utf-8
  2. from __future__ import unicode_literals
  3. import os
  4. import base64
  5. from lib import k3s
  6. from . import ansible
  7. from . import utils
  8. from . import consts
  9. from .color import RB as Red
  10. from .k3s import is_using_k3s
  11. # Import IPv6 support functions
  12. try:
  13. import run
  14. from run import match_ipaddr
  15. except ImportError:
  16. # Fallback function if import fails
  17. def match_ipaddr(ip_str):
  18. import re
  19. # Simple IPv4 regex
  20. ipv4_pattern = r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
  21. if re.match(ipv4_pattern, ip_str):
  22. return (True, consts.IP_TYPE_IPV4)
  23. # Simple IPv6 regex (basic check)
  24. if ':' in ip_str:
  25. return (True, consts.IP_TYPE_IPV6)
  26. return (False, None)
  27. GROUP_MARIADB_NODE = "mariadb_node"
  28. GROUP_MARIADB_HA_NODES = "mariadb_ha_nodes"
  29. GROUP_CLICKHOUSE_NODE = "clickhouse_node"
  30. GROUP_REGISTRY_NODE = "registry_node"
  31. GROUP_PRIMARY_MASTER_NODE = "primary_master_node"
  32. GROUP_MASTER_NODES = "master_nodes"
  33. GROUP_WORKER_NODES = "worker_nodes"
  34. GROUP_NODES = "nodes"
  35. KEY_HOSTNAME = 'hostname'
  36. KEY_ONECLOUD_VERSION = 'onecloud_version'
  37. KEY_OPERATOR_VERSION = 'operator_version'
  38. KEY_ONECLOUD_MAJOR_VERSION = 'onecloud_major_version'
  39. KEY_PRODUCT_VERSION = 'product_version'
  40. KEY_AS_HOST = 'as_host'
  41. KEY_AS_HOST_ON_VM = 'as_host_on_vm'
  42. KEY_EXTRA_PACKAGES = 'extra_packages'
  43. KEY_IMAGE_REPOSITORY = 'image_repository'
  44. KEY_K8S_CONTROLPLANE_HOST = 'k8s_controlplane_host'
  45. KEY_K8S_OR_K3S = 'env_k8s_or_k3s'
  46. KEY_K3S_API_ENDPOINT = "api_endpoint"
  47. KEY_K3S_API_PORT = "api_port"
  48. KEY_K3S_AIRGAP_DIR = "airgap_dir"
  49. KEY_K3S_VERSION = "k3s_version"
  50. VAL_K3S_VERSION = k3s.VERSION_V1_28_5_K3S_1
  51. KEY_K3S_TOKEN = "token"
  52. VAL_K3S_TOKEN = "mytoken@yunionio"
  53. KEY_STACK_FULLSTACK = 'FullStack'
  54. KEY_STACK_EDGE = 'Edge'
  55. KEY_STACK_LIGHT_EDGE = 'LightEdge'
  56. KEY_STACK_CMP = 'CMP'
  57. KEY_STACK_AI = 'AI'
  58. KEY_STACK_LIST = [KEY_STACK_FULLSTACK, KEY_STACK_EDGE, KEY_STACK_LIGHT_EDGE, KEY_STACK_CMP, KEY_STACK_AI]
  59. KEY_TARGET_EDITION = 'TARGET_EDITION'
  60. KEY_USER_DNS = 'user_dns'
  61. KEY_REGION = "region"
  62. KEY_ZONE = "zone"
  63. KEY_ENABLE_CONTAINERD = "enable_containerd"
  64. KEY_HOST_NETWORKS = "host_networks"
  65. KEY_DISK_PATHS = "disk_paths"
  66. KEY_PRIMARY_MASTER_NODE_IP = "primary_master_node_ip"
  67. def load_config(config_file):
  68. import yaml
  69. with open(config_file) as f:
  70. config = Config(yaml.safe_load(f))
  71. return OcbootConfig(config)
  72. def get_ansible_global_vars_by_cluster(cluster):
  73. vars = get_ansible_global_vars(
  74. cluster.get_current_version(),
  75. cluster.is_using_k3s())
  76. return vars
  77. def get_ansible_global_vars(version, _is_using_k3s=None):
  78. if _is_using_k3s is None:
  79. _is_using_k3s = is_using_k3s()
  80. major_version = utils.get_major_version(version)
  81. vars = {
  82. KEY_ONECLOUD_VERSION: version,
  83. KEY_ONECLOUD_MAJOR_VERSION: major_version,
  84. KEY_EXTRA_PACKAGES: [],
  85. KEY_K3S_VERSION: VAL_K3S_VERSION,
  86. KEY_K3S_AIRGAP_DIR: k3s.GET_AIRGAP_DIR(),
  87. KEY_K3S_TOKEN: VAL_K3S_TOKEN,
  88. KEY_K8S_OR_K3S: 'k3s' if _is_using_k3s else 'k8s', # for path, eg: utils/k8s/kubelet or utils/k3s/kubelet,
  89. }
  90. # set yunion_qemu_package for pre released version
  91. yunion_qemu_package = 'yunion-qemu-4.2.0'
  92. extra_packages = []
  93. if utils.is_below_v3_9(version):
  94. yunion_qemu_package = 'yunion-qemu-2.12.1'
  95. if yunion_qemu_package:
  96. vars['yunion_qemu_package'] = yunion_qemu_package
  97. vars[KEY_EXTRA_PACKAGES] = extra_packages
  98. return vars
  99. class OcbootConfig(object):
  100. def __init__(self, config):
  101. self.config = config
  102. self.bastion_host = self.load_bastion_host(config)
  103. self.mariadb_config = self._fetch_conf(MariadbConfig)
  104. self.mariadb_ha_config = self._fetch_conf(MariadbHAConfig)
  105. self.clickhouse_config = self._fetch_conf(ClickhouseConfig)
  106. self.registry_config = self._fetch_conf(RegistryConfig)
  107. self.primary_master_config = self._fetch_conf(PrimaryMasterConfig)
  108. self.master_config = self._fetch_conf(MasterConfig)
  109. if self.master_config:
  110. self.master_config.set_primary_master_config(self.primary_master_config)
  111. self.worker_config = self._fetch_conf(WorkerConfig)
  112. if self.worker_config:
  113. self.worker_config.set_primary_master_config(self.primary_master_config)
  114. if self.mariadb_config and self.mariadb_ha_config:
  115. raise Exception("mariadb_node and mariadb_ha_nodes can't coexist in config")
  116. def load_bastion_host(self, config):
  117. bastion_config = config.get('bastion_host', None)
  118. if not bastion_config:
  119. return None
  120. bastion_config = Config(bastion_config)
  121. host = bastion_config.ensure_get('host', 'hostname')
  122. user = bastion_config.get('user', 'root')
  123. return ansible.AnsibleBastionHost(host, user)
  124. def is_iso_join_mode(self):
  125. worker_nodes = self.config.get('worker_nodes', None)
  126. master_nodes = self.config.get('master_nodes', None)
  127. if worker_nodes and worker_nodes.get('join_token', None):
  128. return True
  129. if master_nodes and master_nodes.get('join_token', None):
  130. return True
  131. return False
  132. def get_primary_master_ssh_port(self):
  133. if self.is_iso_join_mode():
  134. return
  135. return self.primary_master_config.node.port
  136. def is_controller_node(self):
  137. if self.primary_master_config:
  138. return True
  139. if self.master_config:
  140. return True
  141. return False
  142. def _fetch_conf(self, config_cls):
  143. group = config_cls.get_group()
  144. group_config = self.config.get(group, None)
  145. if not group_config:
  146. return None
  147. if group in [GROUP_MASTER_NODES, GROUP_WORKER_NODES]:
  148. if not group_config.get('controlplane_ssh_port', None):
  149. group_config['controlplane_ssh_port'] = self.get_primary_master_ssh_port()
  150. return config_cls(Config(group_config), self.bastion_host)
  151. def get_onecloud_version(self):
  152. for node in [self.mariadb_config, self.mariadb_ha_config, self.clickhouse_config, self.registry_config, self.primary_master_config, self.master_config, self.worker_config]:
  153. if not node:
  154. continue
  155. version = getattr(node, KEY_ONECLOUD_VERSION, None)
  156. if version:
  157. return version
  158. raise Exception("get attr onecloud_version error")
  159. def ansible_global_vars(self):
  160. return get_ansible_global_vars(self.get_onecloud_version())
  161. def get_ansible_inventory(self):
  162. return ansible.get_inventory_config(
  163. self.mariadb_config,
  164. self.mariadb_ha_config,
  165. self.clickhouse_config,
  166. self.registry_config,
  167. self.primary_master_config,
  168. self.master_config,
  169. self.worker_config)
  170. def generate_inventory_file(self):
  171. content = self.get_ansible_inventory()
  172. yaml_content = utils.to_yaml(content)
  173. filepath = './host_inventory.yml'
  174. with open(filepath, 'w') as f:
  175. f.write(yaml_content)
  176. return filepath
  177. def get_login_info(self):
  178. if self.primary_master_config is None:
  179. return None
  180. p_master_config = self.primary_master_config
  181. frontend_ip = p_master_config.controlplane_host
  182. user = p_master_config.onecloud_user
  183. password = p_master_config.onecloud_user_password
  184. if frontend_ip is None:
  185. raise Exception("Not found controlplane_host in config")
  186. if user is None:
  187. raise Exception("Not found onecloud_user in config")
  188. if password is None:
  189. raise Exception("Not found onecloud_user_password in config")
  190. # 处理IPv6地址:为URL显示添加方括号
  191. from run import _match_ipv6addr
  192. if _match_ipv6addr(frontend_ip):
  193. display_ip = f"[{frontend_ip}]"
  194. else:
  195. display_ip = frontend_ip
  196. return (display_ip, user, password)
  197. def is_using_ee(self):
  198. return self.primary_master_config.use_ee
  199. class ConfigNotFoundException(Exception):
  200. def __init__(self, config, what):
  201. self.config = config
  202. self.what = what
  203. def __str__(self):
  204. return "not found '%s' in config %s" % (self.what, self.config)
  205. class Config(object):
  206. def __init__(self, config):
  207. self.config = config
  208. def get_config(self, group):
  209. config = self.ensure_get(group)
  210. return Config(config)
  211. def get(self, key, default):
  212. return self.config.get(key, default)
  213. def __getattr__(self, item):
  214. return self.config[item]
  215. def ensure_get(self, key, alter_key=None):
  216. val = self.get(key, None)
  217. if val:
  218. return val
  219. if alter_key:
  220. val = self.get(alter_key, None)
  221. if not val:
  222. what = key
  223. if alter_key:
  224. what = '%s or %s' % (key, alter_key)
  225. raise ConfigNotFoundException(self.config, what)
  226. return val
  227. class Node(object):
  228. def __init__(self, config):
  229. self.use_local = config.get('use_local', False)
  230. self.host = '127.0.0.1' if self.use_local else config.ensure_get('host', 'hostname')
  231. self.user = config.get('user', 'root')
  232. self.port = config.get('port', 22)
  233. self.host_networks = config.get(KEY_HOST_NETWORKS, None)
  234. if isinstance(self.host_networks, str):
  235. self.host_networks = [self.host_networks]
  236. self.disk_paths = config.get(KEY_DISK_PATHS, None)
  237. self.enable_hugepage = config.get('enable_hugepage', True)
  238. self.node_ip = config.get('node_ip', None)
  239. if not self.node_ip:
  240. self.node_ip = self.host
  241. self.bastion_host = None
  242. self.vrrp_priority = config.get('vrrp_priority', 0)
  243. self.vrrp_interface = config.get('vrrp_interface', None)
  244. self.vrrp_vip = config.get('vrrp_vip', None)
  245. self.vrrp_router_id = config.get('vrrp_router_id', None)
  246. def get_host(self):
  247. return self.host
  248. def with_bastion(self, bastion_host):
  249. self.bastion_host = bastion_host
  250. return self
  251. def ansible_host_vars(self):
  252. vars = {
  253. 'ansible_host': self.host,
  254. 'ansible_user': self.user,
  255. 'ansible_port': self.port,
  256. }
  257. if self.user != 'root':
  258. vars['ansible_become'] = 'yes'
  259. if self.bastion_host:
  260. vars['ansible_ssh_common_args'] = self.bastion_host.to_option()
  261. if self.host != "127.0.0.1":
  262. vars['node_ip'] = self.node_ip
  263. if self.use_local:
  264. vars['ansible_connection'] = 'local'
  265. if self.host_networks:
  266. vars[KEY_HOST_NETWORKS] = self.host_networks
  267. if self.disk_paths:
  268. vars[KEY_DISK_PATHS] = self.disk_paths
  269. if self.enable_hugepage:
  270. vars['enable_hugepage'] = self.enable_hugepage
  271. if self.vrrp_interface or self.vrrp_vip:
  272. vars['vrrp_vip'] = self.vrrp_vip
  273. vars['vrrp_interface'] = self.vrrp_interface
  274. vars['vrrp_priority'] = self.vrrp_priority
  275. vars['vrrp_router_id'] = self.vrrp_router_id
  276. return vars
  277. def __str__(self):
  278. ret = self.host
  279. if self.host != "127.0.0.1":
  280. ret = "%s node_ip=%s" % (ret, self.node_ip)
  281. if self.use_local:
  282. ret = "%s ansible_connection=local" % ret
  283. if self.user is not None:
  284. ret = "%s ansible_user=%s" % (ret, self.user)
  285. if self.host_networks is not None:
  286. ret = "%s host_networks=%s" % (ret, self.host_networks)
  287. return ret
  288. class MariadbConfig(object):
  289. def __init__(self, config, bastion_host=None):
  290. self.node = Node(config).with_bastion(bastion_host)
  291. self.db_user = config.get('db_user', 'root')
  292. self.db_password = config.ensure_get('db_password')
  293. self.db_port = config.get('db_port', 3306)
  294. @classmethod
  295. def get_group(cls):
  296. return GROUP_MARIADB_NODE
  297. def get_nodes(self):
  298. return [self.node]
  299. def ansible_vars(self):
  300. return {
  301. "db_user": self.db_user,
  302. "db_password": self.db_password,
  303. "db_port": self.db_port,
  304. "db_host": self.node.host,
  305. }
  306. class MariadbHAConfig(object):
  307. def __init__(self, config, bastion_host=None):
  308. self.nodes = get_nodes(config, bastion_host)
  309. self.db_user = config.get('db_user', 'root')
  310. self.db_password = config.ensure_get('db_password')
  311. self.db_port = config.get('db_port', 3306)
  312. self.db_vip = config.get('db_vip', None)
  313. self.db_nic = config.get('db_nic', None)
  314. @classmethod
  315. def get_group(cls):
  316. return GROUP_MARIADB_HA_NODES
  317. def get_nodes(self):
  318. return self.nodes
  319. def ansible_vars(self):
  320. vars = {
  321. "db_user": self.db_user,
  322. "db_password": self.db_password,
  323. "db_port": self.db_port,
  324. }
  325. if self.db_vip:
  326. vars['db_vip'] = self.db_vip
  327. vars['db_nic'] = self.db_nic
  328. return vars
  329. class ClickhouseConfig(object):
  330. def __init__(self, config, bastion_host=None):
  331. self.node = Node(config).with_bastion(bastion_host)
  332. self.ch_password = config.ensure_get('ch_password')
  333. self.ch_port = config.get('ch_port', 9000)
  334. @classmethod
  335. def get_group(cls):
  336. return GROUP_CLICKHOUSE_NODE
  337. def get_nodes(self):
  338. return [self.node]
  339. def ansible_vars(self):
  340. vars = {
  341. "ch_password": self.ch_password,
  342. "ch_port": self.ch_port,
  343. }
  344. return vars
  345. class RegistryConfig(object):
  346. def __init__(self, config, bastion_host=None):
  347. self.node = Node(config).with_bastion(bastion_host)
  348. self.port = config.get('port', '5000')
  349. self.root_dir = config.get('root_dir', '/opt/registry')
  350. @classmethod
  351. def get_group(cls):
  352. return GROUP_REGISTRY_NODE
  353. def get_nodes(self):
  354. return [self.node]
  355. def ansible_vars(self):
  356. return {
  357. "listen_port": self.port,
  358. "root_dir": self.root_dir,
  359. }
  360. class OnecloudConfig(object):
  361. def __init__(self, config):
  362. self.controlplane_host = config.ensure_get('controlplane_host')
  363. self.controlplane_port = config.get('controlplane_port', '6443')
  364. self.as_host = config.get('as_host', None)
  365. self.as_host_on_vm = config.get('as_host_on_vm', None)
  366. self.registry_mirrors = config.get('registry_mirrors', [])
  367. self.insecure_registries = config.get('insecure_registries', [])
  368. self.skip_docker_config = config.get('skip_docker_config', False)
  369. self.node_ip = config.get('node_ip', None)
  370. self.onecloud_version = config.get(KEY_ONECLOUD_VERSION, None)
  371. self.high_availability = config.get('high_availability', False)
  372. self.high_availability_vip = None
  373. self.keepalived_version_tag = None
  374. self.keepalived_password = None
  375. default_keepalived_version_tag = 'v2.0.29' if is_using_k3s() else 'v2.0.25'
  376. if self.high_availability:
  377. self.high_availability_vip = self.controlplane_host
  378. self.keepalived_password = base64.b64encode(self.high_availability_vip.encode('ascii'))[0:8].decode()
  379. # 计算 keepalived_router_id,支持 IPv4 和 IPv6
  380. if ':' in self.high_availability_vip:
  381. # IPv6 地址:使用哈希值计算 router_id
  382. import hashlib
  383. hash_value = hashlib.md5(self.high_availability_vip.encode()).hexdigest()
  384. self.keepalived_router_id = int(hash_value[:8], 16) % 255
  385. else:
  386. # IPv4 地址:使用原来的逻辑
  387. self.keepalived_router_id = int(self.high_availability_vip.replace('.', '')) % 255
  388. if self.keepalived_router_id == 0:
  389. self.keepalived_router_id = 100
  390. self.keepalived_version_tag = config.get('keepalived_version_tag', default_keepalived_version_tag)
  391. self.iso_install_mode = config.get('iso_install_mode', False)
  392. self.enable_eip_man = config.get('enable_eip_man', False)
  393. self.offline_deploy = config.get('offline_deploy', False) or os.environ.get('OFFLINE_DEPLOY') == 'true'
  394. self.enable_lbagent = config.get('enable_lbagent', False)
  395. self.enable_containerd = config.get(KEY_ENABLE_CONTAINERD, False)
  396. # AI 环境开关:仅在 product_version 为 AI 且显式开启时触发 utils/ai-env role
  397. self.enable_ai_env = config.get('enable_ai_env', False)
  398. self.host_networks = config.get(KEY_HOST_NETWORKS, None)
  399. if isinstance(self.host_networks, str):
  400. self.host_networks = [self.host_networks]
  401. self.disk_paths = config.get(KEY_DISK_PATHS, None)
  402. self.primary_master_node_ip = config.get(KEY_PRIMARY_MASTER_NODE_IP, None)
  403. def ansible_vars(self):
  404. vars = {
  405. 'docker_registry_mirrors': self.registry_mirrors,
  406. 'docker_insecure_registries': self.insecure_registries,
  407. KEY_K8S_CONTROLPLANE_HOST: self.controlplane_host,
  408. KEY_K3S_API_ENDPOINT: self.controlplane_host,
  409. 'k8s_controlplane_port': self.controlplane_port,
  410. KEY_K3S_API_PORT: self.controlplane_port,
  411. 'k8s_node_as_oc_host': self.as_host,
  412. 'k8s_node_as_oc_host_on_vm': self.as_host_on_vm,
  413. 'enable_eip_man': self.enable_eip_man,
  414. 'offline_deploy': self.offline_deploy,
  415. 'enable_lbagent': self.enable_lbagent,
  416. KEY_ENABLE_CONTAINERD: self.enable_containerd,
  417. 'enable_ai_env': self.enable_ai_env,
  418. }
  419. if self.high_availability_vip:
  420. vars['high_availability_vip'] = self.high_availability_vip
  421. vars['keepalived_version_tag'] = self.keepalived_version_tag
  422. vars['keepalived_password'] = self.keepalived_password
  423. vars['keepalived_router_id'] = self.keepalived_router_id
  424. if self.onecloud_version:
  425. vars[KEY_ONECLOUD_VERSION] = self.onecloud_version
  426. if self.node_ip:
  427. vars['node_ip'] = self.node_ip
  428. if self.iso_install_mode:
  429. vars['iso_install_mode'] = True
  430. if self.host_networks:
  431. vars[KEY_HOST_NETWORKS] = self.host_networks
  432. if self.disk_paths:
  433. vars[KEY_DISK_PATHS] = self.disk_paths
  434. if self.primary_master_node_ip:
  435. vars[KEY_PRIMARY_MASTER_NODE_IP] = self.primary_master_node_ip
  436. return vars
  437. class PrimaryMasterConfig(OnecloudConfig):
  438. # All kinds of products
  439. PRODUCT_VERSION_FULL_STACK = "FullStack"
  440. # Cloud Management Platform product
  441. PRODUCT_VERSION_CMP = "CMP"
  442. # Private Cloud Edge on-premise product
  443. PRODUCT_VERSION_EDGE = "Edge"
  444. PRODUCT_VERSION_LIGHT_EDGE = "LightEdge"
  445. # AI Cloud product
  446. PRODUCT_VERSION_AI = "AI"
  447. PRODUCT_VERSIONS = [
  448. PRODUCT_VERSION_FULL_STACK,
  449. PRODUCT_VERSION_CMP,
  450. PRODUCT_VERSION_EDGE,
  451. PRODUCT_VERSION_LIGHT_EDGE,
  452. PRODUCT_VERSION_AI,
  453. ]
  454. def __init__(self, config, bastion_host=None):
  455. super(PrimaryMasterConfig, self).__init__(config)
  456. self.node = Node(config).with_bastion(bastion_host)
  457. self.db_user = config.get('db_user', 'root')
  458. self.db_host = config.ensure_get('db_host')
  459. self.db_port = config.get('db_port', 3306)
  460. self.db_password = config.ensure_get('db_password')
  461. self.onecloud_version = config.ensure_get(KEY_ONECLOUD_VERSION)
  462. self.operator_version = config.get(KEY_OPERATOR_VERSION, self.onecloud_version)
  463. self.restore_mode = config.get('restore_mode', False)
  464. # 优先使用配置文件中设置的ip_type,如果没有设置则自动检测
  465. config_ip_type = config.get('ip_type', None)
  466. print(f"PrimaryMasterConfig config_ip_type is {config_ip_type}")
  467. if config_ip_type:
  468. # 使用配置文件中设置的ip_type
  469. self.ip_type = config_ip_type
  470. else:
  471. # 自动检测IP类型
  472. match_ip, ip_type = match_ipaddr(self.node.node_ip)
  473. self.ip_type = ip_type if match_ip else consts.IP_TYPE_IPV4
  474. # 支持双栈配置
  475. if self.ip_type == consts.IP_TYPE_DUAL_STACK:
  476. # 双栈配置:需要获取IPv4和IPv6地址
  477. self.node_ip_v4 = config.get('node_ip_v4', None)
  478. self.node_ip_v6 = config.get('node_ip_v6', None)
  479. self.pod_network_cidr_v4 = config.get('pod_network_cidr_v4', '10.40.0.0/16')
  480. self.service_cidr_v4 = config.get('service_cidr_v4', '10.96.0.0/12')
  481. # IPIP配置选项
  482. self.enable_ipip = config.get('enable_ipip', False)
  483. # set calico ip_autodetection_method only in primary master
  484. self.ip_autodetection_method = config.get('ip_autodetection_method', None)
  485. if not self.ip_autodetection_method:
  486. if self.ip_type == consts.IP_TYPE_IPV6:
  487. # For IPv6, use interface-based detection to avoid syntax issues
  488. self.ip_autodetection_method = "'first-found'"
  489. elif self.ip_type == consts.IP_TYPE_DUAL_STACK:
  490. # For dual-stack, use can-reach with primary IP to ensure correct interface selection
  491. self.ip_autodetection_method = "'can-reach=%s'" % self.node.node_ip
  492. else:
  493. self.ip_autodetection_method = "'can-reach=%s'" % self.node.node_ip
  494. self.onecloud_user = config.get('onecloud_user', 'admin')
  495. self.onecloud_user_password = config.get('onecloud_user_password', 'admin@123')
  496. self.use_ee = config.get('use_ee', False)
  497. self.image_repository = config.get('image_repository', consts.REGISTRY_ALI_YUNION)
  498. if utils.is_below_v3_9(self.onecloud_version):
  499. self.image_repository = consts.REGISTRY_ALI_YUNIONIO
  500. self.enable_minio = config.get('enable_minio', False)
  501. self.offline_nodes = config.get('offline_nodes', '')
  502. # Set default network CIDRs based on IP type
  503. if self.ip_type == consts.IP_TYPE_IPV6:
  504. # IPv6 default networks - k3s treats IPv6+IPv4 dual-stack as IPv4
  505. # For pure IPv6, cluster-cidr must be smaller than node-cidr-mask-size (/64)
  506. # Use /56 for cluster to allow multiple /64 node subnets
  507. default_pod_cidr = config.get('pod_network_cidr', 'fd85:ee78:d8a6:8607::/56')
  508. default_service_cidr = config.get('service_cidr', 'fd85:ee78:d8a6:8608::/112')
  509. elif self.ip_type == consts.IP_TYPE_DUAL_STACK:
  510. # Dual-stack networks
  511. default_pod_cidr = config.get('pod_network_cidr', 'fd85:ee78:d8a6:8607::/56')
  512. default_service_cidr = config.get('service_cidr', 'fd85:ee78:d8a6:8608::/112')
  513. else:
  514. # IPv4 default networks (also used for dual-stack as per k3s behavior)
  515. default_pod_cidr = config.get('pod_network_cidr', '10.40.0.0/16')
  516. default_service_cidr = config.get('service_cidr', '10.96.0.0/12')
  517. self.pod_network_cidr = default_pod_cidr
  518. self.service_cidr = default_service_cidr
  519. self.service_dns_domain = config.get('service_dns_domain', 'cluster.local')
  520. self.product_version = self.get_product_version(config)
  521. self.user_dns = config.get(KEY_USER_DNS, [])
  522. self.region = config.get(KEY_REGION, "region0")
  523. self.zone = config.get(KEY_ZONE, "zone0")
  524. def get_product_version(self, config):
  525. pv = config.get('product_version', self.PRODUCT_VERSION_FULL_STACK)
  526. if pv in self.PRODUCT_VERSIONS:
  527. return pv
  528. raise Exception("Unsupported product_version: %s" % pv)
  529. @classmethod
  530. def get_group(cls):
  531. return GROUP_PRIMARY_MASTER_NODE
  532. def ansible_vars(self):
  533. vars = super(PrimaryMasterConfig, self).ansible_vars()
  534. vars['db_host'] = self.db_host
  535. vars['db_port'] = self.db_port
  536. vars['db_user'] = self.db_user
  537. vars['db_password'] = self.db_password
  538. vars[KEY_ONECLOUD_VERSION] = self.onecloud_version
  539. vars[KEY_OPERATOR_VERSION] = self.operator_version
  540. vars['onecloud_user'] = self.onecloud_user
  541. vars['onecloud_user_password'] = self.onecloud_user_password
  542. vars['use_ee'] = self.use_ee
  543. vars['apiserver_advertise_address'] = self.node.node_ip
  544. vars[KEY_K3S_API_ENDPOINT] = self.controlplane_host
  545. vars['ip_autodetection_method'] = self.ip_autodetection_method
  546. vars['image_repository'] = self.image_repository
  547. vars['enable_minio'] = self.enable_minio
  548. vars['restore_mode'] = self.restore_mode
  549. vars['pod_network_cidr'] = self.pod_network_cidr
  550. vars['service_cidr'] = self.service_cidr
  551. vars['service_dns_domain'] = self.service_dns_domain
  552. vars['ip_type'] = self.ip_type
  553. # 添加双栈配置变量
  554. if self.ip_type == consts.IP_TYPE_DUAL_STACK:
  555. vars['node_ip_v4'] = self.node_ip_v4
  556. vars['node_ip_v6'] = self.node_ip_v6
  557. vars['pod_network_cidr_v4'] = self.pod_network_cidr_v4
  558. vars['service_cidr_v4'] = self.service_cidr_v4
  559. vars['enable_ipip'] = self.enable_ipip
  560. if len(self.offline_nodes) > 0:
  561. vars['offline_nodes'] = ' '.join(self.offline_nodes)
  562. vars['product_version'] = self.product_version
  563. if self.user_dns:
  564. vars[KEY_USER_DNS] = self.user_dns
  565. vars[KEY_REGION] = self.region
  566. vars[KEY_ZONE] = self.zone
  567. return vars
  568. def get_nodes(self):
  569. return [self.node]
  570. class OnecloudJointConfig(OnecloudConfig):
  571. def __init__(self, config):
  572. super(OnecloudJointConfig, self).__init__(config)
  573. self.as_controller = config.get('as_controller', None)
  574. self.join_token = config.get('join_token', None)
  575. self.join_cert_key = config.get('join_certificate_key', None)
  576. self.ntpd_server = config.get('ntpd_server', None)
  577. self.controlplane_ssh_port = config.get('controlplane_ssh_port', 22)
  578. def ansible_vars(self):
  579. vars = super(OnecloudJointConfig, self).ansible_vars()
  580. vars['k8s_node_as_oc_controller'] = self.as_controller
  581. vars['k8s_controlplane_ssh_port'] = self.controlplane_ssh_port
  582. if self.join_token:
  583. vars['k8s_join_token'] = self.join_token
  584. if self.join_cert_key:
  585. vars['k8s_join_certificate_key'] = self.join_cert_key
  586. # TODO: ntpd_server should define in all nodes config?
  587. if self.ntpd_server:
  588. vars['ntpd_server'] = self.ntpd_server
  589. return vars
  590. def get_nodes(config, bastion_host=None):
  591. host_configs = config.ensure_get('hosts')
  592. nodes = []
  593. for host_config in host_configs:
  594. node = Node(Config(host_config)).with_bastion(bastion_host)
  595. nodes.append(node)
  596. return nodes
  597. class MasterConfig(OnecloudJointConfig):
  598. def __init__(self, config, bastion_host=None):
  599. super(MasterConfig, self).__init__(config)
  600. if self.as_controller is None:
  601. self.as_controller = True
  602. self.nodes = get_nodes(config, bastion_host)
  603. self.primary_master_config = None
  604. def set_primary_master_config(self, primary_master_config):
  605. self.primary_master_config = primary_master_config
  606. def ansible_vars(self):
  607. vars = super(MasterConfig, self).ansible_vars()
  608. pc = self.primary_master_config
  609. vars['pod_network_cidr'] = pc.pod_network_cidr
  610. vars['service_cidr'] = pc.service_cidr
  611. vars['service_dns_domain'] = pc.service_dns_domain
  612. vars['image_repository'] = pc.image_repository
  613. vars['ip_type'] = pc.ip_type
  614. # 添加双栈配置变量
  615. if pc.ip_type == consts.IP_TYPE_DUAL_STACK:
  616. vars['node_ip_v4'] = pc.node_ip_v4
  617. vars['node_ip_v6'] = pc.node_ip_v6
  618. vars['pod_network_cidr_v4'] = pc.pod_network_cidr_v4
  619. vars['service_cidr_v4'] = pc.service_cidr_v4
  620. vars['enable_ipip'] = pc.enable_ipip
  621. return vars
  622. @classmethod
  623. def get_group(cls):
  624. return GROUP_MASTER_NODES
  625. def get_nodes(self):
  626. return self.nodes
  627. class WorkerConfig(OnecloudJointConfig):
  628. def __init__(self, config, bastion_host=None):
  629. super(WorkerConfig, self).__init__(config)
  630. if self.as_host is None:
  631. self.as_host = True
  632. if self.as_host_on_vm is None:
  633. self.as_host_on_vm = False
  634. self.nodes = get_nodes(config, bastion_host)
  635. self.primary_master_config = None
  636. def set_primary_master_config(self, primary_master_config):
  637. self.primary_master_config = primary_master_config
  638. def ansible_vars(self):
  639. vars = super(WorkerConfig, self).ansible_vars()
  640. # Worker-specific: k3s agent token
  641. vars[KEY_K3S_TOKEN] = VAL_K3S_TOKEN
  642. if self.primary_master_config:
  643. # Use exactly the same logic as MasterConfig
  644. pc = self.primary_master_config
  645. print("WorkerConfig primary_master_config", pc)
  646. for attr in dir(pc):
  647. if not attr.startswith("__") and not callable(getattr(pc, attr)):
  648. print(f"pc.{attr} = {getattr(pc, attr)}")
  649. vars['pod_network_cidr'] = pc.pod_network_cidr
  650. vars['service_cidr'] = pc.service_cidr
  651. vars['service_dns_domain'] = pc.service_dns_domain
  652. vars['image_repository'] = pc.image_repository
  653. vars['ip_type'] = pc.ip_type
  654. # Worker-specific: needed for Calico IP detection on worker nodes
  655. vars['ip_autodetection_method'] = pc.ip_autodetection_method
  656. # 添加双栈配置变量
  657. if pc.ip_type == consts.IP_TYPE_DUAL_STACK:
  658. vars['node_ip_v4'] = pc.node_ip_v4
  659. vars['node_ip_v6'] = pc.node_ip_v6
  660. vars['pod_network_cidr_v4'] = pc.pod_network_cidr_v4
  661. vars['service_cidr_v4'] = pc.service_cidr_v4
  662. vars['enable_ipip'] = pc.enable_ipip
  663. # Critical: API endpoint variables for kubernetes-services-endpoint ConfigMap
  664. vars[KEY_K3S_API_ENDPOINT] = self.controlplane_host
  665. vars[KEY_K3S_API_PORT] = self.controlplane_port
  666. return vars
  667. @classmethod
  668. def get_group(cls):
  669. return GROUP_WORKER_NODES
  670. def get_nodes(self):
  671. return self.nodes
  672. class NodeConfig(object):
  673. def __init__(self, config, bastion_host=None):
  674. self.nodes = get_nodes(config, bastion_host)
  675. @classmethod
  676. def get_group(cls):
  677. return GROUP_NODES
  678. def get_nodes(self):
  679. return self.nodes
  680. def ansible_vars(self):
  681. return {}