openshift_facts.py 41 KB

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