openshift_facts.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217
  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. facts['common']['all_hostnames'] = list(all_hostnames)
  469. facts['common']['internal_hostnames'] = list(internal_hostnames)
  470. return facts
  471. def set_etcd_facts_if_unset(facts):
  472. """
  473. If using embedded etcd, loads the data directory from master-config.yaml.
  474. If using standalone etcd, loads ETCD_DATA_DIR from etcd.conf.
  475. If anything goes wrong parsing these, the fact will not be set.
  476. """
  477. if 'master' in facts and facts['master']['embedded_etcd']:
  478. etcd_facts = facts['etcd'] if 'etcd' in facts else dict()
  479. if 'etcd_data_dir' not in etcd_facts:
  480. try:
  481. # Parse master config to find actual etcd data dir:
  482. master_cfg_path = os.path.join(facts['common']['config_base'],
  483. 'master/master-config.yaml')
  484. master_cfg_f = open(master_cfg_path, 'r')
  485. config = yaml.safe_load(master_cfg_f.read())
  486. master_cfg_f.close()
  487. etcd_facts['etcd_data_dir'] = \
  488. config['etcdConfig']['storageDirectory']
  489. facts['etcd'] = etcd_facts
  490. # We don't want exceptions bubbling up here:
  491. # pylint: disable=broad-except
  492. except Exception:
  493. pass
  494. else:
  495. etcd_facts = facts['etcd'] if 'etcd' in facts else dict()
  496. # Read ETCD_DATA_DIR from /etc/etcd/etcd.conf:
  497. try:
  498. # Add a fake section for parsing:
  499. ini_str = '[root]\n' + open('/etc/etcd/etcd.conf', 'r').read()
  500. ini_fp = StringIO.StringIO(ini_str)
  501. config = ConfigParser.RawConfigParser()
  502. config.readfp(ini_fp)
  503. etcd_data_dir = config.get('root', 'ETCD_DATA_DIR')
  504. if etcd_data_dir.startswith('"') and etcd_data_dir.endswith('"'):
  505. etcd_data_dir = etcd_data_dir[1:-1]
  506. etcd_facts['etcd_data_dir'] = etcd_data_dir
  507. facts['etcd'] = etcd_facts
  508. # We don't want exceptions bubbling up here:
  509. # pylint: disable=broad-except
  510. except Exception:
  511. pass
  512. return facts
  513. def set_deployment_facts_if_unset(facts):
  514. """ Set Facts that vary based on deployment_type. This currently
  515. includes common.service_type, common.config_base, master.registry_url,
  516. node.registry_url, node.storage_plugin_deps
  517. Args:
  518. facts (dict): existing facts
  519. Returns:
  520. dict: the facts dict updated with the generated deployment_type
  521. facts
  522. """
  523. # disabled to avoid breaking up facts related to deployment type into
  524. # multiple methods for now.
  525. # pylint: disable=too-many-statements, too-many-branches
  526. if 'common' in facts:
  527. deployment_type = facts['common']['deployment_type']
  528. if 'service_type' not in facts['common']:
  529. service_type = 'atomic-openshift'
  530. if deployment_type == 'origin':
  531. service_type = 'origin'
  532. elif deployment_type in ['enterprise', 'online']:
  533. service_type = 'openshift'
  534. facts['common']['service_type'] = service_type
  535. if 'config_base' not in facts['common']:
  536. config_base = '/etc/origin'
  537. if deployment_type in ['enterprise', 'online']:
  538. config_base = '/etc/openshift'
  539. # Handle upgrade scenarios when symlinks don't yet exist:
  540. if not os.path.exists(config_base) and os.path.exists('/etc/openshift'):
  541. config_base = '/etc/openshift'
  542. facts['common']['config_base'] = config_base
  543. if 'data_dir' not in facts['common']:
  544. data_dir = '/var/lib/origin'
  545. if deployment_type in ['enterprise', 'online']:
  546. data_dir = '/var/lib/openshift'
  547. # Handle upgrade scenarios when symlinks don't yet exist:
  548. if not os.path.exists(data_dir) and os.path.exists('/var/lib/openshift'):
  549. data_dir = '/var/lib/openshift'
  550. facts['common']['data_dir'] = data_dir
  551. for role in ('master', 'node'):
  552. if role in facts:
  553. deployment_type = facts['common']['deployment_type']
  554. if 'registry_url' not in facts[role]:
  555. registry_url = 'openshift/origin-${component}:${version}'
  556. if deployment_type in ['enterprise', 'online', 'openshift-enterprise']:
  557. registry_url = 'openshift3/ose-${component}:${version}'
  558. elif deployment_type == 'atomic-enterprise':
  559. registry_url = 'aep3/aep-${component}:${version}'
  560. facts[role]['registry_url'] = registry_url
  561. if 'master' in facts:
  562. deployment_type = facts['common']['deployment_type']
  563. openshift_features = ['Builder', 'S2IBuilder', 'WebConsole']
  564. if 'disabled_features' in facts['master']:
  565. if deployment_type == 'atomic-enterprise':
  566. curr_disabled_features = set(facts['master']['disabled_features'])
  567. facts['master']['disabled_features'] = list(curr_disabled_features.union(openshift_features))
  568. else:
  569. if deployment_type == 'atomic-enterprise':
  570. facts['master']['disabled_features'] = openshift_features
  571. if 'node' in facts:
  572. deployment_type = facts['common']['deployment_type']
  573. if 'storage_plugin_deps' not in facts['node']:
  574. if deployment_type in ['openshift-enterprise', 'atomic-enterprise']:
  575. facts['node']['storage_plugin_deps'] = ['ceph', 'glusterfs']
  576. else:
  577. facts['node']['storage_plugin_deps'] = []
  578. return facts
  579. def set_version_facts_if_unset(facts):
  580. """ Set version facts. This currently includes common.version and
  581. common.version_greater_than_3_1_or_1_1.
  582. Args:
  583. facts (dict): existing facts
  584. Returns:
  585. dict: the facts dict updated with version facts.
  586. """
  587. if 'common' in facts:
  588. deployment_type = facts['common']['deployment_type']
  589. facts['common']['version'] = version = get_openshift_version()
  590. if version is not None:
  591. if deployment_type == 'origin':
  592. version_gt_3_1_or_1_1 = LooseVersion(version) > LooseVersion('1.0.6')
  593. else:
  594. version_gt_3_1_or_1_1 = LooseVersion(version) > LooseVersion('3.0.2.900')
  595. else:
  596. version_gt_3_1_or_1_1 = True
  597. facts['common']['version_greater_than_3_1_or_1_1'] = version_gt_3_1_or_1_1
  598. return facts
  599. def set_sdn_facts_if_unset(facts, system_facts):
  600. """ Set sdn facts if not already present in facts dict
  601. Args:
  602. facts (dict): existing facts
  603. system_facts (dict): ansible_facts
  604. Returns:
  605. dict: the facts dict updated with the generated sdn facts if they
  606. were not already present
  607. """
  608. if 'common' in facts:
  609. use_sdn = facts['common']['use_openshift_sdn']
  610. if not (use_sdn == '' or isinstance(use_sdn, bool)):
  611. facts['common']['use_openshift_sdn'] = bool(strtobool(str(use_sdn)))
  612. if 'sdn_network_plugin_name' not in facts['common']:
  613. plugin = 'redhat/openshift-ovs-subnet' if use_sdn else ''
  614. facts['common']['sdn_network_plugin_name'] = plugin
  615. if 'master' in facts:
  616. if 'sdn_cluster_network_cidr' not in facts['master']:
  617. facts['master']['sdn_cluster_network_cidr'] = '10.1.0.0/16'
  618. if 'sdn_host_subnet_length' not in facts['master']:
  619. facts['master']['sdn_host_subnet_length'] = '8'
  620. if 'node' in facts and 'sdn_mtu' not in facts['node']:
  621. node_ip = facts['common']['ip']
  622. # default MTU if interface MTU cannot be detected
  623. facts['node']['sdn_mtu'] = '1450'
  624. for val in system_facts.itervalues():
  625. if isinstance(val, dict) and 'mtu' in val:
  626. mtu = val['mtu']
  627. if 'ipv4' in val and val['ipv4'].get('address') == node_ip:
  628. facts['node']['sdn_mtu'] = str(mtu - 50)
  629. return facts
  630. def format_url(use_ssl, hostname, port, path=''):
  631. """ Format url based on ssl flag, hostname, port and path
  632. Args:
  633. use_ssl (bool): is ssl enabled
  634. hostname (str): hostname
  635. port (str): port
  636. path (str): url path
  637. Returns:
  638. str: The generated url string
  639. """
  640. scheme = 'https' if use_ssl else 'http'
  641. netloc = hostname
  642. if (use_ssl and port != '443') or (not use_ssl and port != '80'):
  643. netloc += ":%s" % port
  644. return urlparse.urlunparse((scheme, netloc, path, '', '', ''))
  645. def get_current_config(facts):
  646. """ Get current openshift config
  647. Args:
  648. facts (dict): existing facts
  649. Returns:
  650. dict: the facts dict updated with the current openshift config
  651. """
  652. current_config = dict()
  653. roles = [role for role in facts if role not in ['common', 'provider']]
  654. for role in roles:
  655. if 'roles' in current_config:
  656. current_config['roles'].append(role)
  657. else:
  658. current_config['roles'] = [role]
  659. # TODO: parse the /etc/sysconfig/openshift-{master,node} config to
  660. # determine the location of files.
  661. # TODO: I suspect this isn't working right now, but it doesn't prevent
  662. # anything from working properly as far as I can tell, perhaps because
  663. # we override the kubeconfig path everywhere we use it?
  664. # Query kubeconfig settings
  665. kubeconfig_dir = '/var/lib/origin/openshift.local.certificates'
  666. if role == 'node':
  667. kubeconfig_dir = os.path.join(
  668. kubeconfig_dir, "node-%s" % facts['common']['hostname']
  669. )
  670. kubeconfig_path = os.path.join(kubeconfig_dir, '.kubeconfig')
  671. if (os.path.isfile('/usr/bin/openshift')
  672. and os.path.isfile(kubeconfig_path)):
  673. try:
  674. _, output, _ = module.run_command(
  675. ["/usr/bin/openshift", "ex", "config", "view", "-o",
  676. "json", "--kubeconfig=%s" % kubeconfig_path],
  677. check_rc=False
  678. )
  679. config = json.loads(output)
  680. cad = 'certificate-authority-data'
  681. try:
  682. for cluster in config['clusters']:
  683. config['clusters'][cluster][cad] = 'masked'
  684. except KeyError:
  685. pass
  686. try:
  687. for user in config['users']:
  688. config['users'][user][cad] = 'masked'
  689. config['users'][user]['client-key-data'] = 'masked'
  690. except KeyError:
  691. pass
  692. current_config['kubeconfig'] = config
  693. # override pylint broad-except warning, since we do not want
  694. # to bubble up any exceptions if oc config view
  695. # fails
  696. # pylint: disable=broad-except
  697. except Exception:
  698. pass
  699. return current_config
  700. def get_openshift_version():
  701. """ Get current version of openshift on the host
  702. Returns:
  703. version: the current openshift version
  704. """
  705. version = None
  706. if os.path.isfile('/usr/bin/openshift'):
  707. _, output, _ = module.run_command(['/usr/bin/openshift', 'version'])
  708. versions = dict(e.split(' v') for e in output.splitlines() if ' v' in e)
  709. version = versions.get('openshift', '')
  710. #TODO: acknowledge the possility of a containerized install
  711. return version
  712. def apply_provider_facts(facts, provider_facts):
  713. """ Apply provider facts to supplied facts dict
  714. Args:
  715. facts (dict): facts dict to update
  716. provider_facts (dict): provider facts to apply
  717. roles: host roles
  718. Returns:
  719. dict: the merged facts
  720. """
  721. if not provider_facts:
  722. return facts
  723. use_openshift_sdn = provider_facts.get('use_openshift_sdn')
  724. if isinstance(use_openshift_sdn, bool):
  725. facts['common']['use_openshift_sdn'] = use_openshift_sdn
  726. common_vars = [('hostname', 'ip'), ('public_hostname', 'public_ip')]
  727. for h_var, ip_var in common_vars:
  728. ip_value = provider_facts['network'].get(ip_var)
  729. if ip_value:
  730. facts['common'][ip_var] = ip_value
  731. facts['common'][h_var] = choose_hostname(
  732. [provider_facts['network'].get(h_var)],
  733. facts['common'][ip_var]
  734. )
  735. facts['provider'] = provider_facts
  736. return facts
  737. def merge_facts(orig, new):
  738. """ Recursively merge facts dicts
  739. Args:
  740. orig (dict): existing facts
  741. new (dict): facts to update
  742. Returns:
  743. dict: the merged facts
  744. """
  745. facts = dict()
  746. for key, value in orig.iteritems():
  747. if key in new:
  748. if isinstance(value, dict) and isinstance(new[key], dict):
  749. facts[key] = merge_facts(value, new[key])
  750. else:
  751. facts[key] = copy.copy(new[key])
  752. else:
  753. facts[key] = copy.deepcopy(value)
  754. new_keys = set(new.keys()) - set(orig.keys())
  755. for key in new_keys:
  756. facts[key] = copy.deepcopy(new[key])
  757. return facts
  758. def save_local_facts(filename, facts):
  759. """ Save local facts
  760. Args:
  761. filename (str): local facts file
  762. facts (dict): facts to set
  763. """
  764. try:
  765. fact_dir = os.path.dirname(filename)
  766. if not os.path.exists(fact_dir):
  767. os.makedirs(fact_dir)
  768. with open(filename, 'w') as fact_file:
  769. fact_file.write(module.jsonify(facts))
  770. except (IOError, OSError) as ex:
  771. raise OpenShiftFactsFileWriteError(
  772. "Could not create fact file: %s, error: %s" % (filename, ex)
  773. )
  774. def get_local_facts_from_file(filename):
  775. """ Retrieve local facts from fact file
  776. Args:
  777. filename (str): local facts file
  778. Returns:
  779. dict: the retrieved facts
  780. """
  781. local_facts = dict()
  782. try:
  783. # Handle conversion of INI style facts file to json style
  784. ini_facts = ConfigParser.SafeConfigParser()
  785. ini_facts.read(filename)
  786. for section in ini_facts.sections():
  787. local_facts[section] = dict()
  788. for key, value in ini_facts.items(section):
  789. local_facts[section][key] = value
  790. except (ConfigParser.MissingSectionHeaderError,
  791. ConfigParser.ParsingError):
  792. try:
  793. with open(filename, 'r') as facts_file:
  794. local_facts = json.load(facts_file)
  795. except (ValueError, IOError):
  796. pass
  797. return local_facts
  798. class OpenShiftFactsUnsupportedRoleError(Exception):
  799. """Origin Facts Unsupported Role Error"""
  800. pass
  801. class OpenShiftFactsFileWriteError(Exception):
  802. """Origin Facts File Write Error"""
  803. pass
  804. class OpenShiftFactsMetadataUnavailableError(Exception):
  805. """Origin Facts Metadata Unavailable Error"""
  806. pass
  807. class OpenShiftFacts(object):
  808. """ Origin Facts
  809. Attributes:
  810. facts (dict): facts for the host
  811. Args:
  812. role (str): role for setting local facts
  813. filename (str): local facts file to use
  814. local_facts (dict): local facts to set
  815. Raises:
  816. OpenShiftFactsUnsupportedRoleError:
  817. """
  818. known_roles = ['common', 'master', 'node', 'master_sdn', 'node_sdn', 'dns', 'etcd']
  819. def __init__(self, role, filename, local_facts):
  820. self.changed = False
  821. self.filename = filename
  822. if role not in self.known_roles:
  823. raise OpenShiftFactsUnsupportedRoleError(
  824. "Role %s is not supported by this module" % role
  825. )
  826. self.role = role
  827. self.system_facts = ansible_facts(module)
  828. self.facts = self.generate_facts(local_facts)
  829. def generate_facts(self, local_facts):
  830. """ Generate facts
  831. Args:
  832. local_facts (dict): local_facts for overriding generated
  833. defaults
  834. Returns:
  835. dict: The generated facts
  836. """
  837. local_facts = self.init_local_facts(local_facts)
  838. roles = local_facts.keys()
  839. defaults = self.get_defaults(roles)
  840. provider_facts = self.init_provider_facts()
  841. facts = apply_provider_facts(defaults, provider_facts)
  842. facts = merge_facts(facts, local_facts)
  843. facts['current_config'] = get_current_config(facts)
  844. facts = set_url_facts_if_unset(facts)
  845. facts = set_project_cfg_facts_if_unset(facts)
  846. facts = set_fluentd_facts_if_unset(facts)
  847. facts = set_flannel_facts_if_unset(facts)
  848. facts = set_node_schedulability(facts)
  849. facts = set_master_selectors(facts)
  850. facts = set_metrics_facts_if_unset(facts)
  851. facts = set_identity_providers_if_unset(facts)
  852. facts = set_sdn_facts_if_unset(facts, self.system_facts)
  853. facts = set_deployment_facts_if_unset(facts)
  854. facts = set_version_facts_if_unset(facts)
  855. facts = set_aggregate_facts(facts)
  856. facts = set_etcd_facts_if_unset(facts)
  857. return dict(openshift=facts)
  858. def get_defaults(self, roles):
  859. """ Get default fact values
  860. Args:
  861. roles (list): list of roles for this host
  862. Returns:
  863. dict: The generated default facts
  864. """
  865. defaults = dict()
  866. ip_addr = self.system_facts['default_ipv4']['address']
  867. exit_code, output, _ = module.run_command(['hostname', '-f'])
  868. hostname_f = output.strip() if exit_code == 0 else ''
  869. hostname_values = [hostname_f, self.system_facts['nodename'],
  870. self.system_facts['fqdn']]
  871. hostname = choose_hostname(hostname_values, ip_addr)
  872. common = dict(use_openshift_sdn=True, ip=ip_addr, public_ip=ip_addr,
  873. deployment_type='origin', hostname=hostname,
  874. public_hostname=hostname)
  875. common['client_binary'] = 'oc' if os.path.isfile('/usr/bin/oc') else 'osc'
  876. common['admin_binary'] = 'oadm' if os.path.isfile('/usr/bin/oadm') else 'osadm'
  877. defaults['common'] = common
  878. if 'master' in roles:
  879. master = dict(api_use_ssl=True, api_port='8443',
  880. console_use_ssl=True, console_path='/console',
  881. console_port='8443', etcd_use_ssl=True, etcd_hosts='',
  882. etcd_port='4001', portal_net='172.30.0.0/16',
  883. embedded_etcd=True, embedded_kube=True,
  884. embedded_dns=True, dns_port='53',
  885. bind_addr='0.0.0.0', session_max_seconds=3600,
  886. session_name='ssn', session_secrets_file='',
  887. access_token_max_seconds=86400,
  888. auth_token_max_seconds=500,
  889. oauth_grant_method='auto')
  890. defaults['master'] = master
  891. if 'node' in roles:
  892. node = dict(labels={}, annotations={}, portal_net='172.30.0.0/16',
  893. iptables_sync_period='5s')
  894. defaults['node'] = node
  895. return defaults
  896. def guess_host_provider(self):
  897. """ Guess the host provider
  898. Returns:
  899. dict: The generated default facts for the detected provider
  900. """
  901. # TODO: cloud provider facts should probably be submitted upstream
  902. product_name = self.system_facts['product_name']
  903. product_version = self.system_facts['product_version']
  904. virt_type = self.system_facts['virtualization_type']
  905. virt_role = self.system_facts['virtualization_role']
  906. provider = None
  907. metadata = None
  908. # TODO: this is not exposed through module_utils/facts.py in ansible,
  909. # need to create PR for ansible to expose it
  910. bios_vendor = get_file_content(
  911. '/sys/devices/virtual/dmi/id/bios_vendor'
  912. )
  913. if bios_vendor == 'Google':
  914. provider = 'gce'
  915. metadata_url = ('http://metadata.google.internal/'
  916. 'computeMetadata/v1/?recursive=true')
  917. headers = {'Metadata-Flavor': 'Google'}
  918. metadata = get_provider_metadata(metadata_url, True, headers,
  919. True)
  920. # Filter sshKeys and serviceAccounts from gce metadata
  921. if metadata:
  922. metadata['project']['attributes'].pop('sshKeys', None)
  923. metadata['instance'].pop('serviceAccounts', None)
  924. elif (virt_type == 'xen' and virt_role == 'guest'
  925. and re.match(r'.*\.amazon$', product_version)):
  926. provider = 'ec2'
  927. metadata_url = 'http://169.254.169.254/latest/meta-data/'
  928. metadata = get_provider_metadata(metadata_url)
  929. elif re.search(r'OpenStack', product_name):
  930. provider = 'openstack'
  931. metadata_url = ('http://169.254.169.254/openstack/latest/'
  932. 'meta_data.json')
  933. metadata = get_provider_metadata(metadata_url, True, None,
  934. True)
  935. if metadata:
  936. ec2_compat_url = 'http://169.254.169.254/latest/meta-data/'
  937. metadata['ec2_compat'] = get_provider_metadata(
  938. ec2_compat_url
  939. )
  940. # disable pylint maybe-no-member because overloaded use of
  941. # the module name causes pylint to not detect that results
  942. # is an array or hash
  943. # pylint: disable=maybe-no-member
  944. # Filter public_keys and random_seed from openstack metadata
  945. metadata.pop('public_keys', None)
  946. metadata.pop('random_seed', None)
  947. if not metadata['ec2_compat']:
  948. metadata = None
  949. return dict(name=provider, metadata=metadata)
  950. def init_provider_facts(self):
  951. """ Initialize the provider facts
  952. Returns:
  953. dict: The normalized provider facts
  954. """
  955. provider_info = self.guess_host_provider()
  956. provider_facts = normalize_provider_facts(
  957. provider_info.get('name'),
  958. provider_info.get('metadata')
  959. )
  960. return provider_facts
  961. def init_local_facts(self, facts=None):
  962. """ Initialize the provider facts
  963. Args:
  964. facts (dict): local facts to set
  965. Returns:
  966. dict: The result of merging the provided facts with existing
  967. local facts
  968. """
  969. changed = False
  970. facts_to_set = {self.role: dict()}
  971. if facts is not None:
  972. facts_to_set[self.role] = facts
  973. local_facts = get_local_facts_from_file(self.filename)
  974. for arg in ['labels', 'annotations']:
  975. if arg in facts_to_set and isinstance(facts_to_set[arg],
  976. basestring):
  977. facts_to_set[arg] = module.from_json(facts_to_set[arg])
  978. new_local_facts = merge_facts(local_facts, facts_to_set)
  979. for facts in new_local_facts.values():
  980. keys_to_delete = []
  981. for fact, value in facts.iteritems():
  982. if value == "" or value is None:
  983. keys_to_delete.append(fact)
  984. for key in keys_to_delete:
  985. del facts[key]
  986. if new_local_facts != local_facts:
  987. changed = True
  988. if not module.check_mode:
  989. save_local_facts(self.filename, new_local_facts)
  990. self.changed = changed
  991. return new_local_facts
  992. def main():
  993. """ main """
  994. # disabling pylint errors for global-variable-undefined and invalid-name
  995. # for 'global module' usage, since it is required to use ansible_facts
  996. # pylint: disable=global-variable-undefined, invalid-name
  997. global module
  998. module = AnsibleModule(
  999. argument_spec=dict(
  1000. role=dict(default='common', required=False,
  1001. choices=OpenShiftFacts.known_roles),
  1002. local_facts=dict(default=None, type='dict', required=False),
  1003. ),
  1004. supports_check_mode=True,
  1005. add_file_common_args=True,
  1006. )
  1007. role = module.params['role']
  1008. local_facts = module.params['local_facts']
  1009. fact_file = '/etc/ansible/facts.d/openshift.fact'
  1010. openshift_facts = OpenShiftFacts(role, fact_file, local_facts)
  1011. file_params = module.params.copy()
  1012. file_params['path'] = fact_file
  1013. file_args = module.load_file_common_arguments(file_params)
  1014. changed = module.set_fs_attributes_if_different(file_args,
  1015. openshift_facts.changed)
  1016. return module.exit_json(changed=changed,
  1017. ansible_facts=openshift_facts.facts)
  1018. # ignore pylint errors related to the module_utils import
  1019. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import
  1020. # import module snippets
  1021. from ansible.module_utils.basic import *
  1022. from ansible.module_utils.facts import *
  1023. from ansible.module_utils.urls import *
  1024. if __name__ == '__main__':
  1025. main()