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