Skip to content

Commit

Permalink
Added support for retrievel of firmware from an FTP server
Browse files Browse the repository at this point in the history
Details:

* Added support for retrievel of firmware from an FTP server to the
  'cpc/console upgrade' commands. (issue #518)

Signed-off-by: Andreas Maier <[email protected]>
  • Loading branch information
andy-maier committed Dec 7, 2023
1 parent 4fc2a3a commit 3cca72e
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 43 deletions.
5 changes: 4 additions & 1 deletion docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ 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
Expand All @@ -76,6 +76,9 @@ Released: not yet
* Fixed an error in the "zhmc lpar update" command when updating the
zAware and SSC master passwords.

* Added support for retrievel of firmware from an FTP server to the
'cpc/console upgrade' commands. (issue #518)

**Cleanup:**

**Known issues:**
Expand Down
2 changes: 1 addition & 1 deletion minimum-constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
84 changes: 64 additions & 20 deletions zhmccli/_cmd_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 "
Expand All @@ -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.')
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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))
87 changes: 67 additions & 20 deletions zhmccli/_cmd_cpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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 "
Expand All @@ -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:
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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))
33 changes: 33 additions & 0 deletions zhmccli/_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -1718,3 +1718,36 @@ def absolute_capping_value(cmd_ctx, options, option_name):
return dict(
type='processors',
value=str2float(cmd_ctx, option_name, option_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

0 comments on commit 3cca72e

Please sign in to comment.