diff options
Diffstat (limited to 'tests/integration_tests/modules')
15 files changed, 913 insertions, 0 deletions
diff --git a/tests/integration_tests/modules/test_apt_configure_sources_list.py b/tests/integration_tests/modules/test_apt_configure_sources_list.py new file mode 100644 index 00000000..d2bcc61a --- /dev/null +++ b/tests/integration_tests/modules/test_apt_configure_sources_list.py @@ -0,0 +1,51 @@ +"""Integration test for the apt module's ``sources_list`` functionality. + +This test specifies a ``sources_list`` and then checks that (a) the expected +number of sources.list entries is present, and (b) that each expected line +appears in the file. + +(This is ported from +``tests/cloud_tests/testcases/modules/apt_configure_sources_list.yaml``.)""" +import re + +import pytest + + +USER_DATA = """\ +#cloud-config +apt: + primary: + - arches: [default] + uri: http://archive.ubuntu.com/ubuntu + security: + - arches: [default] + uri: http://security.ubuntu.com/ubuntu + sources_list: | + deb $MIRROR $RELEASE main restricted + deb-src $MIRROR $RELEASE main restricted + deb $PRIMARY $RELEASE universe restricted + deb-src $PRIMARY $RELEASE universe restricted + deb $SECURITY $RELEASE-security multiverse + deb-src $SECURITY $RELEASE-security multiverse +""" + +EXPECTED_REGEXES = [ + r"deb http://archive.ubuntu.com/ubuntu [a-z].* main restricted", + r"deb-src http://archive.ubuntu.com/ubuntu [a-z].* main restricted", + r"deb http://archive.ubuntu.com/ubuntu [a-z].* universe restricted", + r"deb-src http://archive.ubuntu.com/ubuntu [a-z].* universe restricted", + r"deb http://security.ubuntu.com/ubuntu [a-z].*security multiverse", + r"deb-src http://security.ubuntu.com/ubuntu [a-z].*security multiverse", +] + + +@pytest.mark.ci +class TestAptConfigureSourcesList: + + @pytest.mark.user_data(USER_DATA) + def test_sources_list(self, client): + sources_list = client.read_from_file("/etc/apt/sources.list") + assert 6 == len(sources_list.rstrip().split('\n')) + + for expected_re in EXPECTED_REGEXES: + assert re.search(expected_re, sources_list) is not None diff --git a/tests/integration_tests/modules/test_ntp_servers.py b/tests/integration_tests/modules/test_ntp_servers.py new file mode 100644 index 00000000..e72389c1 --- /dev/null +++ b/tests/integration_tests/modules/test_ntp_servers.py @@ -0,0 +1,58 @@ +"""Integration test for the ntp module's ``servers`` functionality with ntp. + +This test specifies the use of the `ntp` NTP client, and ensures that the given +NTP servers are configured as expected. + +(This is ported from ``tests/cloud_tests/testcases/modules/ntp_servers.yaml``.) +""" +import re + +import yaml +import pytest + +USER_DATA = """\ +#cloud-config +ntp: + ntp_client: ntp + servers: + - 172.16.15.14 + - 172.16.17.18 +""" + +EXPECTED_SERVERS = yaml.safe_load(USER_DATA)["ntp"]["servers"] + + +@pytest.mark.ci +@pytest.mark.user_data(USER_DATA) +class TestNtpServers: + + def test_ntp_installed(self, class_client): + """Test that `ntpd --version` succeeds, indicating installation.""" + result = class_client.execute("ntpd --version") + assert 0 == result.return_code + + def test_dist_config_file_is_empty(self, class_client): + """Test that the distributed config file is empty. + + (This test is skipped on all currently supported Ubuntu releases, so + may not actually be needed any longer.) + """ + if class_client.execute("test -e /etc/ntp.conf.dist").failed: + pytest.skip("/etc/ntp.conf.dist does not exist") + dist_file = class_client.read_from_file("/etc/ntp.conf.dist") + assert 0 == len(dist_file.strip().splitlines()) + + def test_ntp_entries(self, class_client): + ntp_conf = class_client.read_from_file("/etc/ntp.conf") + for expected_server in EXPECTED_SERVERS: + assert re.search( + r"^server {} iburst".format(expected_server), + ntp_conf, + re.MULTILINE + ) + + def test_ntpq_servers(self, class_client): + result = class_client.execute("ntpq -p -w -n") + assert result.ok + for expected_server in EXPECTED_SERVERS: + assert expected_server in result.stdout diff --git a/tests/integration_tests/modules/test_package_update_upgrade_install.py b/tests/integration_tests/modules/test_package_update_upgrade_install.py new file mode 100644 index 00000000..8a38ad84 --- /dev/null +++ b/tests/integration_tests/modules/test_package_update_upgrade_install.py @@ -0,0 +1,74 @@ +"""Integration test for the package update upgrade install module. + +This test module asserts that packages are upgraded/updated during boot +with the ``package_update_upgrade_install`` module. We are also testing +if we can install new packages during boot too. + +(This is ported from +``tests/cloud_tests/testcases/modules/package_update_upgrade_install.yaml``.) + +NOTE: the testcase for this looks for the command in history.log as + /usr/bin/apt-get..., which is not how it always appears. it should + instead look for just apt-get... +""" + +import re +import pytest + + +USER_DATA = """\ +#cloud-config +packages: + - sl + - tree +package_update: true +package_upgrade: true +""" + + +@pytest.mark.user_data(USER_DATA) +class TestPackageUpdateUpgradeInstall: + + def assert_package_installed(self, pkg_out, name, version=None): + """Check dpkg-query --show output for matching package name. + + @param name: package base name + @param version: string representing a package version or part of a + version. + """ + pkg_match = re.search( + "^%s\t(?P<version>.*)$" % name, pkg_out, re.MULTILINE) + if pkg_match: + installed_version = pkg_match.group("version") + if not version: + return # Success + if installed_version.startswith(version): + return # Success + raise AssertionError( + "Expected package version %s-%s not found. Found %s" % + name, version, installed_version) + raise AssertionError("Package not installed: %s" % name) + + def test_new_packages_are_installed(self, class_client): + pkg_out = class_client.execute("dpkg-query --show") + + self.assert_package_installed(pkg_out, "sl") + self.assert_package_installed(pkg_out, "tree") + + def test_packages_were_updated(self, class_client): + out = class_client.execute( + "grep ^Commandline: /var/log/apt/history.log") + assert ( + "Commandline: /usr/bin/apt-get --option=Dpkg::Options" + "::=--force-confold --option=Dpkg::options::=--force-unsafe-io " + "--assume-yes --quiet install sl tree") in out + + def test_packages_were_upgraded(self, class_client): + """Test cloud-init-output for install & upgrade stuff.""" + out = class_client.read_from_file("/var/log/cloud-init-output.log") + assert "Setting up tree (" in out + assert "Setting up sl (" in out + assert "Reading package lists..." in out + assert "Building dependency tree..." in out + assert "Reading state information..." in out + assert "Calculating upgrade..." in out diff --git a/tests/integration_tests/modules/test_runcmd.py b/tests/integration_tests/modules/test_runcmd.py new file mode 100644 index 00000000..50d1851e --- /dev/null +++ b/tests/integration_tests/modules/test_runcmd.py @@ -0,0 +1,25 @@ +"""Integration test for the runcmd module. + +This test specifies a command to be executed by the ``runcmd`` module +and then checks if that command was executed during boot. + +(This is ported from +``tests/cloud_tests/testcases/modules/runcmd.yaml``.)""" + +import pytest + + +USER_DATA = """\ +#cloud-config +runcmd: + - echo cloud-init run cmd test > /var/tmp/run_cmd +""" + + +@pytest.mark.ci +class TestRuncmd: + + @pytest.mark.user_data(USER_DATA) + def test_runcmd(self, client): + runcmd_output = client.read_from_file("/var/tmp/run_cmd") + assert runcmd_output.strip() == "cloud-init run cmd test" diff --git a/tests/integration_tests/modules/test_seed_random_data.py b/tests/integration_tests/modules/test_seed_random_data.py new file mode 100644 index 00000000..b365fa98 --- /dev/null +++ b/tests/integration_tests/modules/test_seed_random_data.py @@ -0,0 +1,28 @@ +"""Integration test for the random seed module. + +This test specifies a command to be executed by the ``seed_random`` module, by +providing a different data to be used as seed data. We will then check +if that seed data was actually used. + +(This is ported from +``tests/cloud_tests/testcases/modules/seed_random_data.yaml``.)""" + +import pytest + + +USER_DATA = """\ +#cloud-config +random_seed: + data: 'MYUb34023nD:LFDK10913jk;dfnk:Df' + encoding: raw + file: /root/seed +""" + + +@pytest.mark.ci +class TestSeedRandomData: + + @pytest.mark.user_data(USER_DATA) + def test_seed_random_data(self, client): + seed_output = client.read_from_file("/root/seed") + assert seed_output.strip() == "MYUb34023nD:LFDK10913jk;dfnk:Df" diff --git a/tests/integration_tests/modules/test_set_hostname.py b/tests/integration_tests/modules/test_set_hostname.py new file mode 100644 index 00000000..2bfa403d --- /dev/null +++ b/tests/integration_tests/modules/test_set_hostname.py @@ -0,0 +1,47 @@ +"""Integration test for the set_hostname module. + +This module specify two tests: One updates only the hostname and the other +one updates the hostname and fqdn of the system. For both of these tests +we will check is the changes requested by the user data are being respected +after the system is boot. + +(This is ported from +``tests/cloud_tests/testcases/modules/set_hostname.yaml`` and +``tests/cloud_tests/testcases/modules/set_hostname_fqdn.yaml``.)""" + +import pytest + + +USER_DATA_HOSTNAME = """\ +#cloud-config +hostname: cloudinit2 +""" + +USER_DATA_FQDN = """\ +#cloud-config +manage_etc_hosts: true +hostname: cloudinit1 +fqdn: cloudinit2.i9n.cloud-init.io +""" + + +@pytest.mark.ci +class TestHostname: + + @pytest.mark.user_data(USER_DATA_HOSTNAME) + def test_hostname(self, client): + hostname_output = client.execute("hostname") + assert "cloudinit2" in hostname_output.strip() + + @pytest.mark.user_data(USER_DATA_FQDN) + def test_hostname_and_fqdn(self, client): + hostname_output = client.execute("hostname") + assert "cloudinit1" in hostname_output.strip() + + fqdn_output = client.execute("hostname --fqdn") + assert "cloudinit2.i9n.cloud-init.io" in fqdn_output.strip() + + host_output = client.execute("grep ^127 /etc/hosts") + assert '127.0.1.1 {} {}'.format( + fqdn_output, hostname_output) in host_output + assert '127.0.0.1 localhost' in host_output diff --git a/tests/integration_tests/modules/test_set_password.py b/tests/integration_tests/modules/test_set_password.py new file mode 100644 index 00000000..b13f76fb --- /dev/null +++ b/tests/integration_tests/modules/test_set_password.py @@ -0,0 +1,151 @@ +"""Integration test for the set_password module. + +This test specifies a combination of user/password pairs, and ensures that the +system has the correct passwords set. + +There are two tests run here: one tests chpasswd's list being a YAML list, the +other tests chpasswd's list being a string. Both expect the same results, so +they use a mixin to share their test definitions, because we can (of course) +only specify one user-data per instance. +""" +import crypt + +import pytest +import yaml + + +COMMON_USER_DATA = """\ +#cloud-config +ssh_pwauth: yes +users: + - default + - name: tom + # md5 gotomgo + passwd: "$1$S7$tT1BEDIYrczeryDQJfdPe0" + lock_passwd: false + - name: dick + # md5 gocubsgo + passwd: "$1$ssisyfpf$YqvuJLfrrW6Cg/l53Pi1n1" + lock_passwd: false + - name: harry + # sha512 goharrygo + passwd: "$6$LF$9Z2p6rWK6TNC1DC6393ec0As.18KRAvKDbfsGJEdWN3sRQRwpdfoh37EQ3y\ +Uh69tP4GSrGW5XKHxMLiKowJgm/" + lock_passwd: false + - name: jane + # sha256 gojanego + passwd: "$5$iW$XsxmWCdpwIW8Yhv.Jn/R3uk6A4UaicfW5Xp7C9p9pg." + lock_passwd: false + - name: "mikey" + lock_passwd: false +""" + +LIST_USER_DATA = COMMON_USER_DATA + """ +chpasswd: + list: + - tom:mypassword123! + - dick:RANDOM + - harry:RANDOM + - mikey:$5$xZ$B2YGGEx2AOf4PeW48KC6.QyT1W2B4rZ9Qbltudtha89 +""" + +STRING_USER_DATA = COMMON_USER_DATA + """ +chpasswd: + list: | + tom:mypassword123! + dick:RANDOM + harry:RANDOM + mikey:$5$xZ$B2YGGEx2AOf4PeW48KC6.QyT1W2B4rZ9Qbltudtha89 +""" + +USERS_DICTS = yaml.safe_load(COMMON_USER_DATA)["users"] +USERS_PASSWD_VALUES = { + user_dict["name"]: user_dict["passwd"] + for user_dict in USERS_DICTS + if "name" in user_dict and "passwd" in user_dict +} + + +class Mixin: + """Shared test definitions.""" + + def _fetch_and_parse_etc_shadow(self, class_client): + """Fetch /etc/shadow and parse it into Python data structures + + Returns: ({user: password}, [duplicate, users]) + """ + shadow_content = class_client.read_from_file("/etc/shadow") + users = {} + dupes = [] + for line in shadow_content.splitlines(): + user, encpw = line.split(":")[0:2] + if user in users: + dupes.append(user) + users[user] = encpw + return users, dupes + + def test_no_duplicate_users_in_shadow(self, class_client): + """Confirm that set_passwords has not added duplicate shadow entries""" + _, dupes = self._fetch_and_parse_etc_shadow(class_client) + + assert [] == dupes + + def test_password_in_users_dict_set_correctly(self, class_client): + """Test that the password specified in the users dict is set.""" + shadow_users, _ = self._fetch_and_parse_etc_shadow(class_client) + assert USERS_PASSWD_VALUES["jane"] == shadow_users["jane"] + + def test_password_in_chpasswd_list_set_correctly(self, class_client): + """Test that a chpasswd password overrides one in the users dict.""" + shadow_users, _ = self._fetch_and_parse_etc_shadow(class_client) + mikey_hash = "$5$xZ$B2YGGEx2AOf4PeW48KC6.QyT1W2B4rZ9Qbltudtha89" + assert mikey_hash == shadow_users["mikey"] + + def test_random_passwords_set_correctly(self, class_client): + """Test that RANDOM chpasswd entries replace users dict passwords.""" + shadow_users, _ = self._fetch_and_parse_etc_shadow(class_client) + + # These should have been changed + assert shadow_users["harry"] != USERS_PASSWD_VALUES["harry"] + assert shadow_users["dick"] != USERS_PASSWD_VALUES["dick"] + + # To random passwords + assert shadow_users["harry"].startswith("$") + assert shadow_users["dick"].startswith("$") + + # Which are not the same + assert shadow_users["harry"] != shadow_users["dick"] + + def test_explicit_password_set_correctly(self, class_client): + """Test that an explicitly-specified password is set correctly.""" + shadow_users, _ = self._fetch_and_parse_etc_shadow(class_client) + + fmt_and_salt = shadow_users["tom"].rsplit("$", 1)[0] + expected_value = crypt.crypt("mypassword123!", fmt_and_salt) + + assert expected_value == shadow_users["tom"] + + def test_shadow_expected_users(self, class_client): + """Test that the right set of users is in /etc/shadow.""" + shadow = class_client.read_from_file("/etc/shadow") + for user_dict in USERS_DICTS: + if "name" in user_dict: + assert "{}:".format(user_dict["name"]) in shadow + + def test_sshd_config(self, class_client): + """Test that SSH password auth is enabled.""" + sshd_config = class_client.read_from_file("/etc/ssh/sshd_config") + # We look for the exact line match, to avoid a commented line matching + assert "PasswordAuthentication yes" in sshd_config.splitlines() + + +@pytest.mark.ci +@pytest.mark.user_data(LIST_USER_DATA) +class TestPasswordList(Mixin): + """Launch an instance with LIST_USER_DATA, ensure Mixin tests pass.""" + + +@pytest.mark.ci +@pytest.mark.user_data(STRING_USER_DATA) +class TestPasswordListString(Mixin): + """Launch an instance with STRING_USER_DATA, ensure Mixin tests pass.""" diff --git a/tests/integration_tests/modules/test_snap.py b/tests/integration_tests/modules/test_snap.py new file mode 100644 index 00000000..b626f6b0 --- /dev/null +++ b/tests/integration_tests/modules/test_snap.py @@ -0,0 +1,29 @@ +"""Integration test for the snap module. + +This test specifies a command to be executed by the ``snap`` module +and then checks that if that command was executed during boot. + +(This is ported from +``tests/cloud_tests/testcases/modules/runcmd.yaml``.)""" + +import pytest + + +USER_DATA = """\ +#cloud-config +package_update: true +snap: + squashfuse_in_container: true + commands: + - snap install hello-world +""" + + +@pytest.mark.ci +class TestSnap: + + @pytest.mark.user_data(USER_DATA) + def test_snap(self, client): + snap_output = client.execute("snap list") + assert "core " in snap_output + assert "hello-world " in snap_output diff --git a/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py b/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py new file mode 100644 index 00000000..b9b0d85e --- /dev/null +++ b/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py @@ -0,0 +1,48 @@ +"""Integration test for the ssh_authkey_fingerprints module. + +This modules specifies two tests regarding the ``ssh_authkey_fingerprints`` +module. The first one verifies that we can disable the module behavior while +the second one verifies if the module is working as expected if enabled. + +(This is ported from +``tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_disable.yaml``, +``tests/cloud_tests/testcases/modules/ssh_auth_key_fingerprints_enable.yaml``. +)""" +import re + +import pytest + + +USER_DATA_SSH_AUTHKEY_DISABLE = """\ +#cloud-config +no_ssh_fingerprints: true +""" + +USER_DATA_SSH_AUTHKEY_ENABLE="""\ +#cloud-config +ssh_genkeytypes: + - ecdsa + - ed25519 +ssh_authorized_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDXW9Gg5H7ehjdSc6qDzwNtgCy94XYHhEYlXZMO2+FJrH3wfHGiMfCwOHxcOMt2QiXItULthdeQWS9QjBSSjVRXf6731igFrqPFyS9qBlOQ5D29C4HBXFnQggGVpBNJ82IRJv7szbbe/vpgLBP4kttUza9Dr4e1YM1ln4PRnjfXea6T0m+m1ixNb5432pTXlqYOnNOxSIm1gHgMLxPuDrJvQERDKrSiKSjIdyC9Jd8t2e1tkNLY0stmckVRbhShmcJvlyofHWbc2Ca1mmtP7MlS1VQnfLkvU1IrFwkmaQmaggX6WR6coRJ6XFXdWcq/AI2K6GjSnl1dnnCxE8VCEXBlXgFzad+PMSG4yiL5j8Oo1ZVpkTdgBnw4okGqTYCXyZg6X00As9IBNQfZMFlQXlIo4FiWgj3CO5QHQOyOX6FuEumaU13GnERrSSdp9tCs1Qm3/DG2RSCQBWTfcgMcStIvKqvJ3IjFn0vGLvI3Ampnq9q1SHwmmzAPSdzcMA76HyMUA5VWaBvWHlUxzIM6unxZASnwvuCzpywSEB5J2OF+p6H+cStJwQ32XwmOG8pLp1srlVWpqZI58Du/lzrkPqONphoZx0LDV86w7RUz1ksDzAdcm0tvmNRFMN1a0frDs506oA3aWK0oDk4Nmvk8sXGTYYw3iQSkOvDUUlIsqdaO+w== +""" # noqa + + +@pytest.mark.ci +class TestSshAuthkeyFingerprints: + + @pytest.mark.user_data(USER_DATA_SSH_AUTHKEY_DISABLE) + def test_ssh_authkey_fingerprints_disable(self, client): + cloudinit_output = client.read_from_file("/var/log/cloud-init.log") + assert ( + "Skipping module named ssh-authkey-fingerprints, " + "logging of SSH fingerprints disabled") in cloudinit_output + + @pytest.mark.user_data(USER_DATA_SSH_AUTHKEY_ENABLE) + def test_ssh_authkey_fingerprints_enable(self, client): + syslog_output = client.read_from_file("/var/log/syslog") + + assert re.search(r'256 SHA256:.*(ECDSA)', syslog_output) is not None + assert re.search(r'256 SHA256:.*(ED25519)', syslog_output) is not None + assert re.search(r'1024 SHA256:.*(DSA)', syslog_output) is None + assert re.search(r'2048 SHA256:.*(RSA)', syslog_output) is None diff --git a/tests/integration_tests/modules/test_ssh_generate.py b/tests/integration_tests/modules/test_ssh_generate.py new file mode 100644 index 00000000..60c36982 --- /dev/null +++ b/tests/integration_tests/modules/test_ssh_generate.py @@ -0,0 +1,51 @@ +"""Integration test for the ssh module. + +This module has two tests to verify if we can create ssh keys +through the ``ssh`` module. The first test asserts that some keys +were not created while the second one verifies if the expected +keys were created. + +(This is ported from +``tests/cloud_tests/testcases/modules/ssh_keys_generate.yaml``.)""" + +import pytest + + +USER_DATA = """\ +#cloud-config +ssh_genkeytypes: + - ecdsa + - ed25519 +authkey_hash: sha512 +""" + + +@pytest.mark.ci +@pytest.mark.user_data(USER_DATA) +class TestSshKeysGenerate: + + @pytest.mark.parametrize( + "ssh_key_path", ( + "/etc/ssh/ssh_host_dsa_key.pub", + "/etc/ssh/ssh_host_dsa_key", + "/etc/ssh/ssh_host_rsa_key.pub", + "/etc/ssh/ssh_host_rsa_key", + ) + ) + def test_ssh_keys_not_generated(self, ssh_key_path, class_client): + out = class_client.execute( + "test -e {}".format(ssh_key_path) + ) + assert out.failed + + @pytest.mark.parametrize( + "ssh_key_path", ( + "/etc/ssh/ssh_host_ecdsa_key.pub", + "/etc/ssh/ssh_host_ecdsa_key", + "/etc/ssh/ssh_host_ed25519_key.pub", + "/etc/ssh/ssh_host_ed25519_key", + ) + ) + def test_ssh_keys_generated(self, ssh_key_path, class_client): + out = class_client.read_from_file(ssh_key_path) + assert "" != out.strip() diff --git a/tests/integration_tests/modules/test_ssh_import_id.py b/tests/integration_tests/modules/test_ssh_import_id.py new file mode 100644 index 00000000..45d37d6c --- /dev/null +++ b/tests/integration_tests/modules/test_ssh_import_id.py @@ -0,0 +1,29 @@ +"""Integration test for the ssh_import_id module. + +This test specifies ssh keys to be imported by the ``ssh_import_id`` module +and then checks that if the ssh keys were successfully imported. + +(This is ported from +``tests/cloud_tests/testcases/modules/ssh_import_id.yaml``.)""" + +import pytest + + +USER_DATA = """\ +#cloud-config +ssh_import_id: + - gh:powersj + - lp:smoser +""" + + +@pytest.mark.ci +class TestSshImportId: + + @pytest.mark.user_data(USER_DATA) + def test_ssh_import_id(self, client): + ssh_output = client.read_from_file( + "/home/ubuntu/.ssh/authorized_keys") + + assert '# ssh-import-id gh:powersj' in ssh_output + assert '# ssh-import-id lp:smoser' in ssh_output diff --git a/tests/integration_tests/modules/test_ssh_keys_provided.py b/tests/integration_tests/modules/test_ssh_keys_provided.py new file mode 100644 index 00000000..27d193c1 --- /dev/null +++ b/tests/integration_tests/modules/test_ssh_keys_provided.py @@ -0,0 +1,148 @@ +"""Integration test for the ssh module. + +This test specifies keys to be provided to the system through the ``ssh`` +module and then checks that if those keys were successfully added to the +system. + +(This is ported from +``tests/cloud_tests/testcases/modules/ssh_keys_provided.yaml''``.)""" + +import pytest + + +USER_DATA = """\ +#cloud-config +disable_root: false +ssh_authorized_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDXW9Gg5H7ehjdSc6qDzwNtgCy94XYHhEYlXZMO2+FJrH3wfHGiMfCwOHxcOMt2QiXItULthdeQWS9QjBSSjVRXf6731igFrqPFyS9qBlOQ5D29C4HBXFnQggGVpBNJ82IRJv7szbbe/vpgLBP4kttUza9Dr4e1YM1ln4PRnjfXea6T0m+m1ixNb5432pTXlqYOnNOxSIm1gHgMLxPuDrJvQERDKrSiKSjIdyC9Jd8t2e1tkNLY0stmckVRbhShmcJvlyofHWbc2Ca1mmtP7MlS1VQnfLkvU1IrFwkmaQmaggX6WR6coRJ6XFXdWcq/AI2K6GjSnl1dnnCxE8VCEXBlXgFzad+PMSG4yiL5j8Oo1ZVpkTdgBnw4okGqTYCXyZg6X00As9IBNQfZMFlQXlIo4FiWgj3CO5QHQOyOX6FuEumaU13GnERrSSdp9tCs1Qm3/DG2RSCQBWTfcgMcStIvKqvJ3IjFn0vGLvI3Ampnq9q1SHwmmzAPSdzcMA76HyMUA5VWaBvWHlUxzIM6unxZASnwvuCzpywSEB5J2OF+p6H+cStJwQ32XwmOG8pLp1srlVWpqZI58Du/lzrkPqONphoZx0LDV86w7RUz1ksDzAdcm0tvmNRFMN1a0frDs506oA3aWK0oDk4Nmvk8sXGTYYw3iQSkOvDUUlIsqdaO+w== +ssh_keys: + rsa_private: | + -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEAtPx6PqN3iSEsnTtibyIEy52Tra8T5fn0ryXyg46Di2NBwdnj + o8trNv9jenfV/UhmePl58lXjT43wV8OCMl6KsYXyBdegM35NNtono4I4mLLKFMR9 + 9TOtDn6iYcaNenVhF3ZCj9Z2nNOlTrdc0uchHqKMrxLjCRCUrL91Uf+xioTF901Y + RM+ZqC5lT92yAL76F4qPF+Lq1QtUfNfUIwwvOp5ccDZLPxij0YvyBzubYye9hJHu + yjbJv78R4JHV+L2WhzSoX3W/6WrxVzeXqFGqH894ccOaC/7tnqSP6V8lIQ6fE2+c + DurJcpM3CJRgkndGHjtU55Y71YkcdLksSMvezQIDAQABAoIBAQCrU4IJP8dNeaj5 + IpkY6NQvR/jfZqfogYi+MKb1IHin/4rlDfUvPcY9pt8ttLlObjYK+OcWn3Vx/sRw + 4DOkqNiUGl80Zp1RgZNohHUXlJMtAbrIlAVEk+mTmg7vjfyp2unRQvLZpMRdywBm + lq95OrCghnG03aUsFJUZPpi5ydnwbA12ma+KHkG0EzaVlhA7X9N6z0K6U+zue2gl + goMLt/MH0rsYawkHrwiwXaIFQeyV4MJP0vmrZLbFk1bycu9X/xPtTYotWyWo4eKA + cb05uu04qwexkKHDM0KXtT0JecbTo2rOefFo8Uuab6uJY+fEHNocZ+v1vLA4aOxJ + ovp1JuXlAoGBAOWYNgKrlTfy5n0sKsNk+1RuL2jHJZJ3HMd0EIt7/fFQN3Fi08Hu + jtntqD30Wj+DJK8b8Lrt66FruxyEJm5VhVmwkukrLR5ige2f6ftZnoFCmdyy+0zP + dnPZSUe2H5ZPHa+qthJgHLn+al2P04tGh+1fGHC2PbP+e0Co+/ZRIOxrAoGBAMnN + IEen9/FRsqvnDd36I8XnJGskVRTZNjylxBmbKcuMWm+gNhOI7gsCAcqzD4BYZjjW + pLhrt/u9p+l4MOJy6OUUdM/okg12SnJEGryysOcVBcXyrvOfklWnANG4EAH5jt1N + ftTb1XTxzvWVuR/WJK0B5MZNYM71cumBdUDtPi+nAoGAYmoIXMSnxb+8xNL10aOr + h9ljQQp8NHgSQfyiSufvRk0YNuYh1vMnEIsqnsPrG2Zfhx/25GmvoxXGssaCorDN + 5FAn6QK06F1ZTD5L0Y3sv4OI6G1gAuC66ZWuL6sFhyyKkQ4f1WiVZ7SCa3CHQSAO + i9VDaKz1bf4bXvAQcNj9v9kCgYACSOZCqW4vN0OUmqsXhkt9ZB6Pb/veno70pNPR + jmYsvcwQU3oJQpWfXkhy6RAV3epaXmPDCsUsfns2M3wqNC7a2R5xdCqjKGGzZX4A + AO3rz9se4J6Gd5oKijeCKFlWDGNHsibrdgm2pz42nZlY+O21X74dWKbt8O16I1MW + hxkbJQKBgAXfuen/srVkJgPuqywUYag90VWCpHsuxdn+fZJa50SyZADr+RbiDfH2 + vek8Uo8ap8AEsv4Rfs9opUcUZevLp3g2741eOaidHVLm0l4iLIVl03otGOqvSzs+ + A3tFPEOxauXpzCt8f8eXsz0WQXAgIKW2h8zu5QHjomioU3i27mtE + -----END RSA PRIVATE KEY----- + rsa_public: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0/Ho+o3eJISydO2JvIgTLnZOtrxPl+fSvJfKDjoOLY0HB2eOjy2s2/2N6d9X9SGZ4+XnyVeNPjfBXw4IyXoqxhfIF16Azfk022iejgjiYssoUxH31M60OfqJhxo16dWEXdkKP1nac06VOt1zS5yEeooyvEuMJEJSsv3VR/7GKhMX3TVhEz5moLmVP3bIAvvoXio8X4urVC1R819QjDC86nlxwNks/GKPRi/IHO5tjJ72Eke7KNsm/vxHgkdX4vZaHNKhfdb/pavFXN5eoUaofz3hxw5oL/u2epI/pXyUhDp8Tb5wO6slykzcIlGCSd0YeO1TnljvViRx0uSxIy97N root@xenial-lxd + rsa_certificate: ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgMpgBP4Phn3L8I7Vqh7lmHKcOfIokEvSEbHDw83Y3JloAAAADAQABAAABAQC0/Ho+o3eJISydO2JvIgTLnZOtrxPl+fSvJfKDjoOLY0HB2eOjy2s2/2N6d9X9SGZ4+XnyVeNPjfBXw4IyXoqxhfIF16Azfk022iejgjiYssoUxH31M60OfqJhxo16dWEXdkKP1nac06VOt1zS5yEeooyvEuMJEJSsv3VR/7GKhMX3TVhEz5moLmVP3bIAvvoXio8X4urVC1R819QjDC86nlxwNks/GKPRi/IHO5tjJ72Eke7KNsm/vxHgkdX4vZaHNKhfdb/pavFXN5eoUaofz3hxw5oL/u2epI/pXyUhDp8Tb5wO6slykzcIlGCSd0YeO1TnljvViRx0uSxIy97NAAAAAAAAAAAAAAACAAAACnhlbmlhbC1seGQAAAAAAAAAAF+vVEIAAAAAYY83bgAAAAAAAAAAAAAAAAAAADMAAAALc3NoLWVkMjU1MTkAAAAgz4SlDwbq53ZrRsnS6ISdwxgFDRpnEX44K8jFmLpI9NAAAABTAAAAC3NzaC1lZDI1NTE5AAAAQMWpiRWKNMFvRX0g6OQOELMqDhtNBpkIN92IyO25qiY2oDSd1NyVme6XnGDFt8CS7z5NufV04doP4aacLOBbQww= root@xenial-lxd + dsa_private: | + -----BEGIN DSA PRIVATE KEY----- + MIIBuwIBAAKBgQD5Fstc23IVSDe6k4DNP8smPKuEWUvHDTOGaXrhOVAfzZ6+jklP + 55mzvC7jO53PWWC31hq10xBoWdev0WtcNF9Tv+4bAa1263y51Rqo4GI7xx+xic1d + mLqqfYijBT9k48J/1tV0cs1Wjs6FP/IJTD/kYVC930JjYQMi722lBnUxsQIVAL7i + z3fTGKTvSzvW0wQlwnYpS2QFAoGANp+KdyS9V93HgxGQEN1rlj/TSv/a3EVdCKtE + nQf55aPHxDAVDVw5JtRh4pZbbRV4oGRPc9KOdjo5BU28vSM3Lmhkb+UaaDXwHkgI + nK193o74DKjADWZxuLyyiKHiMOhxozoxDfjWxs8nz6uqvSW0pr521EwIY6RajbED + nZ2a3GkCgYEAyoUomNRB6bmpsIfzt8zdtqLP5umIj2uhr9MVPL8/QdbxmJ72Z7pf + Q2z1B7QAdIBGOlqJXtlau7ABhWK29Efe+99ObyTSSdDc6RCDeAwUmBAiPRQhDH2E + wExw3doDSCUb28L1B50wBzQ8mC3KXp6C7IkBXWspb16DLHUHFSI8bkICFA5kVUcW + nCPOXEQsayANi8+Cb7BH + -----END DSA PRIVATE KEY----- + dsa_public: ssh-dss AAAAB3NzaC1kc3MAAACBAPkWy1zbchVIN7qTgM0/yyY8q4RZS8cNM4ZpeuE5UB/Nnr6OSU/nmbO8LuM7nc9ZYLfWGrXTEGhZ16/Ra1w0X1O/7hsBrXbrfLnVGqjgYjvHH7GJzV2Yuqp9iKMFP2Tjwn/W1XRyzVaOzoU/8glMP+RhUL3fQmNhAyLvbaUGdTGxAAAAFQC+4s930xik70s71tMEJcJ2KUtkBQAAAIA2n4p3JL1X3ceDEZAQ3WuWP9NK/9rcRV0Iq0SdB/nlo8fEMBUNXDkm1GHillttFXigZE9z0o52OjkFTby9IzcuaGRv5RpoNfAeSAicrX3ejvgMqMANZnG4vLKIoeIw6HGjOjEN+NbGzyfPq6q9JbSmvnbUTAhjpFqNsQOdnZrcaQAAAIEAyoUomNRB6bmpsIfzt8zdtqLP5umIj2uhr9MVPL8/QdbxmJ72Z7pfQ2z1B7QAdIBGOlqJXtlau7ABhWK29Efe+99ObyTSSdDc6RCDeAwUmBAiPRQhDH2EwExw3doDSCUb28L1B50wBzQ8mC3KXp6C7IkBXWspb16DLHUHFSI8bkI= root@xenial-lxd + ed25519_private: | + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW + QyNTUxOQAAACDbnQGUruL42aVVsyHeaV5mYNTOhteXao0Nl5DVThJ2+QAAAJgwt+lcMLfp + XAAAAAtzc2gtZWQyNTUxOQAAACDbnQGUruL42aVVsyHeaV5mYNTOhteXao0Nl5DVThJ2+Q + AAAEDQlFZpz9q8+/YJHS9+jPAqy2ZT6cGEv8HTB6RZtTjd/dudAZSu4vjZpVWzId5pXmZg + 1M6G15dqjQ2XkNVOEnb5AAAAD3Jvb3RAeGVuaWFsLWx4ZAECAwQFBg== + -----END OPENSSH PRIVATE KEY----- + ed25519_public: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINudAZSu4vjZpVWzId5pXmZg1M6G15dqjQ2XkNVOEnb5 root@xenial-lxd + ecdsa_private: | + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIDuK+QFc1wmyJY8uDqQVa1qHte30Rk/fdLxGIBkwJAyOoAoGCCqGSM49 + AwEHoUQDQgAEWxLlO+TL8gL91eET9p/HFQbqR1A691AkJgZk3jY5mpZqxgX4vcgb + 7f/CtXuM6s2svcDJqAeXr6Wk8OJJcMxylA== + -----END EC PRIVATE KEY----- + ecdsa_public: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFsS5Tvky/IC/dXhE/afxxUG6kdQOvdQJCYGZN42OZqWasYF+L3IG+3/wrV7jOrNrL3AyagHl6+lpPDiSXDMcpQ= root@xenial-lxd +""" # noqa + + +@pytest.mark.ci +@pytest.mark.user_data(USER_DATA) +class TestSshKeysProvided: + + def test_ssh_dsa_keys_provided(self, class_client): + """Test dsa public key was imported.""" + out = class_client.read_from_file("/etc/ssh/ssh_host_dsa_key.pub") + assert ( + "AAAAB3NzaC1kc3MAAACBAPkWy1zbchVIN7qTgM0/yyY8q4R" + "ZS8cNM4ZpeuE5UB/Nnr6OSU/nmbO8LuM") in out + + """Test dsa private key was imported.""" + out = class_client.read_from_file("/etc/ssh/ssh_host_dsa_key") + assert ( + "MIIBuwIBAAKBgQD5Fstc23IVSDe6k4DNP8smPKuEWUvHDTOGaXr" + "hOVAfzZ6+jklP") in out + + def test_ssh_rsa_keys_provided(self, class_client): + """Test rsa public key was imported.""" + out = class_client.read_from_file("/etc/ssh/ssh_host_rsa_key.pub") + assert ( + "AAAAB3NzaC1yc2EAAAADAQABAAABAQC0/Ho+o3eJISydO2JvIgT" + "LnZOtrxPl+fSvJfKDjoOLY0HB2eOjy2s2/2N6d9X9SGZ4") in out + + """Test rsa private key was imported.""" + out = class_client.read_from_file("/etc/ssh/ssh_host_rsa_key") + assert ( + "4DOkqNiUGl80Zp1RgZNohHUXlJMtAbrIlAVEk+mTmg7vjfyp2un" + "RQvLZpMRdywBm") in out + + def test_ssh_rsa_certificate_provided(self, class_client): + """Test rsa certificate was imported.""" + out = class_client.read_from_file("/etc/ssh/ssh_host_rsa_key-cert.pub") + assert ( + "AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgMpg" + "BP4Phn3L8I7Vqh7lmHKcOfIokEvSEbHDw83Y3JloAAAAD") in out + + def test_ssh_certificate_updated_sshd_config(self, class_client): + """Test ssh certificate was added to /etc/ssh/sshd_config.""" + out = class_client.read_from_file("/etc/ssh/sshd_config").strip() + assert "HostCertificate /etc/ssh/ssh_host_rsa_key-cert.pub" in out + + def test_ssh_ecdsa_keys_provided(self, class_client): + """Test ecdsa public key was imported.""" + out = class_client.read_from_file("/etc/ssh/ssh_host_ecdsa_key.pub") + assert ( + "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAAB" + "BBFsS5Tvky/IC/dXhE/afxxU") in out + + """Test ecdsa private key generated.""" + out = class_client.read_from_file("/etc/ssh/ssh_host_ecdsa_key") + assert ( + "AwEHoUQDQgAEWxLlO+TL8gL91eET9p/HFQbqR1A691AkJgZk3jY" + "5mpZqxgX4vcgb") in out + + def test_ssh_ed25519_keys_provided(self, class_client): + """Test ed25519 public key was imported.""" + out = class_client.read_from_file("/etc/ssh/ssh_host_ed25519_key.pub") + assert ( + "AAAAC3NzaC1lZDI1NTE5AAAAINudAZSu4vjZpVWzId5pXmZg1M6" + "G15dqjQ2XkNVOEnb5") in out + + """Test ed25519 private key was imported.""" + out = class_client.read_from_file("/etc/ssh/ssh_host_ed25519_key") + assert ( + "XAAAAAtzc2gtZWQyNTUxOQAAACDbnQGUruL42aVVsyHeaV5mYNT" + "OhteXao0Nl5DVThJ2+Q") in out diff --git a/tests/integration_tests/modules/test_timezone.py b/tests/integration_tests/modules/test_timezone.py new file mode 100644 index 00000000..111d53f7 --- /dev/null +++ b/tests/integration_tests/modules/test_timezone.py @@ -0,0 +1,25 @@ +"""Integration test for the timezone module. + +This test specifies a timezone to be used by the ``timezone`` module +and then checks that if that timezone was respected during boot. + +(This is ported from +``tests/cloud_tests/testcases/modules/timezone.yaml``.)""" + +import pytest + + +USER_DATA = """\ +#cloud-config +timezone: US/Aleutian +""" + + +@pytest.mark.ci +class TestTimezone: + + @pytest.mark.user_data(USER_DATA) + def test_timezone(self, client): + timezone_output = client.execute( + 'date "+%Z" --date="Thu, 03 Nov 2016 00:47:00 -0400"') + assert timezone_output.strip() == "HDT" diff --git a/tests/integration_tests/modules/test_users_groups.py b/tests/integration_tests/modules/test_users_groups.py new file mode 100644 index 00000000..6a51f5a6 --- /dev/null +++ b/tests/integration_tests/modules/test_users_groups.py @@ -0,0 +1,83 @@ +"""Integration test for the user_groups module. + +This test specifies a number of users and groups via user-data, and confirms +that they have been configured correctly in the system under test. +""" +import re + +import pytest + + +USER_DATA = """\ +#cloud-config +# Add groups to the system +groups: + - secret: [root] + - cloud-users + +# Add users to the system. Users are added after groups are added. +users: + - default + - name: foobar + gecos: Foo B. Bar + primary_group: foobar + groups: users + expiredate: 2038-01-19 + lock_passwd: false + passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYe\ +AHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/ + - name: barfoo + gecos: Bar B. Foo + sudo: ALL=(ALL) NOPASSWD:ALL + groups: [cloud-users, secret] + lock_passwd: true + - name: cloudy + gecos: Magic Cloud App Daemon User + inactive: true + system: true +""" + + +@pytest.mark.ci +@pytest.mark.user_data(USER_DATA) +class TestUsersGroups: + @pytest.mark.parametrize( + "getent_args,regex", + [ + # Test the ubuntu group + (["group", "ubuntu"], r"ubuntu:x:[0-9]{4}:"), + # Test the cloud-users group + (["group", "cloud-users"], r"cloud-users:x:[0-9]{4}:barfoo"), + # Test the ubuntu user + ( + ["passwd", "ubuntu"], + r"ubuntu:x:[0-9]{4}:[0-9]{4}:Ubuntu:/home/ubuntu:/bin/bash", + ), + # Test the foobar user + ( + ["passwd", "foobar"], + r"foobar:x:[0-9]{4}:[0-9]{4}:Foo B. Bar:/home/foobar:", + ), + # Test the barfoo user + ( + ["passwd", "barfoo"], + r"barfoo:x:[0-9]{4}:[0-9]{4}:Bar B. Foo:/home/barfoo:", + ), + # Test the cloudy user + (["passwd", "cloudy"], r"cloudy:x:[0-9]{3,4}:"), + ], + ) + def test_users_groups(self, regex, getent_args, class_client): + """Use getent to interrogate the various expected outcomes""" + result = class_client.execute(["getent"] + getent_args) + assert re.search(regex, result.stdout) is not None, ( + "'getent {}' resulted in '{}', " + "but expected to match regex {}".format( + ' '.join(getent_args), result.stdout, regex)) + + def test_user_root_in_secret(self, class_client): + """Test root user is in 'secret' group.""" + output = class_client.execute("groups root").stdout + _, groups_str = output.split(":", maxsplit=1) + groups = groups_str.split() + assert "secret" in groups diff --git a/tests/integration_tests/modules/test_write_files.py b/tests/integration_tests/modules/test_write_files.py new file mode 100644 index 00000000..15832ae3 --- /dev/null +++ b/tests/integration_tests/modules/test_write_files.py @@ -0,0 +1,66 @@ +"""Integration test for the write_files module. + +This test specifies files to be created by the ``write_files`` module +and then checks if those files were created during boot. + +(This is ported from +``tests/cloud_tests/testcases/modules/write_files.yaml``.)""" + +import base64 +import pytest + + +ASCII_TEXT = "ASCII text" +B64_CONTENT = base64.b64encode(ASCII_TEXT.encode("utf-8")) + +# NOTE: the binary data can be any binary data, not only executables +# and can be generated via the base 64 command as such: +# $ base64 < hello > hello.txt +# the opposite is running: +# $ base64 -d < hello.txt > hello +# +USER_DATA = """\ +#cloud-config +write_files: +- encoding: b64 + content: {} + owner: root:root + path: /root/file_b64 + permissions: '0644' +- content: | + # My new /root/file_text + + SMBDOPTIONS="-D" + path: /root/file_text +- content: !!binary | + /Z/xrHR4WINT0UNoKPQKbuovp6+Js+JK + path: /root/file_binary + permissions: '0555' +- encoding: gzip + content: !!binary | + H4sIAIDb/U8C/1NW1E/KzNMvzuBKTc7IV8hIzcnJVyjPL8pJ4QIA6N+MVxsAAAA= + path: /root/file_gzip + permissions: '0755' +""".format(B64_CONTENT.decode("ascii")) + + +@pytest.mark.ci +@pytest.mark.user_data(USER_DATA) +class TestWriteFiles: + + @pytest.mark.parametrize( + "cmd,expected_out", ( + ("file /root/file_b64", ASCII_TEXT), + ("md5sum </root/file_binary", "3801184b97bb8c6e63fa0e1eae2920d7"), + ("sha256sum </root/file_binary", ( + "2c791c4037ea5bd7e928d6a87380f8ba" + "7a803cd83d5e4f269e28f5090f0f2c9a" + )), + ("file /root/file_gzip", + "POSIX shell script, ASCII text executable"), + ("file /root/file_text", ASCII_TEXT), + ) + ) + def test_write_files(self, cmd, expected_out, class_client): + out = class_client.execute(cmd) + assert expected_out in out |