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
+
+
+
+
+ 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
+
+
+
+
+ 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
+
+
+
+
+ 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 = """
+ 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')))