diff --git a/bob/devtools/dav.py b/bob/devtools/dav.py index cd5c3d4463ffc83b3ff4db9cad7dec3dc697c7f2..7a3578f94474c2a12a946a1c36b93d685b75e9c4 100644 --- a/bob/devtools/dav.py +++ b/bob/devtools/dav.py @@ -4,7 +4,7 @@ import os import configparser -from .log import get_logger +from .log import get_logger, echo_warning, echo_info from .deploy import _setup_webdav_client logger = get_logger(__name__) @@ -20,25 +20,29 @@ def _get_config(): if os.path.exists(k): data = configparser.ConfigParser() data.read(k) - if 'global' not in data or \ - 'server' not in data['global'] or \ - 'username' not in data['global'] or \ - 'password' not in data['global']: - assert KeyError, 'The file %s should contain a single ' \ - '"global" section with 3 variables defined inside: ' \ + if ( + "global" not in data + or "server" not in data["global"] + or "username" not in data["global"] + or "password" not in data["global"] + ): + assert KeyError, ( + "The file %s should contain a single " + '"global" section with 3 variables defined inside: ' '"server", "username", "password".' % (k,) - return data['global'] + ) + return data["global"] # ask the user for the information, cache credentials for future use retval = dict() - retval['server'] = input("The base address of the server: ") - retval['username'] = input("Username: ") - retval['password'] = input("Password: ") + retval["server"] = input("The base address of the server: ") + retval["username"] = input("Username: ") + retval["password"] = input("Password: ") # record file for the user data = configparser.ConfigParser() - data['global'] = retval - with open(cfgs[0], 'w') as f: + data["global"] = retval + with open(cfgs[0], "w") as f: logger.warn('Recorded "%s" configuration file for next queries') data.write(f) os.chmod(cfgs[0], 0o600) @@ -51,7 +55,103 @@ def setup_webdav_client(private): """Returns a ready-to-use WebDAV client""" config = _get_config() - root = '/private-upload' if private else '/public-upload' - c = _setup_webdav_client(config['server'], root, config['username'], - config['password']) + root = "/private-upload" if private else "/public-upload" + c = _setup_webdav_client( + config["server"], root, config["username"], config["password"] + ) return c + + +def remove_old_beta_packages(client, path, dry_run, pyver=True): + """Removes old conda packages from a conda channel. + + What is an old package depends on how the packages are produced. In + BEAT/Bob, we build new beta packages with every commit in the CI and we + want to delete the old ones using this script so that we do not run out of + space. + + The core idea is to remove packages that are not (the latest version AND + the latest build number) for each package name. + + Our CI distributes its build into several jobs. Since each job runs + independently of each other (per OS and per Python version), the build + numbers are estimated independently and they will end up to be different + between jobs. + + So the core idea is needed to be applied on each CI job independently. + + + Parameters: + + client (object): The WebDAV client with a preset public/private path + + path (str): A path, within the preset root of the client, where to + search for beta packages. Beta packages are searched in the directory + itself. + + dry_run (bool): A flag indicating if we should just list what we will + be doing, or really execute the deletions + + pyver (:py:class:`bool`, Optional): If ``True``, the python version of + a package will be a part of a package's name. This is need to account + for the fact that our CI jobs run per Python version. + + """ + + server_path = client.get_url(path) + + if not client.is_dir(path): + echo_warning("Path %s is not a directory - ignoring...", server_path) + return + + betas = dict() + # python version regular expression: + pyver_finder = re.compile("py[1-9][0-9]h.*") + + for f in client.list(path): + + if f.startswith("."): + continue + if not f.endswith(".tar.bz2"): + continue + + name, version, build_string = f[:-8].rsplit("-", 2) + hash_, build = build_string.rsplit("_", 1) + + if pyver: + # try to find the python version if it exists + result = pyver_finder.match(hash_) + if result is not None: + name += "/" + result.string[:4] + + target = '/'.join(path, f) + info = client.info(target) + + betas.setdefault(name, []).append( + ( + StrictVersion(version), + int(build), # build number + info['modified'], + target, + ) + ) + + import ipdb; ipdb.set_trace() + + count = sum([len(k) for k in betas.values()]) - len(betas) + echo_info(" - %d variants" % len(betas)) + echo_info(" - %d packages found" % count) + echo_info(" --------------------") + + for name in sorted(betas.keys()): + echo_info(" - packages for %s (%d) ====" % (name, len(betas[name]))) + sorted_packages = sorted(betas[name]) + keep_version, keep_build, _, _ = sorted_packages[-1] + for version, build, mtime, target in sorted_packages: + if version == keep_version and build == keep_build: + echo_info("[keep] %s (time=%u)" % (target, mtime)) + else: + echo_info("rm %s (time=%u)" % (target, mtime)) + if not dry_run: + #client.clean(target) + echo_info("boooom") diff --git a/bob/devtools/scripts/dav.py b/bob/devtools/scripts/dav.py index 41027a7d2170ff0ea0f5dd0dab8f2e8600ca32f7..a9687bb03fb70d7d5f4239ebdd0fa037e72b78d0 100644 --- a/bob/devtools/scripts/dav.py +++ b/bob/devtools/scripts/dav.py @@ -10,7 +10,7 @@ from click_plugins import with_plugins from . import bdt -from ..dav import setup_webdav_client +from ..dav import setup_webdav_client, remove_old_beta_packages from ..log import verbosity_option, get_logger, echo_normal, echo_info, \ echo_warning @@ -271,3 +271,88 @@ def upload(private, execute, local, remote): echo_info('cp %s %s' % (k, remote_path)) if execute: cl.upload_file(local_path=k, remote_path=actual_remote) + + +@dav.command( + epilog=""" +Examples: + + 1. Cleans-up the excess of beta packages from our conda channels via WebDAV: + + $ bdt dav -vv clean-betas remote/path/foo/bar + + Notice this does not do anything for security. It just displays what it + would do. To actually run the rmtree comment pass the --execute flag (or + -x) + + + 2. Realy removes (recursively), everything under the 'remote/path/foo/bar' + path: + + $ bdt dav -vv rmtree --execute remote/path/foo/bar + + +""" +) +@click.option( + "-p", + "--private/--no-private", + default=False, + help="If set, use the 'private' area instead of the public one", +) +@click.option( + "-x", + "--execute/--no-execute", + default=False, + help="If this flag is set, then execute the removal", +) +@click.argument( + "path", + required=True, +) +@verbosity_option() +@bdt.raise_on_error +def clean_betas(private, execute, path): + """Cleans-up the excess of beta packages from a conda channel via WebDAV + + ATTENTION: There is no undo! Use --execute to execute. + """ + + if not execute: + echo_warning("!!!! DRY RUN MODE !!!!") + echo_warning("Nothing is being executed on server. Use -x to execute.") + + if not path.startswith('/'): path = '/' + path + cl = setup_webdav_client(private) + remote_path = cl.get_url(path) + + if not cl.is_dir(path): + echo_warning('Path %s is not a directory - ignoring...', remote_path) + return + + # go through all possible variants: + archs = [ + 'linux-64', + 'linux-32', + 'linux-armv6l', + 'linux-armv7l', + 'linux-ppc64le', + 'osx-64', + 'osx-32', + 'win-64', + 'win-32', + 'noarch', + ] + + for arch in archs: + + arch_path = '/'.join((path, arch)) + + if not cl.is_dir(arch_path): + # it is normal if the directory does not exist + continue + + server_path = cl.get_url(arch_path) + echo_info('Cleaning beta packages from %s' % server_path) + remove_old_beta_packages(client=cl, path=arch_path, + dry_run=(not execute), pyver=True) diff --git a/conda/meta.yaml b/conda/meta.yaml index ab99512be00d7ed3a0960eb5dc494d168638e6af..6b06ea7a23e2e90c2f768392b91ae99ffca63255 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -93,6 +93,12 @@ test: - bdt ci clean --help - bdt ci nightlies --help - bdt ci docs --help + - bdt dav --help + - bdt dav list --help + - bdt dav makedirs --help + - bdt dav rmtree --help + - bdt dav clean-betas --help + - bdt dav upload --help - sphinx-build -aEW ${PREFIX}/share/doc/{{ name }}/doc sphinx - if [ -n "${CI_PROJECT_DIR}" ]; then mv sphinx "${CI_PROJECT_DIR}/"; fi diff --git a/setup.py b/setup.py index 9d29c85fe9228575eef1e2f80cf5ce71edb56399..badd6d24d4beb7093deeee670d09b629dacbdb6c 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,7 @@ setup( 'makedirs = bob.devtools.scripts.dav:makedirs', 'rmtree = bob.devtools.scripts.dav:rmtree', 'upload = bob.devtools.scripts.dav:upload', + 'clean-betas = bob.devtools.scripts.dav:clean_betas', ], },