diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index 6576470..b5d3b92 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -13,34 +13,47 @@ register: result failed_when: result is success - - name: "Disable one of the system repos" + - name: "Test disable system repo" become: true tripleo.repos.yum_config: type: repo - name: rt - file_path: /etc/yum.repos.d/CentOS-Stream-RealTime.repo + name: "{{ 'rt' if (ansible_distribution_major_version is version(8, '>=')) else 'cr' }}" enabled: false tags: # TODO: fix yum_config to correctly report changed state and uncomment - # the line below which disables molecule idemptotence test. + # the line below which disables molecule idempotence test. - molecule-idempotence-notest - # TODO: code needs a fix to be compatible with CentOS-7 - when: ansible_distribution_major_version is version(8, '>=') - - name: "Update yum_config global config" + - name: "Test create new repo file" + become: true + tripleo.repos.yum_config: + type: repo + name: "fakerepo" + # Keep it disabled to not affect any other test + enabled: false + file_path: "/etc/yum.repos.d/fake_repo.repo" + set_options: + baseurl: "http://fakemirror/fakerepo" + priority: "10" + gpgcheck: "0" + exclude: "fakepkg*" + tags: + # TODO: fix yum_config to correctly report changed state and uncomment + # the line below which disables molecule idempotence test. + - molecule-idempotence-notest + + - name: "Test yum-config global config" become: true tripleo.repos.yum_config: type: global - file_path: /etc/dnf/dnf.conf + file_path: "{{ '/etc/dnf/dnf.conf' if (ansible_distribution_major_version is version(8, '>=')) else '/etc/yum.conf' }}" set_options: skip_if_unavailable: "False" - keepcache: "0" + fake_conf: "True" tags: # TODO: fix yum_config to correctly report changed state and uncomment - # the line below which disables molecule idemptotence test. + # the line below which disables molecule idempotence test. - molecule-idempotence-notest - # TODO: code needs a fix to be compatible with CentOS-7 - when: ansible_distribution_major_version is version(8, '>=') - name: "Test yum_config enable-compose-repos" become: true diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml index 95b222d..29a4057 100644 --- a/molecule/default/verify.yml +++ b/molecule/default/verify.yml @@ -2,32 +2,33 @@ - name: Verify hosts: all tasks: - - name: Validate if RealTime repo is disabled + - name: Check if RT or CR repos are disabled + vars: + section_name: "{{ 'rt' if (ansible_distribution_major_version is version(8, '>=')) else 'cr' }}" + repo_path: /etc/yum.repos.d/CentOS-{{ 'Stream-RealTime' if (ansible_distribution_major_version is version(8, '>=')) else 'CR' }}.repo include_tasks: assert_ini_key_value.yml with_items: - - name: RealTime - path: /etc/yum.repos.d/CentOS-Stream-RealTime.repo - section: rt + - name: "{{ section_name|upper }}" + path: "{{ repo_path }}" + section: "{{ section_name }}" key: enabled value: "0" - # TODO: code needs a fix to be compatible with CentOS-7 - when: ansible_distribution_major_version is version(8, '>=') - - name: Validate yum/dnf conf file + - name: Check if yum/dnf conf file was updated + vars: + conf_file_path: "{{ '/etc/dnf/dnf.conf' if (ansible_distribution_major_version is version(8, '>=')) else '/etc/yum.conf' }}" include_tasks: assert_ini_key_value.yml with_items: - - name: dnf.conf - path: /etc/dnf/dnf.conf + - name: global_conf + path: "{{ conf_file_path }}" section: main key: skip_if_unavailable value: "False" - - name: dnf.conf - path: /etc/dnf/dnf.conf + - name: global_conf + path: "{{ conf_file_path }}" section: main - key: keepcache - value: "0" - # TODO: code needs a fix to be compatible with CentOS-7 - when: ansible_distribution_major_version is version(8, '>=') + key: fake_conf + value: "True" - name: Validate compose repos outputs include_tasks: verify_compose_repos.yml diff --git a/plugins/module_utils/tripleo_repos/yum_config/__main__.py b/plugins/module_utils/tripleo_repos/yum_config/__main__.py index a3c66f3..01b1131 100644 --- a/plugins/module_utils/tripleo_repos/yum_config/__main__.py +++ b/plugins/module_utils/tripleo_repos/yum_config/__main__.py @@ -17,10 +17,9 @@ import argparse import logging import sys -import tripleo_repos.yum_config.compose_repos as compose_repos import tripleo_repos.yum_config.constants as const -import tripleo_repos.yum_config.dnf_manager as dnf_mgr import tripleo_repos.yum_config.yum_config as cfg +import tripleo_repos.yum_config.utils as utils def options_to_dict(options): @@ -39,6 +38,12 @@ def options_to_dict(options): def main(): cfg.TripleOYumConfig.load_logging() + # Get release model and version + distro, major_version, __ = utils.get_distro_info() + py_version = sys.version_info.major + if py_version < 3: + logging.warning("Some operations will be disabled when running with " + "python 2.") # Repo arguments repo_args_parser = argparse.ArgumentParser(add_help=False) @@ -47,6 +52,15 @@ def main(): help='name of the repo to be modified' ) + environment_parse = argparse.ArgumentParser(add_help=False) + environment_parse.add_argument( + '--environment-file', + dest='env_file', + default=None, + help=('path to an environment file to be read before creating repo ' + 'files'), + ) + parser_enable_group = repo_args_parser.add_mutually_exclusive_group() parser_enable_group.add_argument( '--enable', @@ -171,24 +185,32 @@ def main(): # Subcommands subparsers.add_parser( 'repo', - parents=[common_parse, repo_args_parser, options_parse], + parents=[common_parse, environment_parse, repo_args_parser, + options_parse], help='updates a yum repository options' ) - subparsers.add_parser( - 'module', - parents=[dnf_module_parser], - help='updates yum module options' - ) subparsers.add_parser( 'global', - parents=[common_parse, options_parse], + parents=[common_parse, environment_parse, options_parse], help='updates global yum configuration options' ) - subparsers.add_parser( - 'enable-compose-repos', - parents=[compose_args_parser], - help='enable CentOS compose repos based on an compose url.' - ) + + if py_version >= 3: + subparsers.add_parser( + 'enable-compose-repos', + parents=[compose_args_parser, environment_parse], + help='enable CentOS compose repos based on an compose url.' + ) + + for min_distro_ver in const.DNF_MODULE_MINIMAL_DISTRO_VERSIONS: + if (distro == min_distro_ver.get('distro') and int( + major_version) >= min_distro_ver.get('min_version')): + subparsers.add_parser( + 'module', + parents=[dnf_module_parser], + help='updates yum module options' + ) + break args = main_parser.parse_args() if args.command is None: @@ -202,13 +224,14 @@ def main(): if args.command == 'repo': set_dict = options_to_dict(args.set_opts) config_obj = cfg.TripleOYumRepoConfig( - dir_path=args.config_dir_path) - - config_obj.update_section(args.name, set_dict, - file_path=args.config_file_path, - enabled=args.enable) + dir_path=args.config_dir_path, + environment_file=args.env_file) + config_obj.add_or_update_section(args.name, set_dict=set_dict, + file_path=args.config_file_path, + enabled=args.enable) elif args.command == 'module': + import tripleo_repos.yum_config.dnf_manager as dnf_mgr dnf_mod_mgr = dnf_mgr.DnfModuleManager() dnf_method = getattr(dnf_mod_mgr, args.operation + "_module") dnf_method(args.name, stream=args.stream, profile=args.profile) @@ -216,16 +239,19 @@ def main(): elif args.command == 'global': set_dict = options_to_dict(args.set_opts) config_obj = cfg.TripleOYumGlobalConfig( - file_path=args.config_file_path) + file_path=args.config_file_path, + environment_file=args.env_file) config_obj.update_section('main', set_dict) elif args.command == 'enable-compose-repos': + import tripleo_repos.yum_config.compose_repos as compose_repos repo_obj = compose_repos.TripleOYumComposeRepoConfig( args.compose_url, args.release, dir_path=args.config_dir_path, - arch=args.arch) + arch=args.arch, + environment_file=args.env_file) repo_obj.enable_compose_repos(variants=args.variants, override_repos=args.disable_conflicting) diff --git a/plugins/module_utils/tripleo_repos/yum_config/compose_repos.py b/plugins/module_utils/tripleo_repos/yum_config/compose_repos.py index c9b1359..6fc273e 100644 --- a/plugins/module_utils/tripleo_repos/yum_config/compose_repos.py +++ b/plugins/module_utils/tripleo_repos/yum_config/compose_repos.py @@ -18,8 +18,6 @@ import logging import json import os import re -import urllib.parse -import urllib.request from .constants import ( YUM_REPO_DIR, @@ -44,7 +42,8 @@ __metaclass__ = type class TripleOYumComposeRepoConfig(TripleOYumConfig): """Manages yum repo configuration files for CentOS Compose.""" - def __init__(self, compose_url, release, dir_path=None, arch=None): + def __init__(self, compose_url, release, dir_path=None, arch=None, + environment_file=None): conf_dir_path = dir_path or YUM_REPO_DIR self.arch = arch or 'x86_64' @@ -80,11 +79,13 @@ class TripleOYumComposeRepoConfig(TripleOYumConfig): super(TripleOYumComposeRepoConfig, self).__init__( valid_options=YUM_REPO_SUPPORTED_OPTIONS, dir_path=conf_dir_path, - file_extension=YUM_REPO_FILE_EXTENSION) + file_extension=YUM_REPO_FILE_EXTENSION, + environment_file=environment_file) def _get_compose_info(self): """Retrieve compose info for a provided compose-id url.""" # NOTE(dviroel): works for both centos 8 and 9 + import urllib.request try: logging.debug("Retrieving compose info from url: %s", self.compose_info_url) @@ -179,7 +180,7 @@ class TripleOYumComposeRepoConfig(TripleOYumConfig): def add_section(self, section, add_dict, file_path): # Create a new file if it does not exists if not os.path.isfile(file_path): - with open(file_path, '+w'): + with open(file_path, 'w+'): pass super(TripleOYumComposeRepoConfig, self).add_section( section, add_dict, file_path) diff --git a/plugins/module_utils/tripleo_repos/yum_config/constants.py b/plugins/module_utils/tripleo_repos/yum_config/constants.py index 0e322b0..eaf93b0 100644 --- a/plugins/module_utils/tripleo_repos/yum_config/constants.py +++ b/plugins/module_utils/tripleo_repos/yum_config/constants.py @@ -19,14 +19,21 @@ List of options that can be updated for yum repo files. """ __metaclass__ = type + YUM_REPO_SUPPORTED_OPTIONS = [ - 'name', 'baseurl', + 'cost', 'enabled', + 'exclude', + 'excludepkgs', 'gpgcheck', 'gpgkey', - 'priority', - 'exclude', + 'includepkgs', + 'metalink', + 'mirrorlist', + 'module_hotfixes', + 'name', + 'priority' ] """ @@ -68,3 +75,12 @@ COMPOSE_REPOS_INFO_PATH = { "centos-stream-8": "metadata/composeinfo.json", "centos-stream-9": "metadata/composeinfo.json", } + +""" +DNF Manager constants +""" +DNF_MODULE_MINIMAL_DISTRO_VERSIONS = [ + {'distro': 'centos', 'min_version': 8}, + {'distro': 'rhel', 'min_version': 8}, + {'distro': 'fedora', 'min_version': 22}, +] diff --git a/plugins/module_utils/tripleo_repos/yum_config/utils.py b/plugins/module_utils/tripleo_repos/yum_config/utils.py new file mode 100644 index 0000000..c4a3646 --- /dev/null +++ b/plugins/module_utils/tripleo_repos/yum_config/utils.py @@ -0,0 +1,50 @@ +# Copyright 2021 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import (absolute_import, division, print_function) +import os +import platform +import subprocess + +__metaclass__ = type + + +# TODO(dviroel): Merge in a utils file when refactoring tripleo-repos. +def get_distro_info(): + """Get distro info from os-release file. + + :return: distro_id, distro_major_version_id and distro_name + """ + if not os.path.exists('/etc/os-release'): + return platform.system(), 'unknown', 'unknown' + + output = subprocess.Popen( + 'source /etc/os-release && echo -e -n "$ID\n$VERSION_ID\n$NAME"', + shell=True, + stdout=subprocess.PIPE, + stderr=open(os.devnull, 'w'), + executable='/bin/bash', + universal_newlines=True).communicate() + + # distro_id and distro_version_id will always be at least an empty string + distro_id, distro_version_id, distro_name = output[0].split('\n') + + # if distro_version_id is empty string the major version will be empty + # string too + distro_major_version_id = distro_version_id.split('.')[0] + + # check if that is UBI subcase? + if os.path.exists('/etc/yum.repos.d/ubi.repo'): + distro_id = 'ubi' + + return distro_id, distro_major_version_id, distro_name diff --git a/plugins/module_utils/tripleo_repos/yum_config/yum_config.py b/plugins/module_utils/tripleo_repos/yum_config/yum_config.py index 1c338ff..675d0c4 100644 --- a/plugins/module_utils/tripleo_repos/yum_config/yum_config.py +++ b/plugins/module_utils/tripleo_repos/yum_config/yum_config.py @@ -15,9 +15,9 @@ from __future__ import (absolute_import, division, print_function) -import configparser import logging import os +import subprocess import sys from .constants import ( @@ -33,6 +33,54 @@ from .exceptions import ( TripleOYumConfigNotFound, ) +py_version = sys.version_info.major +if py_version < 3: + import ConfigParser as cfg_parser + + def save_section_to_file(file_path, config, section, updates): + """Updates a specific 'section' in a 'config' and write to disk. + + :param file_path: Absolute path to the file to be updated. + :param config: configparser object created from the file. + :param section: section name to be updated. + :param updates: dict with options to update in section. + """ + + for k, v in updates.items(): + config.set(section, k, v) + with open(file_path, 'w') as f: + config.write(f) + + # NOTE(dviroel) Need to manually remove whitespaces around "=", to + # avoid legacy scripts failing on parsing ini files. + with open(file_path, 'r+') as f: + lines = f.readlines() + # erase content before writing again + f.truncate(0) + f.seek(0) + for line in lines: + line = line.strip() + if "=" in line: + option_kv = line.split("=", 1) + option_kv = list(map(str.strip, option_kv)) + f.write("%s%s%s\n" % (option_kv[0], "=", option_kv[1])) + else: + f.write(line + "\n") + +else: + import configparser as cfg_parser + + def save_section_to_file(file_path, config, section, updates): + """Updates a specific 'section' in a 'config' and write to disk. + + :param file_path: Absolute path to the file to be updated. + :param config: configparser object created from the file. + :param section: section name to be updated. + :param updates: dict with options to update in section. + """ + config[section].update(updates) + with open(file_path, 'w') as f: + config.write(f, space_around_delimiters=False) __metaclass__ = type @@ -43,6 +91,21 @@ def validated_file_path(file_path): return False +def source_env_file(source_file, update=True): + """Source a file and get all environment variables in a dict format.""" + p_open = subprocess.Popen(". %s; env" % source_file, + stdout=subprocess.PIPE, + shell=True) + data = p_open.communicate()[0].decode('ascii') + + env_dict = dict( + line.split("=", 1) for line in data.splitlines() + if len(line.split("=", 1)) > 1) + if update: + os.environ.update(env_dict) + return env_dict + + class TripleOYumConfig: """ This class is a base class for updating yum configuration files in @@ -79,7 +142,8 @@ class TripleOYumConfig: logger.addHandler(handler) logger.setLevel(logging.INFO) - def __init__(self, valid_options=None, dir_path=None, file_extension=None): + def __init__(self, valid_options=None, dir_path=None, file_extension=None, + environment_file=None): """ Creates a TripleOYumConfig object that holds configuration file information. @@ -90,10 +154,13 @@ class TripleOYumConfig: for configuration files to be updated. :param: file_extension: File extension to filter configuration files in the search directory. + :param environment_file: File to be read before updating environment + variables. """ self.dir_path = dir_path self.file_extension = file_extension self.valid_options = valid_options + self.env_file = environment_file # Sanity checks if dir_path: @@ -102,6 +169,9 @@ class TripleOYumConfig: 'provided path.').format(dir_path) raise TripleOYumConfigNotFound(error_msg=msg) + if self.env_file: + source_env_file(os.path.expanduser(self.env_file), update=True) + def _read_config_file(self, file_path, section=None): """Reads a configuration file. @@ -109,7 +179,7 @@ class TripleOYumConfig: to fail earlier if the section is not found. :return: a config parser object and the full file path. """ - config = configparser.ConfigParser() + config = cfg_parser.ConfigParser() file_paths = [file_path] if self.dir_path: # if dir_path is configured, we can search for filename there @@ -127,7 +197,7 @@ class TripleOYumConfig: try: config.read(valid_file_path) - except configparser.Error: + except cfg_parser.Error: msg = 'Unable to parse configuration file {0}.'.format( valid_file_path) raise TripleOYumConfigFileParseError(error_msg=msg) @@ -161,10 +231,10 @@ class TripleOYumConfig: if not os.access(os.path.join(self.dir_path, file), os.W_OK): continue - tmp_config = configparser.ConfigParser() + tmp_config = cfg_parser.ConfigParser() try: tmp_config.read(os.path.join(self.dir_path, file)) - except configparser.Error: + except cfg_parser.Error: continue if section in tmp_config.sections(): config_files_path.append(os.path.join(self.dir_path, file)) @@ -195,12 +265,12 @@ class TripleOYumConfig: 'section {0}'.format(section)) raise TripleOYumConfigNotFound(error_msg=msg) + for k, v in set_dict.items(): + set_dict[k] = os.path.expandvars(v) for file in files: config, file = self._read_config_file(file, section=section) # Update configuration file with dict updates - config[section].update(set_dict) - with open(file, 'w') as f: - config.write(f) + save_section_to_file(file, config, section, set_dict) logging.info("Section '%s' was successfully " "updated.", section) @@ -213,6 +283,11 @@ class TripleOYumConfig: new section. :param file_path: Path to the configuration file to be updated. """ + if self.valid_options: + if not all(key in self.valid_options for key in add_dict.keys()): + msg = 'One or more provided options are not valid.' + raise TripleOYumConfigInvalidOption(error_msg=msg) + # This section shouldn't exist in the provided file config, file_path = self._read_config_file(file_path=file_path) if section in config.sections(): @@ -220,13 +295,12 @@ class TripleOYumConfig: "file.", section) raise TripleOYumConfigInvalidSection(error_msg=msg) + for k, v in add_dict.items(): + add_dict[k] = os.path.expandvars(v) # Add new section config.add_section(section) # Update configuration file with dict updates - config[section].update(add_dict) - - with open(file_path, '+w') as file: - config.write(file) + save_section_to_file(file_path, config, section, add_dict) logging.info("Section '%s' was successfully " "added.", section) @@ -245,10 +319,7 @@ class TripleOYumConfig: config, file_path = self._read_config_file(file_path) for section in config.sections(): - config[section].update(set_dict) - - with open(file_path, '+w') as file: - config.write(file) + save_section_to_file(file_path, config, section, set_dict) logging.info("All sections for '%s' were successfully " "updated.", file_path) @@ -257,13 +328,14 @@ class TripleOYumConfig: class TripleOYumRepoConfig(TripleOYumConfig): """Manages yum repo configuration files.""" - def __init__(self, dir_path=None): + def __init__(self, dir_path=None, environment_file=None): conf_dir_path = dir_path or YUM_REPO_DIR super(TripleOYumRepoConfig, self).__init__( valid_options=YUM_REPO_SUPPORTED_OPTIONS, dir_path=conf_dir_path, - file_extension=YUM_REPO_FILE_EXTENSION) + file_extension=YUM_REPO_FILE_EXTENSION, + environment_file=environment_file) def update_section( self, section, set_dict=None, file_path=None, enabled=None): @@ -274,11 +346,36 @@ class TripleOYumRepoConfig(TripleOYumConfig): super(TripleOYumRepoConfig, self).update_section( section, update_dict, file_path=file_path) + def add_section(self, section, add_dict, file_path, enabled=None): + update_dict = add_dict or {} + if enabled is not None: + update_dict['enabled'] = '1' if enabled else '0' + super(TripleOYumRepoConfig, self).add_section( + section, update_dict, file_path) + + def add_or_update_section(self, section, set_dict=None, file_path=None, + enabled=None, create_if_not_exists=True): + try: + self.update_section( + section, set_dict=set_dict, file_path=file_path, + enabled=enabled) + except TripleOYumConfigNotFound: + if not create_if_not_exists or file_path is None: + # there is nothing to do, we can't create a new config file + raise + # Create a new file if it does not exists + with open(file_path, 'w+'): + pass + # When creating a new repo file, make sure that it has a name + if 'name' not in set_dict.keys(): + set_dict['name'] = section + self.add_section(section, set_dict, file_path, enabled=enabled) + class TripleOYumGlobalConfig(TripleOYumConfig): """Manages yum global configuration file.""" - def __init__(self, file_path=None): + def __init__(self, file_path=None, environment_file=None): self.conf_file_path = file_path or YUM_GLOBAL_CONFIG_FILE_PATH logging.info("Using '%s' as yum global configuration " "file.", self.conf_file_path) @@ -290,13 +387,14 @@ class TripleOYumGlobalConfig(TripleOYumConfig): # create it. If the user specify another conf file that doesn't # exists, the operation will fail. if not os.path.isfile(self.conf_file_path): - config = configparser.ConfigParser() + config = cfg_parser.ConfigParser() config.read(self.conf_file_path) config.add_section('main') - with open(self.conf_file_path, '+w') as file: + with open(self.conf_file_path, 'w+') as file: config.write(file) - super(TripleOYumGlobalConfig, self).__init__() + super(TripleOYumGlobalConfig, self).__init__( + environment_file=environment_file) def update_section(self, section, set_dict, file_path=None): super(TripleOYumGlobalConfig, self).update_section( diff --git a/plugins/modules/yum_config.py b/plugins/modules/yum_config.py index 993f493..babdcdc 100644 --- a/plugins/modules/yum_config.py +++ b/plugins/modules/yum_config.py @@ -66,6 +66,11 @@ options: file to be changed. type: path default: /etc/yum.repos.d + environment_file: + description: + - Absolute path to an environment file to be read before updating or + creating yum config and repo files. + type: path compose_url: description: - URL that contains CentOS compose repositories. @@ -151,7 +156,7 @@ EXAMPLES = r''' - name: Configure a set of repos based on latest CentOS Stream 8 compose become: true become_user: root - tripleo_yup_config: + tripleo_yum_config: compose_url: https://composes.centos.org/latest-CentOS-Stream-8/compose/ centos_release: centos-stream-8 variants: @@ -165,6 +170,7 @@ EXAMPLES = r''' RETURN = r''' # ''' +from ansible.module_utils import six # noqa: E402 from ansible.module_utils.basic import AnsibleModule # noqa: E402 @@ -172,10 +178,13 @@ def run_module(): try: import ansible_collections.tripleo.repos.plugins.module_utils. \ tripleo_repos.yum_config.constants as const + import ansible_collections.tripleo.repos.plugins.module_utils. \ + tripleo_repos.yum_config.utils as utils except ImportError: import tripleo_repos.yum_config.constants as const - # define available arguments/parameters a user can pass to the module - supported_config_types = ['repo', 'module', 'global', + import tripleo_repos.yum_config.utils as utils + + supported_config_types = ['repo', 'global', 'module', 'enable-compose-repos'] supported_module_operations = ['install', 'remove', 'reset'] module_args = dict( @@ -188,6 +197,7 @@ def run_module(): set_options=dict(type='dict', default={}), file_path=dict(type='path'), dir_path=dict(type='path', default=const.YUM_REPO_DIR), + environment_file=dict(type='path'), compose_url=dict(type='str'), centos_release=dict(type='str', choices=const.COMPOSE_REPOS_RELEASES), @@ -199,17 +209,37 @@ def run_module(): disable_repos=dict(type='list', default=[], elements='str'), ) + required_if_params = [ + ["type", "repo", ["name"]], + ["type", "module", ["name"]], + ["type", "enable-compose-repos", ["compose_url"]] + ] module = AnsibleModule( argument_spec=module_args, - required_if=[ - ["type", "repo", ["name"]], - ["type", "module", ["name"]], - ["type", "enable-compose-repos", ["compose_url"]], - ], + required_if=required_if_params, supports_check_mode=False ) + operations_not_supp_in_py2 = ['module', 'enable-compose-repos'] + if six.PY2 and module.params['type'] in operations_not_supp_in_py2: + msg = ("The configuration type '{0}' is not " + "supported with python 2.").format(module.params['type']) + module.fail_json(msg=msg) + + distro, major_version, __ = utils.get_distro_info() + dnf_module_support = False + for min_distro_ver in const.DNF_MODULE_MINIMAL_DISTRO_VERSIONS: + if (distro == min_distro_ver.get('distro') and int( + major_version) >= min_distro_ver.get('min_version')): + dnf_module_support = True + break + if module.params['type'] == 'module' and not dnf_module_support: + msg = ("The configuration type 'module' is not " + "supported in this distro version " + "({0}-{1}).".format(distro, major_version)) + module.fail_json(msg=msg) + # 'set_options' expects a dict that can also contains a list of values. # List of elements will be converted to a comma-separated list m_set_opts = module.params.get('set_options') @@ -223,33 +253,41 @@ def run_module(): # Module execution try: try: - import ansible_collections.tripleo.repos.plugins.module_utils.\ - tripleo_repos.yum_config.dnf_manager as dnf_mgr import ansible_collections.tripleo.repos.plugins.module_utils.\ tripleo_repos.yum_config.yum_config as cfg - import ansible_collections.tripleo.repos.plugins.module_utils. \ - tripleo_repos.yum_config.compose_repos as repos except ImportError: - import tripleo_repos.yum_config.dnf_manager as dnf_mgr import tripleo_repos.yum_config.yum_config as cfg - import tripleo_repos.yum_config.compose_repos as repos if module.params['type'] == 'repo': config_obj = cfg.TripleOYumRepoConfig( - dir_path=module.params['dir_path']) - config_obj.update_section( + dir_path=module.params['dir_path'], + environment_file=module.params['environment_file']) + config_obj.add_or_update_section( module.params['name'], - m_set_opts, + set_dict=m_set_opts, file_path=module.params['file_path'], enabled=module.params['enabled']) + elif module.params['type'] == 'global': + config_obj = cfg.TripleOYumGlobalConfig( + file_path=module.params['file_path'], + environment_file=module.params['environment_file']) + config_obj.update_section('main', m_set_opts) + elif module.params['type'] == 'enable-compose-repos': + try: + import ansible_collections.tripleo.repos.plugins.module_utils.\ + tripleo_repos.yum_config.compose_repos as repos + except ImportError: + import tripleo_repos.yum_config.compose_repos as repos + # 1. Create compose repo config object repo_obj = repos.TripleOYumComposeRepoConfig( module.params['compose_url'], module.params['centos_release'], dir_path=module.params['dir_path'], - arch=module.params['arch']) + arch=module.params['arch'], + environment_file=module.params['environment_file']) # 2. enable CentOS compose repos repo_obj.enable_compose_repos( variants=module.params['variants'], @@ -259,6 +297,12 @@ def run_module(): repo_obj.update_all_sections(file, enabled=False) elif module.params['type'] == 'module': + try: + import ansible_collections.tripleo.repos.plugins.module_utils.\ + tripleo_repos.yum_config.dnf_manager as dnf_mgr + except ImportError: + import tripleo_repos.yum_config.dnf_manager as dnf_mgr + dnf_mod_mgr = dnf_mgr.DnfModuleManager() if module.params['enabled']: dnf_mod_mgr.enable_module(module.params['name'], @@ -275,11 +319,6 @@ def run_module(): stream=module.params['stream'], profile=module.params['profile']) - elif module.params['type'] == 'global': - config_obj = cfg.TripleOYumGlobalConfig( - file_path=module.params['file_path']) - config_obj.update_section('main', m_set_opts) - except Exception as exc: module.fail_json(msg=str(exc)) diff --git a/tests/unit/yum_config/fakes.py b/tests/unit/yum_config/fakes.py index 9eeabfd..53f76de 100644 --- a/tests/unit/yum_config/fakes.py +++ b/tests/unit/yum_config/fakes.py @@ -28,6 +28,7 @@ FAKE_SET_DICT = { FAKE_COMPOSE_URL = ( 'https://composes.centos.org/fake-CentOS-Stream/compose/') FAKE_REPO_PATH = '/etc/yum.repos.d/fake.repo' +FAKE_RELEASE_NAME = 'fake_release' FAKE_COMPOSE_INFO = { "header": { @@ -79,13 +80,26 @@ FAKE_COMPOSE_INFO = { }, } +FAKE_ENV_OUTPUT = """ +LANG=C.utf8 +HOSTNAME=4cb7d7db1907 +which_declare=declare -f +container=oci +PWD=/ +HOME=/root +TERM=xterm +SHLVL=1 +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +_=/usr/bin/env +""" + class FakeConfigParser(dict): def __init__(self, *args, **kwargs): super(FakeConfigParser, self).__init__(*args, **kwargs) self.__dict__ = self - def write(self, file): + def write(self, file, space_around_delimiters=False): pass def read(self, file): diff --git a/tests/unit/yum_config/test_main.py b/tests/unit/yum_config/test_main.py index e85431d..6d466aa 100644 --- a/tests/unit/yum_config/test_main.py +++ b/tests/unit/yum_config/test_main.py @@ -23,6 +23,7 @@ import tripleo_repos.yum_config.__main__ as main import tripleo_repos.yum_config.compose_repos as repos import tripleo_repos.yum_config.constants as const import tripleo_repos.yum_config.dnf_manager as dnf_mgr +import tripleo_repos.yum_config.utils as utils import tripleo_repos.yum_config.yum_config as yum_cfg @@ -44,13 +45,19 @@ class TestTripleoYumConfigBase(unittest.TestCase): @ddt.ddt class TestTripleoYumConfigMain(TestTripleoYumConfigBase): """Test class for main method operations.""" + def setUp(self): + super(TestTripleoYumConfigMain, self).setUp() + self.mock_object(utils, 'get_distro_info', + mock.Mock(return_value=("centos", "8", None))) def test_main_repo(self): sys.argv[1:] = ['repo', 'fake_repo', '--enable', '--set-opts', 'key1=value1', 'key2=value2', '--config-file-path', fakes.FAKE_FILE_PATH] + yum_repo_obj = mock.Mock() - mock_update_section = self.mock_object(yum_repo_obj, 'update_section') + mock_update_section = self.mock_object(yum_repo_obj, + 'add_or_update_section') mock_yum_repo_obj = self.mock_object( yum_cfg, 'TripleOYumRepoConfig', mock.Mock(return_value=yum_repo_obj)) @@ -58,10 +65,11 @@ class TestTripleoYumConfigMain(TestTripleoYumConfigBase): main.main() expected_dict = {'key1': 'value1', 'key2': 'value2'} - mock_yum_repo_obj.assert_called_once_with(dir_path=const.YUM_REPO_DIR) + mock_yum_repo_obj.assert_called_once_with(dir_path=const.YUM_REPO_DIR, + environment_file=None) mock_update_section.assert_called_once_with( - 'fake_repo', expected_dict, file_path=fakes.FAKE_FILE_PATH, - enabled=True) + 'fake_repo', set_dict=expected_dict, + file_path=fakes.FAKE_FILE_PATH, enabled=True) @ddt.data('enable', 'disable', 'reset', 'install', 'remove') def test_main_module(self, operation): @@ -92,7 +100,8 @@ class TestTripleoYumConfigMain(TestTripleoYumConfigBase): main.main() expected_dict = {'key1': 'value1', 'key2': 'value2'} - mock_yum_global_obj.assert_called_once_with(file_path=None) + mock_yum_global_obj.assert_called_once_with(file_path=None, + environment_file=None) mock_update_section.assert_called_once_with('main', expected_dict) def test_main_no_command(self): @@ -143,9 +152,20 @@ class TestTripleoYumConfigMain(TestTripleoYumConfigBase): fakes.FAKE_COMPOSE_URL, const.COMPOSE_REPOS_RELEASES[0], dir_path=const.YUM_REPO_DIR, - arch=const.COMPOSE_REPOS_SUPPORTED_ARCHS[0]) + arch=const.COMPOSE_REPOS_SUPPORTED_ARCHS[0], + environment_file=None) mock_enable_composes.assert_called_once_with( variants=['fake_variant'], override_repos=False) mock_update_all.assert_called_once_with( fakes.FAKE_REPO_PATH, enabled=False ) + + def test_main_invalid_release_for_dnf_module(self): + self.mock_object(utils, 'get_distro_info', + mock.Mock(return_value=("centos", "7", None))) + sys.argv[1:] = ['module', 'enable', 'fake_module'] + + with self.assertRaises(SystemExit) as command: + main.main() + + self.assertEqual(2, command.exception.code) diff --git a/tests/unit/yum_config/test_yum_config.py b/tests/unit/yum_config/test_yum_config.py index c37b75a..f69bb2b 100644 --- a/tests/unit/yum_config/test_yum_config.py +++ b/tests/unit/yum_config/test_yum_config.py @@ -16,6 +16,7 @@ import configparser import copy import ddt import os +import subprocess from unittest import mock from . import fakes @@ -226,17 +227,44 @@ class TestTripleOYumConfig(test_main.TestTripleoYumConfigBase): mock_read_config.assert_called_once_with(fakes.FAKE_FILE_PATH) + def test_source_env_file(self): + p_open_mock = mock.Mock() + mock_open = self.mock_object(subprocess, 'Popen', + mock.Mock(return_value=p_open_mock)) + data_mock = mock.Mock() + self.mock_object(data_mock, 'decode', + mock.Mock(return_value=fakes.FAKE_ENV_OUTPUT)) + self.mock_object(p_open_mock, 'communicate', + mock.Mock(return_value=[data_mock])) + env_update_mock = self.mock_object(os.environ, 'update') + + yum_cfg.source_env_file('fake_source_file', update=True) + + exp_env_dict = dict( + line.split("=", 1) for line in fakes.FAKE_ENV_OUTPUT.splitlines() + if len(line.split("=", 1)) > 1) + + mock_open.assert_called_once_with(". fake_source_file; env", + stdout=subprocess.PIPE, + shell=True) + env_update_mock.assert_called_once_with(exp_env_dict) + @ddt.ddt class TestTripleOYumRepoConfig(test_main.TestTripleoYumConfigBase): """Tests for TripleOYumRepoConfig class and its methods.""" + def setUp(self): + super(TestTripleOYumRepoConfig, self).setUp() + self.config_obj = yum_cfg.TripleOYumRepoConfig( + dir_path='/tmp' + ) + @ddt.data(True, False, None) def test_yum_repo_config_update_section(self, enable): self.mock_object(os.path, 'isfile') self.mock_object(os, 'access') self.mock_object(os.path, 'isdir') - cfg_obj = yum_cfg.TripleOYumRepoConfig() mock_update = self.mock_object(yum_cfg.TripleOYumConfig, 'update_section') @@ -246,13 +274,78 @@ class TestTripleOYumRepoConfig(test_main.TestTripleoYumConfigBase): if enable is not None: expected_updates['enabled'] = '1' if enable else '0' - cfg_obj.update_section(fakes.FAKE_SECTION1, set_dict=updates, - file_path=fakes.FAKE_FILE_PATH, enabled=enable) + self.config_obj.update_section( + fakes.FAKE_SECTION1, set_dict=updates, + file_path=fakes.FAKE_FILE_PATH, enabled=enable) mock_update.assert_called_once_with(fakes.FAKE_SECTION1, expected_updates, file_path=fakes.FAKE_FILE_PATH) + @mock.patch('builtins.open') + def test_add_or_update_section(self, open): + mock_update = self.mock_object( + self.config_obj, 'update_section', + mock.Mock(side_effect=exc.TripleOYumConfigNotFound( + error_msg='error'))) + mock_add_section = self.mock_object(self.config_obj, 'add_section') + + self.config_obj.add_or_update_section( + fakes.FAKE_SECTION1, + set_dict=fakes.FAKE_SET_DICT, + file_path=fakes.FAKE_FILE_PATH, + enabled=True, + create_if_not_exists=True) + + mock_update.assert_called_once_with(fakes.FAKE_SECTION1, + set_dict=fakes.FAKE_SET_DICT, + file_path=fakes.FAKE_FILE_PATH, + enabled=True) + fake_set_dict = copy.deepcopy(fakes.FAKE_SET_DICT) + fake_set_dict['name'] = fakes.FAKE_SECTION1 + mock_add_section.assert_called_once_with( + fakes.FAKE_SECTION1, + fake_set_dict, + fakes.FAKE_FILE_PATH, + enabled=True) + + @ddt.data((fakes.FAKE_FILE_PATH, False), (None, True)) + @ddt.unpack + def test_add_or_update_section_file_not_found(self, fake_path, + create_if_not_exists): + mock_update = self.mock_object( + self.config_obj, 'update_section', + mock.Mock(side_effect=exc.TripleOYumConfigNotFound( + error_msg='error'))) + + self.assertRaises( + exc.TripleOYumConfigNotFound, + self.config_obj.add_or_update_section, + fakes.FAKE_SECTION1, + set_dict=fakes.FAKE_SET_DICT, + file_path=fake_path, + enabled=True, + create_if_not_exists=create_if_not_exists) + + mock_update.assert_called_once_with(fakes.FAKE_SECTION1, + set_dict=fakes.FAKE_SET_DICT, + file_path=fake_path, + enabled=True) + + @ddt.data(None, False, True) + def test_add_section(self, enabled): + mock_add = self.mock_object(yum_cfg.TripleOYumConfig, 'add_section') + + self.config_obj.add_section( + fakes.FAKE_SECTION1, fakes.FAKE_SET_DICT, + fakes.FAKE_FILE_PATH, enabled=enabled) + + updated_dict = copy.deepcopy(fakes.FAKE_SET_DICT) + if enabled is not None: + updated_dict['enabled'] = '1' if enabled else '0' + mock_add.assert_called_once_with(fakes.FAKE_SECTION1, updated_dict, + fakes.FAKE_FILE_PATH) + @ddt.ddt class TestTripleOYumGlobalConfig(test_main.TestTripleoYumConfigBase):