| |
@@ -0,0 +1,234 @@
|
| |
+ #!/usr/bin/python3
|
| |
+ # This code runs under python2 and python3
|
| |
+
|
| |
+ #
|
| |
+ # Copyright: (c) 2018, Stef Walter <stefw@redhat.com>
|
| |
+ #
|
| |
+ # The MIT License (MIT)
|
| |
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
|
| |
+ # of this software and associated documentation files (the "Software"), to deal
|
| |
+ # in the Software without restriction, including without limitation the rights
|
| |
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| |
+ # copies of the Software, and to permit persons to whom the Software is
|
| |
+ # furnished to do so, subject to the following conditions:
|
| |
+ #
|
| |
+ # The above copyright notice and this permission notice shall be included in all
|
| |
+ # copies or substantial portions of the Software.
|
| |
+ #
|
| |
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| |
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| |
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| |
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| |
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| |
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| |
+ # SOFTWARE.
|
| |
+ #
|
| |
+
|
| |
+ from ansible.module_utils.basic import AnsibleModule
|
| |
+
|
| |
+ import errno
|
| |
+ import logging
|
| |
+ import glob
|
| |
+ import re
|
| |
+ import os
|
| |
+ import shutil
|
| |
+
|
| |
+ try:
|
| |
+ from urllib.request import urlopen
|
| |
+ from urllib.error import URLError
|
| |
+ from configparser import ConfigParser
|
| |
+ except ImportError:
|
| |
+ from urllib2 import urlopen, URLError
|
| |
+ from ConfigParser import ConfigParser
|
| |
+
|
| |
+ ANSIBLE_METADATA = {
|
| |
+ 'metadata_version': '1.1',
|
| |
+ 'status': ['preview'],
|
| |
+ 'supported_by': 'community'
|
| |
+ }
|
| |
+
|
| |
+ DOCUMENTATION = '''
|
| |
+ ---
|
| |
+ module: source-lookaside
|
| |
+
|
| |
+ short_description: Extract source code from Fedora or RHEL lookaside dist-git caches
|
| |
+
|
| |
+ version_added: "2.4"
|
| |
+
|
| |
+ description:
|
| |
+ - "This module retrives source code artifacts from the Fedora or RHEL dist-git lookaside caches"
|
| |
+
|
| |
+ options:
|
| |
+ package:
|
| |
+ description:
|
| |
+ - The package name
|
| |
+ required: true
|
| |
+ sources:
|
| |
+ description:
|
| |
+ - The sources file that includes hashes. Default: "./sources"
|
| |
+ required: false
|
| |
+ target:
|
| |
+ description:
|
| |
+ - Target directory to write sources to. Default: same directory as sources
|
| |
+ required: false
|
| |
+
|
| |
+ author:
|
| |
+ - Stef Walter (@stefwalter)
|
| |
+ '''
|
| |
+
|
| |
+ EXAMPLES = '''
|
| |
+ # Pull source tarball
|
| |
+ - name: Pull sources
|
| |
+ source-lookaside:
|
| |
+ package: cockpit
|
| |
+
|
| |
+ # Pull a sources from the given sources file
|
| |
+ - name: Pull sources
|
| |
+ source-lookaside:
|
| |
+ package: cockpit
|
| |
+ sources: /path/to/cockpit/sources
|
| |
+
|
| |
+ # Pull the sources into to a specific directory
|
| |
+ - name: Pull sources
|
| |
+ source-lookaside:
|
| |
+ package: cockpit
|
| |
+ target: /path/to/source
|
| |
+ '''
|
| |
+
|
| |
+ RETURN = '''
|
| |
+ original_sources:
|
| |
+ description: The path to the sources file
|
| |
+ sources:
|
| |
+ description: A list of source artifact files
|
| |
+ '''
|
| |
+
|
| |
+ LOOKASIDES = (
|
| |
+ "https://src.fedoraproject.org/repo/pkgs",
|
| |
+ )
|
| |
+
|
| |
+ LOOKASIDE_URI = "/rpms/{name}/{filename}/{hashtype}/{hash}/{filename}"
|
| |
+
|
| |
+ # Location to find more lookaside URLs
|
| |
+ LOOKASIDE_CONFIG = "/etc/rpkg/*.conf"
|
| |
+
|
| |
+ PATTERNS = (
|
| |
+ re.compile(r'^(?P<hashtype>[^ ]+?) \((?P<filename>[^ )]+?)\) = (?P<hash>[^ ]+?)$'),
|
| |
+ re.compile(r'^(?P<hash>[^ ]+?) (?P<filename>[^ )]+?)$'),
|
| |
+ )
|
| |
+
|
| |
+ # A logger to write to
|
| |
+ logger = logging.getLogger(__name__)
|
| |
+ logger.setLevel(logging.INFO)
|
| |
+
|
| |
+
|
| |
+ # Because some people have other lookaside URLs configured, look them up
|
| |
+ def lookasides():
|
| |
+ for url in LOOKASIDES:
|
| |
+ yield url + LOOKASIDE_URI
|
| |
+ config = ConfigParser()
|
| |
+ config.read(glob.glob(LOOKASIDE_CONFIG))
|
| |
+ for section in config.sections():
|
| |
+ if config.has_option(section, "lookaside"):
|
| |
+ yield config.get(section, "lookaside") + LOOKASIDE_URI
|
| |
+
|
| |
+
|
| |
+ # Parses the sources file into a list of possible urls to retrieve
|
| |
+ def urls(package, sources):
|
| |
+ if not os.path.exists(sources):
|
| |
+ return
|
| |
+ with open(sources, 'r') as fp:
|
| |
+ for line in fp.readlines():
|
| |
+ for pattern in PATTERNS:
|
| |
+ match = pattern.match(line.strip())
|
| |
+ if match is None:
|
| |
+ continue
|
| |
+ fields = match.groupdict()
|
| |
+ fields['hashtype'] = fields.get('hashtype', 'md5').lower()
|
| |
+ fields['name'] = package
|
| |
+ yield tuple(map(lambda url: url.format(**fields), lookasides()))
|
| |
+
|
| |
+
|
| |
+ def mkdirs(directory):
|
| |
+ try:
|
| |
+ os.makedirs(directory)
|
| |
+ except OSError as ex:
|
| |
+ if ex.errno == errno.EEXIST and os.path.isdir(directory):
|
| |
+ pass
|
| |
+
|
| |
+
|
| |
+ def retrieve(url, target):
|
| |
+ name = os.path.basename(url)
|
| |
+ dest = os.path.join(target, name)
|
| |
+
|
| |
+ mkdirs(target)
|
| |
+
|
| |
+ try:
|
| |
+ with open(dest, 'wb') as fp:
|
| |
+ shutil.copyfileobj(urlopen(url), fp)
|
| |
+ except URLError as ex:
|
| |
+ if not hasattr(ex, "code") or ex.code != 404:
|
| |
+ logger.error("{0}: {1} {2}".format(name, url, str(ex)))
|
| |
+ return None
|
| |
+
|
| |
+ return dest
|
| |
+
|
| |
+
|
| |
+ def run_module():
|
| |
+ # define available arguments/parameters a user can pass to the module
|
| |
+ module_args = dict(
|
| |
+ package=dict(type='str', required=True),
|
| |
+ sources=dict(type='str', required=False, default="./sources"),
|
| |
+ target=dict(type='str', required=False, default=""),
|
| |
+ )
|
| |
+
|
| |
+ module = AnsibleModule(argument_spec=module_args, supports_check_mode=True)
|
| |
+
|
| |
+ package = module.params['package'].strip()
|
| |
+ sources = module.params['sources']
|
| |
+ target = module.params['target']
|
| |
+
|
| |
+ # Default target to same directory as sources file
|
| |
+ if not target:
|
| |
+ target = os.path.dirname(sources)
|
| |
+
|
| |
+ # The results
|
| |
+ result = dict(changed=False, original_sources=sources, sources=[])
|
| |
+
|
| |
+ if not os.path.exists(sources):
|
| |
+ module.fail_json(msg='The sources file does not exist: {0}'.format(sources), **result)
|
| |
+ return
|
| |
+
|
| |
+ # if the user is working with this module in only check mode we do not
|
| |
+ # want to make any changes to the environment, just return the current
|
| |
+ # state with no modifications
|
| |
+ if module.check_mode:
|
| |
+ return result
|
| |
+
|
| |
+ # Get a list of possible URLs for each of the sources
|
| |
+ for possible in urls(package, sources):
|
| |
+ name = None
|
| |
+ for url in possible:
|
| |
+ name = os.path.basename(url)
|
| |
+ logger.info("{0}: {1}\n".format(name, url))
|
| |
+
|
| |
+ # Try to retrieve the possible url for this source
|
| |
+ dest = retrieve(url, target)
|
| |
+ if dest:
|
| |
+ result['changed'] = True
|
| |
+ result['sources'].append(dest)
|
| |
+ break
|
| |
+
|
| |
+ # We fail module execution if we cannot retrieve each file
|
| |
+ else:
|
| |
+ module.fail_json(msg="Unable to retrieve source file: {}".format(name), **result)
|
| |
+
|
| |
+ # Successful module execution
|
| |
+ module.exit_json(**result)
|
| |
+
|
| |
+
|
| |
+ def main():
|
| |
+ run_module()
|
| |
+
|
| |
+
|
| |
+ if __name__ == '__main__':
|
| |
+ main()
|
| |
Use the rather standard HTTP API for the sources file and the dist-git
lookaside cache to retrieve sources. Don't rely on the fedpkg or rhpkg
tooling because these are unnecessarily diverged between Fedora and RHEL
Solves Issue #271