diff options
author | Wayne Warren <waynr+launchpad@sdf.org> | 2015-12-23 14:58:45 -0800 |
---|---|---|
committer | Darragh Bailey <dbailey@hpe.com> | 2016-07-19 18:57:34 +0100 |
commit | 2cf11f014c45cf7eb47003ac9a34a5bcb25a0a03 (patch) | |
tree | 34707b0062e79d92b2445f2bebe07fc919eee859 /jenkins_jobs/config.py | |
parent | 2bf92c3ed7da5730802dd9a65bc0ecc95956b1f9 (diff) | |
download | python-jenkins-job-builder-2cf11f014c45cf7eb47003ac9a34a5bcb25a0a03.tar.gz python-jenkins-job-builder-2cf11f014c45cf7eb47003ac9a34a5bcb25a0a03.tar.xz python-jenkins-job-builder-2cf11f014c45cf7eb47003ac9a34a5bcb25a0a03.zip |
Use JJBConfig for arg/config initialization.
* Create jenkins_jobs.config module with JJBConfig class.
* Move DEFAULT_CONF from jenkins_jobs.cmd into jenkins_jobs.config
* Move configuration initialization into JJBConfig
* Create method, "do_magical_things" to handle arbitration between
config file and arguments as well as setting default values for
config file settings if it doesn't contain the expected keys.
* Move JenkinsJobs.create_parser into its own module,
jenkins_jobs.cli.parser, it can be used to provide default settings
in the JJBConfig class when an argparse namespace object is not
provided; this is primarily necessary because most of the original
configuration initialization code relies on this being a namespace
object (simple descendant of the object class).
At this point JJBConfig isn't much more than an object-oriented
version of the way configuration handling happened previously. Its
current form, however, is more amenable to the ultimate goal of the
2.0 refactorings--namely, being able to pass a single config object
around rather than breaking it up into apparently arbitrary settings
necessary to instantiate the Builder class and its delegate objects.
Change-Id: Ic0147e1dccbe620aaaba039a434e7cea6c670054
Diffstat (limited to 'jenkins_jobs/config.py')
-rw-r--r-- | jenkins_jobs/config.py | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/jenkins_jobs/config.py b/jenkins_jobs/config.py new file mode 100644 index 00000000..b6fb221c --- /dev/null +++ b/jenkins_jobs/config.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python +# Copyright (C) 2015 Wayne Warren +# +# 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. + +# Manage JJB Configuration sources, defaults, and access. + +import io +import logging +import platform +import os +import yaml + +from six.moves import configparser, StringIO + +from jenkins_jobs import builder +from jenkins_jobs import utils +from jenkins_jobs.cli.parser import create_parser +from jenkins_jobs.errors import JenkinsJobsException + +__all__ = [ + "JJBConfig" +] + +logger = logging.getLogger(__name__) + +DEFAULT_CONF = """ +[job_builder] +keep_descriptions=False +ignore_cache=False +recursive=False +exclude=.* +allow_duplicates=False +allow_empty_variables=False + +[jenkins] +url=http://localhost:8080/ +query_plugins_info=True + +[hipchat] +authtoken=dummy +send-as=Jenkins +""" + + +class JJBConfigException(JenkinsJobsException): + pass + + +class JJBConfig(object): + + def __init__(self, config_filename=None, arguments=None, + config_file_required=False): + + """ + The JJBConfig class is intended to encapsulate and resolve priority + between all sources of configuration for the JJB library. This allows + the various sources of configuration to provide a consistent accessor + interface regardless of where they are used. + + It also allows users of JJB-as-an-API to create minimally valid + configuration and easily make minor modifications to default values + without strictly adhering to the confusing setup (see the + do_magical_things method, the behavior of which largely lived in the + cmd.execute method previously) necessary for the jenkins-jobs command + line tool. + + :arg str config_filename: Name of configuration file on which to base + this config object. + :arg dict arguments: A argparse.Namespace object as produced by the + jenkins-jobs argument parser. + :arg bool config_file_required: Allows users of the JJBConfig class to + decide whether or not it's really necessary for a config file to be + passed in when creating an instance. This has two effects on the + behavior of JJBConfig initialization: + * It determines whether or not we try "local" and "global" config + files. + * It determines whether or not failure to read some config file + will raise an exception or simply print a warning message + indicating that no config file was found. + """ + + config_parser = self._init_defaults() + + if arguments is None: + jenkins_jobs_parser = create_parser() + arguments = jenkins_jobs_parser.parse_args(['test']) + + global_conf = '/etc/jenkins_jobs/jenkins_jobs.ini' + user_conf = os.path.join(os.path.expanduser('~'), '.config', + 'jenkins_jobs', 'jenkins_jobs.ini') + local_conf = os.path.join(os.path.dirname(__file__), + 'jenkins_jobs.ini') + conf = None + if config_filename is not None: + conf = config_filename + + elif hasattr(arguments, 'conf') and arguments.conf is not None: + conf = arguments.conf + + elif config_file_required: + if os.path.isfile(local_conf): + conf = local_conf + elif os.path.isfile(user_conf): + conf = user_conf + else: + conf = global_conf + + config_fp = None + if conf is not None: + try: + config_fp = self._read_config_file(conf) + except JJBConfigException as e: + if config_file_required: + raise e + else: + logger.warn("""Config file, {0}, not found. Using default + config values.""".format(conf)) + + if config_fp is not None: + config_parser.readfp(config_fp) + + self.config_parser = config_parser + self.arguments = arguments + + def _init_defaults(self): + """ Initialize default configuration values using DEFAULT_CONF + """ + config = configparser.ConfigParser() + # Load default config always + config.readfp(StringIO(DEFAULT_CONF)) + return config + + def _read_config_file(self, config_filename): + """ Given path to configuration file, read it in as a ConfigParser + object and return that object. + """ + if os.path.isfile(config_filename): + self.__config_file = config_filename # remember file we read from + logger.debug("Reading config from {0}".format(config_filename)) + config_fp = io.open(config_filename, 'r', encoding='utf-8') + else: + raise JJBConfigException("""A valid configuration file is required. + \n{0} is not valid.""".format(config_filename)) + + return config_fp + + def do_magical_things(self): + config = self.config_parser + options = self.arguments + + logger.debug("Config: {0}".format(config)) + + # check the ignore_cache setting: first from command line, + # if not present check from ini file + self.ignore_cache = False + if options.ignore_cache: + self.ignore_cache = options.ignore_cache + elif config.has_option('jenkins', 'ignore_cache'): + logging.warn('''ignore_cache option should be moved to the + [job_builder] section in the config file, the one + specified in the [jenkins] section will be ignored in + the future''') + self.ignore_cache = config.getboolean('jenkins', 'ignore_cache') + elif config.has_option('job_builder', 'ignore_cache'): + self.ignore_cache = config.getboolean('job_builder', + 'ignore_cache') + + # Jenkins supports access as an anonymous user, which can be used to + # ensure read-only behaviour when querying the version of plugins + # installed for test mode to generate XML output matching what will be + # uploaded. To enable must pass 'None' as the value for user and + # password to python-jenkins + # + # catching 'TypeError' is a workaround for python 2.6 interpolation + # error + # https://bugs.launchpad.net/openstack-ci/+bug/1259631 + if options.user: + self.user = options.user + else: + try: + self.user = config.get('jenkins', 'user') + except (TypeError, configparser.NoOptionError): + self.user = None + + if options.password: + self.password = options.password + else: + try: + self.password = config.get('jenkins', 'password') + except (TypeError, configparser.NoOptionError): + self.password = None + + # Inform the user as to what is likely to happen, as they may specify + # a real jenkins instance in test mode to get the plugin info to check + # the XML generated. + if self.user is None and self.password is None: + logger.info("Will use anonymous access to Jenkins if needed.") + elif (self.user is not None and self.password is None) or ( + self.user is None and self.password is not None): + raise JenkinsJobsException( + "Cannot authenticate to Jenkins with only one of User and " + "Password provided, please check your configuration." + ) + + # None -- no timeout, blocking mode; same as setblocking(True) + # 0.0 -- non-blocking mode; same as setblocking(False) <--- default + # > 0 -- timeout mode; operations time out after timeout seconds + # < 0 -- illegal; raises an exception + # to retain the default must use + # "timeout=jenkins_jobs.builder._DEFAULT_TIMEOUT" or not set timeout at + # all. + self.timeout = builder._DEFAULT_TIMEOUT + try: + self.timeout = config.getfloat('jenkins', 'timeout') + except (ValueError): + raise JenkinsJobsException("Jenkins timeout config is invalid") + except (TypeError, configparser.NoOptionError): + pass + + self.plugins_info = None + + if getattr(options, 'plugins_info_path', None) is not None: + with io.open(options.plugins_info_path, 'r', + encoding='utf-8') as yaml_file: + self.plugins_info = yaml.load(yaml_file) + if not isinstance(self.plugins_info, list): + raise JenkinsJobsException("{0} must contain a Yaml list!" + .format(options.plugins_info_path)) + elif (not options.conf or not + config.getboolean("jenkins", "query_plugins_info")): + logger.debug("Skipping plugin info retrieval") + self.plugins_info = {} + + if options.allow_empty_variables is not None: + config.set('job_builder', + 'allow_empty_variables', + str(options.allow_empty_variables)) + + if getattr(options, 'path', None): + if hasattr(options.path, 'read'): + logger.debug("Input file is stdin") + if options.path.isatty(): + if platform.system() == 'Windows': + key = 'CTRL+Z' + else: + key = 'CTRL+D' + logger.warn("""Reading configuration from STDIN. Press %s + to end input.""", key) + else: + # take list of paths + options.path = options.path.split(os.pathsep) + + do_recurse = (getattr(options, 'recursive', False) or + config.getboolean('job_builder', 'recursive')) + + excludes = [e for elist in options.exclude + for e in elist.split(os.pathsep)] or \ + config.get('job_builder', 'exclude').split(os.pathsep) + paths = [] + for path in options.path: + if do_recurse and os.path.isdir(path): + paths.extend(utils.recurse_path(path, excludes)) + else: + paths.append(path) + options.path = paths + + self.config_parser = config + self.arguments = options |