tdawson / centos / centpkg

Forked from centos/centpkg 3 years ago
Clone
Blob Blame History Raw
# Copyright (c) 2018 - Red Hat Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.


"""Interact with the Red Hat lookaside cache

We need to override the pyrpkg.lookasidecache module to handle our custom
download path.
"""

import io
import os
import pycurl
import six
import sys

from pyrpkg.errors import InvalidHashType, UploadError, LayoutError
from pyrpkg.lookaside import CGILookasideCache
from pyrpkg.layout.layouts import DistGitLayout

from . import utils


def is_dist_git(folder):
    """
    Indicates if a folder is using a dist-git layout.

    Parameters
    ----------
    folder: str
        The directory to inspect.

    Returns
    -------
    bool
        A bool flag indicating if `folder` is using
        a dist-git layout format.
    """
    result = False

    try:
        DistGitLayout.from_path(folder)
        result = True
    except LayoutError:
        result = False
    finally:
        return result


class StreamLookasideCache(CGILookasideCache):
    """
    CentosStream lookaside specialized class.

    It inherits most of its behavior from `pyrpkg.lookasideCGILookasideCache`.
    """

    def __init__(self, hashtype, download_url, upload_url, client_cert=None, ca_cert=None):
        super(StreamLookasideCache, self).__init__(
            hashtype, download_url, upload_url,
            client_cert=client_cert, ca_cert=ca_cert)

    def get_download_url(self, name, filename, hash, hashtype=None, **kwargs):
        _name = utils.get_repo_name(name) if is_dist_git(os.getcwd()) else name

        return super(StreamLookasideCache, self).get_download_url(
                _name, filename, hash, hashtype=hashtype, **kwargs)

    def remote_file_exists(self, name, filename, hashstr):
        """
        Check if a remote file exists.

        This method inherits the behavior of its parent class from pyrpkg.

        It uses the internal `utils.get_repo_name` method to parse the name in case
        it is a scm url. 

        Parameters
        ----------
        name: str
            The repository name and org.

        filename: str
            The filename (something.tar.gz).

        hash:
            The hash string for the file.

        Returns
        -------
        bool
            A boolean value to inditicate if the file exists.
        """
        _name = utils.get_repo_name(name) if is_dist_git(os.getcwd()) else name

        try:
            status = super(StreamLookasideCache, self).remote_file_exists(
                _name, filename, hashstr)
        except UploadError as e:
            self.log.error('Error checking for %s at %s'
                          % (filename, self.upload_url))
            self.log.error(e)
            raise SystemExit(1)

        return status

    def upload(self, name, filename, hashstr, offline=False):
        """
        Uploads a file to lookaside cache.

        This method inherits the behavior of its parent class from pyrpkg.

        It uses the internal `utils.get_repo_name` method to parse the name in case
        it is a scm url. 

        Parameters
        ----------
        name: str
            The repository name and org.

        filename: str
            The filename (something.tar.gz).

        hash:
            The hash string for the file.

        Raises
        ------
        pyrpkg.errors.rpkgError
            Raises specialized classes that inherits from pyrpkg base errors.

        Returns
        -------
        None
            Does not return anything
        """
        _name = utils.get_repo_name(name) if is_dist_git(os.getcwd()) else name

        return super(StreamLookasideCache, self).upload(
            _name, filename, hashstr)

    def download(self, name, filename, hashstr, outfile, hashtype=None, **kwargs):
        """
        Downloads a file from lookaside cache to the local filesystem.

        This method inherits the behavior of its parent class from pyrpkg.

        It uses the internal `utils.get_repo_name` method to parse the name in case
        it is a scm url. 

        Parameters
        ----------
        name: str
            The repository name and org.

        filename: str
            The filename (something.tar.gz).

        hash: str
            The hash string for the file.

        outfile: str


        Raises
        ------
        pyrpkg.errors.rpkgError
            Raises specialized implementations of  `yrpkg.errors.rpkgError`.

        Returns
        -------
        None
            Does not return anything
        """
        _name = utils.get_repo_name(name) if is_dist_git(os.getcwd()) else name

        return super(StreamLookasideCache, self).download(
           _name, filename, hashstr, outfile, hashtype=hashtype, **kwargs)


class CLLookasideCache(CGILookasideCache):
    """
    Centos Linux lookaside specialized class.

    It inherits most of its behavior from `pyrpkg.lookasideCGILookasideCache`.
    """

    def __init__(self, hashtype, download_url, upload_url, name, branch):
        super(CLLookasideCache, self).__init__(
            hashtype, download_url, upload_url, name, branch)
        self.name = name
        self.branch = branch

    def get_download_url(self, name, filename, hash, hashtype=None, **kwargs):
        self.download_path='%(name)s/%(branch)s/%(hash)s'
        if "/" in name:
                real_name = name.split("/")[-1]
        else:
                real_name = name
        path_dict = {
                'name': real_name,
                'filename': filename,
                'branch': self.branch,
                'hash': hash,
                'hashtype': hashtype
        }
        path = self.download_path % path_dict
        return os.path.join(self.download_url, path)


class SIGLookasideCache(CGILookasideCache):
    """
    Centos SIG lookaside specialized class.

    It inherits most of its behavior from `pyrpkg.lookasideCGILookasideCache`.
    """
    def __init__(self, hashtype, download_url, upload_url, name, branch, client_cert=None, ca_cert=None):
        super(SIGLookasideCache, self).__init__(
            hashtype, download_url, upload_url, client_cert=client_cert, ca_cert=ca_cert)

        self.name = name
        self.branch = branch

    def get_download_url(self, name, filename, hash, hashtype=None, **kwargs):
        download_path =  '%(name)s/%(branch)s/%(hash)s'
        if "/" in name:
                real_name = name.split("/")[-1]
        else:
                real_name = name
        path_dict = {
                'name': real_name,
                'filename': filename,
                'branch': self.branch,
                'hash': hash,
                'hashtype': hashtype
        }
        path = download_path % path_dict
        return os.path.join(self.download_url, path)

    def remote_file_exists(self, name, filename, hash):
        """Verify whether a file exists on the lookaside cache

        :param str name: The name of the module. (usually the name of the
            SRPM). This can include the namespace as well (depending on what
            the server side expects).
        :param str filename: The name of the file to check for.
        :param str hash: The known good hash of the file.
        """

        # RHEL 7 ships pycurl that does not accept unicode. When given unicode
        # type it would explode with "unsupported second type in tuple". Let's
        # convert to str just to be sure.
        # https://bugzilla.redhat.com/show_bug.cgi?id=1241059
        _name = utils.get_repo_name(name) if is_dist_git(os.getcwd()) else name

        if six.PY2 and isinstance(filename, six.text_type):
            filename = filename.encode('utf-8')

        if six.PY2 and isinstance(self.branch, six.text_type):
            self.branch = self.branch.encode('utf-8')

        post_data = [('name', _name),
                     ('%ssum' % self.hashtype, hash),
                     ('filename', filename)]

        with io.BytesIO() as buf:
            c = pycurl.Curl()
            c.setopt(pycurl.URL, self.upload_url)
            c.setopt(pycurl.WRITEFUNCTION, buf.write)
            c.setopt(pycurl.HTTPPOST, post_data)

            if self.client_cert is not None:
                if os.path.exists(self.client_cert):
                    c.setopt(pycurl.SSLCERT, self.client_cert)
                else:
                    self.log.warning("Missing certificate: %s"
                                     % self.client_cert)

            if self.ca_cert is not None:
                if os.path.exists(self.ca_cert):
                    c.setopt(pycurl.CAINFO, self.ca_cert)
                else:
                    self.log.warning("Missing certificate: %s", self.ca_cert)

            c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_GSSNEGOTIATE)
            c.setopt(pycurl.USERPWD, ':')

            try:
                c.perform()
                status = c.getinfo(pycurl.RESPONSE_CODE)

            except Exception as e:
                raise UploadError(e)

            finally:
                c.close()

            output = buf.getvalue().strip()

        if status != 200:
            self.raise_upload_error(status)

        # Lookaside CGI script returns these strings depending on whether
        # or not the file exists:
        if output == b'Available':
            return True

        if output == b'Missing':
            return False

        # Something unexpected happened
        self.log.debug(output)
        raise UploadError('Error checking for %s at %s'
                          % (filename, self.upload_url))

    def upload(self, name, filepath, hash):
        """Upload a source file

        :param str name: The name of the module. (usually the name of the SRPM)
            This can include the namespace as well (depending on what the
            server side expects).
        :param str filepath: The full path to the file to upload.
        :param str hash: The known good hash of the file.
        """
        filename = os.path.basename(filepath)

        # As in remote_file_exists, we need to convert unicode strings to str
        if six.PY2:
            if isinstance(name, six.text_type):
                name = name.encode('utf-8')
            if isinstance(filepath, six.text_type):
                filepath = filepath.encode('utf-8')

        if self.remote_file_exists(name, filename, hash):
            self.log.info("File already uploaded: %s", filepath)
            return

        self.log.info("Uploading: %s", filepath)
        post_data = [('name', name),
                     ('%ssum' % self.hashtype, hash),
                     ('file', (pycurl.FORM_FILE, filepath))]

        with io.BytesIO() as buf:
            c = pycurl.Curl()
            c.setopt(pycurl.URL, self.upload_url)
            c.setopt(pycurl.NOPROGRESS, False)
            c.setopt(pycurl.PROGRESSFUNCTION, self.print_progress)
            c.setopt(pycurl.WRITEFUNCTION, buf.write)
            c.setopt(pycurl.HTTPPOST, post_data)

            if self.client_cert is not None:
                if os.path.exists(self.client_cert):
                    c.setopt(pycurl.SSLCERT, self.client_cert)
                else:
                    self.log.warning("Missing certificate: %s", self.client_cert)

            if self.ca_cert is not None:
                if os.path.exists(self.ca_cert):
                    c.setopt(pycurl.CAINFO, self.ca_cert)
                else:
                    self.log.warning("Missing certificate: %s", self.ca_cert)

            c.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_GSSNEGOTIATE)
            c.setopt(pycurl.USERPWD, ':')

            try:
                c.perform()
                status = c.getinfo(pycurl.RESPONSE_CODE)

            except Exception as e:
                raise UploadError(e)

            finally:
                c.close()

            output = buf.getvalue().strip()

        # Get back a new line, after displaying the download progress
        sys.stdout.write('\n')
        sys.stdout.flush()

        if status != 200:
            self.raise_upload_error(status)

        if output:
            self.log.debug(output)