Add ISSU capability and fix workflow bug for nxos_install_os module (#32540)

* Initial issu support

* Enhance ISSU support

* Additional refactoring to simplify code flow

* Remove debugs and bug fixes

* Update doc example output

* Update provider line in task example

* Remove unneeded else clause and comments

* Fix ansible-doc errors

* Satisfy ansibot requirements

* Update header docs

* Update nxos_install_os.py
pull/32755/head
Mike Wiebe 7 years ago committed by Trishna Guha
parent db749de5b8
commit fedd1779cc

@ -164,19 +164,26 @@ class Cli:
responses.append(out)
return responses
def load_config(self, config, return_error=False):
def load_config(self, config, return_error=False, opts=None):
"""Sends configuration commands to the remote device
"""
if opts is None:
opts = {}
errors = 'surrogate_then_replace'
rc, out, err = self.exec_command('configure')
if rc != 0:
self._module.fail_json(msg='unable to enter configuration mode', output=to_text(err, errors='surrogate_then_replace'))
msg = 'unable to enter configuration mode'
self._module.fail_json(msg=msg, output=to_text(err, errors=errors))
msgs = []
for cmd in config:
rc, out, err = self.exec_command(cmd)
if rc != 0:
self._module.fail_json(msg=to_text(err, errors='surrogate_then_replace'))
if opts.get('ignore_timeout') and rc == 1:
msgs.append(err)
return msgs
elif rc != 0:
self._module.fail_json(msg=to_text(err, errors=errors))
elif out:
msgs.append(out)
@ -243,9 +250,12 @@ class Nxapi:
return dict(ins_api=msg)
def send_request(self, commands, output='text', check_status=True, return_error=False):
def send_request(self, commands, output='text', check_status=True,
return_error=False, opts=None):
# only 10 show commands can be encoded in each request
# messages sent to the remote device
if opts is None:
opts = {}
if output != 'config':
commands = collections.deque(to_list(commands))
stack = list()
@ -282,7 +292,10 @@ class Nxapi:
)
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
if opts.get('ignore_timeout') and headers['status'] == -1:
result.append(headers['msg'])
return result
elif headers['status'] != 200:
self._error(**headers)
try:
@ -351,11 +364,12 @@ class Nxapi:
return responses
def load_config(self, commands, return_error=False):
def load_config(self, commands, return_error=False, opts=None):
"""Sends the ordered set of commands to the device
"""
commands = to_list(commands)
msg = self.send_request(commands, output='config', check_status=True, return_error=return_error)
msg = self.send_request(commands, output='config', check_status=True,
return_error=return_error, opts=opts)
if return_error:
return msg
else:
@ -410,6 +424,6 @@ def run_commands(module, commands, check_rc=True):
return conn.run_commands(to_command(module, commands), check_rc)
def load_config(module, config, return_error=False):
def load_config(module, config, return_error=False, opts=None):
conn = get_connection(module)
return conn.load_config(config, return_error=return_error)
return conn.load_config(config, return_error, opts)

@ -25,27 +25,29 @@ DOCUMENTATION = '''
---
module: nxos_install_os
extends_documentation_fragment: nxos
short_description: Set boot options like boot image and kickstart image.
short_description: Set boot options like boot, kickstart image and issu.
description:
- Install an operating system by setting the boot options like boot
image and kickstart image.
image and kickstart image and optionally select to install using
ISSU (In Server Software Upgrade).
notes:
- Tested against NXOSv 7.3.(0)D1(1) on VIRL
- The module will fail due to timeout issues, but the install will go on
anyway. Ansible's block and rescue can be leveraged to handle this kind
of failure and check actual module results. See EXAMPLE for more about
this. The first task on the rescue block is needed to make sure the
device has completed all checks and it started to reboot. The second
task is needed to wait for the device to come back up. The last two tasks
are used to verify the installation process was successful.
- Tested against the following platforms and images
- N9k 7.0(3)I4(6), 7.0(3)I5(3), 7.0(3)I6(1), 7.0(3)I7(1), 7.0(3)F2(2), 7.0(3)F3(2)
- N3k 6.0(2)A8(6), 6.0(2)A8(8), 7.0(3)I6(1), 7.0(3)I7(1)
- N7k 7.3(0)D1(1), 8.0(1), 8.2(1)
- This module executes longer then the default ansible timeout value and
will generate errors unless the module timeout parameter is set to a
value of 500 seconds or higher.
The example time is sufficent for most upgrades but this can be
tuned higher based on specific upgrade time requirements.
The module will exit with a failure message if the timer is
not set to 500 seconds or higher.
- Do not include full file paths, just the name of the file(s) stored on
the top level flash directory.
- You must know if your platform supports taking a kickstart image as a
parameter. If supplied but not supported, errors may occur.
- This module attempts to install the software immediately,
which may trigger a reboot.
- In check mode, the module tells you if the current boot images are set
to the desired images.
- In check mode, the module will indicate if an upgrade is needed and
whether or not the upgrade is disruptive or non-disruptive(ISSU).
author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbibo (@GGabriele)
@ -60,34 +62,45 @@ options:
- Name of the kickstart image file on flash.
required: false
default: null
issu:
version_added: "2.5"
description:
- Upgrade using In Service Software Upgrade (ISSU).
(Only supported on N9k platforms)
- Selecting 'required' or 'yes' means that upgrades will only
proceed if the switch is capable of ISSU.
- Selecting 'desired' means that upgrades will use ISSU if possible
but will fall back to disruptive upgrade if needed.
- Selecting 'no' means do not use ISSU. Forced disruptive.
required: false
choices: ['required','desired', 'yes', 'no']
default: 'no'
'''
EXAMPLES = '''
- block:
- name: Install OS
nxos_install_os:
system_image_file: nxos.7.0.3.I2.2d.bin
rescue:
- name: Wait for device to perform checks
wait_for:
port: 22
state: stopped
timeout: 300
delay: 60
- name: Wait for device to come back up
wait_for:
port: 22
state: started
timeout: 300
delay: 60
- name: Check installed OS
nxos_command:
commands:
- show version
register: output
- assert:
that:
- output['stdout'][0]['kickstart_ver_str'] == '7.0(3)I4(1)'
- name: Install OS on N9k
check_mode: no
nxos_install_os:
system_image_file: nxos.7.0.3.I6.1.bin
issu: desired
provider: "{{ connection | combine({'timeout': 500}) }}"
- name: Wait for device to come back up with new image
wait_for:
port: 22
state: started
timeout: 500
delay: 60
host: "{{ inventory_hostname }}"
- name: Check installed OS for newly installed version
nxos_command:
commands: ['show version | json']
provider: "{{ connection }}"
register: output
- assert:
that:
- output['stdout'][0]['kickstart_ver_str'] == '7.0(3)I6(1)'
'''
RETURN = '''
@ -96,125 +109,462 @@ install_state:
returned: always
type: dictionary
sample: {
"kick": "n5000-uk9-kickstart.7.2.1.N1.1.bin",
"sys": "n5000-uk9.7.2.1.N1.1.bin",
"status": "This is the log of last installation.\n
Continuing with installation process, please wait.\n
The login will be disabled until the installation is completed.\n
Performing supervisor state verification. \n
SUCCESS\n
Supervisor non-disruptive upgrade successful.\n
Install has been successful.\n",
"install_state": [
"Compatibility check is done:",
"Module bootable Impact Install-type Reason",
"------ -------- -------------- ------------ ------",
" 1 yes non-disruptive reset ",
"Images will be upgraded according to following table:",
"Module Image Running-Version(pri:alt) New-Version Upg-Required",
"------ ---------- ---------------------------------------- -------------------- ------------",
" 1 nxos 7.0(3)I6(1) 7.0(3)I7(1) yes",
" 1 bios v4.4.0(07/12/2017) v4.4.0(07/12/2017) no"
],
}
'''
import re
from ansible.module_utils.nxos import get_config, load_config, run_commands
from time import sleep
from ansible.module_utils.nxos import load_config, run_commands
from ansible.module_utils.nxos import nxos_argument_spec, check_args
from ansible.module_utils.basic import AnsibleModule
def execute_show_command(command, module):
command = {
def check_ansible_timer(module):
'''Check Ansible Timer Values'''
msg = "The 'timeout' provider param value for this module to execute\n"
msg = msg + 'properly is too low.\n'
msg = msg + 'Upgrades can take a long time so the value needs to be set\n'
msg = msg + 'to the recommended value of 500 seconds or higher in the\n'
msg = msg + 'ansible playbook for the nxos_install_os module.\n'
msg = msg + '\n'
msg = msg + 'provider: "{{ connection | combine({\'timeout\': 500}) }}"'
data = module.params.get('provider')
timer_low = False
if data.get('timeout') is None:
timer_low = True
if data.get('timeout') is not None and data.get('timeout') < 500:
timer_low = True
if timer_low:
module.fail_json(msg=msg.split('\n'))
# Output options are 'text' or 'json'
def execute_show_command(module, command, output='text'):
cmds = [{
'command': command,
'output': 'text',
}
'output': output,
}]
return run_commands(module, cmds)
def get_platform(module):
"""Determine platform type"""
data = execute_show_command(module, 'show inventory', 'json')
pid = data[0]['TABLE_inv']['ROW_inv'][0]['productid']
if re.search(r'N3K', pid):
type = 'N3K'
elif re.search(r'N5K', pid):
type = 'N5K'
elif re.search(r'N6K', pid):
type = 'N6K'
elif re.search(r'N7K', pid):
type = 'N7K'
elif re.search(r'N9K', pid):
type = 'N9K'
else:
type = 'unknown'
return run_commands(module, [command])
return type
def get_boot_options(module):
"""Get current boot variables
like system image and kickstart image.
Returns:
A dictionary, e.g. { 'kick': router_kick.img, 'sys': 'router_sys.img'}
"""
command = 'show boot'
body = execute_show_command(command, module)[0]
boot_options_raw_text = body.split('Boot Variables on next reload')[1]
def kickstart_image_required(module):
'''Determine if platform requires a kickstart image'''
data = execute_show_command(module, 'show version')[0]
kickstart_required = False
for x in data.split('\n'):
if re.search(r'kickstart image file is:', x):
kickstart_required = True
if 'kickstart' in boot_options_raw_text:
kick_regex = r'kickstart variable = bootflash:/(\S+)'
sys_regex = r'system variable = bootflash:/(\S+)'
return kickstart_required
kick = re.search(kick_regex, boot_options_raw_text).group(1)
sys = re.search(sys_regex, boot_options_raw_text).group(1)
retdict = dict(kick=kick, sys=sys)
else:
nxos_regex = r'NXOS variable = bootflash:/(\S+)'
nxos = re.search(nxos_regex, boot_options_raw_text).group(1)
retdict = dict(sys=nxos)
command = 'show install all status'
retdict['status'] = execute_show_command(command, module)[0]
def parse_show_install(data):
"""Helper method to parse the output of the 'show install all impact' or
'install all' commands.
Sample Output:
Installer will perform impact only check. Please wait.
Verifying image bootflash:/nxos.7.0.3.F2.2.bin for boot variable "nxos".
[####################] 100% -- SUCCESS
Verifying image type.
[####################] 100% -- SUCCESS
Preparing "bios" version info using image bootflash:/nxos.7.0.3.F2.2.bin.
[####################] 100% -- SUCCESS
Preparing "nxos" version info using image bootflash:/nxos.7.0.3.F2.2.bin.
[####################] 100% -- SUCCESS
Performing module support checks.
[####################] 100% -- SUCCESS
return retdict
Notifying services about system upgrade.
[####################] 100% -- SUCCESS
def already_set(current_boot_options, system_image_file, kickstart_image_file):
return current_boot_options.get('sys') == system_image_file \
and current_boot_options.get('kick') == kickstart_image_file
Compatibility check is done:
Module bootable Impact Install-type Reason
------ -------- -------------- ------------ ------
8 yes disruptive reset Incompatible image for ISSU
21 yes disruptive reset Incompatible image for ISSU
def set_boot_options(module, image_name, kickstart=None):
"""Set boot variables
like system image and kickstart image.
Args:
The main system image file name.
Keyword Args: many implementors may choose
to supply a kickstart parameter to specify a kickstart image.
Images will be upgraded according to following table:
Module Image Running-Version(pri:alt) New-Version Upg-Required
------ ---------- ---------------------------------------- ------------
8 lcn9k 7.0(3)F3(2) 7.0(3)F2(2) yes
8 bios v01.17 v01.17 no
21 lcn9k 7.0(3)F3(2) 7.0(3)F2(2) yes
21 bios v01.70 v01.70 no
"""
if len(data) > 0:
data = massage_install_data(data)
ud = {'raw': data}
ud['list_data'] = data.split('\n')
ud['processed'] = []
ud['disruptive'] = False
ud['upgrade_needed'] = False
ud['error'] = False
ud['install_in_progress'] = False
ud['backend_processing_error'] = False
ud['upgrade_succeeded'] = False
ud['use_impact_data'] = False
for x in ud['list_data']:
# Check for errors and exit if found.
if re.search(r'Pre-upgrade check failed', x):
ud['error'] = True
break
if re.search(r'[I|i]nvalid command', x):
ud['error'] = True
break
if re.search(r'No install all data found', x):
ud['error'] = True
break
# Check for potentially transient conditions
if re.search(r'Another install procedure may be in progress', x):
ud['install_in_progress'] = True
break
if re.search(r'Backend processing error', x):
ud['backend_processing_error'] = True
break
# Check for messages indicating a successful upgrade.
if re.search(r'Finishing the upgrade', x):
ud['upgrade_succeeded'] = True
break
if re.search(r'Install has been successful', x):
ud['upgrade_succeeded'] = True
break
# We get these messages when the upgrade is non-disruptive and
# we loose connection with the switchover but far enough along that
# we can be confident the upgrade succeeded.
if re.search(r'timeout trying to send command: install', x):
ud['upgrade_succeeded'] = True
ud['use_impact_data'] = True
break
if re.search(r'[C|c]onnection failure: timed out', x):
ud['upgrade_succeeded'] = True
ud['use_impact_data'] = True
break
# Begin normal parsing.
if re.search(r'----|Module|Images will|Compatibility', x):
ud['processed'].append(x)
continue
# Check to see if upgrade will be disruptive or non-disruptive and
# build dictionary of individual modules and their status.
# Sample Line:
#
# Module bootable Impact Install-type Reason
# ------ -------- ---------- ------------ ------
# 8 yes disruptive reset Incompatible image
rd = r'(\d+)\s+(\S+)\s+(disruptive|non-disruptive)\s+(\S+)'
mo = re.search(rd, x)
if mo:
ud['processed'].append(x)
key = 'm%s' % mo.group(1)
field = 'disruptive'
if mo.group(3) == 'non-disruptive':
ud[key] = {field: False}
else:
ud[field] = True
ud[key] = {field: True}
field = 'bootable'
if mo.group(2) == 'yes':
ud[key].update({field: True})
else:
ud[key].update({field: False})
continue
# Check to see if switch needs an upgrade and build a dictionary
# of individual modules and their individual upgrade status.
# Sample Line:
#
# Module Image Running-Version(pri:alt) New-Version Upg-Required
# ------ ----- ---------------------------------------- ------------
# 8 lcn9k 7.0(3)F3(2) 7.0(3)F2(2) yes
mo = re.search(r'(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(yes|no)', x)
if mo:
ud['processed'].append(x)
key = 'm%s_%s' % (mo.group(1), mo.group(2))
field = 'upgrade_needed'
if mo.group(5) == 'yes':
ud[field] = True
ud[key] = {field: True}
else:
ud[key] = {field: False}
continue
return ud
def massage_install_data(data):
# Transport cli returns a list containing one result item.
# Transport nxapi returns a list containing two items. The second item
# contains the data we are interested in.
default_error_msg = 'No install all data found'
if len(data) == 1:
result_data = data[0]
elif len(data) == 2:
result_data = data[1]
else:
result_data = default_error_msg
# Further processing may be needed for result_data
if len(data) == 2 and isinstance(data[1], dict):
if 'clierror' in data[1].keys():
result_data = data[1]['clierror']
elif 'code' in data[1].keys() and data[1]['code'] == '500':
# We encountered a backend processing error for nxapi
result_data = data[1]['msg']
else:
result_data = default_error_msg
return result_data
def build_install_cmd_set(issu, image, kick, type):
commands = ['terminal dont-ask']
if kickstart is None:
commands.append('install all nxos %s' % image_name)
if re.search(r'required|desired|yes', issu):
issu_cmd = 'non-disruptive'
else:
issu_cmd = ''
if type == 'impact':
rootcmd = 'show install all impact'
else:
rootcmd = 'install all'
if kick is None:
commands.append(
'%s nxos %s %s' % (rootcmd, image, issu_cmd))
else:
commands.append(
'install all system %s kickstart %s' % (image_name, kickstart))
load_config(module, commands)
'%s system %s kickstart %s' % (rootcmd, image, kick))
return commands
def parse_show_version(data):
version_data = {'raw': data[0].split('\n')}
version_data['version'] = ''
version_data['error'] = False
for x in version_data['raw']:
mo = re.search(r'(kickstart|system|NXOS):\s+version\s+(\S+)', x)
if mo:
version_data['version'] = mo.group(2)
continue
if version_data['version'] == '':
version_data['error'] = True
return version_data
def check_mode_legacy(module, issu, image, kick=None):
"""Some platforms/images/transports don't support the 'install all impact'
command so we need to use a different method."""
current = execute_show_command(module, 'show version', 'json')[0]
# Call parse_show_data on empty string to create the default upgrade
# data stucture dictionary
data = parse_show_install('')
upgrade_msg = 'No upgrade required'
# Process System Image
data['error'] = False
tsver = 'show version image bootflash:%s' % image
target_image = parse_show_version(execute_show_command(module, tsver))
if target_image['error']:
data['error'] = True
data['raw'] = target_image['raw']
if current['kickstart_ver_str'] != target_image['version'] and not data['error']:
data['upgrade_needed'] = True
data['disruptive'] = True
upgrade_msg = 'Switch upgraded: system: %s' % tsver
# Process Kickstart Image
if kick is not None and not data['error']:
tkver = 'show version image bootflash:%s' % kick
target_kick = parse_show_version(execute_show_command(module, tkver))
if target_kick['error']:
data['error'] = True
data['raw'] = target_kick['raw']
if current['kickstart_ver_str'] != target_kick['version'] and not data['error']:
data['upgrade_needed'] = True
data['disruptive'] = True
upgrade_msg = upgrade_msg + ' kickstart: %s' % tkver
data['processed'] = upgrade_msg
return data
def check_mode_nextgen(module, issu, image, kick=None):
"""Use the 'install all impact' command for check_mode"""
opts = {'ignore_timeout': True}
commands = build_install_cmd_set(issu, image, kick, 'impact')
data = parse_show_install(load_config(module, commands, True, opts))
# If an error is encountered when issu is 'desired' then try again
# but set issu to 'no'
if data['error'] and issu == 'desired':
issu = 'no'
commands = build_install_cmd_set(issu, image, kick, 'impact')
# The system may be busy from the previous call to check_mode so loop
# until it's done.
data = check_install_in_progress(module, commands, opts)
if re.search(r'No install all data found', data['raw']):
data['error'] = True
return data
def check_install_in_progress(module, commands, opts):
for attempt in range(20):
data = parse_show_install(load_config(module, commands, True, opts))
if data['install_in_progress']:
sleep(1)
continue
break
return data
def check_mode(module, issu, image, kick=None):
"""Check switch upgrade impact using 'show install all impact' command"""
data = check_mode_nextgen(module, issu, image, kick)
if data['backend_processing_error']:
# We encountered an unrecoverable error in the attempt to get upgrade
# impact data from the 'show install all impact' command.
# Fallback to legacy method.
data = check_mode_legacy(module, issu, image, kick)
return data
def do_install_all(module, issu, image, kick=None):
"""Perform the switch upgrade using the 'install all' command"""
impact_data = check_mode(module, issu, image, kick)
if module.check_mode:
# Check mode set in the playbook so just return the impact data.
msg = '*** SWITCH WAS NOT UPGRADED: IMPACT DATA ONLY ***'
impact_data['processed'].append(msg)
return impact_data
if impact_data['error']:
# Check mode discovered an error so return with this info.
return impact_data
elif not impact_data['upgrade_needed']:
# The switch is already upgraded. Nothing more to do.
return impact_data
else:
# If we get here, check_mode returned no errors and the switch
# needs to be upgraded.
if impact_data['disruptive']:
# Check mode indicated that ISSU is not possible so issue the
# upgrade command without the non-disruptive flag.
issu = 'no'
commands = build_install_cmd_set(issu, image, kick, 'install')
opts = {'ignore_timeout': True}
# The system may be busy from the call to check_mode so loop until
# it's done.
upgrade = check_install_in_progress(module, commands, opts)
# Special case: If we encounter a backend processing error at this
# stage it means the command was sent and the upgrade was started but
# we will need to use the impact data instead of the current install
# data.
if upgrade['backend_processing_error']:
upgrade['upgrade_succeeded'] = True
upgrade['use_impact_data'] = True
if upgrade['use_impact_data']:
if upgrade['upgrade_succeeded']:
upgrade = impact_data
upgrade['upgrade_succeeded'] = True
else:
upgrade = impact_data
upgrade['upgrade_succeeded'] = False
if not upgrade['upgrade_succeeded']:
upgrade['error'] = True
return upgrade
def main():
argument_spec = dict(
system_image_file=dict(required=True),
kickstart_image_file=dict(required=False),
issu=dict(choices=['required', 'desired', 'no', 'yes'], default='no'),
)
argument_spec.update(nxos_argument_spec)
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
supports_check_mode=True)
warnings = list()
check_args(module, warnings)
system_image_file = module.params['system_image_file']
kickstart_image_file = module.params['kickstart_image_file']
if kickstart_image_file == 'null':
kickstart_image_file = None
current_boot_options = get_boot_options(module)
changed = False
if not already_set(current_boot_options,
system_image_file,
kickstart_image_file):
changed = True
install_state = current_boot_options
if not module.check_mode and changed is True:
set_boot_options(module,
system_image_file,
kickstart=kickstart_image_file)
if not already_set(install_state,
system_image_file,
kickstart_image_file):
module.fail_json(msg='Install not successful',
install_state=install_state)
module.exit_json(changed=changed, install_state=install_state, warnings=warnings)
# This module will error out if the Ansible task timeout value is not
# tuned high enough.
check_ansible_timer(module)
# Get system_image_file(sif), kickstart_image_file(kif) and
# issu settings from module params.
sif = module.params['system_image_file']
kif = module.params['kickstart_image_file']
issu = module.params['issu']
if kif == 'null' or kif == '':
kif = None
if kickstart_image_required(module) and kif is None:
msg = 'This platform requires a kickstart_image_file'
module.fail_json(msg=msg)
install_result = do_install_all(module, issu, sif, kick=kif)
if install_result['error']:
msg = "Failed to upgrade device using image "
if kif:
msg = msg + "files: kickstart: %s, system: %s" % (kif, sif)
else:
msg = msg + "file: system: %s" % sif
module.fail_json(msg=msg, raw_data=install_result['list_data'])
state = install_result['processed']
changed = install_result['upgrade_needed']
module.exit_json(changed=changed, install_state=state, warnings=warnings)
if __name__ == '__main__':

@ -205,7 +205,6 @@ lib/ansible/modules/network/nxos/nxos_gir_profile_management.py
lib/ansible/modules/network/nxos/nxos_igmp.py
lib/ansible/modules/network/nxos/nxos_igmp_interface.py
lib/ansible/modules/network/nxos/nxos_igmp_snooping.py
lib/ansible/modules/network/nxos/nxos_install_os.py
lib/ansible/modules/network/nxos/nxos_ntp_auth.py
lib/ansible/modules/network/nxos/nxos_ntp_options.py
lib/ansible/modules/network/nxos/nxos_nxapi.py

Loading…
Cancel
Save