openshift_master.py 22 KB

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