openshift_facts.py 37 KB

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