diff --git a/CHANGELOG.md b/CHANGELOG.md index b09e43ad2805..2926b171dc45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ This project versioning is _similar_ to [Semantic Versioning](https://semver.org Versions are `MAJOR.PATCH`. ## 3001 - Sodium +## 3001 - Sodium [yyyy-mm-dd] + +### Removed + +### Deprecated + +### Changed + +### Fixed + +### Added +- [#56637](https://github.com/saltstack/salt/pull/56637) - Add ``win_wua.installed`` to the ``win_wua`` execution module + +## 3000.1 ### Removed diff --git a/salt/modules/win_wua.py b/salt/modules/win_wua.py index dad87182926a..e6c4ab681c03 100644 --- a/salt/modules/win_wua.py +++ b/salt/modules/win_wua.py @@ -109,6 +109,7 @@ def available( skip_reboot=False, categories=None, severities=None, + online=True, ): """ .. versionadded:: 2017.7.0 @@ -125,8 +126,8 @@ def available( Include driver updates in the results. Default is ``True`` summary (bool): - - True: Return a summary of updates available for each category. - - False (default): Return a detailed list of available updates. + - ``True``: Return a summary of updates available for each category. + - ``False`` (default): Return a detailed list of available updates. skip_installed (bool): Skip updates that are already installed. Default is ``True`` @@ -148,7 +149,7 @@ def available( * Critical Updates * Definition Updates - * Drivers (make sure you set drivers=True) + * Drivers (make sure you set ``drivers=True``) * Feature Packs * Security Updates * Update Rollups @@ -169,39 +170,48 @@ def available( * Critical * Important + online (bool): + Tells the Windows Update Agent go online to update its local update + database. ``True`` will go online. ``False`` will use the local + update database as is. Default is ``True`` + + .. versionadded:: Sodium + Returns: dict: Returns a dict containing either a summary or a list of updates: .. code-block:: cfg - List of Updates: - {'': {'Title': , - 'KB': <KB>, - 'GUID': <the globally unique identifier for the update> - 'Description': <description>, - 'Downloaded': <has the update been downloaded>, - 'Installed': <has the update been installed>, - 'Mandatory': <is the update mandatory>, - 'UserInput': <is user input required>, - 'EULAAccepted': <has the EULA been accepted>, - 'Severity': <update severity>, - 'NeedsReboot': <is the update installed and awaiting reboot>, - 'RebootBehavior': <will the update require a reboot>, - 'Categories': [ '<category 1>', - '<category 2>', - ...] - } - } + Dict of Updates: + {'<GUID>': { + 'Title': <title>, + 'KB': <KB>, + 'GUID': <the globally unique identifier for the update>, + 'Description': <description>, + 'Downloaded': <has the update been downloaded>, + 'Installed': <has the update been installed>, + 'Mandatory': <is the update mandatory>, + 'UserInput': <is user input required>, + 'EULAAccepted': <has the EULA been accepted>, + 'Severity': <update severity>, + 'NeedsReboot': <is the update installed and awaiting reboot>, + 'RebootBehavior': <will the update require a reboot>, + 'Categories': [ + '<category 1>', + '<category 2>', + ... ] + }} Summary of Updates: {'Total': <total number of updates returned>, 'Available': <updates that are not downloaded or installed>, 'Downloaded': <updates that are downloaded but not installed>, 'Installed': <updates installed (usually 0 unless installed=True)>, - 'Categories': { <category 1>: <total for that category>, - <category 2>: <total for category 2>, - ... } + 'Categories': { + <category 1>: <total for that category>, + <category 2>: <total for category 2>, + ... } } CLI Examples: @@ -228,7 +238,7 @@ def available( """ # Create a Windows Update Agent instance - wua = salt.utils.win_update.WindowsUpdateAgent() + wua = salt.utils.win_update.WindowsUpdateAgent(online=online) # Look for available updates = wua.available( @@ -246,7 +256,7 @@ def available( return updates.summary() if summary else updates.list() -def get(name, download=False, install=False): +def get(name, download=False, install=False, online=True): """ .. versionadded:: 2017.7.0 @@ -270,35 +280,44 @@ def get(name, download=False, install=False): first to see if the update exists, then set ``install=True`` to install the update. + online (bool): + Tells the Windows Update Agent go online to update its local update + database. ``True`` will go online. ``False`` will use the local + update database as is. Default is ``True`` + + .. versionadded:: Sodium + Returns: - dict: Returns a dict containing a list of updates that match the name if - download and install are both set to False. Should usually be a single - update, but can return multiple if a partial name is given. + dict: + Returns a dict containing a list of updates that match the name if + download and install are both set to False. Should usually be a + single update, but can return multiple if a partial name is given. If download or install is set to true it will return the results of the operation. .. code-block:: cfg - List of Updates: - {'<GUID>': {'Title': <title>, - 'KB': <KB>, - 'GUID': <the globally unique identifier for the update> - 'Description': <description>, - 'Downloaded': <has the update been downloaded>, - 'Installed': <has the update been installed>, - 'Mandatory': <is the update mandatory>, - 'UserInput': <is user input required>, - 'EULAAccepted': <has the EULA been accepted>, - 'Severity': <update severity>, - 'NeedsReboot': <is the update installed and awaiting reboot>, - 'RebootBehavior': <will the update require a reboot>, - 'Categories': [ '<category 1>', - '<category 2>', - ...] - } - } + Dict of Updates: + {'<GUID>': { + 'Title': <title>, + 'KB': <KB>, + 'GUID': <the globally unique identifier for the update>, + 'Description': <description>, + 'Downloaded': <has the update been downloaded>, + 'Installed': <has the update been installed>, + 'Mandatory': <is the update mandatory>, + 'UserInput': <is user input required>, + 'EULAAccepted': <has the EULA been accepted>, + 'Severity': <update severity>, + 'NeedsReboot': <is the update installed and awaiting reboot>, + 'RebootBehavior': <will the update require a reboot>, + 'Categories': [ + '<category 1>', + '<category 2>', + ... ] + }} CLI Examples: @@ -320,7 +339,7 @@ def get(name, download=False, install=False): salt '*' win_wua.get 'Microsoft Camera Codec Pack' """ # Create a Windows Update Agent instance - wua = salt.utils.win_update.WindowsUpdateAgent() + wua = salt.utils.win_update.WindowsUpdateAgent(online=online) # Search for Update updates = wua.search(name) @@ -347,12 +366,14 @@ def list( severities=None, download=False, install=False, + online=True, ): """ .. versionadded:: 2017.7.0 - Returns a detailed list of available updates or a summary. If download or - install is True the same list will be downloaded and/or installed. + Returns a detailed list of available updates or a summary. If ``download`` + or ``install`` is ``True`` the same list will be downloaded and/or + installed. Args: @@ -363,8 +384,8 @@ def list( Include driver updates in the results. Default is ``False`` summary (bool): - - True: Return a summary of updates available for each category. - - False (default): Return a detailed list of available updates. + - ``True``: Return a summary of updates available for each category. + - ``False`` (default): Return a detailed list of available updates. skip_installed (bool): Skip installed updates in the results. Default is ``True`` @@ -389,7 +410,7 @@ def list( * Critical Updates * Definition Updates - * Drivers (make sure you set drivers=True) + * Drivers (make sure you set ``drivers=True``) * Feature Packs * Security Updates * Update Rollups @@ -410,39 +431,48 @@ def list( * Critical * Important + online (bool): + Tells the Windows Update Agent go online to update its local update + database. ``True`` will go online. ``False`` will use the local + update database as is. Default is ``True`` + + .. versionadded:: Sodium + Returns: dict: Returns a dict containing either a summary or a list of updates: .. code-block:: cfg - List of Updates: - {'<GUID>': {'Title': <title>, - 'KB': <KB>, - 'GUID': <the globally unique identifier for the update> - 'Description': <description>, - 'Downloaded': <has the update been downloaded>, - 'Installed': <has the update been installed>, - 'Mandatory': <is the update mandatory>, - 'UserInput': <is user input required>, - 'EULAAccepted': <has the EULA been accepted>, - 'Severity': <update severity>, - 'NeedsReboot': <is the update installed and awaiting reboot>, - 'RebootBehavior': <will the update require a reboot>, - 'Categories': [ '<category 1>', - '<category 2>', - ...] - } - } + Dict of Updates: + {'<GUID>': { + 'Title': <title>, + 'KB': <KB>, + 'GUID': <the globally unique identifier for the update>, + 'Description': <description>, + 'Downloaded': <has the update been downloaded>, + 'Installed': <has the update been installed>, + 'Mandatory': <is the update mandatory>, + 'UserInput': <is user input required>, + 'EULAAccepted': <has the EULA been accepted>, + 'Severity': <update severity>, + 'NeedsReboot': <is the update installed and awaiting reboot>, + 'RebootBehavior': <will the update require a reboot>, + 'Categories': [ + '<category 1>', + '<category 2>', + ... ] + }} Summary of Updates: {'Total': <total number of updates returned>, 'Available': <updates that are not downloaded or installed>, 'Downloaded': <updates that are downloaded but not installed>, 'Installed': <updates installed (usually 0 unless installed=True)>, - 'Categories': { <category 1>: <total for that category>, - <category 2>: <total for category 2>, - ... } + 'Categories': { + <category 1>: <total for that category>, + <category 2>: <total for category 2>, + ... } } CLI Examples: @@ -468,7 +498,7 @@ def list( salt '*' win_wua.list categories=['Feature Packs','Windows 8.1'] summary=True """ # Create a Windows Update Agent instance - wua = salt.utils.win_update.WindowsUpdateAgent() + wua = salt.utils.win_update.WindowsUpdateAgent(online=online) # Search for Update updates = wua.available( @@ -495,12 +525,77 @@ def list( return ret +def installed(summary=False, kbs_only=False): + """ + .. versionadded:: Sodium + + Get a list of all updates that are currently installed on the system. + + .. note:: + + This list may not necessarily match the Update History on the machine. + This will only show the updates that apply to the current build of + Windows. So, for example, the system may have shipped with Windows 10 + Build 1607. That machine received updates to the 1607 build. Later the + machine was upgraded to a newer feature release, 1803 for example. Then + more updates were applied. This will only return the updates applied to + the 1803 build and not those applied when the system was at the 1607 + build. + + Args: + + summary (bool): + Return a summary instead of a detailed list of updates. ``True`` + will return a Summary, ``False`` will return a detailed list of + installed updates. Default is ``False`` + + kbs_only (bool): + Only return a list of KBs installed on the system. If this parameter + is passed, the ``summary`` parameter will be ignored. Default is + ``False`` + + Returns: + dict: + Returns a dictionary of either a Summary or a detailed list of + updates installed on the system when ``kbs_only=False`` + + list: + Returns a list of KBs installed on the system when ``kbs_only=True`` + + CLI Examples: + + .. code-block:: bash + + # Get a detailed list of all applicable updates installed on the system + salt '*' win_wua.installed + + # Get a summary of all applicable updates installed on the system + salt '*' win_wua.installed summary=True + + # Get a simple list of KBs installed on the system + salt '*' win_wua.installed kbs_only=True + """ + # Create a Windows Update Agent instance. Since we're only listing installed + # updates, there's no need to go online to update the Windows Update db + wua = salt.utils.win_update.WindowsUpdateAgent(online=False) + updates = wua.installed() # Get installed Updates objects + results = updates.list() # Convert to list + + if kbs_only: + list_kbs = set() + for item in results: + list_kbs.update(results[item]["KBs"]) + return sorted(list_kbs) + + return updates.summary() if summary else results + + def download(names): """ .. versionadded:: 2017.7.0 Downloads updates that match the list of passed identifiers. It's easier to - use this function by using list_updates and setting install=True. + use this function by using list_updates and setting ``download=True``. Args: @@ -509,15 +604,16 @@ def download(names): combination of GUIDs, KB numbers, or names. GUIDs or KBs are preferred. - .. note:: - An error will be raised if there are more results than there are items - in the names parameter + .. note:: + + An error will be raised if there are more results than there are + items in the names parameter Returns: dict: A dictionary containing the details about the downloaded updates - CLI Examples: + CLI Example: .. code-block:: bash @@ -542,7 +638,7 @@ def download(names): if updates.count() > len(names): raise CommandExecutionError( - "Multiple updates found, names need to be " "more specific" + "Multiple updates found, names need to be more specific" ) return wua.download(updates) @@ -553,7 +649,7 @@ def install(names): .. versionadded:: 2017.7.0 Installs updates that match the list of identifiers. It may be easier to use - the list_updates function and set install=True. + the list_updates function and set ``install=True``. Args: @@ -563,6 +659,7 @@ def install(names): preferred. .. note:: + An error will be raised if there are more results than there are items in the names parameter @@ -595,7 +692,7 @@ def install(names): if updates.count() > len(names): raise CommandExecutionError( - "Multiple updates found, names need to be " "more specific" + "Multiple updates found, names need to be more specific" ) return wua.install(updates) @@ -672,7 +769,8 @@ def set_wu_settings( Number from 1 to 4 indicating the update level: 1. Never check for updates - 2. Check for updates but let me choose whether to download and install them + 2. Check for updates but let me choose whether to download and + install them 3. Download updates but let me choose whether to install them 4. Install updates automatically @@ -804,7 +902,7 @@ def set_wu_settings( } if day not in days: ret["Comment"] = ( - "Day needs to be one of the following: Everyday," + "Day needs to be one of the following: Everyday, " "Monday, Tuesday, Wednesday, Thursday, Friday, " "Saturday" ) @@ -824,15 +922,15 @@ def set_wu_settings( # treat it as an integer if not isinstance(time, six.string_types): ret["Comment"] = ( - "Time argument needs to be a string; it may need to" + "Time argument needs to be a string; it may need to " "be quoted. Passed {0}. Time not set.".format(time) ) ret["Success"] = False # Check for colon in the time elif ":" not in time: ret["Comment"] = ( - "Time argument needs to be in 00:00 format." - " Passed {0}. Time not set.".format(time) + "Time argument needs to be in 00:00 format. " + "Passed {0}. Time not set.".format(time) ) ret["Success"] = False else: @@ -906,35 +1004,46 @@ def get_wu_settings(): Featured Updates: Boolean value that indicates whether to display notifications for featured updates. + Group Policy Required (Read-only): Boolean value that indicates whether Group Policy requires the Automatic Updates service. + Microsoft Update: Boolean value that indicates whether to turn on Microsoft Update for other Microsoft Products + Needs Reboot: Boolean value that indicates whether the machine is in a reboot pending state. + Non Admins Elevated: Boolean value that indicates whether non-administrators can perform some update-related actions without administrator approval. + Notification Level: + Number 1 to 4 indicating the update level: + 1. Never check for updates 2. Check for updates but let me choose whether to download and install them 3. Download updates but let me choose whether to install them 4. Install updates automatically + Read Only (Read-only): Boolean value that indicates whether the Automatic Update settings are read-only. + Recommended Updates: Boolean value that indicates whether to include optional or recommended updates when a search for updates and installation of updates is performed. + Scheduled Day: Days of the week on which Automatic Updates installs or uninstalls updates. + Scheduled Time: Time at which Automatic Updates installs or uninstalls updates. @@ -1019,7 +1128,7 @@ def get_needs_reboot(): Returns: - bool: True if the system requires a reboot, otherwise False + bool: ``True`` if the system requires a reboot, otherwise ``False`` CLI Examples: diff --git a/salt/utils/win_update.py b/salt/utils/win_update.py index 7d3ee4b98ebd..6ff7225c0129 100644 --- a/salt/utils/win_update.py +++ b/salt/utils/win_update.py @@ -32,7 +32,7 @@ def __virtual__(): if not salt.utils.platform.is_windows(): - return False, "win_update: Not available on Windows" + return False, "win_update: Only available on Windows" if not HAS_PYWIN32: return False, "win_update: Missing pywin32" return __virtualname__ @@ -43,7 +43,8 @@ class Updates(object): Wrapper around the 'Microsoft.Update.UpdateColl' instance Adds the list and summary functions. For use by the WindowUpdateAgent class. - Usage: + Code Example: + .. code-block:: python # Create an instance @@ -110,24 +111,25 @@ def list(self): .. code-block:: cfg - List of Updates: - {'<GUID>': {'Title': <title>, - 'KB': <KB>, - 'GUID': <the globally unique identifier for the update> - 'Description': <description>, - 'Downloaded': <has the update been downloaded>, - 'Installed': <has the update been installed>, - 'Mandatory': <is the update mandatory>, - 'UserInput': <is user input required>, - 'EULAAccepted': <has the EULA been accepted>, - 'Severity': <update severity>, - 'NeedsReboot': <is the update installed and awaiting reboot>, - 'RebootBehavior': <will the update require a reboot>, - 'Categories': [ '<category 1>', - '<category 2>', - ...] - } - } + Dict of Updates: + {'<GUID>': { + 'Title': <title>, + 'KB': <KB>, + 'GUID': <the globally unique identifier for the update>, + 'Description': <description>, + 'Downloaded': <has the update been downloaded>, + 'Installed': <has the update been installed>, + 'Mandatory': <is the update mandatory>, + 'UserInput': <is user input required>, + 'EULAAccepted': <has the EULA been accepted>, + 'Severity': <update severity>, + 'NeedsReboot': <is the update installed and awaiting reboot>, + 'RebootBehavior': <will the update require a reboot>, + 'Categories': [ + '<category 1>', + '<category 2>', + ... ] + }} Code Example: @@ -182,10 +184,12 @@ def summary(self): 'Available': <updates that are not downloaded or installed>, 'Downloaded': <updates that are downloaded but not installed>, 'Installed': <updates installed (usually 0 unless installed=True)>, - 'Categories': { <category 1>: <total for that category>, - <category 2>: <total for category 2>, - ... } + 'Categories': { + <category 1>: <total for that category>, + <category 2>: <total for category 2>, + ... } } + Code Example: .. code-block:: python @@ -251,7 +255,6 @@ def summary(self): class WindowsUpdateAgent(object): """ Class for working with the Windows update agent - """ # Error codes found at the following site: @@ -285,12 +288,21 @@ class WindowsUpdateAgent(object): -4292607995: "Reboot required: 0x00240005", } - def __init__(self): + def __init__(self, online=True): """ Initialize the session and load all updates into the ``_updates`` collection. This collection is used by the other class functions instead of querying Windows update (expensive). + Args: + + online (bool): + Tells the Windows Update Agent go online to update its local + update database. ``True`` will go online. ``False`` will use the + local update database as is. Default is ``True`` + + .. versionadded:: Sodium + Need to look at the possibility of loading this into ``__context__`` """ # Initialize the PyCom system @@ -302,7 +314,7 @@ def __init__(self): # Create Collection for Updates self._updates = win32com.client.Dispatch("Microsoft.Update.UpdateColl") - self.refresh() + self.refresh(online=online) def updates(self): """ @@ -310,8 +322,12 @@ def updates(self): Updates class to expose the list and summary functions. Returns: - Updates: An instance of the Updates class with all updates for the - system. + + Updates: + An instance of the Updates class with all updates for the + system. + + Code Example: .. code-block:: python @@ -333,12 +349,21 @@ def updates(self): return updates - def refresh(self): + def refresh(self, online=True): """ Refresh the contents of the ``_updates`` collection. This gets all updates in the Windows Update system and loads them into the collection. This is the part that is slow. + Args: + + online (bool): + Tells the Windows Update Agent go online to update its local + update database. ``True`` will go online. ``False`` will use the + local update database as is. Default is ``True`` + + .. versionadded:: Sodium + Code Example: .. code-block:: python @@ -352,6 +377,7 @@ def refresh(self): # Create searcher object searcher = self._session.CreateUpdateSearcher() + searcher.Online = online self._session.ClientApplicationID = "Salt: Load Updates" # Load all updates into the updates collection @@ -373,6 +399,32 @@ def refresh(self): self._updates = results.Updates + def installed(self): + """ + Gets a list of all updates available on the system that have the + ``IsInstalled`` attribute set to ``True``. + + Returns: + + Updates: An instance of Updates with the results. + + Code Example: + + .. code-block:: python + + import salt.utils.win_update + wua = salt.utils.win_update.WindowsUpdateAgent(online=False) + installed_updates = wua.installed() + """ + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa386099(v=vs.85).aspx + updates = Updates() + + for update in self._updates: + if salt.utils.data.is_true(update.IsInstalled): + updates.updates.Add(update) + + return updates + def available( self, skip_hidden=True, @@ -390,23 +442,27 @@ def available( Args: - skip_hidden (bool): Skip hidden updates. Default is True + skip_hidden (bool): + Skip hidden updates. Default is ``True`` - skip_installed (bool): Skip installed updates. Default is True + skip_installed (bool): + Skip installed updates. Default is ``True`` - skip_mandatory (bool): Skip mandatory updates. Default is False + skip_mandatory (bool): + Skip mandatory updates. Default is ``False`` - skip_reboot (bool): Skip updates that can or do require reboot. - Default is False + skip_reboot (bool): + Skip updates that can or do require reboot. Default is ``False`` - software (bool): Include software updates. Default is True + software (bool): + Include software updates. Default is ``True`` - drivers (bool): Include driver updates. Default is True + drivers (bool): + Include driver updates. Default is ``True`` - categories (list): Include updates that have these categories. - Default is none (all categories). - - Categories include the following: + categories (list): + Include updates that have these categories. Default is none + (all categories). Categories include the following: * Critical Updates * Definition Updates @@ -422,16 +478,17 @@ def available( * Windows 8.1 and later drivers * Windows Defender - severities (list): Include updates that have these severities. - Default is none (all severities). - - Severities include the following: + severities (list): + Include updates that have these severities. Default is none + (all severities). Severities include the following: * Critical * Important - .. note:: All updates are either software or driver updates. If both - ``software`` and ``drivers`` is False, nothing will be returned. + .. note:: + + All updates are either software or driver updates. If both + ``software`` and ``drivers`` is ``False``, nothing will be returned. Returns: @@ -445,7 +502,7 @@ def available( wua = salt.utils.win_update.WindowsUpdateAgent() # Gets all updates and shows a summary - updates = wua.available + updates = wua.available() updates.summary() # Get a list of Critical updates @@ -502,11 +559,11 @@ def search(self, search_string): Args: - search_string (str, list): The search string to use to find the - update. This can be the GUID or KB of the update (preferred). It can - also be the full Title of the update or any part of the Title. A - partial Title search is less specific and can return multiple - results. + search_string (str, list): + The search string to use to find the update. This can be the + GUID or KB of the update (preferred). It can also be the full + Title of the update or any part of the Title. A partial Title + search is less specific and can return multiple results. Returns: Updates: An instance of Updates with the results of the search @@ -568,8 +625,9 @@ def download(self, updates): Args: - updates (Updates): An instance of the Updates class containing a - the updates to be downloaded. + updates (Updates): + An instance of the Updates class containing a the updates to be + downloaded. Returns: dict: A dictionary containing the results of the download @@ -681,8 +739,9 @@ def install(self, updates): Args: - updates (Updates): An instance of the Updates class containing a - the updates to be installed. + updates (Updates): + An instance of the Updates class containing a the updates to be + installed. Returns: dict: A dictionary containing the results of the installation @@ -789,19 +848,23 @@ def uninstall(self, updates): Uninstall the updates passed in the updates collection. Load the updates collection using the ``search`` or ``available`` functions. - .. note:: Starting with Windows 10 the Windows Update Agent is unable to - uninstall updates. An ``Uninstall Not Allowed`` error is returned. If - this error is encountered this function will instead attempt to use - ``dism.exe`` to perform the uninstallation. ``dism.exe`` may fail to - to find the KB number for the package. In that case, removal will fail. + .. note:: + + Starting with Windows 10 the Windows Update Agent is unable to + uninstall updates. An ``Uninstall Not Allowed`` error is returned. + If this error is encountered this function will instead attempt to + use ``dism.exe`` to perform the un-installation. ``dism.exe`` may + fail to to find the KB number for the package. In that case, removal + will fail. Args: - updates (Updates): An instance of the Updates class containing a - the updates to be uninstalled. + updates (Updates): + An instance of the Updates class containing a the updates to be + uninstalled. Returns: - dict: A dictionary containing the results of the uninstallation + dict: A dictionary containing the results of the un-installation Code Example: @@ -825,7 +888,7 @@ def uninstall(self, updates): return ret installer = self._session.CreateUpdateInstaller() - self._session.ClientApplicationID = "Salt: Install Update" + self._session.ClientApplicationID = "Salt: Uninstall Update" with salt.utils.winapi.Com(): uninstall_list = win32com.client.Dispatch("Microsoft.Update.UpdateColl") @@ -920,7 +983,7 @@ def uninstall(self, updates): log.debug("NeedsReboot: %s", ret["NeedsReboot"]) # Refresh the Updates Table - self.refresh() + self.refresh(online=False) reboot = {0: "Never Reboot", 1: "Always Reboot", 2: "Poss Reboot"} @@ -941,7 +1004,7 @@ def uninstall(self, updates): return ret - # Found a differenct exception, Raise error + # Found a different exception, Raise error log.error("Uninstall Failed: %s", failure_code) raise CommandExecutionError(failure_code) @@ -984,7 +1047,8 @@ def _run(self, cmd): Internal function for running commands. Used by the uninstall function. Args: - cmd (str, list): The command to run + cmd (str, list): + The command to run Returns: str: The stdout of the command @@ -1012,7 +1076,7 @@ def needs_reboot(): Returns: - bool: True if the system requires a reboot, False if not + bool: ``True`` if the system requires a reboot, ``False`` if not CLI Examples: diff --git a/tests/unit/modules/test_win_wua.py b/tests/unit/modules/test_win_wua.py new file mode 100644 index 000000000000..18492421e736 --- /dev/null +++ b/tests/unit/modules/test_win_wua.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +""" +Test the win_wua execution module +""" +# Import Python Libs +from __future__ import absolute_import, print_function, unicode_literals + +# Import Salt Libs +import salt.modules.win_wua as win_wua +import salt.utils.platform +import salt.utils.win_update + +# Import Salt Testing Libs +from tests.support.mock import patch +from tests.support.unit import TestCase, skipIf + +UPDATES_LIST = { + "ca3bb521-a8ea-4e26-a563-2ad6e3108b9a": {"KBs": ["KB4481252"]}, + "07609d43-d518-4e77-856e-d1b316d1b8a8": {"KBs": ["KB925673"]}, + "fbaa5360-a440-49d8-a3b6-0c4fc7ecaa19": {"KBs": ["KB4481252"]}, + "a873372b-7a5c-443c-8022-cd59a550bef4": {"KBs": ["KB3193497"]}, + "14075cbe-822e-4004-963b-f50e08d45563": {"KBs": ["KB4540723"]}, + "d931e99c-4dda-4d39-9905-0f6a73f7195f": {"KBs": ["KB3193497"]}, + "afda9e11-44a0-4602-9e9b-423af11ecaed": {"KBs": ["KB4541329"]}, + "a0f997b1-1abe-4a46-941f-b37f732f9fbd": {"KBs": ["KB3193497"]}, + "eac02b09-d745-4891-b80f-400e0e5e4b6d": {"KBs": ["KB4052623"]}, + "0689e74b-54d1-4f55-a916-96e3c737db90": {"KBs": ["KB890830"]}, +} +UPDATES_SUMMARY = {"Installed": 10} + + +class Updates(object): + @staticmethod + def list(): + return UPDATES_LIST + + @staticmethod + def summary(): + return UPDATES_SUMMARY + + +@skipIf(not salt.utils.platform.is_windows(), "System is not Windows") +class WinWuaInstalledTestCase(TestCase): + """ + Test the functions in the win_wua.installed function + """ + + def test_installed(self): + """ + Test installed function default + """ + expected = UPDATES_LIST + with patch("salt.utils.winapi.Com", autospec=True), patch( + "win32com.client.Dispatch", autospec=True + ), patch.object( + salt.utils.win_update.WindowsUpdateAgent, "refresh", autospec=True + ), patch.object( + salt.utils.win_update, "Updates", autospec=True, return_value=Updates() + ): + result = win_wua.installed() + self.assertDictEqual(result, expected) + + def test_installed_summary(self): + """ + Test installed function with summary=True + """ + expected = UPDATES_SUMMARY + # Remove all updates that are not installed + with patch("salt.utils.winapi.Com", autospec=True), patch( + "win32com.client.Dispatch", autospec=True + ), patch.object( + salt.utils.win_update.WindowsUpdateAgent, "refresh", autospec=True + ), patch.object( + salt.utils.win_update, "Updates", autospec=True, return_value=Updates() + ): + result = win_wua.installed(summary=True) + self.assertDictEqual(result, expected) + + def test_installed_kbs_only(self): + """ + Test installed function with kbs_only=True + """ + expected = set() + for update in UPDATES_LIST: + expected.update(UPDATES_LIST[update]["KBs"]) + expected = sorted(expected) + # Remove all updates that are not installed + with patch("salt.utils.winapi.Com", autospec=True), patch( + "win32com.client.Dispatch", autospec=True + ), patch.object( + salt.utils.win_update.WindowsUpdateAgent, "refresh", autospec=True + ), patch.object( + salt.utils.win_update, "Updates", autospec=True, return_value=Updates() + ): + result = win_wua.installed(kbs_only=True) + self.assertListEqual(result, expected) diff --git a/tests/unit/utils/test_win_update.py b/tests/unit/utils/test_win_update.py new file mode 100644 index 000000000000..c660ccd03c58 --- /dev/null +++ b/tests/unit/utils/test_win_update.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +# Import Python Libs +from __future__ import absolute_import, print_function, unicode_literals + +# Import Salt Libs +import salt.utils.platform +import salt.utils.win_update as win_update + +# Import Salt Testing Libs +from tests.support.mock import MagicMock, patch +from tests.support.unit import TestCase, skipIf + + +@skipIf(not salt.utils.platform.is_windows(), "System is not Windows") +class WinUpdateTestCase(TestCase): + """ + Test cases for salt.utils.win_update + """ + + def test_installed_no_updates(self): + """ + Test installed when there are no updates on the system + """ + with patch("salt.utils.winapi.Com", autospec=True), patch( + "win32com.client.Dispatch", autospec=True + ), patch.object(win_update.WindowsUpdateAgent, "refresh", autospec=True): + wua = win_update.WindowsUpdateAgent(online=False) + wua._updates = [] + + installed_updates = wua.installed() + + assert installed_updates.updates.Add.call_count == 0 + + def test_installed_no_updates_installed(self): + """ + Test installed when there are no Installed updates on the system + """ + with patch("salt.utils.winapi.Com", autospec=True), patch( + "win32com.client.Dispatch", autospec=True + ), patch.object(win_update.WindowsUpdateAgent, "refresh", autospec=True): + wua = win_update.WindowsUpdateAgent(online=False) + + wua._updates = [ + MagicMock(IsInstalled=False), + MagicMock(IsInstalled=False), + MagicMock(IsInstalled=False), + ] + + installed_updates = wua.installed() + + assert installed_updates.updates.Add.call_count == 0 + + def test_installed_updates_all_installed(self): + """ + Test installed when all updates on the system are Installed + """ + with patch("salt.utils.winapi.Com", autospec=True), patch( + "win32com.client.Dispatch", autospec=True + ), patch.object(win_update.WindowsUpdateAgent, "refresh", autospec=True): + wua = win_update.WindowsUpdateAgent(online=False) + + wua._updates = [ + MagicMock(IsInstalled=True), + MagicMock(IsInstalled=True), + MagicMock(IsInstalled=True), + ] + + installed_updates = wua.installed() + + assert installed_updates.updates.Add.call_count == 3 + + def test_installed_updates_some_installed(self): + """ + Test installed when some updates are installed on the system + """ + with patch("salt.utils.winapi.Com", autospec=True), patch( + "win32com.client.Dispatch", autospec=True + ), patch.object(win_update.WindowsUpdateAgent, "refresh", autospec=True): + wua = win_update.WindowsUpdateAgent(online=False) + + wua._updates = [ + MagicMock(IsInstalled=True), + MagicMock(IsInstalled=False), + MagicMock(IsInstalled=True), + MagicMock(IsInstalled=False), + MagicMock(IsInstalled=True), + ] + + installed_updates = wua.installed() + + assert installed_updates.updates.Add.call_count == 3