Add the ability to remove a KB from the DISM module

Adds the ability to remove a KB using the DISM module by passing
just the KB number
Adds a state for doing the same thing
This commit is contained in:
Twangboy 2022-09-02 11:40:30 -06:00 committed by Megan Wilhite
parent 29e00d7f11
commit 38e23f1b44
5 changed files with 325 additions and 7 deletions

1
changelog/62366.added Normal file
View file

@ -0,0 +1 @@
Added the ability to remove a KB using the DISM state/execution modules

View file

@ -11,6 +11,7 @@ import re
import salt.utils.platform
import salt.utils.versions
from salt.exceptions import CommandExecutionError
log = logging.getLogger(__name__)
__virtualname__ = "dism"
@ -517,12 +518,13 @@ def remove_package(package, image=None, restart=False):
Args:
package (str): The full path to the package. Can be either a .cab file
or a folder. Should point to the original source of the package, not
to where the file is installed. This can also be the name of a package as listed in
``dism.installed_packages``
to where the file is installed. This can also be the name of a
package as listed in ``dism.installed_packages``
image (Optional[str]): The path to the root directory of an offline
Windows image. If `None` is passed, the running operating system is
targeted. Default is None.
restart (Optional[bool]): Reboot the machine if required by the install
restart (Optional[bool]): Reboot the machine if required by the
uninstall
Returns:
dict: A dictionary containing the results of the command
@ -555,6 +557,73 @@ def remove_package(package, image=None, restart=False):
return __salt__["cmd.run_all"](cmd)
def get_kg_package_name(kb, image=None):
"""
Get the actual package name on the system based on the KB name
Args:
kb (str): The name of the KB to remove. Can also be just the KB number
image (Optional[str]): The path to the root directory of an offline
Windows image. If `None` is passed, the running operating system is
targeted. Default is None.
Returns:
str: The name of the package found on the system
None: If the package is not installed on the system
CLI Example:
.. code-block:: bash
# Get the package name for KB1231231
salt '*' dism.get_kb_package_name KB1231231
# Get the package name for KB1231231 using just the number
salt '*' dism.get_kb_package_name 1231231
"""
packages = installed_packages(image=image)
search = kb.upper() if kb.lower().startswith("kb") else "KB{}".format(kb)
for package in packages:
if "_{}~".format(search) in package:
return package
return None
def remove_kb(kb, image=None, restart=False):
"""
Remove a package by passing a KB number. This searches the installed
packages to get the full package name of the KB. It then calls the
``dism.remove_package`` function to remove the package.
Args:
kb (str): The name of the KB to remove. Can also be just the KB number
image (Optional[str]): The path to the root directory of an offline
Windows image. If `None` is passed, the running operating system is
targeted. Default is None.
restart (Optional[bool]): Reboot the machine if required by the
uninstall
Returns:
dict: A dictionary containing the results of the command
CLI Example:
.. code-block:: bash
# Remove the KB5007575 just passing the number
salt '*' dism.remove_kb 5007575
# Remove the KB5007575 just passing the full name
salt '*' dism.remove_kb KB5007575
"""
pkg_name = get_kb_package_name(kb=kb, image=image)
if pkg_name is None:
msg = "{} not installed".format(search)
raise CommandExecutionError(msg)
log.debug("Found: %s", pkg_name)
return remove_package(package=pkg_name, image=image, restart=restart)
def installed_packages(image=None):
"""
List the packages installed on the system
@ -573,7 +642,12 @@ def installed_packages(image=None):
salt '*' dism.installed_packages
"""
return _get_components("Package Identity", "Packages", "Installed")
return _get_components(
type_regex="Package Identity",
plural_type="Packages",
install_value="Installed",
image=image,
)
def package_info(package, image=None):

View file

@ -99,7 +99,8 @@ def capability_removed(name, image=None, restart=False):
image (Optional[str]): The path to the root directory of an offline
Windows image. If `None` is passed, the running operating system is
targeted. Default is None.
restart (Optional[bool]): Reboot the machine if required by the install
restart (Optional[bool]): Reboot the machine if required by the
uninstall
Example:
Run ``dism.installed_capabilities`` to get a list of installed
@ -223,7 +224,8 @@ def feature_removed(name, remove_payload=False, image=None, restart=False):
image (Optional[str]): The path to the root directory of an offline
Windows image. If `None` is passed, the running operating system is
targeted. Default is None.
restart (Optional[bool]): Reboot the machine if required by the install
restart (Optional[bool]): Reboot the machine if required by the
uninstall
Example:
Run ``dism.installed_features`` to get a list of installed features.
@ -352,7 +354,8 @@ def package_removed(name, image=None, restart=False):
image (Optional[str]): The path to the root directory of an offline
Windows image. If `None` is passed, the running operating system is
targeted. Default is None.
restart (Optional[bool]): Reboot the machine if required by the install
restart (Optional[bool]): Reboot the machine if required by the
uninstall
Example:
@ -414,3 +417,66 @@ def package_removed(name, image=None, restart=False):
ret["changes"]["package"] = changes
return ret
def kb_removed(name, image=None, restart=False):
"""
Uninstall a KB package
Args:
name (str): The name of the KB. Can be with or without the KB at the
beginning.
image (Optional[str]): The path to the root directory of an offline
Windows image. If `None` is passed, the running operating system is
targeted. Default is None.
restart (Optional[bool]): Reboot the machine if required by the
uninstall
Example:
.. code-block:: yaml
# Example using full KB name
remove_KB1231231:
dism.package_installed:
- name: KB1231231
# Example using just he KB number
remove_KB1231231:
dism.package_installed:
- name: 1231231
"""
ret = {"name": name, "result": True, "comment": "", "changes": {}}
pkg_name = __salt__["dism.get_kb_package_name"](kb=name, image=image)
# If pkg_name is None, the package is not installed
if pkg_name is None:
ret["comment"] = "{} is not installed".format(name)
return ret
if __opts__["test"]:
ret["changes"]["package"] = "{} will be removed".format(name)
ret["result"] = None
return ret
# Fail if using a non-existent package path
old = __salt__["dism.installed_packages"]()
# Remove the package
status = __salt__["dism.remove_kb"](kb=name, image=image, restart=restart)
if status["retcode"] not in [0, 1641, 3010]:
ret["comment"] = "Failed to remove {}: {}".format(name, status["stdout"])
ret["result"] = False
return ret
new = __salt__["dism.installed_packages"]()
changes = salt.utils.data.compare_lists(old, new)
if changes:
ret["comment"] = "Removed {}".format(name)
ret["changes"] = status
ret["changes"]["package"] = changes
return ret

View file

@ -1,6 +1,7 @@
import pytest
import salt.modules.win_dism as dism
from salt.exceptions import CommandExecutionError
from tests.support.mock import MagicMock, patch
@ -322,6 +323,49 @@ def test_remove_package():
)
def test_remove_kb():
"""
Test uninstalling a KB with DISM
"""
pkg_name = "Package_for_KB1002345~31bf3856ad364e35~amd64~~22000.345.1.1"
mock_search = MagicMock(return_value=[pkg_name])
mock_remove = MagicMock()
with patch("salt.modules.win_dism.installed_packages", mock_search):
with patch("salt.modules.win_dism.remove_package", mock_remove):
dism.remove_kb("KB1002345")
mock_remove.assert_called_once_with(
package=pkg_name,
image=None,
restart=False,
)
def test_remove_kb_number():
"""
Test uninstalling a KB with DISM with just the KB number
"""
pkg_name = "Package_for_KB1002345~31bf3856ad364e35~amd64~~22000.345.1.1"
mock_search = MagicMock(return_value=[pkg_name])
mock_remove = MagicMock()
with patch("salt.modules.win_dism.installed_packages", mock_search):
with patch("salt.modules.win_dism.remove_package", mock_remove):
dism.remove_kb("1002345")
mock_remove.assert_called_once_with(
package=pkg_name,
image=None,
restart=False,
)
def test_remove_kb_not_found():
pkg_name = "Package_for_KB1002345~31bf3856ad364e35~amd64~~22000.345.1.1"
mock_search = MagicMock(return_value=[pkg_name])
with patch("salt.modules.win_dism.installed_packages", mock_search):
with pytest.raises(CommandExecutionError) as err:
dism.remove_kb("1001111")
assert str(err.value) == "KB1001111 not installed"
def test_installed_packages():
"""
Test getting all the installed features

View file

@ -548,3 +548,136 @@ def test_package_removed_removed():
mock_removed.assert_called_once_with()
assert not mock_remove.called
assert out == expected
def test_kb_removed():
"""
Test removing a package using the KB number
"""
pkg_name = "Package_for_KB1231231~31bf3856ad364e35~amd64~~22000.345.1.1"
expected = {
"comment": "Removed KB1231231",
"changes": {"package": {"old": [pkg_name]}, "retcode": 0},
"name": "KB1231231",
"result": True,
}
pre_removed = [
"Package_for_KB5007575~31bf3856ad364e35~amd64~~22000.345.1.1",
"Package_for_KB5012170~31bf3856ad364e35~amd64~~22000.850.1.1",
pkg_name,
]
post_removed = [
"Package_for_KB5007575~31bf3856ad364e35~amd64~~22000.345.1.1",
"Package_for_KB5012170~31bf3856ad364e35~amd64~~22000.850.1.1",
]
mock_get_name = MagicMock(return_value=pkg_name)
mock_installed = MagicMock(side_effect=[pre_removed, post_removed])
mock_remove = MagicMock(return_value={"retcode": 0})
patch_salt = {
"dism.get_kb_package_name": mock_get_name,
"dism.installed_packages": mock_installed,
"dism.remove_kb": mock_remove,
}
with patch.dict(dism.__salt__, patch_salt):
with patch.dict(dism.__opts__, {"test": False}):
result = dism.kb_removed("KB1231231")
mock_remove.assert_called_once_with(
kb="KB1231231", image=None, restart=False
)
assert result == expected
def test_kb_removed_not_installed():
"""
Test removing a package using the KB number when the package is not
installed
"""
pkg_name = None
expected = {
"comment": "KB1231231 is not installed",
"changes": {},
"name": "KB1231231",
"result": True,
}
mock_get_name = MagicMock(return_value=pkg_name)
patch_salt = {"dism.get_kb_package_name": mock_get_name}
with patch.dict(dism.__salt__, patch_salt):
result = dism.kb_removed("KB1231231")
assert result == expected
def test_kb_removed_test():
"""
Test removing a package using the KB number with test=True
"""
pkg_name = "Package_for_KB1231231~31bf3856ad364e35~amd64~~22000.345.1.1"
expected = {
"comment": "",
"changes": {"package": "KB1231231 will be removed"},
"name": "KB1231231",
"result": None,
}
pre_removed = [
"Package_for_KB5007575~31bf3856ad364e35~amd64~~22000.345.1.1",
"Package_for_KB5012170~31bf3856ad364e35~amd64~~22000.850.1.1",
pkg_name,
]
post_removed = [
"Package_for_KB5007575~31bf3856ad364e35~amd64~~22000.345.1.1",
"Package_for_KB5012170~31bf3856ad364e35~amd64~~22000.850.1.1",
]
mock_get_name = MagicMock(return_value=pkg_name)
mock_installed = MagicMock(side_effect=[pre_removed, post_removed])
patch_salt = {
"dism.get_kb_package_name": mock_get_name,
"dism.installed_packages": mock_installed,
}
with patch.dict(dism.__salt__, patch_salt):
with patch.dict(dism.__opts__, {"test": True}):
result = dism.kb_removed("KB1231231")
assert result == expected
def test_kb_removed_failed():
"""
Test removing a package using the KB number with a failure
"""
pkg_name = "Package_for_KB1231231~31bf3856ad364e35~amd64~~22000.345.1.1"
expected = {
"comment": "Failed to remove KB1231231: error",
"changes": {},
"name": "KB1231231",
"result": False,
}
pre_removed = [
"Package_for_KB5007575~31bf3856ad364e35~amd64~~22000.345.1.1",
"Package_for_KB5012170~31bf3856ad364e35~amd64~~22000.850.1.1",
pkg_name,
]
post_removed = [
"Package_for_KB5007575~31bf3856ad364e35~amd64~~22000.345.1.1",
"Package_for_KB5012170~31bf3856ad364e35~amd64~~22000.850.1.1",
]
mock_get_name = MagicMock(return_value=pkg_name)
mock_installed = MagicMock(side_effect=[pre_removed, post_removed])
mock_remove = MagicMock(return_value={"retcode": 1, "stdout": "error"})
patch_salt = {
"dism.get_kb_package_name": mock_get_name,
"dism.installed_packages": mock_installed,
"dism.remove_kb": mock_remove,
}
with patch.dict(dism.__salt__, patch_salt):
with patch.dict(dism.__opts__, {"test": False}):
result = dism.kb_removed("KB1231231")
mock_remove.assert_called_once_with(
kb="KB1231231", image=None, restart=False
)
assert result == expected