openshift_facts.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. # vim: expandtab:tabstop=4:shiftwidth=4
  4. """Ansible module for retrieving and setting openshift related facts"""
  5. DOCUMENTATION = '''
  6. ---
  7. module: openshift_facts
  8. short_description: Cluster Facts
  9. author: Jason DeTiberus
  10. requirements: [ ]
  11. '''
  12. EXAMPLES = '''
  13. '''
  14. import ConfigParser
  15. import copy
  16. import os
  17. from distutils.util import strtobool
  18. def hostname_valid(hostname):
  19. """ Test if specified hostname should be considered valid
  20. Args:
  21. hostname (str): hostname to test
  22. Returns:
  23. bool: True if valid, otherwise False
  24. """
  25. if (not hostname or
  26. hostname.startswith('localhost') or
  27. hostname.endswith('localdomain') or
  28. len(hostname.split('.')) < 2):
  29. return False
  30. return True
  31. def choose_hostname(hostnames=None, fallback=''):
  32. """ Choose a hostname from the provided hostnames
  33. Given a list of hostnames and a fallback value, choose a hostname to
  34. use. This function will prefer fqdns if they exist (excluding any that
  35. begin with localhost or end with localdomain) over ip addresses.
  36. Args:
  37. hostnames (list): list of hostnames
  38. fallback (str): default value to set if hostnames does not contain
  39. a valid hostname
  40. Returns:
  41. str: chosen hostname
  42. """
  43. hostname = fallback
  44. if hostnames is None:
  45. return hostname
  46. ip_regex = r'\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z'
  47. ips = [i for i in hostnames
  48. if (i is not None and isinstance(i, basestring)
  49. and re.match(ip_regex, i))]
  50. hosts = [i for i in hostnames
  51. if i is not None and i != '' and i not in ips]
  52. for host_list in (hosts, ips):
  53. for host in host_list:
  54. if hostname_valid(host):
  55. return host
  56. return hostname
  57. def query_metadata(metadata_url, headers=None, expect_json=False):
  58. """ Return metadata from the provided metadata_url
  59. Args:
  60. metadata_url (str): metadata url
  61. headers (dict): headers to set for metadata request
  62. expect_json (bool): does the metadata_url return json
  63. Returns:
  64. dict or list: metadata request result
  65. """
  66. result, info = fetch_url(module, metadata_url, headers=headers)
  67. if info['status'] != 200:
  68. raise OpenShiftFactsMetadataUnavailableError("Metadata unavailable")
  69. if expect_json:
  70. return module.from_json(result.read())
  71. else:
  72. return [line.strip() for line in result.readlines()]
  73. def walk_metadata(metadata_url, headers=None, expect_json=False):
  74. """ Walk the metadata tree and return a dictionary of the entire tree
  75. Args:
  76. metadata_url (str): metadata url
  77. headers (dict): headers to set for metadata request
  78. expect_json (bool): does the metadata_url return json
  79. Returns:
  80. dict: the result of walking the metadata tree
  81. """
  82. metadata = dict()
  83. for line in query_metadata(metadata_url, headers, expect_json):
  84. if line.endswith('/') and not line == 'public-keys/':
  85. key = line[:-1]
  86. metadata[key] = walk_metadata(metadata_url + line,
  87. headers, expect_json)
  88. else:
  89. results = query_metadata(metadata_url + line, headers,
  90. expect_json)
  91. if len(results) == 1:
  92. # disable pylint maybe-no-member because overloaded use of
  93. # the module name causes pylint to not detect that results
  94. # is an array or hash
  95. # pylint: disable=maybe-no-member
  96. metadata[line] = results.pop()
  97. else:
  98. metadata[line] = results
  99. return metadata
  100. def get_provider_metadata(metadata_url, supports_recursive=False,
  101. headers=None, expect_json=False):
  102. """ Retrieve the provider metadata
  103. Args:
  104. metadata_url (str): metadata url
  105. supports_recursive (bool): does the provider metadata api support
  106. recursion
  107. headers (dict): headers to set for metadata request
  108. expect_json (bool): does the metadata_url return json
  109. Returns:
  110. dict: the provider metadata
  111. """
  112. try:
  113. if supports_recursive:
  114. metadata = query_metadata(metadata_url, headers,
  115. expect_json)
  116. else:
  117. metadata = walk_metadata(metadata_url, headers,
  118. expect_json)
  119. except OpenShiftFactsMetadataUnavailableError:
  120. metadata = None
  121. return metadata
  122. def normalize_gce_facts(metadata, facts):
  123. """ Normalize gce facts
  124. Args:
  125. metadata (dict): provider metadata
  126. facts (dict): facts to update
  127. Returns:
  128. dict: the result of adding the normalized metadata to the provided
  129. facts dict
  130. """
  131. for interface in metadata['instance']['networkInterfaces']:
  132. int_info = dict(ips=[interface['ip']], network_type='gce')
  133. int_info['public_ips'] = [ac['externalIp'] for ac
  134. in interface['accessConfigs']]
  135. int_info['public_ips'].extend(interface['forwardedIps'])
  136. _, _, network_id = interface['network'].rpartition('/')
  137. int_info['network_id'] = network_id
  138. facts['network']['interfaces'].append(int_info)
  139. _, _, zone = metadata['instance']['zone'].rpartition('/')
  140. facts['zone'] = zone
  141. # Default to no sdn for GCE deployments
  142. facts['use_openshift_sdn'] = False
  143. # GCE currently only supports a single interface
  144. facts['network']['ip'] = facts['network']['interfaces'][0]['ips'][0]
  145. pub_ip = facts['network']['interfaces'][0]['public_ips'][0]
  146. facts['network']['public_ip'] = pub_ip
  147. facts['network']['hostname'] = metadata['instance']['hostname']
  148. # TODO: attempt to resolve public_hostname
  149. facts['network']['public_hostname'] = facts['network']['public_ip']
  150. return facts
  151. def normalize_aws_facts(metadata, facts):
  152. """ Normalize aws facts
  153. Args:
  154. metadata (dict): provider metadata
  155. facts (dict): facts to update
  156. Returns:
  157. dict: the result of adding the normalized metadata to the provided
  158. facts dict
  159. """
  160. for interface in sorted(
  161. metadata['network']['interfaces']['macs'].values(),
  162. key=lambda x: x['device-number']
  163. ):
  164. int_info = dict()
  165. var_map = {'ips': 'local-ipv4s', 'public_ips': 'public-ipv4s'}
  166. for ips_var, int_var in var_map.iteritems():
  167. ips = interface.get(int_var)
  168. if isinstance(ips, basestring):
  169. int_info[ips_var] = [ips]
  170. else:
  171. int_info[ips_var] = ips
  172. if 'vpc-id' in interface:
  173. int_info['network_type'] = 'vpc'
  174. else:
  175. int_info['network_type'] = 'classic'
  176. if int_info['network_type'] == 'vpc':
  177. int_info['network_id'] = interface['subnet-id']
  178. else:
  179. int_info['network_id'] = None
  180. facts['network']['interfaces'].append(int_info)
  181. facts['zone'] = metadata['placement']['availability-zone']
  182. # TODO: actually attempt to determine default local and public ips
  183. # by using the ansible default ip fact and the ipv4-associations
  184. # from the ec2 metadata
  185. facts['network']['ip'] = metadata.get('local-ipv4')
  186. facts['network']['public_ip'] = metadata.get('public-ipv4')
  187. # TODO: verify that local hostname makes sense and is resolvable
  188. facts['network']['hostname'] = metadata.get('local-hostname')
  189. # TODO: verify that public hostname makes sense and is resolvable
  190. facts['network']['public_hostname'] = metadata.get('public-hostname')
  191. return facts
  192. def normalize_openstack_facts(metadata, facts):
  193. """ Normalize openstack facts
  194. Args:
  195. metadata (dict): provider metadata
  196. facts (dict): facts to update
  197. Returns:
  198. dict: the result of adding the normalized metadata to the provided
  199. facts dict
  200. """
  201. # openstack ec2 compat api does not support network interfaces and
  202. # the version tested on did not include the info in the openstack
  203. # metadata api, should be updated if neutron exposes this.
  204. facts['zone'] = metadata['availability_zone']
  205. local_ipv4 = metadata['ec2_compat']['local-ipv4'].split(',')[0]
  206. facts['network']['ip'] = local_ipv4
  207. facts['network']['public_ip'] = metadata['ec2_compat']['public-ipv4']
  208. # TODO: verify local hostname makes sense and is resolvable
  209. facts['network']['hostname'] = metadata['hostname']
  210. # TODO: verify that public hostname makes sense and is resolvable
  211. pub_h = metadata['ec2_compat']['public-hostname']
  212. facts['network']['public_hostname'] = pub_h
  213. return facts
  214. def normalize_provider_facts(provider, metadata):
  215. """ Normalize provider facts
  216. Args:
  217. provider (str): host provider
  218. metadata (dict): provider metadata
  219. Returns:
  220. dict: the normalized provider facts
  221. """
  222. if provider is None or metadata is None:
  223. return {}
  224. # TODO: test for ipv6_enabled where possible (gce, aws do not support)
  225. # and configure ipv6 facts if available
  226. # TODO: add support for setting user_data if available
  227. facts = dict(name=provider, metadata=metadata,
  228. network=dict(interfaces=[], ipv6_enabled=False))
  229. if provider == 'gce':
  230. facts = normalize_gce_facts(metadata, facts)
  231. elif provider == 'ec2':
  232. facts = normalize_aws_facts(metadata, facts)
  233. elif provider == 'openstack':
  234. facts = normalize_openstack_facts(metadata, facts)
  235. return facts
  236. def set_fluentd_facts_if_unset(facts):
  237. """ Set fluentd facts if not already present in facts dict
  238. dict: the facts dict updated with the generated fluentd facts if
  239. missing
  240. Args:
  241. facts (dict): existing facts
  242. Returns:
  243. dict: the facts dict updated with the generated fluentd
  244. facts if they were not already present
  245. """
  246. if 'common' in facts:
  247. if 'use_fluentd' not in facts['common']:
  248. use_fluentd = False
  249. facts['common']['use_fluentd'] = use_fluentd
  250. return facts
  251. def set_node_schedulability(facts):
  252. """ Set schedulable facts if not already present in facts dict
  253. Args:
  254. facts (dict): existing facts
  255. Returns:
  256. dict: the facts dict updated with the generated schedulable
  257. facts if they were not already present
  258. """
  259. if 'node' in facts:
  260. if 'schedulable' not in facts['node']:
  261. if 'master' in facts:
  262. facts['node']['schedulable'] = False
  263. else:
  264. facts['node']['schedulable'] = True
  265. return facts
  266. def set_metrics_facts_if_unset(facts):
  267. """ Set cluster metrics facts if not already present in facts dict
  268. dict: the facts dict updated with the generated cluster metrics facts if
  269. missing
  270. Args:
  271. facts (dict): existing facts
  272. Returns:
  273. dict: the facts dict updated with the generated cluster metrics
  274. facts if they were not already present
  275. """
  276. if 'common' in facts:
  277. if 'use_cluster_metrics' not in facts['common']:
  278. use_cluster_metrics = False
  279. facts['common']['use_cluster_metrics'] = use_cluster_metrics
  280. return facts
  281. def set_identity_providers_if_unset(facts):
  282. """ Set identity_providers fact if not already present in facts dict
  283. Args:
  284. facts (dict): existing facts
  285. Returns:
  286. dict: the facts dict updated with the generated identity providers
  287. facts if they were not already present
  288. """
  289. if 'master' in facts:
  290. deployment_type = facts['common']['deployment_type']
  291. if 'identity_providers' not in facts['master']:
  292. identity_provider = dict(
  293. name='allow_all', challenge=True, login=True,
  294. kind='AllowAllPasswordIdentityProvider'
  295. )
  296. if deployment_type == 'enterprise':
  297. identity_provider = dict(
  298. name='deny_all', challenge=True, login=True,
  299. kind='DenyAllPasswordIdentityProvider'
  300. )
  301. facts['master']['identity_providers'] = [identity_provider]
  302. return facts
  303. def set_url_facts_if_unset(facts):
  304. """ Set url facts if not already present in facts dict
  305. Args:
  306. facts (dict): existing facts
  307. Returns:
  308. dict: the facts dict updated with the generated url facts if they
  309. were not already present
  310. """
  311. if 'master' in facts:
  312. api_use_ssl = facts['master']['api_use_ssl']
  313. api_port = facts['master']['api_port']
  314. console_use_ssl = facts['master']['console_use_ssl']
  315. console_port = facts['master']['console_port']
  316. console_path = facts['master']['console_path']
  317. etcd_use_ssl = facts['master']['etcd_use_ssl']
  318. etcd_hosts = facts['master']['etcd_hosts']
  319. etcd_port = facts['master']['etcd_port']
  320. hostname = facts['common']['hostname']
  321. public_hostname = facts['common']['public_hostname']
  322. cluster_hostname = facts['master'].get('cluster_hostname')
  323. cluster_public_hostname = facts['master'].get('cluster_public_hostname')
  324. if 'etcd_urls' not in facts['master']:
  325. etcd_urls = []
  326. if etcd_hosts != '':
  327. facts['master']['etcd_port'] = etcd_port
  328. facts['master']['embedded_etcd'] = False
  329. for host in etcd_hosts:
  330. etcd_urls.append(format_url(etcd_use_ssl, host,
  331. etcd_port))
  332. else:
  333. etcd_urls = [format_url(etcd_use_ssl, hostname,
  334. etcd_port)]
  335. facts['master']['etcd_urls'] = etcd_urls
  336. if 'api_url' not in facts['master']:
  337. api_hostname = cluster_hostname if cluster_hostname else hostname
  338. facts['master']['api_url'] = format_url(api_use_ssl, api_hostname,
  339. api_port)
  340. if 'public_api_url' not in facts['master']:
  341. api_public_hostname = cluster_public_hostname if cluster_public_hostname else public_hostname
  342. facts['master']['public_api_url'] = format_url(api_use_ssl,
  343. api_public_hostname,
  344. api_port)
  345. if 'console_url' not in facts['master']:
  346. console_hostname = cluster_hostname if cluster_hostname else hostname
  347. facts['master']['console_url'] = format_url(console_use_ssl,
  348. console_hostname,
  349. console_port,
  350. console_path)
  351. if 'public_console_url' not in facts['master']:
  352. console_public_hostname = cluster_public_hostname if cluster_public_hostname else public_hostname
  353. facts['master']['public_console_url'] = format_url(console_use_ssl,
  354. console_public_hostname,
  355. console_port,
  356. console_path)
  357. return facts
  358. def set_aggregate_facts(facts):
  359. """ Set aggregate facts
  360. Args:
  361. facts (dict): existing facts
  362. Returns:
  363. dict: the facts dict updated with aggregated facts
  364. """
  365. all_hostnames = set()
  366. if 'common' in facts:
  367. all_hostnames.add(facts['common']['hostname'])
  368. all_hostnames.add(facts['common']['public_hostname'])
  369. if 'master' in facts:
  370. if 'cluster_hostname' in facts['master']:
  371. all_hostnames.add(facts['master']['cluster_hostname'])
  372. if 'cluster_public_hostname' in facts['master']:
  373. all_hostnames.add(facts['master']['cluster_public_hostname'])
  374. facts['common']['all_hostnames'] = list(all_hostnames)
  375. return facts
  376. def set_deployment_facts_if_unset(facts):
  377. """ Set Facts that vary based on deployment_type. This currently
  378. includes common.service_type, common.config_base, master.registry_url,
  379. node.registry_url
  380. Args:
  381. facts (dict): existing facts
  382. Returns:
  383. dict: the facts dict updated with the generated deployment_type
  384. facts
  385. """
  386. # Perhaps re-factor this as a map?
  387. # pylint: disable=too-many-branches
  388. if 'common' in facts:
  389. deployment_type = facts['common']['deployment_type']
  390. if 'service_type' not in facts['common']:
  391. service_type = 'atomic-openshift'
  392. if deployment_type == 'origin':
  393. service_type = 'origin'
  394. elif deployment_type in ['enterprise', 'online']:
  395. service_type = 'openshift'
  396. facts['common']['service_type'] = service_type
  397. if 'config_base' not in facts['common']:
  398. config_base = '/etc/origin'
  399. if deployment_type in ['enterprise', 'online']:
  400. config_base = '/etc/openshift'
  401. facts['common']['config_base'] = config_base
  402. if 'data_dir' not in facts['common']:
  403. data_dir = '/var/lib/origin'
  404. if deployment_type in ['enterprise', 'online']:
  405. data_dir = '/var/lib/openshift'
  406. facts['common']['data_dir'] = data_dir
  407. facts['common']['version'] = get_openshift_version()
  408. for role in ('master', 'node'):
  409. if role in facts:
  410. deployment_type = facts['common']['deployment_type']
  411. if 'registry_url' not in facts[role]:
  412. registry_url = 'aos3/aos-${component}:${version}'
  413. if deployment_type in ['enterprise', 'online']:
  414. registry_url = 'openshift3/ose-${component}:${version}'
  415. elif deployment_type == 'origin':
  416. registry_url = 'openshift/origin-${component}:${version}'
  417. facts[role]['registry_url'] = registry_url
  418. return facts
  419. def set_sdn_facts_if_unset(facts):
  420. """ Set sdn facts if not already present in facts dict
  421. Args:
  422. facts (dict): existing facts
  423. Returns:
  424. dict: the facts dict updated with the generated sdn facts if they
  425. were not already present
  426. """
  427. if 'common' in facts:
  428. use_sdn = facts['common']['use_openshift_sdn']
  429. if not (use_sdn == '' or isinstance(use_sdn, bool)):
  430. facts['common']['use_openshift_sdn'] = bool(strtobool(str(use_sdn)))
  431. if 'sdn_network_plugin_name' not in facts['common']:
  432. plugin = 'redhat/openshift-ovs-subnet' if use_sdn else ''
  433. facts['common']['sdn_network_plugin_name'] = plugin
  434. if 'master' in facts:
  435. if 'sdn_cluster_network_cidr' not in facts['master']:
  436. facts['master']['sdn_cluster_network_cidr'] = '10.1.0.0/16'
  437. if 'sdn_host_subnet_length' not in facts['master']:
  438. facts['master']['sdn_host_subnet_length'] = '8'
  439. if 'node' in facts:
  440. if 'sdn_mtu' not in facts['node']:
  441. facts['node']['sdn_mtu'] = '1450'
  442. return facts
  443. def format_url(use_ssl, hostname, port, path=''):
  444. """ Format url based on ssl flag, hostname, port and path
  445. Args:
  446. use_ssl (bool): is ssl enabled
  447. hostname (str): hostname
  448. port (str): port
  449. path (str): url path
  450. Returns:
  451. str: The generated url string
  452. """
  453. scheme = 'https' if use_ssl else 'http'
  454. netloc = hostname
  455. if (use_ssl and port != '443') or (not use_ssl and port != '80'):
  456. netloc += ":%s" % port
  457. return urlparse.urlunparse((scheme, netloc, path, '', '', ''))
  458. def get_current_config(facts):
  459. """ Get current openshift config
  460. Args:
  461. facts (dict): existing facts
  462. Returns:
  463. dict: the facts dict updated with the current openshift config
  464. """
  465. current_config = dict()
  466. roles = [role for role in facts if role not in ['common', 'provider']]
  467. for role in roles:
  468. if 'roles' in current_config:
  469. current_config['roles'].append(role)
  470. else:
  471. current_config['roles'] = [role]
  472. # TODO: parse the /etc/sysconfig/openshift-{master,node} config to
  473. # determine the location of files.
  474. # TODO: I suspect this isn't working right now, but it doesn't prevent
  475. # anything from working properly as far as I can tell, perhaps because
  476. # we override the kubeconfig path everywhere we use it?
  477. # Query kubeconfig settings
  478. kubeconfig_dir = '/var/lib/origin/openshift.local.certificates'
  479. if role == 'node':
  480. kubeconfig_dir = os.path.join(
  481. kubeconfig_dir, "node-%s" % facts['common']['hostname']
  482. )
  483. kubeconfig_path = os.path.join(kubeconfig_dir, '.kubeconfig')
  484. if (os.path.isfile('/usr/bin/openshift')
  485. and os.path.isfile(kubeconfig_path)):
  486. try:
  487. _, output, _ = module.run_command(
  488. ["/usr/bin/openshift", "ex", "config", "view", "-o",
  489. "json", "--kubeconfig=%s" % kubeconfig_path],
  490. check_rc=False
  491. )
  492. config = json.loads(output)
  493. cad = 'certificate-authority-data'
  494. try:
  495. for cluster in config['clusters']:
  496. config['clusters'][cluster][cad] = 'masked'
  497. except KeyError:
  498. pass
  499. try:
  500. for user in config['users']:
  501. config['users'][user][cad] = 'masked'
  502. config['users'][user]['client-key-data'] = 'masked'
  503. except KeyError:
  504. pass
  505. current_config['kubeconfig'] = config
  506. # override pylint broad-except warning, since we do not want
  507. # to bubble up any exceptions if oc config view
  508. # fails
  509. # pylint: disable=broad-except
  510. except Exception:
  511. pass
  512. return current_config
  513. def get_openshift_version():
  514. """ Get current version of openshift on the host
  515. Returns:
  516. version: the current openshift version
  517. """
  518. if os.path.isfile('/usr/bin/openshift'):
  519. _, output, _ = module.run_command(['/usr/bin/openshift', 'version'])
  520. versions = dict(e.split(' v') for e in output.splitlines())
  521. version = versions.get('openshift', '')
  522. #TODO: acknowledge the possility of a containerized install
  523. return version
  524. def apply_provider_facts(facts, provider_facts):
  525. """ Apply provider facts to supplied facts dict
  526. Args:
  527. facts (dict): facts dict to update
  528. provider_facts (dict): provider facts to apply
  529. roles: host roles
  530. Returns:
  531. dict: the merged facts
  532. """
  533. if not provider_facts:
  534. return facts
  535. use_openshift_sdn = provider_facts.get('use_openshift_sdn')
  536. if isinstance(use_openshift_sdn, bool):
  537. facts['common']['use_openshift_sdn'] = use_openshift_sdn
  538. common_vars = [('hostname', 'ip'), ('public_hostname', 'public_ip')]
  539. for h_var, ip_var in common_vars:
  540. ip_value = provider_facts['network'].get(ip_var)
  541. if ip_value:
  542. facts['common'][ip_var] = ip_value
  543. facts['common'][h_var] = choose_hostname(
  544. [provider_facts['network'].get(h_var)],
  545. facts['common'][ip_var]
  546. )
  547. facts['provider'] = provider_facts
  548. return facts
  549. def merge_facts(orig, new):
  550. """ Recursively merge facts dicts
  551. Args:
  552. orig (dict): existing facts
  553. new (dict): facts to update
  554. Returns:
  555. dict: the merged facts
  556. """
  557. facts = dict()
  558. for key, value in orig.iteritems():
  559. if key in new:
  560. if isinstance(value, dict) and isinstance(new[key], dict):
  561. facts[key] = merge_facts(value, new[key])
  562. else:
  563. facts[key] = copy.copy(new[key])
  564. else:
  565. facts[key] = copy.deepcopy(value)
  566. new_keys = set(new.keys()) - set(orig.keys())
  567. for key in new_keys:
  568. facts[key] = copy.deepcopy(new[key])
  569. return facts
  570. def save_local_facts(filename, facts):
  571. """ Save local facts
  572. Args:
  573. filename (str): local facts file
  574. facts (dict): facts to set
  575. """
  576. try:
  577. fact_dir = os.path.dirname(filename)
  578. if not os.path.exists(fact_dir):
  579. os.makedirs(fact_dir)
  580. with open(filename, 'w') as fact_file:
  581. fact_file.write(module.jsonify(facts))
  582. except (IOError, OSError) as ex:
  583. raise OpenShiftFactsFileWriteError(
  584. "Could not create fact file: %s, error: %s" % (filename, ex)
  585. )
  586. def get_local_facts_from_file(filename):
  587. """ Retrieve local facts from fact file
  588. Args:
  589. filename (str): local facts file
  590. Returns:
  591. dict: the retrieved facts
  592. """
  593. local_facts = dict()
  594. try:
  595. # Handle conversion of INI style facts file to json style
  596. ini_facts = ConfigParser.SafeConfigParser()
  597. ini_facts.read(filename)
  598. for section in ini_facts.sections():
  599. local_facts[section] = dict()
  600. for key, value in ini_facts.items(section):
  601. local_facts[section][key] = value
  602. except (ConfigParser.MissingSectionHeaderError,
  603. ConfigParser.ParsingError):
  604. try:
  605. with open(filename, 'r') as facts_file:
  606. local_facts = json.load(facts_file)
  607. except (ValueError, IOError):
  608. pass
  609. return local_facts
  610. class OpenShiftFactsUnsupportedRoleError(Exception):
  611. """Origin Facts Unsupported Role Error"""
  612. pass
  613. class OpenShiftFactsFileWriteError(Exception):
  614. """Origin Facts File Write Error"""
  615. pass
  616. class OpenShiftFactsMetadataUnavailableError(Exception):
  617. """Origin Facts Metadata Unavailable Error"""
  618. pass
  619. class OpenShiftFacts(object):
  620. """ Origin Facts
  621. Attributes:
  622. facts (dict): facts for the host
  623. Args:
  624. role (str): role for setting local facts
  625. filename (str): local facts file to use
  626. local_facts (dict): local facts to set
  627. Raises:
  628. OpenShiftFactsUnsupportedRoleError:
  629. """
  630. known_roles = ['common', 'master', 'node', 'master_sdn', 'node_sdn', 'dns']
  631. def __init__(self, role, filename, local_facts):
  632. self.changed = False
  633. self.filename = filename
  634. if role not in self.known_roles:
  635. raise OpenShiftFactsUnsupportedRoleError(
  636. "Role %s is not supported by this module" % role
  637. )
  638. self.role = role
  639. self.system_facts = ansible_facts(module)
  640. self.facts = self.generate_facts(local_facts)
  641. def generate_facts(self, local_facts):
  642. """ Generate facts
  643. Args:
  644. local_facts (dict): local_facts for overriding generated
  645. defaults
  646. Returns:
  647. dict: The generated facts
  648. """
  649. local_facts = self.init_local_facts(local_facts)
  650. roles = local_facts.keys()
  651. defaults = self.get_defaults(roles)
  652. provider_facts = self.init_provider_facts()
  653. facts = apply_provider_facts(defaults, provider_facts)
  654. facts = merge_facts(facts, local_facts)
  655. facts['current_config'] = get_current_config(facts)
  656. facts = set_url_facts_if_unset(facts)
  657. facts = set_fluentd_facts_if_unset(facts)
  658. facts = set_node_schedulability(facts)
  659. facts = set_metrics_facts_if_unset(facts)
  660. facts = set_identity_providers_if_unset(facts)
  661. facts = set_sdn_facts_if_unset(facts)
  662. facts = set_deployment_facts_if_unset(facts)
  663. facts = set_aggregate_facts(facts)
  664. return dict(openshift=facts)
  665. def get_defaults(self, roles):
  666. """ Get default fact values
  667. Args:
  668. roles (list): list of roles for this host
  669. Returns:
  670. dict: The generated default facts
  671. """
  672. defaults = dict()
  673. ip_addr = self.system_facts['default_ipv4']['address']
  674. exit_code, output, _ = module.run_command(['hostname', '-f'])
  675. hostname_f = output.strip() if exit_code == 0 else ''
  676. hostname_values = [hostname_f, self.system_facts['nodename'],
  677. self.system_facts['fqdn']]
  678. hostname = choose_hostname(hostname_values, ip_addr)
  679. common = dict(use_openshift_sdn=True, ip=ip_addr, public_ip=ip_addr,
  680. deployment_type='origin', hostname=hostname,
  681. public_hostname=hostname)
  682. common['client_binary'] = 'oc' if os.path.isfile('/usr/bin/oc') else 'osc'
  683. common['admin_binary'] = 'oadm' if os.path.isfile('/usr/bin/oadm') else 'osadm'
  684. defaults['common'] = common
  685. if 'master' in roles:
  686. master = dict(api_use_ssl=True, api_port='8443',
  687. console_use_ssl=True, console_path='/console',
  688. console_port='8443', etcd_use_ssl=True, etcd_hosts='',
  689. etcd_port='4001', portal_net='172.30.0.0/16',
  690. embedded_etcd=True, embedded_kube=True,
  691. embedded_dns=True, dns_port='53',
  692. bind_addr='0.0.0.0', session_max_seconds=3600,
  693. session_name='ssn', session_secrets_file='',
  694. access_token_max_seconds=86400,
  695. auth_token_max_seconds=500,
  696. oauth_grant_method='auto', cluster_defer_ha=False)
  697. defaults['master'] = master
  698. if 'node' in roles:
  699. node = dict(labels={}, annotations={}, portal_net='172.30.0.0/16')
  700. defaults['node'] = node
  701. return defaults
  702. def guess_host_provider(self):
  703. """ Guess the host provider
  704. Returns:
  705. dict: The generated default facts for the detected provider
  706. """
  707. # TODO: cloud provider facts should probably be submitted upstream
  708. product_name = self.system_facts['product_name']
  709. product_version = self.system_facts['product_version']
  710. virt_type = self.system_facts['virtualization_type']
  711. virt_role = self.system_facts['virtualization_role']
  712. provider = None
  713. metadata = None
  714. # TODO: this is not exposed through module_utils/facts.py in ansible,
  715. # need to create PR for ansible to expose it
  716. bios_vendor = get_file_content(
  717. '/sys/devices/virtual/dmi/id/bios_vendor'
  718. )
  719. if bios_vendor == 'Google':
  720. provider = 'gce'
  721. metadata_url = ('http://metadata.google.internal/'
  722. 'computeMetadata/v1/?recursive=true')
  723. headers = {'Metadata-Flavor': 'Google'}
  724. metadata = get_provider_metadata(metadata_url, True, headers,
  725. True)
  726. # Filter sshKeys and serviceAccounts from gce metadata
  727. if metadata:
  728. metadata['project']['attributes'].pop('sshKeys', None)
  729. metadata['instance'].pop('serviceAccounts', None)
  730. elif (virt_type == 'xen' and virt_role == 'guest'
  731. and re.match(r'.*\.amazon$', product_version)):
  732. provider = 'ec2'
  733. metadata_url = 'http://169.254.169.254/latest/meta-data/'
  734. metadata = get_provider_metadata(metadata_url)
  735. elif re.search(r'OpenStack', product_name):
  736. provider = 'openstack'
  737. metadata_url = ('http://169.254.169.254/openstack/latest/'
  738. 'meta_data.json')
  739. metadata = get_provider_metadata(metadata_url, True, None,
  740. True)
  741. if metadata:
  742. ec2_compat_url = 'http://169.254.169.254/latest/meta-data/'
  743. metadata['ec2_compat'] = get_provider_metadata(
  744. ec2_compat_url
  745. )
  746. # disable pylint maybe-no-member because overloaded use of
  747. # the module name causes pylint to not detect that results
  748. # is an array or hash
  749. # pylint: disable=maybe-no-member
  750. # Filter public_keys and random_seed from openstack metadata
  751. metadata.pop('public_keys', None)
  752. metadata.pop('random_seed', None)
  753. if not metadata['ec2_compat']:
  754. metadata = None
  755. return dict(name=provider, metadata=metadata)
  756. def init_provider_facts(self):
  757. """ Initialize the provider facts
  758. Returns:
  759. dict: The normalized provider facts
  760. """
  761. provider_info = self.guess_host_provider()
  762. provider_facts = normalize_provider_facts(
  763. provider_info.get('name'),
  764. provider_info.get('metadata')
  765. )
  766. return provider_facts
  767. def init_local_facts(self, facts=None):
  768. """ Initialize the provider facts
  769. Args:
  770. facts (dict): local facts to set
  771. Returns:
  772. dict: The result of merging the provided facts with existing
  773. local facts
  774. """
  775. changed = False
  776. facts_to_set = {self.role: dict()}
  777. if facts is not None:
  778. facts_to_set[self.role] = facts
  779. local_facts = get_local_facts_from_file(self.filename)
  780. for arg in ['labels', 'annotations']:
  781. if arg in facts_to_set and isinstance(facts_to_set[arg],
  782. basestring):
  783. facts_to_set[arg] = module.from_json(facts_to_set[arg])
  784. new_local_facts = merge_facts(local_facts, facts_to_set)
  785. for facts in new_local_facts.values():
  786. keys_to_delete = []
  787. for fact, value in facts.iteritems():
  788. if value == "" or value is None:
  789. keys_to_delete.append(fact)
  790. for key in keys_to_delete:
  791. del facts[key]
  792. if new_local_facts != local_facts:
  793. changed = True
  794. if not module.check_mode:
  795. save_local_facts(self.filename, new_local_facts)
  796. self.changed = changed
  797. return new_local_facts
  798. def main():
  799. """ main """
  800. # disabling pylint errors for global-variable-undefined and invalid-name
  801. # for 'global module' usage, since it is required to use ansible_facts
  802. # pylint: disable=global-variable-undefined, invalid-name
  803. global module
  804. module = AnsibleModule(
  805. argument_spec=dict(
  806. role=dict(default='common', required=False,
  807. choices=OpenShiftFacts.known_roles),
  808. local_facts=dict(default=None, type='dict', required=False),
  809. ),
  810. supports_check_mode=True,
  811. add_file_common_args=True,
  812. )
  813. role = module.params['role']
  814. local_facts = module.params['local_facts']
  815. fact_file = '/etc/ansible/facts.d/openshift.fact'
  816. openshift_facts = OpenShiftFacts(role, fact_file, local_facts)
  817. file_params = module.params.copy()
  818. file_params['path'] = fact_file
  819. file_args = module.load_file_common_arguments(file_params)
  820. changed = module.set_fs_attributes_if_different(file_args,
  821. openshift_facts.changed)
  822. return module.exit_json(changed=changed,
  823. ansible_facts=openshift_facts.facts)
  824. # ignore pylint errors related to the module_utils import
  825. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import
  826. # import module snippets
  827. from ansible.module_utils.basic import *
  828. from ansible.module_utils.facts import *
  829. from ansible.module_utils.urls import *
  830. if __name__ == '__main__':
  831. main()