openshift_facts.py 34 KB

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