openshift_facts.py 39 KB

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