1# stdlib 2from __future__ import print_function 3import hashlib 4import re 5from os import path 6import sys 7 8try: 9 # Python 3.x 10 from urllib.parse import urlparse 11except ImportError: 12 # Python 2.x 13 from urlparse import urlparse 14 15# 3rd-party modules 16from lxml.builder import E 17from lxml import etree 18 19# local modules 20from jnpr.junos.decorators import timeoutDecorator 21from jnpr.junos.utils.util import Util 22from jnpr.junos.utils.scp import SCP 23from jnpr.junos.utils.ftp import FTP 24from jnpr.junos.utils.start_shell import StartShell 25from jnpr.junos.exception import SwRollbackError, RpcTimeoutError, RpcError 26from ncclient.xml_ import NCElement 27from jnpr.junos import jxml as JXML 28 29""" 30Software Installation Utilities 31""" 32 33__all__ = ["SW"] 34 35 36def _hashfile(afile, hasher, blocksize=65536): 37 buf = afile.read(blocksize) 38 while len(buf) > 0: 39 hasher.update(buf) 40 buf = afile.read(blocksize) 41 return hasher.hexdigest() 42 43 44class SW(Util): 45 """ 46 Software Utility class, used to perform a software upgrade and 47 associated functions. These methods have been tested on 48 *simple deployments*. Refer to **install** for restricted 49 use-cases for software upgrades. 50 51 **Primary methods:** 52 * :meth:`install`: perform the entire software installation process 53 * :meth:`reboot`: reboots the system for the new image to take effect 54 * :meth:`poweroff`: shutdown the system 55 56 **Helpers:** (Useful as standalone as well) 57 * :meth:`put`: SCP put package file onto Junos device 58 * :meth:`pkgadd`: performs the 'request' operation to install the package 59 * :meth:`validate`: performs the 'request' to validate the package 60 61 **Miscellaneous:** 62 * rollback: same as 'request software rollback' 63 * inventory: (property) provides file info for current and rollback 64 images on the device 65 """ 66 67 def __init__(self, dev): 68 Util.__init__(self, dev) 69 self._dev = dev 70 self._RE_list = [] 71 if "junos_info" in dev.facts and dev.facts["junos_info"] is not None: 72 self._RE_list = list(dev.facts["junos_info"].keys()) 73 else: 74 self._RE_list = [x for x in dev.facts.keys() if x.startswith("version_RE")] 75 self._multi_RE = bool(dev.facts.get("2RE")) 76 # Branch SRX in an SRX cluster doesn't really support multi_RE 77 # functionality for SW. 78 if ( 79 dev.facts.get("personality", "") == "SRX_BRANCH" 80 and dev.facts.get("srx_cluster") is True 81 ): 82 self._multi_RE = False 83 self._multi_VC = bool( 84 self._multi_RE is True 85 and dev.facts.get("vc_capable") is True 86 and dev.facts.get("vc_mode") != "Disabled" 87 ) 88 self._mixed_VC = bool(dev.facts.get("vc_mode") == "Mixed") 89 # The devices which currently support single-RE ISSU, communicate with 90 # the new Junos VM using internal IP 128.0.0.63. 91 # Therefore, the 'localre' value in the 'current_re' fact can currently 92 # be used to check for this capability. 93 # {master: 0} 94 # user @ s0 > file show / etc / hosts.junos | match localre 95 # 128.0.0.63 localre 96 self._single_re_issu = bool( 97 "current_re" in dev.facts and "localre" in dev.facts["current_re"] 98 ) 99 self.log = lambda report: None 100 101 # ----------------------------------------------------------------------- 102 # CLASS METHODS 103 # ----------------------------------------------------------------------- 104 105 @classmethod 106 def local_sha256(cls, package): 107 """ 108 Computes the SHA-256 value on the package file. 109 110 :param str package: 111 File-path to the package (\*.tgz) file on the local server 112 113 :returns: SHA-256 checksum (str) 114 :raises IOError: when **package** file does not exist 115 """ 116 return _hashfile(open(package, "rb"), hashlib.sha256()) 117 118 @classmethod 119 def local_md5(cls, package): 120 """ 121 Computes the MD5 checksum value on the local package file. 122 123 :param str package: 124 File-path to the package (\*.tgz) file on the local server 125 126 :returns: MD5 checksum (str) 127 :raises IOError: when **package** file does not exist 128 """ 129 return _hashfile(open(package, "rb"), hashlib.md5()) 130 131 @classmethod 132 def local_sha1(cls, package): 133 """ 134 Computes the SHA1 checksum value on the local package file. 135 136 :param str package: 137 File-path to the package (\*.tgz) file on the local server 138 139 :returns: SHA1 checksum (str) 140 :raises IOError: when **package** file does not exist 141 """ 142 return _hashfile(open(package, "rb"), hashlib.sha1()) 143 144 @classmethod 145 def local_checksum(cls, package, algorithm="md5"): 146 """ 147 Computes the checksum value on the local package file. 148 149 :param str package: 150 File-path to the package (\*.tgz) file on the local server 151 :param str algorithm: 152 The algorithm to use for computing the checksum. Valid values are: 153 'md5', 'sha1', and 'sha256'. Defaults to 'md5'. 154 155 :returns: checksum (str) 156 :raises IOError: when **package** file does not exist 157 """ 158 if algorithm == "md5": 159 return cls.local_md5(package) 160 elif algorithm == "sha1": 161 return cls.local_sha1(package) 162 elif algorithm == "sha256": 163 return cls.local_sha256(package) 164 else: 165 raise ValueError("Unknown checksum algorithm: %s" % (algorithm)) 166 167 @classmethod 168 def progress(cls, dev, report): 169 """ simple progress report function """ 170 print(dev.hostname + ": " + report) 171 172 # ------------------------------------------------------------------------- 173 # put - Copy the image onto the device 174 # ------------------------------------------------------------------------- 175 176 def put(self, package, remote_path="/var/tmp", progress=None): 177 """ 178 SCP or FTP 'put' the package file from the local server to the remote 179 device. 180 181 :param str package: 182 File path to the package file on the local file system 183 184 :param str remote_path: 185 The directory on the device where the package will be copied to. 186 187 :param func progress: 188 Callback function to indicate progress. If set to ``True`` 189 uses :meth:`scp._scp_progress` for basic reporting by default. 190 See that class method for details. 191 """ 192 # execute FTP when connection mode if telnet 193 if hasattr(self._dev, "_mode") and self._dev._mode == "telnet": 194 with FTP(self._dev) as ftp: 195 ftp.put(package, remote_path) 196 else: 197 # execute the secure-copy with the Python SCP module 198 with SCP(self._dev, progress=progress) as scp: 199 scp.put(package, remote_path) 200 201 # ------------------------------------------------------------------------- 202 # pkgadd - used to perform the 'request system software add ...' 203 # ------------------------------------------------------------------------- 204 205 def pkgadd(self, remote_package, vmhost=False, **kvargs): 206 """ 207 Issue the RPC equivalent of the 'request system software add' command 208 or the 'request vmhost software add' command on the package. 209 If vhmhost=False, the <request-package-add> RPC is used and the 210 The "no-validate" options is set. If you want to validate 211 the image, do that using the specific :meth:`validate` method. 212 If vmhost=True, the <request-vmhost-package-add> RPC is used. 213 214 If you want to reboot the device, invoke the :meth:`reboot` method 215 after installing the software rather than passing the ``reboot=True`` 216 parameter. 217 218 :param str remote_package: 219 The file-path to the install package on the remote (Junos) device. 220 221 222 :param bool vhmhost: 223 (Optional) A boolean indicating if this is a software update of the 224 vhmhost. The default is ``vmhost=False``. 225 226 :param dict kvargs: 227 Any additional parameters to the 'request' command can 228 be passed within **kvargs**, following the RPC syntax 229 methodology (dash-2-underscore,etc.) 230 231 .. warning:: Refer to the restrictions listed in :meth:`install`. 232 """ 233 234 if vmhost is False: 235 if isinstance(remote_package, (list, tuple)) and self._mixed_VC: 236 args = dict(no_validate=True, set=remote_package) 237 else: 238 args = dict(no_validate=True, package_name=remote_package) 239 args.update(kvargs) 240 rsp = self.rpc.request_package_add(**args) 241 else: 242 rsp = self.rpc.request_vmhost_package_add( 243 package_name=remote_package, **kvargs 244 ) 245 246 return self._parse_pkgadd_response(rsp) 247 248 # ------------------------------------------------------------------------- 249 # pkgaddNSSU - used to perform NSSU upgrade 250 # ------------------------------------------------------------------------- 251 252 def pkgaddNSSU(self, remote_package, **kvargs): 253 """ 254 Issue the 'request system software nonstop-upgrade' command on the 255 package. 256 257 :param str remote_package: 258 The file-path to the install package on the remote (Junos) device. 259 """ 260 261 rsp = self.rpc.request_package_nonstop_upgrade( 262 package_name=remote_package, **kvargs 263 ) 264 return self._parse_pkgadd_response(rsp) 265 266 # ------------------------------------------------------------------------- 267 # pkgaddISSU - used to perform ISSU upgrade 268 # ------------------------------------------------------------------------- 269 270 def pkgaddISSU(self, remote_package, vmhost=False, **kvargs): 271 """ 272 Issue the RPC equivalent of the 273 'request system software in-service-upgrade' command 274 or the 'request vmhost software in-service-upgrade' command on the 275 package. If vhmhost=False, the <request-package-in-service-upgrade> 276 RPC is used. If vmhost=True, the 277 <request-vmhost-package-in-service-upgrade> RPC is used. 278 279 :param str remote_package: 280 The file-path to the install package on the remote (Junos) device. 281 282 283 :param bool vmhost: 284 (Optional) A boolean indicating if this is a software update of the 285 vhmhost. The default is ``vmhost=False``. 286 """ 287 288 if vmhost is False: 289 rsp = self.rpc.request_package_in_service_upgrade( 290 package_name=remote_package, **kvargs 291 ) 292 else: 293 rsp = self.rpc.request_vmhost_package_in_service_upgrade( 294 package_name=remote_package, **kvargs 295 ) 296 return self._parse_pkgadd_response(rsp) 297 298 def _parse_pkgadd_response(self, rsp): 299 got = rsp.getparent() 300 # If <package-result> is not present, then assume success. 301 # That is, assume <package-result>0</package-result> 302 rc = 0 303 package_result = got.findtext("package-result") 304 if package_result is None: 305 self.log( 306 "software pkgadd response is missing package-result " 307 "element. Assuming success." 308 ) 309 else: 310 for result in got.findall("package-result"): 311 rc += int(result.text.strip()) 312 output_msg = "\n".join( 313 [i.text for i in got.findall("output") if i.text is not None] 314 ) 315 self.log("software pkgadd package-result: %s\nOutput: %s" % (rc, output_msg)) 316 return rc == 0, output_msg 317 318 # ------------------------------------------------------------------------- 319 # validate - perform 'request' operation to validate the package 320 # ------------------------------------------------------------------------- 321 322 def validate(self, remote_package, issu=False, nssu=False, **kwargs): 323 """ 324 Issues the 'request' operation to validate the package against the 325 config. 326 327 :returns: 328 * ``True`` if validation passes. i.e return code (rc) value is 0 329 * * ``False`` otherwise 330 """ 331 if nssu and not self._issu_nssu_requirement_validation(): 332 return False 333 if issu: 334 if not self._issu_requirement_validation(): 335 return False 336 rsp = self.rpc.check_in_service_upgrade( 337 package_name=remote_package, **kwargs 338 ).getparent() 339 else: 340 rsp = self.rpc.request_package_validate( 341 package_name=remote_package, **kwargs 342 ).getparent() 343 rc = int(rsp.findtext("package-result")) 344 output_msg = "\n".join( 345 [i.text for i in rsp.findall("output") if i.text is not None] 346 ) 347 self.log("software validate package-result: %s\nOutput: %s" % (rc, output_msg)) 348 return 0 == rc 349 350 def _issu_requirement_validation(self): 351 """ 352 Checks: 353 * The master Routing Engine and backup Routing Engine must be 354 running the same software version before you can perform a 355 unified ISSU. 356 * Check GRES is enabled 357 * Check NSR is enabled 358 * Check commit synchronize is enabled 359 * Verify that NSR is configured on the master Routing Engine 360 by using the "show task replication" command. 361 * Verify that GRES is enabled on the backup Routing Engine 362 by using the show system switchover command. 363 364 :returns: 365 * ``True`` if validation passes. 366 * * ``False`` otherwise 367 """ 368 self.log( 369 "ISSU requirement validation: The master Routing Engine and\n" 370 "backup Routing engine must be running the same software\n" 371 "version before you can perform a unified ISSU." 372 ) 373 if not ( 374 self._dev.facts["2RE"] 375 and self._dev.facts["version_RE0"] == self._dev.facts["version_RE1"] 376 ): 377 self.log( 378 "Requirement FAILED: The master Routing Engine (%s) and\n" 379 "backup Routing Engine (%s) must be running the same\n" 380 "software version before it can perform a unified ISSU" 381 % (self._dev.facts["version_RE0"], self._dev.facts["version_RE1"]) 382 ) 383 return False 384 if not self._issu_nssu_requirement_validation(): 385 return False 386 self.log( 387 "Verify that GRES is enabled on the backup Routing Engine\n" 388 'by using the command "show system switchover"' 389 ) 390 output = "" 391 try: 392 op = self._dev.rpc.request_shell_execute( 393 routing_engine="backup", command="cli show system switchover" 394 ) 395 if op.findtext(".//switchover-state", default="").lower() == "on": 396 self.log("Graceful switchover status is On") 397 return True 398 output = op.findtext(".//output", default="") 399 except RpcError: 400 # request-shell-execute rpc is not available for <14.1 401 with StartShell(self._dev) as ss: 402 ss.run("cli", "> ", timeout=5) 403 if ss.run("request routing-engine " "login other-routing-engine")[0]: 404 # depending on user permission, prompt will go to either 405 # cli or shell, below line of code prompt will finally end 406 # up in cli mode 407 ss.run("cli", "> ", timeout=5) 408 data = ss.run("show system switchover", "> ", timeout=5) 409 output = data[1] 410 ss.run("exit") 411 else: 412 self.log( 413 "Requirement FAILED: Not able run " '"show system switchover"' 414 ) 415 return False 416 gres_status = re.search(r"Graceful switchover: (\w+)", output, re.I) 417 if not (gres_status is not None and gres_status.group(1).lower() == "on"): 418 self.log("Requirement FAILED: Graceful switchover status " "is not On") 419 return False 420 self.log("Graceful switchover status is On") 421 return True 422 423 def _issu_nssu_requirement_validation(self): 424 """ 425 Checks: 426 * Check GRES is enabled 427 * Check NSR is enabled 428 * Check commit synchronize is enabled 429 * Verify that NSR is configured on the master Routing Engine 430 by using the "show task replication" command. 431 432 :returns: 433 * ``True`` if validation passes. 434 * * ``False`` otherwise 435 """ 436 self.log("Checking GRES configuration") 437 conf = self._dev.rpc.get_config( 438 filter_xml=etree.XML( 439 """ 440 <configuration> 441 <chassis> 442 <redundancy> 443 <graceful-switchover/> 444 </redundancy> 445 </chassis> 446 </configuration>""" 447 ), 448 options={ 449 "database": "committed", 450 "inherit": "inherit", 451 "commit-scripts": "apply", 452 }, 453 ) 454 if conf.find("chassis/redundancy/graceful-switchover") is None: 455 self.log("Requirement FAILED: GRES is not Enabled " "in configuration") 456 return False 457 self.log("Checking commit synchronize configuration") 458 conf = self._dev.rpc.get_config( 459 filter_xml=etree.XML( 460 """ 461 <configuration> 462 <system> 463 <commit> 464 <synchronize/> 465 </commit> 466 </system> 467 </configuration>""" 468 ), 469 options={ 470 "database": "committed", 471 "inherit": "inherit", 472 "commit-scripts": "apply", 473 }, 474 ) 475 if conf.find("system/commit/synchronize") is None: 476 self.log( 477 "Requirement FAILED: commit synchronize is not " 478 "Enabled in configuration" 479 ) 480 return False 481 self.log("Checking NSR configuration") 482 conf = self._dev.rpc.get_config( 483 filter_xml=etree.XML( 484 """ 485 <configuration> 486 <routing-options> 487 <nonstop-routing/> 488 </routing-options> 489 </configuration> 490 """ 491 ), 492 options={ 493 "database": "committed", 494 "inherit": "inherit", 495 "commit-scripts": "apply", 496 }, 497 ) 498 if conf.find("routing-options/nonstop-routing") is None: 499 self.log("Requirement FAILED: NSR is not Enabled in configuration") 500 return False 501 self.log( 502 "Verifying that GRES status on the current Routing Engine " 503 'is Enabled by using the "show task replication" command.' 504 ) 505 op = self._dev.rpc.get_routing_task_replication_state() 506 if not ( 507 op.findtext("task-gres-state") == "Enabled" 508 and op.findtext("task-re-mode") == "Master" 509 ): 510 self.log( 511 "Requirement FAILED: Either Stateful Replication is not " 512 "Enabled or RE mode\nis not Master" 513 ) 514 return False 515 return True 516 517 def remote_checksum(self, remote_package, timeout=300, algorithm="md5"): 518 """ 519 Computes a checksum of the remote_package file on the remote device. 520 521 :param str remote_package: 522 The file-path on the remote Junos device 523 :param int timeout: 524 The amount of time (seconds) before declaring an RPC timeout. 525 The default RPC timeout is generally around 30 seconds. So this 526 :timeout: value will be used in the context of the checksum process. 527 Defaults to 5 minutes (5*60=300) 528 :param str algorithm: 529 The algorithm to use for computing the checksum. Valid values are: 530 'md5', 'sha1', and 'sha256'. Defaults to 'md5'. 531 532 :returns: 533 * The checksum string 534 * ``None`` when the **remote_package** is not found. 535 536 :raises RpcError: RPC errors other than **remote_package** not found. 537 """ 538 kwargs = {"path": remote_package, "dev_timeout": timeout, "normalize": True} 539 try: 540 if algorithm == "md5": 541 rsp = self.rpc.get_checksum_information(**kwargs) 542 elif algorithm == "sha1": 543 rsp = self.rpc.get_sha1_checksum_information(**kwargs) 544 elif algorithm == "sha256": 545 rsp = self.rpc.get_sha256_checksum_information(**kwargs) 546 else: 547 raise ValueError("Unknown checksum algorithm: %s" % (algorithm)) 548 return rsp.findtext(".//checksum") 549 except RpcError as e: 550 if "No such file or directory" in getattr(e, "message", ""): 551 return None 552 else: 553 raise 554 555 # ------------------------------------------------------------------------- 556 # safe_copy - copies the package and performs checksum 557 # ------------------------------------------------------------------------- 558 559 def safe_copy( 560 self, 561 package, 562 remote_path="/var/tmp", 563 progress=None, 564 cleanfs=True, 565 cleanfs_timeout=300, 566 checksum=None, 567 checksum_timeout=300, 568 checksum_algorithm="md5", 569 force_copy=False, 570 ): 571 """ 572 Copy the install package safely to the remote device. By default 573 this means to clean the filesystem to make space, perform the 574 secure-copy, and then verify the checksum. 575 576 :param str package: 577 file-path to package on local filesystem 578 :param str remote_path: 579 file-path to directory on remote device 580 :param func progress: 581 call-back function for progress updates. If set to ``True`` uses 582 :meth:`sw.progress` for basic reporting by default. 583 :param bool cleanfs: 584 When ``True`` (default) perform a 585 "request system storage cleanup" on the device. 586 :param int cleanfs_timeout: 587 Number of seconds (default 300) to wait for the 588 "request system storage cleanup" to complete. 589 :param str checksum: 590 This is the checksum string as computed on the local system. 591 This value will be used to compare the checksum on the 592 remote Junos device. 593 :param int checksum_timeout: 594 Number of seconds (default 300) to wait for the calculation of the 595 checksum on the remote Junos device. 596 :param str checksum_algorithm: 597 The algorithm to use for computing the checksum. Valid values are: 598 'md5', 'sha1', and 'sha256'. Defaults to 'md5'. 599 :param bool force_copy: 600 When ``True`` perform the copy even if the package is already 601 present at the remote_path on the device. When ``False`` (default) 602 if the package is already present at the remote_path, and the local 603 checksum matches the remote checksum, then skip the copy to 604 optimize time. 605 606 :returns: 607 * ``True`` when the copy was successful 608 * ``False`` otherwise 609 """ 610 611 def _progress(report): 612 if progress is True: 613 self.progress(self._dev, report) 614 elif callable(progress): 615 progress(self._dev, report) 616 617 if checksum is None: 618 _progress("computing checksum on local package: %s" % (package)) 619 try: 620 checksum = SW.local_checksum(package, algorithm=checksum_algorithm) 621 except IOError: 622 _progress( 623 "error computing checksum on local package: %s. " 624 "Ensure the local package exists." % (package) 625 ) 626 return False 627 628 if checksum is None: 629 _progress( 630 "Unable to calculate the checksum on local package: %s." % (package) 631 ) 632 return False 633 634 if cleanfs is True: 635 _progress("cleaning filesystem ...") 636 try: 637 self.rpc.request_system_storage_cleanup(dev_timeout=cleanfs_timeout) 638 except RpcError as err: 639 _progress("Problem cleaning filesystem: %s" % (str(err))) 640 return False 641 642 # Calculate the remote package name. 643 remote_package = remote_path + "/" + path.basename(package) 644 645 remote_checksum = None 646 # Check to see if the package file already exists on the remote 647 # device by trying to get the checksum. 648 if force_copy is False: 649 _progress( 650 "before copy, computing checksum on remote package: %s" % remote_package 651 ) 652 remote_checksum = self.remote_checksum( 653 remote_package, timeout=checksum_timeout, algorithm=checksum_algorithm 654 ) 655 656 if remote_checksum != checksum: 657 # Need to copy the file. 658 self.put(package, remote_path=remote_path, progress=progress) 659 660 # Now validate checksum of the recently copied file. 661 _progress( 662 "after copy, computing checksum on remote package: %s" % remote_package 663 ) 664 remote_checksum = self.remote_checksum( 665 remote_package, timeout=checksum_timeout, algorithm=checksum_algorithm 666 ) 667 668 if remote_checksum != checksum: 669 _progress("checksum check failed.") 670 return False 671 672 _progress("checksum check passed.") 673 return True 674 675 # ------------------------------------------------------------------------- 676 # install - complete installation process, but not reboot 677 # ------------------------------------------------------------------------- 678 679 def install( 680 self, 681 package=None, 682 pkg_set=None, 683 remote_path="/var/tmp", 684 progress=None, 685 validate=False, 686 checksum=None, 687 cleanfs=True, 688 no_copy=False, 689 issu=False, 690 nssu=False, 691 timeout=1800, 692 cleanfs_timeout=300, 693 checksum_timeout=300, 694 checksum_algorithm="md5", 695 force_copy=False, 696 all_re=True, 697 vmhost=False, 698 **kwargs 699 ): 700 """ 701 Performs the complete installation of the **package** that includes the 702 following steps: 703 704 1. If :package: is a URL, or :no_copy: is True, skip to step 8. 705 2. computes the checksum of :package: or :pgk_set: on the local host 706 if :checksum: was not provided. 707 3. performs a storage cleanup on the remote Junos device if :cleanfs: 708 is ``True`` 709 4. Attempts to compute the checksum of the :package: filename in the 710 :remote_path: directory of the remote Junos device if the 711 :force_copy: argument is ``False`` 712 5. SCP or FTP copies the :package: file from the local host to the 713 :remote_path: directory on the remote Junos device under any of the 714 following conditions: 715 716 a) The :force_copy: argument is ``True`` 717 b) The :package: filename doesn't already exist in the 718 :remote_path: directory of the remote Junos device. 719 c) The checksum computed in step 2 does not match the checksum 720 computed in step 4. 721 6. If step 5 was executed, computes the checksum of the :package: 722 filename in the :remote_path: directory of the remote Junos device. 723 7. Validates the checksum computed in step 2 matches the checksum 724 computed in step 6. 725 8. validates the package if :validate: is True 726 9. installs the package 727 728 .. warning:: This process has been validated on the following 729 deployments. 730 731 Tested: 732 733 * Single RE devices (EX, QFX, MX, SRX). 734 * MX dual-RE 735 * EX virtual-chassis when all same HW model 736 * QFX virtual-chassis when all same HW model 737 * QFX/EX mixed virtual-chassis 738 * Mixed mode VC 739 740 Known Restrictions: 741 742 * SRX cluster 743 * MX virtual-chassis 744 745 You can get a progress report on this process by providing a 746 **progress** callback. 747 748 .. note:: You will need to invoke the :meth:`reboot` method explicitly 749 to reboot the device. 750 751 :param str package: 752 Either the full file path to the install package tarball on the local 753 (PyEZ host's) filesystem OR a URL (from the target device's 754 perspcective) from which the device retrieves installed. When the 755 value is a URL, then the :no_copy: and :remote_path: values are 756 unused. The acceptable formats for a URL value may be found at: 757 https://www.juniper.net/documentation/en_US/junos/topics/concept/junos-software-formats-filenames-urls.html 758 759 :param list pkg_set: 760 A list/tuple of :package: values which will be installed on a mixed 761 VC setup. 762 763 :param str remote_path: 764 If the value of :package: or :pkg_set: is a file path on the local 765 (PyEZ host's) filesystem, then the image is copied from the local 766 filesystem to the :remote_path: directory on the target Junos 767 device. The default is ``/var/tmp``. If the value of :package: or 768 :pkg_set: is a URL, then the value of :remote_path: is unused. 769 770 :param func progress: 771 If provided, this is a callback function with a function prototype 772 given the Device instance and the report string:: 773 774 def myprogress(dev, report): 775 print "host: %s, report: %s" % (dev.hostname, report) 776 777 If set to ``True``, it uses :meth:`sw.progress` 778 for basic reporting by default. 779 780 :param bool validate: 781 When ``True`` this method will perform a config validation against 782 the new image 783 784 :param str checksum: 785 hexdigest of the package file. If this is not provided, then this 786 method will perform the calculation. If you are planning on using the 787 same image for multiple updates, you should consider using the 788 :meth:`local_checksum` method to pre calculate this value and then 789 provide to this method. 790 791 :param bool cleanfs: 792 When ``True`` will perform a 'storage cleanup' before copying the 793 file to the device. Default is ``True``. 794 795 :param bool no_copy: 796 When the value of :package: or :pkg_set is not a URL, and the value 797 of :no_copy: is ``True`` the software package will not be copied to 798 the device and is presumed to already exist on the :remote_path: 799 directory of the target Junos device. When the value of :no_copy: is 800 ``False`` (the default), then the package is copied from the local 801 PyEZ host to the :remote_path: directory of the target Junos device. 802 If the value of :package: or :pkg_set: is a URL, then the value of 803 :no_copy: is unused. 804 805 :param bool issu: 806 (Optional) When ``True`` allows unified in-service software upgrade 807 (ISSU) feature enables you to upgrade between two different Junos OS 808 releases with no disruption on the control plane and with minimal 809 disruption of traffic. 810 811 :param bool nssu: 812 (Optional) When ``True`` allows nonstop software upgrade (NSSU) 813 enables you to upgrade the software running on a Juniper Networks 814 EX Series Virtual Chassis or a Juniper Networks EX Series Ethernet 815 Switch with redundant Routing Engines with a single command and 816 minimal disruption to network traffic. 817 818 :param int timeout: 819 (Optional) The amount of time (seconds) to wait for the 820 :package: installation to complete before declaring an RPC 821 timeout. This argument was added since most of the time the 822 "package add" RPC takes a significant amount of time. The default 823 RPC timeout is 30 seconds. So this :timeout: value will be 824 used in the context of the SW installation process. Defaults to 825 30 minutes (30*60=1800) 826 827 :param int cleanfs_timeout: 828 (Optional) Number of seconds (default 300) to wait for the 829 "request system storage cleanup" to complete. 830 831 :param int checksum_timeout: 832 (Optional) Number of seconds (default 300) to wait for the 833 calculation of the checksum on the remote Junos device. 834 :param str checksum_algorithm: 835 (Optional) The algorithm to use for computing the checksum. 836 Valid values are: 'md5', 'sha1', and 'sha256'. Defaults to 'md5'. 837 838 :param bool force_copy: 839 (Optional) When ``True`` perform the copy even if :package: is 840 already present at the :remote_path: directory on the remote Junos 841 device. When ``False`` (default) if the :package: is already present 842 at the :remote_path:, AND the local checksum matches the remote 843 checksum, then skip the copy to optimize time. 844 845 :param bool all_re: 846 (Optional) When ``True`` (default) install the package on 847 all Routing Engines of the Junos device. When ``False`` perform 848 the software install only on the current Routing Engine. 849 850 :param bool vmhost: 851 (Optional) A boolean indicating if this is a software update of the 852 vhmhost. The default is ``vmhost=False``. 853 854 :param kwargs **kwargs: 855 (Optional) Additional keyword arguments are passed through to the 856 "package add" RPC. 857 858 :returns: tuple(<status>, <msg>) 859 * status : ``True`` when the installation is successful and ``False`` otherwise 860 * msg : msg received as response or error message created 861 """ 862 if issu is True and nssu is True: 863 raise TypeError("install function can either take issu or nssu not both") 864 elif (issu is True or nssu is True) and ( 865 self._multi_RE is not True and self._single_re_issu is not True 866 ): 867 raise TypeError("ISSU/NSSU requires Multi RE setup") 868 869 def _progress(report): 870 if progress is True: 871 self.progress(self._dev, report) 872 elif callable(progress): 873 progress(self._dev, report) 874 875 self.log = _progress 876 877 # --------------------------------------------------------------------- 878 # Before doing anything, Do check if any pending install exists. 879 # --------------------------------------------------------------------- 880 try: 881 pending_install = self._dev.rpc.request_package_checks_pending_install() 882 msg = pending_install.text 883 if ( 884 msg 885 and msg.strip() != "" 886 and pending_install.getparent().findtext("package-result").strip() 887 == "1" 888 ): 889 _progress(msg) 890 return False 891 except RpcError: 892 _progress( 893 "request-package-checks-pending-install rpc is not " 894 "supported on given device" 895 ) 896 except Exception as ex: 897 _progress("check pending install failed with exception: %s" % ex) 898 # Continue with software installation 899 900 # --------------------------------------------------------------------- 901 # perform a 'safe-copy' of the image to the remote device 902 # --------------------------------------------------------------------- 903 904 if package is None and pkg_set is None: 905 raise TypeError( 906 "install() requires either the package or pkg_set argument." 907 ) 908 909 remote_pkg_set = [] 910 if (sys.version < "3" and isinstance(package, (str, unicode))) or isinstance( 911 package, str 912 ): 913 pkg_set = [package] 914 if isinstance(pkg_set, (list, tuple)) and len(pkg_set) > 0: 915 for pkg in pkg_set: 916 parsed_url = urlparse(pkg) 917 if parsed_url.scheme == "": 918 if no_copy is False: 919 # To disable cleanfs after 1st iteration 920 cleanfs = cleanfs and pkg_set.index(pkg) == 0 921 copy_ok = self.safe_copy( 922 pkg, 923 remote_path=remote_path, 924 progress=progress, 925 cleanfs=cleanfs, 926 checksum=checksum, 927 cleanfs_timeout=cleanfs_timeout, 928 checksum_timeout=checksum_timeout, 929 checksum_algorithm=checksum_algorithm, 930 force_copy=force_copy, 931 ) 932 if copy_ok is False: 933 return False, "Package %s couldn't be copied" % pkg 934 pkg = remote_path + "/" + path.basename(pkg) 935 936 remote_pkg_set.append(pkg) 937 else: 938 raise ValueError("proper value for either package or pkg_set is missing") 939 # --------------------------------------------------------------------- 940 # at this point, the file exists on the remote device 941 # or will be loaded directly from a URL. 942 # --------------------------------------------------------------------- 943 944 if len(remote_pkg_set) == 1: 945 remote_package = remote_pkg_set[0] 946 # validate can't be used in the case of a Mixed VC 947 # With vmhost=True, validate is handled in the package add. 948 if validate is True: 949 if self._mixed_VC is False and vmhost is not True: 950 _progress( 951 "validating software against current config," 952 " please be patient ..." 953 ) 954 v_ok = self.validate( 955 remote_package, issu, nssu, dev_timeout=timeout 956 ) 957 if v_ok is not True: 958 return v_ok, "Package validation failed" 959 else: 960 if vmhost is True: 961 # Need to pass the no_validate option via kwargs. 962 kwargs.update({"no_validate": True}) 963 964 if issu is True: 965 _progress("ISSU: installing software ... please be patient ...") 966 return self.pkgaddISSU( 967 remote_package, vmhost=vmhost, dev_timeout=timeout, **kwargs 968 ) 969 elif nssu is True: 970 _progress("NSSU: installing software ... please be patient ...") 971 return self.pkgaddNSSU(remote_package, dev_timeout=timeout, **kwargs) 972 elif self._multi_RE is False or all_re is False: 973 # simple case of single RE upgrade. 974 _progress("installing software ... please be patient ...") 975 add_ok = self.pkgadd( 976 remote_package, vmhost=vmhost, dev_timeout=timeout, **kwargs 977 ) 978 return add_ok 979 else: 980 # we need to update multiple devices 981 if self._multi_VC is True: 982 ok = True, "" 983 # extract the VC number out of the _RE_list 984 vc_members = [re.search("(\d+)", x).group(1) for x in self._RE_list] 985 for vc_id in vc_members: 986 _progress( 987 "installing software on VC member: {} ... please " 988 "be patient ...".format(vc_id) 989 ) 990 bool_ret, msg = self.pkgadd( 991 remote_package, 992 vmhost=vmhost, 993 member=vc_id, 994 dev_timeout=timeout, 995 **kwargs 996 ) 997 ok = ok[0] and bool_ret, ok[1] + "\n" + msg 998 return ok 999 else: 1000 # then this is a device with two RE that supports the "re0" 1001 # and "re1" options to the command (M, MX tested only) 1002 _progress("installing software on RE0 ... please be patient ...") 1003 ok = self.pkgadd( 1004 remote_package, 1005 vmhost=vmhost, 1006 re0=True, 1007 dev_timeout=timeout, 1008 **kwargs 1009 ) 1010 _progress("installing software on RE1 ... please be patient ...") 1011 bool_ret, msg = self.pkgadd( 1012 remote_package, 1013 vmhost=vmhost, 1014 re1=True, 1015 dev_timeout=timeout, 1016 **kwargs 1017 ) 1018 ok = ok[0] and bool_ret, ok[1] + "\n" + msg 1019 return ok 1020 1021 elif len(remote_pkg_set) > 1 and self._mixed_VC: 1022 _progress("installing software ... please be patient ...") 1023 add_ok = self.pkgadd( 1024 remote_pkg_set, vmhost=vmhost, dev_timeout=timeout, **kwargs 1025 ) 1026 return add_ok 1027 1028 def _system_operation( 1029 self, cmd, in_min=0, at=None, all_re=True, other_re=False, vmhost=False 1030 ): 1031 """ 1032 Send the rpc for actions like shutdown, reboot, halt with optional 1033 delay (in minutes) or at a specified date and time. 1034 1035 :param int in_min: time (minutes) before rebooting/shutting down the device. 1036 1037 :param str at: date and time the reboot should take place. The 1038 string must match the junos cli reboot/poweroff/halt syntax 1039 1040 :param bool all_re: In case of dual re or VC setup, function by default 1041 will reboot/shutdown all. If all is False will only reboot/shutdown connected device 1042 1043 :param str on_node: In case of linux based device, function will by default 1044 reboot the whole device. If any specific node is mentioned, 1045 reboot will be performed on mentioned node 1046 1047 :param str other_re: If the system has dual Routing Engines and this option is C(true), 1048 then the action is performed on the other REs in the system. 1049 1050 :param bool vmhost: 1051 (Optional) A boolean indicating to run 'request vmhost reboot'. 1052 The default is ``vmhost=False``. 1053 1054 :returns: 1055 * rpc response message (string) if command successful 1056 1057 :raises RpcError: when command is not successful. 1058 """ 1059 if other_re is True: 1060 if self._dev.facts["2RE"]: 1061 cmd = E("other-routing-engine") 1062 elif all_re is True: 1063 if self._multi_RE is True and vmhost is True: 1064 cmd.append(E("routing-engine", "both")) 1065 elif self._multi_RE is True and self._multi_VC is False: 1066 cmd.append(E("both-routing-engines")) 1067 elif self._mixed_VC is True: 1068 cmd.append(E("all-members")) 1069 if in_min >= 0 and at is None: 1070 cmd.append(E("in", str(in_min))) 1071 elif at is not None: 1072 cmd.append(E("at", str(at))) 1073 try: 1074 rsp = self.rpc(cmd, ignore_warning=True, normalize=True) 1075 if self._dev.facts["_is_linux"]: 1076 got = rsp.text 1077 else: 1078 got = rsp.getparent().findtext(".//request-reboot-status") 1079 if got is None: 1080 # On some platforms stopping/rebooting 1081 # REs produces <output> messages and 1082 # <request-reboot-status> messages. 1083 output_msg = "\n".join( 1084 [ 1085 i.text 1086 for i in rsp.getparent().xpath("//output") 1087 if i.text is not None 1088 ] 1089 ) 1090 if output_msg is not "": 1091 got = output_msg 1092 return got 1093 except Exception as err: 1094 raise err 1095 1096 # ------------------------------------------------------------------------- 1097 # reboot - system reboot 1098 # ------------------------------------------------------------------------- 1099 def reboot( 1100 self, in_min=0, at=None, all_re=True, on_node=None, vmhost=False, other_re=False 1101 ): 1102 """ 1103 Perform a system reboot, with optional delay (in minutes) or at 1104 a specified date and time. 1105 1106 If the device is equipped with dual-RE, then both RE will be 1107 rebooted. This code also handles EX/QFX VC. 1108 1109 :param int in_min: time (minutes) before rebooting the device. 1110 1111 :param str at: date and time the reboot should take place. The 1112 string must match the junos cli reboot syntax 1113 1114 :param bool all_re: In case of dual re or VC setup, function by default 1115 will reboot all. If all is False will only reboot connected device 1116 1117 :param str on_node: In case of linux based device, function will by default 1118 reboot the whole device. If any specific node is mentioned, 1119 reboot will be performed on mentioned node 1120 1121 :param bool vmhost: 1122 (Optional) A boolean indicating to run 'request vmhost reboot'. 1123 The default is ``vmhost=False``. 1124 1125 :param str other_re: If the system has dual Routing Engines and this option is C(true), 1126 then the action is performed on the other REs in the system. 1127 1128 :returns: 1129 * reboot message (string) if command successful 1130 """ 1131 if self._dev.facts["_is_linux"]: 1132 if on_node is None: 1133 cmd = E("request-shutdown-reboot") 1134 else: 1135 cmd = E("request-node-reboot") 1136 cmd.append(E("node", on_node)) 1137 elif vmhost is True: 1138 cmd = E("request-vmhost-reboot") 1139 else: 1140 cmd = E("request-reboot") 1141 1142 try: 1143 return self._system_operation(cmd, in_min, at, all_re, other_re, vmhost) 1144 except RpcTimeoutError as err: 1145 raise err 1146 except Exception as err: 1147 raise err 1148 1149 # ------------------------------------------------------------------------- 1150 # poweroff - system shutdown 1151 # ------------------------------------------------------------------------- 1152 def poweroff(self, in_min=0, at=None, on_node=None, all_re=True, other_re=False): 1153 """ 1154 Perform a system shutdown, with optional delay (in minutes) . 1155 1156 If the device is equipped with dual-RE, then both RE will be 1157 shut down. This code also handles EX/QFX VC. 1158 1159 :param int in_min: time (minutes) before shutting down the device. 1160 1161 :param str at: date and time the poweroff should take place. The 1162 string must match the junos cli poweroff syntax 1163 1164 :param str on_node: In case of linux based device, function will by default 1165 shutdown the whole device. If any specific node is mentioned, 1166 shutdown will be performed on mentioned node 1167 1168 :param bool all_re: In case of dual re or VC setup, function by default 1169 will shutdown all. If all is False will only shutdown connected device 1170 1171 :param str other_re: If the system has dual Routing Engines and this option is C(true), 1172 then the action is performed on the other REs in the system. 1173 1174 :returns: 1175 * power-off message (string) if command successful 1176 1177 :raises RpcError: when command is not successful. 1178 1179 .. todo:: need to better handle the exception event. 1180 """ 1181 if self._dev.facts["_is_linux"]: 1182 if on_node is None: 1183 cmd = E("request-shutdown-power-off") 1184 else: 1185 cmd = E("request-node-power-off") 1186 cmd.append(E("node", on_node)) 1187 else: 1188 cmd = E("request-power-off") 1189 try: 1190 return self._system_operation( 1191 cmd, in_min, at, all_re, other_re, vmhost=False 1192 ) 1193 except Exception as err: 1194 if err.rsp.findtext(".//error-severity") != "warning": 1195 raise err 1196 1197 # ------------------------------------------------------------------------- 1198 # halt - system halt 1199 # ------------------------------------------------------------------------- 1200 def halt(self, in_min=0, at=None, all_re=True, other_re=False): 1201 """ 1202 Perform a system halt, with optional delay (in minutes) or at 1203 a specified date and time. 1204 1205 :param int in_min: time (minutes) before halting the device. 1206 1207 :param str at: date and time the halt should take place. The 1208 string must match the junos cli reboot syntax 1209 1210 :param bool all_re: In case of dual re or VC setup, function by default 1211 will halt all. If all is False will only halt connected device 1212 1213 :param str other_re: If the system has dual Routing Engines and this option is C(true), 1214 then the action is performed on the other REs in the system. 1215 1216 :returns: 1217 * rpc response message (string) if command successful 1218 """ 1219 if self._dev.facts["_is_linux"]: 1220 cmd = E("request-shutdown-halt") 1221 else: 1222 cmd = E("request-halt") 1223 1224 try: 1225 return self._system_operation( 1226 cmd, in_min, at, all_re, other_re, vmhost=False 1227 ) 1228 except Exception as err: 1229 raise err 1230 1231 def zeroize(self, all_re=False, media=None): 1232 """ 1233 Restore the system (configuration, log files, etc.) to a 1234 factory default state. This is the equivalent of the 1235 C(request system zeroize) CLI command. 1236 1237 :param bool all_re: In case of dual re or VC setup, function by default 1238 will halt all. If all is False will only halt connected device 1239 1240 :param str media: Overwrite media when performing the zeroize operation. 1241 1242 :returns: 1243 * rpc response message (string) if command successful 1244 """ 1245 cmd = E("request-system-zeroize") 1246 if all_re is False: 1247 if self._dev.facts["2RE"]: 1248 cmd = E("local") 1249 if media is True: 1250 cmd = E("media") 1251 1252 # initialize an empty output message 1253 output_msg = "" 1254 1255 try: 1256 # For zeroize we don't get a response similar to reboot, shutdown. 1257 # The response may come as a warning message only. 1258 # Code is added here to extract the warning message and append it. 1259 # Don't pass ignore warning true and handle the warning here. 1260 rsp = self.rpc(cmd, normalize=True) 1261 except RpcError as ex: 1262 if hasattr(ex, "xml"): 1263 if hasattr(ex, "errs"): 1264 errors = ex.errs 1265 else: 1266 errors = [ex] 1267 for err in errors: 1268 if err.get("severity", "") != "warning": 1269 # Not a warning (probably an error). 1270 raise ex 1271 output_msg += err.get("message", "") + "\n" 1272 rsp = ex.xml.getroottree().getroot() 1273 # 1) A normal response has been run through the XSLT 1274 # transformation, but ex.xml has not. Do that now. 1275 encode = None if sys.version < "3" else "unicode" 1276 rsp = NCElement( 1277 etree.tostring(rsp, encoding=encode), self._dev.transform() 1278 )._NCElement__doc 1279 # 2) Now remove all of the <rpc-error> elements from 1280 # the response. We've already confirmed they are all warnings 1281 rsp = etree.fromstring(str(JXML.strip_rpc_error_transform(rsp))) 1282 else: 1283 # ignore_warning was false, or an RPCError which doesn't have 1284 # an XML attribute. Raise it up for the caller to deal with. 1285 raise ex 1286 except Exception as err: 1287 raise err 1288 1289 # safety check added in case the rpc-reply for zeroize doesn't have message 1290 # This scenario is not expected. 1291 if isinstance(rsp, bool): 1292 return "zeroize initiated with no message" 1293 1294 output_msg += "\n".join( 1295 [i.text for i in rsp.xpath("//message") if i.text is not None] 1296 ) 1297 return output_msg 1298 1299 # ------------------------------------------------------------------------- 1300 # rollback - clears the install request 1301 # ------------------------------------------------------------------------- 1302 1303 def rollback(self): 1304 """ 1305 Issues the 'request' command to do the rollback and returns the string 1306 output of the results. 1307 1308 :returns: 1309 Rollback results (str) 1310 """ 1311 rsp = self.rpc.request_package_rollback() 1312 fail_list = ["Cannot rollback", "rollback aborted"] 1313 multi = rsp.xpath("//multi-routing-engine-item") 1314 if multi: 1315 rsp = {} 1316 for x in multi: 1317 re = x.findtext("re-name") 1318 output = x.findtext("output") 1319 if any(x in output for x in fail_list): 1320 raise SwRollbackError(re=re, rsp=output) 1321 else: 1322 rsp[re] = output 1323 return str(rsp) 1324 else: 1325 output = rsp.xpath("//output")[0].text 1326 if any(x in output for x in fail_list): 1327 raise SwRollbackError(rsp=output) 1328 else: 1329 return output 1330 1331 # ------------------------------------------------------------------------- 1332 # inventory - file info on current and rollback packages 1333 # ------------------------------------------------------------------------- 1334 1335 @property 1336 def inventory(self): 1337 """ 1338 Returns dictionary of file listing information for current and rollback 1339 Junos install packages. This information comes from the /packages 1340 directory. 1341 1342 .. warning:: Experimental method; may not work on all platforms. If 1343 you find this not working, please report issue. 1344 """ 1345 from jnpr.junos.utils.fs import FS 1346 1347 fs = FS(self.dev) 1348 pkgs = fs.ls("/packages") 1349 return dict( 1350 current=pkgs["files"].get("junos"), rollback=pkgs["files"].get("junos.old") 1351 ) 1352