oo_config.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. # pylint: disable=bad-continuation,missing-docstring,no-self-use,invalid-name,too-many-instance-attributes,too-few-public-methods
  2. from __future__ import (absolute_import, print_function)
  3. import os
  4. import sys
  5. import logging
  6. import yaml
  7. from pkg_resources import resource_filename
  8. installer_log = logging.getLogger('installer')
  9. CONFIG_PERSIST_SETTINGS = [
  10. 'ansible_ssh_user',
  11. 'ansible_callback_facts_yaml',
  12. 'ansible_inventory_path',
  13. 'ansible_log_path',
  14. 'deployment',
  15. 'version',
  16. 'variant',
  17. 'variant_subtype',
  18. 'variant_version',
  19. ]
  20. DEPLOYMENT_VARIABLES_BLACKLIST = [
  21. 'hosts',
  22. 'roles',
  23. ]
  24. HOST_VARIABLES_BLACKLIST = [
  25. 'ip',
  26. 'public_ip',
  27. 'hostname',
  28. 'public_hostname',
  29. 'node_labels',
  30. 'containerized',
  31. 'preconfigured',
  32. 'schedulable',
  33. 'other_variables',
  34. 'roles',
  35. ]
  36. DEFAULT_REQUIRED_FACTS = ['ip', 'public_ip', 'hostname', 'public_hostname']
  37. PRECONFIGURED_REQUIRED_FACTS = ['hostname', 'public_hostname']
  38. def print_read_config_error(error, path='the configuration file'):
  39. message = """
  40. Error loading config. {}.
  41. See https://docs.openshift.com/enterprise/latest/install_config/install/quick_install.html#defining-an-installation-configuration-file
  42. for information on creating a configuration file or delete {} and re-run the installer.
  43. """
  44. print(message.format(error, path))
  45. class OOConfigFileError(Exception):
  46. """The provided config file path can't be read/written
  47. """
  48. pass
  49. class OOConfigInvalidHostError(Exception):
  50. """ Host in config is missing both ip and hostname. """
  51. pass
  52. class Host(object):
  53. """ A system we will or have installed OpenShift on. """
  54. def __init__(self, **kwargs):
  55. self.ip = kwargs.get('ip', None)
  56. self.hostname = kwargs.get('hostname', None)
  57. self.public_ip = kwargs.get('public_ip', None)
  58. self.public_hostname = kwargs.get('public_hostname', None)
  59. self.connect_to = kwargs.get('connect_to', None)
  60. self.preconfigured = kwargs.get('preconfigured', None)
  61. self.schedulable = kwargs.get('schedulable', None)
  62. self.new_host = kwargs.get('new_host', None)
  63. self.containerized = kwargs.get('containerized', False)
  64. self.node_labels = kwargs.get('node_labels', '')
  65. # allowable roles: master, node, etcd, storage, master_lb
  66. self.roles = kwargs.get('roles', [])
  67. self.other_variables = kwargs.get('other_variables', {})
  68. if self.connect_to is None:
  69. raise OOConfigInvalidHostError(
  70. "You must specify either an ip or hostname as 'connect_to'")
  71. def __str__(self):
  72. return self.connect_to
  73. def __repr__(self):
  74. return self.connect_to
  75. def to_dict(self):
  76. """ Used when exporting to yaml. """
  77. d = {}
  78. for prop in ['ip', 'hostname', 'public_ip', 'public_hostname', 'connect_to',
  79. 'preconfigured', 'containerized', 'schedulable', 'roles', 'node_labels', ]:
  80. # If the property is defined (not None or False), export it:
  81. if getattr(self, prop):
  82. d[prop] = getattr(self, prop)
  83. for variable, value in self.other_variables.items():
  84. d[variable] = value
  85. return d
  86. def is_master(self):
  87. return 'master' in self.roles
  88. def is_node(self):
  89. return 'node' in self.roles
  90. def is_master_lb(self):
  91. return 'master_lb' in self.roles
  92. def is_storage(self):
  93. return 'storage' in self.roles
  94. def is_etcd(self):
  95. """ Does this host have the etcd role """
  96. return 'etcd' in self.roles
  97. def is_dedicated_node(self):
  98. """ Will this host be a dedicated node. (not a master) """
  99. return self.is_node() and not self.is_master()
  100. def is_schedulable_node(self, all_hosts):
  101. """ Will this host be a node marked as schedulable. """
  102. if not self.is_node():
  103. return False
  104. if not self.is_master():
  105. return True
  106. masters = [host for host in all_hosts if host.is_master()]
  107. nodes = [host for host in all_hosts if host.is_node()]
  108. if len(masters) == len(nodes):
  109. return True
  110. return False
  111. class Role(object):
  112. """ A role that will be applied to a host. """
  113. def __init__(self, name, variables):
  114. self.name = name
  115. self.variables = variables
  116. def __str__(self):
  117. return self.name
  118. def __repr__(self):
  119. return self.name
  120. def to_dict(self):
  121. """ Used when exporting to yaml. """
  122. d = {}
  123. for prop in ['name', 'variables']:
  124. # If the property is defined (not None or False), export it:
  125. if getattr(self, prop):
  126. d[prop] = getattr(self, prop)
  127. return d
  128. class Deployment(object):
  129. def __init__(self, **kwargs):
  130. self.hosts = kwargs.get('hosts', [])
  131. self.roles = kwargs.get('roles', {})
  132. self.variables = kwargs.get('variables', {})
  133. class OOConfig(object):
  134. default_dir = os.path.normpath(
  135. os.environ.get('XDG_CONFIG_HOME',
  136. os.environ.get('HOME', '') + '/.config/') + '/openshift/')
  137. default_file = '/installer.cfg.yml'
  138. def __init__(self, config_path):
  139. if config_path:
  140. self.config_path = os.path.normpath(config_path)
  141. else:
  142. self.config_path = os.path.normpath(self.default_dir +
  143. self.default_file)
  144. self.deployment = Deployment(hosts=[], roles={}, variables={})
  145. self.settings = {}
  146. self._read_config()
  147. self._set_defaults()
  148. # pylint: disable=too-many-branches
  149. # Lots of different checks ran in a single method, could
  150. # use a little refactoring-love some time
  151. def _read_config(self):
  152. installer_log.debug("Attempting to read the OO Config")
  153. try:
  154. installer_log.debug("Attempting to see if the provided config file exists: %s", self.config_path)
  155. if os.path.exists(self.config_path):
  156. installer_log.debug("We think the config file exists: %s", self.config_path)
  157. with open(self.config_path, 'r') as cfgfile:
  158. loaded_config = yaml.safe_load(cfgfile.read())
  159. if 'version' not in loaded_config:
  160. print_read_config_error('Legacy configuration file found', self.config_path)
  161. sys.exit(0)
  162. if loaded_config.get('version', '') == 'v1':
  163. loaded_config = self._upgrade_v1_config(loaded_config)
  164. try:
  165. host_list = loaded_config['deployment']['hosts']
  166. role_list = loaded_config['deployment']['roles']
  167. except KeyError as e:
  168. print_read_config_error("No such key: {}".format(e), self.config_path)
  169. sys.exit(0)
  170. for setting in CONFIG_PERSIST_SETTINGS:
  171. persisted_value = loaded_config.get(setting)
  172. if persisted_value is not None:
  173. self.settings[setting] = str(persisted_value)
  174. # We've loaded any persisted configs, let's verify any
  175. # paths which are required for a correct and complete
  176. # install
  177. # - ansible_callback_facts_yaml - Settings from a
  178. # pervious run. If the file doesn't exist then we
  179. # will just warn about it for now and recollect the
  180. # facts.
  181. if self.settings.get('ansible_callback_facts_yaml', None) is not None:
  182. if not os.path.exists(self.settings['ansible_callback_facts_yaml']):
  183. # Cached callback facts file does not exist
  184. installer_log.warning("The specified 'ansible_callback_facts_yaml'"
  185. "file does not exist (%s)",
  186. self.settings['ansible_callback_facts_yaml'])
  187. installer_log.debug("Remote system facts will be collected again later")
  188. self.settings.pop('ansible_callback_facts_yaml')
  189. for setting in loaded_config['deployment']:
  190. try:
  191. if setting not in DEPLOYMENT_VARIABLES_BLACKLIST:
  192. self.deployment.variables[setting] = \
  193. str(loaded_config['deployment'][setting])
  194. except KeyError:
  195. continue
  196. # Parse the hosts into DTO objects:
  197. for host in host_list:
  198. host['other_variables'] = {}
  199. for variable, value in host.items():
  200. if variable not in HOST_VARIABLES_BLACKLIST:
  201. host['other_variables'][variable] = value
  202. self.deployment.hosts.append(Host(**host))
  203. # Parse the roles into Objects
  204. for name, variables in role_list.items():
  205. self.deployment.roles.update({name: Role(name, variables)})
  206. except IOError as ferr:
  207. raise OOConfigFileError('Cannot open config file "{}": {}'.format(ferr.filename,
  208. ferr.strerror))
  209. except yaml.scanner.ScannerError:
  210. raise OOConfigFileError(
  211. 'Config file "{}" is not a valid YAML document'.format(self.config_path))
  212. installer_log.debug("Parsed the config file")
  213. def _upgrade_v1_config(self, config):
  214. new_config_data = {}
  215. new_config_data['deployment'] = {}
  216. new_config_data['deployment']['hosts'] = []
  217. new_config_data['deployment']['roles'] = {}
  218. new_config_data['deployment']['variables'] = {}
  219. role_list = {}
  220. if config.get('ansible_ssh_user', False):
  221. new_config_data['deployment']['ansible_ssh_user'] = config['ansible_ssh_user']
  222. if config.get('variant', False):
  223. new_config_data['variant'] = config['variant']
  224. if config.get('variant_version', False):
  225. new_config_data['variant_version'] = config['variant_version']
  226. for host in config['hosts']:
  227. host_props = {}
  228. host_props['roles'] = []
  229. host_props['connect_to'] = host['connect_to']
  230. for prop in ['ip', 'public_ip', 'hostname', 'public_hostname', 'containerized', 'preconfigured']:
  231. host_props[prop] = host.get(prop, None)
  232. for role in ['master', 'node', 'master_lb', 'storage', 'etcd']:
  233. if host.get(role, False):
  234. host_props['roles'].append(role)
  235. role_list[role] = ''
  236. new_config_data['deployment']['hosts'].append(host_props)
  237. new_config_data['deployment']['roles'] = role_list
  238. return new_config_data
  239. def _set_defaults(self):
  240. installer_log.debug("Setting defaults, current OOConfig settings: %s", self.settings)
  241. if 'ansible_inventory_directory' not in self.settings:
  242. self.settings['ansible_inventory_directory'] = self._default_ansible_inv_dir()
  243. if not os.path.exists(self.settings['ansible_inventory_directory']):
  244. installer_log.debug("'ansible_inventory_directory' does not exist, "
  245. "creating it now (%s)",
  246. self.settings['ansible_inventory_directory'])
  247. os.makedirs(self.settings['ansible_inventory_directory'])
  248. else:
  249. installer_log.debug("We think this 'ansible_inventory_directory' "
  250. "is OK: %s",
  251. self.settings['ansible_inventory_directory'])
  252. if 'ansible_plugins_directory' not in self.settings:
  253. self.settings['ansible_plugins_directory'] = \
  254. resource_filename(__name__, 'ansible_plugins')
  255. installer_log.debug("We think the ansible plugins directory should be: %s (it is not already set)",
  256. self.settings['ansible_plugins_directory'])
  257. else:
  258. installer_log.debug("The ansible plugins directory is already set: %s",
  259. self.settings['ansible_plugins_directory'])
  260. if 'version' not in self.settings:
  261. self.settings['version'] = 'v2'
  262. if 'ansible_callback_facts_yaml' not in self.settings:
  263. installer_log.debug("No 'ansible_callback_facts_yaml' in self.settings")
  264. self.settings['ansible_callback_facts_yaml'] = '%s/callback_facts.yaml' % \
  265. self.settings['ansible_inventory_directory']
  266. installer_log.debug("Value: %s", self.settings['ansible_callback_facts_yaml'])
  267. else:
  268. installer_log.debug("'ansible_callback_facts_yaml' already set "
  269. "in self.settings: %s",
  270. self.settings['ansible_callback_facts_yaml'])
  271. if 'ansible_ssh_user' not in self.settings:
  272. self.settings['ansible_ssh_user'] = ''
  273. self.settings['ansible_inventory_path'] = \
  274. '{}/hosts'.format(os.path.dirname(self.config_path))
  275. # clean up any empty sets
  276. empty_keys = []
  277. for setting in self.settings:
  278. if not self.settings[setting]:
  279. empty_keys.append(setting)
  280. for key in empty_keys:
  281. self.settings.pop(key)
  282. installer_log.debug("Updated OOConfig settings: %s", self.settings)
  283. def _default_ansible_inv_dir(self):
  284. return os.path.normpath(
  285. os.path.dirname(self.config_path) + "/.ansible")
  286. def calc_missing_facts(self):
  287. """
  288. Determine which host facts are not defined in the config.
  289. Returns a hash of host to a list of the missing facts.
  290. """
  291. result = {}
  292. for host in self.deployment.hosts:
  293. missing_facts = []
  294. if host.preconfigured:
  295. required_facts = PRECONFIGURED_REQUIRED_FACTS
  296. else:
  297. required_facts = DEFAULT_REQUIRED_FACTS
  298. for required_fact in required_facts:
  299. if not getattr(host, required_fact):
  300. missing_facts.append(required_fact)
  301. if len(missing_facts) > 0:
  302. result[host.connect_to] = missing_facts
  303. return result
  304. def save_to_disk(self):
  305. out_file = open(self.config_path, 'w')
  306. out_file.write(self.yaml())
  307. out_file.close()
  308. def persist_settings(self):
  309. p_settings = {}
  310. for setting in CONFIG_PERSIST_SETTINGS:
  311. if setting in self.settings and self.settings[setting]:
  312. p_settings[setting] = self.settings[setting]
  313. p_settings['deployment'] = {}
  314. p_settings['deployment']['hosts'] = []
  315. p_settings['deployment']['roles'] = {}
  316. for host in self.deployment.hosts:
  317. p_settings['deployment']['hosts'].append(host.to_dict())
  318. for name, role in self.deployment.roles.items():
  319. p_settings['deployment']['roles'][name] = role.variables
  320. for setting in self.deployment.variables:
  321. if setting not in DEPLOYMENT_VARIABLES_BLACKLIST:
  322. p_settings['deployment'][setting] = self.deployment.variables[setting]
  323. try:
  324. p_settings['variant'] = self.settings['variant']
  325. p_settings['variant_version'] = self.settings['variant_version']
  326. if self.settings['ansible_inventory_directory'] != self._default_ansible_inv_dir():
  327. p_settings['ansible_inventory_directory'] = self.settings['ansible_inventory_directory']
  328. except KeyError as e:
  329. print("Error persisting settings: {}".format(e))
  330. sys.exit(0)
  331. return p_settings
  332. def yaml(self):
  333. return yaml.safe_dump(self.persist_settings(), default_flow_style=False)
  334. def __str__(self):
  335. return self.yaml()
  336. def get_host_roles_set(self):
  337. roles_set = set()
  338. for host in self.deployment.hosts:
  339. for role in host.roles:
  340. roles_set.add(role)
  341. return roles_set