From 07e35f75056bfa9d1467bb872071a619c449d77c Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 21 Nov 2020 21:41:09 +0100 Subject: [PATCH] feat(pkgin): add support for installation of full versioned package names (#1256) (#1349) * * pkgin query_package(..) understands now also package name with version (my-package-1.2nb123456). * pkgin query_package(..) will distinct between not-installed and not-found packages. * pkgin install_package(..) fails with proper error if a non-existing package is attempted to be installed. * fixup ansibot comments * add changelog fragment * add example * use more pythonic condition Co-authored-by: Andrew Klychkov * remove extra dot between description and link to PR Co-authored-by: Andrew Klychkov * fix file extension of changelog fragment * add pkgin unit tests for query_package function * fix pep8 issues * use enum Flag instead mix of strings, bools and None as return value * use IntEnum instead Flag to support Python versions >= 2.6 * fix pep8 and pylint errors * use regular class instead enum to avoid issues with older Python releases * fix comment * remove the combined package state since without an IntFlag or Flag the usage of it would require to use logical operators Co-authored-by: Erik van Nooijen Co-authored-by: Andrew Klychkov (cherry picked from commit ece020250749cb71277205f783f35667e541f490) Co-authored-by: Martin Migasiewicz <616250+martinm82@users.noreply.github.com> --- ...at-pkgin-add-full-version-package-name.yml | 2 + plugins/modules/packaging/os/pkgin.py | 46 ++++-- .../modules/packaging/os/test_pkgin.py | 143 ++++++++++++++++++ 3 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 changelogs/fragments/1256-feat-pkgin-add-full-version-package-name.yml create mode 100644 tests/unit/plugins/modules/packaging/os/test_pkgin.py diff --git a/changelogs/fragments/1256-feat-pkgin-add-full-version-package-name.yml b/changelogs/fragments/1256-feat-pkgin-add-full-version-package-name.yml new file mode 100644 index 0000000000..b0d7ffb4e9 --- /dev/null +++ b/changelogs/fragments/1256-feat-pkgin-add-full-version-package-name.yml @@ -0,0 +1,2 @@ +minor_changes: + - pkgin - add support for installation of full versioned package names (https://github.com/ansible-collections/community.general/pull/1256). diff --git a/plugins/modules/packaging/os/pkgin.py b/plugins/modules/packaging/os/pkgin.py index 9e50fc5491..2937314fa1 100644 --- a/plugins/modules/packaging/os/pkgin.py +++ b/plugins/modules/packaging/os/pkgin.py @@ -77,6 +77,11 @@ EXAMPLES = ''' name: foo state: present +- name: Install specific version of foo package + community.general.pkgin: + name: foo-2.0.1 + state: present + - name: Update cache and install foo package community.general.pkgin: name: foo @@ -120,13 +125,15 @@ import re from ansible.module_utils.basic import AnsibleModule -def query_package(module, name): - """Search for the package by name. +class PackageState(object): + PRESENT = 1 + NOT_INSTALLED = 2 + OUTDATED = 4 + NOT_FOUND = 8 - Possible return values: - * "present" - installed, no upgrade needed - * "outdated" - installed, but can be upgraded - * False - not installed or not found + +def query_package(module, name): + """Search for the package by name and return state of the package. """ # test whether '-p' (parsable) flag is supported. @@ -172,20 +179,24 @@ def query_package(module, name): # Grab matched string pkgname_without_version = pkg_search_obj.group(1) - if name != pkgname_without_version: + if name not in (pkgname_with_version, pkgname_without_version): continue # The package was found; now return its state if raw_state == '<': - return 'outdated' + return PackageState.OUTDATED elif raw_state == '=' or raw_state == '>': - return 'present' + return PackageState.PRESENT else: - return False + # Package found but not installed + return PackageState.NOT_INSTALLED # no fall-through - # No packages were matched, so return False - return False + # No packages were matched + return PackageState.NOT_FOUND + + # Search failed + return PackageState.NOT_FOUND def format_action_message(module, action, count): @@ -233,13 +244,13 @@ def remove_packages(module, packages): # Using a for loop in case of error, we can report the package that failed for package in packages: # Query the package first, to see if we even need to remove - if not query_package(module, package): + if query_package(module, package) in [PackageState.NOT_INSTALLED, PackageState.NOT_FOUND]: continue rc, out, err = module.run_command( format_pkgin_command(module, "remove", package)) - if not module.check_mode and query_package(module, package): + if not module.check_mode and query_package(module, package) in [PackageState.PRESENT, PackageState.OUTDATED]: module.fail_json(msg="failed to remove %s: %s" % (package, out)) remove_c += 1 @@ -255,13 +266,16 @@ def install_packages(module, packages): install_c = 0 for package in packages: - if query_package(module, package): + query_result = query_package(module, package) + if query_result in [PackageState.PRESENT, PackageState.OUTDATED]: continue + elif query_result is PackageState.NOT_FOUND: + module.fail_json(msg="failed to find package %s for installation" % package) rc, out, err = module.run_command( format_pkgin_command(module, "install", package)) - if not module.check_mode and not query_package(module, package): + if not module.check_mode and not query_package(module, package) in [PackageState.PRESENT, PackageState.OUTDATED]: module.fail_json(msg="failed to install %s: %s" % (package, out)) install_c += 1 diff --git a/tests/unit/plugins/modules/packaging/os/test_pkgin.py b/tests/unit/plugins/modules/packaging/os/test_pkgin.py new file mode 100644 index 0000000000..a53cfd493b --- /dev/null +++ b/tests/unit/plugins/modules/packaging/os/test_pkgin.py @@ -0,0 +1,143 @@ +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible_collections.community.general.tests.unit.compat import mock +from ansible_collections.community.general.tests.unit.compat import unittest + +from ansible_collections.community.general.plugins.modules.packaging.os import pkgin + + +class TestPkginQueryPackage(unittest.TestCase): + + def setUp(self): + pkgin.PKGIN_PATH = "" + + @mock.patch('ansible_collections.community.general.plugins.modules.packaging.os.pkgin.AnsibleModule') + def test_package_without_version_is_present(self, mock_module): + # given + package = 'py37-conan' + parseable_flag_not_supported = 1 + mock_module.run_command.side_effect = [ + (parseable_flag_not_supported, "pkgin 0.11.7 for Darwin-18.6.0 x86_64 (using SQLite 3.27.2)", None), + (0, "%s-1.21.0 = C/C++ package manager" % package, None), + ] + + # when + command_result = pkgin.query_package(mock_module, package) + + # then + self.assertEquals(command_result, pkgin.PackageState.PRESENT) + + @mock.patch('ansible_collections.community.general.plugins.modules.packaging.os.pkgin.AnsibleModule') + def test_package_with_version_is_present(self, mock_module): + # given + package = 'py37-conan-1.21.0' + parseable_flag_not_supported = 1 + mock_module.run_command.side_effect = [ + (parseable_flag_not_supported, "pkgin 0.11.7 for Darwin-18.6.0 x86_64 (using SQLite 3.27.2)", None), + (0, "%s = C/C++ package manager" % package, None), + ] + + # when + command_result = pkgin.query_package(mock_module, package) + + # then + self.assertEquals(command_result, pkgin.PackageState.PRESENT) + + @mock.patch('ansible_collections.community.general.plugins.modules.packaging.os.pkgin.AnsibleModule') + def test_package_found_but_not_installed(self, mock_module): + # given + package = 'cmake' + parseable_flag_not_supported = 1 + mock_module.run_command.side_effect = [ + (parseable_flag_not_supported, "pkgin 0.11.7 for Darwin-18.6.0 x86_64 (using SQLite 3.27.2)", None), + (0, "cmake316-3.16.0nb1 = Cross platform make\ncmake314-3.14.6nb1 = Cross platform make\ncmake-3.14.0 Cross platform make", None), + ] + + # when + command_result = pkgin.query_package(mock_module, package) + + # then + self.assertEquals(command_result, pkgin.PackageState.NOT_INSTALLED) + + @mock.patch('ansible_collections.community.general.plugins.modules.packaging.os.pkgin.AnsibleModule') + def test_package_found_outdated(self, mock_module): + # given + package = 'cmake316' + parseable_flag_not_supported = 1 + mock_module.run_command.side_effect = [ + (parseable_flag_not_supported, "pkgin 0.11.7 for Darwin-18.6.0 x86_64 (using SQLite 3.27.2)", None), + (0, "cmake316-3.16.0nb1 < Cross platform make", None), + ] + + # when + command_result = pkgin.query_package(mock_module, package) + + # then + self.assertEquals(command_result, pkgin.PackageState.OUTDATED) + + @mock.patch('ansible_collections.community.general.plugins.modules.packaging.os.pkgin.AnsibleModule') + def test_package_with_version_found_outdated(self, mock_module): + # given + package = 'cmake316-3.16.0nb1' + parseable_flag_not_supported = 1 + mock_module.run_command.side_effect = [ + (parseable_flag_not_supported, "pkgin 0.11.7 for Darwin-18.6.0 x86_64 (using SQLite 3.27.2)", None), + (0, "cmake316-3.16.0nb1 < Cross platform make", None), + ] + + # when + command_result = pkgin.query_package(mock_module, package) + + # then + self.assertEquals(command_result, pkgin.PackageState.OUTDATED) + + @mock.patch('ansible_collections.community.general.plugins.modules.packaging.os.pkgin.AnsibleModule') + def test_package_not_found(self, mock_module): + # given + package = 'cmake320-3.20.0nb1' + parseable_flag_not_supported = 1 + mock_module.run_command.side_effect = [ + (parseable_flag_not_supported, "pkgin 0.11.7 for Darwin-18.6.0 x86_64 (using SQLite 3.27.2)", None), + (1, None, "No results found for %s" % package), + ] + + # when + command_result = pkgin.query_package(mock_module, package) + + # then + self.assertEquals(command_result, pkgin.PackageState.NOT_FOUND) + + @mock.patch('ansible_collections.community.general.plugins.modules.packaging.os.pkgin.AnsibleModule') + def test_with_parseable_flag_supported_package_is_present(self, mock_module): + # given + package = 'py37-conan' + parseable_flag_supported = 0 + mock_module.run_command.side_effect = [ + (parseable_flag_supported, "pkgin 0.11.7 for Darwin-18.6.0 x86_64 (using SQLite 3.27.2)", None), + (0, "%s-1.21.0;=;C/C++ package manager" % package, None), + ] + + # when + command_result = pkgin.query_package(mock_module, package) + + # then + self.assertEquals(command_result, pkgin.PackageState.PRESENT) + + @mock.patch('ansible_collections.community.general.plugins.modules.packaging.os.pkgin.AnsibleModule') + def test_with_parseable_flag_not_supported_package_is_present(self, mock_module): + # given + package = 'py37-conan' + parseable_flag_not_supported = 1 + mock_module.run_command.side_effect = [ + (parseable_flag_not_supported, "pkgin 0.11.7 for Darwin-18.6.0 x86_64 (using SQLite 3.27.2)", None), + (0, "%s-1.21.0 = C/C++ package manager" % package, None), + ] + + # when + command_result = pkgin.query_package(mock_module, package) + + # then + self.assertEquals(command_result, pkgin.PackageState.PRESENT)