openshift_master.py 23 KB

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