openshift_facts.py 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300
  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'] == 'openstack':
  530. controller_args['cloud-provider'] = ['openstack']
  531. controller_args['cloud-config'] = [cloud_cfg_path + '/openstack.conf']
  532. if facts['cloudprovider']['kind'] == 'gce':
  533. controller_args['cloud-provider'] = ['gce']
  534. controller_args['cloud-config'] = [cloud_cfg_path + '/gce.conf']
  535. if controller_args != {}:
  536. facts = merge_facts({'master': {'controller_args': controller_args}}, facts, [])
  537. return facts
  538. def build_api_server_args(facts):
  539. """ Build master api_server_args """
  540. cloud_cfg_path = os.path.join(facts['common']['config_base'],
  541. 'cloudprovider')
  542. if 'master' in facts:
  543. api_server_args = {}
  544. if 'cloudprovider' in facts:
  545. if 'kind' in facts['cloudprovider']:
  546. if facts['cloudprovider']['kind'] == 'aws':
  547. api_server_args['cloud-provider'] = ['aws']
  548. api_server_args['cloud-config'] = [cloud_cfg_path + '/aws.conf']
  549. if facts['cloudprovider']['kind'] == 'openstack':
  550. api_server_args['cloud-provider'] = ['openstack']
  551. api_server_args['cloud-config'] = [cloud_cfg_path + '/openstack.conf']
  552. if facts['cloudprovider']['kind'] == 'gce':
  553. api_server_args['cloud-provider'] = ['gce']
  554. api_server_args['cloud-config'] = [cloud_cfg_path + '/gce.conf']
  555. if api_server_args != {}:
  556. facts = merge_facts({'master': {'api_server_args': api_server_args}}, facts, [])
  557. return facts
  558. def apply_provider_facts(facts, provider_facts):
  559. """ Apply provider facts to supplied facts dict
  560. Args:
  561. facts (dict): facts dict to update
  562. provider_facts (dict): provider facts to apply
  563. roles: host roles
  564. Returns:
  565. dict: the merged facts
  566. """
  567. if not provider_facts:
  568. return facts
  569. common_vars = [('hostname', 'ip'), ('public_hostname', 'public_ip')]
  570. for h_var, ip_var in common_vars:
  571. ip_value = provider_facts['network'].get(ip_var)
  572. if ip_value:
  573. facts['common'][ip_var] = ip_value
  574. facts['common'][h_var] = choose_hostname(
  575. [provider_facts['network'].get(h_var)],
  576. facts['common'][h_var]
  577. )
  578. facts['provider'] = provider_facts
  579. return facts
  580. # Disabling pylint too many branches. This function needs refactored
  581. # but is a very core part of openshift_facts.
  582. # pylint: disable=too-many-branches, too-many-nested-blocks
  583. def merge_facts(orig, new, additive_facts_to_overwrite):
  584. """ Recursively merge facts dicts
  585. Args:
  586. orig (dict): existing facts
  587. new (dict): facts to update
  588. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  589. '.' notation ex: ['master.named_certificates']
  590. Returns:
  591. dict: the merged facts
  592. """
  593. additive_facts = ['named_certificates']
  594. # Facts we do not ever want to merge. These originate in inventory variables
  595. # and contain JSON dicts. We don't ever want to trigger a merge
  596. # here, just completely overwrite with the new if they are present there.
  597. inventory_json_facts = ['admission_plugin_config',
  598. 'kube_admission_plugin_config',
  599. 'image_policy_config',
  600. "builddefaults",
  601. "buildoverrides"]
  602. facts = dict()
  603. for key, value in iteritems(orig):
  604. # Key exists in both old and new facts.
  605. if key in new:
  606. if key in inventory_json_facts:
  607. # Watchout for JSON facts that sometimes load as strings.
  608. # (can happen if the JSON contains a boolean)
  609. if isinstance(new[key], string_types):
  610. facts[key] = yaml.safe_load(new[key])
  611. else:
  612. facts[key] = copy.deepcopy(new[key])
  613. # Continue to recurse if old and new fact is a dictionary.
  614. elif isinstance(value, dict) and isinstance(new[key], dict):
  615. # Collect the subset of additive facts to overwrite if
  616. # key matches. These will be passed to the subsequent
  617. # merge_facts call.
  618. relevant_additive_facts = []
  619. for item in additive_facts_to_overwrite:
  620. if '.' in item and item.startswith(key + '.'):
  621. relevant_additive_facts.append(item)
  622. facts[key] = merge_facts(value, new[key], relevant_additive_facts)
  623. # Key matches an additive fact and we are not overwriting
  624. # it so we will append the new value to the existing value.
  625. elif key in additive_facts and key not in [x.split('.')[-1] for x in additive_facts_to_overwrite]:
  626. if isinstance(value, list) and isinstance(new[key], list):
  627. new_fact = []
  628. for item in copy.deepcopy(value) + copy.deepcopy(new[key]):
  629. if item not in new_fact:
  630. new_fact.append(item)
  631. facts[key] = new_fact
  632. # No other condition has been met. Overwrite the old fact
  633. # with the new value.
  634. else:
  635. facts[key] = copy.deepcopy(new[key])
  636. # Key isn't in new so add it to facts to keep it.
  637. else:
  638. facts[key] = copy.deepcopy(value)
  639. new_keys = set(new.keys()) - set(orig.keys())
  640. for key in new_keys:
  641. # Watchout for JSON facts that sometimes load as strings.
  642. # (can happen if the JSON contains a boolean)
  643. if key in inventory_json_facts and isinstance(new[key], string_types):
  644. facts[key] = yaml.safe_load(new[key])
  645. else:
  646. facts[key] = copy.deepcopy(new[key])
  647. return facts
  648. def save_local_facts(filename, facts):
  649. """ Save local facts
  650. Args:
  651. filename (str): local facts file
  652. facts (dict): facts to set
  653. """
  654. try:
  655. fact_dir = os.path.dirname(filename)
  656. try:
  657. os.makedirs(fact_dir) # try to make the directory
  658. except OSError as exception:
  659. if exception.errno != errno.EEXIST: # but it is okay if it is already there
  660. raise # pass any other exceptions up the chain
  661. with open(filename, 'w') as fact_file:
  662. fact_file.write(module.jsonify(facts)) # noqa: F405
  663. os.chmod(filename, 0o600)
  664. except (IOError, OSError) as ex:
  665. raise OpenShiftFactsFileWriteError(
  666. "Could not create fact file: %s, error: %s" % (filename, ex)
  667. )
  668. def get_local_facts_from_file(filename):
  669. """ Retrieve local facts from fact file
  670. Args:
  671. filename (str): local facts file
  672. Returns:
  673. dict: the retrieved facts
  674. """
  675. try:
  676. with open(filename, 'r') as facts_file:
  677. local_facts = json.load(facts_file)
  678. except (ValueError, IOError):
  679. local_facts = {}
  680. return local_facts
  681. def sort_unique(alist):
  682. """ Sorts and de-dupes a list
  683. Args:
  684. list: a list
  685. Returns:
  686. list: a sorted de-duped list
  687. """
  688. return sorted(list(set(alist)))
  689. def safe_get_bool(fact):
  690. """ Get a boolean fact safely.
  691. Args:
  692. facts: fact to convert
  693. Returns:
  694. bool: given fact as a bool
  695. """
  696. return bool(strtobool(str(fact)))
  697. def set_proxy_facts(facts):
  698. """ Set global proxy facts
  699. Args:
  700. facts(dict): existing facts
  701. Returns:
  702. facts(dict): Updated facts with missing values
  703. """
  704. if 'common' in facts:
  705. common = facts['common']
  706. if 'http_proxy' in common or 'https_proxy' in common or 'no_proxy' in common:
  707. if 'no_proxy' in common and isinstance(common['no_proxy'], string_types):
  708. common['no_proxy'] = common['no_proxy'].split(",")
  709. elif 'no_proxy' not in common:
  710. common['no_proxy'] = []
  711. # See https://bugzilla.redhat.com/show_bug.cgi?id=1466783
  712. # masters behind a proxy need to connect to etcd via IP
  713. if 'no_proxy_etcd_host_ips' in common:
  714. if isinstance(common['no_proxy_etcd_host_ips'], string_types):
  715. common['no_proxy'].extend(common['no_proxy_etcd_host_ips'].split(','))
  716. if 'generate_no_proxy_hosts' in common and safe_get_bool(common['generate_no_proxy_hosts']):
  717. if 'no_proxy_internal_hostnames' in common:
  718. common['no_proxy'].extend(common['no_proxy_internal_hostnames'].split(','))
  719. # We always add local dns domain and ourselves no matter what
  720. kube_svc_ip = str(ipaddress.ip_network(text_type(common['portal_net']))[1])
  721. common['no_proxy'].append(kube_svc_ip)
  722. common['no_proxy'].append('.' + common['dns_domain'])
  723. common['no_proxy'].append('.svc')
  724. common['no_proxy'].append(common['hostname'])
  725. common['no_proxy'] = ','.join(sort_unique(common['no_proxy']))
  726. facts['common'] = common
  727. return facts
  728. def set_builddefaults_facts(facts):
  729. """ Set build defaults including setting proxy values from http_proxy, https_proxy,
  730. no_proxy to the more specific builddefaults and builddefaults_git vars.
  731. 1. http_proxy, https_proxy, no_proxy
  732. 2. builddefaults_*
  733. 3. builddefaults_git_*
  734. Args:
  735. facts(dict): existing facts
  736. Returns:
  737. facts(dict): Updated facts with missing values
  738. """
  739. if 'builddefaults' in facts:
  740. builddefaults = facts['builddefaults']
  741. common = facts['common']
  742. # Copy values from common to builddefaults
  743. if 'http_proxy' not in builddefaults and 'http_proxy' in common:
  744. builddefaults['http_proxy'] = common['http_proxy']
  745. if 'https_proxy' not in builddefaults and 'https_proxy' in common:
  746. builddefaults['https_proxy'] = common['https_proxy']
  747. if 'no_proxy' not in builddefaults and 'no_proxy' in common:
  748. builddefaults['no_proxy'] = common['no_proxy']
  749. # Create git specific facts from generic values, if git specific values are
  750. # not defined.
  751. if 'git_http_proxy' not in builddefaults and 'http_proxy' in builddefaults:
  752. builddefaults['git_http_proxy'] = builddefaults['http_proxy']
  753. if 'git_https_proxy' not in builddefaults and 'https_proxy' in builddefaults:
  754. builddefaults['git_https_proxy'] = builddefaults['https_proxy']
  755. if 'git_no_proxy' not in builddefaults and 'no_proxy' in builddefaults:
  756. builddefaults['git_no_proxy'] = builddefaults['no_proxy']
  757. # If we're actually defining a builddefaults config then create admission_plugin_config
  758. # then merge builddefaults[config] structure into admission_plugin_config
  759. # 'config' is the 'openshift_builddefaults_json' inventory variable
  760. if 'config' in builddefaults:
  761. if 'admission_plugin_config' not in facts['master']:
  762. # Scaffold out the full expected datastructure
  763. facts['master']['admission_plugin_config'] = {'BuildDefaults': {'configuration': {'env': {}}}}
  764. facts['master']['admission_plugin_config'].update(builddefaults['config'])
  765. if 'env' in facts['master']['admission_plugin_config']['BuildDefaults']['configuration']:
  766. delete_empty_keys(facts['master']['admission_plugin_config']['BuildDefaults']['configuration']['env'])
  767. return facts
  768. def delete_empty_keys(keylist):
  769. """ Delete dictionary elements from keylist where "value" is empty.
  770. Args:
  771. keylist(list): A list of builddefault configuration envs.
  772. Returns:
  773. none
  774. Example:
  775. keylist = [{'name': 'HTTP_PROXY', 'value': 'http://file.rdu.redhat.com:3128'},
  776. {'name': 'HTTPS_PROXY', 'value': 'http://file.rdu.redhat.com:3128'},
  777. {'name': 'NO_PROXY', 'value': ''}]
  778. After calling delete_empty_keys the provided list is modified to become:
  779. [{'name': 'HTTP_PROXY', 'value': 'http://file.rdu.redhat.com:3128'},
  780. {'name': 'HTTPS_PROXY', 'value': 'http://file.rdu.redhat.com:3128'}]
  781. """
  782. count = 0
  783. for i in range(0, len(keylist)):
  784. if len(keylist[i - count]['value']) == 0:
  785. del keylist[i - count]
  786. count += 1
  787. def set_buildoverrides_facts(facts):
  788. """ Set build overrides
  789. Args:
  790. facts(dict): existing facts
  791. Returns:
  792. facts(dict): Updated facts with missing values
  793. """
  794. if 'buildoverrides' in facts:
  795. buildoverrides = facts['buildoverrides']
  796. # If we're actually defining a buildoverrides config then create admission_plugin_config
  797. # then merge buildoverrides[config] structure into admission_plugin_config
  798. if 'config' in buildoverrides:
  799. if 'admission_plugin_config' not in facts['master']:
  800. facts['master']['admission_plugin_config'] = dict()
  801. facts['master']['admission_plugin_config'].update(buildoverrides['config'])
  802. return facts
  803. def pop_obsolete_local_facts(local_facts):
  804. """Remove unused keys from local_facts"""
  805. keys_to_remove = {
  806. 'master': ('etcd_port', 'etcd_use_ssl', 'etcd_hosts')
  807. }
  808. for role in keys_to_remove:
  809. if role in local_facts:
  810. for key in keys_to_remove[role]:
  811. local_facts[role].pop(key, None)
  812. class OpenShiftFactsUnsupportedRoleError(Exception):
  813. """Origin Facts Unsupported Role Error"""
  814. pass
  815. class OpenShiftFactsFileWriteError(Exception):
  816. """Origin Facts File Write Error"""
  817. pass
  818. class OpenShiftFactsMetadataUnavailableError(Exception):
  819. """Origin Facts Metadata Unavailable Error"""
  820. pass
  821. class OpenShiftFacts(object):
  822. """ Origin Facts
  823. Attributes:
  824. facts (dict): facts for the host
  825. Args:
  826. module (AnsibleModule): an AnsibleModule object
  827. role (str): role for setting local facts
  828. filename (str): local facts file to use
  829. local_facts (dict): local facts to set
  830. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  831. '.' notation ex: ['master.named_certificates']
  832. Raises:
  833. OpenShiftFactsUnsupportedRoleError:
  834. """
  835. known_roles = ['builddefaults',
  836. 'buildoverrides',
  837. 'cloudprovider',
  838. 'common',
  839. 'etcd',
  840. 'master',
  841. 'node']
  842. # Disabling too-many-arguments, this should be cleaned up as a TODO item.
  843. # pylint: disable=too-many-arguments,no-value-for-parameter
  844. def __init__(self, role, filename, local_facts,
  845. additive_facts_to_overwrite=None):
  846. self.changed = False
  847. self.filename = filename
  848. if role not in self.known_roles:
  849. raise OpenShiftFactsUnsupportedRoleError(
  850. "Role %s is not supported by this module" % role
  851. )
  852. self.role = role
  853. # Collect system facts and preface each fact with 'ansible_'.
  854. try:
  855. # pylint: disable=too-many-function-args,invalid-name
  856. self.system_facts = ansible_facts(module, ['hardware', 'network', 'virtual', 'facter']) # noqa: F405
  857. additional_facts = {}
  858. for (k, v) in self.system_facts.items():
  859. additional_facts["ansible_%s" % k.replace('-', '_')] = v
  860. self.system_facts.update(additional_facts)
  861. except UnboundLocalError:
  862. # ansible-2.2,2.3
  863. self.system_facts = get_all_facts(module)['ansible_facts'] # noqa: F405
  864. self.facts = self.generate_facts(local_facts,
  865. additive_facts_to_overwrite)
  866. def generate_facts(self,
  867. local_facts,
  868. additive_facts_to_overwrite):
  869. """ Generate facts
  870. Args:
  871. local_facts (dict): local_facts for overriding generated defaults
  872. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  873. '.' notation ex: ['master.named_certificates']
  874. Returns:
  875. dict: The generated facts
  876. """
  877. local_facts = self.init_local_facts(local_facts,
  878. additive_facts_to_overwrite)
  879. roles = local_facts.keys()
  880. defaults = self.get_defaults(roles)
  881. provider_facts = self.init_provider_facts()
  882. facts = apply_provider_facts(defaults, provider_facts)
  883. facts = merge_facts(facts,
  884. local_facts,
  885. additive_facts_to_overwrite)
  886. facts['current_config'] = get_current_config(facts)
  887. facts = set_url_facts_if_unset(facts)
  888. facts = set_sdn_facts_if_unset(facts, self.system_facts)
  889. facts = build_controller_args(facts)
  890. facts = build_api_server_args(facts)
  891. facts = set_aggregate_facts(facts)
  892. facts = set_proxy_facts(facts)
  893. facts = set_builddefaults_facts(facts)
  894. facts = set_buildoverrides_facts(facts)
  895. facts = set_nodename(facts)
  896. return dict(openshift=facts)
  897. def get_defaults(self, roles):
  898. """ Get default fact values
  899. Args:
  900. roles (list): list of roles for this host
  901. Returns:
  902. dict: The generated default facts
  903. """
  904. defaults = {}
  905. ip_addr = self.system_facts['ansible_default_ipv4']['address']
  906. exit_code, output, _ = module.run_command(['hostname', '-f']) # noqa: F405
  907. hostname_f = output.strip() if exit_code == 0 else ''
  908. hostname_values = [hostname_f, self.system_facts['ansible_nodename'],
  909. self.system_facts['ansible_fqdn']]
  910. hostname = choose_hostname(hostname_values, ip_addr).lower()
  911. exit_code, output, _ = module.run_command(['hostname']) # noqa: F405
  912. raw_hostname = output.strip() if exit_code == 0 else hostname
  913. defaults['common'] = dict(ip=ip_addr,
  914. public_ip=ip_addr,
  915. raw_hostname=raw_hostname,
  916. hostname=hostname,
  917. public_hostname=hostname,
  918. portal_net='172.30.0.0/16',
  919. dns_domain='cluster.local',
  920. config_base='/etc/origin')
  921. if 'master' in roles:
  922. defaults['master'] = dict(api_use_ssl=True, api_port='8443',
  923. controllers_port='8444',
  924. console_use_ssl=True,
  925. console_path='/console',
  926. console_port='8443',
  927. portal_net='172.30.0.0/16',
  928. bind_addr='0.0.0.0',
  929. session_max_seconds=3600,
  930. session_name='ssn')
  931. if 'cloudprovider' in roles:
  932. defaults['cloudprovider'] = dict(kind=None)
  933. return defaults
  934. def guess_host_provider(self):
  935. """ Guess the host provider
  936. Returns:
  937. dict: The generated default facts for the detected provider
  938. """
  939. # TODO: cloud provider facts should probably be submitted upstream
  940. product_name = self.system_facts['ansible_product_name']
  941. product_version = self.system_facts['ansible_product_version']
  942. virt_type = self.system_facts['ansible_virtualization_type']
  943. virt_role = self.system_facts['ansible_virtualization_role']
  944. bios_vendor = self.system_facts['ansible_system_vendor']
  945. provider = None
  946. metadata = None
  947. if bios_vendor == 'Google':
  948. provider = 'gce'
  949. metadata_url = ('http://metadata.google.internal/'
  950. 'computeMetadata/v1/?recursive=true')
  951. headers = {'Metadata-Flavor': 'Google'}
  952. metadata = get_provider_metadata(metadata_url, True, headers,
  953. True)
  954. # Filter sshKeys and serviceAccounts from gce metadata
  955. if metadata:
  956. metadata['project']['attributes'].pop('sshKeys', None)
  957. metadata['instance'].pop('serviceAccounts', None)
  958. elif bios_vendor == 'Amazon EC2':
  959. # Adds support for Amazon EC2 C5 instance types
  960. provider = 'aws'
  961. metadata_url = 'http://169.254.169.254/latest/meta-data/'
  962. metadata = get_provider_metadata(metadata_url)
  963. elif virt_type == 'xen' and virt_role == 'guest' and re.match(r'.*\.amazon$', product_version):
  964. provider = 'aws'
  965. metadata_url = 'http://169.254.169.254/latest/meta-data/'
  966. metadata = get_provider_metadata(metadata_url)
  967. elif re.search(r'OpenStack', product_name):
  968. provider = 'openstack'
  969. metadata_url = ('http://169.254.169.254/openstack/latest/'
  970. 'meta_data.json')
  971. metadata = get_provider_metadata(metadata_url, True, None,
  972. True)
  973. if metadata:
  974. ec2_compat_url = 'http://169.254.169.254/latest/meta-data/'
  975. metadata['ec2_compat'] = get_provider_metadata(
  976. ec2_compat_url
  977. )
  978. # disable pylint maybe-no-member because overloaded use of
  979. # the module name causes pylint to not detect that results
  980. # is an array or hash
  981. # pylint: disable=maybe-no-member
  982. # Filter public_keys and random_seed from openstack metadata
  983. metadata.pop('public_keys', None)
  984. metadata.pop('random_seed', None)
  985. if not metadata['ec2_compat']:
  986. metadata = None
  987. return dict(name=provider, metadata=metadata)
  988. def init_provider_facts(self):
  989. """ Initialize the provider facts
  990. Returns:
  991. dict: The normalized provider facts
  992. """
  993. provider_info = self.guess_host_provider()
  994. provider_facts = normalize_provider_facts(
  995. provider_info.get('name'),
  996. provider_info.get('metadata')
  997. )
  998. return provider_facts
  999. # Disabling too-many-branches and too-many-locals.
  1000. # This should be cleaned up as a TODO item.
  1001. # pylint: disable=too-many-branches, too-many-locals
  1002. def init_local_facts(self, facts=None,
  1003. additive_facts_to_overwrite=None):
  1004. """ Initialize the local facts
  1005. Args:
  1006. facts (dict): local facts to set
  1007. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  1008. '.' notation ex: ['master.named_certificates']
  1009. Returns:
  1010. dict: The result of merging the provided facts with existing
  1011. local facts
  1012. """
  1013. changed = False
  1014. facts_to_set = dict()
  1015. if facts is not None:
  1016. facts_to_set[self.role] = facts
  1017. local_facts = get_local_facts_from_file(self.filename)
  1018. migrated_facts = migrate_local_facts(local_facts)
  1019. new_local_facts = merge_facts(migrated_facts,
  1020. facts_to_set,
  1021. additive_facts_to_overwrite)
  1022. new_local_facts = self.remove_empty_facts(new_local_facts)
  1023. pop_obsolete_local_facts(new_local_facts)
  1024. if new_local_facts != local_facts:
  1025. changed = True
  1026. if not module.check_mode: # noqa: F405
  1027. save_local_facts(self.filename, new_local_facts)
  1028. self.changed = changed
  1029. return new_local_facts
  1030. def remove_empty_facts(self, facts=None):
  1031. """ Remove empty facts
  1032. Args:
  1033. facts (dict): facts to clean
  1034. """
  1035. facts_to_remove = []
  1036. for fact, value in iteritems(facts):
  1037. if isinstance(facts[fact], dict):
  1038. facts[fact] = self.remove_empty_facts(facts[fact])
  1039. else:
  1040. if value == "" or value == [""] or value is None:
  1041. facts_to_remove.append(fact)
  1042. for fact in facts_to_remove:
  1043. del facts[fact]
  1044. return facts
  1045. def main():
  1046. """ main """
  1047. # disabling pylint errors for global-variable-undefined and invalid-name
  1048. # for 'global module' usage, since it is required to use ansible_facts
  1049. # pylint: disable=global-variable-undefined, invalid-name
  1050. global module
  1051. module = AnsibleModule( # noqa: F405
  1052. argument_spec=dict(
  1053. role=dict(default='common', required=False,
  1054. choices=OpenShiftFacts.known_roles),
  1055. local_facts=dict(default=None, type='dict', required=False),
  1056. additive_facts_to_overwrite=dict(default=[], type='list', required=False),
  1057. ),
  1058. supports_check_mode=True,
  1059. add_file_common_args=True,
  1060. )
  1061. module.params['gather_subset'] = ['hardware', 'network', 'virtual', 'facter'] # noqa: F405
  1062. module.params['gather_timeout'] = 10 # noqa: F405
  1063. module.params['filter'] = '*' # noqa: F405
  1064. role = module.params['role'] # noqa: F405
  1065. local_facts = module.params['local_facts'] # noqa: F405
  1066. additive_facts_to_overwrite = module.params['additive_facts_to_overwrite'] # noqa: F405
  1067. fact_file = '/etc/ansible/facts.d/openshift.fact'
  1068. openshift_facts = OpenShiftFacts(role,
  1069. fact_file,
  1070. local_facts,
  1071. additive_facts_to_overwrite)
  1072. file_params = module.params.copy() # noqa: F405
  1073. file_params['path'] = fact_file
  1074. file_args = module.load_file_common_arguments(file_params) # noqa: F405
  1075. changed = module.set_fs_attributes_if_different(file_args, # noqa: F405
  1076. openshift_facts.changed)
  1077. return module.exit_json(changed=changed, # noqa: F405
  1078. ansible_facts=openshift_facts.facts)
  1079. if __name__ == '__main__':
  1080. main()