run.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  1. #!/usr/bin/env python3
  2. # encoding: utf-8
  3. from __future__ import unicode_literals
  4. from __future__ import absolute_import
  5. import os
  6. import os.path
  7. from os import path
  8. import sys
  9. import re
  10. import argparse
  11. import subprocess
  12. from lib import install
  13. from lib import cmd
  14. from lib.parser import inject_add_hostagent_options, inject_add_nodes_runtime_options, help_d, inject_ssh_options, inject_ai_nvidia_options
  15. from lib.utils import init_local_user_path
  16. from lib.utils import pr_red, pr_green
  17. from lib.utils import regex_search
  18. from lib.utils import is_valid_dns
  19. from lib import ocboot
  20. from lib import consts
  21. def show_usage():
  22. usage = '''
  23. Usage: %s [master_ip|<config_file>.yml]
  24. ''' % __file__
  25. print(usage)
  26. IPADDR_REG_PATTERN = r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'
  27. IPADDR_REG = re.compile(IPADDR_REG_PATTERN)
  28. def _match_ip4addr(string):
  29. global IPADDR_REG
  30. return IPADDR_REG.match(string) is not None
  31. def _match_ipv6addr(string):
  32. # 判断字符串是否为合法 IPv6 地址
  33. ipv6_pattern = re.compile(
  34. r'^('
  35. r'(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}' # 全写
  36. r'|(?:[A-Fa-f0-9]{1,4}:){1,7}:' # 以::结尾
  37. r'|:(?::[A-Fa-f0-9]{1,4}){1,7}' # 以::开头
  38. r'|(?:[A-Fa-f0-9]{1,4}:){1,6}:[A-Fa-f0-9]{1,4}' # 单::中间
  39. r'|(?:[A-Fa-f0-9]{1,4}:){1,5}(?::[A-Fa-f0-9]{1,4}){1,2}'
  40. r'|(?:[A-Fa-f0-9]{1,4}:){1,4}(?::[A-Fa-f0-9]{1,4}){1,3}'
  41. r'|(?:[A-Fa-f0-9]{1,4}:){1,3}(?::[A-Fa-f0-9]{1,4}){1,4}'
  42. r'|(?:[A-Fa-f0-9]{1,4}:){1,2}(?::[A-Fa-f0-9]{1,4}){1,5}'
  43. r'|[A-Fa-f0-9]{1,4}:(?:(?::[A-Fa-f0-9]{1,4}){1,6})'
  44. r'|:(?:(?::[A-Fa-f0-9]{1,4}){1,7}|:)' # :: 或 ::xxxx
  45. r')'
  46. r'(?:/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]))?' # 可选前缀长度
  47. r'$'
  48. )
  49. return ipv6_pattern.match(string) is not None
  50. def match_ipaddr(string):
  51. if _match_ip4addr(string):
  52. return (True, consts.IP_TYPE_IPV4)
  53. if _match_ipv6addr(string):
  54. return (True, consts.IP_TYPE_IPV6)
  55. return (False, None)
  56. def match_dual_stack_ipaddr(ip1_string, ip2_string):
  57. """检测双栈IP地址配置"""
  58. # 检测两个IP地址的类型
  59. ip1_is_ipv4 = _match_ip4addr(ip1_string) if ip1_string else False
  60. ip1_is_ipv6 = _match_ipv6addr(ip1_string) if ip1_string else False
  61. ip2_is_ipv4 = _match_ip4addr(ip2_string) if ip2_string else False
  62. ip2_is_ipv6 = _match_ipv6addr(ip2_string) if ip2_string else False
  63. # 检查是否构成有效的双栈配置
  64. if (ip1_is_ipv4 and ip2_is_ipv6) or (ip1_is_ipv6 and ip2_is_ipv4):
  65. return (True, consts.IP_TYPE_DUAL_STACK)
  66. elif ip1_is_ipv4 or ip2_is_ipv4:
  67. return (True, consts.IP_TYPE_IPV4)
  68. elif ip1_is_ipv6 or ip2_is_ipv6:
  69. return (True, consts.IP_TYPE_IPV6)
  70. else:
  71. return (False, None)
  72. def versiontuple(v):
  73. return tuple(map(int, (v.split("."))))
  74. def version_ge(v1, v2):
  75. return versiontuple(v1) >= versiontuple(v2)
  76. def get_username():
  77. import getpass
  78. # python2 / python3 are all tested to get username
  79. return getpass.getuser()
  80. def check_pip3():
  81. ret = os.system("pip3 --version >/dev/null 2>&1")
  82. if ret == 0:
  83. return
  84. if install_packages(['python3-pip']) == 0:
  85. return
  86. raise Exception("install python3-pip failed")
  87. def check_ansible(pip_mirror):
  88. minimal_ansible_version = '2.11.12'
  89. cmd.init_ansible_playbook_path()
  90. ret = os.system("ansible-playbook --version >/dev/null 2>&1")
  91. if ret == 0:
  92. ver_out = os.popen("""ansible-playbook --version | head -1""").read().strip()
  93. ver_re = re.compile(r'ansible-playbook \[core\s(.*)]')
  94. match = ver_re.match(ver_out)
  95. if match:
  96. ansible_version = match.group(1)
  97. if version_ge(ansible_version, minimal_ansible_version):
  98. print("current ansible version: %s. PASS" % ansible_version)
  99. return
  100. else:
  101. print("Current ansible version (%s) is lower than expected(%s). upgrading ... " % (
  102. ansible_version, minimal_ansible_version))
  103. else:
  104. raise Exception(f"Invalid ansible-playbook --version output: {ver_out}")
  105. else:
  106. print("No ansible found. Installing ... ")
  107. try:
  108. install_ansible(pip_mirror)
  109. except Exception as e:
  110. print("Install ansible failed, please try to install ansible manually")
  111. raise e
  112. def install_packages(pkgs):
  113. ignore_check = os.getenv("IGNORE_ALL_CHECKS")
  114. if ignore_check == "true":
  115. return
  116. packager = None
  117. for p in ['/usr/bin/dnf', '/usr/bin/yum', '/usr/bin/apt']:
  118. if os.path.isfile(p) and os.access(p, os.X_OK):
  119. packager = p
  120. break
  121. if packager is None:
  122. print('Current os-release:')
  123. with open('/etc/os-release', 'r') as f:
  124. print(f.read())
  125. raise Exception("Install ansible failed for os-release is not supported.")
  126. username = get_username()
  127. if packager == '/usr/bin/apt' and username != 'root':
  128. packager == 'sudo /usr/bin/apt'
  129. cmdline = '%s install -y %s' % (packager, ' '.join(pkgs))
  130. return os.system(cmdline)
  131. def install_ansible(mirror):
  132. def get_pip_install_cmd(suffix_cmd, mirror):
  133. cmd = "python3 -m pip install --user --upgrade"
  134. if mirror:
  135. cmd = f'{cmd} -i {mirror}'
  136. return f'{cmd} {suffix_cmd}'
  137. for pkg in ['PyYAML']:
  138. install_packages([pkg])
  139. if os.system('rpm -qa | grep -q python3-pip') != 0:
  140. ret = os.system(get_pip_install_cmd('pip setuptools wheel', mirror))
  141. if ret != 0:
  142. raise Exception("Install/updrade pip3 failed. ")
  143. os.system(get_pip_install_cmd('pip', mirror))
  144. ret = os.system(get_pip_install_cmd("'ansible<=9.0.0'", mirror))
  145. if ret != 0:
  146. raise Exception("Install ansible failed. ")
  147. def check_passless_ssh(ipaddr, ip_type):
  148. username = get_username()
  149. cmd = f"ssh -o 'StrictHostKeyChecking=no' -o 'PasswordAuthentication=no' {username}@{ipaddr} uptime"
  150. print('cmd:', cmd)
  151. ret = os.system(cmd)
  152. if ret == 0:
  153. return
  154. try:
  155. install_passless_ssh(ipaddr)
  156. except Exception as e:
  157. print("Configure passwordless ssh failed, please try to configure it manually")
  158. raise e
  159. def install_passless_ssh(ipaddr):
  160. username = get_username()
  161. rsa_path = os.path.join(os.environ.get("HOME"), ".ssh/id_rsa")
  162. if not os.path.exists(rsa_path):
  163. ret = os.system("ssh-keygen -f %s -P '' -N ''" % (rsa_path))
  164. if ret != 0:
  165. raise Exception("ssh-keygen")
  166. print("We are going to run the following command to enable passwordless SSH login:")
  167. print("")
  168. print(" ssh-copy-id -i ~/.ssh/id_rsa.pub %s@%s" % (username, ipaddr))
  169. print("")
  170. print("Press any key to continue and then input %s's password to %s" % (username, ipaddr))
  171. os.system("read")
  172. ret = os.system("ssh-copy-id -i ~/.ssh/id_rsa.pub %s@%s" % (username, ipaddr))
  173. if ret != 0:
  174. raise Exception("ssh-copy-id")
  175. ret = os.system(
  176. "ssh -o 'StrictHostKeyChecking=no' -o 'PasswordAuthentication=no' %s@%s hostname" % (username, ipaddr))
  177. if ret != 0:
  178. raise Exception("check passwordless ssh login failed")
  179. def check_env(ipaddr=None, pip_mirror=None):
  180. ignore_check = os.getenv("IGNORE_ALL_CHECKS")
  181. if ignore_check == "true":
  182. return
  183. check_pip3()
  184. # check_ansible(pip_mirror)
  185. match_ip, ip_type = match_ipaddr(ipaddr)
  186. if match_ip:
  187. check_passless_ssh(ipaddr, ip_type)
  188. def random_password(num):
  189. assert (num >= 6)
  190. digits = r'23456789'
  191. letters = r'abcdefghjkmnpqrstuvwxyz'
  192. uppers = letters.upper()
  193. punc = r'' # !$@#%^&*-=+?;'
  194. chars = digits + letters + uppers + punc
  195. npass = None
  196. while True:
  197. npass = ''
  198. digits_cnt = 0
  199. letters_cnt = 0
  200. uppers_cnt = 0
  201. for i in range(num):
  202. import random
  203. ch = random.choice(chars)
  204. if ch in digits:
  205. digits_cnt += 1
  206. elif ch in letters:
  207. letters_cnt += 1
  208. elif ch in uppers:
  209. uppers_cnt += 1
  210. npass += ch
  211. if digits_cnt > 1 and letters_cnt > 1 and uppers_cnt > 1:
  212. return npass
  213. return npass
  214. conf = """
  215. # # clickhouse_node indicates the node where the clickhouse service needs to be deployed
  216. # clickhouse_node:
  217. # # IP of the machine to be deployed
  218. # hostname: 10.127.10.158
  219. # # SSH Login username of the machine to be deployed
  220. # user: ocboot_user
  221. # # Password of clickhouse
  222. # ch_password: your-clickhouse-password
  223. # mariadb_node indicates the node where the mariadb service needs to be deployed
  224. mariadb_node:
  225. # IP of the machine to be deployed
  226. hostname: 10.127.10.158
  227. # SSH Login username of the machine to be deployed
  228. user: ocboot_user
  229. # Username of mariadb
  230. db_user: root
  231. # Password of mariadb
  232. db_password: your-sql-password
  233. # primary_master_node indicates the machine running Kubernetes and OneCloud Platform
  234. primary_master_node:
  235. hostname: 10.127.10.158
  236. user: ocboot_user
  237. # Database connection address
  238. db_host: 10.127.10.158
  239. # Database connection username
  240. db_user: root
  241. # Database connection password
  242. db_password: your-sql-password
  243. # IP of Kubernetes controlplane
  244. controlplane_host: 10.127.10.158
  245. # Port of Kubernetes controlplane
  246. controlplane_port: "6443"
  247. # OneCloud version
  248. onecloud_version: 'v3.4.12'
  249. # OneCloud login username
  250. onecloud_user: admin
  251. # OneCloud login user's password
  252. onecloud_user_password: admin@123
  253. # This machine serves as a OneCloud private cloud computing node
  254. # as_host: true
  255. # as_host_on_vm: true
  256. # enable_eip_man for all-in-one mode only
  257. enable_eip_man: true
  258. product_version: 'product_stack'
  259. image_repository: registry.cn-beijing.aliyuncs.com/yunion
  260. # host_networks: '<interface>/br0/<ip>'
  261. """
  262. def dynamic_load():
  263. username = get_username()
  264. homepath = '/root' if username == 'root' else os.path.expanduser("~" + username)
  265. import glob
  266. paths = glob.glob('/usr/local/lib64/python3.?/site-packages/') + \
  267. glob.glob('/usr/local/lib64/python3.??/site-packages/') + \
  268. glob.glob(f'{homepath}/.local/lib/python3.?/site-packages') + \
  269. glob.glob(f'{homepath}/.local/lib/python3.??/site-packages')
  270. print("loading path:")
  271. for p in paths:
  272. if os.path.isdir(p) and p not in sys.path:
  273. sys.path.append(p)
  274. print("\t%s" % p)
  275. def update_config(yaml_conf, produc_stack, runtime):
  276. import os.path
  277. import os
  278. import yaml
  279. yaml_data = {}
  280. to_write = False
  281. offline_path = os.environ.get('OFFLINE_DATA_PATH', )
  282. if offline_path:
  283. pr_green('offline mode, no need to update config.')
  284. return yaml_conf
  285. assert produc_stack in ocboot.KEY_STACK_LIST
  286. try:
  287. if path.isfile(yaml_conf) and path.getsize(yaml_conf) > 0:
  288. with open(yaml_conf, 'r') as stream:
  289. yaml_data.update(yaml.safe_load(stream))
  290. except yaml.YAMLError as exc:
  291. pr_red("paring %s error: %s" % (yaml_conf, exc))
  292. raise Exception("paring %s error: %s" % (yaml_conf, exc))
  293. if not yaml_data.get(ocboot.GROUP_PRIMARY_MASTER_NODE, {}):
  294. return yaml_conf
  295. if yaml_data.get(ocboot.GROUP_PRIMARY_MASTER_NODE, {}).get(ocboot.KEY_PRODUCT_VERSION, '') != produc_stack:
  296. to_write = True
  297. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE][ocboot.KEY_PRODUCT_VERSION] = produc_stack
  298. if produc_stack == ocboot.KEY_STACK_CMP:
  299. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE][ocboot.KEY_AS_HOST] = False
  300. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE][ocboot.KEY_AS_HOST_ON_VM] = False
  301. else:
  302. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE][ocboot.KEY_AS_HOST] = True
  303. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE][ocboot.KEY_AS_HOST_ON_VM] = True
  304. enable_containerd = yaml_data.get(ocboot.GROUP_PRIMARY_MASTER_NODE, {}).get(ocboot.KEY_ENABLE_CONTAINERD, False)
  305. if enable_containerd:
  306. if runtime != consts.RUNTIME_CONTAINERD:
  307. to_write = True
  308. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE][ocboot.KEY_ENABLE_CONTAINERD] = False
  309. else:
  310. if runtime == consts.RUNTIME_CONTAINERD:
  311. to_write = True
  312. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE][ocboot.KEY_ENABLE_CONTAINERD] = True
  313. if to_write:
  314. with open(yaml_conf, 'w') as f:
  315. f.write(yaml.dump(yaml_data))
  316. return yaml_conf
  317. def generate_config(
  318. ipaddr, produc_stack,
  319. dns_list=[], runtime=consts.RUNTIME_QEMU,
  320. image_repository=None,
  321. region=consts.DEFAULT_REGION_NAME,
  322. zone=consts.DEFAULT_ZONE_NAME,
  323. ip_dual_conf=None, ip_type=None, enable_ipip=False):
  324. global conf
  325. import os.path
  326. import os
  327. dynamic_load()
  328. import yaml
  329. from lib.get_interface_by_ip import get_interface_by_ip
  330. config_dir = os.getenv("OCBOOT_CONFIG_DIR")
  331. cur_path = os.path.abspath(os.path.dirname(__file__))
  332. if not config_dir:
  333. config_dir = cur_path
  334. # 使用传入的ip_type,如果没有则重新检测
  335. if ip_type is None:
  336. match_ip, ip_type = match_ipaddr(ipaddr)
  337. if not match_ip:
  338. pr_red(f'invalid ipaddr {ipaddr}!')
  339. exit(1)
  340. temp = os.path.join(config_dir, "config-allinone-current.yml")
  341. verf = os.path.join(cur_path, "VERSION")
  342. brand_new = True
  343. yaml_data = yaml.safe_load(conf)
  344. with open(verf, 'r') as f:
  345. ver = f.read().strip()
  346. try:
  347. if path.isfile(temp) and path.getsize(temp) > 0:
  348. with open(temp, 'r') as stream:
  349. yaml_data.update(yaml.safe_load(stream))
  350. brand_new = False
  351. except yaml.YAMLError as exc:
  352. pr_red("paring %s error: %s" % (temp, exc))
  353. raise Exception("paring %s error: %s" % (temp, exc))
  354. if yaml_data.get(ocboot.GROUP_PRIMARY_MASTER_NODE, {}).get(ocboot.KEY_HOSTNAME, '') == ipaddr and \
  355. yaml_data.get(ocboot.GROUP_PRIMARY_MASTER_NODE, {}).get(ocboot.KEY_ONECLOUD_VERSION, '') == ver:
  356. update_config(temp, produc_stack, runtime)
  357. pr_green(f"reuse conf: {temp}")
  358. return temp
  359. # using given image_repository if provided;
  360. if image_repository not in ['', None, 'none']:
  361. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE]['image_repository'] = image_repository
  362. # else set to 'yunionio' namespace if it is daily build.
  363. # default is 'yunion', for the official and public release.
  364. elif re.search(r'\b\d{8}\.\d$', ver):
  365. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE]['image_repository'] = consts.REGISTRY_ALI_YUNIONIO
  366. if image_repository and '5000' in image_repository:
  367. r = image_repository
  368. if '/' in r:
  369. r = r.split('/')[0]
  370. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE]['insecure_registries'] = [r]
  371. interface = get_interface_by_ip(ipaddr)
  372. username = get_username()
  373. db_password = random_password(12) if brand_new else yaml_data.get(ocboot.GROUP_PRIMARY_MASTER_NODE, {}).get('db_password')
  374. assert db_password
  375. extra_db_dict = {
  376. 'db_password': db_password,
  377. 'user': username,
  378. ocboot.KEY_HOSTNAME: ipaddr,
  379. }
  380. enable_host = produc_stack in [ocboot.KEY_STACK_FULLSTACK, ocboot.KEY_STACK_EDGE, ocboot.KEY_STACK_LIGHT_EDGE, ocboot.KEY_STACK_AI]
  381. # 基础配置
  382. extra_pri_dict = {
  383. 'controlplane_host': ipaddr,
  384. 'db_host': ipaddr,
  385. 'db_password': db_password,
  386. 'user': username,
  387. ocboot.KEY_AS_HOST: enable_host,
  388. ocboot.KEY_AS_HOST_ON_VM: enable_host,
  389. ocboot.KEY_HOSTNAME: ipaddr,
  390. ocboot.KEY_ONECLOUD_VERSION: ver,
  391. ocboot.KEY_PRODUCT_VERSION: produc_stack,
  392. ocboot.KEY_REGION: region,
  393. ocboot.KEY_ZONE: zone,
  394. }
  395. # 添加双栈配置
  396. if ip_type == consts.IP_TYPE_DUAL_STACK and ip_dual_conf:
  397. extra_pri_dict['ip_type'] = ip_type
  398. extra_pri_dict['enable_ipip'] = enable_ipip
  399. # 确定哪个是IPv4,哪个是IPv6
  400. if _match_ip4addr(ipaddr):
  401. # 主IP是IPv4,ip_dual_conf是IPv6
  402. extra_pri_dict['node_ip'] = ipaddr # 主IP作为node_ip
  403. extra_pri_dict['node_ip_v4'] = ipaddr
  404. extra_pri_dict['node_ip_v6'] = ip_dual_conf
  405. extra_pri_dict['pod_network_cidr_v4'] = '10.40.0.0/16'
  406. extra_pri_dict['service_cidr_v4'] = '10.96.0.0/12'
  407. extra_pri_dict['pod_network_cidr'] = 'fd85:ee78:d8a6:8607::/56'
  408. extra_pri_dict['service_cidr'] = 'fd85:ee78:d8a6:8608::/112'
  409. # 双栈host_networks格式:interface/br0/ipv4/ipv6
  410. extra_pri_dict['host_networks'] = f'{interface}/br0/{ipaddr}/{ip_dual_conf}'
  411. else:
  412. # 主IP是IPv6,ip_dual_conf是IPv4
  413. extra_pri_dict['node_ip'] = ipaddr # 主IP作为node_ip
  414. extra_pri_dict['node_ip_v4'] = ip_dual_conf
  415. extra_pri_dict['node_ip_v6'] = ipaddr
  416. extra_pri_dict['pod_network_cidr'] = 'fd85:ee78:d8a6:8607::/56'
  417. extra_pri_dict['service_cidr'] = 'fd85:ee78:d8a6:8608::/112'
  418. extra_pri_dict['pod_network_cidr_v4'] = '10.40.0.0/16'
  419. extra_pri_dict['service_cidr_v4'] = '10.96.0.0/12'
  420. # 双栈host_networks格式:interface/br0/ipv4/ipv6
  421. extra_pri_dict['host_networks'] = f'{interface}/br0/{ip_dual_conf}/{ipaddr}'
  422. else:
  423. # 单栈配置
  424. extra_pri_dict['ip_type'] = ip_type
  425. if ip_type == consts.IP_TYPE_IPV4:
  426. extra_pri_dict['enable_ipip'] = enable_ipip
  427. extra_pri_dict['host_networks'] = f'{interface}/br0/{ipaddr}'
  428. if runtime == consts.RUNTIME_CONTAINERD:
  429. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE].update({
  430. ocboot.KEY_ENABLE_CONTAINERD: True,
  431. })
  432. if len(dns_list) > 0:
  433. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE].update({
  434. ocboot.KEY_USER_DNS: dns_list
  435. })
  436. yaml_data[ocboot.GROUP_PRIMARY_MASTER_NODE].update(extra_pri_dict)
  437. yaml_data[ocboot.GROUP_MARIADB_NODE].update(extra_db_dict)
  438. with open(temp, 'w') as f:
  439. f.write(yaml.dump(yaml_data))
  440. return temp
  441. parser = None
  442. def check_cluster_deployed(ipaddr):
  443. """检查目标节点是否已经部署了 OneCloud 集群。
  444. 通过 SSH 到目标节点执行 kubectl 命令来检测。
  445. """
  446. username = get_username()
  447. # 尝试通过 SSH 在目标节点上执行 kubectl 命令检查 onecloud 集群是否存在
  448. # 先尝试 k3s kubectl,再尝试 kubectl
  449. check_cmd = (
  450. "k3s kubectl -n onecloud get onecloudclusters default -o name 2>/dev/null"
  451. " || kubectl -n onecloud get onecloudclusters default -o name 2>/dev/null"
  452. )
  453. ssh_cmd = (
  454. f"ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no"
  455. f" -o PasswordAuthentication=no -o LogLevel=error"
  456. f" {username}@{ipaddr} '{check_cmd}'"
  457. )
  458. try:
  459. ret = subprocess.run(ssh_cmd, shell=True, capture_output=True, timeout=15)
  460. if ret.returncode == 0 and b'onecloudcluster' in ret.stdout.lower():
  461. return True
  462. except (subprocess.TimeoutExpired, Exception):
  463. pass
  464. return False
  465. def inject_common_options(parser):
  466. """添加 run.py 中所有命令共用的参数"""
  467. parser.add_argument('--force', action='store_true', default=False,
  468. help="Force install even if a cluster is already deployed on the target node")
  469. parser.add_argument('--ip-dual-conf', type=str, dest='ip_dual_conf',
  470. help="Input the second IP address for dual-stack configuration (IPv6 if IP_CONF is IPv4, or IPv4 if IP_CONF is IPv6)")
  471. parser.add_argument('--enable-ipip', action='store_true', dest='enable_ipip',
  472. help="Enable IPIP mode for IPv4 (default: VXLAN mode for both IPv4 single-stack and dual-stack)")
  473. parser.add_argument('--offline-data-path', nargs='?',
  474. help="offline packages location")
  475. parser.add_argument('--dns', nargs='*', help='Space seperated DNS server(s), eg: --dns 1.1.1.1 8.8.8.8')
  476. pip_mirror_help = "specify pip mirror to install python packages smoothly"
  477. pip_mirror_suggest = "https://mirrors.aliyun.com/pypi/simple/"
  478. parser.add_argument('--pip-mirror', '-m', type=str, dest='pip_mirror',
  479. help=f"{pip_mirror_help}, e.g.: {pip_mirror_suggest}")
  480. parser.add_argument('--k8s-v115', action='store_true', default=False,
  481. help="Using old k8s v1.15 rather than k3s to manage the cluster. Default: False (using k3s)")
  482. parser.add_argument('--image-repository', '-i', type=str, dest='image_repository',
  483. default=consts.REGISTRY_ALI_YUNIONIO,
  484. help=f"Image repository for container images, e.g.: docker.io/yunion. Default: {consts.REGISTRY_ALI_YUNIONIO}")
  485. parser.add_argument('--region', type=str, dest='region',
  486. default=consts.DEFAULT_REGION_NAME,
  487. help=f"Default region name: {consts.DEFAULT_REGION_NAME}")
  488. parser.add_argument('--zone', type=str, dest='zone',
  489. default=consts.DEFAULT_ZONE_NAME,
  490. help=f"Default zone name: {consts.DEFAULT_ZONE_NAME}")
  491. def get_args():
  492. global parser
  493. parser = argparse.ArgumentParser()
  494. parser.add_argument('STACK', metavar="stack", type=str, nargs=1,
  495. help="Choose the product type from ['full', 'cmp', 'virt', 'light-virt', 'ai']",
  496. choices=['full', 'cmp', 'virt', 'light-virt', 'ai'])
  497. parser.add_argument('IP_CONF', metavar="ip_conf", type=str, nargs='?',
  498. help="Input the target IPv4 or Config file")
  499. # 添加共用参数
  500. inject_common_options(parser)
  501. # 添加 hostagent 和 runtime 选项
  502. inject_add_hostagent_options(parser)
  503. inject_add_nodes_runtime_options(parser)
  504. # 如果是 ai stack,添加 NVIDIA 相关参数
  505. # 注意:这里需要在解析后才能判断,所以参数总是添加,但只在 ai 模式下必需
  506. inject_ai_nvidia_options(parser)
  507. return parser.parse_args()
  508. def ensure_python3_yaml(os):
  509. username = get_username()
  510. if os == 'redhat':
  511. query = "sudo rpm -qa"
  512. installer = "yum"
  513. elif os == 'debian':
  514. if username == 'root':
  515. query = "dpkg -l"
  516. installer = "apt"
  517. else:
  518. query = "sudo dpkg -l"
  519. installer = "sudo apt"
  520. # subprocess.check_output(f"{installer} update -y", shell=True)
  521. else:
  522. print("OS not supported")
  523. exit(1)
  524. print(f'ensure_python3_yaml: os: {os}; query: {query}; installer: {installer}')
  525. output = subprocess.check_output(query, shell=True).decode('utf-8')
  526. if regex_search(r'python3.*pyyaml', output, ignore_case=True):
  527. print("PyYAML already installed")
  528. return
  529. output = subprocess.check_output(f"{installer} search yaml", shell=True).decode('utf-8')
  530. pkg = regex_search(r'python3\d?-(py)?yaml|PyYAML', output, ignore_case=True)
  531. if not pkg:
  532. print("No python3 package found")
  533. else:
  534. cmd = f"{installer} install -y {pkg}"
  535. print(f'command to run : [{cmd}]')
  536. subprocess.run(f"{installer} install -y {pkg}", shell=True)
  537. def get_default_ip(args):
  538. ip_conf = args.IP_CONF
  539. if ip_conf and len(ip_conf) > 0:
  540. return str(ip_conf)
  541. # find default ip address
  542. import socket
  543. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  544. s.connect(('8.8.8.8', 1)) # connect() for UDP doesn't send packets
  545. sockname = s.getsockname()
  546. local_ip_address = sockname[0]
  547. s.close()
  548. return local_ip_address
  549. def main():
  550. init_local_user_path()
  551. args = get_args()
  552. user_dns = []
  553. if args.dns:
  554. user_dns = [i for i in args.dns if is_valid_dns(i)]
  555. stack = args.STACK[0]
  556. ip_conf = get_default_ip(args)
  557. # 检测IP类型,支持双栈配置
  558. detect_and_display_ip_type(ip_conf, args.ip_dual_conf)
  559. os.chdir(os.path.dirname(os.path.realpath(__file__)))
  560. stackDict = {
  561. 'full': ocboot.KEY_STACK_FULLSTACK,
  562. 'cmp': ocboot.KEY_STACK_CMP,
  563. 'virt': ocboot.KEY_STACK_EDGE,
  564. 'light-virt': ocboot.KEY_STACK_LIGHT_EDGE,
  565. 'ai': ocboot.KEY_STACK_AI,
  566. }
  567. # 设置共同环境
  568. setup_common_environment(args)
  569. # 重新检测IP类型(因为上面的检测可能被覆盖)
  570. if args.ip_dual_conf:
  571. match_ip, ip_type = match_dual_stack_ipaddr(ip_conf, args.ip_dual_conf)
  572. else:
  573. match_ip, ip_type = match_ipaddr(ip_conf)
  574. # 处理 ai 模式
  575. is_ai_mode = (stack == 'ai')
  576. if is_ai_mode:
  577. # ai 模式自动使用 containerd runtime
  578. runtime = consts.RUNTIME_CONTAINERD
  579. pr_green("AI mode: Using containerd runtime and full stack")
  580. else:
  581. # 普通模式:使用用户指定的 runtime
  582. runtime = args.runtime
  583. # 生成配置文件
  584. if match_ip:
  585. conf = generate_config(ip_conf, stackDict.get(stack),
  586. user_dns, runtime,
  587. args.image_repository,
  588. args.region, args.zone,
  589. ip_dual_conf=args.ip_dual_conf,
  590. ip_type=ip_type,
  591. enable_ipip=args.enable_ipip)
  592. elif path.isfile(ip_conf) and path.getsize(ip_conf) > 0:
  593. conf = update_config(ip_conf, stackDict.get(stack), runtime)
  594. else:
  595. pr_red(f'The configuration file <{ip_conf}> does not exist or is not valid!')
  596. exit()
  597. check_env(ip_conf, pip_mirror=args.pip_mirror)
  598. # 检查目标节点是否已经部署了集群
  599. if not args.force:
  600. if check_cluster_deployed(ip_conf):
  601. pr_red(f"Error: A OneCloud cluster is already deployed on {ip_conf}.")
  602. sys.exit(1)
  603. # 如果是 ai 模式,设置 enable_ai_env,并根据是否提供参数决定是否传递 NVIDIA 变量
  604. extra_vars = None
  605. if is_ai_mode:
  606. extra_vars = {
  607. 'enable_ai_env': True,
  608. 'gpu_device_virtual_number': args.gpu_device_virtual_number,
  609. }
  610. # 只有在提供了 NVIDIA 相关参数时才添加这些变量
  611. if args.nvidia_driver_installer_path:
  612. extra_vars['nvidia_driver_installer_path'] = args.nvidia_driver_installer_path
  613. if args.cuda_installer_path:
  614. extra_vars['cuda_installer_path'] = args.cuda_installer_path
  615. if args.nvidia_driver_installer_path and args.cuda_installer_path:
  616. pr_green("Starting allinone installation with containerd runtime and AI environment setup (including NVIDIA driver installation)...")
  617. else:
  618. pr_green("Starting allinone installation with containerd runtime and AI environment setup (skipping NVIDIA GPU/driver/CUDA checks/installation and NVIDIA runtime configuration; assuming it is already set up)...")
  619. else:
  620. pr_green(f"Starting installation with {stack} stack...")
  621. return install.start(conf, extra_vars=extra_vars)
  622. def detect_and_display_ip_type(ip_conf, ip_dual_conf=None):
  623. """检测并显示 IP 地址类型"""
  624. if ip_dual_conf:
  625. match_ip, ip_type = match_dual_stack_ipaddr(ip_conf, ip_dual_conf)
  626. if match_ip:
  627. if ip_type == consts.IP_TYPE_DUAL_STACK:
  628. if _match_ip4addr(ip_conf):
  629. ipv4_addr = ip_conf
  630. ipv6_addr = ip_dual_conf
  631. else:
  632. ipv4_addr = ip_dual_conf
  633. ipv6_addr = ip_conf
  634. pr_green(f"choose dual-stack configuration: IPv4={ipv4_addr}, IPv6={ipv6_addr}")
  635. else:
  636. pr_green(f"choose {ip_type} address: {ip_conf}")
  637. else:
  638. pr_red(f"Invalid dual-stack configuration: {ip_conf}, {ip_dual_conf}")
  639. exit(1)
  640. return match_ip, ip_type
  641. else:
  642. match_ip, ip_type = match_ipaddr(ip_conf)
  643. if match_ip:
  644. pr_green(f"choose local {ip_type} address: {ip_conf}")
  645. return match_ip, ip_type
  646. def setup_common_environment(args):
  647. """设置共同的环境变量和处理离线数据路径"""
  648. # 设置环境变量
  649. if not args.k8s_v115:
  650. os.environ[consts.ENV_K3S] = consts.ENV_VAL_TRUE
  651. else:
  652. os.environ[consts.ENV_K8S_V115] = consts.ENV_VAL_TRUE
  653. # 处理离线数据路径
  654. offline_data_path = None
  655. if args.offline_data_path and os.path.isdir(args.offline_data_path):
  656. offline_data_path = os.path.realpath(args.offline_data_path)
  657. elif os.environ.get('OFFLINE_DATA_PATH') and os.path.isdir(os.environ.get('OFFLINE_DATA_PATH')):
  658. offline_data_path = os.path.realpath(os.environ.get('OFFLINE_DATA_PATH'))
  659. if offline_data_path:
  660. os.environ['OFFLINE_DATA_PATH'] = offline_data_path
  661. else:
  662. os.environ['OFFLINE_DATA_PATH'] = ''
  663. if os.system('test -x /usr/bin/apt') == 0:
  664. install_packages(['python3-pip'])
  665. ensure_python3_yaml('debian')
  666. elif os.system('test -x /usr/bin/yum') == 0:
  667. install_packages(['python3-pip'])
  668. os.system('python3 -m pip install pyyaml')
  669. ensure_python3_yaml('redhat')
  670. if __name__ == "__main__":
  671. sys.exit(main())