diff --git a/.gitignore b/.gitignore index 51cbe852..28017de4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +.testrepository/ # Translations *.mo @@ -52,3 +53,4 @@ coverage.xml # Sphinx documentation docs/_build/ + diff --git a/doc/hpssa/index.rst b/doc/hpssa/index.rst new file mode 100644 index 00000000..1c13e6f6 --- /dev/null +++ b/doc/hpssa/index.rst @@ -0,0 +1,62 @@ +hpssa module +============ + +Example:: + + # cat raid_configuration.json + { + "logical_disks": [ + { + "size_gb": 100, + "raid_level": "1", + "controller": "Smart Array P822 in Slot 2", + "physical_disks": [ + "5I:1:1", + "5I:1:2" + ] + }, + { + "size_gb": 100, + "raid_level": "5", + "controller": "Smart Array P822 in Slot 2", + "physical_disks": [ + "5I:1:3", + "5I:1:4", + "6I:1:5" + ] + } + ] + } + + # python + Python 2.7.5 (default, Nov 3 2014, 14:26:24) + [GCC 4.8.3 20140911 (Red Hat 4.8.3-7)] on linux2 + Type "help", "copyright", "credits" or "license" for more information. + >>> import json + >>> raid_config = json.loads(open('raid_configuration.json', 'r').read()) + >>> from proliantutils.hpssa import manager + >>> manager.get_configuration() + {'logical_disks': []} + >>> manager.create_configuration(raid_config) + >>> manager.get_configuration() + {'logical_disks': [{'size_gb': 100, 'physical_disks': ['5I:1:3', '6I:1:5', '5I:1:4'], 'raid_level': '5', 'root_device_hint': {'wwn': '600508B1001C9F62EB256593E19BBA30'}, 'controller': 'Smart Array P822 in Slot 2', 'volume_name': '061D6735PDVTF0BRH5T0MO4682'}, {'size_gb': 100, 'physical_disks': ['5I:1:1', '5I:1:2'], 'raid_level': '1', 'root_device_hint': {'wwn': '600508B1001C59DB9584108610B04BB0'}, 'controller': 'Smart Array P822 in Slot 2', 'volume_name': '021D672FPDVTF0BRH5T0MO287A'}]} + >>> exit + Use exit() or Ctrl-D (i.e. EOF) to exit + >>> exit() + # ls /dev/sd* + /dev/sda /dev/sdb + # python + Python 2.7.5 (default, Nov 3 2014, 14:26:24) + [GCC 4.8.3 20140911 (Red Hat 4.8.3-7)] on linux2 + Type "help", "copyright", "credits" or "license" for more information. + >>> from proliantutils.hpssa import manager + >>> manager.delete_configuration() + >>> manager.get_configuration() + {'logical_disks': []} + >>> + # ls /dev/sd* + ls: cannot access /dev/sd*: No such file or directory + # + + + diff --git a/proliantutils/ilo/exception.py b/proliantutils/exception.py similarity index 67% rename from proliantutils/ilo/exception.py rename to proliantutils/exception.py index d744a9cb..779c3c27 100644 --- a/proliantutils/ilo/exception.py +++ b/proliantutils/exception.py @@ -12,10 +12,20 @@ # License for the specific language governing permissions and limitations # under the License. -"""Exception Class for iLO""" +"""Exception Class for proliantutils module.""" -class IloError(Exception): +class ProliantUtilsException(Exception): + """Parent class for all Proliantutils exceptions.""" + pass + + +class InvalidInputError(Exception): + + message = "Invalid Input: %(reason)s" + + +class IloError(ProliantUtilsException): """Base Exception. This exception is used when a problem is encountered in @@ -69,6 +79,11 @@ class IloConnectionError(IloError): def __init__(self, message): super(IloConnectionError, self).__init__(message) +# This is not merged with generic InvalidInputError because +# of backward-compatibility reasons. If we changed this, +# use-cases of excepting 'IloError' to catch 'IloInvalidInputError' +# will be broken. + class IloInvalidInputError(IloError): """Invalid Input passed. @@ -78,3 +93,27 @@ class IloInvalidInputError(IloError): """ def __init__(self, message): super(IloInvalidInputError, self).__init__(message) + + +class HPSSAException(ProliantUtilsException): + + message = "An exception occured in hpssa module" + + def __init__(self, message=None, **kwargs): + if not message: + message = self.message + + message = message % kwargs + super(HPSSAException, self).__init__(message) + + +class PhysicalDisksNotFoundError(HPSSAException): + + message = ("Not enough physical disks were found to create logical disk " + "of size %(size_gb)s GB and raid level %(raid_level)s") + + +class HPSSAOperationError(HPSSAException): + + message = ("An error was encountered while doing hpssa configuration: " + "%(reason)s.") diff --git a/proliantutils/hpssa/__init__.py b/proliantutils/hpssa/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/proliantutils/hpssa/manager.py b/proliantutils/hpssa/manager.py new file mode 100644 index 00000000..2ac60e85 --- /dev/null +++ b/proliantutils/hpssa/manager.py @@ -0,0 +1,158 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# 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 json +import os + +import jsonschema +from jsonschema import exceptions as json_schema_exc + +from proliantutils import exception +from proliantutils.hpssa import objects + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +RAID_CONFIG_SCHEMA = os.path.join(CURRENT_DIR, "raid_config_schema.json") + + +def _compare_logical_disks(ld1, ld2): + """Compares the two logical disks provided based on size.""" + return ld1['size_gb'] - ld2['size_gb'] + + +def _find_physical_disks(logical_disk, server): + # To be implemented + pass + + +def validate(raid_config): + """Validates the RAID configuration provided. + + This method validates the RAID configuration provided against + a JSON schema. + + :param raid_config: The RAID configuration to be validated. + :raises: InvalidInputError, if validation of the input fails. + """ + raid_schema_fobj = open(RAID_CONFIG_SCHEMA, 'r') + raid_config_schema = json.load(raid_schema_fobj) + try: + jsonschema.validate(raid_config, raid_config_schema) + except json_schema_exc.ValidationError as e: + raise exception.InvalidInputError(e.message) + + +def create_configuration(raid_config): + """Create a RAID configuration on this server. + + This method creates the given RAID configuration on the + server based on the input passed. + :param raid_config: The dictionary containing the requested + RAID configuration. This data structure should be as follows: + raid_config = {'logical_disks': [{'raid_level': 1, 'size_gb': 100}, + + ]} + :raises exception.InvalidInputError, if input is invalid. + """ + validate(raid_config) + + server = objects.Server() + logical_disks_sorted = sorted(raid_config['logical_disks'], + cmp=_compare_logical_disks) + + for logical_disk in logical_disks_sorted: + + if 'physical_disks' not in logical_disk: + # TODO(rameshg87): hpssa module should be capable of finding + # the suitable controller and physical disks if it not provided + # by using hints. This is a supported use-case, but not implemented + # as of now. Will be implemented soon. + # _find_physical_disks(logical_disk, server) + msg = ("Mentioning logical_disks without 'controller' and " + "'physical_disks' is not supported as of now.") + raise exception.InvalidInputError(reason=msg) + + controller_id = logical_disk['controller'] + + controller = server.get_controller_by_id(controller_id) + if not controller: + msg = ("Unable to find controller named '%s'" % controller_id) + raise exception.InvalidInputError(reason=msg) + + for physical_disk in logical_disk['physical_disks']: + disk_obj = controller.get_physical_drive_by_id(physical_disk) + if not disk_obj: + msg = ("Unable to find physical disk '%(physical_disk)s' " + "on '%(controller)s'" % + {'physical_disk': physical_disk, + 'controller': controller_id}) + raise exception.InvalidInputError(reason=msg) + + physical_drive_ids = logical_disk['physical_disks'] + + controller.create_logical_drive(logical_disk, physical_drive_ids) + server.refresh() + + +def delete_configuration(): + """Delete a RAID configuration on this server.""" + server = objects.Server() + for controller in server.controllers: + controller.delete_all_logical_drives() + + +def get_configuration(): + """Get the current RAID configuration. + + Get the RAID configuration from the server and return it + as a dictionary. + + :returns: A dictionary of the below format. + raid_config = { + 'logical_disks': [{ + 'size_gb': 100, + 'raid_level': 1, + 'physical_disks': [ + '5I:0:1', + '5I:0:2'], + 'controller': 'Smart array controller' + }, + ] + } + """ + server = objects.Server() + logical_drives = server.get_logical_drives() + raid_config = dict() + raid_config['logical_disks'] = [] + + for logical_drive in logical_drives: + logical_drive_info = {} + logical_drive_info['size_gb'] = logical_drive.size_gb + logical_drive_info['raid_level'] = logical_drive.raid_level + + array = logical_drive.parent + controller = array.parent + logical_drive_info['controller'] = controller.id + + physical_drive_ids = map(lambda x: x.id, array.physical_drives) + logical_drive_info['physical_disks'] = physical_drive_ids + + vol_name = logical_drive.get_property('Logical Drive Label') + logical_drive_info['volume_name'] = vol_name + + wwn = logical_drive.get_property('Unique Identifier') + logical_drive_info['root_device_hint'] = {'wwn': wwn} + + raid_config['logical_disks'].append(logical_drive_info) + + return raid_config diff --git a/proliantutils/hpssa/objects.py b/proliantutils/hpssa/objects.py new file mode 100644 index 00000000..4266f693 --- /dev/null +++ b/proliantutils/hpssa/objects.py @@ -0,0 +1,395 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# 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 time + +from oslo.concurrency import processutils + +from proliantutils import exception +from proliantutils.hpssa import types + + +def _get_indentation(string): + """Return the number of spaces before the current line.""" + return len(string) - len(string.lstrip(' ')) + + +def _get_key_value(string): + """Return the (key, value) as a tuple from a string.""" + # Normally all properties look like this: + # Unique Identifier: 600508B1001CE4ACF473EE9C826230FF + # Disk Name: /dev/sda + # Mount Points: None + key = '' + value = '' + try: + key, value = string.split(':') + except ValueError: + # This handles the case when the property of a logical drive + # returned is as follows. Here we cannot split by ':' because + # the disk id has colon in it. So if this is about disk, + # then strip it accordingly. + # Mirror Group 0: physicaldrive 6I:1:5 + string = string.lstrip(' ') + if string.startswith('physicaldrive'): + fields = string.split(' ') + key = fields[0] + value = fields[1] + else: + # TODO(rameshg87): Check if this ever occurs. + return None, None + + return key.lstrip(' ').rstrip(' '), value.lstrip(' ').rstrip(' ') + + +def _get_dict(lines, start_index, indentation): + """Recursive function for parsing hpssacli output.""" + + info = dict() + current_item = None + + i = start_index + while i < len(lines): + + current_line = lines[i] + current_line_indentation = _get_indentation(current_line) + + if current_line_indentation == indentation: + current_item = current_line.lstrip(' ') + info[current_item] = dict() + i = i + 1 + continue + + if i >= len(lines) - 1: + key, value = _get_key_value(current_line) + # If this is some unparsable information, then + # just skip it. + if key: + info[current_item][key] = value + return info, i + + next_line = lines[i+1] + next_line_indentation = _get_indentation(next_line) + + if current_line_indentation == next_line_indentation: + key, value = _get_key_value(current_line) + if key: + info[current_item][key] = value + i = i + 1 + elif next_line_indentation > current_line_indentation: + ret_dict, j = _get_dict(lines, i, current_line_indentation) + info[current_item].update(ret_dict) + i = j + 1 + elif next_line_indentation < current_line_indentation: + key, value = _get_key_value(current_line) + if key: + info[current_item][key] = value + return info, i + + return info, i + + +def _convert_to_dict(stdout): + """Wrapper function for parsing hpssacli command. + + This function gets the output from hpssacli command + and calls the recursive function _get_dict to return + the complete dictionary containing the RAID information. + """ + + lines = stdout.split("\n") + lines = filter(None, lines) + info_dict, j = _get_dict(lines, 0, 0) + return info_dict + + +def _hpssacli(*args): + """Wrapper function for executing hpssacli command.""" + try: + stdout, stderr = processutils.execute("hpssacli", + *args) + except (OSError, processutils.ProcessExecutionError) as e: + raise exception.HPSSAOperationError(reason=e) + + return stdout, stderr + + +class Server(object): + """Class for Server object + + This can consists of many RAID controllers - both internal + and external. + """ + + def __init__(self): + """Constructor for Server object.""" + self.last_updated = None + self.controllers = [] + self.refresh() + + def _get_all_details(self): + """Gets the current RAID configuration on the server. + + This methods gets the current RAID configuration on the server using + hpssacli command and returns the output. + + :returns: stdout after running the hpssacli command. The output looks + as follows: + + Smart Array P822 in Slot 2 + Bus Interface: PCI + Slot: 2 + Serial Number: PDVTF0BRH5T0MO + . + . + . + Array: A + Interface Type: SAS + Unused Space: 0 MB + Status: OK + + Logical Drive: 1 + Size: 2.7 TB + Fault Tolerance: 6 + Heads: 255 + Unique Identifier: 600508B1001C45441D106BDFAAEBA41E + Disk Name: /dev/sda + . + . + + physicaldrive 5I:1:1 + Port: 5I + Box: 1 + Bay: 1 + Status: OK + Interface Type: SAS + Drive Type: Data Drive + . + . + . + physicaldrive 5I:1:2 + Port: 5I + Box: 1 + Bay: 2 + Status: OK + Interface Type: SAS + Drive Type: Data Drive + + :raises: HPSSAOperationError, if hpssacli operation failed. + """ + stdout, stderr = _hpssacli("controller", "all", "show", + "config", "detail") + return stdout + + def refresh(self): + """Refresh the server and it's child objects. + + This method removes all the cache information in the server + and it's child objects, and fetches the information again from + the server using hpssacli command. + + :raises: HPSSAOperationError, if hpssacli operation failed. + """ + config = self._get_all_details() + + raid_info = _convert_to_dict(config) + self.controllers = [] + + for key, value in raid_info.iteritems(): + self.controllers.append(Controller(key, value, self)) + + self.last_updated = time.time() + + def get_controller_by_id(self, id): + """Get the controller object given the id. + + This method returns the controller object for given id. + + :param id: id of the controller, for example + 'Smart Array P822 in Slot 2' + :returns: Controller object which has the id or None if the + controller is not found. + """ + for controller in self.controllers: + if controller.id == id: + return controller + return None + + def get_logical_drives(self): + """Get all the RAID logical drives in the Server. + + This method returns all the RAID logical drives on the server + by examining all the controllers. + + :returns: a list of LogicalDrive objects. + """ + logical_drives = [] + for controller in self.controllers: + for array in controller.raid_arrays: + for logical_drive in array.logical_drives: + logical_drives.append(logical_drive) + return logical_drives + + +class Controller(object): + """This is the class for RAID controller.""" + + def __init__(self, id, properties, parent): + """Constructor for Controller object.""" + self.parent = parent + self.properties = properties + self.id = id + self.unassigned_physical_drives = [] + self.raid_arrays = [] + + unassigned_drives = properties.get('unassigned', dict()) + for key, value in unassigned_drives.iteritems(): + self.unassigned_physical_drives.append(PhysicalDrive(key, + value, + self)) + + raid_arrays = filter(lambda x: x.startswith('Array'), + properties.keys()) + for array in raid_arrays: + self.raid_arrays.append(RaidArray(array, properties[array], self)) + + def get_physical_drive_by_id(self, id): + """Get a PhysicalDrive object for given id. + + This method examines both assigned and unassigned physical + drives of the controller and returns the physical drive. + + :param id: id of physical drive, for example '5I:1:1'. + :returns: PhysicalDrive object having the id, or None if + physical drive is not found. + """ + for phy_drive in self.unassigned_physical_drives: + if phy_drive.id == id: + return phy_drive + for array in self.raid_arrays: + for phy_drive in array.physical_drives: + if phy_drive.id == id: + return phy_drive + return None + + def execute_cmd(self, *args): + """Execute a given hpssacli command on the controller. + + This method executes a given command on the controller. + + :params args: a tuple consisting of sub-commands to be appended + after specifying the controller in hpssacli command. + :raises: HPSSAOperationError, if hpssacli operation failed. + """ + + slot = self.properties['Slot'] + base_cmd = ("controller", "slot=%s" % slot) + cmd = base_cmd + args + return _hpssacli(*cmd) + + def create_logical_drive(self, logical_drive_info, physical_drive_ids): + """Create a logical drive on the controller. + + This method creates a logical drive on the controller when the + logical drive details and physical drive ids are passed to it. + + :param logical_drive_info: a dictionary containing the details + of the logical drive as specified in raid config. + :param physical_drive_ids: a list of physical drive ids to be used. + :raises: HPSSAOperationError, if hpssacli operation failed. + """ + phy_drive_ids = ','.join(physical_drive_ids) + size_mb = logical_drive_info['size_gb'] * 1024 + raid_level = logical_drive_info['raid_level'] + self.execute_cmd("create", "type=logicaldrive", + "drives=%s" % phy_drive_ids, + "raid=%s" % raid_level, + "size=%s" % size_mb) + + def delete_all_logical_drives(self): + """Deletes all logical drives on trh controller. + + This method deletes all logical drives on trh controller. + :raises: HPSSAOperationError, if hpssacli operation failed. + """ + self.execute_cmd("logicaldrive", "all", "delete", "forced") + + +class RaidArray(object): + """Class for a RAID Array. + + RAID array consists of many logical drives and many physical + drives. + """ + def __init__(self, id, properties, parent): + """Constructor for a RAID Array object.""" + self.parent = parent + self.properties = properties + self.id = id[7:] + + self.logical_drives = [] + self.physical_drives = [] + + logical_drives = filter(lambda x: x.startswith('Logical Drive'), + properties.keys()) + for logical_drive in logical_drives: + self.logical_drives.append(LogicalDrive(logical_drive, + properties[logical_drive], + self)) + + physical_drives = filter(lambda x: x.startswith('physicaldrive'), + properties.keys()) + for physical_drive in physical_drives: + self.physical_drives.append(PhysicalDrive(physical_drive, + properties[physical_drive], + self)) + + +class LogicalDrive(object): + """Class for LogicalDrive object.""" + + def __init__(self, id, properties, parent): + """Constructor for a LogicalDrive object.""" + # Strip off 'Logical Drive' before storing it in id + self.id = id[15:] + self.parent = parent + self.properties = properties + + # TODO(rameshg87): Check if size is always reported in GB + self.size_gb = int(float(self.properties['Size'].rstrip(' GB'))) + self.raid_level = self.properties['Fault Tolerance'] + + def get_property(self, prop): + if not self.properties: + return None + return self.properties.get(prop) + + +class PhysicalDrive: + """Class for PhysicalDrive object.""" + + def __init__(self, id, properties, parent): + """Constructor for a PhysicalDrive object.""" + self.parent = parent + self.properties = properties + + # Strip off physicaldrive before storing it in id + self.id = id[14:] + + # TODO(rameshg87): Check if size is always reported in GB + self.size_gb = int(float(self.properties['Size'].rstrip(' GB'))) + + ssa_interface = self.properties['Interface Type'] + self.interface_type = types.get_interface_type(ssa_interface) + self.disk_type = types.get_disk_type(ssa_interface) diff --git a/proliantutils/hpssa/raid_config_schema.json b/proliantutils/hpssa/raid_config_schema.json new file mode 100644 index 00000000..5e689aaf --- /dev/null +++ b/proliantutils/hpssa/raid_config_schema.json @@ -0,0 +1,67 @@ +{ + "title": "raid configuration json schema", + "type": "object", + "properties": { + "logical_disks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "raid_level": { + "type": "string", + "enum": [ "0", "1", "2", "5", "6", "1+0" ], + "description": "RAID level for the logical disk. Required." + }, + "size_gb": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "description": "Size (Integer) for the logical disk. Required." + }, + "volume_name": { + "type": "string", + "description": "Name of the volume to be created. Optional." + }, + "is_root_volume": { + "type": "boolean", + "description": "Specifies whether this disk is a root volume. Optional." + }, + "share_physical_disks": { + "type": "boolean", + "description": "Specifies whether other logical disks can share physical disks with this logical disk. Optional." + }, + "disk_type": { + "type": "string", + "enum": [ "hdd", "ssd" ], + "description": "Specifies the type of disk preferred. Valid values are 'hdd' and 'ssd'. Optional." + }, + "interface_type": { + "type": "string", + "enum": [ "sata", "scsi", "sas" ], + "description": "Specifies the interface type of disk. Valid values are 'sata', 'scsi' and 'sas'. Optional." + }, + "number_of_physical_disks": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "description": "Number of physical disks to use for this logical disk. Optional." + }, + "controller": { + "type": "string", + "description": "Controller to use for this logical disk. Optional." + }, + "physical_disks": { + "type": "array", + "items": { "type": "string" }, + "description": "The physical disks to use for this logical disk. Optional" + } + }, + "required": ["raid_level", "size_gb"], + "additionalProperties": false + }, + "minItems": 1 + } + }, + "required": ["logical_disks"], + "additionalProperties": false +} diff --git a/proliantutils/hpssa/types.py b/proliantutils/hpssa/types.py new file mode 100644 index 00000000..4ebaeb84 --- /dev/null +++ b/proliantutils/hpssa/types.py @@ -0,0 +1,51 @@ +# Copyright 2015 Hewlett-Packard Development Company, L.P. +# +# 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. + +INTERFACE_TYPE_SAS = 'SAS' +INTERFACE_TYPE_SCSI = 'SCSI' +INTERFACE_TYPE_SATA = 'SATA' + +DISK_TYPE_HDD = 'HDD' +DISK_TYPE_SSD = 'SSD' + +RAID_0 = '0' +RAID_1 = '1' +RAID_1_ADM = '1ADM' +RAID_10 = '10' +RAID_10_ADM = '10ADM' +RAID_5 = '5' +RAID_6 = '6' +RAID_50 = '50' +RAID_60 = '60' + + +INTERFACE_TYPE_MAP = {'SCSI': INTERFACE_TYPE_SCSI, + 'SAS': INTERFACE_TYPE_SAS, + 'SATA': INTERFACE_TYPE_SATA, + 'SATASSD': INTERFACE_TYPE_SATA, + 'SASSSD': INTERFACE_TYPE_SAS} + +DISK_TYPE_MAP = {'SCSI': DISK_TYPE_HDD, + 'SAS': DISK_TYPE_HDD, + 'SATA': DISK_TYPE_HDD, + 'SATASSD': DISK_TYPE_SSD, + 'SASSSD': DISK_TYPE_SSD} + + +def get_interface_type(ssa_interface): + return INTERFACE_TYPE_MAP[ssa_interface] + + +def get_disk_type(ssa_interface): + return DISK_TYPE_MAP[ssa_interface] diff --git a/proliantutils/ilo/operations.py b/proliantutils/ilo/operations.py index d385cb4e..f01491e8 100644 --- a/proliantutils/ilo/operations.py +++ b/proliantutils/ilo/operations.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from proliantutils.ilo import exception +from proliantutils import exception ERRMSG = "The specified operation is not supported on current platform." diff --git a/proliantutils/ilo/ribcl.py b/proliantutils/ilo/ribcl.py index 564ee055..d210ef8d 100644 --- a/proliantutils/ilo/ribcl.py +++ b/proliantutils/ilo/ribcl.py @@ -23,7 +23,7 @@ import xml.etree.ElementTree as etree import six -from proliantutils.ilo import exception +from proliantutils import exception from proliantutils.ilo import operations diff --git a/proliantutils/ilo/ris.py b/proliantutils/ilo/ris.py index 764c8b55..d9cdbc2f 100644 --- a/proliantutils/ilo/ris.py +++ b/proliantutils/ilo/ris.py @@ -22,7 +22,7 @@ import json import StringIO import urlparse -from proliantutils.ilo import exception +from proliantutils import exception from proliantutils.ilo import operations """ Currently this class supports only secure boot and firmware settings @@ -589,4 +589,4 @@ class RISOperations(operations.IloOperations): new_bios_settings) if status >= 300: msg = self._get_extended_error(response) - raise exception.IloError(msg) \ No newline at end of file + raise exception.IloError(msg) diff --git a/proliantutils/tests/hpssa/__init__.py b/proliantutils/tests/hpssa/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/proliantutils/tests/hpssa/outputs/no_drives.out b/proliantutils/tests/hpssa/outputs/no_drives.out new file mode 100644 index 00000000..6bfe7673 --- /dev/null +++ b/proliantutils/tests/hpssa/outputs/no_drives.out @@ -0,0 +1,191 @@ + +Smart Array P822 in Slot 2 + Bus Interface: PCI + Slot: 2 + Serial Number: PDVTF0BRH5T0MO + Cache Serial Number: PBKUD0BRH5T3I6 + RAID 6 (ADG) Status: Enabled + Controller Status: OK + Hardware Revision: B + Firmware Version: 4.68 + Wait for Cache Room: Disabled + Surface Analysis Inconsistency Notification: Disabled + Post Prompt Timeout: 15 secs + Cache Board Present: True + Cache Status: OK + Drive Write Cache: Disabled + Total Cache Size: 2.0 GB + Total Cache Memory Available: 1.8 GB + No-Battery Write Cache: Disabled + Cache Backup Power Source: Capacitors + Battery/Capacitor Count: 1 + Battery/Capacitor Status: OK + SATA NCQ Supported: True + Spare Activation Mode: Activate on physical drive failure (default) + Controller Temperature (C): 88 + Cache Module Temperature (C): 37 + Capacitor Temperature (C): 21 + Number of Ports: 6 (2 Internal / 4 External ) + Driver Name: hpsa + Driver Version: 3.4.4 + Driver Supports HP SSD Smart Path: True + + + + unassigned + + physicaldrive 5I:1:1 + Port: 5I + Box: 1 + Bay: 1 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7G55D0000N4173JLT + Model: HP EF0600FARNA + Current Temperature (C): 35 + Maximum Temperature (C): 43 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 5I:1:2 + Port: 5I + Box: 1 + Bay: 2 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7H2DM0000B41800Y0 + Model: HP EF0600FARNA + Current Temperature (C): 35 + Maximum Temperature (C): 44 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 5I:1:3 + Port: 5I + Box: 1 + Bay: 3 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7G4ZN0000B41707PD + Model: HP EF0600FARNA + Current Temperature (C): 33 + Maximum Temperature (C): 42 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 5I:1:4 + Port: 5I + Box: 1 + Bay: 4 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7H27F0000B41800S0 + Model: HP EF0600FARNA + Current Temperature (C): 36 + Maximum Temperature (C): 45 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 6I:1:5 + Port: 6I + Box: 1 + Bay: 5 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7H2BR0000B41800V8 + Model: HP EF0600FARNA + Current Temperature (C): 32 + Maximum Temperature (C): 41 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 6I:1:6 + Port: 6I + Box: 1 + Bay: 6 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7G4WD0000N4180GEJ + Model: HP EF0600FARNA + Current Temperature (C): 35 + Maximum Temperature (C): 44 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 6I:1:7 + Port: 6I + Box: 1 + Bay: 7 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7G54Q0000N4180W34 + Model: HP EF0600FARNA + Current Temperature (C): 31 + Maximum Temperature (C): 39 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + + SEP (Vendor ID PMCSIERA, Model SRCv24x6G) 380 + Device Number: 380 + Firmware Version: RevB + WWID: 5001438028842E1F + Vendor ID: PMCSIERA + Model: SRCv24x6G + diff --git a/proliantutils/tests/hpssa/outputs/one_drive.out b/proliantutils/tests/hpssa/outputs/one_drive.out new file mode 100644 index 00000000..9770ae65 --- /dev/null +++ b/proliantutils/tests/hpssa/outputs/one_drive.out @@ -0,0 +1,232 @@ + +Smart Array P822 in Slot 2 + Bus Interface: PCI + Slot: 2 + Serial Number: PDVTF0BRH5T0MO + Cache Serial Number: PBKUD0BRH5T3I6 + RAID 6 (ADG) Status: Enabled + Controller Status: OK + Hardware Revision: B + Firmware Version: 4.68 + Rebuild Priority: Medium + Expand Priority: Medium + Surface Scan Delay: 3 secs + Surface Scan Mode: Idle + Queue Depth: Automatic + Monitor and Performance Delay: 60 min + Elevator Sort: Enabled + Degraded Performance Optimization: Disabled + Inconsistency Repair Policy: Disabled + Wait for Cache Room: Disabled + Surface Analysis Inconsistency Notification: Disabled + Post Prompt Timeout: 15 secs + Cache Board Present: True + Cache Status: OK + Cache Ratio: 10% Read / 90% Write + Drive Write Cache: Disabled + Total Cache Size: 2.0 GB + Total Cache Memory Available: 1.8 GB + No-Battery Write Cache: Disabled + Cache Backup Power Source: Capacitors + Battery/Capacitor Count: 1 + Battery/Capacitor Status: OK + SATA NCQ Supported: True + Spare Activation Mode: Activate on physical drive failure (default) + Controller Temperature (C): 88 + Cache Module Temperature (C): 37 + Capacitor Temperature (C): 22 + Number of Ports: 6 (2 Internal / 4 External ) + Driver Name: hpsa + Driver Version: 3.4.4 + Driver Supports HP SSD Smart Path: True + + Array: A + Interface Type: SAS + Unused Space: 0 MB + Status: OK + MultiDomain Status: OK + Array Type: Data + HP SSD Smart Path: disable + + + + Logical Drive: 1 + Size: 558.9 GB + Fault Tolerance: 1 + Heads: 255 + Sectors Per Track: 32 + Cylinders: 65535 + Strip Size: 256 KB + Full Stripe Size: 256 KB + Status: OK + MultiDomain Status: OK + Caching: Enabled + Unique Identifier: 600508B1001C321CCA06EB7CD847939D + Disk Name: /dev/sda + Mount Points: None + Logical Drive Label: 01F42227PDVTF0BRH5T0MOAB64 + Mirror Group 0: + physicaldrive 5I:1:1 (port 5I:box 1:bay 1, SAS, 600 GB, OK) + Mirror Group 1: + physicaldrive 5I:1:2 (port 5I:box 1:bay 2, SAS, 600 GB, OK) + Drive Type: Data + LD Acceleration Method: Controller Cache + + physicaldrive 5I:1:1 + Port: 5I + Box: 1 + Bay: 1 + Status: OK + Drive Type: Data Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7G55D0000N4173JLT + Model: HP EF0600FARNA + Current Temperature (C): 35 + Maximum Temperature (C): 43 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 5I:1:2 + Port: 5I + Box: 1 + Bay: 2 + Status: OK + Drive Type: Data Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7H2DM0000B41800Y0 + Model: HP EF0600FARNA + Current Temperature (C): 35 + Maximum Temperature (C): 44 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + + unassigned + + physicaldrive 5I:1:3 + Port: 5I + Box: 1 + Bay: 3 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7G4ZN0000B41707PD + Model: HP EF0600FARNA + Current Temperature (C): 33 + Maximum Temperature (C): 42 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 5I:1:4 + Port: 5I + Box: 1 + Bay: 4 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7H27F0000B41800S0 + Model: HP EF0600FARNA + Current Temperature (C): 36 + Maximum Temperature (C): 45 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 6I:1:5 + Port: 6I + Box: 1 + Bay: 5 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7H2BR0000B41800V8 + Model: HP EF0600FARNA + Current Temperature (C): 32 + Maximum Temperature (C): 41 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 6I:1:6 + Port: 6I + Box: 1 + Bay: 6 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7G4WD0000N4180GEJ + Model: HP EF0600FARNA + Current Temperature (C): 35 + Maximum Temperature (C): 44 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + physicaldrive 6I:1:7 + Port: 6I + Box: 1 + Bay: 7 + Status: OK + Drive Type: Unassigned Drive + Interface Type: SAS + Size: 600 GB + Native Block Size: 512 + Rotational Speed: 15000 + Firmware Revision: HPD6 + Serial Number: 6SL7G54Q0000N4180W34 + Model: HP EF0600FARNA + Current Temperature (C): 31 + Maximum Temperature (C): 39 + PHY Count: 2 + PHY Transfer Rate: 6.0Gbps, Unknown + Drive Authentication Status: OK + Carrier Application Version: 11 + Carrier Bootloader Version: 6 + + + SEP (Vendor ID PMCSIERA, Model SRCv24x6G) 380 + Device Number: 380 + Firmware Version: RevB + WWID: 5001438028842E1F + Vendor ID: PMCSIERA + Model: SRCv24x6G + diff --git a/proliantutils/tests/hpssa/test_manager.py b/proliantutils/tests/hpssa/test_manager.py new file mode 100644 index 00000000..1080c1ae --- /dev/null +++ b/proliantutils/tests/hpssa/test_manager.py @@ -0,0 +1,122 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 proliantutils import exception +from proliantutils.hpssa import manager +from proliantutils.hpssa import objects + + +@mock.patch.object(objects.Server, '_get_all_details') +class ManagerTestCases(testtools.TestCase): + + @mock.patch.object(objects.Controller, 'execute_cmd') + def test_create_configuration(self, controller_exec_cmd_mock, + get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/no_drives.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + ld1 = {'size_gb': 50, + 'raid_level': '1', + 'controller': 'Smart Array P822 in Slot 2', + 'physical_disks': ['5I:1:1', + '5I:1:2']} + ld2 = {'size_gb': 100, + 'raid_level': '5', + 'controller': 'Smart Array P822 in Slot 2', + 'physical_disks': ['5I:1:3', + '5I:1:4', + '6I:1:5']} + + raid_info = {'logical_disks': [ld1, ld2]} + + manager.create_configuration(raid_info) + + ld1_drives = '5I:1:1,5I:1:2' + ld2_drives = '5I:1:3,5I:1:4,6I:1:5' + controller_exec_cmd_mock.assert_any_call("create", + "type=logicaldrive", + "drives=%s" % ld1_drives, + "raid=1", + "size=%d" % (50*1024)) + controller_exec_cmd_mock.assert_any_call("create", + "type=logicaldrive", + "drives=%s" % ld2_drives, + "raid=5", + "size=%d" % (100*1024)) + + def test_create_configuration_invalid_logical_disks(self, + get_all_details_mock): + + raid_info = {} + self.assertRaises(exception.InvalidInputError, + manager.create_configuration, + raid_info) + + raid_info = {'logical_disks': 'foo'} + self.assertRaises(exception.InvalidInputError, + manager.create_configuration, + raid_info) + + @mock.patch.object(objects.Controller, 'execute_cmd') + def test_delete_configuration(self, controller_exec_cmd_mock, + get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/one_drive.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + manager.delete_configuration() + + controller_exec_cmd_mock.assert_called_with("logicaldrive", + "all", + "delete", + "forced") + + def test_get_configuration(self, get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/one_drive.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + raid_info_returned = manager.get_configuration() + + ld1_expected = {'size_gb': 558, + 'raid_level': '1', + 'controller': 'Smart Array P822 in Slot 2', + 'physical_disks': ['5I:1:1', + '5I:1:2'], + 'volume_name': '01F42227PDVTF0BRH5T0MOAB64', + 'root_device_hint': { + 'wwn': '600508B1001C321CCA06EB7CD847939D'}} + + # NOTE(rameshg87: Cannot directly compare because + # of 'physical_disks' key. + ld1_returned = raid_info_returned['logical_disks'][0] + self.assertEqual(ld1_expected['size_gb'], + ld1_returned['size_gb']) + self.assertEqual(ld1_expected['raid_level'], + ld1_returned['raid_level']) + self.assertEqual(ld1_expected['controller'], + ld1_returned['controller']) + self.assertEqual(ld1_expected['volume_name'], + ld1_returned['volume_name']) + self.assertEqual(ld1_expected['root_device_hint'], + ld1_returned['root_device_hint']) + self.assertEqual(sorted(ld1_expected['physical_disks']), + sorted(ld1_returned['physical_disks'])) diff --git a/proliantutils/tests/hpssa/test_objects.py b/proliantutils/tests/hpssa/test_objects.py new file mode 100644 index 00000000..1d236bda --- /dev/null +++ b/proliantutils/tests/hpssa/test_objects.py @@ -0,0 +1,242 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 +from oslo.concurrency import processutils +import testtools + +from proliantutils import exception +from proliantutils.hpssa import objects +from proliantutils.hpssa import types + + +@mock.patch.object(objects.Server, '_get_all_details') +class ServerTest(testtools.TestCase): + + def test_server_object_no_logical_drives(self, get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/no_drives.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + server = objects.Server() + + # Assertions on server + self.assertEqual(1, len(server.controllers)) + + # Assertions on RAID controller properties + controller = server.controllers[0] + self.assertEqual(server, controller.parent) + self.assertIsInstance(controller.properties, dict) + self.assertEqual('Smart Array P822 in Slot 2', controller.id) + self.assertEqual(7, len(controller.unassigned_physical_drives)) + self.assertFalse(controller.raid_arrays) + + # Assertion on physical drives on controller + physical_drives_expected = ['5I:1:1', '5I:1:2', '5I:1:3', '5I:1:4', + '6I:1:5', '6I:1:6', '6I:1:7'] + physical_drives_found = map(lambda x: x.id, + controller.unassigned_physical_drives) + self.assertEqual(sorted(physical_drives_expected), + sorted(physical_drives_found)) + + physical_drive = filter(lambda x: x.id == '5I:1:1', + controller.unassigned_physical_drives)[0] + self.assertEqual(controller, physical_drive.parent) + self.assertEqual(600, physical_drive.size_gb) + self.assertEqual(types.INTERFACE_TYPE_SAS, + physical_drive.interface_type) + self.assertEqual(types.DISK_TYPE_HDD, + physical_drive.disk_type) + + def test_server_object_one_logical_drive(self, get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/one_drive.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + server = objects.Server() + + controller = server.controllers[0] + self.assertEqual(5, len(controller.unassigned_physical_drives)) + self.assertEqual(1, len(controller.raid_arrays)) + + # Assertion on raid_arrays + array = controller.raid_arrays[0] + self.assertEqual(array.parent, controller) + self.assertIsInstance(array.properties, dict) + self.assertEqual('A', array.id) + self.assertEqual(1, len(array.logical_drives)) + self.assertEqual(2, len(array.physical_drives)) + + # Assertion on logical drives of array + logical_drive = array.logical_drives[0] + self.assertEqual('1', logical_drive.id) + self.assertEqual(logical_drive.parent, array) + self.assertEqual(558, logical_drive.size_gb) + self.assertEqual(types.RAID_1, logical_drive.raid_level) + self.assertIsInstance(logical_drive.properties, dict) + + # Assertion on physical drives of array + physical_drive = filter(lambda x: x.id == '5I:1:1', + array.physical_drives)[0] + self.assertEqual(array, physical_drive.parent) + self.assertEqual(600, physical_drive.size_gb) + + # Assertion on physical drives of controller + physical_drive = filter(lambda x: x.id == '5I:1:3', + controller.unassigned_physical_drives)[0] + self.assertEqual(controller, physical_drive.parent) + self.assertEqual(600, physical_drive.size_gb) + + def test_get_controller_by_id(self, get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/one_drive.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + server = objects.Server() + + id = 'Smart Array P822 in Slot 2' + self.assertEqual(server.controllers[0], + server.get_controller_by_id(id)) + self.assertIsNone(server.get_controller_by_id('foo')) + + def test_get_logical_drives(self, get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/one_drive.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + server = objects.Server() + + exp_ld = server.controllers[0].raid_arrays[0].logical_drives[0] + self.assertEqual(exp_ld, server.get_logical_drives()[0]) + + def test_get_logical_drives_no_drives(self, get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/no_drives.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + server = objects.Server() + self.assertFalse(server.get_logical_drives()) + + +@mock.patch.object(objects.Server, '_get_all_details') +class ControllerTest(testtools.TestCase): + + @mock.patch.object(processutils, 'execute') + def test_execute_cmd(self, processutils_mock, get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/no_drives.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + server = objects.Server() + controller = server.controllers[0] + + processutils_mock.return_value = ('stdout', 'stderr') + + stdout, stderr = controller.execute_cmd('foo', 'bar') + + processutils_mock.assert_called_once_with("hpssacli", + "controller", + "slot=2", + "foo", + "bar") + self.assertEqual(stdout, 'stdout') + self.assertEqual(stderr, 'stderr') + + @mock.patch.object(processutils, 'execute') + def test_execute_cmd_fails(self, processutils_mock, get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/no_drives.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + server = objects.Server() + controller = server.controllers[0] + + processutils_mock.side_effect = OSError + + self.assertRaises(exception.HPSSAOperationError, + controller.execute_cmd, + 'foo', 'bar') + + @mock.patch.object(objects.Controller, 'execute_cmd') + def test_create_logical_drive(self, execute_mock, + get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/no_drives.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + server = objects.Server() + controller = server.controllers[0] + + logical_drive_info = {'size_gb': 50, + 'raid_level': '1', + 'volume_name': 'boot_volume', + 'is_boot_volume': 'true', + 'controller': 'Smart Array P822 in Slot 2', + 'physical_disks': ['5I:1:1', + '5I:1:2', + '5I:1:3']} + + controller.create_logical_drive(logical_drive_info, + ['5I:1:1', + '5I:1:2', + '5I:1:3']) + execute_mock.assert_called_once_with("create", + "type=logicaldrive", + "drives=5I:1:1,5I:1:2,5I:1:3", + "raid=1", + "size=51200") + + @mock.patch.object(objects.Controller, 'execute_cmd') + def test_delete_all_logical_drives(self, execute_mock, + get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/no_drives.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + server = objects.Server() + controller = server.controllers[0] + + controller.delete_all_logical_drives() + execute_mock.assert_called_once_with("logicaldrive", "all", + "delete", "forced") + + def test_get_physical_drive_by_id(self, get_all_details_mock): + + fobj = open('proliantutils/tests/hpssa/outputs/one_drive.out', 'r') + stdout = '\n'.join(fobj.readlines()) + get_all_details_mock.return_value = stdout + + server = objects.Server() + controller = server.controllers[0] + array = controller.raid_arrays[0] + + physical_drive = filter(lambda x: x.id == '5I:1:1', + array.physical_drives)[0] + self.assertEqual(physical_drive, + controller.get_physical_drive_by_id('5I:1:1')) + + physical_drive = filter(lambda x: x.id == '5I:1:3', + controller.unassigned_physical_drives)[0] + self.assertEqual(physical_drive, + controller.get_physical_drive_by_id('5I:1:3')) + + self.assertIsNone(controller.get_physical_drive_by_id('foo')) diff --git a/proliantutils/tests/ilo/test_ribcl.py b/proliantutils/tests/ilo/test_ribcl.py index 8451db38..78930f09 100644 --- a/proliantutils/tests/ilo/test_ribcl.py +++ b/proliantutils/tests/ilo/test_ribcl.py @@ -20,7 +20,7 @@ import unittest import constants import mock -from proliantutils.ilo import exception +from proliantutils import exception from proliantutils.ilo import ribcl diff --git a/requirements.txt b/requirements.txt index dde81855..427b2c48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ six>=1.9.0 +oslo.concurrency>=1.4.1 # Apache-2.0 +jsonschema>=2.4.0