openshift_facts.py 67 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707
  1. #!/usr/bin/python
  2. # pylint: disable=too-many-lines
  3. # -*- coding: utf-8 -*-
  4. # vim: expandtab:tabstop=4:shiftwidth=4
  5. # Reason: Disable pylint too-many-lines because we don't want to split up this file.
  6. # Status: Permanently disabled to keep this module as self-contained as possible.
  7. """Ansible module for retrieving and setting openshift related facts"""
  8. DOCUMENTATION = '''
  9. ---
  10. module: openshift_facts
  11. short_description: Cluster Facts
  12. author: Jason DeTiberus
  13. requirements: [ ]
  14. '''
  15. EXAMPLES = '''
  16. '''
  17. import ConfigParser
  18. import copy
  19. import os
  20. import StringIO
  21. import yaml
  22. from distutils.util import strtobool
  23. from distutils.version import LooseVersion
  24. import struct
  25. import socket
  26. def migrate_docker_facts(facts):
  27. """ Apply migrations for docker facts """
  28. params = {
  29. 'common': (
  30. 'additional_registries',
  31. 'insecure_registries',
  32. 'blocked_registries',
  33. 'options'
  34. ),
  35. 'node': (
  36. 'log_driver',
  37. 'log_options'
  38. )
  39. }
  40. if 'docker' not in facts:
  41. facts['docker'] = {}
  42. for role in params.keys():
  43. if role in facts:
  44. for param in params[role]:
  45. old_param = 'docker_' + param
  46. if old_param in facts[role]:
  47. facts['docker'][param] = facts[role].pop(old_param)
  48. return facts
  49. def migrate_local_facts(facts):
  50. """ Apply migrations of local facts """
  51. migrated_facts = copy.deepcopy(facts)
  52. return migrate_docker_facts(migrated_facts)
  53. def first_ip(network):
  54. """ Return the first IPv4 address in network
  55. Args:
  56. network (str): network in CIDR format
  57. Returns:
  58. str: first IPv4 address
  59. """
  60. atoi = lambda addr: struct.unpack("!I", socket.inet_aton(addr))[0]
  61. itoa = lambda addr: socket.inet_ntoa(struct.pack("!I", addr))
  62. (address, netmask) = network.split('/')
  63. netmask_i = (0xffffffff << (32 - atoi(netmask))) & 0xffffffff
  64. return itoa((atoi(address) & netmask_i) + 1)
  65. def hostname_valid(hostname):
  66. """ Test if specified hostname should be considered valid
  67. Args:
  68. hostname (str): hostname to test
  69. Returns:
  70. bool: True if valid, otherwise False
  71. """
  72. if (not hostname or
  73. hostname.startswith('localhost') or
  74. hostname.endswith('localdomain') or
  75. len(hostname.split('.')) < 2):
  76. return False
  77. return True
  78. def choose_hostname(hostnames=None, fallback=''):
  79. """ Choose a hostname from the provided hostnames
  80. Given a list of hostnames and a fallback value, choose a hostname to
  81. use. This function will prefer fqdns if they exist (excluding any that
  82. begin with localhost or end with localdomain) over ip addresses.
  83. Args:
  84. hostnames (list): list of hostnames
  85. fallback (str): default value to set if hostnames does not contain
  86. a valid hostname
  87. Returns:
  88. str: chosen hostname
  89. """
  90. hostname = fallback
  91. if hostnames is None:
  92. return hostname
  93. ip_regex = r'\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z'
  94. ips = [i for i in hostnames
  95. if (i is not None and isinstance(i, basestring)
  96. and re.match(ip_regex, i))]
  97. hosts = [i for i in hostnames
  98. if i is not None and i != '' and i not in ips]
  99. for host_list in (hosts, ips):
  100. for host in host_list:
  101. if hostname_valid(host):
  102. return host
  103. return hostname
  104. def query_metadata(metadata_url, headers=None, expect_json=False):
  105. """ Return metadata from the provided metadata_url
  106. Args:
  107. metadata_url (str): metadata url
  108. headers (dict): headers to set for metadata request
  109. expect_json (bool): does the metadata_url return json
  110. Returns:
  111. dict or list: metadata request result
  112. """
  113. result, info = fetch_url(module, metadata_url, headers=headers)
  114. if info['status'] != 200:
  115. raise OpenShiftFactsMetadataUnavailableError("Metadata unavailable")
  116. if expect_json:
  117. return module.from_json(result.read())
  118. else:
  119. return [line.strip() for line in result.readlines()]
  120. def walk_metadata(metadata_url, headers=None, expect_json=False):
  121. """ Walk the metadata tree and return a dictionary of the entire tree
  122. Args:
  123. metadata_url (str): metadata url
  124. headers (dict): headers to set for metadata request
  125. expect_json (bool): does the metadata_url return json
  126. Returns:
  127. dict: the result of walking the metadata tree
  128. """
  129. metadata = dict()
  130. for line in query_metadata(metadata_url, headers, expect_json):
  131. if line.endswith('/') and not line == 'public-keys/':
  132. key = line[:-1]
  133. metadata[key] = walk_metadata(metadata_url + line,
  134. headers, expect_json)
  135. else:
  136. results = query_metadata(metadata_url + line, headers,
  137. expect_json)
  138. if len(results) == 1:
  139. # disable pylint maybe-no-member because overloaded use of
  140. # the module name causes pylint to not detect that results
  141. # is an array or hash
  142. # pylint: disable=maybe-no-member
  143. metadata[line] = results.pop()
  144. else:
  145. metadata[line] = results
  146. return metadata
  147. def get_provider_metadata(metadata_url, supports_recursive=False,
  148. headers=None, expect_json=False):
  149. """ Retrieve the provider metadata
  150. Args:
  151. metadata_url (str): metadata url
  152. supports_recursive (bool): does the provider metadata api support
  153. recursion
  154. headers (dict): headers to set for metadata request
  155. expect_json (bool): does the metadata_url return json
  156. Returns:
  157. dict: the provider metadata
  158. """
  159. try:
  160. if supports_recursive:
  161. metadata = query_metadata(metadata_url, headers,
  162. expect_json)
  163. else:
  164. metadata = walk_metadata(metadata_url, headers,
  165. expect_json)
  166. except OpenShiftFactsMetadataUnavailableError:
  167. metadata = None
  168. return metadata
  169. def normalize_gce_facts(metadata, facts):
  170. """ Normalize gce facts
  171. Args:
  172. metadata (dict): provider metadata
  173. facts (dict): facts to update
  174. Returns:
  175. dict: the result of adding the normalized metadata to the provided
  176. facts dict
  177. """
  178. for interface in metadata['instance']['networkInterfaces']:
  179. int_info = dict(ips=[interface['ip']], network_type='gce')
  180. int_info['public_ips'] = [ac['externalIp'] for ac
  181. in interface['accessConfigs']]
  182. int_info['public_ips'].extend(interface['forwardedIps'])
  183. _, _, network_id = interface['network'].rpartition('/')
  184. int_info['network_id'] = network_id
  185. facts['network']['interfaces'].append(int_info)
  186. _, _, zone = metadata['instance']['zone'].rpartition('/')
  187. facts['zone'] = zone
  188. # GCE currently only supports a single interface
  189. facts['network']['ip'] = facts['network']['interfaces'][0]['ips'][0]
  190. pub_ip = facts['network']['interfaces'][0]['public_ips'][0]
  191. facts['network']['public_ip'] = pub_ip
  192. facts['network']['hostname'] = metadata['instance']['hostname']
  193. # TODO: attempt to resolve public_hostname
  194. facts['network']['public_hostname'] = facts['network']['public_ip']
  195. return facts
  196. def normalize_aws_facts(metadata, facts):
  197. """ Normalize aws facts
  198. Args:
  199. metadata (dict): provider metadata
  200. facts (dict): facts to update
  201. Returns:
  202. dict: the result of adding the normalized metadata to the provided
  203. facts dict
  204. """
  205. for interface in sorted(
  206. metadata['network']['interfaces']['macs'].values(),
  207. key=lambda x: x['device-number']
  208. ):
  209. int_info = dict()
  210. var_map = {'ips': 'local-ipv4s', 'public_ips': 'public-ipv4s'}
  211. for ips_var, int_var in var_map.iteritems():
  212. ips = interface.get(int_var)
  213. if isinstance(ips, basestring):
  214. int_info[ips_var] = [ips]
  215. else:
  216. int_info[ips_var] = ips
  217. if 'vpc-id' in interface:
  218. int_info['network_type'] = 'vpc'
  219. else:
  220. int_info['network_type'] = 'classic'
  221. if int_info['network_type'] == 'vpc':
  222. int_info['network_id'] = interface['subnet-id']
  223. else:
  224. int_info['network_id'] = None
  225. facts['network']['interfaces'].append(int_info)
  226. facts['zone'] = metadata['placement']['availability-zone']
  227. # TODO: actually attempt to determine default local and public ips
  228. # by using the ansible default ip fact and the ipv4-associations
  229. # from the ec2 metadata
  230. facts['network']['ip'] = metadata.get('local-ipv4')
  231. facts['network']['public_ip'] = metadata.get('public-ipv4')
  232. # TODO: verify that local hostname makes sense and is resolvable
  233. facts['network']['hostname'] = metadata.get('local-hostname')
  234. # TODO: verify that public hostname makes sense and is resolvable
  235. facts['network']['public_hostname'] = metadata.get('public-hostname')
  236. return facts
  237. def normalize_openstack_facts(metadata, facts):
  238. """ Normalize openstack facts
  239. Args:
  240. metadata (dict): provider metadata
  241. facts (dict): facts to update
  242. Returns:
  243. dict: the result of adding the normalized metadata to the provided
  244. facts dict
  245. """
  246. # openstack ec2 compat api does not support network interfaces and
  247. # the version tested on did not include the info in the openstack
  248. # metadata api, should be updated if neutron exposes this.
  249. facts['zone'] = metadata['availability_zone']
  250. local_ipv4 = metadata['ec2_compat']['local-ipv4'].split(',')[0]
  251. facts['network']['ip'] = local_ipv4
  252. facts['network']['public_ip'] = metadata['ec2_compat']['public-ipv4']
  253. # TODO: verify local hostname makes sense and is resolvable
  254. facts['network']['hostname'] = metadata['hostname']
  255. # TODO: verify that public hostname makes sense and is resolvable
  256. pub_h = metadata['ec2_compat']['public-hostname']
  257. facts['network']['public_hostname'] = pub_h
  258. return facts
  259. def normalize_provider_facts(provider, metadata):
  260. """ Normalize provider facts
  261. Args:
  262. provider (str): host provider
  263. metadata (dict): provider metadata
  264. Returns:
  265. dict: the normalized provider facts
  266. """
  267. if provider is None or metadata is None:
  268. return {}
  269. # TODO: test for ipv6_enabled where possible (gce, aws do not support)
  270. # and configure ipv6 facts if available
  271. # TODO: add support for setting user_data if available
  272. facts = dict(name=provider, metadata=metadata,
  273. network=dict(interfaces=[], ipv6_enabled=False))
  274. if os.path.exists('/etc/cloud.conf'):
  275. for arg in ('api_server_args', 'controller_args', 'kubelet_args'):
  276. facts[arg] = {'cloud-provider': [provider],
  277. 'cloud-config': ['/etc/cloud.conf']}
  278. if provider == 'gce':
  279. facts = normalize_gce_facts(metadata, facts)
  280. elif provider == 'ec2':
  281. facts = normalize_aws_facts(metadata, facts)
  282. elif provider == 'openstack':
  283. facts = normalize_openstack_facts(metadata, facts)
  284. return facts
  285. def set_flannel_facts_if_unset(facts):
  286. """ Set flannel facts if not already present in facts dict
  287. dict: the facts dict updated with the flannel facts if
  288. missing
  289. Args:
  290. facts (dict): existing facts
  291. Returns:
  292. dict: the facts dict updated with the flannel
  293. facts if they were not already present
  294. """
  295. if 'common' in facts:
  296. if 'use_flannel' not in facts['common']:
  297. use_flannel = False
  298. facts['common']['use_flannel'] = use_flannel
  299. return facts
  300. def set_nuage_facts_if_unset(facts):
  301. """ Set nuage facts if not already present in facts dict
  302. dict: the facts dict updated with the nuage facts if
  303. missing
  304. Args:
  305. facts (dict): existing facts
  306. Returns:
  307. dict: the facts dict updated with the nuage
  308. facts if they were not already present
  309. """
  310. if 'common' in facts:
  311. if 'use_nuage' not in facts['common']:
  312. use_nuage = False
  313. facts['common']['use_nuage'] = use_nuage
  314. return facts
  315. def set_node_schedulability(facts):
  316. """ Set schedulable facts if not already present in facts dict
  317. Args:
  318. facts (dict): existing facts
  319. Returns:
  320. dict: the facts dict updated with the generated schedulable
  321. facts if they were not already present
  322. """
  323. if 'node' in facts:
  324. if 'schedulable' not in facts['node']:
  325. if 'master' in facts:
  326. facts['node']['schedulable'] = False
  327. else:
  328. facts['node']['schedulable'] = True
  329. return facts
  330. def set_master_selectors(facts):
  331. """ Set selectors facts if not already present in facts dict
  332. Args:
  333. facts (dict): existing facts
  334. Returns:
  335. dict: the facts dict updated with the generated selectors
  336. facts if they were not already present
  337. """
  338. if 'master' in facts:
  339. if 'infra_nodes' in facts['master']:
  340. deployment_type = facts['common']['deployment_type']
  341. if deployment_type == 'online':
  342. selector = "type=infra"
  343. else:
  344. selector = "region=infra"
  345. if 'router_selector' not in facts['master']:
  346. facts['master']['router_selector'] = selector
  347. if 'registry_selector' not in facts['master']:
  348. facts['master']['registry_selector'] = selector
  349. return facts
  350. def set_metrics_facts_if_unset(facts):
  351. """ Set cluster metrics facts if not already present in facts dict
  352. dict: the facts dict updated with the generated cluster metrics facts if
  353. missing
  354. Args:
  355. facts (dict): existing facts
  356. Returns:
  357. dict: the facts dict updated with the generated cluster metrics
  358. facts if they were not already present
  359. """
  360. if 'common' in facts:
  361. if 'use_cluster_metrics' not in facts['common']:
  362. use_cluster_metrics = False
  363. facts['common']['use_cluster_metrics'] = use_cluster_metrics
  364. return facts
  365. def set_project_cfg_facts_if_unset(facts):
  366. """ Set Project Configuration facts if not already present in facts dict
  367. dict:
  368. Args:
  369. facts (dict): existing facts
  370. Returns:
  371. dict: the facts dict updated with the generated Project Configuration
  372. facts if they were not already present
  373. """
  374. config = {
  375. 'default_node_selector': '',
  376. 'project_request_message': '',
  377. 'project_request_template': '',
  378. 'mcs_allocator_range': 's0:/2',
  379. 'mcs_labels_per_project': 5,
  380. 'uid_allocator_range': '1000000000-1999999999/10000'
  381. }
  382. if 'master' in facts:
  383. for key, value in config.items():
  384. if key not in facts['master']:
  385. facts['master'][key] = value
  386. return facts
  387. def set_identity_providers_if_unset(facts):
  388. """ Set identity_providers fact 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 identity providers
  393. facts if they were not already present
  394. """
  395. if 'master' in facts:
  396. deployment_type = facts['common']['deployment_type']
  397. if 'identity_providers' not in facts['master']:
  398. identity_provider = dict(
  399. name='allow_all', challenge=True, login=True,
  400. kind='AllowAllPasswordIdentityProvider'
  401. )
  402. if deployment_type in ['enterprise', 'atomic-enterprise', 'openshift-enterprise']:
  403. identity_provider = dict(
  404. name='deny_all', challenge=True, login=True,
  405. kind='DenyAllPasswordIdentityProvider'
  406. )
  407. facts['master']['identity_providers'] = [identity_provider]
  408. return facts
  409. def set_url_facts_if_unset(facts):
  410. """ Set url facts if not already present in facts dict
  411. Args:
  412. facts (dict): existing facts
  413. Returns:
  414. dict: the facts dict updated with the generated url facts if they
  415. were not already present
  416. """
  417. if 'master' in facts:
  418. hostname = facts['common']['hostname']
  419. cluster_hostname = facts['master'].get('cluster_hostname')
  420. cluster_public_hostname = facts['master'].get('cluster_public_hostname')
  421. public_hostname = facts['common']['public_hostname']
  422. api_hostname = cluster_hostname if cluster_hostname else hostname
  423. api_public_hostname = cluster_public_hostname if cluster_public_hostname else public_hostname
  424. console_path = facts['master']['console_path']
  425. etcd_hosts = facts['master']['etcd_hosts']
  426. use_ssl = dict(
  427. api=facts['master']['api_use_ssl'],
  428. public_api=facts['master']['api_use_ssl'],
  429. loopback_api=facts['master']['api_use_ssl'],
  430. console=facts['master']['console_use_ssl'],
  431. public_console=facts['master']['console_use_ssl'],
  432. etcd=facts['master']['etcd_use_ssl']
  433. )
  434. ports = dict(
  435. api=facts['master']['api_port'],
  436. public_api=facts['master']['api_port'],
  437. loopback_api=facts['master']['api_port'],
  438. console=facts['master']['console_port'],
  439. public_console=facts['master']['console_port'],
  440. etcd=facts['master']['etcd_port'],
  441. )
  442. etcd_urls = []
  443. if etcd_hosts != '':
  444. facts['master']['etcd_port'] = ports['etcd']
  445. facts['master']['embedded_etcd'] = False
  446. for host in etcd_hosts:
  447. etcd_urls.append(format_url(use_ssl['etcd'], host,
  448. ports['etcd']))
  449. else:
  450. etcd_urls = [format_url(use_ssl['etcd'], hostname,
  451. ports['etcd'])]
  452. facts['master'].setdefault('etcd_urls', etcd_urls)
  453. prefix_hosts = [('api', api_hostname),
  454. ('public_api', api_public_hostname),
  455. ('loopback_api', hostname)]
  456. for prefix, host in prefix_hosts:
  457. facts['master'].setdefault(prefix + '_url', format_url(use_ssl[prefix],
  458. host,
  459. ports[prefix]))
  460. r_lhn = "{0}:{1}".format(hostname, ports['api']).replace('.', '-')
  461. r_lhu = "system:openshift-master/{0}:{1}".format(api_hostname, ports['api']).replace('.', '-')
  462. facts['master'].setdefault('loopback_cluster_name', r_lhn)
  463. facts['master'].setdefault('loopback_context_name', "default/{0}/system:openshift-master".format(r_lhn))
  464. facts['master'].setdefault('loopback_user', r_lhu)
  465. prefix_hosts = [('console', api_hostname), ('public_console', api_public_hostname)]
  466. for prefix, host in prefix_hosts:
  467. facts['master'].setdefault(prefix + '_url', format_url(use_ssl[prefix],
  468. host,
  469. ports[prefix],
  470. console_path))
  471. return facts
  472. def set_aggregate_facts(facts):
  473. """ Set aggregate facts
  474. Args:
  475. facts (dict): existing facts
  476. Returns:
  477. dict: the facts dict updated with aggregated facts
  478. """
  479. all_hostnames = set()
  480. internal_hostnames = set()
  481. if 'common' in facts:
  482. all_hostnames.add(facts['common']['hostname'])
  483. all_hostnames.add(facts['common']['public_hostname'])
  484. all_hostnames.add(facts['common']['ip'])
  485. all_hostnames.add(facts['common']['public_ip'])
  486. internal_hostnames.add(facts['common']['hostname'])
  487. internal_hostnames.add(facts['common']['ip'])
  488. cluster_domain = facts['common']['dns_domain']
  489. if 'master' in facts:
  490. if 'cluster_hostname' in facts['master']:
  491. all_hostnames.add(facts['master']['cluster_hostname'])
  492. if 'cluster_public_hostname' in facts['master']:
  493. all_hostnames.add(facts['master']['cluster_public_hostname'])
  494. svc_names = ['openshift', 'openshift.default', 'openshift.default.svc',
  495. 'openshift.default.svc.' + cluster_domain, 'kubernetes', 'kubernetes.default',
  496. 'kubernetes.default.svc', 'kubernetes.default.svc.' + cluster_domain]
  497. all_hostnames.update(svc_names)
  498. internal_hostnames.update(svc_names)
  499. first_svc_ip = first_ip(facts['master']['portal_net'])
  500. all_hostnames.add(first_svc_ip)
  501. internal_hostnames.add(first_svc_ip)
  502. facts['common']['all_hostnames'] = list(all_hostnames)
  503. facts['common']['internal_hostnames'] = list(internal_hostnames)
  504. return facts
  505. def set_etcd_facts_if_unset(facts):
  506. """
  507. If using embedded etcd, loads the data directory from master-config.yaml.
  508. If using standalone etcd, loads ETCD_DATA_DIR from etcd.conf.
  509. If anything goes wrong parsing these, the fact will not be set.
  510. """
  511. if 'master' in facts and facts['master']['embedded_etcd']:
  512. etcd_facts = facts['etcd'] if 'etcd' in facts else dict()
  513. if 'etcd_data_dir' not in etcd_facts:
  514. try:
  515. # Parse master config to find actual etcd data dir:
  516. master_cfg_path = os.path.join(facts['common']['config_base'],
  517. 'master/master-config.yaml')
  518. master_cfg_f = open(master_cfg_path, 'r')
  519. config = yaml.safe_load(master_cfg_f.read())
  520. master_cfg_f.close()
  521. etcd_facts['etcd_data_dir'] = \
  522. config['etcdConfig']['storageDirectory']
  523. facts['etcd'] = etcd_facts
  524. # We don't want exceptions bubbling up here:
  525. # pylint: disable=broad-except
  526. except Exception:
  527. pass
  528. else:
  529. etcd_facts = facts['etcd'] if 'etcd' in facts else dict()
  530. # Read ETCD_DATA_DIR from /etc/etcd/etcd.conf:
  531. try:
  532. # Add a fake section for parsing:
  533. ini_str = '[root]\n' + open('/etc/etcd/etcd.conf', 'r').read()
  534. ini_fp = StringIO.StringIO(ini_str)
  535. config = ConfigParser.RawConfigParser()
  536. config.readfp(ini_fp)
  537. etcd_data_dir = config.get('root', 'ETCD_DATA_DIR')
  538. if etcd_data_dir.startswith('"') and etcd_data_dir.endswith('"'):
  539. etcd_data_dir = etcd_data_dir[1:-1]
  540. etcd_facts['etcd_data_dir'] = etcd_data_dir
  541. facts['etcd'] = etcd_facts
  542. # We don't want exceptions bubbling up here:
  543. # pylint: disable=broad-except
  544. except Exception:
  545. pass
  546. return facts
  547. def set_deployment_facts_if_unset(facts):
  548. """ Set Facts that vary based on deployment_type. This currently
  549. includes common.service_type, common.config_base, master.registry_url,
  550. node.registry_url, node.storage_plugin_deps
  551. Args:
  552. facts (dict): existing facts
  553. Returns:
  554. dict: the facts dict updated with the generated deployment_type
  555. facts
  556. """
  557. # disabled to avoid breaking up facts related to deployment type into
  558. # multiple methods for now.
  559. # pylint: disable=too-many-statements, too-many-branches
  560. if 'common' in facts:
  561. deployment_type = facts['common']['deployment_type']
  562. if 'service_type' not in facts['common']:
  563. service_type = 'atomic-openshift'
  564. if deployment_type == 'origin':
  565. service_type = 'origin'
  566. elif deployment_type in ['enterprise']:
  567. service_type = 'openshift'
  568. facts['common']['service_type'] = service_type
  569. if 'config_base' not in facts['common']:
  570. config_base = '/etc/origin'
  571. if deployment_type in ['enterprise']:
  572. config_base = '/etc/openshift'
  573. # Handle upgrade scenarios when symlinks don't yet exist:
  574. if not os.path.exists(config_base) and os.path.exists('/etc/openshift'):
  575. config_base = '/etc/openshift'
  576. facts['common']['config_base'] = config_base
  577. if 'data_dir' not in facts['common']:
  578. data_dir = '/var/lib/origin'
  579. if deployment_type in ['enterprise']:
  580. data_dir = '/var/lib/openshift'
  581. # Handle upgrade scenarios when symlinks don't yet exist:
  582. if not os.path.exists(data_dir) and os.path.exists('/var/lib/openshift'):
  583. data_dir = '/var/lib/openshift'
  584. facts['common']['data_dir'] = data_dir
  585. if 'docker' in facts:
  586. deployment_type = facts['common']['deployment_type']
  587. if deployment_type in ['enterprise', 'atomic-enterprise', 'openshift-enterprise']:
  588. addtl_regs = facts['docker'].get('additional_registries', [])
  589. ent_reg = 'registry.access.redhat.com'
  590. if ent_reg not in addtl_regs:
  591. facts['docker']['additional_registries'] = addtl_regs + [ent_reg]
  592. for role in ('master', 'node'):
  593. if role in facts:
  594. deployment_type = facts['common']['deployment_type']
  595. if 'registry_url' not in facts[role]:
  596. registry_url = 'openshift/origin-${component}:${version}'
  597. if deployment_type in ['enterprise', 'online', 'openshift-enterprise']:
  598. registry_url = 'openshift3/ose-${component}:${version}'
  599. elif deployment_type == 'atomic-enterprise':
  600. registry_url = 'aep3_beta/aep-${component}:${version}'
  601. facts[role]['registry_url'] = registry_url
  602. if 'master' in facts:
  603. deployment_type = facts['common']['deployment_type']
  604. openshift_features = ['Builder', 'S2IBuilder', 'WebConsole']
  605. if 'disabled_features' in facts['master']:
  606. if deployment_type == 'atomic-enterprise':
  607. curr_disabled_features = set(facts['master']['disabled_features'])
  608. facts['master']['disabled_features'] = list(curr_disabled_features.union(openshift_features))
  609. else:
  610. if deployment_type == 'atomic-enterprise':
  611. facts['master']['disabled_features'] = openshift_features
  612. if 'node' in facts:
  613. deployment_type = facts['common']['deployment_type']
  614. if 'storage_plugin_deps' not in facts['node']:
  615. if deployment_type in ['openshift-enterprise', 'atomic-enterprise', 'origin']:
  616. facts['node']['storage_plugin_deps'] = ['ceph', 'glusterfs', 'iscsi']
  617. else:
  618. facts['node']['storage_plugin_deps'] = []
  619. return facts
  620. def set_version_facts_if_unset(facts):
  621. """ Set version facts. This currently includes common.version and
  622. common.version_gte_3_1_or_1_1.
  623. Args:
  624. facts (dict): existing facts
  625. Returns:
  626. dict: the facts dict updated with version facts.
  627. """
  628. if 'common' in facts:
  629. deployment_type = facts['common']['deployment_type']
  630. facts['common']['version'] = version = get_openshift_version(facts)
  631. if version is not None:
  632. if deployment_type == 'origin':
  633. version_gte_3_1_or_1_1 = LooseVersion(version) >= LooseVersion('1.1.0')
  634. version_gte_3_1_1_or_1_1_1 = LooseVersion(version) >= LooseVersion('1.1.1')
  635. version_gte_3_2_or_1_2 = LooseVersion(version) >= LooseVersion('1.2.0')
  636. else:
  637. version_gte_3_1_or_1_1 = LooseVersion(version) >= LooseVersion('3.0.2.905')
  638. version_gte_3_1_1_or_1_1_1 = LooseVersion(version) >= LooseVersion('3.1.1')
  639. version_gte_3_2_or_1_2 = LooseVersion(version) >= LooseVersion('3.1.1.901')
  640. else:
  641. version_gte_3_1_or_1_1 = True
  642. version_gte_3_1_1_or_1_1_1 = True
  643. version_gte_3_2_or_1_2 = True
  644. facts['common']['version_gte_3_1_or_1_1'] = version_gte_3_1_or_1_1
  645. facts['common']['version_gte_3_1_1_or_1_1_1'] = version_gte_3_1_1_or_1_1_1
  646. facts['common']['version_gte_3_2_or_1_2'] = version_gte_3_2_or_1_2
  647. if version_gte_3_2_or_1_2:
  648. examples_content_version = 'v1.2'
  649. elif version_gte_3_1_or_1_1:
  650. examples_content_version = 'v1.1'
  651. else:
  652. examples_content_version = 'v1.0'
  653. facts['common']['examples_content_version'] = examples_content_version
  654. return facts
  655. def set_manageiq_facts_if_unset(facts):
  656. """ Set manageiq facts. This currently includes common.use_manageiq.
  657. Args:
  658. facts (dict): existing facts
  659. Returns:
  660. dict: the facts dict updated with version facts.
  661. Raises:
  662. OpenShiftFactsInternalError:
  663. """
  664. if 'common' not in facts:
  665. if 'version_gte_3_1_or_1_1' not in facts['common']:
  666. raise OpenShiftFactsInternalError(
  667. "Invalid invocation: The required facts are not set"
  668. )
  669. if 'use_manageiq' not in facts['common']:
  670. facts['common']['use_manageiq'] = facts['common']['version_gte_3_1_or_1_1']
  671. return facts
  672. def set_sdn_facts_if_unset(facts, system_facts):
  673. """ Set sdn facts if not already present in facts dict
  674. Args:
  675. facts (dict): existing facts
  676. system_facts (dict): ansible_facts
  677. Returns:
  678. dict: the facts dict updated with the generated sdn facts if they
  679. were not already present
  680. """
  681. if 'common' in facts:
  682. use_sdn = facts['common']['use_openshift_sdn']
  683. if not (use_sdn == '' or isinstance(use_sdn, bool)):
  684. use_sdn = safe_get_bool(use_sdn)
  685. facts['common']['use_openshift_sdn'] = use_sdn
  686. if 'sdn_network_plugin_name' not in facts['common']:
  687. plugin = 'redhat/openshift-ovs-subnet' if use_sdn else ''
  688. facts['common']['sdn_network_plugin_name'] = plugin
  689. if 'master' in facts:
  690. if 'sdn_cluster_network_cidr' not in facts['master']:
  691. facts['master']['sdn_cluster_network_cidr'] = '10.1.0.0/16'
  692. if 'sdn_host_subnet_length' not in facts['master']:
  693. facts['master']['sdn_host_subnet_length'] = '8'
  694. if 'node' in facts and 'sdn_mtu' not in facts['node']:
  695. node_ip = facts['common']['ip']
  696. # default MTU if interface MTU cannot be detected
  697. facts['node']['sdn_mtu'] = '1450'
  698. for val in system_facts.itervalues():
  699. if isinstance(val, dict) and 'mtu' in val:
  700. mtu = val['mtu']
  701. if 'ipv4' in val and val['ipv4'].get('address') == node_ip:
  702. facts['node']['sdn_mtu'] = str(mtu - 50)
  703. return facts
  704. def format_url(use_ssl, hostname, port, path=''):
  705. """ Format url based on ssl flag, hostname, port and path
  706. Args:
  707. use_ssl (bool): is ssl enabled
  708. hostname (str): hostname
  709. port (str): port
  710. path (str): url path
  711. Returns:
  712. str: The generated url string
  713. """
  714. scheme = 'https' if use_ssl else 'http'
  715. netloc = hostname
  716. if (use_ssl and port != '443') or (not use_ssl and port != '80'):
  717. netloc += ":%s" % port
  718. return urlparse.urlunparse((scheme, netloc, path, '', '', ''))
  719. def get_current_config(facts):
  720. """ Get current openshift config
  721. Args:
  722. facts (dict): existing facts
  723. Returns:
  724. dict: the facts dict updated with the current openshift config
  725. """
  726. current_config = dict()
  727. roles = [role for role in facts if role not in ['common', 'provider']]
  728. for role in roles:
  729. if 'roles' in current_config:
  730. current_config['roles'].append(role)
  731. else:
  732. current_config['roles'] = [role]
  733. # TODO: parse the /etc/sysconfig/openshift-{master,node} config to
  734. # determine the location of files.
  735. # TODO: I suspect this isn't working right now, but it doesn't prevent
  736. # anything from working properly as far as I can tell, perhaps because
  737. # we override the kubeconfig path everywhere we use it?
  738. # Query kubeconfig settings
  739. kubeconfig_dir = '/var/lib/origin/openshift.local.certificates'
  740. if role == 'node':
  741. kubeconfig_dir = os.path.join(
  742. kubeconfig_dir, "node-%s" % facts['common']['hostname']
  743. )
  744. kubeconfig_path = os.path.join(kubeconfig_dir, '.kubeconfig')
  745. if (os.path.isfile('/usr/bin/openshift')
  746. and os.path.isfile(kubeconfig_path)):
  747. try:
  748. _, output, _ = module.run_command(
  749. ["/usr/bin/openshift", "ex", "config", "view", "-o",
  750. "json", "--kubeconfig=%s" % kubeconfig_path],
  751. check_rc=False
  752. )
  753. config = json.loads(output)
  754. cad = 'certificate-authority-data'
  755. try:
  756. for cluster in config['clusters']:
  757. config['clusters'][cluster][cad] = 'masked'
  758. except KeyError:
  759. pass
  760. try:
  761. for user in config['users']:
  762. config['users'][user][cad] = 'masked'
  763. config['users'][user]['client-key-data'] = 'masked'
  764. except KeyError:
  765. pass
  766. current_config['kubeconfig'] = config
  767. # override pylint broad-except warning, since we do not want
  768. # to bubble up any exceptions if oc config view
  769. # fails
  770. # pylint: disable=broad-except
  771. except Exception:
  772. pass
  773. return current_config
  774. def get_openshift_version(facts, cli_image=None):
  775. """ Get current version of openshift on the host
  776. Args:
  777. facts (dict): existing facts
  778. optional cli_image for pulling the version number
  779. Returns:
  780. version: the current openshift version
  781. """
  782. version = None
  783. # No need to run this method repeatedly on a system if we already know the
  784. # version
  785. if 'common' in facts:
  786. if 'version' in facts['common'] and facts['common']['version'] is not None:
  787. return facts['common']['version']
  788. if os.path.isfile('/usr/bin/openshift'):
  789. _, output, _ = module.run_command(['/usr/bin/openshift', 'version'])
  790. version = parse_openshift_version(output)
  791. if 'is_containerized' in facts['common'] and safe_get_bool(facts['common']['is_containerized']):
  792. container = None
  793. if 'master' in facts:
  794. if 'cluster_method' in facts['master']:
  795. container = facts['common']['service_type'] + '-master-api'
  796. else:
  797. container = facts['common']['service_type'] + '-master'
  798. elif 'node' in facts:
  799. container = facts['common']['service_type'] + '-node'
  800. if container is not None:
  801. exit_code, output, _ = module.run_command(['docker', 'exec', container, 'openshift', 'version'])
  802. # if for some reason the container is installed but not running
  803. # we'll fall back to using docker run later in this method.
  804. if exit_code == 0:
  805. version = parse_openshift_version(output)
  806. if version is None and cli_image is not None:
  807. # Assume we haven't installed the environment yet and we need
  808. # to query the latest image
  809. exit_code, output, _ = module.run_command(['docker', 'run', '--rm', cli_image, 'version'])
  810. version = parse_openshift_version(output)
  811. return version
  812. def parse_openshift_version(output):
  813. """ Apply provider facts to supplied facts dict
  814. Args:
  815. string: output of 'openshift version'
  816. Returns:
  817. string: the version number
  818. """
  819. versions = dict(e.split(' v') for e in output.splitlines() if ' v' in e)
  820. return versions.get('openshift', '')
  821. def apply_provider_facts(facts, provider_facts):
  822. """ Apply provider facts to supplied facts dict
  823. Args:
  824. facts (dict): facts dict to update
  825. provider_facts (dict): provider facts to apply
  826. roles: host roles
  827. Returns:
  828. dict: the merged facts
  829. """
  830. if not provider_facts:
  831. return facts
  832. common_vars = [('hostname', 'ip'), ('public_hostname', 'public_ip')]
  833. for h_var, ip_var in common_vars:
  834. ip_value = provider_facts['network'].get(ip_var)
  835. if ip_value:
  836. facts['common'][ip_var] = ip_value
  837. facts['common'][h_var] = choose_hostname(
  838. [provider_facts['network'].get(h_var)],
  839. facts['common'][ip_var]
  840. )
  841. facts['provider'] = provider_facts
  842. return facts
  843. # Disabling pylint too many branches. This function needs refactored
  844. # but is a very core part of openshift_facts.
  845. # pylint: disable=too-many-branches
  846. def merge_facts(orig, new, additive_facts_to_overwrite, protected_facts_to_overwrite):
  847. """ Recursively merge facts dicts
  848. Args:
  849. orig (dict): existing facts
  850. new (dict): facts to update
  851. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  852. '.' notation ex: ['master.named_certificates']
  853. protected_facts_to_overwrite (list): protected facts to overwrite in jinja
  854. '.' notation ex: ['master.master_count']
  855. Returns:
  856. dict: the merged facts
  857. """
  858. additive_facts = ['named_certificates']
  859. protected_facts = ['ha', 'master_count']
  860. facts = dict()
  861. for key, value in orig.iteritems():
  862. # Key exists in both old and new facts.
  863. if key in new:
  864. # Continue to recurse if old and new fact is a dictionary.
  865. if isinstance(value, dict) and isinstance(new[key], dict):
  866. # Collect the subset of additive facts to overwrite if
  867. # key matches. These will be passed to the subsequent
  868. # merge_facts call.
  869. relevant_additive_facts = []
  870. for item in additive_facts_to_overwrite:
  871. if '.' in item and item.startswith(key + '.'):
  872. relevant_additive_facts.append(item)
  873. # Collect the subset of protected facts to overwrite
  874. # if key matches. These will be passed to the
  875. # subsequent merge_facts call.
  876. relevant_protected_facts = []
  877. for item in protected_facts_to_overwrite:
  878. if '.' in item and item.startswith(key + '.'):
  879. relevant_protected_facts.append(item)
  880. facts[key] = merge_facts(value, new[key], relevant_additive_facts, relevant_protected_facts)
  881. # Key matches an additive fact and we are not overwriting
  882. # it so we will append the new value to the existing value.
  883. elif key in additive_facts and key not in [x.split('.')[-1] for x in additive_facts_to_overwrite]:
  884. if isinstance(value, list) and isinstance(new[key], list):
  885. new_fact = []
  886. for item in copy.deepcopy(value) + copy.deepcopy(new[key]):
  887. if item not in new_fact:
  888. new_fact.append(item)
  889. facts[key] = new_fact
  890. # Key matches a protected fact and we are not overwriting
  891. # it so we will determine if it is okay to change this
  892. # fact.
  893. elif key in protected_facts and key not in [x.split('.')[-1] for x in protected_facts_to_overwrite]:
  894. # The master count (int) can only increase unless it
  895. # has been passed as a protected fact to overwrite.
  896. if key == 'master_count':
  897. if int(value) <= int(new[key]):
  898. facts[key] = copy.deepcopy(new[key])
  899. else:
  900. module.fail_json(msg='openshift_facts received a lower value for openshift.master.master_count')
  901. # ha (bool) can not change unless it has been passed
  902. # as a protected fact to overwrite.
  903. if key == 'ha':
  904. if safe_get_bool(value) != safe_get_bool(new[key]):
  905. module.fail_json(msg='openshift_facts received a different value for openshift.master.ha')
  906. else:
  907. facts[key] = value
  908. # No other condition has been met. Overwrite the old fact
  909. # with the new value.
  910. else:
  911. facts[key] = copy.deepcopy(new[key])
  912. # Key isn't in new so add it to facts to keep it.
  913. else:
  914. facts[key] = copy.deepcopy(value)
  915. new_keys = set(new.keys()) - set(orig.keys())
  916. for key in new_keys:
  917. facts[key] = copy.deepcopy(new[key])
  918. return facts
  919. def merge_provider_facts(facts):
  920. """ Recursively merge provider facts dicts
  921. Args:
  922. facts (dict): existing facts
  923. Returns:
  924. dict: the facts dict updated with the provider config
  925. """
  926. if 'provider' not in facts:
  927. return facts
  928. if 'master' in facts:
  929. for arg in ('api_server_args', 'controller_args'):
  930. facts['master'][arg] = merge_facts(
  931. facts['provider'].get(arg, {}),
  932. facts['master'].get(arg, {}),
  933. [], [])
  934. if 'node' in facts:
  935. facts['node']['kubelet_args'] = merge_facts(
  936. facts['provider'].get('kubelet_args', {}),
  937. facts['node'].get('kubelet_args', {}),
  938. [], [])
  939. return facts
  940. def save_local_facts(filename, facts):
  941. """ Save local facts
  942. Args:
  943. filename (str): local facts file
  944. facts (dict): facts to set
  945. """
  946. try:
  947. fact_dir = os.path.dirname(filename)
  948. if not os.path.exists(fact_dir):
  949. os.makedirs(fact_dir)
  950. with open(filename, 'w') as fact_file:
  951. fact_file.write(module.jsonify(facts))
  952. os.chmod(filename, 0o600)
  953. except (IOError, OSError) as ex:
  954. raise OpenShiftFactsFileWriteError(
  955. "Could not create fact file: %s, error: %s" % (filename, ex)
  956. )
  957. def get_local_facts_from_file(filename):
  958. """ Retrieve local facts from fact file
  959. Args:
  960. filename (str): local facts file
  961. Returns:
  962. dict: the retrieved facts
  963. """
  964. local_facts = dict()
  965. try:
  966. # Handle conversion of INI style facts file to json style
  967. ini_facts = ConfigParser.SafeConfigParser()
  968. ini_facts.read(filename)
  969. for section in ini_facts.sections():
  970. local_facts[section] = dict()
  971. for key, value in ini_facts.items(section):
  972. local_facts[section][key] = value
  973. except (ConfigParser.MissingSectionHeaderError,
  974. ConfigParser.ParsingError):
  975. try:
  976. with open(filename, 'r') as facts_file:
  977. local_facts = json.load(facts_file)
  978. except (ValueError, IOError):
  979. pass
  980. return local_facts
  981. def safe_get_bool(fact):
  982. """ Get a boolean fact safely.
  983. Args:
  984. facts: fact to convert
  985. Returns:
  986. bool: given fact as a bool
  987. """
  988. return bool(strtobool(str(fact)))
  989. def set_container_facts_if_unset(facts):
  990. """ Set containerized facts.
  991. Args:
  992. facts (dict): existing facts
  993. Returns:
  994. dict: the facts dict updated with the generated containerization
  995. facts
  996. """
  997. deployment_type = facts['common']['deployment_type']
  998. if deployment_type in ['enterprise', 'openshift-enterprise']:
  999. master_image = 'openshift3/ose'
  1000. cli_image = master_image
  1001. node_image = 'openshift3/node'
  1002. ovs_image = 'openshift3/openvswitch'
  1003. etcd_image = 'registry.access.redhat.com/rhel7/etcd'
  1004. elif deployment_type == 'atomic-enterprise':
  1005. master_image = 'aep3_beta/aep'
  1006. cli_image = master_image
  1007. node_image = 'aep3_beta/node'
  1008. ovs_image = 'aep3_beta/openvswitch'
  1009. etcd_image = 'registry.access.redhat.com/rhel7/etcd'
  1010. else:
  1011. master_image = 'openshift/origin'
  1012. cli_image = master_image
  1013. node_image = 'openshift/node'
  1014. ovs_image = 'openshift/openvswitch'
  1015. etcd_image = 'registry.access.redhat.com/rhel7/etcd'
  1016. facts['common']['is_atomic'] = os.path.isfile('/run/ostree-booted')
  1017. if 'is_containerized' not in facts['common']:
  1018. facts['common']['is_containerized'] = facts['common']['is_atomic']
  1019. if 'cli_image' not in facts['common']:
  1020. facts['common']['cli_image'] = cli_image
  1021. if 'etcd' in facts and 'etcd_image' not in facts['etcd']:
  1022. facts['etcd']['etcd_image'] = etcd_image
  1023. if 'master' in facts and 'master_image' not in facts['master']:
  1024. facts['master']['master_image'] = master_image
  1025. if 'node' in facts:
  1026. if 'node_image' not in facts['node']:
  1027. facts['node']['node_image'] = node_image
  1028. if 'ovs_image' not in facts['node']:
  1029. facts['node']['ovs_image'] = ovs_image
  1030. if safe_get_bool(facts['common']['is_containerized']):
  1031. facts['common']['admin_binary'] = '/usr/local/bin/oadm'
  1032. facts['common']['client_binary'] = '/usr/local/bin/oc'
  1033. base_version = get_openshift_version(facts, cli_image).split('-')[0]
  1034. facts['common']['image_tag'] = "v" + base_version
  1035. return facts
  1036. def set_installed_variant_rpm_facts(facts):
  1037. """ Set RPM facts of installed variant
  1038. Args:
  1039. facts (dict): existing facts
  1040. Returns:
  1041. dict: the facts dict updated with installed_variant_rpms
  1042. """
  1043. installed_rpms = []
  1044. for base_rpm in ['openshift', 'atomic-openshift', 'origin']:
  1045. optional_rpms = ['master', 'node', 'clients', 'sdn-ovs']
  1046. variant_rpms = [base_rpm] + \
  1047. ['{0}-{1}'.format(base_rpm, r) for r in optional_rpms] + \
  1048. ['tuned-profiles-%s-node' % base_rpm]
  1049. for rpm in variant_rpms:
  1050. exit_code, _, _ = module.run_command(['rpm', '-q', rpm])
  1051. if exit_code == 0:
  1052. installed_rpms.append(rpm)
  1053. facts['common']['installed_variant_rpms'] = installed_rpms
  1054. return facts
  1055. class OpenShiftFactsInternalError(Exception):
  1056. """Origin Facts Error"""
  1057. pass
  1058. class OpenShiftFactsUnsupportedRoleError(Exception):
  1059. """Origin Facts Unsupported Role Error"""
  1060. pass
  1061. class OpenShiftFactsFileWriteError(Exception):
  1062. """Origin Facts File Write Error"""
  1063. pass
  1064. class OpenShiftFactsMetadataUnavailableError(Exception):
  1065. """Origin Facts Metadata Unavailable Error"""
  1066. pass
  1067. class OpenShiftFacts(object):
  1068. """ Origin Facts
  1069. Attributes:
  1070. facts (dict): facts for the host
  1071. Args:
  1072. module (AnsibleModule): an AnsibleModule object
  1073. role (str): role for setting local facts
  1074. filename (str): local facts file to use
  1075. local_facts (dict): local facts to set
  1076. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  1077. '.' notation ex: ['master.named_certificates']
  1078. protected_facts_to_overwrite (list): protected facts to overwrite in jinja
  1079. '.' notation ex: ['master.master_count']
  1080. Raises:
  1081. OpenShiftFactsUnsupportedRoleError:
  1082. """
  1083. known_roles = ['common', 'master', 'node', 'etcd', 'hosted', 'docker']
  1084. # Disabling too-many-arguments, this should be cleaned up as a TODO item.
  1085. # pylint: disable=too-many-arguments
  1086. def __init__(self, role, filename, local_facts,
  1087. additive_facts_to_overwrite=None,
  1088. openshift_env=None,
  1089. protected_facts_to_overwrite=None):
  1090. self.changed = False
  1091. self.filename = filename
  1092. if role not in self.known_roles:
  1093. raise OpenShiftFactsUnsupportedRoleError(
  1094. "Role %s is not supported by this module" % role
  1095. )
  1096. self.role = role
  1097. self.system_facts = ansible_facts(module)
  1098. self.facts = self.generate_facts(local_facts,
  1099. additive_facts_to_overwrite,
  1100. openshift_env,
  1101. protected_facts_to_overwrite)
  1102. def generate_facts(self,
  1103. local_facts,
  1104. additive_facts_to_overwrite,
  1105. openshift_env,
  1106. protected_facts_to_overwrite):
  1107. """ Generate facts
  1108. Args:
  1109. local_facts (dict): local_facts for overriding generated defaults
  1110. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  1111. '.' notation ex: ['master.named_certificates']
  1112. openshift_env (dict): openshift_env facts for overriding generated defaults
  1113. protected_facts_to_overwrite (list): protected facts to overwrite in jinja
  1114. '.' notation ex: ['master.master_count']
  1115. Returns:
  1116. dict: The generated facts
  1117. """
  1118. local_facts = self.init_local_facts(local_facts,
  1119. additive_facts_to_overwrite,
  1120. openshift_env,
  1121. protected_facts_to_overwrite)
  1122. roles = local_facts.keys()
  1123. if 'common' in local_facts and 'deployment_type' in local_facts['common']:
  1124. deployment_type = local_facts['common']['deployment_type']
  1125. else:
  1126. deployment_type = 'origin'
  1127. defaults = self.get_defaults(roles, deployment_type)
  1128. provider_facts = self.init_provider_facts()
  1129. facts = apply_provider_facts(defaults, provider_facts)
  1130. facts = merge_facts(facts,
  1131. local_facts,
  1132. additive_facts_to_overwrite,
  1133. protected_facts_to_overwrite)
  1134. facts = merge_provider_facts(facts)
  1135. facts['current_config'] = get_current_config(facts)
  1136. facts = set_url_facts_if_unset(facts)
  1137. facts = set_project_cfg_facts_if_unset(facts)
  1138. facts = set_flannel_facts_if_unset(facts)
  1139. facts = set_nuage_facts_if_unset(facts)
  1140. facts = set_node_schedulability(facts)
  1141. facts = set_master_selectors(facts)
  1142. facts = set_metrics_facts_if_unset(facts)
  1143. facts = set_identity_providers_if_unset(facts)
  1144. facts = set_sdn_facts_if_unset(facts, self.system_facts)
  1145. facts = set_deployment_facts_if_unset(facts)
  1146. facts = set_version_facts_if_unset(facts)
  1147. facts = set_manageiq_facts_if_unset(facts)
  1148. facts = set_aggregate_facts(facts)
  1149. facts = set_etcd_facts_if_unset(facts)
  1150. facts = set_container_facts_if_unset(facts)
  1151. if not safe_get_bool(facts['common']['is_containerized']):
  1152. facts = set_installed_variant_rpm_facts(facts)
  1153. return dict(openshift=facts)
  1154. def get_defaults(self, roles, deployment_type):
  1155. """ Get default fact values
  1156. Args:
  1157. roles (list): list of roles for this host
  1158. Returns:
  1159. dict: The generated default facts
  1160. """
  1161. defaults = {}
  1162. ip_addr = self.system_facts['default_ipv4']['address']
  1163. exit_code, output, _ = module.run_command(['hostname', '-f'])
  1164. hostname_f = output.strip() if exit_code == 0 else ''
  1165. hostname_values = [hostname_f, self.system_facts['nodename'],
  1166. self.system_facts['fqdn']]
  1167. hostname = choose_hostname(hostname_values, ip_addr)
  1168. defaults['common'] = dict(use_openshift_sdn=True, ip=ip_addr,
  1169. public_ip=ip_addr,
  1170. deployment_type=deployment_type,
  1171. hostname=hostname,
  1172. public_hostname=hostname,
  1173. client_binary='oc', admin_binary='oadm',
  1174. dns_domain='cluster.local',
  1175. install_examples=True,
  1176. debug_level=2)
  1177. if 'master' in roles:
  1178. defaults['master'] = dict(api_use_ssl=True, api_port='8443',
  1179. controllers_port='8444',
  1180. console_use_ssl=True,
  1181. console_path='/console',
  1182. console_port='8443', etcd_use_ssl=True,
  1183. etcd_hosts='', etcd_port='4001',
  1184. portal_net='172.30.0.0/16',
  1185. embedded_etcd=True, embedded_kube=True,
  1186. embedded_dns=True, dns_port='53',
  1187. bind_addr='0.0.0.0',
  1188. session_max_seconds=3600,
  1189. session_name='ssn',
  1190. session_secrets_file='',
  1191. access_token_max_seconds=86400,
  1192. auth_token_max_seconds=500,
  1193. oauth_grant_method='auto')
  1194. if 'node' in roles:
  1195. defaults['node'] = dict(labels={}, annotations={},
  1196. portal_net='172.30.0.0/16',
  1197. iptables_sync_period='5s',
  1198. set_node_ip=False)
  1199. if 'docker' in roles:
  1200. defaults['docker'] = dict(disable_push_dockerhub=False)
  1201. defaults['hosted'] = dict(
  1202. registry=dict(
  1203. storage=dict(
  1204. kind=None,
  1205. volume=dict(
  1206. name='registry',
  1207. size='5Gi'
  1208. ),
  1209. nfs=dict(
  1210. directory='/exports',
  1211. options='*(rw,root_squash)'),
  1212. host=None,
  1213. access_modes=['ReadWriteMany'],
  1214. create_pv=True
  1215. )
  1216. )
  1217. )
  1218. return defaults
  1219. def guess_host_provider(self):
  1220. """ Guess the host provider
  1221. Returns:
  1222. dict: The generated default facts for the detected provider
  1223. """
  1224. # TODO: cloud provider facts should probably be submitted upstream
  1225. product_name = self.system_facts['product_name']
  1226. product_version = self.system_facts['product_version']
  1227. virt_type = self.system_facts['virtualization_type']
  1228. virt_role = self.system_facts['virtualization_role']
  1229. provider = None
  1230. metadata = None
  1231. # TODO: this is not exposed through module_utils/facts.py in ansible,
  1232. # need to create PR for ansible to expose it
  1233. bios_vendor = get_file_content(
  1234. '/sys/devices/virtual/dmi/id/bios_vendor'
  1235. )
  1236. if bios_vendor == 'Google':
  1237. provider = 'gce'
  1238. metadata_url = ('http://metadata.google.internal/'
  1239. 'computeMetadata/v1/?recursive=true')
  1240. headers = {'Metadata-Flavor': 'Google'}
  1241. metadata = get_provider_metadata(metadata_url, True, headers,
  1242. True)
  1243. # Filter sshKeys and serviceAccounts from gce metadata
  1244. if metadata:
  1245. metadata['project']['attributes'].pop('sshKeys', None)
  1246. metadata['instance'].pop('serviceAccounts', None)
  1247. elif (virt_type == 'xen' and virt_role == 'guest'
  1248. and re.match(r'.*\.amazon$', product_version)):
  1249. provider = 'ec2'
  1250. metadata_url = 'http://169.254.169.254/latest/meta-data/'
  1251. metadata = get_provider_metadata(metadata_url)
  1252. elif re.search(r'OpenStack', product_name):
  1253. provider = 'openstack'
  1254. metadata_url = ('http://169.254.169.254/openstack/latest/'
  1255. 'meta_data.json')
  1256. metadata = get_provider_metadata(metadata_url, True, None,
  1257. True)
  1258. if metadata:
  1259. ec2_compat_url = 'http://169.254.169.254/latest/meta-data/'
  1260. metadata['ec2_compat'] = get_provider_metadata(
  1261. ec2_compat_url
  1262. )
  1263. # disable pylint maybe-no-member because overloaded use of
  1264. # the module name causes pylint to not detect that results
  1265. # is an array or hash
  1266. # pylint: disable=maybe-no-member
  1267. # Filter public_keys and random_seed from openstack metadata
  1268. metadata.pop('public_keys', None)
  1269. metadata.pop('random_seed', None)
  1270. if not metadata['ec2_compat']:
  1271. metadata = None
  1272. return dict(name=provider, metadata=metadata)
  1273. def init_provider_facts(self):
  1274. """ Initialize the provider facts
  1275. Returns:
  1276. dict: The normalized provider facts
  1277. """
  1278. provider_info = self.guess_host_provider()
  1279. provider_facts = normalize_provider_facts(
  1280. provider_info.get('name'),
  1281. provider_info.get('metadata')
  1282. )
  1283. return provider_facts
  1284. # Disabling too-many-branches. This should be cleaned up as a TODO item.
  1285. #pylint: disable=too-many-branches
  1286. def init_local_facts(self, facts=None,
  1287. additive_facts_to_overwrite=None,
  1288. openshift_env=None,
  1289. protected_facts_to_overwrite=None):
  1290. """ Initialize the local facts
  1291. Args:
  1292. facts (dict): local facts to set
  1293. additive_facts_to_overwrite (list): additive facts to overwrite in jinja
  1294. '.' notation ex: ['master.named_certificates']
  1295. openshift_env (dict): openshift env facts to set
  1296. protected_facts_to_overwrite (list): protected facts to overwrite in jinja
  1297. '.' notation ex: ['master.master_count']
  1298. Returns:
  1299. dict: The result of merging the provided facts with existing
  1300. local facts
  1301. """
  1302. changed = False
  1303. facts_to_set = dict()
  1304. if facts is not None:
  1305. facts_to_set[self.role] = facts
  1306. if openshift_env != {} and openshift_env != None:
  1307. for fact, value in openshift_env.iteritems():
  1308. oo_env_facts = dict()
  1309. current_level = oo_env_facts
  1310. keys = fact.split('_')[1:]
  1311. if keys[0] != self.role:
  1312. continue
  1313. for key in keys:
  1314. if key == keys[-1]:
  1315. current_level[key] = value
  1316. elif key not in current_level:
  1317. current_level[key] = dict()
  1318. current_level = current_level[key]
  1319. facts_to_set = merge_facts(orig=facts_to_set,
  1320. new=oo_env_facts,
  1321. additive_facts_to_overwrite=[],
  1322. protected_facts_to_overwrite=[])
  1323. local_facts = get_local_facts_from_file(self.filename)
  1324. migrated_facts = migrate_local_facts(local_facts)
  1325. new_local_facts = merge_facts(migrated_facts,
  1326. facts_to_set,
  1327. additive_facts_to_overwrite,
  1328. protected_facts_to_overwrite)
  1329. if 'docker' in new_local_facts:
  1330. # remove duplicate and empty strings from registry lists
  1331. for cat in ['additional', 'blocked', 'insecure']:
  1332. key = '{0}_registries'.format(cat)
  1333. if key in new_local_facts['docker']:
  1334. val = new_local_facts['docker'][key]
  1335. if isinstance(val, basestring):
  1336. val = [x.strip() for x in val.split(',')]
  1337. new_local_facts['docker'][key] = list(set(val) - set(['']))
  1338. for facts in new_local_facts.values():
  1339. keys_to_delete = []
  1340. if isinstance(facts, dict):
  1341. for fact, value in facts.iteritems():
  1342. if value == "" or value is None:
  1343. keys_to_delete.append(fact)
  1344. for key in keys_to_delete:
  1345. del facts[key]
  1346. if new_local_facts != local_facts:
  1347. self.validate_local_facts(new_local_facts)
  1348. changed = True
  1349. if not module.check_mode:
  1350. save_local_facts(self.filename, new_local_facts)
  1351. self.changed = changed
  1352. return new_local_facts
  1353. def validate_local_facts(self, facts=None):
  1354. """ Validate local facts
  1355. Args:
  1356. facts (dict): local facts to validate
  1357. """
  1358. invalid_facts = dict()
  1359. invalid_facts = self.validate_master_facts(facts, invalid_facts)
  1360. if invalid_facts:
  1361. msg = 'Invalid facts detected:\n'
  1362. for key in invalid_facts.keys():
  1363. msg += '{0}: {1}\n'.format(key, invalid_facts[key])
  1364. module.fail_json(msg=msg,
  1365. changed=self.changed)
  1366. # disabling pylint errors for line-too-long since we're dealing
  1367. # with best effort reduction of error messages here.
  1368. # disabling errors for too-many-branches since we require checking
  1369. # many conditions.
  1370. # pylint: disable=line-too-long, too-many-branches
  1371. @staticmethod
  1372. def validate_master_facts(facts, invalid_facts):
  1373. """ Validate master facts
  1374. Args:
  1375. facts (dict): local facts to validate
  1376. invalid_facts (dict): collected invalid_facts
  1377. Returns:
  1378. dict: Invalid facts
  1379. """
  1380. if 'master' in facts:
  1381. # openshift.master.session_auth_secrets
  1382. if 'session_auth_secrets' in facts['master']:
  1383. session_auth_secrets = facts['master']['session_auth_secrets']
  1384. if not issubclass(type(session_auth_secrets), list):
  1385. invalid_facts['session_auth_secrets'] = 'Expects session_auth_secrets is a list.'
  1386. elif 'session_encryption_secrets' not in facts['master']:
  1387. invalid_facts['session_auth_secrets'] = ('openshift_master_session_encryption secrets must be set '
  1388. 'if openshift_master_session_auth_secrets is provided.')
  1389. elif len(session_auth_secrets) != len(facts['master']['session_encryption_secrets']):
  1390. invalid_facts['session_auth_secrets'] = ('openshift_master_session_auth_secrets and '
  1391. 'openshift_master_session_encryption_secrets must be '
  1392. 'equal length.')
  1393. else:
  1394. for secret in session_auth_secrets:
  1395. if len(secret) < 32:
  1396. invalid_facts['session_auth_secrets'] = ('Invalid secret in session_auth_secrets. '
  1397. 'Secrets must be at least 32 characters in length.')
  1398. # openshift.master.session_encryption_secrets
  1399. if 'session_encryption_secrets' in facts['master']:
  1400. session_encryption_secrets = facts['master']['session_encryption_secrets']
  1401. if not issubclass(type(session_encryption_secrets), list):
  1402. invalid_facts['session_encryption_secrets'] = 'Expects session_encryption_secrets is a list.'
  1403. elif 'session_auth_secrets' not in facts['master']:
  1404. invalid_facts['session_encryption_secrets'] = ('openshift_master_session_auth_secrets must be '
  1405. 'set if openshift_master_session_encryption_secrets '
  1406. 'is provided.')
  1407. else:
  1408. for secret in session_encryption_secrets:
  1409. if len(secret) not in [16, 24, 32]:
  1410. invalid_facts['session_encryption_secrets'] = ('Invalid secret in session_encryption_secrets. '
  1411. 'Secrets must be 16, 24, or 32 characters in length.')
  1412. return invalid_facts
  1413. def main():
  1414. """ main """
  1415. # disabling pylint errors for global-variable-undefined and invalid-name
  1416. # for 'global module' usage, since it is required to use ansible_facts
  1417. # pylint: disable=global-variable-undefined, invalid-name
  1418. global module
  1419. module = AnsibleModule(
  1420. argument_spec=dict(
  1421. role=dict(default='common', required=False,
  1422. choices=OpenShiftFacts.known_roles),
  1423. local_facts=dict(default=None, type='dict', required=False),
  1424. additive_facts_to_overwrite=dict(default=[], type='list', required=False),
  1425. openshift_env=dict(default={}, type='dict', required=False),
  1426. protected_facts_to_overwrite=dict(default=[], type='list', required=False),
  1427. ),
  1428. supports_check_mode=True,
  1429. add_file_common_args=True,
  1430. )
  1431. role = module.params['role']
  1432. local_facts = module.params['local_facts']
  1433. additive_facts_to_overwrite = module.params['additive_facts_to_overwrite']
  1434. openshift_env = module.params['openshift_env']
  1435. protected_facts_to_overwrite = module.params['protected_facts_to_overwrite']
  1436. fact_file = '/etc/ansible/facts.d/openshift.fact'
  1437. openshift_facts = OpenShiftFacts(role,
  1438. fact_file,
  1439. local_facts,
  1440. additive_facts_to_overwrite,
  1441. openshift_env,
  1442. protected_facts_to_overwrite)
  1443. file_params = module.params.copy()
  1444. file_params['path'] = fact_file
  1445. file_args = module.load_file_common_arguments(file_params)
  1446. changed = module.set_fs_attributes_if_different(file_args,
  1447. openshift_facts.changed)
  1448. return module.exit_json(changed=changed,
  1449. ansible_facts=openshift_facts.facts)
  1450. # ignore pylint errors related to the module_utils import
  1451. # pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import
  1452. # import module snippets
  1453. from ansible.module_utils.basic import *
  1454. from ansible.module_utils.facts import *
  1455. from ansible.module_utils.urls import *
  1456. if __name__ == '__main__':
  1457. main()