def test_content_large_diff_logged(output, root): path = "path" p = Content(path, content="\n".join(["asdf"] * 21)) p._max_diff = 20 p._max_diff_lead = 5 root.component += p with open(p.path, "w") as f: f.write("\n".join(["bsdf"] * 21)) root.component.deploy() log = os.listdir(p.diff_dir)[0] with open(os.path.join(p.diff_dir, log)) as f: assert (f.read() == """\ --- +++ @@ -1,21 +1,21 @@ """ + "\n".join(["-bsdf"] * 21) + "\n" + "\n".join(["+asdf"] * 21) + "\n") assert output.backend.output == Ellipsis("""\ host > MyComponent > Content('work/mycomponent/path') More than 20 lines of diff. Showing first and last 5 lines. see ... for the full diff. path --- path +++ path @@ -1,21 +1,21 @@ path -bsdf path -bsdf path ... path +asdf path +asdf path +asdf path +asdf path +asdf """)
def test_yaml_diff(root): from batou import output from batou._output import TestBackend output.backend = TestBackend() p = YAMLContent("target.yaml", data={"asdf": 1, "bsdf": 2}) root.component += p with open(p.path, "w") as f: assert f.write(json.dumps({"bsdf": 2}, sort_keys=True, indent=4)) p.deploy() # fmt: off assert output.backend.output == Ellipsis("""\ host > MyComponent > YAMLContent(\'work/mycomponent/target.yaml\') target.yaml --- target.yaml +++ target.yaml @@ -1,3 +1,2 @@ target.yaml -{ target.yaml - "bsdf": 2 target.yaml -} target.yaml +asdf: 1 target.yaml +bsdf: 2 """)
def test_example_errors_late(): os.chdir("examples/errors2") out, _ = cmd("./batou deploy errors", acceptable_returncodes=[1]) assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================== main: Loading environment `errors`... main: Verifying repository ... main: Loading secrets ... ================================ Connecting ... ================================ localhost: Connecting via local (1/1) ============================ Configuring model ... ============================= ERROR: Trying to access address family IPv6 which is not configured for localhost:22. Hint: Use `require_v6=True` when instantiating the Address object. ERROR: crontab@localhost: No cron jobs found. ERROR: Failed override attribute conversion Host: localhost Attribute: Component1.do_what_is_needed Conversion: convert_literal('false') Error: malformed node or string: <...Name object at 0x...> ERROR: Failed override attribute conversion Host: localhost Attribute: DNSProblem.attribute_with_problem Conversion: Address('localhost') Error: Need port for service address. ERROR: Failed override attribute conversion Host: localhost Attribute: FileMode > File('work/filemode/new-file.txt') > Mode('new-file.txt').mode Conversion: convert_mode('wrongmode') Error: Mode-string should be between `---------` and `rwxrwxrwx`. ERROR: Overrides for undefined attributes Host: localhost Component: Component2 Attributes: this_does_not_exist, this_also_does_not_exist ERROR: Unused provided resources Resource "backend" provided by component3 with value ['192.168.0.1'] Resource "frontend" provided by component3 with value ['test00.gocept.net'] Resource "sub" provided by component3 with value [<SubComponent (localhost) "Component3 > SubComponent('sub sub')">] ERROR: Unsatisfied resource requirements Resource "application" required by component4 ERROR: Found dependency cycle cycle1 depends on cycle2 cycle2 depends on cycle1 ERROR: 8 remaining unconfigured component(s) ======================= 10 ERRORS - CONFIGURATION FAILED ======================= ===================== DEPLOYMENT FAILED (during configure) ===================== """) # noqa: E501 line too long
def test_example_ignores(): os.chdir('examples/ignores') out, _ = cmd('./batou deploy ignores') assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================\ == main: Loading environment `ignores`... main: Verifying repository ... main: Loading secrets ... ================================ Connecting ... ==============================\ == localhost: Connecting via local (1/2) otherhost: Connection ignored (2/2) ============================ Configuring model ... ===========================\ == ==================== Waiting for remaining connections ... ===================\ == ================================== Deploying =================================\ == localhost: Scheduling component component1 ... localhost: Skipping component fail ... (Component ignored) otherhost: Skipping component fail2 ... (Host ignored) ============================= DEPLOYMENT FINISHED ============================\ == """)
def test_example_errors(): os.chdir('examples/errors') out, _ = cmd('./batou deploy errors', acceptable_returncodes=[1]) assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================\ == main: Loading environment `errors`... main: Verifying repository ... main: Loading secrets ... ================================ Connecting ... ==============================\ == localhost: Connecting via local (1/1) ============================ Configuring model ... ===========================\ == ERROR: Failed loading component file File: .../component5/component.py Exception: invalid syntax (component.py, line 1) ERROR: Failed loading component file File: .../component6/component.py Exception: No module named 'asdf' ERROR: Missing component Component: missingcomponent Host: localhost ERROR: Superfluous section in environment configuration Section: superfluoussection ERROR: Override section for unknown component found Component: nonexisting-component-section ERROR: crontab@localhost: No cron jobs found. ERROR: Failed override attribute conversion Host: localhost Attribute: Component1.do_what_is_needed Conversion: convert_literal('false') Error: malformed node or string: <_ast.Name object at 0x...> ERROR: Overrides for undefined attributes Host: localhost Component: Component2 Attributes: this_does_not_exist ERROR: Failed override attribute conversion Host: localhost Attribute: DNSProblem.attribute_with_problem Conversion: Address('localhost') Error: Need port for service address. ERROR: Unused provided resources Resource "backend" provided by component3 with value ['192.168.0.1'] Resource "frontend" provided by component3 with value ['test00.gocept.net'] Resource "sub" provided by component3 with value [<SubComponent (localhost) "Component3 > SubComponent(sub sub)">] ERROR: Unsatisfied resource requirements Resource "application" required by component4 ERROR: Found dependency cycle cycle1 depends on cycle2 cycle2 depends on cycle1 ERROR: 6 remaining unconfigured component(s) ======================= 13 ERRORS - CONFIGURATION FAILED =====================\ == ============================== DEPLOYMENT FAILED =============================\ == """) # NOQA
def test_json_diff(root): from batou import output from batou._output import TestBackend output.backend = TestBackend() p = JSONContent("target.json", data={"asdf": 1, "bsdf": 2}) root.component += p with open(p.path, "w") as f: assert f.write(json.dumps({"bsdf": 2}, sort_keys=True, indent=4)) p.deploy() assert output.backend.output == Ellipsis( """\ host > MyComponent > JSONContent('work/mycomponent/target.json') target.json --- target.json +++ target.json @@ -1,3 +1,4 @@ target.json { target.json + "asdf": 1, target.json "bsdf": 2 target.json } """ )
def test_main_with_errors(capsys): os.chdir("examples/errors") from batou.deploy import main with pytest.raises(SystemExit) as r: main(environment='errors', platform=None, timeout=None, dirty=False, consistency_only=False, predict_only=False, jobs=None, provision_rebuild=False) assert r.value.code == 1 out, err = capsys.readouterr() assert err == '' assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================== main: Loading environment `errors`... main: Verifying repository ... main: Loading secrets ... ERROR: Failed loading component file File: .../examples/errors/components/component5/component.py Exception: invalid syntax (component.py, line 1) ERROR: Failed loading component file File: .../examples/errors/components/component6/component.py Exception: No module named 'asdf' ERROR: Failed loading component file File: .../examples/errors/components/component7/component.py Exception: Attributes only support one of those parameters: either `default` or `default_conf_string`. ERROR: Missing component Component: component7 Host: localhost ERROR: Missing component Component: missingcomponent Host: localhost ERROR: Superfluous section in environment configuration Section: superfluoussection ERROR: Override section for unknown component found Component: nonexisting-component-section ERROR: Attribute override found both in environment and secrets Component: component1 Attribute: my_attribute ERROR: Secrets section for unknown component found Component: another-nonexisting-component-section ======================= DEPLOYMENT FAILED (during load) ======================== """) # noqa: E501 line too long
def test_example_errors_early(): os.chdir("examples/errors") out, _ = cmd("./batou deploy errors", acceptable_returncodes=[1]) assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================== main: Loading environment `errors`... main: Verifying repository ... main: Loading secrets ... ERROR: Failed loading component file File: .../examples/errors/components/component5/component.py Exception: invalid syntax (component.py, line 1) ERROR: Failed loading component file File: .../examples/errors/components/component6/component.py Exception: No module named 'asdf' ERROR: Missing component Component: missingcomponent Host: localhost ERROR: Superfluous section in environment configuration Section: superfluoussection ERROR: Override section for unknown component found Component: nonexisting-component-section ERROR: Secrets section for unknown component found Component: another-nonexisting-component-section ======================= DEPLOYMENT FAILED (during load) ======================== """)
def test_example_errors_late(): os.chdir("examples/errors2") out, _ = cmd("./batou deploy errors", acceptable_returncodes=[1]) assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================\ == main: Loading environment `errors`... main: Verifying repository ... main: Loading secrets ... ================================ Connecting ... ==============================\ == localhost: Connecting via local (1/1) ============================ Configuring model ... ===========================\ == ERROR: crontab@localhost: No cron jobs found. ERROR: Failed override attribute conversion Host: localhost Attribute: Component1.do_what_is_needed Conversion: convert_literal('false') Error: malformed node or string: <...Name object at 0x...> ERROR: Failed override attribute conversion Host: localhost Attribute: DNSProblem.attribute_with_problem Conversion: Address('localhost') Error: Need port for service address. ERROR: Overrides for undefined attributes Host: localhost Component: Component2 Attributes: this_does_not_exist, this_also_does_not_exist ERROR: Unused provided resources Resource "backend" provided by component3 with value ['192.168.0.1'] Resource "frontend" provided by component3 with value ['test00.gocept.net'] Resource "sub" provided by component3 with value [<SubComponent (localhost) "Component3 > SubComponent('sub sub')">] ERROR: Unsatisfied resource requirements Resource "application" required by component4 ERROR: Found dependency cycle cycle1 depends on cycle2 cycle2 depends on cycle1 ERROR: 6 remaining unconfigured component(s) ======================= 8 ERRORS - CONFIGURATION FAILED ======================\ == ===================== DEPLOYMENT FAILED (during configure) ===================\ == """) # NOQA
def test_example_errors_missing_environment(): os.chdir("examples/errors") out, _ = cmd("./batou deploy production", acceptable_returncodes=[1]) assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================== main: Loading environment `production`... ERROR: Missing environment Environment: production ======================= DEPLOYMENT FAILED (during load) ======================== """) # noqa: E501 line too long
def test_example_errors_missing_environment(): os.chdir('examples/errors') out, _ = cmd('./batou deploy production', acceptable_returncodes=[1]) assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================\ == main: Loading environment `production`... ERROR: Missing environment Environment: production ============================== DEPLOYMENT FAILED =============================\ == """) # NOQA
def test_yaml_diff_not_for_sensitive(output, root): p = YAMLContent("target.yaml", data={ "asdf": 1, "bsdf": 2 }, sensitive_data=True) root.component += p with open(p.path, "w") as f: assert f.write(json.dumps({"bsdf": 2}, sort_keys=True, indent=4)) p.deploy() assert output.backend.output == Ellipsis("""\ host > MyComponent > YAMLContent('work/mycomponent/target.yaml') Not showing diff as it contains sensitive data. """)
def test_example_errors_gpg_cannot_decrypt(monkeypatch): monkeypatch.setitem(os.environ, "GNUPGHOME", '') os.chdir("examples/errors") out, _ = cmd("./batou deploy errors", acceptable_returncodes=[1]) assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================== main: Loading environment `errors`... main: Verifying repository ... main: Loading secrets ... ERROR: Error while calling GPG command: gpg --decrypt environments/errors/secrets.cfg exit code: 2 message: gpg: ... ... ERROR: Failed loading component file File: .../examples/errors/components/component5/component.py Exception: invalid syntax (component.py, line 1) ERROR: Failed loading component file File: .../examples/errors/components/component6/component.py Exception: No module named 'asdf' ERROR: Failed loading component file File: .../examples/errors/components/component7/component.py Exception: Attributes only support one of those parameters: either `default` or `default_conf_string`. ERROR: Missing component Component: component7 Host: localhost ERROR: Missing component Component: missingcomponent Host: localhost ERROR: Superfluous section in environment configuration Section: superfluoussection ERROR: Override section for unknown component found Component: nonexisting-component-section ======================= DEPLOYMENT FAILED (during load) ======================== """) # noqa: E501 line too long
def test_json_diff_not_for_sensitive(root): from batou import output from batou._output import TestBackend output.backend = TestBackend() p = JSONContent( "target.json", data={ "asdf": 1, "bsdf": 2}, sensitive_data=True) root.component += p with open(p.path, "w") as f: assert f.write(json.dumps({"bsdf": 2}, sort_keys=True, indent=4)) p.deploy() assert output.backend.output == Ellipsis("""\ host > MyComponent > JSONContent('work/mycomponent/target.json') Not showing diff as it contains sensitive data. """)
def test_diff_is_not_shown_for_keys_in_secrets(tmp_path, monkeypatch, capsys): """It does not render diffs for files which contain secrets. Secrets might be in the config file in secrets/ or additional encrypted files belonging to the environment. """ monkeypatch.chdir('examples/tutorial-secrets') if os.path.exists('work'): shutil.rmtree('work') try: out, _ = cmd("./batou deploy tutorial") finally: shutil.rmtree('work') assert out == Ellipsis("""\ batou/2... (cpython 3...) ================================== Preparing =================================== main: Loading environment `tutorial`... main: Verifying repository ... main: Loading secrets ... ================================ Connecting ... ================================ localhost: Connecting via local (1/1) ============================ Configuring model ... ============================= ==================== Waiting for remaining connections ... ===================== ================================== Deploying =================================== 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 ...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 ...diff for the diff. =================================== Summary ==================================== ============================= DEPLOYMENT FINISHED ============================== """) # noqa: E501 line too long