openshift_facts.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. # vim: expandtab:tabstop=4:shiftwidth=4
  4. DOCUMENTATION = '''
  5. ---
  6. module: openshift_facts
  7. short_description: OpenShift Facts
  8. author: Jason DeTiberus
  9. requirements: [ ]
  10. '''
  11. EXAMPLES = '''
  12. '''
  13. import ConfigParser
  14. import copy
  15. class OpenShiftFactsUnsupportedRoleError(Exception):
  16. pass
  17. class OpenShiftFactsFileWriteError(Exception):
  18. pass
  19. class OpenShiftFacts():
  20. known_roles = ['common', 'master', 'node', 'master_sdn', 'node_sdn']
  21. def __init__(self, role, filename, local_facts):
  22. self.changed = False
  23. self.filename = filename
  24. if role not in self.known_roles:
  25. raise OpenShiftFactsUnsupportedRoleError("Role %s is not supported by this module" % role)
  26. self.role = role
  27. self.facts = self.generate_facts(local_facts)
  28. def generate_facts(self, local_facts):
  29. local_facts = self.init_local_facts(local_facts)
  30. roles = local_facts.keys()
  31. defaults = self.get_defaults(roles)
  32. provider_facts = self.init_provider_facts()
  33. facts = self.apply_provider_facts(defaults, provider_facts, roles)
  34. facts = self.merge_facts(facts, local_facts)
  35. facts['current_config'] = self.current_config(facts)
  36. self.set_url_facts_if_unset(facts)
  37. return dict(openshift=facts)
  38. def set_url_facts_if_unset(self, facts):
  39. if 'master' in facts:
  40. for (url_var, use_ssl, port, default) in [
  41. ('api_url',
  42. facts['master']['api_use_ssl'],
  43. facts['master']['api_port'],
  44. facts['common']['hostname']),
  45. ('public_api_url',
  46. facts['master']['api_use_ssl'],
  47. facts['master']['api_port'],
  48. facts['common']['public_hostname']),
  49. ('console_url',
  50. facts['master']['console_use_ssl'],
  51. facts['master']['console_port'],
  52. facts['common']['hostname']),
  53. ('public_console_url' 'console_use_ssl',
  54. facts['master']['console_use_ssl'],
  55. facts['master']['console_port'],
  56. facts['common']['public_hostname'])]:
  57. if url_var not in facts['master']:
  58. scheme = 'https' if use_ssl else 'http'
  59. netloc = default
  60. if (scheme == 'https' and port != '443') or (scheme == 'http' and port != '80'):
  61. netloc = "%s:%s" % (netloc, port)
  62. facts['master'][url_var] = urlparse.urlunparse((scheme, netloc, '', '', '', ''))
  63. # Query current OpenShift config and return a dictionary containing
  64. # settings that may be valuable for determining actions that need to be
  65. # taken in the playbooks/roles
  66. def current_config(self, facts):
  67. current_config=dict()
  68. roles = [ role for role in facts if role not in ['common','provider'] ]
  69. for role in roles:
  70. if 'roles' in current_config:
  71. current_config['roles'].append(role)
  72. else:
  73. current_config['roles'] = [role]
  74. # TODO: parse the /etc/sysconfig/openshift-{master,node} config to
  75. # determine the location of files.
  76. # Query kubeconfig settings
  77. kubeconfig_dir = '/var/lib/openshift/openshift.local.certificates'
  78. if role == 'node':
  79. kubeconfig_dir = os.path.join(kubeconfig_dir, "node-%s" % facts['common']['hostname'])
  80. kubeconfig_path = os.path.join(kubeconfig_dir, '.kubeconfig')
  81. if os.path.isfile('/usr/bin/openshift') and os.path.isfile(kubeconfig_path):
  82. try:
  83. _, output, error = module.run_command(["/usr/bin/openshift", "ex",
  84. "config", "view", "-o",
  85. "json",
  86. "--kubeconfig=%s" % kubeconfig_path],
  87. check_rc=False)
  88. config = json.loads(output)
  89. try:
  90. for cluster in config['clusters']:
  91. config['clusters'][cluster]['certificate-authority-data'] = 'masked'
  92. except KeyError:
  93. pass
  94. try:
  95. for user in config['users']:
  96. config['users'][user]['client-certificate-data'] = 'masked'
  97. config['users'][user]['client-key-data'] = 'masked'
  98. except KeyError:
  99. pass
  100. current_config['kubeconfig'] = config
  101. except Exception:
  102. pass
  103. return current_config
  104. def apply_provider_facts(self, facts, provider_facts, roles):
  105. if not provider_facts:
  106. return facts
  107. use_openshift_sdn = provider_facts.get('use_openshift_sdn')
  108. if isinstance(use_openshift_sdn, bool):
  109. facts['common']['use_openshift_sdn'] = use_openshift_sdn
  110. common_vars = [('hostname', 'ip'), ('public_hostname', 'public_ip')]
  111. for h_var, ip_var in common_vars:
  112. ip_value = provider_facts['network'].get(ip_var)
  113. if ip_value:
  114. facts['common'][ip_var] = ip_value
  115. facts['common'][h_var] = self.choose_hostname([provider_facts['network'].get(h_var)], facts['common'][ip_var])
  116. if 'node' in roles:
  117. ext_id = provider_facts.get('external_id')
  118. if ext_id:
  119. facts['node']['external_id'] = ext_id
  120. facts['provider'] = provider_facts
  121. return facts
  122. def hostname_valid(self, hostname):
  123. if (not hostname or
  124. hostname.startswith('localhost') or
  125. hostname.endswith('localdomain') or
  126. len(hostname.split('.')) < 2):
  127. return False
  128. return True
  129. def choose_hostname(self, hostnames=[], fallback=''):
  130. hostname = fallback
  131. ips = [ i for i in hostnames if i is not None and re.match(r'\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\Z', i) ]
  132. hosts = [ i for i in hostnames if i is not None and i not in set(ips) ]
  133. for host_list in (hosts, ips):
  134. for h in host_list:
  135. if self.hostname_valid(h):
  136. return h
  137. return hostname
  138. def get_defaults(self, roles):
  139. hardware_facts = self.get_hardware_facts()
  140. net_facts = self.get_net_facts()
  141. base_facts = self.get_base_facts()
  142. defaults = dict()
  143. common = dict(use_openshift_sdn=True)
  144. ip = net_facts['default_ipv4']['address']
  145. common['ip'] = ip
  146. common['public_ip'] = ip
  147. rc, output, error = module.run_command(['hostname', '-f'])
  148. hostname_f = output.strip() if rc == 0 else ''
  149. hostname_values = [hostname_f, base_facts['nodename'], base_facts['fqdn']]
  150. hostname = self.choose_hostname(hostname_values)
  151. common['hostname'] = hostname
  152. common['public_hostname'] = hostname
  153. defaults['common'] = common
  154. if 'master' in roles:
  155. # TODO: provide for a better way to override just the port, or just
  156. # the urls, instead of forcing both, also to override the hostname
  157. # without having to re-generate these urls later
  158. master = dict(api_use_ssl=True, api_port='8443',
  159. console_use_ssl=True, console_path='/console',
  160. console_port='8443', etcd_use_ssl=False,
  161. etcd_port='4001')
  162. defaults['master'] = master
  163. if 'node' in roles:
  164. node = dict(external_id=common['hostname'], pod_cidr='',
  165. labels={}, annotations={})
  166. node['resources_cpu'] = hardware_facts['processor_cores']
  167. node['resources_memory'] = int(int(hardware_facts['memtotal_mb']) * 1024 * 1024 * 0.75)
  168. defaults['node'] = node
  169. return defaults
  170. def merge_facts(self, orig, new):
  171. facts = dict()
  172. for key, value in orig.iteritems():
  173. if key in new:
  174. if isinstance(value, dict):
  175. facts[key] = self.merge_facts(value, new[key])
  176. else:
  177. facts[key] = copy.copy(new[key])
  178. else:
  179. facts[key] = copy.deepcopy(value)
  180. new_keys = set(new.keys()) - set(orig.keys())
  181. for key in new_keys:
  182. facts[key] = copy.deepcopy(new[key])
  183. return facts
  184. def query_metadata(self, metadata_url, headers=None, expect_json=False):
  185. r, info = fetch_url(module, metadata_url, headers=headers)
  186. if info['status'] != 200:
  187. module.fail_json(msg='Failed to query metadata', result=r,
  188. info=info)
  189. if expect_json:
  190. return module.from_json(r.read())
  191. else:
  192. return [line.strip() for line in r.readlines()]
  193. def walk_metadata(self, metadata_url, headers=None, expect_json=False):
  194. metadata = dict()
  195. for line in self.query_metadata(metadata_url, headers, expect_json):
  196. if line.endswith('/') and not line == 'public-keys/':
  197. key = line[:-1]
  198. metadata[key]=self.walk_metadata(metadata_url + line, headers,
  199. expect_json)
  200. else:
  201. results = self.query_metadata(metadata_url + line, headers,
  202. expect_json)
  203. if len(results) == 1:
  204. metadata[line] = results.pop()
  205. else:
  206. metadata[line] = results
  207. return metadata
  208. def get_provider_metadata(self, metadata_url, supports_recursive=False,
  209. headers=None, expect_json=False):
  210. if supports_recursive:
  211. metadata = self.query_metadata(metadata_url, headers, expect_json)
  212. else:
  213. metadata = self.walk_metadata(metadata_url, headers, expect_json)
  214. return metadata
  215. def get_hardware_facts(self):
  216. if not hasattr(self, 'hardware_facts'):
  217. self.hardware_facts = Hardware().populate()
  218. return self.hardware_facts
  219. def get_base_facts(self):
  220. if not hasattr(self, 'base_facts'):
  221. self.base_facts = Facts().populate()
  222. return self.base_facts
  223. def get_virt_facts(self):
  224. if not hasattr(self, 'virt_facts'):
  225. self.virt_facts = Virtual().populate()
  226. return self.virt_facts
  227. def get_net_facts(self):
  228. if not hasattr(self, 'net_facts'):
  229. self.net_facts = Network(module).populate()
  230. return self.net_facts
  231. def guess_host_provider(self):
  232. # TODO: cloud provider facts should probably be submitted upstream
  233. virt_facts = self.get_virt_facts()
  234. hardware_facts = self.get_hardware_facts()
  235. product_name = hardware_facts['product_name']
  236. product_version = hardware_facts['product_version']
  237. virt_type = virt_facts['virtualization_type']
  238. virt_role = virt_facts['virtualization_role']
  239. provider = None
  240. metadata = None
  241. # TODO: this is not exposed through module_utils/facts.py in ansible,
  242. # need to create PR for ansible to expose it
  243. bios_vendor = get_file_content('/sys/devices/virtual/dmi/id/bios_vendor')
  244. if bios_vendor == 'Google':
  245. provider = 'gce'
  246. metadata_url = 'http://metadata.google.internal/computeMetadata/v1/?recursive=true'
  247. headers = {'Metadata-Flavor': 'Google'}
  248. metadata = self.get_provider_metadata(metadata_url, True, headers,
  249. True)
  250. # Filter sshKeys and serviceAccounts from gce metadata
  251. metadata['project']['attributes'].pop('sshKeys', None)
  252. metadata['instance'].pop('serviceAccounts', None)
  253. elif virt_type == 'xen' and virt_role == 'guest' and re.match(r'.*\.amazon$', product_version):
  254. provider = 'ec2'
  255. metadata_url = 'http://169.254.169.254/latest/meta-data/'
  256. metadata = self.get_provider_metadata(metadata_url)
  257. elif re.search(r'OpenStack', product_name):
  258. provider = 'openstack'
  259. metadata_url = 'http://169.254.169.254/openstack/latest/meta_data.json'
  260. metadata = self.get_provider_metadata(metadata_url, True, None, True)
  261. ec2_compat_url = 'http://169.254.169.254/latest/meta-data/'
  262. metadata['ec2_compat'] = self.get_provider_metadata(ec2_compat_url)
  263. # Filter public_keys and random_seed from openstack metadata
  264. metadata.pop('public_keys', None)
  265. metadata.pop('random_seed', None)
  266. return dict(name=provider, metadata=metadata)
  267. def normalize_provider_facts(self, provider, metadata):
  268. if provider is None or metadata is None:
  269. return {}
  270. # TODO: test for ipv6_enabled where possible (gce, aws do not support)
  271. # and configure ipv6 facts if available
  272. # TODO: add support for setting user_data if available
  273. facts = dict(name=provider, metadata=metadata)
  274. network = dict(interfaces=[], ipv6_enabled=False)
  275. if provider == 'gce':
  276. for interface in metadata['instance']['networkInterfaces']:
  277. int_info = dict(ips=[interface['ip']], network_type=provider)
  278. int_info['public_ips'] = [ ac['externalIp'] for ac in interface['accessConfigs'] ]
  279. int_info['public_ips'].extend(interface['forwardedIps'])
  280. _, _, network_id = interface['network'].rpartition('/')
  281. int_info['network_id'] = network_id
  282. network['interfaces'].append(int_info)
  283. _, _, zone = metadata['instance']['zone'].rpartition('/')
  284. facts['zone'] = zone
  285. facts['external_id'] = metadata['instance']['id']
  286. # Default to no sdn for GCE deployments
  287. facts['use_openshift_sdn'] = False
  288. # GCE currently only supports a single interface
  289. network['ip'] = network['interfaces'][0]['ips'][0]
  290. network['public_ip'] = network['interfaces'][0]['public_ips'][0]
  291. network['hostname'] = metadata['instance']['hostname']
  292. # TODO: attempt to resolve public_hostname
  293. network['public_hostname'] = network['public_ip']
  294. elif provider == 'ec2':
  295. for interface in sorted(metadata['network']['interfaces']['macs'].values(),
  296. key=lambda x: x['device-number']):
  297. int_info = dict()
  298. var_map = {'ips': 'local-ipv4s', 'public_ips': 'public-ipv4s'}
  299. for ips_var, int_var in var_map.iteritems():
  300. ips = interface[int_var]
  301. int_info[ips_var] = [ips] if isinstance(ips, basestring) else ips
  302. int_info['network_type'] = 'vpc' if 'vpc-id' in interface else 'classic'
  303. int_info['network_id'] = interface['subnet-id'] if int_info['network_type'] == 'vpc' else None
  304. network['interfaces'].append(int_info)
  305. facts['zone'] = metadata['placement']['availability-zone']
  306. facts['external_id'] = metadata['instance-id']
  307. # TODO: actually attempt to determine default local and public ips
  308. # by using the ansible default ip fact and the ipv4-associations
  309. # form the ec2 metadata
  310. network['ip'] = metadata['local-ipv4']
  311. network['public_ip'] = metadata['public-ipv4']
  312. # TODO: verify that local hostname makes sense and is resolvable
  313. network['hostname'] = metadata['local-hostname']
  314. # TODO: verify that public hostname makes sense and is resolvable
  315. network['public_hostname'] = metadata['public-hostname']
  316. elif provider == 'openstack':
  317. # openstack ec2 compat api does not support network interfaces and
  318. # the version tested on did not include the info in the openstack
  319. # metadata api, should be updated if neutron exposes this.
  320. facts['zone'] = metadata['availability_zone']
  321. facts['external_id'] = metadata['uuid']
  322. network['ip'] = metadata['ec2_compat']['local-ipv4']
  323. network['public_ip'] = metadata['ec2_compat']['public-ipv4']
  324. # TODO: verify local hostname makes sense and is resolvable
  325. network['hostname'] = metadata['hostname']
  326. # TODO: verify that public hostname makes sense and is resolvable
  327. network['public_hostname'] = metadata['ec2_compat']['public-hostname']
  328. facts['network'] = network
  329. return facts
  330. def init_provider_facts(self):
  331. provider_info = self.guess_host_provider()
  332. provider_facts = self.normalize_provider_facts(
  333. provider_info.get('name'),
  334. provider_info.get('metadata')
  335. )
  336. return provider_facts
  337. def get_facts(self):
  338. # TODO: transform facts into cleaner format (openshift_<blah> instead
  339. # of openshift.<blah>
  340. return self.facts
  341. def init_local_facts(self, facts={}):
  342. changed = False
  343. local_facts = ConfigParser.SafeConfigParser()
  344. local_facts.read(self.filename)
  345. section = self.role
  346. if not local_facts.has_section(section):
  347. local_facts.add_section(section)
  348. changed = True
  349. for key, value in facts.iteritems():
  350. if isinstance(value, bool):
  351. value = str(value)
  352. if not value:
  353. continue
  354. if not local_facts.has_option(section, key) or local_facts.get(section, key) != value:
  355. local_facts.set(section, key, value)
  356. changed = True
  357. if changed and not module.check_mode:
  358. try:
  359. fact_dir = os.path.dirname(self.filename)
  360. if not os.path.exists(fact_dir):
  361. os.makedirs(fact_dir)
  362. with open(self.filename, 'w') as fact_file:
  363. local_facts.write(fact_file)
  364. except (IOError, OSError) as e:
  365. raise OpenShiftFactsFileWriteError("Could not create fact file: %s, error: %s" % (self.filename, e))
  366. self.changed = changed
  367. role_facts = dict()
  368. for section in local_facts.sections():
  369. role_facts[section] = dict()
  370. for opt, val in local_facts.items(section):
  371. role_facts[section][opt] = val
  372. return role_facts
  373. def main():
  374. global module
  375. module = AnsibleModule(
  376. argument_spec = dict(
  377. role=dict(default='common',
  378. choices=OpenShiftFacts.known_roles,
  379. required=False),
  380. local_facts=dict(default={}, type='dict', required=False),
  381. ),
  382. supports_check_mode=True,
  383. add_file_common_args=True,
  384. )
  385. role = module.params['role']
  386. local_facts = module.params['local_facts']
  387. fact_file = '/etc/ansible/facts.d/openshift.fact'
  388. openshift_facts = OpenShiftFacts(role, fact_file, local_facts)
  389. file_params = module.params.copy()
  390. file_params['path'] = fact_file
  391. file_args = module.load_file_common_arguments(file_params)
  392. changed = module.set_fs_attributes_if_different(file_args,
  393. openshift_facts.changed)
  394. return module.exit_json(changed=changed,
  395. ansible_facts=openshift_facts.get_facts())
  396. # import module snippets
  397. from ansible.module_utils.basic import *
  398. from ansible.module_utils.facts import *
  399. from ansible.module_utils.urls import *
  400. main()