diff --git a/fdroidserver/__main__.py b/fdroidserver/__main__.py index c3631213..0e5a2386 100755 --- a/fdroidserver/__main__.py +++ b/fdroidserver/__main__.py @@ -65,6 +65,7 @@ COMMANDS = OrderedDict([ # interactively because they rely on the presense of a VM/container, # modify the local environment, or even run things as root. COMMANDS_INTERNAL = [ + "destroy", "up", ] diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 33944b99..86440109 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -5137,3 +5137,15 @@ def get_vagrantfile_path(appid, vercode): """ return Path('tmp/buildserver', get_container_name(appid, vercode), 'Vagrantfile') + + +def vagrant_destroy(appid, vercode): + import vagrant + + vagrantfile = get_vagrantfile_path(appid, vercode) + if vagrantfile.is_file(): + logging.info(f"Destroying Vagrant buildserver VM ({appid}:{vercode})") + v = vagrant.Vagrant(vagrantfile.parent) + v.destroy() + if vagrantfile.parent.exists(): + shutil.rmtree(vagrantfile.parent, ignore_errors=True) diff --git a/fdroidserver/destroy.py b/fdroidserver/destroy.py new file mode 100644 index 00000000..69eb9e07 --- /dev/null +++ b/fdroidserver/destroy.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# +# destroy.py - part of the FDroid server tools +# Copyright (C) 2024, Hans-Christoph Steiner +# Copyright (C) 2024, Michael Pöhn +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Destroy any existing per-build container/VM structures. + +After this runs, there should be no trace of the given +ApplicationID:versionCode left in the container/VM system. + +Since this is an internal command, the strings are not localized. + +""" + +import logging +import sys +import traceback +from argparse import ArgumentParser + +from . import common + +# TODO should this track whether it actually removed something? +# What do `podman rm` and `vagrant destroy` do? + + +def podman_rm(appid, vercode): + """Remove a Podman pod and all its containers.""" + pod_name = common.get_pod_name(appid, vercode) + for p in common.get_podman_client().pods.list(): + if p.name == pod_name: + logging.debug(f'Removing {pod_name}.') + p.remove(force=True) + + +def destroy_wrapper(appid, vercode, virt_container_type): + if virt_container_type == 'vagrant': + common.vagrant_destroy(appid, vercode) + elif virt_container_type == 'podman': + podman_rm(appid, vercode) + + +def main(): + parser = ArgumentParser( + description="Push files into the buildserver container/box." + ) + common.setup_global_opts(parser) + common.setup_virt_container_type_opts(parser) + parser.add_argument( + "APPID:VERCODE", + help="Application ID with Version Code in the form APPID:VERCODE", + ) + options = common.parse_args(parser) + common.set_console_logging(options.verbose) + + try: + appid, vercode = common.split_pkg_arg(options.__dict__['APPID:VERCODE']) + destroy_wrapper( + appid, + vercode, + common.get_virt_container_type(options), + ) + except Exception as e: + if options.verbose: + logging.error(traceback.format_exc()) + else: + logging.error(e) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/fdroidserver/up.py b/fdroidserver/up.py index b6605fed..927e123a 100644 --- a/fdroidserver/up.py +++ b/fdroidserver/up.py @@ -102,6 +102,9 @@ def run_vagrant(appid, vercode, cpus, memory): vagrantfile = common.get_vagrantfile_path(appid, vercode) + # cleanup potentially still existsing vagrant VMs/dirs + common.vagrant_destroy(appid, vercode) + # start new dedicated buildserver vagrant vm from scratch vagrantfile.parent.mkdir(exist_ok=True, parents=True) vagrantfile.write_text( diff --git a/tests/test_up.py b/tests/test_up.py index 67783296..e97c9954 100755 --- a/tests/test_up.py +++ b/tests/test_up.py @@ -3,6 +3,7 @@ import importlib import os import shutil +import time import unittest from pathlib import Path @@ -125,3 +126,19 @@ class Up_run_vagrant(UpTest): up.run_vagrant(APPID, VERCODE, 1, 1) vagrant_up.assert_called_once() self.assertTrue((Path(self.testdir) / self.vagrantdir).exists()) + + @mock.patch('vagrant.Vagrant.up') + @mock.patch('vagrant.Vagrant.destroy') + @mock.patch('vagrant.Vagrant.box_list') + def test_existing(self, box_list, vagrant_destroy, vagrant_up): + "This should never reuse an existing VM." + box_list.return_value = self.box_list_return + up.run_vagrant(APPID, VERCODE, 1, 1) + vagrantfile = self.vagrantdir / 'Vagrantfile' + ctime = os.path.getctime(vagrantfile) + vagrant_up.assert_called_once() + + time.sleep(0.01) # ensure reliable failure when testing ctime + up.run_vagrant(APPID, VERCODE, 1, 1) + vagrant_destroy.assert_called_once() + self.assertNotEqual(ctime, os.path.getctime(vagrantfile))