openshift_master.py 21 KB

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