openshift_master.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. # vim: expandtab:tabstop=4:shiftwidth=4
  4. '''
  5. Custom filters for use in openshift-master
  6. '''
  7. import copy
  8. import sys
  9. from distutils.version import LooseVersion # pylint: disable=no-name-in-module,import-error
  10. from ansible import errors
  11. from ansible.parsing.yaml.dumper import AnsibleDumper
  12. from ansible.plugins.filter.core import to_bool as ansible_bool
  13. from six import string_types
  14. import yaml
  15. class IdentityProviderBase(object):
  16. """ IdentityProviderBase
  17. Attributes:
  18. name (str): Identity provider Name
  19. login (bool): Is this identity provider a login provider?
  20. challenge (bool): Is this identity provider a challenge provider?
  21. provider (dict): Provider specific config
  22. _idp (dict): internal copy of the IDP dict passed in
  23. _required (list): List of lists of strings for required attributes
  24. _optional (list): List of lists of strings for optional attributes
  25. _allow_additional (bool): Does this provider support attributes
  26. not in _required and _optional
  27. Args:
  28. api_version(str): OpenShift config version
  29. idp (dict): idp config dict
  30. Raises:
  31. AnsibleFilterError:
  32. """
  33. # disabling this check since the number of instance attributes are
  34. # necessary for this class
  35. # pylint: disable=too-many-instance-attributes
  36. def __init__(self, api_version, idp):
  37. if api_version not in ['v1']:
  38. raise errors.AnsibleFilterError("|failed api version {0} unknown".format(api_version))
  39. self._idp = copy.deepcopy(idp)
  40. if 'name' not in self._idp:
  41. raise errors.AnsibleFilterError("|failed identity provider missing a name")
  42. if 'kind' not in self._idp:
  43. raise errors.AnsibleFilterError("|failed identity provider missing a kind")
  44. self.name = self._idp.pop('name')
  45. self.login = ansible_bool(self._idp.pop('login', False))
  46. self.challenge = ansible_bool(self._idp.pop('challenge', False))
  47. self.provider = dict(apiVersion=api_version, kind=self._idp.pop('kind'))
  48. mm_keys = ('mappingMethod', 'mapping_method')
  49. mapping_method = None
  50. for key in mm_keys:
  51. if key in self._idp:
  52. mapping_method = self._idp.pop(key)
  53. if mapping_method is None:
  54. mapping_method = self.get_default('mappingMethod')
  55. self.mapping_method = mapping_method
  56. valid_mapping_methods = ['add', 'claim', 'generate', 'lookup']
  57. if self.mapping_method not in valid_mapping_methods:
  58. raise errors.AnsibleFilterError("|failed unknown mapping method "
  59. "for provider {0}".format(self.__class__.__name__))
  60. self._required = []
  61. self._optional = []
  62. self._allow_additional = True
  63. @staticmethod
  64. def validate_idp_list(idp_list, openshift_version, deployment_type):
  65. ''' validates a list of idps '''
  66. login_providers = [x.name for x in idp_list if x.login]
  67. multiple_logins_unsupported = False
  68. if len(login_providers) > 1:
  69. if deployment_type in ['enterprise', 'online', 'atomic-enterprise', 'openshift-enterprise']:
  70. if LooseVersion(openshift_version) < LooseVersion('3.2'):
  71. multiple_logins_unsupported = True
  72. if deployment_type in ['origin']:
  73. if LooseVersion(openshift_version) < LooseVersion('1.2'):
  74. multiple_logins_unsupported = True
  75. if multiple_logins_unsupported:
  76. raise errors.AnsibleFilterError("|failed multiple providers are "
  77. "not allowed for login. login "
  78. "providers: {0}".format(', '.join(login_providers)))
  79. names = [x.name for x in idp_list]
  80. if len(set(names)) != len(names):
  81. raise errors.AnsibleFilterError("|failed more than one provider configured with the same name")
  82. for idp in idp_list:
  83. idp.validate()
  84. def validate(self):
  85. ''' validate an instance of this idp class '''
  86. pass
  87. @staticmethod
  88. def get_default(key):
  89. ''' get a default value for a given key '''
  90. if key == 'mappingMethod':
  91. return 'claim'
  92. else:
  93. return None
  94. def set_provider_item(self, items, required=False):
  95. ''' set a provider item based on the list of item names provided. '''
  96. for item in items:
  97. provider_key = items[0]
  98. if item in self._idp:
  99. self.provider[provider_key] = self._idp.pop(item)
  100. break
  101. else:
  102. default = self.get_default(provider_key)
  103. if default is not None:
  104. self.provider[provider_key] = default
  105. elif required:
  106. raise errors.AnsibleFilterError("|failed provider {0} missing "
  107. "required key {1}".format(self.__class__.__name__, provider_key))
  108. def set_provider_items(self):
  109. ''' set the provider items for this idp '''
  110. for items in self._required:
  111. self.set_provider_item(items, True)
  112. for items in self._optional:
  113. self.set_provider_item(items)
  114. if self._allow_additional:
  115. for key in self._idp.keys():
  116. self.set_provider_item([key])
  117. else:
  118. if len(self._idp) > 0:
  119. raise errors.AnsibleFilterError("|failed provider {0} "
  120. "contains unknown keys "
  121. "{1}".format(self.__class__.__name__, ', '.join(self._idp.keys())))
  122. def to_dict(self):
  123. ''' translate this idp to a dictionary '''
  124. return dict(name=self.name, challenge=self.challenge,
  125. login=self.login, mappingMethod=self.mapping_method,
  126. provider=self.provider)
  127. class LDAPPasswordIdentityProvider(IdentityProviderBase):
  128. """ LDAPPasswordIdentityProvider
  129. Attributes:
  130. Args:
  131. api_version(str): OpenShift config version
  132. idp (dict): idp config dict
  133. Raises:
  134. AnsibleFilterError:
  135. """
  136. def __init__(self, api_version, idp):
  137. super(self.__class__, self).__init__(api_version, idp)
  138. self._allow_additional = False
  139. self._required += [['attributes'], ['url'], ['insecure']]
  140. self._optional += [['ca'],
  141. ['bindDN', 'bind_dn'],
  142. ['bindPassword', 'bind_password']]
  143. self._idp['insecure'] = ansible_bool(self._idp.pop('insecure', False))
  144. if 'attributes' in self._idp and 'preferred_username' in self._idp['attributes']:
  145. pref_user = self._idp['attributes'].pop('preferred_username')
  146. self._idp['attributes']['preferredUsername'] = pref_user
  147. def validate(self):
  148. ''' validate this idp instance '''
  149. if not isinstance(self.provider['attributes'], dict):
  150. raise errors.AnsibleFilterError("|failed attributes for provider "
  151. "{0} must be a dictionary".format(self.__class__.__name__))
  152. attrs = ['id', 'email', 'name', 'preferredUsername']
  153. for attr in attrs:
  154. if attr in self.provider['attributes'] and not isinstance(self.provider['attributes'][attr], list):
  155. raise errors.AnsibleFilterError("|failed {0} attribute for "
  156. "provider {1} must be a list".format(attr, self.__class__.__name__))
  157. unknown_attrs = set(self.provider['attributes'].keys()) - set(attrs)
  158. if len(unknown_attrs) > 0:
  159. raise errors.AnsibleFilterError("|failed provider {0} has unknown "
  160. "attributes: {1}".format(self.__class__.__name__, ', '.join(unknown_attrs)))
  161. class KeystonePasswordIdentityProvider(IdentityProviderBase):
  162. """ KeystoneIdentityProvider
  163. Attributes:
  164. Args:
  165. api_version(str): OpenShift config version
  166. idp (dict): idp config dict
  167. Raises:
  168. AnsibleFilterError:
  169. """
  170. def __init__(self, api_version, idp):
  171. super(self.__class__, self).__init__(api_version, idp)
  172. self._allow_additional = False
  173. self._required += [['url'], ['domainName', 'domain_name']]
  174. self._optional += [['ca'], ['certFile', 'cert_file'], ['keyFile', 'key_file']]
  175. class RequestHeaderIdentityProvider(IdentityProviderBase):
  176. """ RequestHeaderIdentityProvider
  177. Attributes:
  178. Args:
  179. api_version(str): OpenShift config version
  180. idp (dict): idp config dict
  181. Raises:
  182. AnsibleFilterError:
  183. """
  184. def __init__(self, api_version, idp):
  185. super(self.__class__, self).__init__(api_version, idp)
  186. self._allow_additional = False
  187. self._required += [['headers']]
  188. self._optional += [['challengeURL', 'challenge_url'],
  189. ['loginURL', 'login_url'],
  190. ['clientCA', 'client_ca'],
  191. ['clientCommonNames', 'client_common_names'],
  192. ['emailHeaders', 'email_headers'],
  193. ['nameHeaders', 'name_headers'],
  194. ['preferredUsernameHeaders', 'preferred_username_headers']]
  195. def validate(self):
  196. ''' validate this idp instance '''
  197. if not isinstance(self.provider['headers'], list):
  198. raise errors.AnsibleFilterError("|failed headers for provider {0} "
  199. "must be a list".format(self.__class__.__name__))
  200. class AllowAllPasswordIdentityProvider(IdentityProviderBase):
  201. """ AllowAllPasswordIdentityProvider
  202. Attributes:
  203. Args:
  204. api_version(str): OpenShift config version
  205. idp (dict): idp config dict
  206. Raises:
  207. AnsibleFilterError:
  208. """
  209. def __init__(self, api_version, idp):
  210. super(self.__class__, self).__init__(api_version, idp)
  211. self._allow_additional = False
  212. class DenyAllPasswordIdentityProvider(IdentityProviderBase):
  213. """ DenyAllPasswordIdentityProvider
  214. Attributes:
  215. Args:
  216. api_version(str): OpenShift config version
  217. idp (dict): idp config dict
  218. Raises:
  219. AnsibleFilterError:
  220. """
  221. def __init__(self, api_version, idp):
  222. super(self.__class__, self).__init__(api_version, idp)
  223. self._allow_additional = False
  224. class HTPasswdPasswordIdentityProvider(IdentityProviderBase):
  225. """ HTPasswdPasswordIdentity
  226. Attributes:
  227. Args:
  228. api_version(str): OpenShift config version
  229. idp (dict): idp config dict
  230. Raises:
  231. AnsibleFilterError:
  232. """
  233. def __init__(self, api_version, idp):
  234. super(self.__class__, self).__init__(api_version, idp)
  235. self._allow_additional = False
  236. self._required += [['file', 'filename', 'fileName', 'file_name']]
  237. @staticmethod
  238. def get_default(key):
  239. if key == 'file':
  240. return '/etc/origin/htpasswd'
  241. else:
  242. return IdentityProviderBase.get_default(key)
  243. class BasicAuthPasswordIdentityProvider(IdentityProviderBase):
  244. """ BasicAuthPasswordIdentityProvider
  245. Attributes:
  246. Args:
  247. api_version(str): OpenShift config version
  248. idp (dict): idp config dict
  249. Raises:
  250. AnsibleFilterError:
  251. """
  252. def __init__(self, api_version, idp):
  253. super(self.__class__, self).__init__(api_version, idp)
  254. self._allow_additional = False
  255. self._required += [['url']]
  256. self._optional += [['ca'], ['certFile', 'cert_file'], ['keyFile', 'key_file']]
  257. class IdentityProviderOauthBase(IdentityProviderBase):
  258. """ IdentityProviderOauthBase
  259. Attributes:
  260. Args:
  261. api_version(str): OpenShift config version
  262. idp (dict): idp config dict
  263. Raises:
  264. AnsibleFilterError:
  265. """
  266. def __init__(self, api_version, idp):
  267. super(self.__class__, self).__init__(api_version, idp)
  268. self._allow_additional = False
  269. self._required += [['clientID', 'client_id'], ['clientSecret', 'client_secret']]
  270. def validate(self):
  271. ''' validate this idp instance '''
  272. if self.challenge:
  273. raise errors.AnsibleFilterError("|failed provider {0} does not "
  274. "allow challenge authentication".format(self.__class__.__name__))
  275. class OpenIDIdentityProvider(IdentityProviderOauthBase):
  276. """ OpenIDIdentityProvider
  277. Attributes:
  278. Args:
  279. api_version(str): OpenShift config version
  280. idp (dict): idp config dict
  281. Raises:
  282. AnsibleFilterError:
  283. """
  284. def __init__(self, api_version, idp):
  285. IdentityProviderOauthBase.__init__(self, api_version, idp)
  286. self._required += [['claims'], ['urls']]
  287. self._optional += [['ca'],
  288. ['extraScopes'],
  289. ['extraAuthorizeParameters']]
  290. if 'claims' in self._idp and 'preferred_username' in self._idp['claims']:
  291. pref_user = self._idp['claims'].pop('preferred_username')
  292. self._idp['claims']['preferredUsername'] = pref_user
  293. if 'urls' in self._idp and 'user_info' in self._idp['urls']:
  294. user_info = self._idp['urls'].pop('user_info')
  295. self._idp['urls']['userInfo'] = user_info
  296. if 'extra_scopes' in self._idp:
  297. self._idp['extraScopes'] = self._idp.pop('extra_scopes')
  298. if 'extra_authorize_parameters' in self._idp:
  299. self._idp['extraAuthorizeParameters'] = self._idp.pop('extra_authorize_parameters')
  300. if 'extraAuthorizeParameters' in self._idp:
  301. if 'include_granted_scopes' in self._idp['extraAuthorizeParameters']:
  302. val = ansible_bool(self._idp['extraAuthorizeParameters'].pop('include_granted_scopes'))
  303. self._idp['extraAuthorizeParameters']['include_granted_scopes'] = val
  304. def validate(self):
  305. ''' validate this idp instance '''
  306. IdentityProviderOauthBase.validate(self)
  307. if not isinstance(self.provider['claims'], dict):
  308. raise errors.AnsibleFilterError("|failed claims for provider {0} "
  309. "must be a dictionary".format(self.__class__.__name__))
  310. for var, var_type in (('extraScopes', list), ('extraAuthorizeParameters', dict)):
  311. if var in self.provider and not isinstance(self.provider[var], var_type):
  312. raise errors.AnsibleFilterError("|failed {1} for provider "
  313. "{0} must be a {2}".format(self.__class__.__name__,
  314. var,
  315. var_type.__class__.__name__))
  316. required_claims = ['id']
  317. optional_claims = ['email', 'name', 'preferredUsername']
  318. all_claims = required_claims + optional_claims
  319. for claim in required_claims:
  320. if claim in required_claims and claim not in self.provider['claims']:
  321. raise errors.AnsibleFilterError("|failed {0} claim missing "
  322. "for provider {1}".format(claim, self.__class__.__name__))
  323. for claim in all_claims:
  324. if claim in self.provider['claims'] and not isinstance(self.provider['claims'][claim], list):
  325. raise errors.AnsibleFilterError("|failed {0} claims for "
  326. "provider {1} must be a list".format(claim, self.__class__.__name__))
  327. unknown_claims = set(self.provider['claims'].keys()) - set(all_claims)
  328. if len(unknown_claims) > 0:
  329. raise errors.AnsibleFilterError("|failed provider {0} has unknown "
  330. "claims: {1}".format(self.__class__.__name__, ', '.join(unknown_claims)))
  331. if not isinstance(self.provider['urls'], dict):
  332. raise errors.AnsibleFilterError("|failed urls for provider {0} "
  333. "must be a dictionary".format(self.__class__.__name__))
  334. required_urls = ['authorize', 'token']
  335. optional_urls = ['userInfo']
  336. all_urls = required_urls + optional_urls
  337. for url in required_urls:
  338. if url not in self.provider['urls']:
  339. raise errors.AnsibleFilterError("|failed {0} url missing for "
  340. "provider {1}".format(url, self.__class__.__name__))
  341. unknown_urls = set(self.provider['urls'].keys()) - set(all_urls)
  342. if len(unknown_urls) > 0:
  343. raise errors.AnsibleFilterError("|failed provider {0} has unknown "
  344. "urls: {1}".format(self.__class__.__name__, ', '.join(unknown_urls)))
  345. class GoogleIdentityProvider(IdentityProviderOauthBase):
  346. """ GoogleIdentityProvider
  347. Attributes:
  348. Args:
  349. api_version(str): OpenShift config version
  350. idp (dict): idp config dict
  351. Raises:
  352. AnsibleFilterError:
  353. """
  354. def __init__(self, api_version, idp):
  355. IdentityProviderOauthBase.__init__(self, api_version, idp)
  356. self._optional += [['hostedDomain', 'hosted_domain']]
  357. class GitHubIdentityProvider(IdentityProviderOauthBase):
  358. """ GitHubIdentityProvider
  359. Attributes:
  360. Args:
  361. api_version(str): OpenShift config version
  362. idp (dict): idp config dict
  363. Raises:
  364. AnsibleFilterError:
  365. """
  366. def __init__(self, api_version, idp):
  367. IdentityProviderOauthBase.__init__(self, api_version, idp)
  368. self._optional += [['organizations']]
  369. class FilterModule(object):
  370. ''' Custom ansible filters for use by the openshift_master role'''
  371. @staticmethod
  372. def translate_idps(idps, api_version, openshift_version, deployment_type):
  373. ''' Translates a list of dictionaries into a valid identityProviders config '''
  374. idp_list = []
  375. if not isinstance(idps, list):
  376. raise errors.AnsibleFilterError("|failed expects to filter on a list of identity providers")
  377. for idp in idps:
  378. if not isinstance(idp, dict):
  379. raise errors.AnsibleFilterError("|failed identity providers must be a list of dictionaries")
  380. cur_module = sys.modules[__name__]
  381. idp_class = getattr(cur_module, idp['kind'], None)
  382. idp_inst = idp_class(api_version, idp) if idp_class is not None else IdentityProviderBase(api_version, idp)
  383. idp_inst.set_provider_items()
  384. idp_list.append(idp_inst)
  385. IdentityProviderBase.validate_idp_list(idp_list, openshift_version, deployment_type)
  386. return yaml.dump([idp.to_dict() for idp in idp_list],
  387. allow_unicode=True,
  388. default_flow_style=False,
  389. Dumper=AnsibleDumper)
  390. @staticmethod
  391. def validate_pcs_cluster(data, masters=None):
  392. ''' Validates output from "pcs status", ensuring that each master
  393. provided is online.
  394. Ex: data = ('...',
  395. 'PCSD Status:',
  396. 'master1.example.com: Online',
  397. 'master2.example.com: Online',
  398. 'master3.example.com: Online',
  399. '...')
  400. masters = ['master1.example.com',
  401. 'master2.example.com',
  402. 'master3.example.com']
  403. returns True
  404. '''
  405. if not issubclass(type(data), string_types):
  406. raise errors.AnsibleFilterError("|failed expects data is a string or unicode")
  407. if not issubclass(type(masters), list):
  408. raise errors.AnsibleFilterError("|failed expects masters is a list")
  409. valid = True
  410. for master in masters:
  411. if "{0}: Online".format(master) not in data:
  412. valid = False
  413. return valid
  414. @staticmethod
  415. def certificates_to_synchronize(hostvars, include_keys=True, include_ca=True):
  416. ''' Return certificates to synchronize based on facts. '''
  417. if not issubclass(type(hostvars), dict):
  418. raise errors.AnsibleFilterError("|failed expects hostvars is a dict")
  419. certs = ['admin.crt',
  420. 'admin.key',
  421. 'admin.kubeconfig',
  422. 'master.kubelet-client.crt',
  423. 'master.kubelet-client.key']
  424. if bool(include_ca):
  425. certs += ['ca.crt', 'ca.key']
  426. if bool(include_keys):
  427. certs += ['serviceaccounts.private.key',
  428. 'serviceaccounts.public.key']
  429. if bool(hostvars['openshift']['common']['version_gte_3_1_or_1_1']):
  430. certs += ['master.proxy-client.crt',
  431. 'master.proxy-client.key']
  432. if not bool(hostvars['openshift']['common']['version_gte_3_2_or_1_2']):
  433. certs += ['openshift-master.crt',
  434. 'openshift-master.key',
  435. 'openshift-master.kubeconfig']
  436. if bool(hostvars['openshift']['common']['version_gte_3_3_or_1_3']):
  437. certs += ['service-signer.crt',
  438. 'service-signer.key']
  439. if not bool(hostvars['openshift']['common']['version_gte_3_5_or_1_5']):
  440. certs += ['openshift-registry.crt',
  441. 'openshift-registry.key',
  442. 'openshift-registry.kubeconfig',
  443. 'openshift-router.crt',
  444. 'openshift-router.key',
  445. 'openshift-router.kubeconfig']
  446. return certs
  447. @staticmethod
  448. def oo_htpasswd_users_from_file(file_contents):
  449. ''' return a dictionary of htpasswd users from htpasswd file contents '''
  450. htpasswd_entries = {}
  451. if not isinstance(file_contents, string_types):
  452. raise errors.AnsibleFilterError("failed, expects to filter on a string")
  453. for line in file_contents.splitlines():
  454. user = None
  455. passwd = None
  456. if len(line) == 0:
  457. continue
  458. if ':' in line:
  459. user, passwd = line.split(':', 1)
  460. if user is None or len(user) == 0 or passwd is None or len(passwd) == 0:
  461. error_msg = "failed, expects each line to be a colon separated string representing the user and passwd"
  462. raise errors.AnsibleFilterError(error_msg)
  463. htpasswd_entries[user] = passwd
  464. return htpasswd_entries
  465. def filters(self):
  466. ''' returns a mapping of filters to methods '''
  467. return {"translate_idps": self.translate_idps,
  468. "validate_pcs_cluster": self.validate_pcs_cluster,
  469. "certificates_to_synchronize": self.certificates_to_synchronize,
  470. "oo_htpasswd_users_from_file": self.oo_htpasswd_users_from_file}