openshift_facts.py 44 KB

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