openshift_facts.py 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306
  1. #!/usr/bin/python
  2. # pylint: disable=too-many-lines
  3. # -*- coding: utf-8 -*-
  4. # Reason: Disable pylint too-many-lines because we don't want to split up this file.
  5. # Status: Permanently disabled to keep this module as self-contained as possible.
  6. """Ansible module for retrieving and setting openshift related facts"""
  7. # pylint: disable=no-name-in-module, import-error, wrong-import-order
  8. import copy
  9. import errno
  10. import json
  11. import re
  12. import os
  13. import yaml
  14. import struct
  15. import socket
  16. import ipaddress
  17. from distutils.util import strtobool
  18. from ansible.module_utils.six import text_type
  19. from ansible.module_utils.six import string_types
  20. # ignore pylint errors related to the module_utils import
  21. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import
  22. # import module snippets
  23. from ansible.module_utils.basic import * # noqa: F403
  24. from ansible.module_utils.facts import * # noqa: F403
  25. from ansible.module_utils.urls import * # noqa: F403
  26. from ansible.module_utils.six import iteritems, itervalues
  27. from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse
  28. from ansible.module_utils._text import to_native
  29. DOCUMENTATION = '''
  30. ---
  31. module: openshift_facts
  32. short_description: Cluster Facts
  33. author: Jason DeTiberus
  34. requirements: [ ]
  35. '''
  36. EXAMPLES = '''
  37. '''
  38. # TODO: We should add a generic migration function that takes source and destination
  39. # paths and does the right thing rather than one function for common, one for node, etc.
  40. def migrate_common_facts(facts):
  41. """ Migrate facts from various roles into common """
  42. params = {
  43. 'node': ('portal_net'),
  44. 'master': ('portal_net')
  45. }
  46. if 'common' not in facts:
  47. facts['common'] = {}
  48. # pylint: disable=consider-iterating-dictionary
  49. for role in params.keys():
  50. if role in facts:
  51. for param in params[role]:
  52. if param in facts[role]:
  53. facts['common'][param] = facts[role].pop(param)
  54. return facts
  55. def migrate_admission_plugin_facts(facts):
  56. """ Apply migrations for admission plugin facts """
  57. if 'master' in facts:
  58. if 'kube_admission_plugin_config' in facts['master']:
  59. if 'admission_plugin_config' not in facts['master']:
  60. facts['master']['admission_plugin_config'] = dict()
  61. # Merge existing kube_admission_plugin_config with admission_plugin_config.
  62. facts['master']['admission_plugin_config'] = merge_facts(facts['master']['admission_plugin_config'],
  63. facts['master']['kube_admission_plugin_config'],
  64. additive_facts_to_overwrite=[])
  65. # Remove kube_admission_plugin_config fact
  66. facts['master'].pop('kube_admission_plugin_config', None)
  67. return facts
  68. def migrate_local_facts(facts):
  69. """ Apply migrations of local facts """
  70. migrated_facts = copy.deepcopy(facts)
  71. migrated_facts = migrate_common_facts(migrated_facts)
  72. migrated_facts = migrate_admission_plugin_facts(migrated_facts)
  73. return migrated_facts
  74. def first_ip(network):
  75. """ Return the first IPv4 address in network
  76. Args:
  77. network (str): network in CIDR format
  78. Returns:
  79. str: first IPv4 address
  80. """
  81. atoi = lambda addr: struct.unpack("!I", socket.inet_aton(addr))[0] # noqa: E731
  82. itoa = lambda addr: socket.inet_ntoa(struct.pack("!I", addr)) # noqa: E731
  83. (address, netmask) = network.split('/')
  84. netmask_i = (0xffffffff << (32 - atoi(netmask))) & 0xffffffff
  85. return itoa((atoi(address) & netmask_i) + 1)
  86. def hostname_valid(hostname):
  87. """ Test if specified hostname should be considered valid
  88. Args:
  89. hostname (str): hostname to test
  90. Returns:
  91. bool: True if valid, otherwise False
  92. """
  93. if (not hostname or
  94. hostname.startswith('localhost') or
  95. hostname.endswith('localdomain') or
  96. # OpenShift will not allow a node with more than 63 chars in name.
  97. len(hostname) > 63):
  98. return False
  99. return True
  100. def choose_hostname(hostnames=None, fallback=''):
  101. """ Choose a hostname from the provided hostnames
  102. Given a list of hostnames and a fallback value, choose a hostname to
  103. use. This function will prefer fqdns if they exist (excluding any that
  104. begin with localhost or end with localdomain) over ip addresses.
  105. Args:
  106. hostnames (list): list of hostnames
  107. fallback (str): default value to set if hostnames does not contain
  108. a valid hostname
  109. Returns:
  110. str: chosen hostname
  111. """
  112. hostname = fallback
  113. if hostnames is None:
  114. return hostname
  115. ip_regex = r'\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z'
  116. ips = [i for i in hostnames if i is not None and isinstance(i, string_types) and re.match(ip_regex, i)]
  117. hosts = [i for i in hostnames if i is not None and i != '' and i not in ips]
  118. for host_list in (hosts, ips):
  119. for host in host_list:
  120. if hostname_valid(host):
  121. return host
  122. return hostname
  123. def query_metadata(metadata_url, headers=None, expect_json=False):
  124. """ Return metadata from the provided metadata_url
  125. Args:
  126. metadata_url (str): metadata url
  127. headers (dict): headers to set for metadata request
  128. expect_json (bool): does the metadata_url return json
  129. Returns:
  130. dict or list: metadata request result
  131. """
  132. result, info = fetch_url(module, metadata_url, headers=headers) # noqa: F405
  133. if info['status'] != 200:
  134. raise OpenShiftFactsMetadataUnavailableError("Metadata unavailable")
  135. if expect_json:
  136. return module.from_json(to_native(result.read())) # noqa: F405
  137. else:
  138. return [to_native(line.strip()) for line in result.readlines()]
  139. def walk_metadata(metadata_url, headers=None, expect_json=False):
  140. """ Walk the metadata tree and return a dictionary of the entire tree
  141. Args:
  142. metadata_url (str): metadata url
  143. headers (dict): headers to set for metadata request
  144. expect_json (bool): does the metadata_url return json
  145. Returns:
  146. dict: the result of walking the metadata tree
  147. """
  148. metadata = dict()
  149. for line in query_metadata(metadata_url, headers, expect_json):
  150. if line.endswith('/') and not line == 'public-keys/':
  151. key = line[:-1]
  152. metadata[key] = walk_metadata(metadata_url + line,
  153. headers, expect_json)
  154. else:
  155. results = query_metadata(metadata_url + line, headers,
  156. expect_json)
  157. if len(results) == 1:
  158. # disable pylint maybe-no-member because overloaded use of
  159. # the module name causes pylint to not detect that results
  160. # is an array or hash
  161. # pylint: disable=maybe-no-member
  162. metadata[line] = results.pop()
  163. else:
  164. metadata[line] = results
  165. return metadata
  166. def get_provider_metadata(metadata_url, supports_recursive=False,
  167. headers=None, expect_json=False):
  168. """ Retrieve the provider metadata
  169. Args:
  170. metadata_url (str): metadata url
  171. supports_recursive (bool): does the provider metadata api support
  172. recursion
  173. headers (dict): headers to set for metadata request
  174. expect_json (bool): does the metadata_url return json
  175. Returns:
  176. dict: the provider metadata
  177. """
  178. try:
  179. if supports_recursive:
  180. metadata = query_metadata(metadata_url, headers,
  181. expect_json)
  182. else:
  183. metadata = walk_metadata(metadata_url, headers,
  184. expect_json)
  185. except OpenShiftFactsMetadataUnavailableError:
  186. metadata = None
  187. return metadata
  188. def normalize_gce_facts(metadata, facts):
  189. """ Normalize gce facts
  190. Args:
  191. metadata (dict): provider metadata
  192. facts (dict): facts to update
  193. Returns:
  194. dict: the result of adding the normalized metadata to the provided
  195. facts dict
  196. """
  197. for interface in metadata['instance']['networkInterfaces']:
  198. int_info = dict(ips=[interface['ip']], network_type='gce')
  199. int_info['public_ips'] = [ac['externalIp'] for ac
  200. in interface['accessConfigs']]
  201. int_info['public_ips'].extend(interface['forwardedIps'])
  202. _, _, network_id = interface['network'].rpartition('/')
  203. int_info['network_id'] = network_id
  204. facts['network']['interfaces'].append(int_info)
  205. _, _, zone = metadata['instance']['zone'].rpartition('/')
  206. facts['zone'] = zone
  207. # GCE currently only supports a single interface
  208. facts['network']['ip'] = facts['network']['interfaces'][0]['ips'][0]
  209. pub_ip = facts['network']['interfaces'][0]['public_ips'][0]
  210. facts['network']['public_ip'] = pub_ip
  211. # Split instance hostname from GCE metadata to use the short instance name
  212. facts['network']['hostname'] = metadata['instance']['hostname'].split('.')[0]
  213. # TODO: attempt to resolve public_hostname
  214. facts['network']['public_hostname'] = facts['network']['public_ip']
  215. return facts
  216. def normalize_aws_facts(metadata, facts):
  217. """ Normalize aws facts
  218. Args:
  219. metadata (dict): provider metadata
  220. facts (dict): facts to update
  221. Returns:
  222. dict: the result of adding the normalized metadata to the provided
  223. facts dict
  224. """
  225. for interface in sorted(
  226. metadata['network']['interfaces']['macs'].values(),
  227. key=lambda x: x['device-number']
  228. ):
  229. int_info = dict()
  230. var_map = {'ips': 'local-ipv4s', 'public_ips': 'public-ipv4s'}
  231. for ips_var, int_var in iteritems(var_map):
  232. ips = interface.get(int_var)
  233. if isinstance(ips, string_types):
  234. int_info[ips_var] = [ips]
  235. else:
  236. int_info[ips_var] = ips
  237. if 'vpc-id' in interface:
  238. int_info['network_type'] = 'vpc'
  239. else:
  240. int_info['network_type'] = 'classic'
  241. if int_info['network_type'] == 'vpc':
  242. int_info['network_id'] = interface['subnet-id']
  243. else:
  244. int_info['network_id'] = None
  245. facts['network']['interfaces'].append(int_info)
  246. facts['zone'] = metadata['placement']['availability-zone']
  247. # TODO: actually attempt to determine default local and public ips
  248. # by using the ansible default ip fact and the ipv4-associations
  249. # from the ec2 metadata
  250. facts['network']['ip'] = metadata.get('local-ipv4')
  251. facts['network']['public_ip'] = metadata.get('public-ipv4')
  252. # TODO: verify that local hostname makes sense and is resolvable
  253. facts['network']['hostname'] = metadata.get('local-hostname')
  254. # TODO: verify that public hostname makes sense and is resolvable
  255. facts['network']['public_hostname'] = metadata.get('public-hostname')
  256. return facts
  257. def normalize_openstack_facts(metadata, facts):
  258. """ Normalize openstack facts
  259. Args:
  260. metadata (dict): provider metadata
  261. facts (dict): facts to update
  262. Returns:
  263. dict: the result of adding the normalized metadata to the provided
  264. facts dict
  265. """
  266. # openstack ec2 compat api does not support network interfaces and
  267. # the version tested on did not include the info in the openstack
  268. # metadata api, should be updated if neutron exposes this.
  269. facts['zone'] = metadata['availability_zone']
  270. local_ipv4 = metadata['ec2_compat']['local-ipv4'].split(',')[0]
  271. facts['network']['ip'] = local_ipv4
  272. facts['network']['public_ip'] = metadata['ec2_compat']['public-ipv4']
  273. for f_var, h_var, ip_var in [('hostname', 'hostname', 'local-ipv4'),
  274. ('public_hostname', 'public-hostname', 'public-ipv4')]:
  275. try:
  276. if socket.gethostbyname(metadata['ec2_compat'][h_var]) == metadata['ec2_compat'][ip_var]:
  277. facts['network'][f_var] = metadata['ec2_compat'][h_var]
  278. else:
  279. facts['network'][f_var] = metadata['ec2_compat'][ip_var]
  280. except socket.gaierror:
  281. facts['network'][f_var] = metadata['ec2_compat'][ip_var]
  282. return facts
  283. def normalize_provider_facts(provider, metadata):
  284. """ Normalize provider facts
  285. Args:
  286. provider (str): host provider
  287. metadata (dict): provider metadata
  288. Returns:
  289. dict: the normalized provider facts
  290. """
  291. if provider is None or metadata is None:
  292. return {}
  293. # TODO: test for ipv6_enabled where possible (gce, aws do not support)
  294. # and configure ipv6 facts if available
  295. # TODO: add support for setting user_data if available
  296. facts = dict(name=provider, metadata=metadata,
  297. network=dict(interfaces=[], ipv6_enabled=False))
  298. if provider == 'gce':
  299. facts = normalize_gce_facts(metadata, facts)
  300. elif provider == 'aws':
  301. facts = normalize_aws_facts(metadata, facts)
  302. elif provider == 'openstack':
  303. facts = normalize_openstack_facts(metadata, facts)
  304. return facts
  305. def set_url_facts_if_unset(facts):
  306. """ Set url facts if not already present in facts dict
  307. Args:
  308. facts (dict): existing facts
  309. Returns:
  310. dict: the facts dict updated with the generated url facts if they
  311. were not already present
  312. """
  313. if 'master' in facts:
  314. hostname = facts['common']['hostname']
  315. cluster_hostname = facts['master'].get('cluster_hostname')
  316. cluster_public_hostname = facts['master'].get('cluster_public_hostname')
  317. public_hostname = facts['common']['public_hostname']
  318. api_hostname = cluster_hostname if cluster_hostname else hostname
  319. api_public_hostname = cluster_public_hostname if cluster_public_hostname else public_hostname
  320. console_path = facts['master']['console_path']
  321. use_ssl = dict(
  322. api=facts['master']['api_use_ssl'],
  323. public_api=facts['master']['api_use_ssl'],
  324. loopback_api=facts['master']['api_use_ssl'],
  325. console=facts['master']['console_use_ssl'],
  326. public_console=facts['master']['console_use_ssl'],
  327. )
  328. ports = dict(
  329. api=facts['master']['api_port'],
  330. public_api=facts['master']['api_port'],
  331. loopback_api=facts['master']['api_port'],
  332. console=facts['master']['console_port'],
  333. public_console=facts['master']['console_port'],
  334. )
  335. prefix_hosts = [('api', api_hostname),
  336. ('public_api', api_public_hostname),
  337. ('loopback_api', hostname)]
  338. for prefix, host in prefix_hosts:
  339. facts['master'].setdefault(prefix + '_url', format_url(use_ssl[prefix],
  340. host,
  341. ports[prefix]))
  342. r_lhn = "{0}:{1}".format(hostname, ports['api']).replace('.', '-')
  343. r_lhu = "system:openshift-master/{0}:{1}".format(api_hostname, ports['api']).replace('.', '-')
  344. facts['master'].setdefault('loopback_cluster_name', r_lhn)
  345. facts['master'].setdefault('loopback_context_name', "default/{0}/system:openshift-master".format(r_lhn))
  346. facts['master'].setdefault('loopback_user', r_lhu)
  347. prefix_hosts = [('console', api_hostname), ('public_console', api_public_hostname)]
  348. for prefix, host in prefix_hosts:
  349. facts['master'].setdefault(prefix + '_url', format_url(use_ssl[prefix],
  350. host,
  351. ports[prefix],
  352. console_path))
  353. return facts
  354. def set_aggregate_facts(facts):
  355. """ Set aggregate facts
  356. Args:
  357. facts (dict): existing facts
  358. Returns:
  359. dict: the facts dict updated with aggregated facts
  360. """
  361. all_hostnames = set()
  362. internal_hostnames = set()
  363. kube_svc_ip = first_ip(facts['common']['portal_net'])
  364. if 'common' in facts:
  365. all_hostnames.add(facts['common']['hostname'])
  366. all_hostnames.add(facts['common']['public_hostname'])
  367. all_hostnames.add(facts['common']['ip'])
  368. all_hostnames.add(facts['common']['public_ip'])
  369. facts['common']['kube_svc_ip'] = kube_svc_ip
  370. internal_hostnames.add(facts['common']['hostname'])
  371. internal_hostnames.add(facts['common']['ip'])
  372. cluster_domain = facts['common']['dns_domain']
  373. if 'master' in facts:
  374. if 'cluster_hostname' in facts['master']:
  375. all_hostnames.add(facts['master']['cluster_hostname'])
  376. if 'cluster_public_hostname' in facts['master']:
  377. all_hostnames.add(facts['master']['cluster_public_hostname'])
  378. svc_names = ['openshift', 'openshift.default', 'openshift.default.svc',
  379. 'openshift.default.svc.' + cluster_domain, 'kubernetes', 'kubernetes.default',
  380. 'kubernetes.default.svc', 'kubernetes.default.svc.' + cluster_domain]
  381. all_hostnames.update(svc_names)
  382. internal_hostnames.update(svc_names)
  383. all_hostnames.add(kube_svc_ip)
  384. internal_hostnames.add(kube_svc_ip)
  385. facts['common']['all_hostnames'] = list(all_hostnames)
  386. facts['common']['internal_hostnames'] = list(internal_hostnames)
  387. return facts
  388. def set_sdn_facts_if_unset(facts, system_facts):
  389. """ Set sdn facts if not already present in facts dict
  390. Args:
  391. facts (dict): existing facts
  392. system_facts (dict): ansible_facts
  393. Returns:
  394. dict: the facts dict updated with the generated sdn facts if they
  395. were not already present
  396. """
  397. if 'master' in facts:
  398. # set defaults for sdn_cluster_network_cidr and sdn_host_subnet_length
  399. # these might be overridden if they exist in the master config file
  400. sdn_cluster_network_cidr = '10.128.0.0/14'
  401. sdn_host_subnet_length = '9'
  402. master_cfg_path = os.path.join(facts['common']['config_base'],
  403. 'master/master-config.yaml')
  404. if os.path.isfile(master_cfg_path):
  405. with open(master_cfg_path, 'r') as master_cfg_f:
  406. config = yaml.safe_load(master_cfg_f.read())
  407. if 'networkConfig' in config:
  408. if 'clusterNetworkCIDR' in config['networkConfig']:
  409. sdn_cluster_network_cidr = \
  410. config['networkConfig']['clusterNetworkCIDR']
  411. if 'hostSubnetLength' in config['networkConfig']:
  412. sdn_host_subnet_length = \
  413. config['networkConfig']['hostSubnetLength']
  414. if 'sdn_cluster_network_cidr' not in facts['master']:
  415. facts['master']['sdn_cluster_network_cidr'] = sdn_cluster_network_cidr
  416. if 'sdn_host_subnet_length' not in facts['master']:
  417. facts['master']['sdn_host_subnet_length'] = sdn_host_subnet_length
  418. if 'node' in facts and 'sdn_mtu' not in facts['node']:
  419. node_ip = facts['common']['ip']
  420. # default MTU if interface MTU cannot be detected
  421. facts['node']['sdn_mtu'] = '1450'
  422. for val in itervalues(system_facts):
  423. if isinstance(val, dict) and 'mtu' in val:
  424. mtu = val['mtu']
  425. if 'ipv4' in val and val['ipv4'].get('address') == node_ip:
  426. facts['node']['sdn_mtu'] = str(mtu - 50)
  427. return facts
  428. def set_nodename(facts):
  429. """ set nodename """
  430. if 'node' in facts and 'common' in facts:
  431. if 'cloudprovider' in facts and facts['cloudprovider']['kind'] == 'gce':
  432. facts['node']['nodename'] = facts['provider']['metadata']['instance']['hostname'].split('.')[0]
  433. # TODO: The openstack cloudprovider nodename setting was too opinionaed.
  434. # It needs to be generalized before it can be enabled again.
  435. # elif 'cloudprovider' in facts and facts['cloudprovider']['kind'] == 'openstack':
  436. # facts['node']['nodename'] = facts['provider']['metadata']['hostname'].replace('.novalocal', '')
  437. else:
  438. if 'bootstrapped' in facts['node'] and facts['node']['bootstrapped']:
  439. facts['node']['nodename'] = facts['common']['raw_hostname'].lower()
  440. else:
  441. facts['node']['nodename'] = facts['common']['hostname'].lower()
  442. return facts
  443. def format_url(use_ssl, hostname, port, path=''):
  444. """ Format url based on ssl flag, hostname, port and path
  445. Args:
  446. use_ssl (bool): is ssl enabled
  447. hostname (str): hostname
  448. port (str): port
  449. path (str): url path
  450. Returns:
  451. str: The generated url string
  452. """
  453. scheme = 'https' if use_ssl else 'http'
  454. netloc = hostname
  455. if (use_ssl and port != '443') or (not use_ssl and port != '80'):
  456. netloc += ":%s" % port
  457. try:
  458. url = urlparse.urlunparse((scheme, netloc, path, '', '', ''))
  459. except AttributeError:
  460. # pylint: disable=undefined-variable
  461. url = urlunparse((scheme, netloc, path, '', '', ''))
  462. return url
  463. def get_current_config(facts):
  464. """ Get current openshift config
  465. Args:
  466. facts (dict): existing facts
  467. Returns:
  468. dict: the facts dict updated with the current openshift config
  469. """
  470. current_config = dict()
  471. roles = [role for role in facts if role not in ['common', 'provider']]
  472. for role in roles:
  473. if 'roles' in current_config:
  474. current_config['roles'].append(role)
  475. else:
  476. current_config['roles'] = [role]
  477. # TODO: parse the /etc/sysconfig/openshift-{master,node} config to
  478. # determine the location of files.
  479. # TODO: I suspect this isn't working right now, but it doesn't prevent
  480. # anything from working properly as far as I can tell, perhaps because
  481. # we override the kubeconfig path everywhere we use it?
  482. # Query kubeconfig settings
  483. kubeconfig_dir = '/var/lib/origin/openshift.local.certificates'
  484. if role == 'node':
  485. kubeconfig_dir = os.path.join(
  486. kubeconfig_dir, "node-%s" % facts['common']['hostname']
  487. )
  488. kubeconfig_path = os.path.join(kubeconfig_dir, '.kubeconfig')
  489. if os.path.isfile('/usr/bin/openshift') and os.path.isfile(kubeconfig_path):
  490. try:
  491. _, output, _ = module.run_command( # noqa: F405
  492. ["/usr/bin/openshift", "ex", "config", "view", "-o",
  493. "json", "--kubeconfig=%s" % kubeconfig_path],
  494. check_rc=False
  495. )
  496. config = json.loads(output)
  497. cad = 'certificate-authority-data'
  498. try:
  499. for cluster in config['clusters']:
  500. config['clusters'][cluster][cad] = 'masked'
  501. except KeyError:
  502. pass
  503. try:
  504. for user in config['users']:
  505. config['users'][user][cad] = 'masked'
  506. config['users'][user]['client-key-data'] = 'masked'
  507. except KeyError:
  508. pass
  509. current_config['kubeconfig'] = config
  510. # override pylint broad-except warning, since we do not want
  511. # to bubble up any exceptions if oc config view
  512. # fails
  513. # pylint: disable=broad-except
  514. except Exception:
  515. pass
  516. return current_config
  517. def build_controller_args(facts):
  518. """ Build master controller_args """
  519. cloud_cfg_path = os.path.join(facts['common']['config_base'],
  520. 'cloudprovider')
  521. if 'master' in facts:
  522. controller_args = {}
  523. if 'cloudprovider' in facts:
  524. if 'kind' in facts['cloudprovider']:
  525. if facts['cloudprovider']['kind'] == 'aws':
  526. controller_args['cloud-provider'] = ['aws']
  527. controller_args['cloud-config'] = [cloud_cfg_path + '/aws.conf']
  528. controller_args['disable-attach-detach-reconcile-sync'] = ['true']
  529. if facts['cloudprovider']['kind'] == 'azure':
  530. controller_args['cloud-provider'] = ['azure']
  531. controller_args['cloud-config'] = [cloud_cfg_path + '/azure.conf']
  532. if facts['cloudprovider']['kind'] == 'openstack':
  533. controller_args['cloud-provider'] = ['openstack']
  534. controller_args['cloud-config'] = [cloud_cfg_path + '/openstack.conf']
  535. if facts['cloudprovider']['kind'] == 'gce':
  536. controller_args['cloud-provider'] = ['gce']
  537. controller_args['cloud-config'] = [cloud_cfg_path + '/gce.conf']
  538. if controller_args != {}:
  539. facts = merge_facts({'master': {'controller_args': controller_args}}, facts, [])
  540. return facts
  541. def build_api_server_args(facts):
  542. """ Build master api_server_args """
  543. cloud_cfg_path = os.path.join(facts['common']['config_base'],
  544. 'cloudprovider')
  545. if 'master' in facts:
  546. api_server_args = {}
  547. if 'cloudprovider' in facts:
  548. if 'kind' in facts['cloudprovider']:
  549. if facts['cloudprovider']['kind'] == 'aws':
  550. api_server_args['cloud-provider'] = ['aws']
  551. api_server_args['cloud-config'] = [cloud_cfg_path + '/aws.conf']
  552. if facts['cloudprovider']['kind'] == 'azure':
  553. api_server_args['cloud-provider'] = ['azure']
  554. api_server_args['cloud-config'] = [cloud_cfg_path + '/azure.conf']
  555. if facts['cloudprovider']['kind'] == 'openstack':
  556. api_server_args['cloud-provider'] = ['openstack']
  557. api_server_args['cloud-config'] = [cloud_cfg_path + '/openstack.conf']
  558. if facts['cloudprovider']['kind'] == 'gce':
  559. api_server_args['cloud-provider'] = ['gce']
  560. api_server_args['cloud-config'] = [cloud_cfg_path + '/gce.conf']
  561. if api_server_args != {}:
  562. facts = merge_facts({'master': {'api_server_args': api_server_args}}, facts, [])
  563. return facts
  564. def apply_provider_facts(facts, provider_facts):
  565. """ Apply provider facts to supplied facts dict
  566. Args:
  567. facts (dict): facts dict to update
  568. provider_facts (dict): provider facts to apply
  569. roles: host roles
  570. Returns:
  571. dict: the merged facts
  572. """
  573. if not provider_facts:
  574. return facts
  575. common_vars = [('hostname', 'ip'), ('public_hostname', 'public_ip')]
  576. for h_var, ip_var in common_vars:
  577. ip_value = provider_facts['network'].get(ip_var)
  578. if ip_value:
  579. facts['common'][ip_var] = ip_value
  580. facts['common'][h_var] = choose_hostname(
  581. [provider_facts['network'].get(h_var)],
  582. facts['common'][h_var]
  583. )
  584. facts['provider'] = provider_facts
  585. return facts
  586. # Disabling pylint too many branches. This function needs refactored
  587. # but is a very core part of openshift_facts.
  588. # pylint: disable=too-many-branches, too-many-nested-blocks
  589. def merge_facts(orig, new, additive_facts_to_overwrite):
  590. """ Recursively merge facts dicts
  591. Args:
  592. orig (dict): existing facts
  593. new (dict): facts to update
  594. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  595. '.' notation ex: ['master.named_certificates']
  596. Returns:
  597. dict: the merged facts
  598. """
  599. additive_facts = ['named_certificates']
  600. # Facts we do not ever want to merge. These originate in inventory variables
  601. # and contain JSON dicts. We don't ever want to trigger a merge
  602. # here, just completely overwrite with the new if they are present there.
  603. inventory_json_facts = ['admission_plugin_config',
  604. 'kube_admission_plugin_config',
  605. 'image_policy_config',
  606. "builddefaults",
  607. "buildoverrides"]
  608. facts = dict()
  609. for key, value in iteritems(orig):
  610. # Key exists in both old and new facts.
  611. if key in new:
  612. if key in inventory_json_facts:
  613. # Watchout for JSON facts that sometimes load as strings.
  614. # (can happen if the JSON contains a boolean)
  615. if isinstance(new[key], string_types):
  616. facts[key] = yaml.safe_load(new[key])
  617. else:
  618. facts[key] = copy.deepcopy(new[key])
  619. # Continue to recurse if old and new fact is a dictionary.
  620. elif isinstance(value, dict) and isinstance(new[key], dict):
  621. # Collect the subset of additive facts to overwrite if
  622. # key matches. These will be passed to the subsequent
  623. # merge_facts call.
  624. relevant_additive_facts = []
  625. for item in additive_facts_to_overwrite:
  626. if '.' in item and item.startswith(key + '.'):
  627. relevant_additive_facts.append(item)
  628. facts[key] = merge_facts(value, new[key], relevant_additive_facts)
  629. # Key matches an additive fact and we are not overwriting
  630. # it so we will append the new value to the existing value.
  631. elif key in additive_facts and key not in [x.split('.')[-1] for x in additive_facts_to_overwrite]:
  632. if isinstance(value, list) and isinstance(new[key], list):
  633. new_fact = []
  634. for item in copy.deepcopy(value) + copy.deepcopy(new[key]):
  635. if item not in new_fact:
  636. new_fact.append(item)
  637. facts[key] = new_fact
  638. # No other condition has been met. Overwrite the old fact
  639. # with the new value.
  640. else:
  641. facts[key] = copy.deepcopy(new[key])
  642. # Key isn't in new so add it to facts to keep it.
  643. else:
  644. facts[key] = copy.deepcopy(value)
  645. new_keys = set(new.keys()) - set(orig.keys())
  646. for key in new_keys:
  647. # Watchout for JSON facts that sometimes load as strings.
  648. # (can happen if the JSON contains a boolean)
  649. if key in inventory_json_facts and isinstance(new[key], string_types):
  650. facts[key] = yaml.safe_load(new[key])
  651. else:
  652. facts[key] = copy.deepcopy(new[key])
  653. return facts
  654. def save_local_facts(filename, facts):
  655. """ Save local facts
  656. Args:
  657. filename (str): local facts file
  658. facts (dict): facts to set
  659. """
  660. try:
  661. fact_dir = os.path.dirname(filename)
  662. try:
  663. os.makedirs(fact_dir) # try to make the directory
  664. except OSError as exception:
  665. if exception.errno != errno.EEXIST: # but it is okay if it is already there
  666. raise # pass any other exceptions up the chain
  667. with open(filename, 'w') as fact_file:
  668. fact_file.write(module.jsonify(facts)) # noqa: F405
  669. os.chmod(filename, 0o600)
  670. except (IOError, OSError) as ex:
  671. raise OpenShiftFactsFileWriteError(
  672. "Could not create fact file: %s, error: %s" % (filename, ex)
  673. )
  674. def get_local_facts_from_file(filename):
  675. """ Retrieve local facts from fact file
  676. Args:
  677. filename (str): local facts file
  678. Returns:
  679. dict: the retrieved facts
  680. """
  681. try:
  682. with open(filename, 'r') as facts_file:
  683. local_facts = json.load(facts_file)
  684. except (ValueError, IOError):
  685. local_facts = {}
  686. return local_facts
  687. def sort_unique(alist):
  688. """ Sorts and de-dupes a list
  689. Args:
  690. list: a list
  691. Returns:
  692. list: a sorted de-duped list
  693. """
  694. return sorted(list(set(alist)))
  695. def safe_get_bool(fact):
  696. """ Get a boolean fact safely.
  697. Args:
  698. facts: fact to convert
  699. Returns:
  700. bool: given fact as a bool
  701. """
  702. return bool(strtobool(str(fact)))
  703. def set_proxy_facts(facts):
  704. """ Set global proxy facts
  705. Args:
  706. facts(dict): existing facts
  707. Returns:
  708. facts(dict): Updated facts with missing values
  709. """
  710. if 'common' in facts:
  711. common = facts['common']
  712. if 'http_proxy' in common or 'https_proxy' in common or 'no_proxy' in common:
  713. if 'no_proxy' in common and isinstance(common['no_proxy'], string_types):
  714. common['no_proxy'] = common['no_proxy'].split(",")
  715. elif 'no_proxy' not in common:
  716. common['no_proxy'] = []
  717. # See https://bugzilla.redhat.com/show_bug.cgi?id=1466783
  718. # masters behind a proxy need to connect to etcd via IP
  719. if 'no_proxy_etcd_host_ips' in common:
  720. if isinstance(common['no_proxy_etcd_host_ips'], string_types):
  721. common['no_proxy'].extend(common['no_proxy_etcd_host_ips'].split(','))
  722. if 'generate_no_proxy_hosts' in common and safe_get_bool(common['generate_no_proxy_hosts']):
  723. if 'no_proxy_internal_hostnames' in common:
  724. common['no_proxy'].extend(common['no_proxy_internal_hostnames'].split(','))
  725. # We always add local dns domain and ourselves no matter what
  726. kube_svc_ip = str(ipaddress.ip_network(text_type(common['portal_net']))[1])
  727. common['no_proxy'].append(kube_svc_ip)
  728. common['no_proxy'].append('.' + common['dns_domain'])
  729. common['no_proxy'].append('.svc')
  730. common['no_proxy'].append(common['hostname'])
  731. common['no_proxy'] = ','.join(sort_unique(common['no_proxy']))
  732. facts['common'] = common
  733. return facts
  734. def set_builddefaults_facts(facts):
  735. """ Set build defaults including setting proxy values from http_proxy, https_proxy,
  736. no_proxy to the more specific builddefaults and builddefaults_git vars.
  737. 1. http_proxy, https_proxy, no_proxy
  738. 2. builddefaults_*
  739. 3. builddefaults_git_*
  740. Args:
  741. facts(dict): existing facts
  742. Returns:
  743. facts(dict): Updated facts with missing values
  744. """
  745. if 'builddefaults' in facts:
  746. builddefaults = facts['builddefaults']
  747. common = facts['common']
  748. # Copy values from common to builddefaults
  749. if 'http_proxy' not in builddefaults and 'http_proxy' in common:
  750. builddefaults['http_proxy'] = common['http_proxy']
  751. if 'https_proxy' not in builddefaults and 'https_proxy' in common:
  752. builddefaults['https_proxy'] = common['https_proxy']
  753. if 'no_proxy' not in builddefaults and 'no_proxy' in common:
  754. builddefaults['no_proxy'] = common['no_proxy']
  755. # Create git specific facts from generic values, if git specific values are
  756. # not defined.
  757. if 'git_http_proxy' not in builddefaults and 'http_proxy' in builddefaults:
  758. builddefaults['git_http_proxy'] = builddefaults['http_proxy']
  759. if 'git_https_proxy' not in builddefaults and 'https_proxy' in builddefaults:
  760. builddefaults['git_https_proxy'] = builddefaults['https_proxy']
  761. if 'git_no_proxy' not in builddefaults and 'no_proxy' in builddefaults:
  762. builddefaults['git_no_proxy'] = builddefaults['no_proxy']
  763. # If we're actually defining a builddefaults config then create admission_plugin_config
  764. # then merge builddefaults[config] structure into admission_plugin_config
  765. # 'config' is the 'openshift_builddefaults_json' inventory variable
  766. if 'config' in builddefaults:
  767. if 'admission_plugin_config' not in facts['master']:
  768. # Scaffold out the full expected datastructure
  769. facts['master']['admission_plugin_config'] = {'BuildDefaults': {'configuration': {'env': {}}}}
  770. facts['master']['admission_plugin_config'].update(builddefaults['config'])
  771. if 'env' in facts['master']['admission_plugin_config']['BuildDefaults']['configuration']:
  772. delete_empty_keys(facts['master']['admission_plugin_config']['BuildDefaults']['configuration']['env'])
  773. return facts
  774. def delete_empty_keys(keylist):
  775. """ Delete dictionary elements from keylist where "value" is empty.
  776. Args:
  777. keylist(list): A list of builddefault configuration envs.
  778. Returns:
  779. none
  780. Example:
  781. keylist = [{'name': 'HTTP_PROXY', 'value': 'http://file.rdu.redhat.com:3128'},
  782. {'name': 'HTTPS_PROXY', 'value': 'http://file.rdu.redhat.com:3128'},
  783. {'name': 'NO_PROXY', 'value': ''}]
  784. After calling delete_empty_keys the provided list is modified to become:
  785. [{'name': 'HTTP_PROXY', 'value': 'http://file.rdu.redhat.com:3128'},
  786. {'name': 'HTTPS_PROXY', 'value': 'http://file.rdu.redhat.com:3128'}]
  787. """
  788. count = 0
  789. for i in range(0, len(keylist)):
  790. if len(keylist[i - count]['value']) == 0:
  791. del keylist[i - count]
  792. count += 1
  793. def set_buildoverrides_facts(facts):
  794. """ Set build overrides
  795. Args:
  796. facts(dict): existing facts
  797. Returns:
  798. facts(dict): Updated facts with missing values
  799. """
  800. if 'buildoverrides' in facts:
  801. buildoverrides = facts['buildoverrides']
  802. # If we're actually defining a buildoverrides config then create admission_plugin_config
  803. # then merge buildoverrides[config] structure into admission_plugin_config
  804. if 'config' in buildoverrides:
  805. if 'admission_plugin_config' not in facts['master']:
  806. facts['master']['admission_plugin_config'] = dict()
  807. facts['master']['admission_plugin_config'].update(buildoverrides['config'])
  808. return facts
  809. def pop_obsolete_local_facts(local_facts):
  810. """Remove unused keys from local_facts"""
  811. keys_to_remove = {
  812. 'master': ('etcd_port', 'etcd_use_ssl', 'etcd_hosts')
  813. }
  814. for role in keys_to_remove:
  815. if role in local_facts:
  816. for key in keys_to_remove[role]:
  817. local_facts[role].pop(key, None)
  818. class OpenShiftFactsUnsupportedRoleError(Exception):
  819. """Origin Facts Unsupported Role Error"""
  820. pass
  821. class OpenShiftFactsFileWriteError(Exception):
  822. """Origin Facts File Write Error"""
  823. pass
  824. class OpenShiftFactsMetadataUnavailableError(Exception):
  825. """Origin Facts Metadata Unavailable Error"""
  826. pass
  827. class OpenShiftFacts(object):
  828. """ Origin Facts
  829. Attributes:
  830. facts (dict): facts for the host
  831. Args:
  832. module (AnsibleModule): an AnsibleModule object
  833. role (str): role for setting local facts
  834. filename (str): local facts file to use
  835. local_facts (dict): local facts to set
  836. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  837. '.' notation ex: ['master.named_certificates']
  838. Raises:
  839. OpenShiftFactsUnsupportedRoleError:
  840. """
  841. known_roles = ['builddefaults',
  842. 'buildoverrides',
  843. 'cloudprovider',
  844. 'common',
  845. 'etcd',
  846. 'master',
  847. 'node']
  848. # Disabling too-many-arguments, this should be cleaned up as a TODO item.
  849. # pylint: disable=too-many-arguments,no-value-for-parameter
  850. def __init__(self, role, filename, local_facts,
  851. additive_facts_to_overwrite=None):
  852. self.changed = False
  853. self.filename = filename
  854. if role not in self.known_roles:
  855. raise OpenShiftFactsUnsupportedRoleError(
  856. "Role %s is not supported by this module" % role
  857. )
  858. self.role = role
  859. # Collect system facts and preface each fact with 'ansible_'.
  860. try:
  861. # pylint: disable=too-many-function-args,invalid-name
  862. self.system_facts = ansible_facts(module, ['hardware', 'network', 'virtual', 'facter']) # noqa: F405
  863. additional_facts = {}
  864. for (k, v) in self.system_facts.items():
  865. additional_facts["ansible_%s" % k.replace('-', '_')] = v
  866. self.system_facts.update(additional_facts)
  867. except UnboundLocalError:
  868. # ansible-2.2,2.3
  869. self.system_facts = get_all_facts(module)['ansible_facts'] # noqa: F405
  870. self.facts = self.generate_facts(local_facts,
  871. additive_facts_to_overwrite)
  872. def generate_facts(self,
  873. local_facts,
  874. additive_facts_to_overwrite):
  875. """ Generate facts
  876. Args:
  877. local_facts (dict): local_facts for overriding generated defaults
  878. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  879. '.' notation ex: ['master.named_certificates']
  880. Returns:
  881. dict: The generated facts
  882. """
  883. local_facts = self.init_local_facts(local_facts,
  884. additive_facts_to_overwrite)
  885. roles = local_facts.keys()
  886. defaults = self.get_defaults(roles)
  887. provider_facts = self.init_provider_facts()
  888. facts = apply_provider_facts(defaults, provider_facts)
  889. facts = merge_facts(facts,
  890. local_facts,
  891. additive_facts_to_overwrite)
  892. facts['current_config'] = get_current_config(facts)
  893. facts = set_url_facts_if_unset(facts)
  894. facts = set_sdn_facts_if_unset(facts, self.system_facts)
  895. facts = build_controller_args(facts)
  896. facts = build_api_server_args(facts)
  897. facts = set_aggregate_facts(facts)
  898. facts = set_proxy_facts(facts)
  899. facts = set_builddefaults_facts(facts)
  900. facts = set_buildoverrides_facts(facts)
  901. facts = set_nodename(facts)
  902. return dict(openshift=facts)
  903. def get_defaults(self, roles):
  904. """ Get default fact values
  905. Args:
  906. roles (list): list of roles for this host
  907. Returns:
  908. dict: The generated default facts
  909. """
  910. defaults = {}
  911. ip_addr = self.system_facts['ansible_default_ipv4']['address']
  912. exit_code, output, _ = module.run_command(['hostname', '-f']) # noqa: F405
  913. hostname_f = output.strip() if exit_code == 0 else ''
  914. hostname_values = [hostname_f, self.system_facts['ansible_nodename'],
  915. self.system_facts['ansible_fqdn']]
  916. hostname = choose_hostname(hostname_values, ip_addr).lower()
  917. exit_code, output, _ = module.run_command(['hostname']) # noqa: F405
  918. raw_hostname = output.strip() if exit_code == 0 else hostname
  919. defaults['common'] = dict(ip=ip_addr,
  920. public_ip=ip_addr,
  921. raw_hostname=raw_hostname,
  922. hostname=hostname,
  923. public_hostname=hostname,
  924. portal_net='172.30.0.0/16',
  925. dns_domain='cluster.local',
  926. config_base='/etc/origin')
  927. if 'master' in roles:
  928. defaults['master'] = dict(api_use_ssl=True, api_port='8443',
  929. controllers_port='8444',
  930. console_use_ssl=True,
  931. console_path='/console',
  932. console_port='8443',
  933. portal_net='172.30.0.0/16',
  934. bind_addr='0.0.0.0',
  935. session_max_seconds=3600,
  936. session_name='ssn')
  937. if 'cloudprovider' in roles:
  938. defaults['cloudprovider'] = dict(kind=None)
  939. return defaults
  940. def guess_host_provider(self):
  941. """ Guess the host provider
  942. Returns:
  943. dict: The generated default facts for the detected provider
  944. """
  945. # TODO: cloud provider facts should probably be submitted upstream
  946. product_name = self.system_facts['ansible_product_name']
  947. product_version = self.system_facts['ansible_product_version']
  948. virt_type = self.system_facts['ansible_virtualization_type']
  949. virt_role = self.system_facts['ansible_virtualization_role']
  950. bios_vendor = self.system_facts['ansible_system_vendor']
  951. provider = None
  952. metadata = None
  953. if bios_vendor == 'Google':
  954. provider = 'gce'
  955. metadata_url = ('http://metadata.google.internal/'
  956. 'computeMetadata/v1/?recursive=true')
  957. headers = {'Metadata-Flavor': 'Google'}
  958. metadata = get_provider_metadata(metadata_url, True, headers,
  959. True)
  960. # Filter sshKeys and serviceAccounts from gce metadata
  961. if metadata:
  962. metadata['project']['attributes'].pop('sshKeys', None)
  963. metadata['instance'].pop('serviceAccounts', None)
  964. elif bios_vendor == 'Amazon EC2':
  965. # Adds support for Amazon EC2 C5 instance types
  966. provider = 'aws'
  967. metadata_url = 'http://169.254.169.254/latest/meta-data/'
  968. metadata = get_provider_metadata(metadata_url)
  969. elif virt_type == 'xen' and virt_role == 'guest' and re.match(r'.*\.amazon$', product_version):
  970. provider = 'aws'
  971. metadata_url = 'http://169.254.169.254/latest/meta-data/'
  972. metadata = get_provider_metadata(metadata_url)
  973. elif re.search(r'OpenStack', product_name):
  974. provider = 'openstack'
  975. metadata_url = ('http://169.254.169.254/openstack/latest/'
  976. 'meta_data.json')
  977. metadata = get_provider_metadata(metadata_url, True, None,
  978. True)
  979. if metadata:
  980. ec2_compat_url = 'http://169.254.169.254/latest/meta-data/'
  981. metadata['ec2_compat'] = get_provider_metadata(
  982. ec2_compat_url
  983. )
  984. # disable pylint maybe-no-member because overloaded use of
  985. # the module name causes pylint to not detect that results
  986. # is an array or hash
  987. # pylint: disable=maybe-no-member
  988. # Filter public_keys and random_seed from openstack metadata
  989. metadata.pop('public_keys', None)
  990. metadata.pop('random_seed', None)
  991. if not metadata['ec2_compat']:
  992. metadata = None
  993. return dict(name=provider, metadata=metadata)
  994. def init_provider_facts(self):
  995. """ Initialize the provider facts
  996. Returns:
  997. dict: The normalized provider facts
  998. """
  999. provider_info = self.guess_host_provider()
  1000. provider_facts = normalize_provider_facts(
  1001. provider_info.get('name'),
  1002. provider_info.get('metadata')
  1003. )
  1004. return provider_facts
  1005. # Disabling too-many-branches and too-many-locals.
  1006. # This should be cleaned up as a TODO item.
  1007. # pylint: disable=too-many-branches, too-many-locals
  1008. def init_local_facts(self, facts=None,
  1009. additive_facts_to_overwrite=None):
  1010. """ Initialize the local facts
  1011. Args:
  1012. facts (dict): local facts to set
  1013. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  1014. '.' notation ex: ['master.named_certificates']
  1015. Returns:
  1016. dict: The result of merging the provided facts with existing
  1017. local facts
  1018. """
  1019. changed = False
  1020. facts_to_set = dict()
  1021. if facts is not None:
  1022. facts_to_set[self.role] = facts
  1023. local_facts = get_local_facts_from_file(self.filename)
  1024. migrated_facts = migrate_local_facts(local_facts)
  1025. new_local_facts = merge_facts(migrated_facts,
  1026. facts_to_set,
  1027. additive_facts_to_overwrite)
  1028. new_local_facts = self.remove_empty_facts(new_local_facts)
  1029. pop_obsolete_local_facts(new_local_facts)
  1030. if new_local_facts != local_facts:
  1031. changed = True
  1032. if not module.check_mode: # noqa: F405
  1033. save_local_facts(self.filename, new_local_facts)
  1034. self.changed = changed
  1035. return new_local_facts
  1036. def remove_empty_facts(self, facts=None):
  1037. """ Remove empty facts
  1038. Args:
  1039. facts (dict): facts to clean
  1040. """
  1041. facts_to_remove = []
  1042. for fact, value in iteritems(facts):
  1043. if isinstance(facts[fact], dict):
  1044. facts[fact] = self.remove_empty_facts(facts[fact])
  1045. else:
  1046. if value == "" or value == [""] or value is None:
  1047. facts_to_remove.append(fact)
  1048. for fact in facts_to_remove:
  1049. del facts[fact]
  1050. return facts
  1051. def main():
  1052. """ main """
  1053. # disabling pylint errors for global-variable-undefined and invalid-name
  1054. # for 'global module' usage, since it is required to use ansible_facts
  1055. # pylint: disable=global-variable-undefined, invalid-name
  1056. global module
  1057. module = AnsibleModule( # noqa: F405
  1058. argument_spec=dict(
  1059. role=dict(default='common', required=False,
  1060. choices=OpenShiftFacts.known_roles),
  1061. local_facts=dict(default=None, type='dict', required=False),
  1062. additive_facts_to_overwrite=dict(default=[], type='list', required=False),
  1063. ),
  1064. supports_check_mode=True,
  1065. add_file_common_args=True,
  1066. )
  1067. module.params['gather_subset'] = ['hardware', 'network', 'virtual', 'facter'] # noqa: F405
  1068. module.params['gather_timeout'] = 10 # noqa: F405
  1069. module.params['filter'] = '*' # noqa: F405
  1070. role = module.params['role'] # noqa: F405
  1071. local_facts = module.params['local_facts'] # noqa: F405
  1072. additive_facts_to_overwrite = module.params['additive_facts_to_overwrite'] # noqa: F405
  1073. fact_file = '/etc/ansible/facts.d/openshift.fact'
  1074. openshift_facts = OpenShiftFacts(role,
  1075. fact_file,
  1076. local_facts,
  1077. additive_facts_to_overwrite)
  1078. file_params = module.params.copy() # noqa: F405
  1079. file_params['path'] = fact_file
  1080. file_args = module.load_file_common_arguments(file_params) # noqa: F405
  1081. changed = module.set_fs_attributes_if_different(file_args, # noqa: F405
  1082. openshift_facts.changed)
  1083. return module.exit_json(changed=changed, # noqa: F405
  1084. ansible_facts=openshift_facts.facts)
  1085. if __name__ == '__main__':
  1086. main()