1""" 2Support for the softwareupdate command on MacOS. 3""" 4 5import os 6import re 7 8import salt.utils.data 9import salt.utils.files 10import salt.utils.mac_utils 11import salt.utils.path 12import salt.utils.platform 13from salt.exceptions import CommandExecutionError, SaltInvocationError 14 15__virtualname__ = "softwareupdate" 16 17 18def __virtual__(): 19 """ 20 Only for MacOS 21 """ 22 if not salt.utils.platform.is_darwin(): 23 return ( 24 False, 25 "The softwareupdate module could not be loaded: " 26 "module only works on MacOS systems.", 27 ) 28 29 return __virtualname__ 30 31 32def _get_available(recommended=False, restart=False, shut_down=False): 33 """ 34 Utility function to get all available update packages. 35 36 Sample return date: 37 { 'updatename': '1.2.3-45', ... } 38 """ 39 cmd = ["softwareupdate", "--list"] 40 out = salt.utils.mac_utils.execute_return_result(cmd) 41 42 if __grains__["osrelease_info"][0] > 10 or __grains__["osrelease_info"][1] >= 15: 43 # Example output: 44 # Software Update Tool 45 # 46 # Finding available software 47 # Software Update found the following new or updated software: 48 # * Label: Command Line Tools beta 5 for Xcode-11.0 49 # Title: Command Line Tools beta 5 for Xcode, Version: 11.0, Size: 224804K, Recommended: YES, 50 # * Label: macOS Catalina Developer Beta-6 51 # Title: macOS Catalina Public Beta, Version: 5, Size: 3084292K, Recommended: YES, Action: restart, 52 # * Label: BridgeOSUpdateCustomer 53 # Title: BridgeOSUpdateCustomer, Version: 10.15.0.1.1.1560926689, Size: 390674K, Recommended: YES, Action: shut down, 54 # - Label: iCal-1.0.2 55 # Title: iCal, Version: 1.0.2, Size: 6520K, 56 rexp = re.compile( 57 r"(?m)" # Turn on multiline matching 58 r"^\s*[*-] Label: " # Name lines start with * or - and "Label: " 59 r"(?P<name>[^ ].*)[\r\n]" # Capture the rest of that line; this is the update name. 60 r".*Version: (?P<version>[^,]*), " # Grab the version number. 61 r"Size: (?P<size>[^,]*),\s*" # Grab the size; unused at this time. 62 r"(?P<recommended>Recommended: YES,)?\s*" # Optionally grab the recommended flag. 63 r"(?P<action>Action: (?:restart|shut down),)?" # Optionally grab an action. 64 ) 65 else: 66 # Example output: 67 # Software Update Tool 68 # 69 # Finding available software 70 # Software Update found the following new or updated software: 71 # * Command Line Tools (macOS Mojave version 10.14) for Xcode-10.3 72 # Command Line Tools (macOS Mojave version 10.14) for Xcode (10.3), 199140K [recommended] 73 # * macOS 10.14.1 Update 74 # macOS 10.14.1 Update (10.14.1), 199140K [recommended] [restart] 75 # * BridgeOSUpdateCustomer 76 # BridgeOSUpdateCustomer (10.14.4.1.1.1555388607), 328394K, [recommended] [shut down] 77 # - iCal-1.0.2 78 # iCal, (1.0.2), 6520K 79 rexp = re.compile( 80 r"(?m)" # Turn on multiline matching 81 r"^\s+[*-] " # Name lines start with 3 spaces and either a * or a -. 82 r"(?P<name>.*)[\r\n]" # The rest of that line is the name. 83 r".*\((?P<version>[^ \)]*)" # Capture the last parenthesized value on the next line. 84 r"[^\r\n\[]*(?P<recommended>\[recommended\])?\s?" # Capture [recommended] if there. 85 r"(?P<action>\[(?:restart|shut down)\])?" # Capture an action if present. 86 ) 87 88 # Build a list of lambda funcs to apply to matches to filter based 89 # on our args. 90 conditions = [] 91 if salt.utils.data.is_true(recommended): 92 conditions.append(lambda m: m.group("recommended")) 93 if salt.utils.data.is_true(restart): 94 conditions.append( 95 lambda m: "restart" in (m.group("action") or "") 96 ) # pylint: disable=superfluous-parens 97 if salt.utils.data.is_true(shut_down): 98 conditions.append( 99 lambda m: "shut down" in (m.group("action") or "") 100 ) # pylint: disable=superfluous-parens 101 102 return { 103 m.group("name"): m.group("version") 104 for m in rexp.finditer(out) 105 if all(f(m) for f in conditions) 106 } 107 108 109def list_available(recommended=False, restart=False, shut_down=False): 110 """ 111 List all available updates. 112 113 :param bool recommended: Show only recommended updates. 114 115 :param bool restart: Show only updates that require a restart. 116 117 :return: Returns a dictionary containing the updates 118 :rtype: dict 119 120 CLI Example: 121 122 .. code-block:: bash 123 124 salt '*' softwareupdate.list_available 125 """ 126 return _get_available(recommended, restart, shut_down) 127 128 129def ignore(name): 130 """ 131 Ignore a specific program update. When an update is ignored the '-' and 132 version number at the end will be omitted, so "SecUpd2014-001-1.0" becomes 133 "SecUpd2014-001". It will be removed automatically if present. An update 134 is successfully ignored when it no longer shows up after list_updates. 135 136 :param name: The name of the update to add to the ignore list. 137 :ptype: str 138 139 :return: True if successful, False if not 140 :rtype: bool 141 142 CLI Example: 143 144 .. code-block:: bash 145 146 salt '*' softwareupdate.ignore <update-name> 147 """ 148 # remove everything after and including the '-' in the updates name. 149 to_ignore = name.rsplit("-", 1)[0] 150 151 cmd = ["softwareupdate", "--ignore", to_ignore] 152 salt.utils.mac_utils.execute_return_success(cmd) 153 154 return to_ignore in list_ignored() 155 156 157def list_ignored(): 158 """ 159 List all updates that have been ignored. Ignored updates are shown 160 without the '-' and version number at the end, this is how the 161 softwareupdate command works. 162 163 :return: The list of ignored updates 164 :rtype: list 165 166 CLI Example: 167 168 .. code-block:: bash 169 170 salt '*' softwareupdate.list_ignored 171 """ 172 cmd = ["softwareupdate", "--list", "--ignore"] 173 out = salt.utils.mac_utils.execute_return_result(cmd) 174 175 # rep parses lines that look like the following: 176 # "Safari6.1.2MountainLion-6.1.2", 177 # or: 178 # Safari6.1.2MountainLion-6.1.2 179 rexp = re.compile('(?m)^ ["]?' r'([^,|\s].*[^"|\n|,])[,|"]?') 180 181 return rexp.findall(out) 182 183 184def reset_ignored(): 185 """ 186 Make sure the ignored updates are not ignored anymore, 187 returns a list of the updates that are no longer ignored. 188 189 :return: True if the list was reset, Otherwise False 190 :rtype: bool 191 192 CLI Example: 193 194 .. code-block:: bash 195 196 salt '*' softwareupdate.reset_ignored 197 """ 198 cmd = ["softwareupdate", "--reset-ignored"] 199 salt.utils.mac_utils.execute_return_success(cmd) 200 201 return list_ignored() == [] 202 203 204def schedule_enabled(): 205 """ 206 Check the status of automatic update scheduling. 207 208 :return: True if scheduling is enabled, False if disabled 209 210 :rtype: bool 211 212 CLI Example: 213 214 .. code-block:: bash 215 216 salt '*' softwareupdate.schedule_enabled 217 """ 218 cmd = ["softwareupdate", "--schedule"] 219 ret = salt.utils.mac_utils.execute_return_result(cmd) 220 221 enabled = ret.split()[-1] 222 223 return salt.utils.mac_utils.validate_enabled(enabled) == "on" 224 225 226def schedule_enable(enable): 227 """ 228 Enable/disable automatic update scheduling. 229 230 :param enable: True/On/Yes/1 to turn on automatic updates. False/No/Off/0 231 to turn off automatic updates. If this value is empty, the current 232 status will be returned. 233 234 :type: bool str 235 236 :return: True if scheduling is enabled, False if disabled 237 :rtype: bool 238 239 CLI Example: 240 241 .. code-block:: bash 242 243 salt '*' softwareupdate.schedule_enable on|off 244 """ 245 status = salt.utils.mac_utils.validate_enabled(enable) 246 247 cmd = [ 248 "softwareupdate", 249 "--schedule", 250 salt.utils.mac_utils.validate_enabled(status), 251 ] 252 salt.utils.mac_utils.execute_return_success(cmd) 253 254 return salt.utils.mac_utils.validate_enabled(schedule_enabled()) == status 255 256 257def update_all(recommended=False, restart=True): 258 """ 259 Install all available updates. Returns a dictionary containing the name 260 of the update and the status of its installation. 261 262 :param bool recommended: If set to True, only install the recommended 263 updates. If set to False (default) all updates are installed. 264 265 :param bool restart: Set this to False if you do not want to install updates 266 that require a restart. Default is True 267 268 :return: A dictionary containing the updates that were installed and the 269 status of its installation. If no updates were installed an empty 270 dictionary is returned. 271 272 :rtype: dict 273 274 CLI Example: 275 276 .. code-block:: bash 277 278 salt '*' softwareupdate.update_all 279 """ 280 to_update = _get_available(recommended, restart) 281 282 if not to_update: 283 return {} 284 285 for _update in to_update: 286 cmd = ["softwareupdate", "--install", _update] 287 salt.utils.mac_utils.execute_return_success(cmd) 288 289 ret = {} 290 updates_left = _get_available() 291 292 for _update in to_update: 293 ret[_update] = True if _update not in updates_left else False 294 295 return ret 296 297 298def update(name): 299 """ 300 Install a named update. 301 302 :param str name: The name of the of the update to install. 303 304 :return: True if successfully updated, otherwise False 305 :rtype: bool 306 307 CLI Example: 308 309 .. code-block:: bash 310 311 salt '*' softwareupdate.update <update-name> 312 """ 313 if not update_available(name): 314 raise SaltInvocationError("Update not available: {}".format(name)) 315 316 cmd = ["softwareupdate", "--install", name] 317 salt.utils.mac_utils.execute_return_success(cmd) 318 319 return not update_available(name) 320 321 322def update_available(name): 323 """ 324 Check whether or not an update is available with a given name. 325 326 :param str name: The name of the update to look for 327 328 :return: True if available, False if not 329 :rtype: bool 330 331 CLI Example: 332 333 .. code-block:: bash 334 335 salt '*' softwareupdate.update_available <update-name> 336 salt '*' softwareupdate.update_available "<update with whitespace>" 337 """ 338 return name in _get_available() 339 340 341def list_downloads(): 342 """ 343 Return a list of all updates that have been downloaded locally. 344 345 :return: A list of updates that have been downloaded 346 :rtype: list 347 348 CLI Example: 349 350 .. code-block:: bash 351 352 salt '*' softwareupdate.list_downloads 353 """ 354 outfiles = [] 355 for root, subFolder, files in salt.utils.path.os_walk("/Library/Updates"): 356 for f in files: 357 outfiles.append(os.path.join(root, f)) 358 359 dist_files = [] 360 for f in outfiles: 361 if f.endswith(".dist"): 362 dist_files.append(f) 363 364 ret = [] 365 for update in _get_available(): 366 for f in dist_files: 367 with salt.utils.files.fopen(f) as fhr: 368 if update.rsplit("-", 1)[0] in salt.utils.stringutils.to_unicode( 369 fhr.read() 370 ): 371 ret.append(update) 372 373 return ret 374 375 376def download(name): 377 """ 378 Download a named update so that it can be installed later with the 379 ``update`` or ``update_all`` functions 380 381 :param str name: The update to download. 382 383 :return: True if successful, otherwise False 384 :rtype: bool 385 386 CLI Example: 387 388 .. code-block:: bash 389 390 salt '*' softwareupdate.download <update name> 391 """ 392 if not update_available(name): 393 raise SaltInvocationError("Update not available: {}".format(name)) 394 395 if name in list_downloads(): 396 return True 397 398 cmd = ["softwareupdate", "--download", name] 399 salt.utils.mac_utils.execute_return_success(cmd) 400 401 return name in list_downloads() 402 403 404def download_all(recommended=False, restart=True): 405 """ 406 Download all available updates so that they can be installed later with the 407 ``update`` or ``update_all`` functions. It returns a list of updates that 408 are now downloaded. 409 410 :param bool recommended: If set to True, only install the recommended 411 updates. If set to False (default) all updates are installed. 412 413 :param bool restart: Set this to False if you do not want to install updates 414 that require a restart. Default is True 415 416 :return: A list containing all downloaded updates on the system. 417 :rtype: list 418 419 CLI Example: 420 421 .. code-block:: bash 422 423 salt '*' softwareupdate.download_all 424 """ 425 to_download = _get_available(recommended, restart) 426 427 for name in to_download: 428 download(name) 429 430 return list_downloads() 431 432 433def get_catalog(): 434 """ 435 .. versionadded:: 2016.3.0 436 437 Get the current catalog being used for update lookups. Will return a url if 438 a custom catalog has been specified. Otherwise the word 'Default' will be 439 returned 440 441 :return: The catalog being used for update lookups 442 :rtype: str 443 444 CLI Example: 445 446 .. code-block:: bash 447 448 salt '*' softwareupdates.get_catalog 449 """ 450 cmd = ["defaults", "read", "/Library/Preferences/com.apple.SoftwareUpdate.plist"] 451 out = salt.utils.mac_utils.execute_return_result(cmd) 452 453 if "AppleCatalogURL" in out: 454 cmd.append("AppleCatalogURL") 455 out = salt.utils.mac_utils.execute_return_result(cmd) 456 return out 457 elif "CatalogURL" in out: 458 cmd.append("CatalogURL") 459 out = salt.utils.mac_utils.execute_return_result(cmd) 460 return out 461 else: 462 return "Default" 463 464 465def set_catalog(url): 466 """ 467 .. versionadded:: 2016.3.0 468 469 Set the Software Update Catalog to the URL specified 470 471 :param str url: The url to the update catalog 472 473 :return: True if successful, False if not 474 :rtype: bool 475 476 CLI Example: 477 478 .. code-block:: bash 479 480 salt '*' softwareupdates.set_catalog http://swupd.local:8888/index.sucatalog 481 """ 482 # This command always returns an error code, though it completes 483 # successfully. Success will be determined by making sure get_catalog 484 # returns the passed url 485 cmd = ["softwareupdate", "--set-catalog", url] 486 487 try: 488 salt.utils.mac_utils.execute_return_success(cmd) 489 except CommandExecutionError as exc: 490 pass 491 492 return get_catalog() == url 493 494 495def reset_catalog(): 496 """ 497 .. versionadded:: 2016.3.0 498 499 Reset the Software Update Catalog to the default. 500 501 :return: True if successful, False if not 502 :rtype: bool 503 504 CLI Example: 505 506 .. code-block:: bash 507 508 salt '*' softwareupdates.reset_catalog 509 """ 510 # This command always returns an error code, though it completes 511 # successfully. Success will be determined by making sure get_catalog 512 # returns 'Default' 513 cmd = ["softwareupdate", "--clear-catalog"] 514 515 try: 516 salt.utils.mac_utils.execute_return_success(cmd) 517 except CommandExecutionError as exc: 518 pass 519 520 return get_catalog() == "Default" 521