From 0791f28f3d717f2281f4eb540b9d381affae2f4a Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Thu, 8 Jun 2023 15:17:42 +0200 Subject: [PATCH 1/4] Adjust to libblockdev 3.0 API changes Most API changes in libblockdev 3.0 are backward compatible but there are few changes we need to adjust to. --- blivet/__init__.py | 3 --- blivet/formats/luks.py | 4 +++- blivet/populator/helpers/loop.py | 11 +++++++---- blivet/static_data/nvdimm.py | 2 +- blivet/tasks/availability.py | 6 +++--- blivet/util.py | 7 ++++++- tests/unit_tests/populator_test.py | 20 ++++++++++---------- 7 files changed, 30 insertions(+), 23 deletions(-) diff --git a/blivet/__init__.py b/blivet/__init__.py index f2ed4c309..03d541841 100644 --- a/blivet/__init__.py +++ b/blivet/__init__.py @@ -73,9 +73,6 @@ def log_bd_message(level, msg): _requested_plugins = blockdev.plugin_specs_from_names(_REQUESTED_PLUGIN_NAMES) try: - # do not check for dependencies during libblockdev initialization, do runtime - # checks instead - blockdev.switch_init_checks(False) succ_, avail_plugs = blockdev.try_reinit(require_plugins=_requested_plugins, reload=False, log_func=log_bd_message) except GLib.GError as err: raise RuntimeError("Failed to initialize the libblockdev library: %s" % err) diff --git a/blivet/formats/luks.py b/blivet/formats/luks.py index 3440d3641..62b681681 100644 --- a/blivet/formats/luks.py +++ b/blivet/formats/luks.py @@ -344,9 +344,11 @@ def _post_create(self, **kwargs): super(LUKS, self)._post_create(**kwargs) try: - self.uuid = blockdev.crypto.luks_uuid(self.device) + info = blockdev.crypto.luks_info(self.device) except blockdev.CryptoError as e: raise LUKSError("Failed to get UUID for the newly created LUKS device %s: %s" % (self.device, str(e))) + else: + self.uuid = info.uuid if not self.map_name: self.map_name = "luks-%s" % self.uuid diff --git a/blivet/populator/helpers/loop.py b/blivet/populator/helpers/loop.py index 99fc4bb59..e64339711 100644 --- a/blivet/populator/helpers/loop.py +++ b/blivet/populator/helpers/loop.py @@ -39,12 +39,15 @@ def run(self): name = udev.device_get_name(self.data) log_method_call(self, name=name) sysfs_path = udev.device_get_sysfs_path(self.data) - backing_file = blockdev.loop.get_backing_file(name) - if backing_file is None: + try: + info = blockdev.loop.info(name) + except blockdev.LoopError: return None - file_device = self._devicetree.get_device_by_name(backing_file) + if not info.backing_file: + return None + file_device = self._devicetree.get_device_by_name(info.backing_file) if not file_device: - file_device = FileDevice(backing_file, exists=True) + file_device = FileDevice(info.backing_file, exists=True) self._devicetree._add_device(file_device) device = LoopDevice(name, parents=[file_device], diff --git a/blivet/static_data/nvdimm.py b/blivet/static_data/nvdimm.py index e0a24bf83..41f01f90f 100644 --- a/blivet/static_data/nvdimm.py +++ b/blivet/static_data/nvdimm.py @@ -34,7 +34,7 @@ class NVDIMMDependencyGuard(util.DependencyGuard): def _check_avail(self): try: - BlockDev.nvdimm_is_tech_avail(BlockDev.NVDIMMTech.NVDIMM_TECH_NAMESPACE, + BlockDev.nvdimm_is_tech_avail(BlockDev.NVDIMMTech.NAMESPACE, BlockDev.NVDIMMTechMode.RECONFIGURE | BlockDev.NVDIMMTechMode.QUERY | BlockDev.NVDIMMTechMode.ACTIVATE_DEACTIVATE) diff --git a/blivet/tasks/availability.py b/blivet/tasks/availability.py index 7d4e115e7..df0694a68 100644 --- a/blivet/tasks/availability.py +++ b/blivet/tasks/availability.py @@ -399,7 +399,7 @@ def available_resource(name): blockdev.LoopTechMode.QUERY) BLOCKDEV_LOOP = BlockDevTechInfo(plugin_name="loop", check_fn=blockdev.loop_is_tech_avail, - technologies={blockdev.LoopTech.LOOP_TECH_LOOP: BLOCKDEV_LOOP_ALL_MODES}) + technologies={blockdev.LoopTech.LOOP: BLOCKDEV_LOOP_ALL_MODES}) BLOCKDEV_LOOP_TECH = BlockDevMethod(BLOCKDEV_LOOP) # libblockdev lvm plugin required technologies and modes @@ -437,7 +437,7 @@ def available_resource(name): blockdev.MDTechMode.QUERY) BLOCKDEV_MD = BlockDevTechInfo(plugin_name="mdraid", check_fn=blockdev.md_is_tech_avail, - technologies={blockdev.MDTech.MD_TECH_MDRAID: BLOCKDEV_MD_ALL_MODES}) + technologies={blockdev.MDTech.MDRAID: BLOCKDEV_MD_ALL_MODES}) BLOCKDEV_MD_TECH = BlockDevMethod(BLOCKDEV_MD) # libblockdev mpath plugin required technologies and modes @@ -455,7 +455,7 @@ def available_resource(name): blockdev.SwapTechMode.SET_LABEL) BLOCKDEV_SWAP = BlockDevTechInfo(plugin_name="swap", check_fn=blockdev.swap_is_tech_avail, - technologies={blockdev.SwapTech.SWAP_TECH_SWAP: BLOCKDEV_SWAP_ALL_MODES}) + technologies={blockdev.SwapTech.SWAP: BLOCKDEV_SWAP_ALL_MODES}) BLOCKDEV_SWAP_TECH = BlockDevMethod(BLOCKDEV_SWAP) # libblockdev plugins diff --git a/blivet/util.py b/blivet/util.py index 18015c94c..6ec6708b6 100644 --- a/blivet/util.py +++ b/blivet/util.py @@ -294,7 +294,12 @@ def get_mount_device(mountpoint): if mount_device and re.match(r'/dev/loop\d+$', mount_device): loop_name = os.path.basename(mount_device) - mount_device = blockdev.loop.get_backing_file(loop_name) + try: + info = blockdev.loop.info(loop_name) + except blockdev.LoopError as e: + log.warning("failed to get loop info for %s: %s", loop_name, str(e)) + return None + mount_device = info.backing_file log.debug("found backing file %s for loop device %s", mount_device, loop_name) diff --git a/tests/unit_tests/populator_test.py b/tests/unit_tests/populator_test.py index df56e1f5f..18b8dbaf9 100644 --- a/tests/unit_tests/populator_test.py +++ b/tests/unit_tests/populator_test.py @@ -132,7 +132,7 @@ def test_match(self, *args): # The backing file check is now performed in the "run" method. # Test intentionally left empty - @patch("blivet.populator.helpers.loop.blockdev.loop.get_backing_file") + @patch("blivet.populator.helpers.loop.blockdev.loop.info") @patch("blivet.udev.device_get_name") @patch("blivet.udev.device_is_dm", return_value=False) @patch("blivet.udev.device_is_dm_luks", return_value=False) @@ -144,14 +144,14 @@ def test_match(self, *args): def test_get_helper(self, *args): """Test get_device_helper for loop devices.""" device_is_loop = args[0] - get_backing_file = args[7] + loop_info = args[7] data = {'SYS_PATH': 'dummy'} - get_backing_file.return_value = True + loop_info.return_value = Mock(baking_file="foobar") self.assertEqual(get_device_helper(data), self.helper_class) - get_backing_file.return_value = False + loop_info.return_value = Mock(baking_file=None) self.assertEqual(get_device_helper(data), self.helper_class) - get_backing_file.return_value = True + loop_info.return_value = Mock(baking_file="foobar") # verify that setting one of the required True return values to False prevents success device_is_loop.return_value = False @@ -165,13 +165,13 @@ def test_get_helper(self, *args): @patch.object(DeviceTree, "get_device_by_name") @patch.object(FileDevice, "status", return_value=True) @patch.object(LoopDevice, "status", return_value=True) - @patch("blivet.populator.helpers.loop.blockdev.loop.get_backing_file") + @patch("blivet.populator.helpers.loop.blockdev.loop.info") @patch("blivet.udev.device_get_name") @patch("blivet.udev.device_get_sysfs_path", return_value=sentinel.sysfs_path) def test_run(self, *args): """Test loop device populator.""" device_get_name = args[1] - get_backing_file = args[2] + loop_info = args[2] devicetree = DeviceTree() data = Mock() @@ -182,13 +182,13 @@ def test_run(self, *args): device_get_name.return_value = device_name backing_file = "/some/file" - get_backing_file.return_value = None + loop_info.return_value = Mock(backing_file=None) helper = self.helper_class(devicetree, data) device = helper.run() self.assertIsNone(device) - get_backing_file.return_value = backing_file + loop_info.return_value = Mock(backing_file=backing_file) device = helper.run() @@ -391,7 +391,7 @@ def test_get_helper(self, *args): # verify that setting one of the required False return values to True prevents success # as of now, loop is always checked before partition device_is_loop.return_value = True - with patch("blivet.populator.helpers.loop.blockdev.loop.get_backing_file", return_value=True): + with patch("blivet.populator.helpers.loop.blockdev.loop.info", return_value=Mock(backing_file="foobar")): self.assertNotEqual(get_device_helper(data), self.helper_class) device_is_loop.return_value = False From e58b7291e542a07b5318df627dc0a1f0c8b69689 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Wed, 21 Jun 2023 16:12:21 +0200 Subject: [PATCH 2/4] Adjust to the new libblockdev 3.0 crypto API Changes are mostly related to the new keyslot context that needs to be used instead of passphrase and key file. --- blivet/formats/luks.py | 65 ++++++++++++++----- blivet/tasks/availability.py | 1 - blivet/tasks/lukstasks.py | 10 ++- tests/unit_tests/formats_tests/luks_test.py | 16 ++--- .../unit_tests/formats_tests/methods_test.py | 2 + 5 files changed, 68 insertions(+), 26 deletions(-) diff --git a/blivet/formats/luks.py b/blivet/formats/luks.py index 62b681681..2982cd25b 100644 --- a/blivet/formats/luks.py +++ b/blivet/formats/luks.py @@ -262,10 +262,17 @@ def _pre_setup(self, **kwargs): def _setup(self, **kwargs): log_method_call(self, device=self.device, map_name=self.map_name, type=self.type, status=self.status) + + # passphrase is preferred for open + if self.__passphrase: + context = blockdev.CryptoKeyslotContext(passphrase=self.__passphrase) + elif self._key_file: + context = blockdev.CryptoKeyslotContext(keyfile=self._key_file) + else: + raise LUKSError("Passphrase or key file must be set for LUKS setup") + try: - blockdev.crypto.luks_open(self.device, self.map_name, - passphrase=self.__passphrase, - key_file=self._key_file) + blockdev.crypto.luks_open(self.device, self.map_name, context=context) except blockdev.CryptoError as e: raise LUKSError(e) @@ -328,10 +335,16 @@ def _create(self, **kwargs): else: extra = None + if self.__passphrase: + context = blockdev.CryptoKeyslotContext(passphrase=self.__passphrase) + elif self._key_file: + context = blockdev.CryptoKeyslotContext(keyfile=self._key_file) + else: + raise LUKSError("Passphrase or key file must be set for LUKS create") + try: blockdev.crypto.luks_format(self.device, - passphrase=self.__passphrase, - key_file=self._key_file, + context=context, cipher=self.cipher, key_size=self.key_size, min_entropy=self.min_luks_entropy, @@ -340,6 +353,14 @@ def _create(self, **kwargs): except blockdev.CryptoError as e: raise LUKSError(e) + if self.__passphrase and self._key_file: + # both passphrase and keyfile are set, we need to add the keyfile too + ncontext = blockdev.CryptoKeyslotContext(keyfile=self._key_file) + try: + blockdev.crypto.luks_add_key(self.device, context, ncontext) + except blockdev.CryptoError as e: + raise LUKSError(e) + def _post_create(self, **kwargs): super(LUKS, self)._post_create(**kwargs) @@ -373,11 +394,17 @@ def add_passphrase(self, passphrase): if not self.exists: raise LUKSError("format has not been created") + # if both passphrase and keyfile are set, they both need to be valid so + # we can choose passphrase as default + if self.__passphrase: + context = blockdev.CryptoKeyslotContext(passphrase=self.__passphrase) + elif self._key_file: + context = blockdev.CryptoKeyslotContext(keyfile=self._key_file) + + ncontext = blockdev.CryptoKeyslotContext(passphrase=passphrase) + try: - blockdev.crypto.luks_add_key(self.device, - pass_=self.__passphrase, - key_file=self._key_file, - npass=passphrase) + blockdev.crypto.luks_add_key(self.device, context, ncontext) except blockdev.CryptoError as e: raise LUKSError(e) @@ -386,6 +413,8 @@ def remove_passphrase(self): Remove the saved passphrase (and possibly key file) from the LUKS header. + Note: If both passphrase and keyfile are set for this format, both + will be removed! """ log_method_call(self, device=self.device, @@ -393,12 +422,18 @@ def remove_passphrase(self): if not self.exists: raise LUKSError("format has not been created") - try: - blockdev.crypto.luks_remove_key(self.device, - pass_=self.__passphrase, - key_file=self._key_file) - except blockdev.CryptoError as e: - raise LUKSError(e) + def _remove_passphrase(context): + try: + blockdev.crypto.luks_remove_key(self.device, context=context) + except blockdev.CryptoError as e: + raise LUKSError(e) + + if self.__passphrase: + context = blockdev.CryptoKeyslotContext(passphrase=self.__passphrase) + _remove_passphrase(context) + elif self._key_file: + context = blockdev.CryptoKeyslotContext(keyfile=self._key_file) + _remove_passphrase(context) def escrow(self, directory, backup_passphrase): log.debug("escrow: escrow_volume start for %s", self.device) diff --git a/blivet/tasks/availability.py b/blivet/tasks/availability.py index df0694a68..e0a33c0c0 100644 --- a/blivet/tasks/availability.py +++ b/blivet/tasks/availability.py @@ -371,7 +371,6 @@ def available_resource(name): BLOCKDEV_CRYPTO = BlockDevTechInfo(plugin_name="crypto", check_fn=blockdev.crypto_is_tech_avail, technologies={blockdev.CryptoTech.LUKS: BLOCKDEV_CRYPTO_ALL_MODES, - blockdev.CryptoTech.LUKS2: BLOCKDEV_CRYPTO_ALL_MODES, blockdev.CryptoTech.ESCROW: blockdev.CryptoTechMode.CREATE}) BLOCKDEV_CRYPTO_TECH = BlockDevMethod(BLOCKDEV_CRYPTO) diff --git a/blivet/tasks/lukstasks.py b/blivet/tasks/lukstasks.py index d515e4935..92aca10f1 100644 --- a/blivet/tasks/lukstasks.py +++ b/blivet/tasks/lukstasks.py @@ -87,9 +87,15 @@ def do_task(self): # pylint: disable=arguments-differ """ Resizes the LUKS format. """ try: if self.luks.luks_version == "luks2": + if self.luks._LUKS__passphrase: + context = blockdev.CryptoKeyslotContext(passphrase=self.luks._LUKS__passphrase) + elif self.luks._key_file: + context = blockdev.CryptoKeyslotContext(keyfile=self.luks._key_file) + else: + # context for resize can be NULL -- this means the key is already in the keyring + context = None blockdev.crypto.luks_resize(self.luks.map_name, self.luks.target_size.convert_to(self.unit), - passphrase=self.luks._LUKS__passphrase, - key_file=self.luks._key_file) + context=context) else: blockdev.crypto.luks_resize(self.luks.map_name, self.luks.target_size.convert_to(self.unit)) except blockdev.CryptoError as e: diff --git a/tests/unit_tests/formats_tests/luks_test.py b/tests/unit_tests/formats_tests/luks_test.py index 7e0c9d2c3..1dd597be3 100644 --- a/tests/unit_tests/formats_tests/luks_test.py +++ b/tests/unit_tests/formats_tests/luks_test.py @@ -57,7 +57,7 @@ def test_key_size(self): self.assertEqual(fmt.key_size, 0) def test_luks2_pbkdf_memory_fips(self): - fmt = LUKS() + fmt = LUKS(passphrase="passphrase") with patch("blivet.formats.luks.blockdev.crypto") as bd: # fips enabled, pbkdf memory should not be set with patch("blivet.formats.luks.crypto") as crypto: @@ -82,28 +82,28 @@ def test_luks2_pbkdf_memory_fips(self): self.assertEqual(bd.luks_format.call_args[1]["extra"].pbkdf.max_memory_kb, 256 * 1024) def test_sector_size_luks1(self): - fmt = LUKS() + fmt = LUKS(passphrase="passphrase") self.assertEqual(fmt.luks_sector_size, 0) # sector size is not valid for luks1 with self.assertRaises(ValueError): - fmt = LUKS(luks_version="luks1", luks_sector_size=4096) + fmt = LUKS(luks_version="luks1", luks_sector_size=4096, passphrase="passphrase") # just make sure we won't try to add the extra.sector_size argument ourselves - fmt = LUKS(luks_version="luks1") + fmt = LUKS(luks_version="luks1", passphrase="passphrase") with patch("blivet.devices.lvm.blockdev.crypto") as crypto: fmt._create() crypto.luks_format.assert_called() self.assertIsNone(crypto.luks_format.call_args[1]["extra"]) def test_sector_size_luks2(self): - fmt = LUKS() + fmt = LUKS(passphrase="passphrase") self.assertEqual(fmt.luks_sector_size, 0) - fmt = LUKS(luks_version="luks2", luks_sector_size=4096) + fmt = LUKS(luks_version="luks2", luks_sector_size=4096, passphrase="passphrase") self.assertEqual(fmt.luks_sector_size, 4096) - fmt = LUKS() + fmt = LUKS(passphrase="passphrase") with patch("blivet.devicelibs.crypto.calculate_luks2_max_memory", return_value=None): with patch("blivet.devicelibs.crypto.get_optimal_luks_sector_size", return_value=512): with patch("blivet.devices.lvm.blockdev.crypto") as crypto: @@ -111,7 +111,7 @@ def test_sector_size_luks2(self): crypto.luks_format.assert_called() self.assertEqual(crypto.luks_format.call_args[1]["extra"].sector_size, 512) - fmt = LUKS() + fmt = LUKS(passphrase="passphrase") with patch("blivet.devicelibs.crypto.calculate_luks2_max_memory", return_value=None): with patch("blivet.devicelibs.crypto.get_optimal_luks_sector_size", return_value=0): with patch("blivet.devices.lvm.blockdev.crypto") as crypto: diff --git a/tests/unit_tests/formats_tests/methods_test.py b/tests/unit_tests/formats_tests/methods_test.py index dca308ff2..4f33c0952 100644 --- a/tests/unit_tests/formats_tests/methods_test.py +++ b/tests/unit_tests/formats_tests/methods_test.py @@ -366,12 +366,14 @@ def set_patches(self): def _test_create_backend(self): self.format.exists = False + self.format.passphrase = "passphrase" with patch("blivet.devicelibs.crypto.get_optimal_luks_sector_size", return_value=512): with patch("blivet.devicelibs.crypto.is_fips_enabled", return_value=False): self.format.create() self.assertTrue(self.patches["blockdev"].crypto.luks_format.called) # pylint: disable=no-member def _test_setup_backend(self): + self.format.passphrase = "passphrase" self.format.setup() self.assertTrue(self.patches["blockdev"].crypto.luks_open.called) From fa553be416afa9913889fbfed249e473f5599d79 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Wed, 21 Jun 2023 16:13:39 +0200 Subject: [PATCH 3/4] Add new LUKS tests for add/remove key and key file usage --- tests/storage_tests/formats_test/luks_test.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/storage_tests/formats_test/luks_test.py b/tests/storage_tests/formats_test/luks_test.py index 042f87de1..93c8d7524 100644 --- a/tests/storage_tests/formats_test/luks_test.py +++ b/tests/storage_tests/formats_test/luks_test.py @@ -1,8 +1,10 @@ import os +import tempfile import unittest from blivet.formats.luks import LUKS, Integrity from blivet.devicelibs import crypto +from blivet.errors import LUKSError from blivet.size import Size from . import loopbackedtestcase @@ -65,6 +67,54 @@ def test_map_name(self): self.fmt.teardown() self.assertFalse(self.fmt.status) + def test_add_remove_passphrase(self): + self.fmt.device = self.loop_devices[0] + + # create the luks format + self.fmt.create() + + # add a new passphrase + self.fmt.add_passphrase("password2") + + # we should now be able to setup the format using this passphrase + self.fmt.passphrase = "password2" + self.fmt.setup() + self.fmt.teardown() + + # remove the original passphrase + self.fmt.passphrase = "password" + self.fmt.remove_passphrase() + + # now setup should fail + with self.assertRaises(LUKSError): + self.fmt.setup() + + # setup with the new passphrase should still be possible + self.fmt.passphrase = "password2" + self.fmt.setup() + self.fmt.teardown() + + def test_setup_keyfile(self): + self.fmt.device = self.loop_devices[0] + + with tempfile.NamedTemporaryFile(prefix="blivet_test") as temp: + temp.write(b"password2") + + # create the luks format with both passphrase and keyfile + self.fmt._key_file = temp.name + self.fmt.create() + + # open first with just password + self.fmt._key_file = None + self.fmt.setup() + self.fmt.teardown() + + # now with keyfile + self.fmt._key_file = temp.name + self.fmt.passphrase = None + self.fmt.setup() + self.fmt.teardown() + def _luks_close(self): self.fmt.teardown() From fbbdc7dee49b23b54dadeb8c2dd17b38d2991c95 Mon Sep 17 00:00:00 2001 From: Vojtech Trefny Date: Thu, 22 Jun 2023 12:05:44 +0200 Subject: [PATCH 4/4] ci: Run GH actions tests in a Fedora container We need latest libblockdev for the tests. --- .github/workflows/check.yml | 41 ++++++++++++++++++++++++-------- .github/workflows/unit_tests.yml | 28 ---------------------- 2 files changed, 31 insertions(+), 38 deletions(-) delete mode 100644 .github/workflows/unit_tests.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index eb6f103ce..f6288c453 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -14,15 +14,36 @@ on: jobs: build: - + name: static-analysis runs-on: ubuntu-22.04 - + env: + CI_IMAGE: fedora:latest + CI_CONTAINER: blivet-tests steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: | - sudo apt-get -qq update - sudo apt-get -y -qq install make ansible - sudo make install-requires - - name: Run checks - run: make check + - name: Checkout libblockdev repository + uses: actions/checkout@v3 + + - name: Install podman + run: | + sudo apt -qq update + sudo apt -y -qq install podman + + - name: Start the container + run: | + podman run -d -t --name ${{ env.CI_CONTAINER }} --privileged --volume "$(pwd):/app" --workdir "/app" ${{ env.CI_IMAGE }} + + - name: Install ansible in the container + run: | + podman exec -it ${{ env.CI_CONTAINER }} bash -c "dnf -y install ansible make which" + + - name: Enable our daily builds Copr in the container + run: | + podman exec -it ${{ env.CI_CONTAINER }} bash -c "dnf -y install dnf-plugins-core && dnf -y copr enable @storage/udisks-daily" + + - name: Install test dependencies in the container + run: | + podman exec -it ${{ env.CI_CONTAINER }} bash -c "ansible-playbook -i 'localhost,' -c local misc/install-test-dependencies.yml" + + - name: Run static analysis tests in the container + run: | + podman exec -it ${{ env.CI_CONTAINER }} bash -c "make check" diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml deleted file mode 100644 index a10410f22..000000000 --- a/.github/workflows/unit_tests.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Unit tests - -on: - pull_request: - branches: - - 3.*-devel - - master - push: - branches: - - 3.*-devel - - master - schedule: - - cron: 0 0 * * 0 - -jobs: - build: - - runs-on: ubuntu-22.04 - - steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: | - sudo apt-get -qq update - sudo apt-get -y -qq install make ansible - sudo make install-requires - - name: Run tests - run: make unit-test