From 2edcd1a2f29d50a494ca59abc5bd66c253333324 Mon Sep 17 00:00:00 2001 From: Andreas Maier Date: Fri, 17 Nov 2023 17:17:55 +0100 Subject: [PATCH] Added support for retrievel of firmware from an FTP server Details: * Added support for retrievel of firmware from an FTP server to the 'cpc/console upgrade' commands. (issue #518) Signed-off-by: Andreas Maier --- docs/changes.rst | 5 ++- minimum-constraints.txt | 2 +- requirements.txt | 2 +- zhmccli/_cmd_console.py | 84 +++++++++++++++++++++++++++++---------- zhmccli/_cmd_cpc.py | 87 +++++++++++++++++++++++++++++++---------- zhmccli/_helper.py | 33 ++++++++++++++++ 6 files changed, 170 insertions(+), 43 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 1619fbec..15e71301 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -56,12 +56,15 @@ Released: not yet commands have been grouped to be more easily identifiable. This required adding the "click-option-group" Python package to the dependencies. -* Increased minimum zhmcclient version to 1.12.0 to pick up fixes and +* Increased minimum zhmcclient version to 1.12.1 to pick up fixes and functionality. (issue #510) * Tests: Added an environment variable TESTLOG to enable logging for end2end tests. (issue #414) +* Added support for retrievel of firmware from an FTP server to the + 'cpc/console upgrade' commands. (issue #518) + **Cleanup:** **Known issues:** diff --git a/minimum-constraints.txt b/minimum-constraints.txt index 7e28c441..d7f28271 100644 --- a/minimum-constraints.txt +++ b/minimum-constraints.txt @@ -38,7 +38,7 @@ wheel==0.38.1; python_version >= '3.7' # Direct dependencies for runtime (must be consistent with requirements.txt) -zhmcclient==1.12.0 +zhmcclient==1.12.1 click==8.0.2 click-repl==0.2 diff --git a/requirements.txt b/requirements.txt index 42341382..a52a99c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ # Direct dependencies (except pip, setuptools, wheel): # zhmcclient @ git+https://github.com/zhmcclient/python-zhmcclient.git@master -zhmcclient>=1.12.0 +zhmcclient>=1.12.1 # safety 2.2.0 depends on click>=8.0.2 click>=8.0.2 diff --git a/zhmccli/_cmd_console.py b/zhmccli/_cmd_console.py index ec3609bb..1e2649bd 100644 --- a/zhmccli/_cmd_console.py +++ b/zhmccli/_cmd_console.py @@ -24,7 +24,8 @@ import zhmcclient from .zhmccli import cli from ._helper import print_properties, print_dicts, print_list, \ - TABLE_FORMATS, hide_property, COMMAND_OPTIONS_METAVAR, click_exception + TABLE_FORMATS, hide_property, COMMAND_OPTIONS_METAVAR, click_exception, \ + get_level_str, prompt_ftp_password @cli.group('console', options_metavar=COMMAND_OPTIONS_METAVAR) @@ -146,9 +147,12 @@ def list_api_features(cmd_ctx, **options): @console_group.command('upgrade', options_metavar=COMMAND_OPTIONS_METAVAR) -@click.option('--bundle-level', '-b', type=str, required=True, +@click.option('--bundle-level', '-b', type=str, required=False, help="Name of the bundle to be installed on the HMC " - "(e.g. 'H71').") + "(e.g. 'H71'). " + "Default: When --ftp-host is specified, all code changes on " + "the FTP server will be installed. Otherwise, all locally " + "available code changes will be installed.") @click.option('--backup-location-type', type=str, required=False, default='usb', help="Type of backup location for the HMC backup that is " "performed: 'ftp': The FTP server that was used for the last " @@ -159,6 +163,27 @@ def list_api_features(cmd_ctx, **options): default=True, help="Boolean indicating to accept the previous bundle level " "before installing the new level. Default: true") +@click.option('--ftp-host', type=str, required=False, + help="The hostname for the FTP server from which the firmware " + "will be retrieved. " + "Default: When --bundle-level is specified, firmware will be " + "retrieved from the IBM support site. Otherwise, all locally " + "available code changes will be installed.") +@click.option('--ftp-protocol', type=click.Choice(["sftp", "ftp", "ftps"]), + required=False, default="sftp", + help="The protocol to connect to the FTP server, if the firmware " + "is retrieved from an FTP server. Default: sftp.") +@click.option('--ftp-user', type=str, required=False, + help="The username for the FTP server login, if the firmware " + "is retrieved from an FTP server.") +@click.option('--ftp-password', type=str, required=False, + help="The password for the FTP server login, if the firmware " + "is retrieved from an FTP server. Specifying a hyphen '-' will " + "prompt for the password.") +@click.option('--ftp-directory', type=str, required=False, + help="The path name of the directory on the FTP server with the " + "firmware files, if the firmware is retrieved from an FTP " + "server.") @click.option('--timeout', '-T', type=int, required=False, default=1200, help='Timeout (in seconds) when waiting for the HMC upgrade ' 'to be complete. Default: 1200.') @@ -175,8 +200,12 @@ def console_upgrade(cmd_ctx, **options): this HMC is accepted. Note that once firmware is accepted, it cannot be removed. * A backup of the this HMC is performed to the specified backup device. - * The new firmware identified by the bundle-level field is retrieved from - the IBM support site and installed. + * The new firmware for the specified bundle level is retrieved from the IBM + support site or from an FTP server. If no bundle level is specified, but + an FTP server, all firmware available on the FTP server is retrieved. + If no bundle level is specified and no FTP server, the already locally + available firmware is used and no additional firmware is retrieved. + * The specified firmware is installed. * The newly installed firmware is activated, which includes rebooting this HMC. @@ -354,6 +383,12 @@ def cmd_console_upgrade(cmd_ctx, options): backup_location_type = options['backup_location_type'] timeout = options['timeout'] + ftp_host = options['ftp_host'] + ftp_user = options['ftp_user'] + ftp_password = options['ftp_password'] + if ftp_host and ftp_password == '-': + ftp_password = prompt_ftp_password(cmd_ctx, ftp_host, ftp_user) + ec_mcl = console.prop('ec-mcl-description') hmc_bundle_level = ec_mcl.get('bundle-level', None) if hmc_bundle_level is None: @@ -363,30 +398,39 @@ def cmd_console_upgrade(cmd_ctx, options): "the Web Services API".format(v=hmc_version), cmd_ctx.error_format) - click.echo("Upgrading the HMC to bundle level {bl} and waiting for " - "completion (timeout: {t} s)". - format(bl=bundle_level, t=timeout)) + level_str = get_level_str(bundle_level, ftp_host) + click.echo("Upgrading the HMC to {lvl}, and waiting for completion " + "(timeout: {t} s)". + format(lvl=level_str, t=timeout)) + + kwargs = dict( + bundle_level=bundle_level, + accept_firmware=accept_firmware, + backup_location_type=backup_location_type, + wait_for_completion=True, + operation_timeout=timeout) + if ftp_host: + kwargs['ftp_host'] = ftp_host + kwargs['ftp_protocol'] = options['ftp_protocol'] + kwargs['ftp_user'] = ftp_user + kwargs['ftp_password'] = ftp_password + kwargs['ftp_directory'] = options['ftp_directory'] try: - console.single_step_install( - bundle_level=bundle_level, - accept_firmware=accept_firmware, - backup_location_type=backup_location_type, - wait_for_completion=True, - operation_timeout=timeout) + console.single_step_install(**kwargs) except zhmcclient.HTTPError as exc: if exc.http_status == 400 and exc.reason == 356: # HMC was already at that bundle level cmd_ctx.spinner.stop() - click.echo("The HMC was already at bundle level {bl} and did " - "not need to be changed". - format(bl=bundle_level)) + click.echo("The HMC was already at {lvl} and did not need to be " + "upgraded". + format(lvl=level_str)) return raise click_exception(exc, cmd_ctx.error_format) except zhmcclient.Error as exc: raise click_exception(exc, cmd_ctx.error_format) cmd_ctx.spinner.stop() - click.echo("The HMC has been upgraded to bundle level {bl} and is " - "available again. Any earlier session IDs have become invalid". - format(bl=bundle_level)) + click.echo("The HMC has been upgraded to {lvl} and is available again. " + "Any earlier session IDs have become invalid". + format(lvl=level_str)) diff --git a/zhmccli/_cmd_cpc.py b/zhmccli/_cmd_cpc.py index b63b58f5..c2111c4b 100644 --- a/zhmccli/_cmd_cpc.py +++ b/zhmccli/_cmd_cpc.py @@ -30,7 +30,8 @@ from ._helper import print_properties, print_resources, print_list, \ options_to_properties, original_options, COMMAND_OPTIONS_METAVAR, \ click_exception, add_options, LIST_OPTIONS, TABLE_FORMATS, hide_property, \ - required_option, abort_if_false, validate, print_dicts + required_option, abort_if_false, validate, print_dicts, get_level_str, \ + prompt_ftp_password POWER_SAVING_TYPES = ['high-performance', 'low-power', 'custom'] @@ -524,9 +525,37 @@ def cpc_list_api_features(cmd_ctx, cpc, **options): @cpc_group.command('upgrade', options_metavar=COMMAND_OPTIONS_METAVAR) @click.argument('CPC', type=str, metavar='CPC') -@click.option('--bundle-level', '-b', type=str, required=True, +@click.option('--bundle-level', '-b', type=str, required=False, help="Name of the bundle to be installed on the SE " - "(e.g. 'S71').") + "(e.g. 'S71'). " + "Default: When --ftp-host is specified, all code changes on " + "the FTP server will be installed. Otherwise, all locally " + "available code changes will be installed.") +@click.option('--accept-firmware', '-a', type=bool, required=False, + default=True, + help="Boolean indicating to accept the previous bundle level " + "before installing the new level. Default: true") +@click.option('--ftp-host', type=str, required=False, + help="The hostname for the FTP server from which the firmware " + "will be retrieved. " + "Default: When --bundle-level is specified, firmware will be " + "retrieved from the IBM support site. Otherwise, all locally " + "available code changes will be installed.") +@click.option('--ftp-protocol', type=click.Choice(["sftp", "ftp", "ftps"]), + required=False, default="sftp", + help="The protocol to connect to the FTP server, if the firmware " + "is retrieved from an FTP server. Default: sftp.") +@click.option('--ftp-user', type=str, required=False, + help="The username for the FTP server login, if the firmware " + "is retrieved from an FTP server.") +@click.option('--ftp-password', type=str, required=False, + help="The password for the FTP server login, if the firmware " + "is retrieved from an FTP server. Specifying a hyphen '-' will " + "prompt for the password.") +@click.option('--ftp-directory', type=str, required=False, + help="The path name of the directory on the FTP server with the " + "firmware files, if the firmware is retrieved from an FTP " + "server.") @click.option('--accept-firmware', '-a', type=bool, required=False, default=True, help="Boolean indicating to accept the previous bundle level " @@ -537,8 +566,7 @@ def cpc_list_api_features(cmd_ctx, cpc, **options): @click.pass_obj def cpc_upgrade(cmd_ctx, cpc, **options): """ - Upgrade the firmware on the Support Element (SE) of a CPC to a new bundle - level. + Upgrade the firmware on the Support Element (SE) of a CPC. This is done by performing the "CPC Single Step Install" operation which performs the following steps: @@ -548,8 +576,12 @@ def cpc_upgrade(cmd_ctx, cpc, **options): * If `accept_firmware` is True, the firmware currently installed on the SE of this CPC is accepted. Note that once firmware is accepted, it cannot be removed. - * The new firmware identified by the bundle-level field is retrieved from - the IBM support site and installed. + * The new firmware for the specified bundle level is retrieved from the IBM + support site or from an FTP server. If no bundle level is specified, but + an FTP server, all firmware available on the FTP server is retrieved. + If no bundle level is specified and no FTP server, the already locally + available firmware is used and no additional firmware is retrieved. + * The specified firmware is installed. * The newly installed firmware is activated, which includes rebooting the SE of this CPC. @@ -1084,6 +1116,12 @@ def cmd_cpc_upgrade(cmd_ctx, cpc_name, options): accept_firmware = options['accept_firmware'] timeout = options['timeout'] + ftp_host = options['ftp_host'] + ftp_user = options['ftp_user'] + ftp_password = options['ftp_password'] + if ftp_host and ftp_password == '-': + ftp_password = prompt_ftp_password(cmd_ctx, ftp_host, ftp_user) + ec_mcl = console.prop('ec-mcl-description') hmc_bundle_level = ec_mcl.get('bundle-level', None) if hmc_bundle_level is None: @@ -1093,28 +1131,37 @@ def cmd_cpc_upgrade(cmd_ctx, cpc_name, options): "the Web Services API".format(v=hmc_version), cmd_ctx.error_format) - click.echo("Upgrading the SE of CPC {c} to bundle level {bl} and waiting " - "for completion (timeout: {t} s)". - format(c=cpc_name, bl=bundle_level, t=timeout)) + level_str = get_level_str(bundle_level, ftp_host) + click.echo("Upgrading the SE of CPC {c} to {lvl}, and waiting for " + "completion (timeout: {t} s)". + format(c=cpc_name, lvl=level_str, t=timeout)) + + kwargs = dict( + bundle_level=bundle_level, + accept_firmware=accept_firmware, + wait_for_completion=True, + operation_timeout=timeout) + if ftp_host: + kwargs['ftp_host'] = ftp_host + kwargs['ftp_protocol'] = options['ftp_protocol'] + kwargs['ftp_user'] = ftp_user + kwargs['ftp_password'] = ftp_password + kwargs['ftp_directory'] = options['ftp_directory'] try: - cpc.single_step_install( - bundle_level=bundle_level, - accept_firmware=accept_firmware, - wait_for_completion=True, - operation_timeout=timeout) + cpc.single_step_install(**kwargs) except zhmcclient.HTTPError as exc: if exc.http_status == 400 and exc.reason == 356: # HMC was already at that bundle level cmd_ctx.spinner.stop() - click.echo("The SE of CPC {c} was already at bundle level {bl} " - "and did not need to be changed". - format(c=cpc_name, bl=bundle_level)) + click.echo("The SE of CPC {c} was already at {lvl} and did not " + "need to be upgraded". + format(c=cpc_name, lvl=level_str)) return raise click_exception(exc, cmd_ctx.error_format) except zhmcclient.Error as exc: raise click_exception(exc, cmd_ctx.error_format) cmd_ctx.spinner.stop() - click.echo("The SE of CPC {c} has been upgraded to bundle level {bl}". - format(c=cpc_name, bl=bundle_level)) + click.echo("The SE of CPC {c} has been upgraded to {lvl}". + format(c=cpc_name, lvl=level_str)) diff --git a/zhmccli/_helper.py b/zhmccli/_helper.py index 3b6c3ef5..61418bae 100644 --- a/zhmccli/_helper.py +++ b/zhmccli/_helper.py @@ -1652,3 +1652,36 @@ def validate(data, schema, what): elem='.'.join(str(e) for e in exc.absolute_path), valname=exc.validator, valvalue=exc.validator_value)) + + +def prompt_ftp_password(cmd_ctx, ftp_host, ftp_user): + """ + Prompts for the password to an FTP server. + """ + cmd_ctx.spinner.stop() + password = click.prompt( + "Enter password (for user {user} at FTP server {host})". + format(user=ftp_user, host=ftp_host), hide_input=True, + confirmation_prompt=False, type=str, err=True) + cmd_ctx.spinner.start() + return password + + +def get_level_str(bundle_level, ftp_host): + """ + Get a string for messages about the firmware level to be upgraded to, + including where it comes from. + """ + if bundle_level is not None: + if ftp_host is not None: + source_str = "FTP server {fs!r}".format(fs=ftp_host) + else: + source_str = "the IBM support site" + level_str = "bundle level {bl} with firmware retrieval from {src}". \ + format(bl=bundle_level, src=source_str) + elif ftp_host is not None: + level_str = "all firmware from FTP server {fs!r}". \ + format(fs=ftp_host) + else: + level_str = "all locally available firmware" + return level_str