Support more arguments for node compose command
Add several new arguments, "--description", "--processor", "--memory", "--remote-drives", "--local-drives", "--ethernet" to allow user to specify node composition requirements in details. Change-Id: I86aa04e3e84d449dc184de45a0cfab46c48ba7f2
This commit is contained in:
@@ -3,5 +3,6 @@
|
|||||||
# process, which may cause wedges in the gate later.
|
# process, which may cause wedges in the gate later.
|
||||||
|
|
||||||
pbr>=2.0 # Apache-2.0
|
pbr>=2.0 # Apache-2.0
|
||||||
|
cliff>=2.8.0 # Apache-2.0
|
||||||
osc-lib>=1.7.0 # Apache-2.0
|
osc-lib>=1.7.0 # Apache-2.0
|
||||||
rsd-lib>=0.0.1 # Apache-2.0
|
rsd-lib>=0.0.1 # Apache-2.0
|
||||||
|
|||||||
57
rsdclient/common/command.py
Normal file
57
rsdclient/common/command.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Copyright 2017 Intel, Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
from cliff import _argparse
|
||||||
|
|
||||||
|
from osc_lib.command import command
|
||||||
|
|
||||||
|
|
||||||
|
class Command(command.Command):
|
||||||
|
|
||||||
|
def get_parser(self, prog_name):
|
||||||
|
parser = super(Command, self).get_parser(prog_name)
|
||||||
|
parser.formatter_class = _SmartHelpFormatter
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
class _SmartHelpFormatter(_argparse.HelpFormatter):
|
||||||
|
"""New smart argparse HelpFormatter
|
||||||
|
|
||||||
|
Smart help formatter to output raw help message if it contains newline
|
||||||
|
and heading whitespaces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _split_lines(self, text, width):
|
||||||
|
lines = text.splitlines() if '\n' in text else [text]
|
||||||
|
wrap_lines = []
|
||||||
|
for each_line in lines:
|
||||||
|
if each_line == '':
|
||||||
|
# Handle newline case
|
||||||
|
wrap_lines.append('')
|
||||||
|
elif each_line.startswith(' '):
|
||||||
|
# Handle heading whitespaces case
|
||||||
|
spaces_width = len(each_line) - len(each_line.lstrip())
|
||||||
|
lines = super(_SmartHelpFormatter, self)._split_lines(
|
||||||
|
each_line, width - spaces_width)
|
||||||
|
wrap_lines.extend([' ' * spaces_width + line
|
||||||
|
for line in lines])
|
||||||
|
else:
|
||||||
|
# Handle normal case
|
||||||
|
wrap_lines.extend(
|
||||||
|
super(_SmartHelpFormatter, self)._split_lines(
|
||||||
|
each_line, width)
|
||||||
|
)
|
||||||
|
return wrap_lines
|
||||||
@@ -15,7 +15,18 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from osc_lib.command import command
|
from rsdclient.common import command
|
||||||
|
|
||||||
|
|
||||||
|
ARGUMENTS_NAME_MAPPING = {
|
||||||
|
'name': 'Name',
|
||||||
|
'description': 'Description',
|
||||||
|
'processor': 'Processors',
|
||||||
|
'memory': 'Memory',
|
||||||
|
'remote_drives': 'RemoteDrives',
|
||||||
|
'local_drives': 'LocalDrives',
|
||||||
|
'ethernet': 'EthernetInterfaces'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ComposeNode(command.Command):
|
class ComposeNode(command.Command):
|
||||||
@@ -26,18 +37,200 @@ class ComposeNode(command.Command):
|
|||||||
# NOTE: All arguments are positional and, if not provided
|
# NOTE: All arguments are positional and, if not provided
|
||||||
# with a default, required.
|
# with a default, required.
|
||||||
parser.add_argument('--name',
|
parser.add_argument('--name',
|
||||||
dest='name',
|
|
||||||
required=True,
|
|
||||||
metavar='<name>',
|
metavar='<name>',
|
||||||
help='Name of the composed node.')
|
help='Name of the composed node.')
|
||||||
|
parser.add_argument('--description',
|
||||||
|
metavar='<description>',
|
||||||
|
help='Description of the composed node.')
|
||||||
|
parser.add_argument(
|
||||||
|
'--processor',
|
||||||
|
dest='processor',
|
||||||
|
type=json.loads,
|
||||||
|
metavar='<processor requirements>',
|
||||||
|
help=('Array of requirements for processor for composed node. Each'
|
||||||
|
' processor requirement may contain one or more optional '
|
||||||
|
'attributes:\n'
|
||||||
|
' - Model: Type String, Processor model that should be used'
|
||||||
|
' for composed node (exact model name)\n\n'
|
||||||
|
' - TotalCores: Type Int, Minimum number of processor cores'
|
||||||
|
' - AchievableSpeedMHz: Type Int, Minimum achievable '
|
||||||
|
'processor operating frequency.\n\n'
|
||||||
|
' - InstructionSet: Type String, Processor supported '
|
||||||
|
'instruction set, such as "x86", "x86-64", "IA-64", '
|
||||||
|
'"ARM-A32", "ARM-A64", "MIPS32", "MIPS64", "OEM"\n\n'
|
||||||
|
' - Resource: Object Reference to a particular processor '
|
||||||
|
'that should be used in composed node\n\n'
|
||||||
|
' - Chassis: Object Link to chassis object within this '
|
||||||
|
'processor should be contained.\n\n'
|
||||||
|
' - Brand: Type String, Brand of CPU that should be used to'
|
||||||
|
' allocate node.'
|
||||||
|
' - Capabilities: Array of strings describing processor '
|
||||||
|
'capabilities (like reported in /proc/cpuinfo flags), such '
|
||||||
|
'as "sse", "avx", etc.\n\n'
|
||||||
|
'For example:\n'
|
||||||
|
'[{\n'
|
||||||
|
' "Model": "Multi-Core Intel(R) Xeon(R) processor 7xxx '
|
||||||
|
'Series",\n'
|
||||||
|
' "TotalCores": 2,\n'
|
||||||
|
' "AchievableSpeedMHz": 2000,\n'
|
||||||
|
' "InstructionSet": "x86",\n'
|
||||||
|
' "Oem": {\n'
|
||||||
|
' "Brand": "E5",\n'
|
||||||
|
' "Capabilities": [ "sse" ],\n'
|
||||||
|
' },\n'
|
||||||
|
' "Resource": {\n'
|
||||||
|
' "@odata.id": "/redfish/v1/Systems/System1/Processors'
|
||||||
|
'/CPU1"\n'
|
||||||
|
' }\n'
|
||||||
|
'}]'))
|
||||||
|
parser.add_argument(
|
||||||
|
'--memory',
|
||||||
|
dest='memory',
|
||||||
|
type=json.loads,
|
||||||
|
metavar='<memory requirements>',
|
||||||
|
help=('Array of requirements for memory for composed node. Each '
|
||||||
|
'memory requirement may contain one or more optional '
|
||||||
|
'attributes:\n'
|
||||||
|
' - CapacityMiB: Type Int, Minimum memory capacity '
|
||||||
|
'requested for composed node\n\n'
|
||||||
|
' - MemoryDeviceType: Type String, Type details of DIMM, '
|
||||||
|
'such as "DDR3", "DDR4"\n\n'
|
||||||
|
' - SpeedMHz: Type Int, Minimum supported memory speed\n\n'
|
||||||
|
' - Manufacturer: Type String, Requested memory '
|
||||||
|
'manufacturer\n\n'
|
||||||
|
' - DataWidthBits: Type Int, Requested memory data width in'
|
||||||
|
' bits\n\n'
|
||||||
|
' - Resource: Object Reference to a particular memory '
|
||||||
|
'module that should be used in composed node\n\n'
|
||||||
|
' - Chassis: Object Link to chassis object within this '
|
||||||
|
'memory DIMM should be contained\n\n'
|
||||||
|
'For example:\n'
|
||||||
|
'[{\n'
|
||||||
|
' "CapacityMiB": 16000,\n'
|
||||||
|
' "MemoryDeviceType": "DDR3",\n'
|
||||||
|
' "SpeedMHz": 1600,\n'
|
||||||
|
' "Manufacturer": "Intel",\n'
|
||||||
|
' "DataWidthBits": 64,\n'
|
||||||
|
' "Resource": {\n'
|
||||||
|
' "@odata.id": "/redfish/v1/Systems/System1/Memory/'
|
||||||
|
'Dimm1"\n'
|
||||||
|
' },\n'
|
||||||
|
' "Chassis": {\n'
|
||||||
|
' "@odata.id": "/redfish/v1/Chassis/Rack1"\n'
|
||||||
|
' }\n'
|
||||||
|
'}]'))
|
||||||
|
parser.add_argument(
|
||||||
|
'--remote-drives',
|
||||||
|
dest='remote_drives',
|
||||||
|
type=json.loads,
|
||||||
|
metavar='<remote drives requirements>',
|
||||||
|
help=('Array of requirements for remote drives that should be '
|
||||||
|
'created/connected to composed node. Each remote drives '
|
||||||
|
'requirement may contain one or more optional attributes:\n'
|
||||||
|
' - CapacityGiB: Type Int, Minimum drive capacity requested'
|
||||||
|
' for composed node\n\n'
|
||||||
|
' - iSCSIAddress: Type String, Defines TargetIQN of '
|
||||||
|
'RemoteTarget to be set for new Remote Target (should be '
|
||||||
|
'unique in PODM)\n\n'
|
||||||
|
' - Master: Object Defines master logical volume that '
|
||||||
|
'should be taken to create new remote target. It contains '
|
||||||
|
'following two properties: Type and Resource\n\n'
|
||||||
|
' - Type: Type String, Type of replication of master drive:'
|
||||||
|
' Clone - volume should be cloned, Snapshot - Copy on Write '
|
||||||
|
'should be created from indicated volume\n\n'
|
||||||
|
' - Resource: Object Reference to logical volume that '
|
||||||
|
'should be used as master for replication\n\n'
|
||||||
|
'For example:\n'
|
||||||
|
'[{\n'
|
||||||
|
' "CapacityGiB": 80,\n'
|
||||||
|
' "iSCSIAddress": "iqn.oem.com:fedora21",\n'
|
||||||
|
' "Master": {\n'
|
||||||
|
' "Type": "Snapshot",\n'
|
||||||
|
' "Resource": {\n'
|
||||||
|
' "@odata.id": "/redfish/v1/Services/RSS1/LogicalDrives'
|
||||||
|
'/sda1"\n'
|
||||||
|
' }\n'
|
||||||
|
' }\n'
|
||||||
|
'}]'))
|
||||||
|
parser.add_argument(
|
||||||
|
'--local-drives',
|
||||||
|
dest='local_drives',
|
||||||
|
type=json.loads,
|
||||||
|
metavar='<local drives requirements>',
|
||||||
|
help=('Array of requirements for local drives for composed node. '
|
||||||
|
'Each local drives requirement may contain one or more '
|
||||||
|
'optional attributes:\n'
|
||||||
|
' - CapacityGiB: Type Int, Minimum drive capacity requested'
|
||||||
|
' for composed node\n\n'
|
||||||
|
' - Type: Type String, Drive type: "HDD", "SSD"\n\n'
|
||||||
|
' - MinRPM: Type Int, Minimum rotation speed of requested '
|
||||||
|
'drive\n\n'
|
||||||
|
' - SerialNumber: Type String, Serial number of requested '
|
||||||
|
'drive\n\n'
|
||||||
|
' - Interface: Type String, Interface of requested drive: '
|
||||||
|
'"SAS", "SATA", "NVMe"\n\n'
|
||||||
|
' - Resource: Object Reference to particular local drive '
|
||||||
|
'that should be used in composed node\n\n'
|
||||||
|
' - Chassis: Object Link to chassis object within this '
|
||||||
|
'drive should be contained\n\n'
|
||||||
|
' - FabricSwitch: Type Boolean, Determine if local drive '
|
||||||
|
'should be connected using fabric switch or local '
|
||||||
|
'connected\n\n'
|
||||||
|
'For example:\n'
|
||||||
|
'[{\n'
|
||||||
|
' "CapacityGiB": 500,\n'
|
||||||
|
' "Type": "HDD",\n'
|
||||||
|
' "MinRPM": 5400,\n'
|
||||||
|
' "SerialNumber": "12345678",\n'
|
||||||
|
' "Interface": "SATA",\n'
|
||||||
|
' "Resource": {\n'
|
||||||
|
' "@odata.id": "redfish/v1/Chassis/Blade1/Drives/Disk1"\n'
|
||||||
|
' },\n'
|
||||||
|
' "FabricSwitch": false\n'
|
||||||
|
'}]'))
|
||||||
|
parser.add_argument(
|
||||||
|
'--ethernet',
|
||||||
|
dest='ethernet',
|
||||||
|
type=json.loads,
|
||||||
|
metavar='<ethernet requirements>',
|
||||||
|
help=('Array of requirements for Ethernet interfaces of composed '
|
||||||
|
'node. Each Ethernet interface requirement may contain one '
|
||||||
|
'or more optional attributes:\n'
|
||||||
|
' - SpeedMbps: Type Int, Minimum speed of Ethernet '
|
||||||
|
'interface requested for composed node\n\n'
|
||||||
|
' - VLANs: Type Array, Array of VLANs that should be '
|
||||||
|
'configured on connected switch port for composed node for '
|
||||||
|
'given Ethernet interface in the following format: VLANId - '
|
||||||
|
'number indicating VLAN Id, Tagged - Boolean value '
|
||||||
|
'describing if given VLAN is tagged\n\n'
|
||||||
|
' - PrimaryVLAN: Type Int, Primary VLAN ID that should be '
|
||||||
|
'set for a given Ethernet Interface\n\n'
|
||||||
|
' - Resource: Object Reference to a particular Ethernet '
|
||||||
|
'interface that should be used in composed node\n\n'
|
||||||
|
' - Chassis: Object Link to chassis object within this '
|
||||||
|
'network interface should be contained\n\n'
|
||||||
|
'For example:\n'
|
||||||
|
'[{\n'
|
||||||
|
' "SpeedMbps": 1000,\n'
|
||||||
|
' "PrimaryVLAN": 100,\n'
|
||||||
|
' "VLANs": [{\n'
|
||||||
|
' "VLANId": 100,\n'
|
||||||
|
' "Tagged": false\n'
|
||||||
|
' }],\n'
|
||||||
|
' "Resource": {\n'
|
||||||
|
' "@odata.id": "/redfish/v1/Systems/System1/'
|
||||||
|
'EthernetInterfaces/LAN1"\n'
|
||||||
|
' }\n'
|
||||||
|
'}]'))
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def take_action(self, parsed_args):
|
def take_action(self, parsed_args):
|
||||||
self.log.debug("take_action(%s)", parsed_args)
|
self.log.debug("take_action(%s)", parsed_args)
|
||||||
rsd_client = self.app.client_manager.rsd
|
rsd_client = self.app.client_manager.rsd
|
||||||
args = {
|
args = {}
|
||||||
'Name': parsed_args.name
|
for i in ARGUMENTS_NAME_MAPPING:
|
||||||
}
|
if getattr(parsed_args, i):
|
||||||
|
args[ARGUMENTS_NAME_MAPPING[i]] = getattr(parsed_args, i)
|
||||||
node_id = rsd_client.node.compose(args)
|
node_id = rsd_client.node.compose(args)
|
||||||
print("Node {0} has been composed.".format(node_id))
|
print("Node {0} has been composed.".format(node_id))
|
||||||
|
|
||||||
|
|||||||
35
rsdclient/tests/common/test_command.py
Normal file
35
rsdclient/tests/common/test_command.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Copyright 2017 Intel, Inc.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import testtools
|
||||||
|
|
||||||
|
from rsdclient.common import command
|
||||||
|
|
||||||
|
|
||||||
|
class CommandTest(testtools.TestCase):
|
||||||
|
|
||||||
|
def test__split_lines(self):
|
||||||
|
formatter = command._SmartHelpFormatter(mock.Mock())
|
||||||
|
|
||||||
|
# Test format message with newline
|
||||||
|
result = formatter._split_lines("a\n\nb\nc", 1)
|
||||||
|
expected = ["a", "", "b", "c"]
|
||||||
|
self.assertEqual(result, expected)
|
||||||
|
|
||||||
|
# Test format message with heading whitespaces
|
||||||
|
result = formatter._split_lines(" ab", 3)
|
||||||
|
expected = [" a", " b"]
|
||||||
|
self.assertEqual(result, expected)
|
||||||
Reference in New Issue
Block a user