diff options
Diffstat (limited to 'roles/openshift_master_facts')
4 files changed, 592 insertions, 6 deletions
diff --git a/roles/openshift_master_facts/filter_plugins/oo_filters.py b/roles/openshift_master_facts/filter_plugins/oo_filters.py new file mode 120000 index 000000000..6f9bc47c1 --- /dev/null +++ b/roles/openshift_master_facts/filter_plugins/oo_filters.py @@ -0,0 +1 @@ +../../../filter_plugins/oo_filters.py
\ No newline at end of file diff --git a/roles/openshift_master_facts/filter_plugins/openshift_master.py b/roles/openshift_master_facts/filter_plugins/openshift_master.py new file mode 100644 index 000000000..6d009077a --- /dev/null +++ b/roles/openshift_master_facts/filter_plugins/openshift_master.py @@ -0,0 +1,578 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vim: expandtab:tabstop=4:shiftwidth=4 +''' +Custom filters for use in openshift-master +''' +import copy +import sys + +from distutils.version import LooseVersion  # pylint: disable=no-name-in-module,import-error + +from ansible import errors +from ansible.parsing.yaml.dumper import AnsibleDumper +from ansible.plugins.filter.core import to_bool as ansible_bool +from six import string_types + +import yaml + + +class IdentityProviderBase(object): +    """ IdentityProviderBase + +        Attributes: +            name (str): Identity provider Name +            login (bool): Is this identity provider a login provider? +            challenge (bool): Is this identity provider a challenge provider? +            provider (dict): Provider specific config +            _idp (dict): internal copy of the IDP dict passed in +            _required (list): List of lists of strings for required attributes +            _optional (list): List of lists of strings for optional attributes +            _allow_additional (bool): Does this provider support attributes +                not in _required and _optional + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    # disabling this check since the number of instance attributes are +    # necessary for this class +    # pylint: disable=too-many-instance-attributes +    def __init__(self, api_version, idp): +        if api_version not in ['v1']: +            raise errors.AnsibleFilterError("|failed api version {0} unknown".format(api_version)) + +        self._idp = copy.deepcopy(idp) + +        if 'name' not in self._idp: +            raise errors.AnsibleFilterError("|failed identity provider missing a name") + +        if 'kind' not in self._idp: +            raise errors.AnsibleFilterError("|failed identity provider missing a kind") + +        self.name = self._idp.pop('name') +        self.login = ansible_bool(self._idp.pop('login', False)) +        self.challenge = ansible_bool(self._idp.pop('challenge', False)) +        self.provider = dict(apiVersion=api_version, kind=self._idp.pop('kind')) + +        mm_keys = ('mappingMethod', 'mapping_method') +        mapping_method = None +        for key in mm_keys: +            if key in self._idp: +                mapping_method = self._idp.pop(key) +        if mapping_method is None: +            mapping_method = self.get_default('mappingMethod') +        self.mapping_method = mapping_method + +        valid_mapping_methods = ['add', 'claim', 'generate', 'lookup'] +        if self.mapping_method not in valid_mapping_methods: +            raise errors.AnsibleFilterError("|failed unknown mapping method " +                                            "for provider {0}".format(self.__class__.__name__)) +        self._required = [] +        self._optional = [] +        self._allow_additional = True + +    @staticmethod +    def validate_idp_list(idp_list, openshift_version, deployment_type): +        ''' validates a list of idps ''' +        login_providers = [x.name for x in idp_list if x.login] + +        multiple_logins_unsupported = False +        if len(login_providers) > 1: +            if deployment_type in ['enterprise', 'online', 'atomic-enterprise', 'openshift-enterprise']: +                if LooseVersion(openshift_version) < LooseVersion('3.2'): +                    multiple_logins_unsupported = True +            if deployment_type in ['origin']: +                if LooseVersion(openshift_version) < LooseVersion('1.2'): +                    multiple_logins_unsupported = True +        if multiple_logins_unsupported: +            raise errors.AnsibleFilterError("|failed multiple providers are " +                                            "not allowed for login. login " +                                            "providers: {0}".format(', '.join(login_providers))) + +        names = [x.name for x in idp_list] +        if len(set(names)) != len(names): +            raise errors.AnsibleFilterError("|failed more than one provider configured with the same name") + +        for idp in idp_list: +            idp.validate() + +    def validate(self): +        ''' validate an instance of this idp class ''' +        pass + +    @staticmethod +    def get_default(key): +        ''' get a default value for a given key ''' +        if key == 'mappingMethod': +            return 'claim' +        else: +            return None + +    def set_provider_item(self, items, required=False): +        ''' set a provider item based on the list of item names provided. ''' +        for item in items: +            provider_key = items[0] +            if item in self._idp: +                self.provider[provider_key] = self._idp.pop(item) +                break +        else: +            default = self.get_default(provider_key) +            if default is not None: +                self.provider[provider_key] = default +            elif required: +                raise errors.AnsibleFilterError("|failed provider {0} missing " +                                                "required key {1}".format(self.__class__.__name__, provider_key)) + +    def set_provider_items(self): +        ''' set the provider items for this idp ''' +        for items in self._required: +            self.set_provider_item(items, True) +        for items in self._optional: +            self.set_provider_item(items) +        if self._allow_additional: +            for key in self._idp.keys(): +                self.set_provider_item([key]) +        else: +            if len(self._idp) > 0: +                raise errors.AnsibleFilterError("|failed provider {0} " +                                                "contains unknown keys " +                                                "{1}".format(self.__class__.__name__, ', '.join(self._idp.keys()))) + +    def to_dict(self): +        ''' translate this idp to a dictionary ''' +        return dict(name=self.name, challenge=self.challenge, +                    login=self.login, mappingMethod=self.mapping_method, +                    provider=self.provider) + + +class LDAPPasswordIdentityProvider(IdentityProviderBase): +    """ LDAPPasswordIdentityProvider + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        super(LDAPPasswordIdentityProvider, self).__init__(api_version, idp) +        self._allow_additional = False +        self._required += [['attributes'], ['url'], ['insecure']] +        self._optional += [['ca'], +                           ['bindDN', 'bind_dn'], +                           ['bindPassword', 'bind_password']] + +        self._idp['insecure'] = ansible_bool(self._idp.pop('insecure', False)) + +        if 'attributes' in self._idp and 'preferred_username' in self._idp['attributes']: +            pref_user = self._idp['attributes'].pop('preferred_username') +            self._idp['attributes']['preferredUsername'] = pref_user + +    def validate(self): +        ''' validate this idp instance ''' +        if not isinstance(self.provider['attributes'], dict): +            raise errors.AnsibleFilterError("|failed attributes for provider " +                                            "{0} must be a dictionary".format(self.__class__.__name__)) + +        attrs = ['id', 'email', 'name', 'preferredUsername'] +        for attr in attrs: +            if attr in self.provider['attributes'] and not isinstance(self.provider['attributes'][attr], list): +                raise errors.AnsibleFilterError("|failed {0} attribute for " +                                                "provider {1} must be a list".format(attr, self.__class__.__name__)) + +        unknown_attrs = set(self.provider['attributes'].keys()) - set(attrs) +        if len(unknown_attrs) > 0: +            raise errors.AnsibleFilterError("|failed provider {0} has unknown " +                                            "attributes: {1}".format(self.__class__.__name__, ', '.join(unknown_attrs))) + + +class KeystonePasswordIdentityProvider(IdentityProviderBase): +    """ KeystoneIdentityProvider + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        super(KeystonePasswordIdentityProvider, self).__init__(api_version, idp) +        self._allow_additional = False +        self._required += [['url'], ['domainName', 'domain_name']] +        self._optional += [['ca'], ['certFile', 'cert_file'], ['keyFile', 'key_file']] + + +class RequestHeaderIdentityProvider(IdentityProviderBase): +    """ RequestHeaderIdentityProvider + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        super(RequestHeaderIdentityProvider, self).__init__(api_version, idp) +        self._allow_additional = False +        self._required += [['headers']] +        self._optional += [['challengeURL', 'challenge_url'], +                           ['loginURL', 'login_url'], +                           ['clientCA', 'client_ca'], +                           ['clientCommonNames', 'client_common_names'], +                           ['emailHeaders', 'email_headers'], +                           ['nameHeaders', 'name_headers'], +                           ['preferredUsernameHeaders', 'preferred_username_headers']] + +    def validate(self): +        ''' validate this idp instance ''' +        if not isinstance(self.provider['headers'], list): +            raise errors.AnsibleFilterError("|failed headers for provider {0} " +                                            "must be a list".format(self.__class__.__name__)) + + +class AllowAllPasswordIdentityProvider(IdentityProviderBase): +    """ AllowAllPasswordIdentityProvider + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        super(AllowAllPasswordIdentityProvider, self).__init__(api_version, idp) +        self._allow_additional = False + + +class DenyAllPasswordIdentityProvider(IdentityProviderBase): +    """ DenyAllPasswordIdentityProvider + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        super(DenyAllPasswordIdentityProvider, self).__init__(api_version, idp) +        self._allow_additional = False + + +class HTPasswdPasswordIdentityProvider(IdentityProviderBase): +    """ HTPasswdPasswordIdentity + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        super(HTPasswdPasswordIdentityProvider, self).__init__(api_version, idp) +        self._allow_additional = False +        self._required += [['file', 'filename', 'fileName', 'file_name']] + +    @staticmethod +    def get_default(key): +        if key == 'file': +            return '/etc/origin/htpasswd' +        else: +            return IdentityProviderBase.get_default(key) + + +class BasicAuthPasswordIdentityProvider(IdentityProviderBase): +    """ BasicAuthPasswordIdentityProvider + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        super(BasicAuthPasswordIdentityProvider, self).__init__(api_version, idp) +        self._allow_additional = False +        self._required += [['url']] +        self._optional += [['ca'], ['certFile', 'cert_file'], ['keyFile', 'key_file']] + + +class IdentityProviderOauthBase(IdentityProviderBase): +    """ IdentityProviderOauthBase + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        super(IdentityProviderOauthBase, self).__init__(api_version, idp) +        self._allow_additional = False +        self._required += [['clientID', 'client_id'], ['clientSecret', 'client_secret']] + +    def validate(self): +        ''' validate this idp instance ''' +        if self.challenge: +            raise errors.AnsibleFilterError("|failed provider {0} does not " +                                            "allow challenge authentication".format(self.__class__.__name__)) + + +class OpenIDIdentityProvider(IdentityProviderOauthBase): +    """ OpenIDIdentityProvider + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        IdentityProviderOauthBase.__init__(self, api_version, idp) +        self._required += [['claims'], ['urls']] +        self._optional += [['ca'], +                           ['extraScopes'], +                           ['extraAuthorizeParameters']] +        if 'claims' in self._idp and 'preferred_username' in self._idp['claims']: +            pref_user = self._idp['claims'].pop('preferred_username') +            self._idp['claims']['preferredUsername'] = pref_user +        if 'urls' in self._idp and 'user_info' in self._idp['urls']: +            user_info = self._idp['urls'].pop('user_info') +            self._idp['urls']['userInfo'] = user_info +        if 'extra_scopes' in self._idp: +            self._idp['extraScopes'] = self._idp.pop('extra_scopes') +        if 'extra_authorize_parameters' in self._idp: +            self._idp['extraAuthorizeParameters'] = self._idp.pop('extra_authorize_parameters') + +        if 'extraAuthorizeParameters' in self._idp: +            if 'include_granted_scopes' in self._idp['extraAuthorizeParameters']: +                val = ansible_bool(self._idp['extraAuthorizeParameters'].pop('include_granted_scopes')) +                self._idp['extraAuthorizeParameters']['include_granted_scopes'] = val + +    def validate(self): +        ''' validate this idp instance ''' +        IdentityProviderOauthBase.validate(self) +        if not isinstance(self.provider['claims'], dict): +            raise errors.AnsibleFilterError("|failed claims for provider {0} " +                                            "must be a dictionary".format(self.__class__.__name__)) + +        for var, var_type in (('extraScopes', list), ('extraAuthorizeParameters', dict)): +            if var in self.provider and not isinstance(self.provider[var], var_type): +                raise errors.AnsibleFilterError("|failed {1} for provider " +                                                "{0} must be a {2}".format(self.__class__.__name__, +                                                                           var, +                                                                           var_type.__class__.__name__)) + +        required_claims = ['id'] +        optional_claims = ['email', 'name', 'preferredUsername'] +        all_claims = required_claims + optional_claims + +        for claim in required_claims: +            if claim in required_claims and claim not in self.provider['claims']: +                raise errors.AnsibleFilterError("|failed {0} claim missing " +                                                "for provider {1}".format(claim, self.__class__.__name__)) + +        for claim in all_claims: +            if claim in self.provider['claims'] and not isinstance(self.provider['claims'][claim], list): +                raise errors.AnsibleFilterError("|failed {0} claims for " +                                                "provider {1} must be a list".format(claim, self.__class__.__name__)) + +        unknown_claims = set(self.provider['claims'].keys()) - set(all_claims) +        if len(unknown_claims) > 0: +            raise errors.AnsibleFilterError("|failed provider {0} has unknown " +                                            "claims: {1}".format(self.__class__.__name__, ', '.join(unknown_claims))) + +        if not isinstance(self.provider['urls'], dict): +            raise errors.AnsibleFilterError("|failed urls for provider {0} " +                                            "must be a dictionary".format(self.__class__.__name__)) + +        required_urls = ['authorize', 'token'] +        optional_urls = ['userInfo'] +        all_urls = required_urls + optional_urls + +        for url in required_urls: +            if url not in self.provider['urls']: +                raise errors.AnsibleFilterError("|failed {0} url missing for " +                                                "provider {1}".format(url, self.__class__.__name__)) + +        unknown_urls = set(self.provider['urls'].keys()) - set(all_urls) +        if len(unknown_urls) > 0: +            raise errors.AnsibleFilterError("|failed provider {0} has unknown " +                                            "urls: {1}".format(self.__class__.__name__, ', '.join(unknown_urls))) + + +class GoogleIdentityProvider(IdentityProviderOauthBase): +    """ GoogleIdentityProvider + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        IdentityProviderOauthBase.__init__(self, api_version, idp) +        self._optional += [['hostedDomain', 'hosted_domain']] + + +class GitHubIdentityProvider(IdentityProviderOauthBase): +    """ GitHubIdentityProvider + +        Attributes: + +        Args: +            api_version(str): OpenShift config version +            idp (dict): idp config dict + +        Raises: +            AnsibleFilterError: +    """ +    def __init__(self, api_version, idp): +        IdentityProviderOauthBase.__init__(self, api_version, idp) +        self._optional += [['organizations']] + + +class FilterModule(object): +    ''' Custom ansible filters for use by the openshift_master role''' + +    @staticmethod +    def translate_idps(idps, api_version, openshift_version, deployment_type): +        ''' Translates a list of dictionaries into a valid identityProviders config ''' +        idp_list = [] + +        if not isinstance(idps, list): +            raise errors.AnsibleFilterError("|failed expects to filter on a list of identity providers") +        for idp in idps: +            if not isinstance(idp, dict): +                raise errors.AnsibleFilterError("|failed identity providers must be a list of dictionaries") + +            cur_module = sys.modules[__name__] +            idp_class = getattr(cur_module, idp['kind'], None) +            idp_inst = idp_class(api_version, idp) if idp_class is not None else IdentityProviderBase(api_version, idp) +            idp_inst.set_provider_items() +            idp_list.append(idp_inst) + +        IdentityProviderBase.validate_idp_list(idp_list, openshift_version, deployment_type) +        return yaml.dump([idp.to_dict() for idp in idp_list], +                         allow_unicode=True, +                         default_flow_style=False, +                         Dumper=AnsibleDumper) + +    @staticmethod +    def validate_pcs_cluster(data, masters=None): +        ''' Validates output from "pcs status", ensuring that each master +            provided is online. +            Ex: data = ('...', +                        'PCSD Status:', +                        'master1.example.com: Online', +                        'master2.example.com: Online', +                        'master3.example.com: Online', +                        '...') +                masters = ['master1.example.com', +                           'master2.example.com', +                           'master3.example.com'] +               returns True +        ''' +        if not issubclass(type(data), string_types): +            raise errors.AnsibleFilterError("|failed expects data is a string or unicode") +        if not issubclass(type(masters), list): +            raise errors.AnsibleFilterError("|failed expects masters is a list") +        valid = True +        for master in masters: +            if "{0}: Online".format(master) not in data: +                valid = False +        return valid + +    @staticmethod +    def certificates_to_synchronize(hostvars, include_keys=True, include_ca=True): +        ''' Return certificates to synchronize based on facts. ''' +        if not issubclass(type(hostvars), dict): +            raise errors.AnsibleFilterError("|failed expects hostvars is a dict") +        certs = ['admin.crt', +                 'admin.key', +                 'admin.kubeconfig', +                 'master.kubelet-client.crt', +                 'master.kubelet-client.key'] +        if bool(include_ca): +            certs += ['ca.crt', 'ca.key'] +        if bool(include_keys): +            certs += ['serviceaccounts.private.key', +                      'serviceaccounts.public.key'] +        if bool(hostvars['openshift']['common']['version_gte_3_1_or_1_1']): +            certs += ['master.proxy-client.crt', +                      'master.proxy-client.key'] +        if not bool(hostvars['openshift']['common']['version_gte_3_2_or_1_2']): +            certs += ['openshift-master.crt', +                      'openshift-master.key', +                      'openshift-master.kubeconfig'] +        if bool(hostvars['openshift']['common']['version_gte_3_3_or_1_3']): +            certs += ['service-signer.crt', +                      'service-signer.key'] +        if not bool(hostvars['openshift']['common']['version_gte_3_5_or_1_5']): +            certs += ['openshift-registry.crt', +                      'openshift-registry.key', +                      'openshift-registry.kubeconfig', +                      'openshift-router.crt', +                      'openshift-router.key', +                      'openshift-router.kubeconfig'] +        return certs + +    @staticmethod +    def oo_htpasswd_users_from_file(file_contents): +        ''' return a dictionary of htpasswd users from htpasswd file contents ''' +        htpasswd_entries = {} +        if not isinstance(file_contents, string_types): +            raise errors.AnsibleFilterError("failed, expects to filter on a string") +        for line in file_contents.splitlines(): +            user = None +            passwd = None +            if len(line) == 0: +                continue +            if ':' in line: +                user, passwd = line.split(':', 1) + +            if user is None or len(user) == 0 or passwd is None or len(passwd) == 0: +                error_msg = "failed, expects each line to be a colon separated string representing the user and passwd" +                raise errors.AnsibleFilterError(error_msg) +            htpasswd_entries[user] = passwd +        return htpasswd_entries + +    def filters(self): +        ''' returns a mapping of filters to methods ''' +        return {"translate_idps": self.translate_idps, +                "validate_pcs_cluster": self.validate_pcs_cluster, +                "certificates_to_synchronize": self.certificates_to_synchronize, +                "oo_htpasswd_users_from_file": self.oo_htpasswd_users_from_file} diff --git a/roles/openshift_master_facts/lookup_plugins/oo_option.py b/roles/openshift_master_facts/lookup_plugins/oo_option.py new file mode 120000 index 000000000..5ae43f8dd --- /dev/null +++ b/roles/openshift_master_facts/lookup_plugins/oo_option.py @@ -0,0 +1 @@ +../../../lookup_plugins/oo_option.py
\ No newline at end of file diff --git a/roles/openshift_master_facts/tasks/main.yml b/roles/openshift_master_facts/tasks/main.yml index 74885d713..6f8f09b22 100644 --- a/roles/openshift_master_facts/tasks/main.yml +++ b/roles/openshift_master_facts/tasks/main.yml @@ -6,7 +6,8 @@      openshift_master_default_subdomain: "{{ osm_default_subdomain | default(None) }}"    when: openshift_master_default_subdomain is not defined -- fail: +- name: Verify required variables are set +  fail:      msg: openshift_master_default_subdomain must be set to deploy metrics    when: openshift_hosted_metrics_deploy | default(false) | bool and openshift_master_default_subdomain | default("") == "" @@ -17,7 +18,8 @@  # path must stay consistent. As such if openshift_hosted_metrics_public_url is set in  # inventory, we extract the hostname, and then reset openshift_hosted_metrics_public_url  # to the format that we know is valid. (This may change in future) -- set_fact: +- name: Set g_metrics_hostname +  set_fact:      g_metrics_hostname: "{{ openshift_hosted_metrics_public_url                          | default('hawkular-metrics.' ~ (openshift_master_default_subdomain))                          | oo_hostname_from_url }}" @@ -108,7 +110,8 @@      path: "{{ openshift_master_scheduler_conf }}"    register: scheduler_config_stat -- set_fact: +- name: Set Default scheduler predicates and priorities +  set_fact:      openshift_master_scheduler_default_predicates: "{{ lookup('openshift_master_facts_default_predicates') }}"      openshift_master_scheduler_default_priorities: "{{ lookup('openshift_master_facts_default_priorities') }}" @@ -118,14 +121,17 @@        src: "{{ openshift_master_scheduler_conf }}"      register: current_scheduler_config -  - set_fact: +  - name: Set openshift_master_scheduler_current_config +    set_fact:        openshift_master_scheduler_current_config: "{{ current_scheduler_config.content | b64decode | from_json }}" -  - fail: +  - name: Test if scheduler config is readable +    fail:        msg: "Unknown scheduler config apiVersion {{ openshift_master_scheduler_config.apiVersion }}"      when: "{{ openshift_master_scheduler_current_config.apiVersion | default(None) != 'v1' }}" -  - set_fact: +  - name: Set current scheduler predicates and priorities +    set_fact:        openshift_master_scheduler_current_predicates: "{{ openshift_master_scheduler_current_config.predicates }}"        openshift_master_scheduler_current_priorities: "{{ openshift_master_scheduler_current_config.priorities }}"    when: "{{ scheduler_config_stat.stat.exists }}"  | 
