1"""
2Classes for working with Windows Update Agent
3"""
4import logging
5import subprocess
6
7import salt.utils.args
8import salt.utils.data
9import salt.utils.winapi
10from salt.exceptions import CommandExecutionError
11
12try:
13    import win32com.client
14    import pywintypes
15
16    HAS_PYWIN32 = True
17except ImportError:
18    HAS_PYWIN32 = False
19
20log = logging.getLogger(__name__)
21
22REBOOT_BEHAVIOR = {
23    0: "Never Requires Reboot",
24    1: "Always Requires Reboot",
25    2: "Can Require Reboot",
26}
27
28__virtualname__ = "win_update"
29
30
31def __virtual__():
32    if not salt.utils.platform.is_windows():
33        return False, "win_update: Only available on Windows"
34    if not HAS_PYWIN32:
35        return False, "win_update: Missing pywin32"
36    return __virtualname__
37
38
39class Updates:
40    """
41    Wrapper around the 'Microsoft.Update.UpdateColl' instance
42    Adds the list and summary functions. For use by the WindowUpdateAgent class.
43
44    Code Example:
45
46    .. code-block:: python
47
48        # Create an instance
49        updates = Updates()
50
51        # Bind to the collection object
52        found = updates.updates
53
54        # This exposes Collections properties and methods
55        # https://msdn.microsoft.com/en-us/library/windows/desktop/aa386107(v=vs.85).aspx
56        found.Count
57        found.Add
58
59        # To use custom functions, use the original instance
60        # Return the number of updates inside the collection
61        updates.count()
62
63        # Return a list of updates in the collection and details in a dictionary
64        updates.list()
65
66        # Return a summary of the contents of the updates collection
67        updates.summary()
68    """
69
70    update_types = {1: "Software", 2: "Driver"}
71
72    def __init__(self):
73        """
74        Initialize the updates collection. Can be accessed via
75        ``Updates.updates``
76        """
77        with salt.utils.winapi.Com():
78            self.updates = win32com.client.Dispatch("Microsoft.Update.UpdateColl")
79
80    def count(self):
81        """
82        Return how many records are in the Microsoft Update Collection
83
84        Returns:
85            int: The number of updates in the collection
86
87        Code Example:
88
89        .. code-block:: python
90
91            import salt.utils.win_update
92            updates = salt.utils.win_update.Updates()
93            updates.count()
94        """
95        return self.updates.Count
96
97    def list(self):
98        """
99        Create a dictionary with the details for the updates in the collection.
100
101        Returns:
102            dict: Details about each update
103
104        .. code-block:: cfg
105
106            Dict of Updates:
107            {'<GUID>': {
108                'Title': <title>,
109                'KB': <KB>,
110                'GUID': <the globally unique identifier for the update>,
111                'Description': <description>,
112                'Downloaded': <has the update been downloaded>,
113                'Installed': <has the update been installed>,
114                'Mandatory': <is the update mandatory>,
115                'UserInput': <is user input required>,
116                'EULAAccepted': <has the EULA been accepted>,
117                'Severity': <update severity>,
118                'NeedsReboot': <is the update installed and awaiting reboot>,
119                'RebootBehavior': <will the update require a reboot>,
120                'Categories': [
121                    '<category 1>',
122                    '<category 2>',
123                    ... ]
124            }}
125
126        Code Example:
127
128        .. code-block:: python
129
130            import salt.utils.win_update
131            updates = salt.utils.win_update.Updates()
132            updates.list()
133        """
134        # https://msdn.microsoft.com/en-us/library/windows/desktop/aa386099(v=vs.85).aspx
135        if self.count() == 0:
136            return "Nothing to return"
137
138        log.debug("Building a detailed report of the results.")
139
140        # Build a dictionary containing details for each update
141        results = {}
142        for update in self.updates:
143
144            # Windows 10 build 2004 introduced some problems with the
145            # InstallationBehavior COM Object. See
146            # https://github.com/saltstack/salt/issues/57762 for more details.
147            # The following 2 try/except blocks will output sane defaults
148            try:
149                user_input = bool(update.InstallationBehavior.CanRequestUserInput)
150            except AttributeError:
151                log.debug(
152                    "Windows Update: Error reading InstallationBehavior COM Object"
153                )
154                user_input = False
155
156            try:
157                requires_reboot = update.InstallationBehavior.RebootBehavior
158            except AttributeError:
159                log.debug(
160                    "Windows Update: Error reading InstallationBehavior COM Object"
161                )
162                requires_reboot = 2
163
164            # IUpdate Properties
165            # https://docs.microsoft.com/en-us/windows/win32/wua_sdk/iupdate-properties
166            results[update.Identity.UpdateID] = {
167                "guid": update.Identity.UpdateID,
168                "Title": str(update.Title),
169                "Type": self.update_types[update.Type],
170                "Description": update.Description,
171                "Downloaded": bool(update.IsDownloaded),
172                "Installed": bool(update.IsInstalled),
173                "Mandatory": bool(update.IsMandatory),
174                "EULAAccepted": bool(update.EulaAccepted),
175                "NeedsReboot": bool(update.RebootRequired),
176                "Severity": str(update.MsrcSeverity),
177                "UserInput": user_input,
178                "RebootBehavior": REBOOT_BEHAVIOR[requires_reboot],
179                "KBs": ["KB" + item for item in update.KBArticleIDs],
180                "Categories": [item.Name for item in update.Categories],
181                "SupportUrl": update.SupportUrl,
182            }
183
184        return results
185
186    def summary(self):
187        """
188        Create a dictionary with a summary of the updates in the collection.
189
190        Returns:
191            dict: Summary of the contents of the collection
192
193        .. code-block:: cfg
194
195            Summary of Updates:
196            {'Total': <total number of updates returned>,
197             'Available': <updates that are not downloaded or installed>,
198             'Downloaded': <updates that are downloaded but not installed>,
199             'Installed': <updates installed (usually 0 unless installed=True)>,
200             'Categories': {
201                <category 1>: <total for that category>,
202                <category 2>: <total for category 2>,
203                ... }
204            }
205
206        Code Example:
207
208        .. code-block:: python
209
210            import salt.utils.win_update
211            updates = salt.utils.win_update.Updates()
212            updates.summary()
213        """
214        # https://msdn.microsoft.com/en-us/library/windows/desktop/aa386099(v=vs.85).aspx
215        if self.count() == 0:
216            return "Nothing to return"
217
218        # Build a dictionary containing a summary of updates available
219        results = {
220            "Total": 0,
221            "Available": 0,
222            "Downloaded": 0,
223            "Installed": 0,
224            "Categories": {},
225            "Severity": {},
226        }
227
228        for update in self.updates:
229
230            # Count the total number of updates available
231            results["Total"] += 1
232
233            # Updates available for download
234            if not salt.utils.data.is_true(
235                update.IsDownloaded
236            ) and not salt.utils.data.is_true(update.IsInstalled):
237                results["Available"] += 1
238
239            # Updates downloaded awaiting install
240            if salt.utils.data.is_true(
241                update.IsDownloaded
242            ) and not salt.utils.data.is_true(update.IsInstalled):
243                results["Downloaded"] += 1
244
245            # Updates installed
246            if salt.utils.data.is_true(update.IsInstalled):
247                results["Installed"] += 1
248
249            # Add Categories and increment total for each one
250            # The sum will be more than the total because each update can have
251            # multiple categories
252            for category in update.Categories:
253                if category.Name in results["Categories"]:
254                    results["Categories"][category.Name] += 1
255                else:
256                    results["Categories"][category.Name] = 1
257
258            # Add Severity Summary
259            if update.MsrcSeverity:
260                if update.MsrcSeverity in results["Severity"]:
261                    results["Severity"][update.MsrcSeverity] += 1
262                else:
263                    results["Severity"][update.MsrcSeverity] = 1
264
265        return results
266
267
268class WindowsUpdateAgent:
269    """
270    Class for working with the Windows update agent
271    """
272
273    # Error codes found at the following site:
274    # https://msdn.microsoft.com/en-us/library/windows/desktop/hh968413(v=vs.85).aspx
275    # https://technet.microsoft.com/en-us/library/cc720442(v=ws.10).aspx
276    fail_codes = {
277        -2145107924: "WinHTTP Send/Receive failed: 0x8024402C",
278        -2145124300: "Download failed: 0x80240034",
279        -2145124302: "Invalid search criteria: 0x80240032",
280        -2145124305: "Cancelled by policy: 0x8024002F",
281        -2145124307: "Missing source: 0x8024002D",
282        -2145124308: "Missing source: 0x8024002C",
283        -2145124312: "Uninstall not allowed: 0x80240028",
284        -2145124315: "Prevented by policy: 0x80240025",
285        -2145124316: "No Updates: 0x80240024",
286        -2145124322: "Service being shutdown: 0x8024001E",
287        -2145124325: "Self Update in Progress: 0x8024001B",
288        -2145124327: "Exclusive Install Conflict: 0x80240019",
289        -2145124330: "Install not allowed: 0x80240016",
290        -2145124333: "Duplicate item: 0x80240013",
291        -2145124341: "Operation cancelled: 0x8024000B",
292        -2145124343: "Operation in progress: 0x80240009",
293        -2145124284: "Access Denied: 0x8024044",
294        -2145124283: "Unsupported search scope: 0x80240045",
295        -2147024891: "Access is denied: 0x80070005",
296        -2149843018: "Setup in progress: 0x8024004A",
297        -4292599787: "Install still pending: 0x00242015",
298        -4292607992: "Already downloaded: 0x00240008",
299        -4292607993: "Already uninstalled: 0x00240007",
300        -4292607994: "Already installed: 0x00240006",
301        -4292607995: "Reboot required: 0x00240005",
302    }
303
304    def __init__(self, online=True):
305        """
306        Initialize the session and load all updates into the ``_updates``
307        collection. This collection is used by the other class functions instead
308        of querying Windows update (expensive).
309
310        Args:
311
312            online (bool):
313                Tells the Windows Update Agent go online to update its local
314                update database. ``True`` will go online. ``False`` will use the
315                local update database as is. Default is ``True``
316
317                .. versionadded:: 3001
318
319        Need to look at the possibility of loading this into ``__context__``
320        """
321        # Initialize the PyCom system
322        with salt.utils.winapi.Com():
323
324            # Create a session with the Windows Update Agent
325            self._session = win32com.client.Dispatch("Microsoft.Update.Session")
326
327            # Create Collection for Updates
328            self._updates = win32com.client.Dispatch("Microsoft.Update.UpdateColl")
329
330        self.refresh(online=online)
331
332    def updates(self):
333        """
334        Get the contents of ``_updates`` (all updates) and puts them in an
335        Updates class to expose the list and summary functions.
336
337        Returns:
338
339            Updates:
340                An instance of the Updates class with all updates for the
341                system.
342
343        Code Example:
344
345        .. code-block:: python
346
347            import salt.utils.win_update
348            wua = salt.utils.win_update.WindowsUpdateAgent()
349            updates = wua.updates()
350
351            # To get a list
352            updates.list()
353
354            # To get a summary
355            updates.summary()
356        """
357        updates = Updates()
358        found = updates.updates
359
360        for update in self._updates:
361            found.Add(update)
362
363        return updates
364
365    def refresh(self, online=True):
366        """
367        Refresh the contents of the ``_updates`` collection. This gets all
368        updates in the Windows Update system and loads them into the collection.
369        This is the part that is slow.
370
371        Args:
372
373            online (bool):
374                Tells the Windows Update Agent go online to update its local
375                update database. ``True`` will go online. ``False`` will use the
376                local update database as is. Default is ``True``
377
378                .. versionadded:: 3001
379
380        Code Example:
381
382        .. code-block:: python
383
384            import salt.utils.win_update
385            wua = salt.utils.win_update.WindowsUpdateAgent()
386            wua.refresh()
387        """
388        # https://msdn.microsoft.com/en-us/library/windows/desktop/aa386526(v=vs.85).aspx
389        search_string = "Type='Software' or Type='Driver'"
390
391        # Create searcher object
392        searcher = self._session.CreateUpdateSearcher()
393        searcher.Online = online
394        self._session.ClientApplicationID = "Salt: Load Updates"
395
396        # Load all updates into the updates collection
397        try:
398            results = searcher.Search(search_string)
399            if results.Updates.Count == 0:
400                log.debug("No Updates found for:\n\t\t%s", search_string)
401                return "No Updates found: {}".format(search_string)
402        except pywintypes.com_error as error:
403            # Something happened, raise an error
404            hr, msg, exc, arg = error.args  # pylint: disable=W0633
405            try:
406                failure_code = self.fail_codes[exc[5]]
407            except KeyError:
408                failure_code = "Unknown Failure: {}".format(error)
409
410            log.error("Search Failed: %s\n\t\t%s", failure_code, search_string)
411            raise CommandExecutionError(failure_code)
412
413        self._updates = results.Updates
414
415    def installed(self):
416        """
417        Gets a list of all updates available on the system that have the
418        ``IsInstalled`` attribute set to ``True``.
419
420        Returns:
421
422            Updates: An instance of Updates with the results.
423
424        Code Example:
425
426        .. code-block:: python
427
428            import salt.utils.win_update
429            wua = salt.utils.win_update.WindowsUpdateAgent(online=False)
430            installed_updates = wua.installed()
431        """
432        # https://msdn.microsoft.com/en-us/library/windows/desktop/aa386099(v=vs.85).aspx
433        updates = Updates()
434
435        for update in self._updates:
436            if salt.utils.data.is_true(update.IsInstalled):
437                updates.updates.Add(update)
438
439        return updates
440
441    def available(
442        self,
443        skip_hidden=True,
444        skip_installed=True,
445        skip_mandatory=False,
446        skip_reboot=False,
447        software=True,
448        drivers=True,
449        categories=None,
450        severities=None,
451    ):
452        """
453        Gets a list of all updates available on the system that match the passed
454        criteria.
455
456        Args:
457
458            skip_hidden (bool):
459                Skip hidden updates. Default is ``True``
460
461            skip_installed (bool):
462                Skip installed updates. Default is ``True``
463
464            skip_mandatory (bool):
465                Skip mandatory updates. Default is ``False``
466
467            skip_reboot (bool):
468                Skip updates that can or do require reboot. Default is ``False``
469
470            software (bool):
471                Include software updates. Default is ``True``
472
473            drivers (bool):
474                Include driver updates. Default is ``True``
475
476            categories (list):
477                Include updates that have these categories. Default is none
478                (all categories). Categories include the following:
479
480                * Critical Updates
481                * Definition Updates
482                * Drivers (make sure you set drivers=True)
483                * Feature Packs
484                * Security Updates
485                * Update Rollups
486                * Updates
487                * Update Rollups
488                * Windows 7
489                * Windows 8.1
490                * Windows 8.1 drivers
491                * Windows 8.1 and later drivers
492                * Windows Defender
493
494            severities (list):
495                Include updates that have these severities. Default is none
496                (all severities). Severities include the following:
497
498                * Critical
499                * Important
500
501        .. note::
502
503            All updates are either software or driver updates. If both
504            ``software`` and ``drivers`` is ``False``, nothing will be returned.
505
506        Returns:
507
508            Updates: An instance of Updates with the results of the search.
509
510        Code Example:
511
512        .. code-block:: python
513
514            import salt.utils.win_update
515            wua = salt.utils.win_update.WindowsUpdateAgent()
516
517            # Gets all updates and shows a summary
518            updates = wua.available()
519            updates.summary()
520
521            # Get a list of Critical updates
522            updates = wua.available(categories=['Critical Updates'])
523            updates.list()
524        """
525        # https://msdn.microsoft.com/en-us/library/windows/desktop/aa386099(v=vs.85).aspx
526        updates = Updates()
527        found = updates.updates
528
529        for update in self._updates:
530
531            if salt.utils.data.is_true(update.IsHidden) and skip_hidden:
532                continue
533
534            if salt.utils.data.is_true(update.IsInstalled) and skip_installed:
535                continue
536
537            if salt.utils.data.is_true(update.IsMandatory) and skip_mandatory:
538                continue
539
540            # Windows 10 build 2004 introduced some problems with the
541            # InstallationBehavior COM Object. See
542            # https://github.com/saltstack/salt/issues/57762 for more details.
543            # The following try/except block will default to True
544            try:
545                requires_reboot = salt.utils.data.is_true(
546                    update.InstallationBehavior.RebootBehavior
547                )
548            except AttributeError:
549                log.debug(
550                    "Windows Update: Error reading InstallationBehavior COM Object"
551                )
552                requires_reboot = True
553
554            if requires_reboot and skip_reboot:
555                continue
556
557            if not software and update.Type == 1:
558                continue
559
560            if not drivers and update.Type == 2:
561                continue
562
563            if categories is not None:
564                match = False
565                for category in update.Categories:
566                    if category.Name in categories:
567                        match = True
568                if not match:
569                    continue
570
571            if severities is not None:
572                if update.MsrcSeverity not in severities:
573                    continue
574
575            found.Add(update)
576
577        return updates
578
579    def search(self, search_string):
580        """
581        Search for either a single update or a specific list of updates. GUIDs
582        are searched first, then KB numbers, and finally Titles.
583
584        Args:
585
586            search_string (str, list):
587                The search string to use to find the update. This can be the
588                GUID or KB of the update (preferred). It can also be the full
589                Title of the update or any part of the Title. A partial Title
590                search is less specific and can return multiple results.
591
592        Returns:
593            Updates: An instance of Updates with the results of the search
594
595        Code Example:
596
597        .. code-block:: python
598
599            import salt.utils.win_update
600            wua = salt.utils.win_update.WindowsUpdateAgent()
601
602            # search for a single update and show its details
603            updates = wua.search('KB3194343')
604            updates.list()
605
606            # search for a list of updates and show their details
607            updates = wua.search(['KB3195432', '12345678-abcd-1234-abcd-1234567890ab'])
608            updates.list()
609        """
610        updates = Updates()
611        found = updates.updates
612
613        if isinstance(search_string, str):
614            search_string = [search_string]
615
616        if isinstance(search_string, int):
617            search_string = [str(search_string)]
618
619        for update in self._updates:
620
621            for find in search_string:
622
623                # Search by GUID
624                if find == update.Identity.UpdateID:
625                    found.Add(update)
626                    continue
627
628                # Search by KB
629                if find in ["KB" + item for item in update.KBArticleIDs]:
630                    found.Add(update)
631                    continue
632
633                # Search by KB without the KB in front
634                if find in [item for item in update.KBArticleIDs]:
635                    found.Add(update)
636                    continue
637
638                # Search by Title
639                if find in update.Title:
640                    found.Add(update)
641                    continue
642
643        return updates
644
645    def download(self, updates):
646        """
647        Download the updates passed in the updates collection. Load the updates
648        collection using ``search`` or ``available``
649
650        Args:
651
652            updates (Updates):
653                An instance of the Updates class containing a the updates to be
654                downloaded.
655
656        Returns:
657            dict: A dictionary containing the results of the download
658
659        Code Example:
660
661        .. code-block:: python
662
663            import salt.utils.win_update
664            wua = salt.utils.win_update.WindowsUpdateAgent()
665
666            # Download KB3195454
667            updates = wua.search('KB3195454')
668            results = wua.download(updates)
669        """
670
671        # Check for empty list
672        if updates.count() == 0:
673            ret = {"Success": False, "Updates": "Nothing to download"}
674            return ret
675
676        # Initialize the downloader object and list collection
677        downloader = self._session.CreateUpdateDownloader()
678        self._session.ClientApplicationID = "Salt: Download Update"
679        with salt.utils.winapi.Com():
680            download_list = win32com.client.Dispatch("Microsoft.Update.UpdateColl")
681
682            ret = {"Updates": {}}
683
684            # Check for updates that aren't already downloaded
685            for update in updates.updates:
686
687                # Define uid to keep the lines shorter
688                uid = update.Identity.UpdateID
689                ret["Updates"][uid] = {}
690                ret["Updates"][uid]["Title"] = update.Title
691                ret["Updates"][uid]["AlreadyDownloaded"] = bool(update.IsDownloaded)
692
693                # Accept EULA
694                if not salt.utils.data.is_true(update.EulaAccepted):
695                    log.debug("Accepting EULA: %s", update.Title)
696                    update.AcceptEula()  # pylint: disable=W0104
697
698                # Update already downloaded
699                if not salt.utils.data.is_true(update.IsDownloaded):
700                    log.debug("To Be Downloaded: %s", uid)
701                    log.debug("\tTitle: %s", update.Title)
702                    download_list.Add(update)
703
704            # Check the download list
705            if download_list.Count == 0:
706                ret = {"Success": True, "Updates": "Nothing to download"}
707                return ret
708
709            # Send the list to the downloader
710            downloader.Updates = download_list
711
712            # Download the list
713            try:
714                log.debug("Downloading Updates")
715                result = downloader.Download()
716            except pywintypes.com_error as error:
717                # Something happened, raise an error
718                hr, msg, exc, arg = error.args  # pylint: disable=W0633
719                try:
720                    failure_code = self.fail_codes[exc[5]]
721                except KeyError:
722                    failure_code = "Unknown Failure: {}".format(error)
723
724                log.error("Download Failed: %s", failure_code)
725                raise CommandExecutionError(failure_code)
726
727            # Lookup dictionary
728            result_code = {
729                0: "Download Not Started",
730                1: "Download In Progress",
731                2: "Download Succeeded",
732                3: "Download Succeeded With Errors",
733                4: "Download Failed",
734                5: "Download Aborted",
735            }
736
737            log.debug("Download Complete")
738            log.debug(result_code[result.ResultCode])
739            ret["Message"] = result_code[result.ResultCode]
740
741            # Was the download successful?
742            if result.ResultCode in [2, 3]:
743                log.debug("Downloaded Successfully")
744                ret["Success"] = True
745            else:
746                log.debug("Download Failed")
747                ret["Success"] = False
748
749            # Report results for each update
750            for i in range(download_list.Count):
751                uid = download_list.Item(i).Identity.UpdateID
752                ret["Updates"][uid]["Result"] = result_code[
753                    result.GetUpdateResult(i).ResultCode
754                ]
755
756        return ret
757
758    def install(self, updates):
759        """
760        Install the updates passed in the updates collection. Load the updates
761        collection using the ``search`` or ``available`` functions. If the
762        updates need to be downloaded, use the ``download`` function.
763
764        Args:
765
766            updates (Updates):
767                An instance of the Updates class containing a the updates to be
768                installed.
769
770        Returns:
771            dict: A dictionary containing the results of the installation
772
773        Code Example:
774
775        .. code-block:: python
776
777            import salt.utils.win_update
778            wua = salt.utils.win_update.WindowsUpdateAgent()
779
780            # install KB3195454
781            updates = wua.search('KB3195454')
782            results = wua.download(updates)
783            results = wua.install(updates)
784        """
785        # Check for empty list
786        if updates.count() == 0:
787            ret = {"Success": False, "Updates": "Nothing to install"}
788            return ret
789
790        installer = self._session.CreateUpdateInstaller()
791        self._session.ClientApplicationID = "Salt: Install Update"
792        with salt.utils.winapi.Com():
793            install_list = win32com.client.Dispatch("Microsoft.Update.UpdateColl")
794
795            ret = {"Updates": {}}
796
797            # Check for updates that aren't already installed
798            for update in updates.updates:
799
800                # Define uid to keep the lines shorter
801                uid = update.Identity.UpdateID
802                ret["Updates"][uid] = {}
803                ret["Updates"][uid]["Title"] = update.Title
804                ret["Updates"][uid]["AlreadyInstalled"] = bool(update.IsInstalled)
805
806                # Make sure the update has actually been installed
807                if not salt.utils.data.is_true(update.IsInstalled):
808                    log.debug("To Be Installed: %s", uid)
809                    log.debug("\tTitle: %s", update.Title)
810                    install_list.Add(update)
811
812            # Check the install list
813            if install_list.Count == 0:
814                ret = {"Success": True, "Updates": "Nothing to install"}
815                return ret
816
817            # Send the list to the installer
818            installer.Updates = install_list
819
820            # Install the list
821            try:
822                log.debug("Installing Updates")
823                result = installer.Install()
824
825            except pywintypes.com_error as error:
826                # Something happened, raise an error
827                hr, msg, exc, arg = error.args  # pylint: disable=W0633
828                try:
829                    failure_code = self.fail_codes[exc[5]]
830                except KeyError:
831                    failure_code = "Unknown Failure: {}".format(error)
832
833                log.error("Install Failed: %s", failure_code)
834                raise CommandExecutionError(failure_code)
835
836            # Lookup dictionary
837            result_code = {
838                0: "Installation Not Started",
839                1: "Installation In Progress",
840                2: "Installation Succeeded",
841                3: "Installation Succeeded With Errors",
842                4: "Installation Failed",
843                5: "Installation Aborted",
844            }
845
846            log.debug("Install Complete")
847            log.debug(result_code[result.ResultCode])
848            ret["Message"] = result_code[result.ResultCode]
849
850            if result.ResultCode in [2, 3]:
851                ret["Success"] = True
852                ret["NeedsReboot"] = result.RebootRequired
853                log.debug("NeedsReboot: %s", result.RebootRequired)
854            else:
855                log.debug("Install Failed")
856                ret["Success"] = False
857
858            for i in range(install_list.Count):
859                uid = install_list.Item(i).Identity.UpdateID
860                ret["Updates"][uid]["Result"] = result_code[
861                    result.GetUpdateResult(i).ResultCode
862                ]
863                # Windows 10 build 2004 introduced some problems with the
864                # InstallationBehavior COM Object. See
865                # https://github.com/saltstack/salt/issues/57762 for more details.
866                # The following try/except block will default to 2
867                try:
868                    reboot_behavior = install_list.Item(
869                        i
870                    ).InstallationBehavior.RebootBehavior
871                except AttributeError:
872                    log.debug(
873                        "Windows Update: Error reading InstallationBehavior COM Object"
874                    )
875                    reboot_behavior = 2
876                ret["Updates"][uid]["RebootBehavior"] = REBOOT_BEHAVIOR[reboot_behavior]
877
878        return ret
879
880    def uninstall(self, updates):
881        """
882        Uninstall the updates passed in the updates collection. Load the updates
883        collection using the ``search`` or ``available`` functions.
884
885        .. note::
886
887            Starting with Windows 10 the Windows Update Agent is unable to
888            uninstall updates. An ``Uninstall Not Allowed`` error is returned.
889            If this error is encountered this function will instead attempt to
890            use ``dism.exe`` to perform the un-installation. ``dism.exe`` may
891            fail to to find the KB number for the package. In that case, removal
892            will fail.
893
894        Args:
895
896            updates (Updates):
897                An instance of the Updates class containing a the updates to be
898                uninstalled.
899
900        Returns:
901            dict: A dictionary containing the results of the un-installation
902
903        Code Example:
904
905        .. code-block:: python
906
907            import salt.utils.win_update
908            wua = salt.utils.win_update.WindowsUpdateAgent()
909
910            # uninstall KB3195454
911            updates = wua.search('KB3195454')
912            results = wua.uninstall(updates)
913        """
914        # This doesn't work with the WUA API since Windows 10. It always returns
915        # "0x80240028 # Uninstall not allowed". The full message is: "The update
916        # could not be uninstalled because the request did not originate from a
917        # Windows Server Update Services (WSUS) server.
918
919        # Check for empty list
920        if updates.count() == 0:
921            ret = {"Success": False, "Updates": "Nothing to uninstall"}
922            return ret
923
924        installer = self._session.CreateUpdateInstaller()
925        self._session.ClientApplicationID = "Salt: Uninstall Update"
926        with salt.utils.winapi.Com():
927            uninstall_list = win32com.client.Dispatch("Microsoft.Update.UpdateColl")
928
929            ret = {"Updates": {}}
930
931            # Check for updates that aren't already installed
932            for update in updates.updates:
933
934                # Define uid to keep the lines shorter
935                uid = update.Identity.UpdateID
936                ret["Updates"][uid] = {}
937                ret["Updates"][uid]["Title"] = update.Title
938                ret["Updates"][uid]["AlreadyUninstalled"] = not bool(update.IsInstalled)
939
940                # Make sure the update has actually been Uninstalled
941                if salt.utils.data.is_true(update.IsInstalled):
942                    log.debug("To Be Uninstalled: %s", uid)
943                    log.debug("\tTitle: %s", update.Title)
944                    uninstall_list.Add(update)
945
946            # Check the install list
947            if uninstall_list.Count == 0:
948                ret = {"Success": False, "Updates": "Nothing to uninstall"}
949                return ret
950
951            # Send the list to the installer
952            installer.Updates = uninstall_list
953
954            # Uninstall the list
955            try:
956                log.debug("Uninstalling Updates")
957                result = installer.Uninstall()
958
959            except pywintypes.com_error as error:
960                # Something happened, return error or try using DISM
961                hr, msg, exc, arg = error.args  # pylint: disable=W0633
962                try:
963                    failure_code = self.fail_codes[exc[5]]
964                except KeyError:
965                    failure_code = "Unknown Failure: {}".format(error)
966
967                # If "Uninstall Not Allowed" error, try using DISM
968                if exc[5] == -2145124312:
969                    log.debug("Uninstall Failed with WUA, attempting with DISM")
970                    try:
971
972                        # Go through each update...
973                        for item in uninstall_list:
974
975                            # Look for the KB numbers
976                            for kb in item.KBArticleIDs:
977
978                                # Get the list of packages
979                                cmd = ["dism", "/Online", "/Get-Packages"]
980                                pkg_list = self._run(cmd)[0].splitlines()
981
982                                # Find the KB in the pkg_list
983                                for item in pkg_list:
984
985                                    # Uninstall if found
986                                    if "kb" + kb in item.lower():
987                                        pkg = item.split(" : ")[1]
988
989                                        ret["DismPackage"] = pkg
990
991                                        cmd = [
992                                            "dism",
993                                            "/Online",
994                                            "/Remove-Package",
995                                            "/PackageName:{}".format(pkg),
996                                            "/Quiet",
997                                            "/NoRestart",
998                                        ]
999
1000                                        self._run(cmd)
1001
1002                    except CommandExecutionError as exc:
1003                        log.debug("Uninstall using DISM failed")
1004                        log.debug("Command: %s", " ".join(cmd))
1005                        log.debug("Error: %s", exc)
1006                        raise CommandExecutionError(
1007                            "Uninstall using DISM failed: {}".format(exc)
1008                        )
1009
1010                    # DISM Uninstall Completed Successfully
1011                    log.debug("Uninstall Completed using DISM")
1012
1013                    # Populate the return dictionary
1014                    ret["Success"] = True
1015                    ret["Message"] = "Uninstalled using DISM"
1016                    ret["NeedsReboot"] = needs_reboot()
1017                    log.debug("NeedsReboot: %s", ret["NeedsReboot"])
1018
1019                    # Refresh the Updates Table
1020                    self.refresh(online=False)
1021
1022                    # Check the status of each update
1023                    for update in self._updates:
1024                        uid = update.Identity.UpdateID
1025                        for item in uninstall_list:
1026                            if item.Identity.UpdateID == uid:
1027                                if not update.IsInstalled:
1028                                    ret["Updates"][uid][
1029                                        "Result"
1030                                    ] = "Uninstallation Succeeded"
1031                                else:
1032                                    ret["Updates"][uid][
1033                                        "Result"
1034                                    ] = "Uninstallation Failed"
1035                                # Windows 10 build 2004 introduced some problems with the
1036                                # InstallationBehavior COM Object. See
1037                                # https://github.com/saltstack/salt/issues/57762 for more details.
1038                                # The following try/except block will default to 2
1039                                try:
1040                                    requires_reboot = (
1041                                        update.InstallationBehavior.RebootBehavior
1042                                    )
1043                                except AttributeError:
1044                                    log.debug(
1045                                        "Windows Update: Error reading"
1046                                        " InstallationBehavior COM Object"
1047                                    )
1048                                    requires_reboot = 2
1049                                ret["Updates"][uid]["RebootBehavior"] = REBOOT_BEHAVIOR[
1050                                    requires_reboot
1051                                ]
1052
1053                    return ret
1054
1055                # Found a different exception, Raise error
1056                log.error("Uninstall Failed: %s", failure_code)
1057                raise CommandExecutionError(failure_code)
1058
1059            # Lookup dictionary
1060            result_code = {
1061                0: "Uninstallation Not Started",
1062                1: "Uninstallation In Progress",
1063                2: "Uninstallation Succeeded",
1064                3: "Uninstallation Succeeded With Errors",
1065                4: "Uninstallation Failed",
1066                5: "Uninstallation Aborted",
1067            }
1068
1069            log.debug("Uninstall Complete")
1070            log.debug(result_code[result.ResultCode])
1071            ret["Message"] = result_code[result.ResultCode]
1072
1073            if result.ResultCode in [2, 3]:
1074                ret["Success"] = True
1075                ret["NeedsReboot"] = result.RebootRequired
1076                log.debug("NeedsReboot: %s", result.RebootRequired)
1077            else:
1078                log.debug("Uninstall Failed")
1079                ret["Success"] = False
1080
1081            for i in range(uninstall_list.Count):
1082                uid = uninstall_list.Item(i).Identity.UpdateID
1083                ret["Updates"][uid]["Result"] = result_code[
1084                    result.GetUpdateResult(i).ResultCode
1085                ]
1086                # Windows 10 build 2004 introduced some problems with the
1087                # InstallationBehavior COM Object. See
1088                # https://github.com/saltstack/salt/issues/57762 for more details.
1089                # The following try/except block will default to 2
1090                try:
1091                    reboot_behavior = uninstall_list.Item(
1092                        i
1093                    ).InstallationBehavior.RebootBehavior
1094                except AttributeError:
1095                    log.debug(
1096                        "Windows Update: Error reading InstallationBehavior COM Object"
1097                    )
1098                    reboot_behavior = 2
1099                ret["Updates"][uid]["RebootBehavior"] = REBOOT_BEHAVIOR[reboot_behavior]
1100
1101        return ret
1102
1103    def _run(self, cmd):
1104        """
1105        Internal function for running commands. Used by the uninstall function.
1106
1107        Args:
1108            cmd (str, list):
1109                The command to run
1110
1111        Returns:
1112            str: The stdout of the command
1113        """
1114
1115        if isinstance(cmd, str):
1116            cmd = salt.utils.args.shlex_split(cmd)
1117
1118        try:
1119            log.debug(cmd)
1120            p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1121            return p.communicate()
1122
1123        except OSError as exc:
1124            log.debug("Command Failed: %s", " ".join(cmd))
1125            log.debug("Error: %s", exc)
1126            raise CommandExecutionError(exc)
1127
1128
1129def needs_reboot():
1130    """
1131    Determines if the system needs to be rebooted.
1132
1133    Returns:
1134
1135        bool: ``True`` if the system requires a reboot, ``False`` if not
1136
1137    CLI Examples:
1138
1139    .. code-block:: bash
1140
1141        import salt.utils.win_update
1142
1143        salt.utils.win_update.needs_reboot()
1144
1145    """
1146    # Initialize the PyCom system
1147    with salt.utils.winapi.Com():
1148        # Create an AutoUpdate object
1149        try:
1150            obj_sys = win32com.client.Dispatch("Microsoft.Update.SystemInfo")
1151        except pywintypes.com_error as exc:
1152            _, msg, _, _ = exc.args
1153            log.debug("Failed to create SystemInfo object: %s", msg)
1154            return False
1155        return salt.utils.data.is_true(obj_sys.RebootRequired)
1156