zhmcclient/zhmccli

View on GitHub
zhmccli/_cmd_storagegroup.py

Summary

Maintainability
D
1 day
Test Coverage
# Copyright 2020 IBM Corp. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Commands for storage groups on CPCs in DPM mode.
"""

from __future__ import absolute_import
from __future__ import print_function

import click

import zhmcclient
from .zhmccli import cli
from ._cmd_cpc import find_cpc
from ._cmd_port import find_port
from ._helper import print_properties, print_resources, abort_if_false, \
    options_to_properties, original_options, COMMAND_OPTIONS_METAVAR, \
    click_exception, add_options, LIST_OPTIONS, EMAIL_OPTIONS, \
    ASYNC_TIMEOUT_OPTIONS


ALL_TYPES = ['fcp', 'fc']
ALL_PARTITION_STATUSES = [
    "communications-not-active",
    "status-check",
    "stopped",
    "terminated",
    "starting",
    "active",
    "stopping",
    "degraded",
    "reservation-error",
    "paused",
]

# Defaults for storage group creation unless created from storage template
DEFAULT_TYPE = 'fcp'
DEFAULT_CONNECTIVITY = 2
DEFAULT_SHARED = True
DEFAULT_MAX_PARTITIONS = 2
DEFAULT_DIRECT_CONNECTION_COUNT = 0


def find_storagegroup(cmd_ctx, client, stogrp_name):
    """
    Find a storage group by name and return its resource object.
    """
    console = client.consoles.console
    try:
        stogrp = console.storage_groups.find(name=stogrp_name)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)
    return stogrp


@cli.group('storagegroup', options_metavar=COMMAND_OPTIONS_METAVAR)
def storagegroup_group():
    """
    Command group for managing storage groups (DPM mode only).

    Storage groups are definitions in the HMC that simplify the management of
    storage attached to partitions.

    The commands in this group work only on z14 and later CPCs that are in DPM
    mode.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """


@storagegroup_group.command('list', options_metavar=COMMAND_OPTIONS_METAVAR)
@add_options(LIST_OPTIONS)
@click.pass_obj
def storagegroup_list(cmd_ctx, **options):
    """
    List the storage groups defined in the HMC.

    Storage groups for which the authenticated user does not have
    object-access permission will not be included.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_storagegroup_list(cmd_ctx, options))


@storagegroup_group.command('show', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('STORAGEGROUP', type=str, metavar='STORAGEGROUP')
@click.pass_obj
def storagegroup_show(cmd_ctx, storagegroup):
    """
    Show the details of a storage group.

    The following properties are shown in addition to those returned by the HMC:

    \b
      - 'parent-name' - Name of the parent Console.
      - 'cpc-name' - Name of the CPC referenced by 'cpc-uri', if present.
      - 'storage-volume-names' - Names of the Storage Volumes referenced by
        'storage-volume-uris' (index-correlated).
      - 'virtual-storage-resource-names' - Names of the Virtual Storage
        Resources referenced by 'virtual-storage-resource-uris', if present
        (index-correlated).
      - 'candidate-adapter-port-names' - Names of the candidate Storage Adapters
        referenced by 'candidate-adapter-port-uris', if present
        (index-correlated).

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_storagegroup_show(cmd_ctx, storagegroup))


@storagegroup_group.command('create', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.option('--name', type=str, required=True,
              help='The name of the new storage group.')
@click.option('--cpc', type=str, required=True,
              help='The name of the CPC associated with the new storage group.')
@click.option('--type', type=click.Choice(ALL_TYPES),
              required=False, default=DEFAULT_TYPE,
              help='The type of the new storage group. '
              'Mutually exclusive with --template; one of them is required.')
@click.option('--template', type=str, required=False,
              help='The name of the storage template on which the new storage '
              'group is to be based. '
              'Mutually exclusive with --type; one of them is required.')
@click.option('--description', type=str, required=False,
              help='The description of the new storage group. '
              'Default: Empty, or from template')
@click.option('--shared', type=bool, required=False,
              help='Indicates whether the storage group can be attached to '
              'more than one partition. '
              'Default: {d}, or from template'.
              format(d=DEFAULT_SHARED))
@click.option('--connectivity', type=int, required=False,
              help='The number of adapters to utilize for the new storage '
              'group. '
              'Default: {d}, or from template'.
              format(d=DEFAULT_CONNECTIVITY))
@click.option('--max-partitions', type=int, required=False,
              help='The maximum number of partitions to which the new storage '
              'group can be attached. '
              'Default: {d}, or from template'.
              format(d=DEFAULT_MAX_PARTITIONS))
@click.option('--direct-connection-count', type=int, required=False,
              help='The number of additional virtual storage resource '
              'connections for the host that can be directly assigned to a '
              'guest virtual machine. A value of 0 indicates this feature is '
              'disabled. '
              'Default: {d}, or from template'.
              format(d=DEFAULT_DIRECT_CONNECTION_COUNT))
@add_options(EMAIL_OPTIONS)
@click.pass_obj
def storagegroup_create(cmd_ctx, **options):
    """
    Create a storage group.

    When created using --type, the new storage group will have no storage
    volumes. Storage volumes can be created and added to the storage group
    with the 'storagevolume' command.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(lambda: cmd_storagegroup_create(cmd_ctx, options))


@storagegroup_group.command('update', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('STORAGEGROUP', type=str, metavar='STORAGEGROUP')
@click.option('--name', type=str, required=False,
              help='The new name of the storage group.')
@click.option('--description', type=str, required=False,
              help='The new description of the storage group.')
@click.option('--shared', type=bool, required=False,
              help='Indicates whether the storage group can be attached to '
              'more than one partition.')
@click.option('--connectivity', type=int, required=False,
              help='The number of adapters to utilize for the new storage '
              'group.')
@click.option('--max-partitions', type=int, required=False,
              help='The maximum number of partitions to which the new storage '
              'group can be attached.')
@click.option('--direct-connection-count', type=int, required=False,
              help='The number of additional virtual storage resource '
              'connections for the host that can be directly assigned to a '
              'guest virtual machine. A value of 0 indicates this feature is '
              'disabled.')
@add_options(EMAIL_OPTIONS)
@click.pass_obj
def storagegroup_update(cmd_ctx, storagegroup, **options):
    """
    Update the properties of a storage group.

    Only the properties will be changed for which a corresponding option is
    specified, so the default for all options is not to change properties.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(
        lambda: cmd_storagegroup_update(cmd_ctx, storagegroup, options))


@storagegroup_group.command('delete', options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('STORAGEGROUP', type=str, metavar='STORAGEGROUP')
@click.option('-y', '--yes', is_flag=True, callback=abort_if_false,
              expose_value=False,
              help='Skip prompt to confirm deletion of the storage group.',
              prompt='Are you sure you want to delete this storage group ?')
@add_options(EMAIL_OPTIONS)
@click.pass_obj
def storagegroup_delete(cmd_ctx, storagegroup, **options):
    """
    Delete a storage group.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(
        lambda: cmd_storagegroup_delete(cmd_ctx, storagegroup, options))


@storagegroup_group.command('list-partitions',
                            options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('STORAGEGROUP', type=str, metavar='STORAGEGROUP')
@click.option('--name', type=str, required=False,
              help='Regular expression filter to limit the returned partitions '
              'to those with a matching name.')
@click.option('--status', type=str, required=False,
              help='Filter to limit the returned partitions to those with a '
              'matching status. Valid status values are: {sv}.'.
              format(sv=', '.join(ALL_PARTITION_STATUSES)))
@click.pass_obj
def storagegroup_list_partitions(cmd_ctx, storagegroup, **options):
    """
    List the partitions to which a storage group is attached.

    Partitions for which the authenticated user does not have object-access
    permission will not be included.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(
        lambda: cmd_storagegroup_list_partitions(cmd_ctx, storagegroup,
                                                 options))


@storagegroup_group.command('list-ports',
                            options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('STORAGEGROUP', type=str, metavar='STORAGEGROUP')
@click.pass_obj
def storagegroup_list_ports(cmd_ctx, storagegroup):
    """
    List the candidate adapter ports of a storage group.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(
        lambda: cmd_storagegroup_list_ports(cmd_ctx, storagegroup))


@storagegroup_group.command('add-ports',
                            options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('STORAGEGROUP', type=str, metavar='STORAGEGROUP')
@click.option('--adapter', type=str, metavar='NAME',
              required=False, multiple=True,
              help='The name of the storage adapter with the new port to be '
              'added. '
              'The --adapter and --port options can be specified multiple '
              'times and correspond to each other via their order.')
@click.option('--port', type=str, metavar='NAME',
              required=False, multiple=True,
              help='The name of the storage adapter port to be added. '
              'The --adapter and --port options can be specified multiple '
              'times and correspond to each other via their order.')
@click.pass_obj
def storagegroup_add_ports(cmd_ctx, storagegroup, **options):
    """
    Add storage adapter ports to the candidate adapter port list of a storage
    group.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(
        lambda: cmd_storagegroup_add_ports(cmd_ctx, storagegroup, options))


@storagegroup_group.command('remove-ports',
                            options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('STORAGEGROUP', type=str, metavar='STORAGEGROUP')
@click.option('--adapter', type=str, metavar='NAME',
              required=False, multiple=True,
              help='The name of the storage adapter with the new port to be '
              'added. '
              'The --adapter and --port options can be specified multiple '
              'times and correspond to each other via their order.')
@click.option('--port', type=str, metavar='NAME',
              required=False, multiple=True,
              help='The name of the storage adapter port to be added. '
              'The --adapter and --port options can be specified multiple '
              'times and correspond to each other via their order.')
@click.pass_obj
def storagegroup_remove_ports(cmd_ctx, storagegroup, **options):
    """
    Remove ports from the candidate adapter port list of a storage group.

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(
        lambda: cmd_storagegroup_remove_ports(cmd_ctx, storagegroup, options))


@storagegroup_group.command('discover-fcp',
                            options_metavar=COMMAND_OPTIONS_METAVAR)
@click.argument('STORAGEGROUP', type=str, metavar='STORAGEGROUP')
@click.option('--force-restart', type=bool, required=False, default=False,
              help='Indicates if there is an in-progress discovery operation '
              'for the specified storage group, it should be terminated and '
              'started again.')
@add_options(ASYNC_TIMEOUT_OPTIONS)
@click.pass_obj
def storagegroup_discover_fcp(cmd_ctx, storagegroup, **options):
    """
    Perform Logical Unit Number (LUN) discovery for an FCP storage group.

    This command only applies to storage groups of type "fcp".

    In addition to the command-specific options shown in this help text, the
    general options (see 'zhmc --help') can also be specified right after the
    'zhmc' command name.
    """
    cmd_ctx.execute_cmd(
        lambda: cmd_storagegroup_discover_fcp(cmd_ctx, storagegroup, options))


def cmd_storagegroup_list(cmd_ctx, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console

    try:
        stogrps = console.storage_groups.list()
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    show_list = [
        'name',
    ]
    if not options['names_only']:
        show_list.extend([
            'device-number',
            'type',
            'shared',
            'fulfillment-state',
            'cpc',  # CPC name, as additional property
            'description',
        ])
    if options['uri']:
        show_list.extend([
            'object-uri',
        ])

    cpc_additions = {}
    for sg in stogrps:
        try:
            cpc_uri = sg.prop('cpc-uri')
            cpc = client.cpcs.find(**{'object-uri': cpc_uri})
            cpc_additions[sg.uri] = cpc.name
        except zhmcclient.Error as exc:
            raise click_exception(exc, cmd_ctx.error_format)
    additions = {
        'cpc': cpc_additions,
    }

    try:
        print_resources(cmd_ctx, stogrps, cmd_ctx.output_format, show_list,
                        additions, all=options['all'])
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)


def cmd_storagegroup_show(cmd_ctx, stogrp_name):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console
    stogrp = find_storagegroup(cmd_ctx, client, stogrp_name)

    try:
        stogrp.pull_full_properties()
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    properties = dict(stogrp.properties)

    # Add artificial property 'parent-name'
    properties['parent-name'] = console.name

    # Add artificial property 'cpc-name'
    cpc_uri = stogrp.get_property('cpc-uri')
    if cpc_uri:
        # The storage group is attached to a CPC
        # We use list() because that is faster than Get CPC Properties
        cpcs = client.cpcs.list()
        for cpc in cpcs:
            if cpc.uri == cpc_uri:
                cpc_name = cpc.name
                break
        else:
            # This was an HMC-returned URI, so it must exist (and should also
            # be accessible)
            raise AssertionError(
                "HMC-returned CPC URI cannot be listed: {}".format(cpc_uri))
    else:
        # The storage group is not attached to a CPC
        cpc_name = None
    properties['cpc-name'] = cpc_name

    # Add artificial property 'storage-volume-names'
    stovol_names = []
    for stovol_uri in stogrp.properties['storage-volume-uris']:
        stovol_props = client.session.get(stovol_uri)
        stovol_names.append(stovol_props['name'])
    properties['storage-volume-names'] = stovol_names

    # Add artificial property 'virtual-storage-resource-names'
    try:
        vsr_uris = stogrp.properties['virtual-storage-resource-uris']
    except KeyError:
        pass
    else:
        # Storage group is FCP type
        vsr_names = []
        for vsr_uri in vsr_uris:
            vsr_props = client.session.get(vsr_uri)
            vsr_names.append(vsr_props['name'])
        properties['virtual-storage-resource-names'] = vsr_names

    # Add artificial property 'candidate-adapter-port-names'
    try:
        cap_uris = stogrp.properties['candidate-adapter-port-uris']
    except KeyError:
        pass
    else:
        # Storage group is FCP type
        cap_names = []
        for cap_uri in cap_uris:
            cap_props = client.session.get(cap_uri)
            cap_names.append(cap_props['name'])
        properties['candidate-adapter-port-names'] = cap_names

    print_properties(cmd_ctx, properties, cmd_ctx.output_format)


def cmd_storagegroup_create(cmd_ctx, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    console = client.consoles.console

    name_map = {
        # The following options are handled in this function:
        'cpc': None,
        'email-to-address': None,
        'email-cc-address': None,
    }
    org_options = original_options(options)
    properties = options_to_properties(org_options, name_map)

    cpc_name = org_options['cpc']  # It is required
    cpc = find_cpc(cmd_ctx, client, cpc_name)
    properties['cpc-uri'] = cpc.uri

    email_to_addresses = org_options['email-to-address']
    if email_to_addresses:
        properties['email-to-addresses'] = email_to_addresses

    email_cc_addresses = org_options['email-cc-address']
    if email_cc_addresses:
        properties['email-cc-addresses'] = email_cc_addresses

    try:
        new_stogrp = console.storage_groups.create(properties)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("New storage group '{sg}' has been created.".
               format(sg=new_stogrp.properties['name']))


def cmd_storagegroup_update(cmd_ctx, stogrp_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    stogrp = find_storagegroup(cmd_ctx, client, stogrp_name)

    name_map = {
        # The following options are handled in this function:
        'email-to-address': None,
        'email-cc-address': None,
    }
    org_options = original_options(options)
    properties = options_to_properties(org_options, name_map)

    email_to_addresses = org_options['email-to-address']
    if email_to_addresses:
        properties['email-to-addresses'] = email_to_addresses

    email_cc_addresses = org_options['email-cc-address']
    if email_cc_addresses:
        properties['email-cc-addresses'] = email_cc_addresses

    if not properties:
        cmd_ctx.spinner.stop()
        click.echo("No properties specified for updating storage group '{sg}'.".
                   format(sg=stogrp_name))
        return

    try:
        stogrp.update_properties(properties)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    if 'name' in properties and properties['name'] != stogrp_name:
        click.echo("Storage group '{sg}' has been renamed to '{sgn}' and was "
                   "updated.".
                   format(sg=stogrp_name, sgn=properties['name']))
    else:
        click.echo("Storage group '{sg}' has been updated.".
                   format(sg=stogrp_name))


def cmd_storagegroup_delete(cmd_ctx, stogrp_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    stogrp = find_storagegroup(cmd_ctx, client, stogrp_name)

    org_options = original_options(options)

    email_insert = org_options['email-insert']
    email_to_addresses = org_options['email-to-address'] or None
    email_cc_addresses = org_options['email-cc-address'] or None

    try:
        stogrp.delete(email_to_addresses=email_to_addresses,
                      email_cc_addresses=email_cc_addresses,
                      email_insert=email_insert)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("Storage group '{sg}' has been deleted.".format(sg=stogrp_name))


def cmd_storagegroup_list_partitions(cmd_ctx, stogrp_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    stogrp = find_storagegroup(cmd_ctx, client, stogrp_name)

    filter_name = options['name']
    filter_status = options['status']

    try:
        partitions = stogrp.list_attached_partitions(
            name=filter_name, status=filter_status)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    show_list = [
        'cpc',  # CPC name, as additional property
        'name',
        'type',
        'status',
    ]
    cpc_additions = {}
    for part in partitions:
        cpc = part.manager.parent
        cpc_additions[part.uri] = cpc.name
    additions = {
        'cpc': cpc_additions,
    }

    try:
        print_resources(cmd_ctx, partitions, cmd_ctx.output_format, show_list,
                        additions)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)


def cmd_storagegroup_list_ports(cmd_ctx, stogrp_name):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    stogrp = find_storagegroup(cmd_ctx, client, stogrp_name)

    try:
        ports = stogrp.list_candidate_adapter_ports()
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    show_list = [
        'cpc',  # CPC name, as additional property
        'adapter',  # Adapter name, as additional property
        'name',
        'index',
        'fabric-id',
    ]
    cpc_additions = {}
    adapter_additions = {}
    for port in ports:
        adapter = port.manager.parent
        adapter_additions[port.uri] = adapter.name
        cpc = adapter.manager.parent
        cpc_additions[port.uri] = cpc.name
    additions = {
        'cpc': cpc_additions,
        'adapter': adapter_additions,
    }

    try:
        print_resources(cmd_ctx, ports, cmd_ctx.output_format, show_list,
                        additions)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)


def cmd_storagegroup_add_ports(cmd_ctx, stogrp_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    stogrp = find_storagegroup(cmd_ctx, client, stogrp_name)
    cpc = stogrp.cpc

    adapter_names = options['adapter']  # List
    port_names = options['port']  # List
    if len(adapter_names) != len(port_names):
        raise click_exception(
            "The --adapter and --port options must be specified the same "
            "number of times, but have been specified {na} and {np} times.".
            format(na=len(adapter_names), np=len(port_names)),
            cmd_ctx.error_format)
    ports = []
    for i, adapter_name in enumerate(adapter_names):
        port_name = port_names[i]
        port = find_port(cmd_ctx, client, cpc, adapter_name, port_name)
        ports.append(port)

    if not ports:
        cmd_ctx.spinner.stop()
        click.echo("No ports specified for adding to the candidate list "
                   "of storage group '{sg}'.".format(sg=stogrp_name))
        return

    try:
        stogrp.add_candidate_adapter_ports(ports)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("The specified ports have been added to the candidate list "
               "of storage group '{sg}'.".format(sg=stogrp_name))


def cmd_storagegroup_remove_ports(cmd_ctx, stogrp_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    stogrp = find_storagegroup(cmd_ctx, client, stogrp_name)
    cpc = stogrp.cpc

    adapter_names = options['adapter']  # List
    port_names = options['port']  # List
    if len(adapter_names) != len(port_names):
        raise click_exception(
            "The --adapter and --port options must be specified the same "
            "number of times, but have been specified {na} and {np} times.".
            format(na=len(adapter_names), np=len(port_names)),
            cmd_ctx.error_format)
    ports = []
    for i, adapter_name in enumerate(adapter_names):
        port_name = port_names[i]
        port = find_port(cmd_ctx, client, cpc, adapter_name, port_name)
        ports.append(port)

    if not ports:
        cmd_ctx.spinner.stop()
        click.echo("No ports specified for removing from the candidate list "
                   "of storage group '{sg}'.".format(sg=stogrp_name))
        return

    try:
        stogrp.remove_candidate_adapter_ports(ports)
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("The specified ports have been removed from the candidate list "
               "of storage group '{sg}'.".format(sg=stogrp_name))


def cmd_storagegroup_discover_fcp(cmd_ctx, stogrp_name, options):
    # pylint: disable=missing-function-docstring

    client = zhmcclient.Client(cmd_ctx.session)
    stogrp = find_storagegroup(cmd_ctx, client, stogrp_name)

    force_restart = options['force_restart']

    try:
        stogrp.discover_fcp(
            force_restart=force_restart, wait_for_completion=True,
            operation_timeout=options['operation_timeout'])
    except zhmcclient.Error as exc:
        raise click_exception(exc, cmd_ctx.error_format)

    cmd_ctx.spinner.stop()
    click.echo("LUN discovery has been completed for FCP storage group '{sg}'.".
               format(sg=stogrp_name))