openshift_facts.py 19 KB

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