# This file is part of cloud-init. See LICENSE file for license information. from io import StringIO import pytest from cloudinit.cmd.devel import render from cloudinit.helpers import Paths from cloudinit.util import ensure_dir, write_file from tests.unittests.helpers import mock, skipUnlessJinja M_PATH = "cloudinit.cmd.devel.render." class TestRender: @pytest.fixture(autouse=True) def mocks(self, mocker): mocker.patch("sys.stderr", new_callable=StringIO) def test_error_on_missing_user_data(self, caplog, tmpdir): """When user_data file path does not exist, log an error.""" absent_file = tmpdir.join("user-data") instance_data = tmpdir.join("instance-data") write_file(instance_data, "{}") assert render.render_template(absent_file, instance_data, False) == 1 assert f"Missing user-data file: {absent_file}" in caplog.text def test_error_on_missing_instance_data(self, caplog, tmpdir): """When instance_data file path does not exist, log an error.""" user_data = tmpdir.join("user-data") absent_file = tmpdir.join("instance-data") assert render.render_template(user_data, absent_file, False) == 1 assert f"Missing instance-data.json file: {absent_file}" in caplog.text @mock.patch(f"{M_PATH}read_cfg_paths") def test_default_instance_data(self, m_paths, caplog, tmpdir): """When no instance_data argument, default to configured run_dir.""" user_data = tmpdir.join("user-data") run_dir = tmpdir.join("run_dir") ensure_dir(run_dir) paths = Paths({"run_dir": run_dir}) m_paths.return_value = paths assert render.render_template(user_data, None, False) == 1 json_file = paths.get_runpath("instance_data") msg = f"Missing instance-data.json file: {json_file}" assert msg in caplog.text @mock.patch(f"{M_PATH}read_cfg_paths") def test_root_fallback_from_sensitive_instance_data( self, m_paths, caplog, tmpdir ): """When root user defaults to sensitive.json.""" user_data = tmpdir.join("user-data") run_dir = tmpdir.join("run_dir") ensure_dir(run_dir) paths = Paths({"run_dir": run_dir}) m_paths.return_value = paths with mock.patch("os.getuid") as m_getuid: m_getuid.return_value = 0 assert render.render_template(user_data, None, False) == 1 json_file = paths.get_runpath("instance_data") json_sensitive = paths.get_runpath("instance_data_sensitive") assert ( f"Missing root-readable {json_sensitive}. " f"Using redacted {json_file}" in caplog.text ) assert f"Missing instance-data.json file: {json_file}" in caplog.text @mock.patch(f"{M_PATH}read_cfg_paths") def test_root_uses_sensitive_instance_data(self, m_paths, tmpdir): """When root user, and no instance-data arg, use sensitive.json.""" user_data = tmpdir.join("user-data") write_file(user_data, "##template: jinja\nrendering: {{ my_var }}") run_dir = tmpdir.join("run_dir") json_sensitive = Paths({"run_dir": run_dir}).get_runpath( "instance_data_sensitive" ) ensure_dir(run_dir) write_file(json_sensitive, '{"my-var": "jinja worked"}') m_paths.return_value = Paths({"run_dir": run_dir}) with mock.patch("sys.stdout", new_callable=StringIO) as m_stdout: with mock.patch("os.getuid") as m_getuid: m_getuid.return_value = 0 assert render.render_template(user_data, None, False) == 0 assert "rendering: jinja worked" in m_stdout.getvalue() @skipUnlessJinja() def test_renders_instance_data_vars_in_template(self, caplog, tmpdir): """If user_data file is a jinja template render instance-data vars.""" user_data = tmpdir.join("user-data") write_file(user_data, "##template: jinja\nrendering: {{ my_var }}") instance_data = tmpdir.join("instance-data") write_file(instance_data, '{"my-var": "jinja worked"}') with mock.patch("sys.stdout", new_callable=StringIO) as m_stdout: assert render.render_template(user_data, instance_data, True) == 0 # Make sure the log is correctly captured. There is an issue # with this fixture in pytest==4.6.9 (focal): assert ( "Converted jinja variables\n{" in caplog.records[-1].getMessage() ) assert "rendering: jinja worked" == m_stdout.getvalue() @skipUnlessJinja() def test_render_warns_and_gives_up_on_invalid_jinja_operation( self, caplog, tmpdir ): """If user_data file has invalid jinja operations log warnings.""" user_data = tmpdir.join("user-data") write_file(user_data, "##template: jinja\nrendering: {{ my-var }}") instance_data = tmpdir.join("instance-data") write_file(instance_data, '{"my-var": "jinja worked"}') assert render.render_template(user_data, instance_data, True) == 1 assert ( "Ignoring jinja template for %s: Undefined jinja" ' variable: "my-var". Jinja tried subtraction. Perhaps you meant' ' "my_var"?' % user_data ) in caplog.text @skipUnlessJinja() def test_jinja_load_error(self, caplog, tmpdir): user_data = tmpdir.join("user-data") write_file(user_data, "##template: jinja\nrendering: {{ my-var }}") instance_data = tmpdir.join("instance-data") write_file(instance_data, '{"my-var": "jinja failed"') render.render_template(user_data, instance_data, False) assert ( "Cannot render from instance data due to exception" in caplog.text ) @skipUnlessJinja() def test_not_jinja_error(self, caplog, tmpdir): user_data = tmpdir.join("user-data") write_file(user_data, "{{ my-var }}") instance_data = tmpdir.join("instance-data") write_file(instance_data, '{"my-var": "jinja worked"}') render.render_template(user_data, instance_data, False) assert ( "Cannot render from instance data due to exception" in caplog.text ) @skipUnlessJinja() def test_no_user_data(self, caplog, tmpdir): user_data = tmpdir.join("user-data") write_file(user_data, "##template: jinja") instance_data = tmpdir.join("instance-data") write_file(instance_data, '{"my-var": "jinja worked"}') render.render_template(user_data, instance_data, False) assert "Unable to render user-data file" in caplog.text