From e92948658866333e405b5bf60485c377d679a89d Mon Sep 17 00:00:00 2001 From: Andre Anjos <andre.dos.anjos@gmail.com> Date: Mon, 4 Mar 2019 08:42:30 +0100 Subject: [PATCH] [deploy] Add new deployment module and applies DRY to scripts.ci (closes #18) --- bob/devtools/deploy.py | 134 +++++++++++++++++++++++++++++ bob/devtools/scripts/ci.py | 171 ++++++++++--------------------------- doc/api.rst | 2 + 3 files changed, 179 insertions(+), 128 deletions(-) create mode 100644 bob/devtools/deploy.py diff --git a/bob/devtools/deploy.py b/bob/devtools/deploy.py new file mode 100644 index 00000000..e67e57ab --- /dev/null +++ b/bob/devtools/deploy.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +'''Deployment utilities for conda packages and documentation via webDAV''' + + + +from .constants import WEBDAV_PATHS, SERVER +from .log import get_logger +logger = get_logger(__name__) + + +def _setup_webdav_client(server, root, username, password): + '''Configures and checks the webdav client''' + + # setup webdav connection + webdav_options = dict(webdav_hostname=server, webdav_root=root, + webdav_login=username, webdav_password=password) + + from .webdav3 import client as webdav + + retval = webdav.Client(webdav_options) + assert retval.valid() + + return retval + + +def deploy_conda_package(package, stable, public, username, password, + overwrite, dry_run): + '''Deploys a single conda package on the appropriate path + + Args: + + package (str): Path leading to the conda package to be deployed + stable (bool): Indicates if the package should be deployed on a stable + (``True``) or beta (``False``) channel + public (bool): Indicates if the package is supposed to be distributed + publicly or privatly (within Idiap network) + username (str): The name of the user on the webDAV server to use for + uploading the package + password (str): The password of the user on the webDAV server to use for + uploading the package + overwrite (bool): If we should overwrite a package with equal name existing + on the destination directory. Otherwise, an exception is raised. + dry_run (bool): If we're supposed to really do the actions, or just log + messages. + + ''' + + server_info = WEBDAV_PATHS[stable][public] + davclient = _setup_webdav_client(SERVER, server_info['root'], username, + password) + + logger.info('Deploying %s to %s%s%s...', package, SERVER, + server_info['root'], server_info['conda']) + + basename = os.path.basename(package) + remote_path = '%s/%s/%s' % (server_info['conda'], arch, basename) + + if davclient.check(remote_path): + if not overwrite: + raise RuntimeError('The file %s/%s already exists on the server ' \ + '- this can be due to more than one build with deployment ' \ + 'running at the same time. Re-running the broken builds ' \ + 'normally fixes it' % (SERVER, remote_path)) + + else: + logger.info('[dav] rm -f %s%s%s', SERVER, server_info['root'], + remote_path) + if not dry_run: + davclient.clean(remote_path) + + logger.info('[dav] %s -> %s%s%s', package, SERVER, server_info['root'], + remote_path) + if not dry_run: + davclient.upload(local_path=package, remote_path=remote_path) + + logger.debug('Removing local copy (%s) to avoid re-uploads', k) + os.unlink(package) + + +def deploy_documentation(path, package, stable, public, branch, tag, username, + password, dry_run): + '''Deploys sphinx documentation to the appropriate webdav locations + + Args: + + path (str): Path leading to the root of the documentation to be deployed + package (str): Full name (with namespace) of the package being treated + stable (bool): Indicates if the documentation corresponds to the latest + stable build + public (bool): Indicates if the documentation is supposed to be distributed + publicly or privatly (within Idiap network) + branch (str): The name of the branch for the current build + tag (str): The name of the tag currently built (may be ``None``) + username (str): The name of the user on the webDAV server to use for + uploading the package + password (str): The password of the user on the webDAV server to use for + uploading the package + dry_run (bool): If we're supposed to really do the actions, or just log + messages. + + ''' + + # uploads documentation artifacts + if not os.path.exists(path): + raise RuntimeError('Documentation is not available at %s - ' \ + 'ensure documentation is being produced for your project!' % path) + + server_info = WEBDAV_PATHS[stable][public] + davclient = _setup_webdav_client(SERVER, server_info['root'], username, + password) + + remote_path_prefix = '%s/%s' % (server_info['docs'], package) + + # finds out the correct mixture of sub-directories we should deploy to. + # 1. if ref-name is a tag, don't forget to publish to 'master' as well - + # all tags are checked to come from that branch + # 2. if ref-name is a branch name, deploy to it + # 3. in case a tag is being published, make sure to deploy to the special + # "stable" subdir as well + deploy_docs_to = set([branch]) + if stable: + deploy_docs_to.add('master') + if tag is not None: + deploy_docs_to.add(tag) + deploy_docs_to.add('stable') + + for k in deploy_docs_to: + remote_path = '%s/%s' % (remote_path_prefix, k) + logger.info('[dav] %s -> %s%s%s', path, SERVER, server_info['root'], + remote_path) + if not dry_run: + davclient.upload_directory(local_path=path, remote_path=remote_path) diff --git a/bob/devtools/scripts/ci.py b/bob/devtools/scripts/ci.py index 9f753fa5..f6cc0b33 100644 --- a/bob/devtools/scripts/ci.py +++ b/bob/devtools/scripts/ci.py @@ -14,6 +14,7 @@ from click_plugins import with_plugins from . import bdt from ..constants import SERVER, CONDA_BUILD_CONFIG, CONDA_RECIPE_APPEND, \ WEBDAV_PATHS +from ..deploy import deploy_conda_package, deploy_documentation from ..log import verbosity_option, get_logger, echo_normal logger = get_logger(__name__) @@ -58,47 +59,25 @@ def base_deploy(dry_run): logger.warn('Nothing is being deployed to server') package = os.environ['CI_PROJECT_PATH'] - - server_info = WEBDAV_PATHS[True][True] #stable=True, visible=True - - logger.info('Deploying dependence packages to %s%s%s...', SERVER, - server_info['root'], server_info['conda']) - - # setup webdav connection - webdav_options = { - 'webdav_hostname': SERVER, - 'webdav_root': server_info['root'], - 'webdav_login': os.environ['DOCUSER'], - 'webdav_password': os.environ['DOCPASS'], - } - from ..webdav3 import client as webdav - davclient = webdav.Client(webdav_options) - assert davclient.valid() - group, name = package.split('/') - # uploads conda package artificats + # deploys all conda package artefacts currently available (erases them + # afterwards) for arch in ('linux-64', 'osx-64', 'noarch'): # finds conda dependencies and uploads what we can find package_path = os.path.join(os.environ['CONDA_ROOT'], 'conda-bld', arch, '*.tar.bz2') deploy_packages = glob.glob(package_path) + for k in deploy_packages: - basename = os.path.basename(k) - if basename.startswith(name): + + if os.path.basename(k).startswith(name): logger.debug('Skipping deploying of %s - not a base package', k) continue - remote_path = '%s/%s/%s' % (server_info['conda'], arch, basename) - if davclient.check(remote_path): - raise RuntimeError('The file %s/%s already exists on the server ' \ - '- this can be due to more than one build with deployment ' \ - 'running at the same time. Re-running the broken builds ' \ - 'normally fixes it' % (SERVER, remote_path)) - logger.info('[dav] %s -> %s%s%s', k, SERVER, server_info['root'], - remote_path) - if not dry_run: - davclient.upload(local_path=k, remote_path=remote_path) + deploy_conda_package(k, stable=True, public=True, + username=os.environ['DOCUSER'], password=os.environ['DOCPASS'], + overwrite=False, dry_run=dry_run) @ci.command(epilog=''' @@ -132,79 +111,29 @@ def deploy(dry_run): logger.warn('Nothing is being deployed to server') package = os.environ['CI_PROJECT_PATH'] + group, name = package.split('/') - # determine project visibility - visible = (os.environ['CI_PROJECT_VISIBILITY'] == 'public') - - # determine if building branch or tag + # determine if building branch or tag, and project visibility stable = ('CI_COMMIT_TAG' in os.environ) + public = (os.environ['CI_PROJECT_VISIBILITY'] == 'public') - server_info = WEBDAV_PATHS[stable][visible] - - logger.info('Deploying conda packages to %s%s%s...', SERVER, - server_info['root'], server_info['conda']) - - # setup webdav connection - webdav_options = { - 'webdav_hostname': SERVER, - 'webdav_root': server_info['root'], - 'webdav_login': os.environ['DOCUSER'], - 'webdav_password': os.environ['DOCPASS'], - } - from ..webdav3 import client as webdav - davclient = webdav.Client(webdav_options) - assert davclient.valid() - - group, name = package.split('/') - - # uploads conda package artificats + # deploys all conda package artefacts currently available (erases them + # afterwards) for arch in ('linux-64', 'osx-64', 'noarch'): # finds conda packages and uploads what we can find package_path = os.path.join(os.environ['CONDA_ROOT'], 'conda-bld', arch, name + '*.tar.bz2') deploy_packages = glob.glob(package_path) for k in deploy_packages: - remote_path = '%s/%s/%s' % (server_info['conda'], arch, - os.path.basename(k)) - if davclient.check(remote_path): - raise RuntimeError('The file %s/%s already exists on the server ' \ - '- this can be due to more than one build with deployment ' \ - 'running at the same time. Re-running the broken builds ' \ - 'normally fixes it' % (SERVER, remote_path)) - logger.info('[dav] %s -> %s%s%s', k, SERVER, server_info['root'], - remote_path) - if not dry_run: - davclient.upload(local_path=k, remote_path=remote_path) - - # uploads documentation artifacts + deploy_conda_package(k, stable=stable, public=visible, + username=os.environ['DOCUSER'], password=os.environ['DOCPASS'], + overwrite=False, dry_run=dry_run) + local_docs = os.path.join(os.environ['CI_PROJECT_DIR'], 'sphinx') - if not os.path.exists(local_docs): - raise RuntimeError('Documentation is not available at %s - ' \ - 'ensure documentation is being produced for your project!' % \ - local_docs) - - remote_path_prefix = '%s/%s' % (server_info['docs'], package) - - # finds out the correct mixture of sub-directories we should deploy to. - # 1. if ref-name is a tag, don't forget to publish to 'master' as well - - # all tags are checked to come from that branch - # 2. if ref-name is a branch name, deploy to it - # 3. in case a tag is being published, make sure to deploy to the special - # "stable" subdir as well - deploy_docs_to = set([os.environ['CI_COMMIT_REF_NAME']]) - if stable: - deploy_docs_to.add('master') - if os.environ.get('CI_COMMIT_TAG') is not None: - deploy_docs_to.add(os.environ['CI_COMMIT_TAG']) - deploy_docs_to.add('stable') - - for k in deploy_docs_to: - remote_path = '%s/%s' % (remote_path_prefix, k) - logger.info('[dav] %s -> %s%s%s', local_docs, SERVER, - server_info['root'], remote_path) - if not dry_run: - davclient.upload_directory(local_path=local_docs, - remote_path=remote_path) + deploy_documentation(local_docs, package, stable=stable, public=public, + branch=os.environ['CI_COMMIT_REF_NAME'], + tag=os.environ.get('CI_COMMIT_TAG'), username=os.environ['DOCUSER'], + password=os.environ['DOCPASS'], dry_run=dry_run) @ci.command(epilog=''' @@ -580,43 +509,29 @@ def nightlies(ctx, order, dry_run): ci=True, ) - sphinx_output = os.path.join(os.environ['CI_PROJECT_DIR'], 'sphinx') - if os.path.exists(sphinx_output): - logger.debug('Sphinx output was generated during test/rebuild of %s - ' \ - 'Erasing...', package) - shutil.rmtree(sphinx_output) - - # re-deploys a new conda package if it was rebuilt + local_docs = os.path.join(os.environ['CI_PROJECT_DIR'], 'sphinx') + is_master = os.environ['CI_COMMIT_REF_NAME'] == 'master' + + # re-deploys freshly built documentation if we're on the master branch + # otherwise, removes the documentation + if is_master: + deploy_documentation(local_docs, package, stable=stable, + public=(not private), branch='master', tag=None, + username=os.environ['DOCUSER'], password=os.environ['DOCPASS'], + dry_run=dry_run) + elif os.path.exists(local_docs): + logger.debug('Sphinx output was generated during test/rebuild ' \ + 'of %s - Erasing...', package) + shutil.rmtree(local_docs) + + # re-deploys a new conda package if it was rebuilt and it is the master + # branch # n.b.: can only arrive here if dry_run was ``False`` (no need to check # again) - if 'BDT_REBUILD' in os.environ: - + if 'BDT_REBUILD' in os.environ and is_master: tarball = os.environ['BDT_REBUILD'] del os.environ['BDT_REBUILD'] - server_info = WEBDAV_PATHS[stable][not private] - - logger.info('Deploying conda package to %s%s%s...', SERVER, - server_info['root'], server_info['conda']) - - # setup webdav connection - webdav_options = { - 'webdav_hostname': SERVER, - 'webdav_root': server_info['root'], - 'webdav_login': os.environ['DOCUSER'], - 'webdav_password': os.environ['DOCPASS'], - } - from ..webdav3 import client as webdav - davclient = webdav.Client(webdav_options) - assert davclient.valid() - - remote_path = '%s/%s/%s' % (server_info['conda'], arch, - os.path.basename(tarball)) - if davclient.check(remote_path): - raise RuntimeError('The file %s/%s already exists on the server ' \ - '- this can be due to more than one rebuild with deployment ' \ - 'running at the same time. Re-running the broken builds ' \ - 'normally fixes it' % (SERVER, remote_path)) - logger.info('[dav] %s -> %s%s%s', k, SERVER, server_info['root'], - remote_path) - davclient.upload(local_path=tarball, remote_path=remote_path) + deploy_conda_package(tarball, stable=stable, public=(not private), + username=os.environ['DOCUSER'], password=os.environ['DOCPASS'], + overwrite=False, dry_run=dry_run) diff --git a/doc/api.rst b/doc/api.rst index 3ed4bd75..0d20dd0e 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -33,6 +33,8 @@ Detailed Information .. automodule:: bob.devtools.build +.. automodule:: bob.devtools.deploy + WebDAV Python Client -------------------- -- GitLab