diff --git a/indy_common/test/test_util.py b/indy_common/test/test_util.py index 73f2b55d9..75fe944f8 100644 --- a/indy_common/test/test_util.py +++ b/indy_common/test/test_util.py @@ -1,6 +1,8 @@ +import pytest + from operator import itemgetter from indy_common.util import getIndex - +from indy_common.util import compose_cmd def test_getIndex(): items = [('a', {'key1': 1}), ('b', {'key2': 2})] @@ -12,3 +14,32 @@ def containsKey(key): assert 0 == getIndex(containsKey('key1'), items) assert 1 == getIndex(containsKey('key2'), items) assert -1 == getIndex(containsKey('key3'), items) + +@pytest.mark.parametrize( + 'pkg_name,package', + [ + pytest.param('some_package', 'some_package', id='some_package'), + pytest.param('package_1', 'package_1;echo "hi"&&echo "hello"\necho "hello world!"', id='strips mixed cmd concat'), + pytest.param('package_3', 'package_3;echo "hey"', id='strips semi-colon cmd concat'), + pytest.param('package_4', 'package_4&&echo "hey"', id='strips and cmd concat'), + pytest.param('package_5', 'package_5\necho "hey"', id='strips Cr cmd concat'), + ] +) +def test_compose_cmd(pkg_name, package): + expected_cmd = f'dpkg -s {pkg_name}' + + cmd = compose_cmd(['dpkg', '-s', package]) + assert expected_cmd == cmd + +def test_compose_cmd_allows_whitespace(): + pkg_name = 'package_7 some_other_package' + expected_cmd = f'dpkg -s {pkg_name}' + cmd = compose_cmd(['dpkg', '-s', pkg_name]) + assert expected_cmd == cmd + +def test_compose_cmd_allows_pipe(): + expected_cmd = 'dpkg --get-selections | grep -v deinstall | cut -f1' + cmd = compose_cmd( + ['dpkg', '--get-selections', '|', 'grep', '-v', 'deinstall', '|', 'cut', '-f1'] + ) + assert expected_cmd == cmd \ No newline at end of file diff --git a/indy_common/util.py b/indy_common/util.py index d46a93824..2c51ce2a5 100644 --- a/indy_common/util.py +++ b/indy_common/util.py @@ -1,6 +1,7 @@ import datetime import os import random +import re from typing import Tuple, Union, TypeVar, List, Callable import libnacl.secret @@ -143,6 +144,7 @@ def getIndex(predicateFn: Callable[[T], bool], items: List[T]) -> int: def compose_cmd(cmd): if os.name != 'nt': cmd = ' '.join(cmd) + cmd = re.split(";|&&", cmd.splitlines()[0], 1)[0].rstrip() return cmd diff --git a/indy_node/server/request_handlers/config_req_handlers/pool_upgrade_handler.py b/indy_node/server/request_handlers/config_req_handlers/pool_upgrade_handler.py index f68bb2a69..591be9219 100644 --- a/indy_node/server/request_handlers/config_req_handlers/pool_upgrade_handler.py +++ b/indy_node/server/request_handlers/config_req_handlers/pool_upgrade_handler.py @@ -1,3 +1,5 @@ +import re + from typing import Optional from indy_common.authorize.auth_actions import AuthActionAdd, AuthActionEdit @@ -52,22 +54,6 @@ def additional_dynamic_validation(self, request: Request, req_pp_time: Optional[ self._validate_request_type(request) identifier, req_id, operation = get_request_data(request) status = '*' - - pkg_to_upgrade = operation.get(PACKAGE, getConfig().UPGRADE_ENTRY) - targetVersion = operation[VERSION] - reinstall = operation.get(REINSTALL, False) - - if not pkg_to_upgrade: - raise InvalidClientRequest(identifier, req_id, "Upgrade package name is empty") - - try: - res = self.upgrader.check_upgrade_possible(pkg_to_upgrade, targetVersion, reinstall) - except Exception as exc: - res = str(exc) - - if res: - raise InvalidClientRequest(identifier, req_id, res) - action = operation.get(ACTION) # TODO: Some validation needed for making sure name and version # present @@ -99,6 +85,22 @@ def additional_dynamic_validation(self, request: Request, req_pp_time: Optional[ self.write_req_validator.validate(request, [auth_action]) + pkg_to_upgrade = operation.get(PACKAGE, getConfig().UPGRADE_ENTRY) + if not pkg_to_upgrade: + raise InvalidClientRequest(identifier, req_id, "Upgrade package name is empty") + + # Only allow processing of a single package + pkg_to_upgrade = re.split("\s+|;|&&|\|", pkg_to_upgrade.splitlines()[0], 1)[0].rstrip() + targetVersion = operation[VERSION] + reinstall = operation.get(REINSTALL, False) + try: + res = self.upgrader.check_upgrade_possible(pkg_to_upgrade, targetVersion, reinstall) + except Exception as exc: + res = str(exc) + + if res: + raise InvalidClientRequest(identifier, req_id, res) + def apply_forced_request(self, req: Request): super().apply_forced_request(req) txn = self._req_to_txn(req) diff --git a/indy_node/test/node_control_utils/test_node_control_util.py b/indy_node/test/node_control_utils/test_node_control_util.py index 0ad8344fb..aecad7d23 100644 --- a/indy_node/test/node_control_utils/test_node_control_util.py +++ b/indy_node/test/node_control_utils/test_node_control_util.py @@ -1,5 +1,7 @@ +from ast import arg import pytest import shutil +import re from common.version import DigitDotVersion @@ -13,8 +15,8 @@ # - conditionally skip all tests for non-debian systems # - teste _parse_version_deps_from_pkg_mgr_output deeply -generated_commands = [] +generated_commands = [] @pytest.fixture def catch_generated_commands(monkeypatch): @@ -29,12 +31,107 @@ def _f(command, *args, **kwargs): monkeypatch.setattr(NodeControlUtil, 'run_shell_command', _f) -def test_generated_cmd_get_curr_info(catch_generated_commands): - pkg_name = 'some_package' +some_package_info = 'Package: some_package\nVersion: 1.2.3\nDepends: aaa (= 1.2.4), bbb (>= 1.2.5), ccc, aaa' +some_other_package_info = 'Package: some_other_package\nVersion: 4.5.6\nDepends: ddd (= 3.4.5), eee (>= 5.1.2), fff, ddd' +app_package_info = 'Package: {}\nVersion: 1.2.3\nDepends: aaa (= 1.2.4), bbb (>= 1.2.5), ccc, aaa'.format(APP_NAME) +any_package_info = 'Package: any_package\nVersion: 1.2.3\nDepends: aaa (= 1.2.4), bbb (>= 1.2.5), ccc, aaa' + +@pytest.fixture +def patch_run_shell_command(monkeypatch): + generated_commands[:] = [] + + pkg_list = 'openssl\nsed\ntar\nsome_package\nsome_other_package\n{}\nany_package'.format(APP_NAME) + pkg_info = '{}\n\n{}\n\n{}\n\n{}'.format(some_package_info, some_other_package_info, app_package_info, any_package_info) + + def mock_run_shell_command(command, *args, **kwargs): + # Keep track of the generated commands + generated_commands.append(command) + if command == 'dpkg --get-selections | grep -v deinstall | cut -f1': + return pkg_list + else: + package_name = command.split()[-1] + packages = re.split("\n\n", pkg_info) + for package in packages: + if package_name in package: + return package + + return '' + + monkeypatch.setattr(NodeControlUtil, 'run_shell_command', mock_run_shell_command) + + +@pytest.mark.parametrize( + 'pkg_name', + [ + pytest.param('not_installed_package', id='not_installed_package'), + # Ensure partial matches don't work. + pytest.param('some', id='partial_name_match-some'), + pytest.param('package', id='partial_name_match-package'), + ] +) +def test_generated_cmd_get_curr_info_pkg_not_installed(patch_run_shell_command, pkg_name): + pkg_name = 'not_installed_package' # TODO not an API for now NodeControlUtil._get_curr_info(pkg_name) assert len(generated_commands) == 1 - assert generated_commands[0] == "dpkg -s {}".format(pkg_name) + assert generated_commands[0] == 'dpkg --get-selections | grep -v deinstall | cut -f1' + + +def test_generated_cmd_get_curr_info_pkg_installed(patch_run_shell_command): + pkg_name = 'some_package' + # TODO not an API for now + NodeControlUtil._get_curr_info(pkg_name) + assert len(generated_commands) == 2 + assert generated_commands[0] == 'dpkg --get-selections | grep -v deinstall | cut -f1' + assert generated_commands[1] == "dpkg -s {}".format(pkg_name) + + +def test_generated_cmd_get_curr_info_accepts_single_pkg_only(patch_run_shell_command): + expected_pkg_name = 'some_other_package' + # The extra spaces between the package names is on purpose. + pkg_name = 'some_other_package some_package' + # TODO not an API for now + NodeControlUtil._get_curr_info(pkg_name) + assert len(generated_commands) == 2 + assert generated_commands[0] == 'dpkg --get-selections | grep -v deinstall | cut -f1' + assert generated_commands[1] == "dpkg -s {}".format(expected_pkg_name) + + +@pytest.mark.parametrize( + 'pkg_name,package', + [ + pytest.param('some_package', 'some_package|echo "hey";echo "hi"&&echo "hello"|echo "hello world"\necho "hello world!"', id='strips mixed cmd concat'), + pytest.param('some_package', 'some_package|echo "hey"', id='strips pipe cmd concat'), + pytest.param('some_package', 'some_package;echo "hey"', id='strips semi-colon cmd concat'), + pytest.param('some_package', 'some_package&&echo "hey"', id='strips AND cmd concat'), + pytest.param('some_package', 'some_package\necho "hey"', id='strips Cr cmd concat'), + pytest.param('some_package', 'some_package echo "hey"', id='strips whitespace'), + ] +) +def test_generated_cmd_get_curr_info_with_command_concat(patch_run_shell_command, pkg_name, package): + # TODO not an API for now + NodeControlUtil._get_curr_info(package) + assert len(generated_commands) == 2 + assert generated_commands[0] == 'dpkg --get-selections | grep -v deinstall | cut -f1' + assert generated_commands[1] == "dpkg -s {}".format(pkg_name) + + +@pytest.mark.parametrize( + 'pkg_name,expected_output', + [ + pytest.param('some_package', some_package_info, id='some_package'), + pytest.param('some_other_package', some_other_package_info, id='some_other_package'), + pytest.param(APP_NAME, app_package_info, id=APP_NAME), + pytest.param('any_package', any_package_info, id='any_package'), + pytest.param('not_installed_package', '', id='not_installed_package'), + # Ensure partial matches don't work. + pytest.param('some', '', id='partial_name_match-some'), + pytest.param('package', '', id='partial_name_match-package'), + ] +) +def test_get_curr_info_output(patch_run_shell_command, pkg_name, expected_output): + pkg_info = NodeControlUtil._get_curr_info(pkg_name) + assert pkg_info == expected_output def test_generated_cmd_get_latest_pkg_version(catch_generated_commands): @@ -65,12 +162,119 @@ def test_generated_cmd_get_info_from_package_manager(catch_generated_commands): assert len(generated_commands) == 1 assert generated_commands[0] == "apt-cache show {}".format(" ".join(packages)) - +# apt update is successful def test_generated_cmd_update_package_cache(catch_generated_commands): NodeControlUtil.update_package_cache() assert len(generated_commands) == 1 assert generated_commands[0] == "apt update" +# apt update fails +# apt update dependencies don't need to be upgraded, i.e. only key update is performed. +def test_generated_cmd_update_package_cache_2(monkeypatch): + run_shell_script_counter = 0 + commands = [] + + def _run_shell_script(command, *args, **kwargs): + nonlocal run_shell_script_counter + run_shell_script_counter += 1 + commands.append(command) + + if run_shell_script_counter == 1: + raise Exception("Command 'apt update' returned non-zero exit status") + + return '' + + def _f(command, *args, **kwargs): + commands.append(command) + return '' + + monkeypatch.setattr(NodeControlUtil, 'run_shell_script', _run_shell_script) + monkeypatch.setattr(NodeControlUtil, 'run_shell_script_extended', _f) + monkeypatch.setattr(NodeControlUtil, 'run_shell_command', _f) + + NodeControlUtil.update_package_cache() + assert len(commands) == 4 + assert commands[0] == "apt update" + assert commands[1] == "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88" + assert commands[2] == "apt list --upgradable" + assert commands[3] == "apt update" + + +# apt update fails +# apt update dependencies need to be upgraded +def test_generated_cmd_update_package_cache_3(monkeypatch): + run_shell_script_counter = 0 + commands = [] + + def _run_shell_script(command, *args, **kwargs): + nonlocal run_shell_script_counter + run_shell_script_counter += 1 + commands.append(command) + + if run_shell_script_counter == 1: + raise Exception("Command 'apt update' returned non-zero exit status") + + return '' + + def _run_shell_command(command, *args, **kwargs): + commands.append(command) + return """libgnutls-openssl27/xenial-updates 3.4.10-4ubuntu1.9 amd64 [upgradable from: 3.4.10-4ubuntu1.7] +libgnutls30/xenial-updates 3.4.10-4ubuntu1.9 amd64 [upgradable from: 3.4.10-4ubuntu1.7] +liblxc1/xenial-updates 2.0.11-0ubuntu1~16.04.3 amd64 [upgradable from: 2.0.8-0ubuntu1~16.04.2]""" + + def _f(command, *args, **kwargs): + commands.append(command) + return '' + + monkeypatch.setattr(NodeControlUtil, 'run_shell_script', _run_shell_script) + monkeypatch.setattr(NodeControlUtil, 'run_shell_script_extended', _f) + monkeypatch.setattr(NodeControlUtil, 'run_shell_command', _run_shell_command) + + NodeControlUtil.update_package_cache() + assert len(commands) == 5 + assert commands[0] == "apt update" + assert commands[1] == "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88" + assert commands[2] == "apt list --upgradable" + assert commands[3] == "apt --only-upgrade install -y libgnutls30" + assert commands[4] == "apt update" + + +def test_generated_cmd_update_repo_keys(catch_generated_commands): + NodeControlUtil.update_repo_keys() + assert len(generated_commands) == 1 + assert generated_commands[0] == "apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88" + + +# apt update dependencies don't need to be upgraded +def test_generated_cmd_update_apt_update_dependencies_1(catch_generated_commands): + NodeControlUtil.update_apt_update_dependencies() + assert len(generated_commands) == 1 + assert generated_commands[0] == "apt list --upgradable" + + +# apt update dependencies need to be upgraded +def test_generated_cmd_update_apt_update_dependencies_2(monkeypatch): + commands = [] + + def _run_shell_command(command, *args, **kwargs): + commands.append(command) + return """libgnutls-openssl27/xenial-updates 3.4.10-4ubuntu1.9 amd64 [upgradable from: 3.4.10-4ubuntu1.7] +libgnutls30/xenial-updates 3.4.10-4ubuntu1.9 amd64 [upgradable from: 3.4.10-4ubuntu1.7] +liblxc1/xenial-updates 2.0.11-0ubuntu1~16.04.3 amd64 [upgradable from: 2.0.8-0ubuntu1~16.04.2]""" + + def _f(command, *args, **kwargs): + commands.append(command) + return '' + + monkeypatch.setattr(NodeControlUtil, 'run_shell_script', _f) + monkeypatch.setattr(NodeControlUtil, 'run_shell_script_extended', _f) + monkeypatch.setattr(NodeControlUtil, 'run_shell_command', _run_shell_command) + + NodeControlUtil.update_apt_update_dependencies() + assert len(commands) == 2 + assert commands[0] == "apt list --upgradable" + assert commands[1] == "apt --only-upgrade install -y libgnutls30" + def test_generated_cmd_get_sys_holds(monkeypatch, catch_generated_commands): monkeypatch.setattr(shutil, 'which', lambda *_: 'path') @@ -154,24 +358,52 @@ def test_get_latest_pkg_version_for_unknown_package(): 'some-unknown-package-name', update_cache=False) is None -def test_curr_pkg_info_no_data(monkeypatch): - monkeypatch.setattr(NodeControlUtil, 'run_shell_command', lambda *_: '') - assert (None, []) == NodeControlUtil.curr_pkg_info('any_package') +def test_curr_pkg_info_no_data(patch_run_shell_command): + assert (None, []) == NodeControlUtil.curr_pkg_info('some-unknown-package-name') -def test_curr_pkg_info(monkeypatch): - output = 'Version: 1.2.3\nDepends: aaa (= 1.2.4), bbb (>= 1.2.5), ccc, aaa' - expected_deps = ['aaa=1.2.4', 'bbb=1.2.5', 'ccc'] - monkeypatch.setattr(NodeControlUtil, 'run_shell_command', lambda *_: output) +@pytest.mark.parametrize( + 'pkg_name,version,expected_deps', + [ + pytest.param('some_package', '1.2.3', ['aaa=1.2.4', 'bbb=1.2.5', 'ccc'], id='some_package'), + pytest.param('some_other_package', '4.5.6', ['ddd=3.4.5', 'eee=5.1.2', 'fff'], id='some_other_package'), + pytest.param(APP_NAME, '1.2.3', ['aaa=1.2.4', 'bbb=1.2.5', 'ccc'], id=APP_NAME), + pytest.param('any_package', '1.2.3', ['aaa=1.2.4', 'bbb=1.2.5', 'ccc'], id='any_package'), + ] +) +def test_curr_pkg_info(patch_run_shell_command, pkg_name, version, expected_deps): + upstream_cls = src_version_cls(pkg_name) + expected_version = DebianVersion( + version, upstream_cls=upstream_cls) + + pkg_info = NodeControlUtil.curr_pkg_info(pkg_name) + + assert expected_version == pkg_info[0] + assert isinstance(expected_version, type(pkg_info[0])) + assert isinstance(expected_version.upstream, type(pkg_info[0].upstream)) + assert expected_deps == pkg_info[1] - for pkg_name in [APP_NAME, 'any_package']: - upstream_cls = src_version_cls(pkg_name) - expected_version = DebianVersion( - '1.2.3', upstream_cls=upstream_cls) - pkg_info = NodeControlUtil.curr_pkg_info(pkg_name) +@pytest.mark.parametrize( + 'pkg_name', + [ + pytest.param('{} | echo "hey"; echo "hi" && echo "hello"|echo "hello world"'.format(APP_NAME), id='multiple'), + pytest.param('{}|echo "hey"'.format(APP_NAME), id='pipe'), + pytest.param('{};echo "hey"'.format(APP_NAME), id='semi-colon'), + pytest.param('{}&&echo "hey"'.format(APP_NAME), id='and'), + pytest.param('{}\necho "hey"'.format(APP_NAME), id='Cr'), + pytest.param('{} echo "hey"'.format(APP_NAME), id='whitespace'), + ] +) +def test_curr_pkg_info_with_command_concat(patch_run_shell_command, pkg_name): + expected_deps = ['aaa=1.2.4', 'bbb=1.2.5', 'ccc'] + upstream_cls = src_version_cls(pkg_name) + expected_version = DebianVersion( + '1.2.3', upstream_cls=upstream_cls) + + pkg_info = NodeControlUtil.curr_pkg_info(pkg_name) - assert expected_version == pkg_info[0] - assert isinstance(expected_version, type(pkg_info[0])) - assert isinstance(expected_version.upstream, type(pkg_info[0].upstream)) - assert expected_deps == pkg_info[1] + assert expected_version == pkg_info[0] + assert isinstance(expected_version, type(pkg_info[0])) + assert isinstance(expected_version.upstream, type(pkg_info[0].upstream)) + assert expected_deps == pkg_info[1] \ No newline at end of file diff --git a/indy_node/test/request_handlers/test_pool_upgrade_handler.py b/indy_node/test/request_handlers/test_pool_upgrade_handler.py index 1c4cf7fdc..57a7a87a2 100644 --- a/indy_node/test/request_handlers/test_pool_upgrade_handler.py +++ b/indy_node/test/request_handlers/test_pool_upgrade_handler.py @@ -71,6 +71,11 @@ def test_pool_upgrade_static_validation_passes(pool_upgrade_handler, def test_pool_upgrade_dynamic_validation_fails_pckg(pool_upgrade_handler, pool_upgrade_request, tconf): + pool_upgrade_handler.upgrader.get_upgrade_txn = \ + lambda predicate, reverse: \ + {TXN_PAYLOAD: {TXN_PAYLOAD_DATA: {}}} + pool_upgrade_handler.write_req_validator.validate = lambda a, b: 0 + pool_upgrade_request.operation[PACKAGE] = '' with pytest.raises(InvalidClientRequest) as e: pool_upgrade_handler.dynamic_validation(pool_upgrade_request, 0) @@ -82,11 +87,36 @@ def test_pool_upgrade_dynamic_validation_fails_not_installed( pool_upgrade_handler, pool_upgrade_request, tconf): + pool_upgrade_handler.upgrader.get_upgrade_txn = \ + lambda predicate, reverse: \ + {TXN_PAYLOAD: {TXN_PAYLOAD_DATA: {}}} + pool_upgrade_handler.write_req_validator.validate = lambda a, b: 0 + + monkeypatch.setattr(NodeControlUtil, 'curr_pkg_info', + lambda *x: (None, None)) + with pytest.raises(InvalidClientRequest) as e: + pool_upgrade_handler.dynamic_validation(pool_upgrade_request, 0) + e.match('{} is not installed and cannot be upgraded'.format(pool_upgrade_request.operation[PACKAGE])) + + +def test_pool_upgrade_dynamic_validation_fails_not_installed_mpr( + monkeypatch, + pool_upgrade_handler, + pool_upgrade_request, + tconf): + pool_upgrade_handler.upgrader.get_upgrade_txn = \ + lambda predicate, reverse: \ + {TXN_PAYLOAD: {TXN_PAYLOAD_DATA: {}}} + pool_upgrade_handler.write_req_validator.validate = lambda a, b: 0 + monkeypatch.setattr(NodeControlUtil, 'curr_pkg_info', lambda *x: (None, None)) + + # When multiple packages are requested, only the first should be processed. + pool_upgrade_request.operation[PACKAGE] = 'some_package some_other_package' with pytest.raises(InvalidClientRequest) as e: pool_upgrade_handler.dynamic_validation(pool_upgrade_request, 0) - e.match('is not installed and cannot be upgraded') + e.match('some_package is not installed and cannot be upgraded') def test_pool_upgrade_dynamic_validation_fails_belong( @@ -94,11 +124,16 @@ def test_pool_upgrade_dynamic_validation_fails_belong( pool_upgrade_handler, pool_upgrade_request, tconf): + pool_upgrade_handler.upgrader.get_upgrade_txn = \ + lambda predicate, reverse: \ + {TXN_PAYLOAD: {TXN_PAYLOAD_DATA: {}}} + pool_upgrade_handler.write_req_validator.validate = lambda a, b: 0 + monkeypatch.setattr(NodeControlUtil, 'curr_pkg_info', lambda *x: ('1.1.1', ['some_pkg'])) with pytest.raises(InvalidClientRequest) as e: pool_upgrade_handler.dynamic_validation(pool_upgrade_request, 0) - e.match('doesn\'t belong to pool') + e.match('{} doesn\'t belong to pool'.format(pool_upgrade_request.operation[PACKAGE])) def test_pool_upgrade_dynamic_validation_fails_upgradable( @@ -107,6 +142,11 @@ def test_pool_upgrade_dynamic_validation_fails_upgradable( pool_upgrade_request, pkg_version, tconf): + pool_upgrade_handler.upgrader.get_upgrade_txn = \ + lambda predicate, reverse: \ + {TXN_PAYLOAD: {TXN_PAYLOAD_DATA: {}}} + pool_upgrade_handler.write_req_validator.validate = lambda a, b: 0 + monkeypatch.setattr( NodeControlUtil, 'curr_pkg_info', lambda *x: (pkg_version, [APP_NAME]) diff --git a/indy_node/utils/node_control_utils.py b/indy_node/utils/node_control_utils.py index 318e1fa9e..43ca42ba2 100644 --- a/indy_node/utils/node_control_utils.py +++ b/indy_node/utils/node_control_utils.py @@ -199,9 +199,27 @@ def run_shell_script_extended( @classmethod def _get_curr_info(cls, package): + # Only allow processing of a single package + package = re.split("\s+|;|&&|\|", package.splitlines()[0], 1)[0].rstrip() + + # Ensure the package exists before fetching the details directly from dpkg + if not cls._package_exists(package): + return '' + cmd = compose_cmd(['dpkg', '-s', package]) return cls.run_shell_command(cmd) + @classmethod + def _package_exists(cls, package): + cmd = compose_cmd( + ['dpkg', '--get-selections', '|', 'grep', '-v', 'deinstall', '|', 'cut', '-f1'] + ) + installed_packages = cls.run_shell_command(cmd) + + # Ensure full match of package names. + is_installed = True if package in installed_packages.split('\n') else False + return is_installed + @classmethod def _parse_deps(cls, deps: str): ret = [] @@ -341,8 +359,48 @@ def _get_info_from_package_manager(cls, *package): @classmethod def update_package_cache(cls): cmd = compose_cmd(['apt', 'update']) + try: + cls.run_shell_script(cmd) + except Exception as e: + # Currently two issues can stop this from working. + # 1) The Sovrin Repo key needs to be updated + # apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CE7709D068DB5E88 + # 2) The following certificate validation error occurs: + # Err:6 https://repo.sovrin.org/deb xenial Release + # server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none + # Reading package lists... Done + # E: The repository 'https://repo.sovrin.org/deb xenial Release' does not have a Release file. + # N: Updating from such a repository can't be done securely, and is therefore disabled by default. + # N: See apt-secure(8) manpage for repository creation and user configuration details. + # This can be fixed by updating libgnutls30: + # apt --only-upgrade install -y libgnutls30 + logger.warning("Call to apt update failed in update_package_cache; {}".format(e)) + cls.update_repo_keys() + cls.update_apt_update_dependencies() + + # Try again ... + logger.info("Trying apt update again ...") + cls.run_shell_script(cmd) + + @classmethod + def update_repo_keys(cls): + logger.info("Updating signing keys for the artifact repository ...") + cmd = compose_cmd(['apt-key', 'adv', '--keyserver', 'keyserver.ubuntu.com', '--recv-keys', 'CE7709D068DB5E88']) cls.run_shell_script(cmd) + @classmethod + def update_apt_update_dependencies(cls): + cmd = compose_cmd(['apt', 'list', '--upgradable']) + logger.info("Getting list of upgradable packages ...") + upgradable_packages = cls.run_shell_command(cmd).split("\n") + libgnutls30 = next((x for x in upgradable_packages if x.find('libgnutls30') != -1), None) + if libgnutls30 is not None: + logger.info("Upgrading libgnutls30 ...") + cmd = compose_cmd(['apt', '--only-upgrade', 'install', '-y', 'libgnutls30']) + cls.run_shell_script(cmd) + else: + logger.info("libgnutls30 is already up to date.") + @classmethod def get_deps_tree(cls, *package, depth=0): ret = list(set(package))