diff --git a/lib/jnpr/junos/utils/sw.py b/lib/jnpr/junos/utils/sw.py index af20f4155..9d73cdb63 100644 --- a/lib/jnpr/junos/utils/sw.py +++ b/lib/jnpr/junos/utils/sw.py @@ -62,6 +62,7 @@ def __init__(self, dev): self._multi_RE is True and dev.facts.get('vc_capable') is True and dev.facts.get('vc_mode') != 'Disabled') self._mixed_VC = bool(dev.facts.get('vc_mode') == 'Mixed') + self.log = lambda report : None # ----------------------------------------------------------------------- # CLASS METHODS @@ -159,7 +160,6 @@ def pkgadd(self, remote_package, **kvargs): be passed within **kvargs**, following the RPC syntax methodology (dash-2-underscore,etc.) - .. todo:: Add way to notify user why installation failed. .. warning:: Refer to the restrictions listed in :meth:`install`. """ @@ -170,30 +170,73 @@ def pkgadd(self, remote_package, **kvargs): args.update(kvargs) rsp = self.rpc.request_package_add(**args) + return self._parse_pkgadd_response(rsp) + + # ------------------------------------------------------------------------- + # pkgaddNSSU - used to perform NSSU upgrade + # ------------------------------------------------------------------------- + + def pkgaddNSSU(self, remote_package, **kvargs): + """ + Issue the 'request system software nonstop-upgrade' command on the package. + + :param str remote_package: + The file-path to the install package on the remote (Junos) device. + """ + + rsp = self.rpc.request_package_nonstop_upgrade( + package_name=remote_package, **kvargs) + return self._parse_pkgadd_response(rsp) + + # ------------------------------------------------------------------------- + # pkgaddISSU - used to perform ISSU upgrade + # ------------------------------------------------------------------------- + + def pkgaddISSU(self, remote_package, **kvargs): + """ + Issue the 'request system software nonstop-upgrade' command on the package. + + :param str remote_package: + The file-path to the install package on the remote (Junos) device. + """ + + rsp = self.rpc.request_package_in_service_upgrade( + package_name=remote_package, **kvargs) + return self._parse_pkgadd_response(rsp) + + def _parse_pkgadd_response(self, rsp): got = rsp.getparent() rc = int(got.findtext('package-result').strip()) - - # return True if rc == 0 else got.findtext('output').strip() - return True if rc == 0 else False + output_msg = '\n'.join([i.text for i in got.findall('output')]) + self.log("software pkgadd package-result: %s\nOutput: %s" % ( + rc, output_msg)) + return rc == 0 # ------------------------------------------------------------------------- # validate - perform 'request' operation to validate the package # ------------------------------------------------------------------------- - def validate(self, remote_package, **kwargs): + def validate(self, remote_package, issu=False, **kwargs): """ Issues the 'request' operation to validate the package against the config. :returns: - * ``True`` if validation passes - * error (str) otherwise + * ``True`` if validation passes. i.e return code (rc) value is 0 + * * ``False`` otherwise """ - rsp = self.rpc.request_package_validate( - package_name=remote_package, **kwargs).getparent() - errcode = int(rsp.findtext('package-result')) - return True if 0 == errcode else rsp.findtext('output').strip() + if issu: + rsp = self.rpc.check_in_service_upgrade( + package_name=remote_package, **kwargs).getparent() + else: + rsp = self.rpc.request_package_validate( + package_name=remote_package, **kwargs).getparent() + rc = int(rsp.findtext('package-result')) + output_msg = '\n'.join([i.text for i in rsp.findall('output')]) + self.log("software validate package-result: %s\nOutput: %s" % ( + rc, output_msg)) + return 0 == rc def remote_checksum(self, remote_package, timeout=300): """ @@ -298,8 +341,8 @@ def _progress(report): # ------------------------------------------------------------------------- def install(self, package=None, pkg_set=None, remote_path='/var/tmp', progress=None, - validate=False, checksum=None, cleanfs=True, no_copy=False, - timeout=1800, **kwargs): + validate=False, checksum=None, cleanfs=True, no_copy=False, issu=False, + nssu=False, timeout=1800, **kwargs): """ Performs the complete installation of the **package** that includes the following steps: @@ -384,13 +427,39 @@ def myprogress(dev, report): :param bool force_host: (Optional) Force the addition of host software package or bundle (ignore warnings) on the QFX5100 device. + + :param bool issu: + (Optional) When ``True`` allows unified in-service software upgrade + (ISSU) feature enables you to upgrade between two different Junos OS + releases with no disruption on the control plane and with minimal + disruption of traffic. + + :param bool nssu: + (Optional) When ``True`` allows nonstop software upgrade (NSSU) + enables you to upgrade the software running on a Juniper Networks + EX Series Virtual Chassis or a Juniper Networks EX Series Ethernet + Switch with redundant Routing Engines with a single command and minimal + disruption to network traffic. + + :returns: + * ``True`` when the installation is successful + * ``False`` otherwise """ + if issu is True and nssu is True: + raise TypeError( + 'install function can either take issu or nssu not both') + elif (issu is True or nssu is True) and self._multi_RE is not True: + raise TypeError( + 'ISSU/NSSU requires Multi RE setup') + def _progress(report): if progress is True: self.progress(self._dev, report) elif callable(progress): progress(self._dev, report) + self.log = _progress + # --------------------------------------------------------------------- # perform a 'safe-copy' of the image to the remote device # --------------------------------------------------------------------- @@ -430,11 +499,20 @@ def _progress(report): _progress( "validating software against current config," " please be patient ...") - v_ok = self.validate(remote_package, dev_timeout=timeout) - if v_ok is not True: - return v_ok # will be the string of output + v_ok = self.validate(remote_package, issu, dev_timeout=timeout) - if self._multi_RE is False: + if v_ok is not True: + return v_ok + + if issu is True: + _progress("ISSU: installing software ... please be patient ...") + return self.pkgaddISSU(remote_package, + dev_timeout=timeout, **kwargs) + elif nssu is True: + _progress("NSSU: installing software ... please be patient ...") + return self.pkgaddNSSU(remote_package, + dev_timeout=timeout, **kwargs) + elif self._multi_RE is False: # simple case of device with only one RE _progress("installing software ... please be patient ...") add_ok = self.pkgadd( diff --git a/tests/unit/utils/rpc-reply/check-in-service-upgrade.xml b/tests/unit/utils/rpc-reply/check-in-service-upgrade.xml new file mode 100644 index 000000000..aa3a9a050 --- /dev/null +++ b/tests/unit/utils/rpc-reply/check-in-service-upgrade.xml @@ -0,0 +1,26 @@ + + + + + + + hup + + + + Verified junos-install-mx-x86-64-16.1-20160925.0 signed by PackageDevelopmentEc_2016 + Verified manifest signed by PackageDevelopmentEc_2016 + Checking PIC combinations + Verified fips-mode signed by PackageDevelopmentEc_2016 + Verified jail-runtime signed by PackageDevelopmentEc_2016 + Verified jdocs signed by PackageDevelopmentEc_2016 + Verified jpfe-X960 signed by PackageDevelopmentEc_2016 + Verified jpfe-common signed by PackageDevelopmentEc_2016 + Verified jpfe-wrlinux signed by PackageDevelopmentEc_2016 + Verified jsd signed by PackageDevelopmentEc_2016 + Verified vrr-mx signed by PackageDevelopmentEc_2016 + + + 0 + + diff --git a/tests/unit/utils/rpc-reply/request-package-in-service-upgrade.xml b/tests/unit/utils/rpc-reply/request-package-in-service-upgrade.xml new file mode 100644 index 000000000..aa3a9a050 --- /dev/null +++ b/tests/unit/utils/rpc-reply/request-package-in-service-upgrade.xml @@ -0,0 +1,26 @@ + + + + + + + hup + + + + Verified junos-install-mx-x86-64-16.1-20160925.0 signed by PackageDevelopmentEc_2016 + Verified manifest signed by PackageDevelopmentEc_2016 + Checking PIC combinations + Verified fips-mode signed by PackageDevelopmentEc_2016 + Verified jail-runtime signed by PackageDevelopmentEc_2016 + Verified jdocs signed by PackageDevelopmentEc_2016 + Verified jpfe-X960 signed by PackageDevelopmentEc_2016 + Verified jpfe-common signed by PackageDevelopmentEc_2016 + Verified jpfe-wrlinux signed by PackageDevelopmentEc_2016 + Verified jsd signed by PackageDevelopmentEc_2016 + Verified vrr-mx signed by PackageDevelopmentEc_2016 + + + 0 + + diff --git a/tests/unit/utils/rpc-reply/request-package-nonstop-upgrade.xml b/tests/unit/utils/rpc-reply/request-package-nonstop-upgrade.xml new file mode 100644 index 000000000..aa3a9a050 --- /dev/null +++ b/tests/unit/utils/rpc-reply/request-package-nonstop-upgrade.xml @@ -0,0 +1,26 @@ + + + + + + + hup + + + + Verified junos-install-mx-x86-64-16.1-20160925.0 signed by PackageDevelopmentEc_2016 + Verified manifest signed by PackageDevelopmentEc_2016 + Checking PIC combinations + Verified fips-mode signed by PackageDevelopmentEc_2016 + Verified jail-runtime signed by PackageDevelopmentEc_2016 + Verified jdocs signed by PackageDevelopmentEc_2016 + Verified jpfe-X960 signed by PackageDevelopmentEc_2016 + Verified jpfe-common signed by PackageDevelopmentEc_2016 + Verified jpfe-wrlinux signed by PackageDevelopmentEc_2016 + Verified jsd signed by PackageDevelopmentEc_2016 + Verified vrr-mx signed by PackageDevelopmentEc_2016 + + + 0 + + diff --git a/tests/unit/utils/test_sw.py b/tests/unit/utils/test_sw.py index 0f26fd292..3a77affa1 100644 --- a/tests/unit/utils/test_sw.py +++ b/tests/unit/utils/test_sw.py @@ -35,6 +35,8 @@ '2RE': False, 'serialnumber': 'aaf5fe5f9b88', 'fqdn': 'firefly', 'virtual': True, 'switch_style': 'NONE', 'version': '12.1X46-D15.3', 'HOME': '/cf/var/home/rick', 'srx_cluster': False, + 'version_RE0': '16.1-20160925.0', + 'version_RE1': '16.1-20160925.0', 'model': 'FIREFLY-PERIMETER', 'RE0': {'status': 'Testing', 'last_reboot_reason': 'Router rebooted after a ' @@ -77,7 +79,7 @@ def test_sw_hashfile(self): def test_sw_constructor_multi_re(self, mock_execute): mock_execute.side_effect = self._mock_manager self.sw = SW(self.dev) - self.assertFalse(self.sw._multi_RE) + self.assertTrue(self.sw._multi_RE) @patch('jnpr.junos.Device.execute') def test_sw_constructor_multi_vc(self, mock_execute): @@ -108,10 +110,23 @@ def test_sw_progress(self): with self.capture(SW.progress, self.dev, 'running') as output: self.assertEqual('1.1.1.1: running\n', output) + def test_sw_progress(self): + with self.capture(SW.progress, self.dev, 'running') as output: + self.assertEqual('1.1.1.1: running\n', output) + + @patch('jnpr.junos.Device.execute') + @patch('paramiko.SSHClient') + @patch('scp.SCPClient.put') + def test_sw_progress_true(self, scp_put, mock_paramiko, mock_execute): + mock_execute.side_effect = self._mock_manager + with self.capture(SW.progress, self.dev, 'testing') as output: + self.sw.install('test.tgz', progress=True, checksum=345, + cleanfs=False) + self.assertEqual('1.1.1.1: testing\n', output) + @patch('paramiko.SSHClient') @patch('scp.SCPClient.put') def test_sw_put(self, mock_scp_put, mock_scp): - # mock_scp_put.side_effect = self.mock_put package = 'test.tgz' self.sw.put(package) self.assertTrue( @@ -119,6 +134,17 @@ def test_sw_put(self, mock_scp_put, mock_scp): 'test.tgz', '/var/tmp') in mock_scp_put.mock_calls) + @patch('jnpr.junos.utils.sw.FTP') + def test_sw_put_ftp(self, mock_ftp_put): + dev = Device(host='1.1.1.1', user='rick', password='password123', + mode='telnet', port=23, gather_facts=False) + sw = SW(dev) + sw.put(package='test.tgz') + self.assertTrue( + call( + 'test.tgz', + '/var/tmp') in mock_ftp_put.mock_calls) + @patch('jnpr.junos.utils.scp.SCP.__exit__') @patch('jnpr.junos.utils.scp.SCP.__init__') @patch('jnpr.junos.utils.scp.SCP.__enter__') @@ -138,6 +164,45 @@ def test_sw_pkgadd(self, mock_execute): package = 'test.tgz' self.assertTrue(self.sw.pkgadd(package)) + @patch('jnpr.junos.Device.execute') + def test_sw_install_issu(self, mock_execute): + mock_execute.side_effect = self._mock_manager + package = 'test.tgz' + self.assertTrue(self.sw.install(package, issu=True, no_copy=True)) + + @patch('jnpr.junos.Device.execute') + def test_sw_install_nssu(self, mock_execute): + mock_execute.side_effect = self._mock_manager + package = 'test.tgz' + self.assertTrue(self.sw.install(package, nssu=True, no_copy=True)) + + @patch('jnpr.junos.Device.execute') + def test_sw_install_issu_nssu_both_error(self, mock_execute): + mock_execute.side_effect = self._mock_manager + package = 'test.tgz' + self.assertRaises(TypeError, self.sw.install, package, + nssu=True, issu=True) + + @patch('jnpr.junos.Device.execute') + def test_sw_install_issu_single_re_error(self, mock_execute): + mock_execute.side_effect = self._mock_manager + package = 'test.tgz' + self.sw._multi_RE = False + self.assertRaises(TypeError, self.sw.install, package, + nssu=True, issu=True) + + @patch('jnpr.junos.Device.execute') + def test_sw_pkgaddISSU(self, mock_execute): + mock_execute.side_effect = self._mock_manager + package = 'test.tgz' + self.assertTrue(self.sw.pkgaddISSU(package)) + + @patch('jnpr.junos.Device.execute') + def test_sw_pkgaddNSSU(self, mock_execute): + mock_execute.side_effect = self._mock_manager + package = 'test.tgz' + self.assertTrue(self.sw.pkgaddNSSU(package)) + @patch('jnpr.junos.Device.execute') def test_sw_pkgadd_pkg_set(self, mock_execute): mock_execute.side_effect = self._mock_manager @@ -154,6 +219,23 @@ def test_sw_validate(self, mock_execute): package = 'package.tgz' self.assertTrue(self.sw.validate(package)) + @patch('jnpr.junos.Device.execute') + def test_sw_validate_issu(self, mock_execute): + mock_execute.side_effect = self._mock_manager + package = 'package.tgz' + self.assertTrue(self.sw.validate(package, issu=True)) + + @patch('jnpr.junos.Device.execute') + def test_sw_validate_issu(self, mock_execute): + rpc_reply = """mgd: commit complete + Validation succeeded + + 1 + """ + mock_execute.side_effect = etree.fromstring(rpc_reply) + package = 'package.tgz' + self.assertFalse(self.sw.validate(package, issu=True)) + @patch('jnpr.junos.Device.execute') def test_sw_remote_checksum_not_found(self, mock_execute): xml = ''' @@ -258,11 +340,11 @@ def test_sw_install_mixed_vc(self, mock_pkgadd): @patch('jnpr.junos.utils.sw.SW.pkgadd') def test_sw_install_multi_vc_mode_disabled(self, mock_pkgadd): mock_pkgadd.return_value = True - self.dev._facts = { + self.dev._facts = {'2RE': True, 'domain': None, 'RE1': { 'status': 'OK', 'model': 'RE-EX8208', 'mastership_state': 'backup'}, 'ifd_style': 'SWITCH', - 'version_RE1': '12.3R7.7', 'version_RE0': '12.3', '2RE': True, + 'version_RE1': '12.3R7.7', 'version_RE0': '12.3', 'serialnumber': 'XXXXXX', 'fqdn': 'XXXXXX', 'RE0': {'status': 'OK', 'model': 'RE-EX8208', 'mastership_state': 'master'}, 'switch_style': 'VLAN', @@ -333,15 +415,31 @@ def test_sw_install_mixed_vc_TypeError(self, mock_pkgadd): def test_sw_install_kwargs_force_host(self, mock_execute): self.sw.install('file', no_copy=True, force_host=True) rpc = [ + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', + '/var/tmp/file', '/var/tmp/file', '/var/tmp/file', '/var/tmp/file', '/var/tmp/file', '/var/tmp/file', '/var/tmp/file'] - self.assertTrue( - (etree.tostring( - mock_execute.call_args[0][0])).decode('utf-8)') in rpc) + self.assertTrue(etree.tostring( + mock_execute.call_args[0][0]).decode('utf-8') in rpc) @patch('jnpr.junos.Device.execute') def test_sw_rollback(self, mock_execute): @@ -398,6 +496,7 @@ def test_sw_reboot_multi_re_vc(self, mock_execute): def test_sw_reboot_mixed_vc(self, mock_execute): mock_execute.side_effect = self._mock_manager self.sw._mixed_VC = True + self.sw._multi_VC = True self.sw.reboot() self.assertTrue('all-members' in (etree.tostring(mock_execute.call_args[0][0]).decode('utf-8')))