diff --git a/CHANGES.d/20241118_043939_elikowa_add_check_and_predict_local_flag.md b/CHANGES.d/20241118_043939_elikowa_add_check_and_predict_local_flag.md new file mode 100644 index 000000000..622053c19 --- /dev/null +++ b/CHANGES.d/20241118_043939_elikowa_add_check_and_predict_local_flag.md @@ -0,0 +1,4 @@ +- Adds a `--local` flag to `./batou deploy`, which can + be used in tandem with `--consistency-only` or `--predict-only` to + check and predict using the local host's state, without connecting to + the remote host. diff --git a/doc/source/cli/index.txt b/doc/source/cli/index.txt index cd5fa41be..3ceab66ec 100644 --- a/doc/source/cli/index.txt +++ b/doc/source/cli/index.txt @@ -30,7 +30,9 @@ batou deploy .. code-block:: console - usage: batou deploy [-h] [-p PLATFORM] [-t TIMEOUT] [-D] [-c] [-P] [-j JOBS] + usage: batou deploy [-h] [-p PLATFORM] [-t TIMEOUT] [-D] [-c] [-P] + [--local] [-j JOBS] + [--provision-rebuild] environment positional arguments: @@ -50,6 +52,9 @@ batou deploy Does not touch anything. -P, --predict-only Only predict what updates would happen. Do not change anything. + -L, --local When running in consistency-only or predict-only mode, + do not connect to the remote host, but check and + predict using the local host's state. -j JOBS, --jobs JOBS Defines number of jobs running parallel to deploy. The default results in a serial deployment of components. Will override the environment settings for operational diff --git a/examples/tutorial-secrets/environments/gocept/secret-foobar.yaml.gpg b/examples/tutorial-secrets/environments/gocept/secret-foobar.yaml.gpg new file mode 100644 index 000000000..2c542f1c5 Binary files /dev/null and b/examples/tutorial-secrets/environments/gocept/secret-foobar.yaml.gpg differ diff --git a/src/batou/deploy.py b/src/batou/deploy.py index 933cd85d2..210337ac4 100644 --- a/src/batou/deploy.py +++ b/src/batou/deploy.py @@ -112,10 +112,15 @@ def __init__( dirty, jobs, predict_only=False, + check_and_predict_local=False, provision_rebuild=False, ): self.environment = Environment( - environment, timeout, platform, provision_rebuild=provision_rebuild + environment, + timeout, + platform, + provision_rebuild=provision_rebuild, + check_and_predict_local=check_and_predict_local, ) self.environment.deployment = self @@ -345,6 +350,7 @@ def main( dirty, consistency_only, predict_only, + check_and_predict_local, jobs, provision_rebuild, ): @@ -361,6 +367,14 @@ def main( else: ACTION = "DEPLOYMENT" SUCCESS_FORMAT = {"green": True} + if check_and_predict_local: + if (not consistency_only) and (not predict_only): + output.error( + "The --local option is only to be used with --consistency-only or --predict-only." + ) + sys.exit(1) + ACTION += " (local)" + with locked(".batou-lock", exit_on_failure=True): deployment = Deployment( environment, @@ -369,6 +383,7 @@ def main( dirty, jobs, predict_only, + check_and_predict_local, provision_rebuild, ) environment = deployment.environment diff --git a/src/batou/environment.py b/src/batou/environment.py index 611a8af67..13c02023a 100644 --- a/src/batou/environment.py +++ b/src/batou/environment.py @@ -130,6 +130,7 @@ def __init__( platform=None, basedir=".", provision_rebuild=False, + check_and_predict_local=False, ): self.name: str = name self.hosts: Dict[str, Host] = {} @@ -140,6 +141,7 @@ def __init__( self.timeout = timeout self.platform = platform self.provision_rebuild = provision_rebuild + self.check_and_predict_local = check_and_predict_local self.hostname_mapping: Dict[str, str] = {} @@ -291,6 +293,9 @@ def load_environment(self, config): self._set_defaults() + if self.check_and_predict_local: + self.connect_method = "local" + if "vfs" in config: sandbox = config["vfs"]["sandbox"] sandbox = getattr(batou.vfs, sandbox)(self, config["vfs"]) diff --git a/src/batou/main.py b/src/batou/main.py index 0be9677c7..fe90ea8c4 100644 --- a/src/batou/main.py +++ b/src/batou/main.py @@ -76,6 +76,15 @@ def main(args: Optional[list] = None) -> None: help="Only predict what updates would happen. " "Do not change anything.", ) + p.add_argument( + "-L", + "--local", + action="store_true", + dest="check_and_predict_local", + help="When running in consistency-only or predict-only mode, " + "do not connect to the remote host, but check and predict " + "using the local host's state.", + ) p.add_argument( "-j", "--jobs", diff --git a/src/batou/tests/test_deploy.py b/src/batou/tests/test_deploy.py index 5703389fc..d47aa6a05 100644 --- a/src/batou/tests/test_deploy.py +++ b/src/batou/tests/test_deploy.py @@ -18,6 +18,7 @@ def test_main_with_errors(capsys): dirty=False, consistency_only=False, predict_only=False, + check_and_predict_local=False, jobs=None, provision_rebuild=False, ) @@ -81,6 +82,7 @@ def test_main_fails_if_no_host_in_environment(capsys): dirty=False, consistency_only=False, predict_only=False, + check_and_predict_local=False, jobs=None, provision_rebuild=False, ) diff --git a/src/batou/tests/test_endtoend.py b/src/batou/tests/test_endtoend.py index a412bd15f..737f2318b 100644 --- a/src/batou/tests/test_endtoend.py +++ b/src/batou/tests/test_endtoend.py @@ -372,3 +372,116 @@ def test_durations_are_shown_for_components(): ============================= DEPLOYMENT FINISHED ============================== """ ) + + +def test_check_consistency_works(): + os.chdir("examples/tutorial-secrets") + out, _ = cmd("./batou deploy tutorial --consistency-only") + assert out == Ellipsis( + """\ +batou/2... (cpython 3...) +================================== Preparing =================================== +main: Loading environment `tutorial`... +main: Verifying repository ... +main: Loading secrets ... +================== Connecting hosts and configuring model ... ================== +localhost: Connecting via local (1/1) +=================================== Summary ==================================== +Deployment took total=...s, connect=...s, deploy=NaN +========================== CONSISTENCY CHECK FINISHED ========================== +""" + ) + + +def test_predicting_deployment_works(): + os.chdir("examples/tutorial-secrets") + out, _ = cmd("./batou deploy tutorial --predict-only") + assert out == Ellipsis( + """\ +batou/2... (cpython 3...) +================================== Preparing =================================== +main: Loading environment `tutorial`... +main: Verifying repository ... +main: Loading secrets ... +================== Connecting hosts and configuring model ... ================== +localhost: Connecting via local (1/1) +======================== Predicting deployment actions ========================= +localhost: Scheduling component hello ... +localhost > Hello > File('work/hello/hello') > Presence('hello') +localhost > Hello > File('work/hello/hello') > Content('hello') +Not showing diff as it contains sensitive data, +see .../examples/tutorial-secrets/work/.batou-diffs/...diff for the diff. +localhost > Hello > File('work/hello/other-secrets.yaml') > Presence('other-secrets.yaml') +localhost > Hello > File('work/hello/other-secrets.yaml') > Content('other-secrets.yaml') +Not showing diff as it contains sensitive data, +see .../examples/tutorial-secrets/work/.batou-diffs/...diff for the diff. +=================================== Summary ==================================== +Deployment took total=...s, connect=...s, deploy=...s +======================== DEPLOYMENT PREDICTION FINISHED ======================== +""" + ) + + +def test_check_consistency_works_with_local(): + os.chdir("examples/tutorial-secrets") + out, _ = cmd("./batou deploy gocept --consistency-only --local") + assert out == Ellipsis( + """\ +batou/2... (cpython 3...) +================================== Preparing =================================== +main: Loading environment `gocept`... +main: Verifying repository ... +main: Loading secrets ... +================== Connecting hosts and configuring model ... ================== +test01: Connecting via local (1/2) +test02: Connecting via local (2/2) +=================================== Summary ==================================== +Deployment took total=...s, connect=...s, deploy=NaN +====================== CONSISTENCY CHECK (local) FINISHED ====================== +""" + ) + + +def test_predicting_deployment_works_with_local(): + os.chdir("examples/tutorial-secrets") + out, _ = cmd("./batou deploy gocept --predict-only --local") + assert out == Ellipsis( + """\ +batou/2... (cpython 3...) +================================== Preparing =================================== +main: Loading environment `gocept`... +main: Verifying repository ... +main: Loading secrets ... +================== Connecting hosts and configuring model ... ================== +test01: Connecting via local (1/2) +test02: Connecting via local (2/2) +======================== Predicting deployment actions ========================= +test01: Scheduling component hello ... +test02: Scheduling component hello ... +test01 > Hello > File('work/hello/hello') > Presence('hello') +test01 > Hello > File('work/hello/hello') > Content('hello') + hello --- + hello +++ + hello @@ -0,0 +1,2 @@ + hello +The magic word is None. + hello +The other word is None. +test01 > Hello > File('work/hello/other-secrets.yaml') > Presence('other-secrets.yaml') +test01 > Hello > File('work/hello/other-secrets.yaml') > Content('other-secrets.yaml') +Not showing diff as it contains sensitive data, +see .../batou/examples/tutorial-secrets/work/.batou-diffs/...diff for the diff. +test02 > Hello > File('work/hello/hello') > Presence('hello') +test02 > Hello > File('work/hello/hello') > Content('hello') + hello --- + hello +++ + hello @@ -0,0 +1,2 @@ + hello +The magic word is None. + hello +The other word is None. +test02 > Hello > File('work/hello/other-secrets.yaml') > Presence('other-secrets.yaml') +test02 > Hello > File('work/hello/other-secrets.yaml') > Content('other-secrets.yaml') +Not showing diff as it contains sensitive data, +see .../batou/examples/tutorial-secrets/work/.batou-diffs/...diff for the diff. +=================================== Summary ==================================== +Deployment took total=...s, connect=...s, deploy=...s +==================== DEPLOYMENT PREDICTION (local) FINISHED ==================== +""" + )