python-ibmcclient-0.2.5.1/0000777000000000000000000000000013677000135013441 5ustar 00000000000000python-ibmcclient-0.2.5.1/CHANGELOG.md0000666000000000000000000000135013676776371015276 0ustar 00000000000000## 0.2.5 (2020-07-01) * feature: waiting storage ready before delete/apply raid configuration * optimize: add client validation for *number_of_physical_disks* option ## 0.2.4 (2020-05-26) * use RST format readme to support RDO rpm package ## 0.2.3 (2020-05-20) * adjust delete raid configuration feature: * remove restore controller action * do not update drive state when JBOD * add storage summary feature ## 0.2.2 (2020-05-14) * add typing lib for python<3.5 ## 0.2.1 (2020-05-14) * Add OOBSupport check * remove __version__ dependency in setup.py ## 0.2.0 (2020-05-14) * Add RAID configuration support. ## 0.0.2 (2019-02-19) * Fix classifiers. ## 0.0.1 (2019-02-19) * Initial release. python-ibmcclient-0.2.5.1/LICENSE.txt0000666000000000000000000000102513662773573015303 0ustar 00000000000000Licensed 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.python-ibmcclient-0.2.5.1/MANIFEST.in0000666000000000000000000000055313663231775015215 0ustar 00000000000000# Include the README include README.rst include CHANGELOG.md # Include the license file include LICENSE.txt # Include dep include requirements.txt # Include the data files recursive-include data * recursive-include ibmc_client *.json exclude tox.ini exclude coverage.sh exclude tests exclude test-requirements.txt recursive-exclude tests * python-ibmcclient-0.2.5.1/PKG-INFO0000666000000000000000000001010113677000135014527 0ustar 00000000000000Metadata-Version: 2.1 Name: python-ibmcclient Version: 0.2.5.1 Summary: HUAWEI iBMC client Home-page: https://github.com/IamFive/python-ibmcclient Author: QianBiao NG Author-email: iampurse@vip.qq.com License: UNKNOWN Project-URL: Bug Reports, https://github.com/IamFive/python-ibmcclient/issues Project-URL: Source, https://github.com/IamFive/python-ibmcclient Description: ================= python-ibmcclient ================= .. image:: https://travis-ci.org/IamFive/python-ibmcclient.svg?branch=master :target: https://travis-ci.org/IamFive/python-ibmcclient python-ibmcclient is a Python library to communicate with HUAWEI `iBMC` based systems. The goal of the library is to be extremely simple, small, have as few dependencies as possible and be very conservative when dealing with BMCs by access HTTP REST API provided by HUAWEI `iBMC` based systems. Currently, the scope of the library has been limited to supporting `OpenStack Ironic ibmc driver`_ Requirements ============ Python 2.7 and 3.4+ Installation ------------ From PyPi: .. code-block:: bash $ pip install python-ibmcclient or .. code-block:: bash $ easy_install python-ibmcclient Or from source: .. code-block:: bash $ python setup.py install Getting Started --------------- Please follow the `Installation`_ and then run the following: .. code-block:: python from __future__ import print_function from pprint import pprint import ibmc_client from ibmc_client import constants # ibmc server address = "https://example.ibmc.com" # credential username = "username" password = "password" # disable certification verify verify = False with ibmc_client.connect(address, username, password, verify) as client: # get system system = client.system.get() print('Power State: ') pprint(system.power_state) print('Boot Sequence: ') pprint(system.boot_sequence) print('Boot Source Override:' ) pprint(system.boot_source_override) # reset system client.system.reset(constants.RESET_FORCE_RESTART) # set boot source override client.system.set_boot_source(constants.BOOT_SOURCE_TARGET_PXE, constants.BOOT_SOURCE_MODE_BIOS, constants.BOOT_SOURCE_ENABLED_ONCE) .. _OpenStack Ironic ibmc driver: https://github.com/openstack/ironic-specs/blob/master/specs/approved/ibmc-driver.rst Keywords: HUAWEI iBMC redfish API client Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Description-Content-Type: text/x-rst Provides-Extra: dev Provides-Extra: test python-ibmcclient-0.2.5.1/README.rst0000666000000000000000000000424013663350015015130 0ustar 00000000000000================= python-ibmcclient ================= .. image:: https://travis-ci.org/IamFive/python-ibmcclient.svg?branch=master :target: https://travis-ci.org/IamFive/python-ibmcclient python-ibmcclient is a Python library to communicate with HUAWEI `iBMC` based systems. The goal of the library is to be extremely simple, small, have as few dependencies as possible and be very conservative when dealing with BMCs by access HTTP REST API provided by HUAWEI `iBMC` based systems. Currently, the scope of the library has been limited to supporting `OpenStack Ironic ibmc driver`_ Requirements ============ Python 2.7 and 3.4+ Installation ------------ From PyPi: .. code-block:: bash $ pip install python-ibmcclient or .. code-block:: bash $ easy_install python-ibmcclient Or from source: .. code-block:: bash $ python setup.py install Getting Started --------------- Please follow the `Installation`_ and then run the following: .. code-block:: python from __future__ import print_function from pprint import pprint import ibmc_client from ibmc_client import constants # ibmc server address = "https://example.ibmc.com" # credential username = "username" password = "password" # disable certification verify verify = False with ibmc_client.connect(address, username, password, verify) as client: # get system system = client.system.get() print('Power State: ') pprint(system.power_state) print('Boot Sequence: ') pprint(system.boot_sequence) print('Boot Source Override:' ) pprint(system.boot_source_override) # reset system client.system.reset(constants.RESET_FORCE_RESTART) # set boot source override client.system.set_boot_source(constants.BOOT_SOURCE_TARGET_PXE, constants.BOOT_SOURCE_MODE_BIOS, constants.BOOT_SOURCE_ENABLED_ONCE) .. _OpenStack Ironic ibmc driver: https://github.com/openstack/ironic-specs/blob/master/specs/approved/ibmc-driver.rstpython-ibmcclient-0.2.5.1/ibmc_client/0000777000000000000000000000000013677000135015711 5ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/__init__.py0000666000000000000000000001034613677000075020031 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.2 import logging from ibmc_client.api.chassis import IbmcChassisClient from ibmc_client.api.task.task import IbmcTaskClient from ibmc_client import constants from ibmc_client.resources import CollectionResource, BaseResource from .api.system import IbmcSystemClient from .connector import Connector __version__ = "0.2.5.1" LOG = logging.getLogger(__name__) def connect(address, username, password, verify_ca=True): return IBMCClient(address, username, password, verify_ca) class IBMCClient(object): """iBMC API Client""" def __init__(self, address, username, password, verify_ca): self.address = address self.username = username self.password = password self.verify_ca = verify_ca self.connector = Connector(address, username, password, verify_ca) # initial iBMC resource client self._system = IbmcSystemClient(self.connector, ibmc_client=self) self._chassis = IbmcChassisClient(self.connector, ibmc_client=self) self._task = IbmcTaskClient(self.connector, ibmc_client=self) def __enter__(self): self.connector.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): self.connector.disconnect() def load_odata(self, odata_id, odata_type): """Load odata resource from odata id :param odata_id: indicates the id of odata :param odata_type: indicates the type of odata (python model class) :return: A python model class object represents the odata """ url = self.connector.get_url(odata_id) resp = self.connector.request(constants.GET, url) return odata_type(resp, ibmc_client=self) def load_collection_resource(self, collection_odata_id): # type: (str|dict) -> CollectionResource """load odata collection resource :param collection_odata_id: indicates the id of odata collection :return: A :class:`ibmc_client.resources.CollectionResource` object """ return self.load_odata(collection_odata_id, CollectionResource) def load_odata_collection(self, collection_odata_id, odata_type): # type: (str|dict, BaseResource) -> list[BaseResource] """load odata collection list by collection odata id :param collection_odata_id: indicates the id of odata collection :param odata_type: indicates the type of odata (python model class) :return: a list of :class:`ibmc_client.resources.BaseResource` object which represents the odata collection """ odata_collection = self.load_collection_resource(collection_odata_id) return [self.load_odata(odata, odata_type) for odata in odata_collection.resources] def delete_odata(self, odata_id): """delete odata resource :param odata_id: indicates the id of odata :return response of redfish HTTP delete request """ url = self.connector.get_url(odata_id) return self.connector.request(constants.DELETE, url) @property def system(self): """reference to ibmc system resource client :return: ibmc system resource client """ return self._system @property def chassis(self): """reference to ibmc chassis resource client :return: ibmc chassis resource client """ return self._chassis @property def task(self): """reference to ibmc task-service resource client :return: ibmc task-service resource client """ return self._task python-ibmcclient-0.2.5.1/ibmc_client/api/0000777000000000000000000000000013677000135016462 5ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/api/__init__.py0000666000000000000000000000536613665701637020621 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 from typing import Union import ibmc_client from ibmc_client.resources import CollectionResource class BaseApiClient(object): """iBMC base API Client""" def __init__(self, connector, ibmcclient): # type: (ibmc_client.Connector, ibmc_client.IBMCClient) -> None self.connector = connector self.ibmc_client = ibmcclient def load_odata(self, odata_id, odata_type): """Load odata resource from odata id :param odata_id: indicates the id of odata :param odata_type: indicates the type of odata (python model class) :return: A python model class object represents the odata """ return self.ibmc_client.load_odata(odata_id, odata_type) def load_collection_resource(self, collection_odata_id): # type: (Union[str, dict]) -> CollectionResource """load odata collection resource :param collection_odata_id: indicates the id of odata collection :return: A :class:`ibmc_client.resources.CollectionResource` object """ return self.ibmc_client.load_collection_resource(collection_odata_id) def load_odata_collection( self, collection_odata_id, # type: Union[str, dict] odata_type # type: ibmc_client.BaseResource ): # type: (...) -> list[ibmc_client.BaseResource] """load odata collection list by collection odata id :param collection_odata_id: indicates the id of odata collection :param odata_type: indicates the type of odata (python model class) :return: a list of :class:`ibmc_client.resources.BaseResource` object which represents the odata collection """ return self.ibmc_client.load_odata_collection(collection_odata_id, odata_type) def delete_odata(self, odata_id): """delete odata resource :param odata_id: indicates the id of odata :return response of redfish HTTP delete request """ return self.ibmc_client.delete_odata(odata_id) python-ibmcclient-0.2.5.1/ibmc_client/api/chassis/0000777000000000000000000000000013677000135020117 5ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/api/chassis/__init__.py0000666000000000000000000000325413657460746022254 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 import logging from ibmc_client import api from ibmc_client.api.chassis import drive from ibmc_client.constants import GET from ibmc_client.resources.chassis import Chassis LOG = logging.getLogger(__name__) class IbmcChassisClient(api.BaseApiClient): """iBMC chassis API Client""" def __init__(self, connector, ibmc_client=None): """Initial a iBMC chassis Resource Client :param connector: iBMC http connector :param ibmc_client: a reference to global :class:`~ibmc_client.IBMCClient` object """ super(IbmcChassisClient, self).__init__(connector, ibmc_client) self._drive_client = drive.IbmcDriveClient(connector, ibmc_client) def get(self): url = self.connector.chassis_base_url resp = self.connector.request(GET, url) return Chassis(resp, ibmc_client=self.ibmc_client) @property def drive(self): """Get iBMC chassis drive client :return: """ return self._drive_client python-ibmcclient-0.2.5.1/ibmc_client/api/chassis/drive.py0000666000000000000000000000371413657260067021621 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 from ibmc_client.api import BaseApiClient from ibmc_client.constants import GET from ibmc_client.resources.chassis.drive import Drive class IbmcDriveClient(BaseApiClient): """iBMC drive API Client""" def __init__(self, connector, ibmc_client=None): """Initial a iBMC drive Resource Client :param connector: iBMC http connector :param ibmc_client: a reference to global :class:`~ibmc_client.IBMCClient` object """ super(IbmcDriveClient, self).__init__(connector, ibmc_client) def get(self, drive_id): """get drive by id :param drive_id: indicates the id of drive :return: A Drive (:class:`~ibmc_client.resources.chassis.drive.Drive`) object """ url = '%s/Drives/%s' % (self.connector.chassis_base_url, drive_id) resp = self.connector.request(GET, url) return Drive(resp, ibmc_client=self.ibmc_client) def list(self, storage_id): """list all drives belong to a storage :param storage_id: indicates the id of RAID storage :return: A list of Drive (:class:`~ibmc_client.resources.chassis.drive .Drive`) object """ storage = self.ibmc_client.system.storage.get(storage_id) return storage.drives() python-ibmcclient-0.2.5.1/ibmc_client/api/system/0000777000000000000000000000000013677000135020006 5ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/api/system/__init__.py0000666000000000000000000000723413657260067022137 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.2 import logging from ibmc_client import constants from ibmc_client.api import BaseApiClient from ibmc_client.api.system.bios import IBMCBiosClient from ibmc_client.api.system.storage import IBMCStorageClient from ibmc_client.api.system.volume import IbmcVolumeClient from ibmc_client.constants import GET, PATCH, POST from ibmc_client.resources.system import System LOG = logging.getLogger(__name__) class IbmcSystemClient(BaseApiClient): """iBMC API Client""" def __init__(self, connector, ibmc_client=None): """Initial a iBMC System Resource Client :param connector: iBMC http connector :param ibmc_client: a reference to global :class:`~ibmc_client.IBMCClient` object """ super(IbmcSystemClient, self).__init__(connector, ibmc_client) self._bios_client = IBMCBiosClient(self.connector, self.ibmc_client) self._storage_client = IBMCStorageClient(self.connector, self.ibmc_client) self._volume_client = IbmcVolumeClient(self.connector, self.ibmc_client) def get(self): uri = self.connector.system_base_url resp = self.connector.request(GET, uri) return System(resp, ibmc_client=self.ibmc_client) @property def bios(self): """Only V5 series servers support this function. :return: """ return self._bios_client @property def storage(self): """Get iBMC out-band storage client :return: """ return self._storage_client @property def volume(self): """Get iBMC volume client :return: """ return self._volume_client def reset(self, reset_type): """Restart server :param reset_type: Reset type """ action_uri = self.get().get_action_uri('ComputerSystem.Reset') url = self.connector.get_url(action_uri) payload = dict(ResetType=reset_type) self.connector.request(POST, url, json=payload) def set_boot_source(self, device, mode=None, enabled=constants.BOOT_SOURCE_ENABLED_ONCE): """Set system boot source :param device: Boot device :param mode: Boot mode :param enabled: The frequency, whether to set it for the next reboot only (BOOT_SOURCE_ENABLED_ONCE) or persistent to all future reboots (BOOT_SOURCE_ENABLED_CONTINUOUS) or disabled (BOOT_SOURCE_ENABLED_DISABLED). """ payload = { 'Boot': { 'BootSourceOverrideTarget': device, 'BootSourceOverrideEnabled': enabled, } } if mode: payload['Boot']['BootSourceOverrideMode'] = mode self.connector.request(PATCH, self.connector.system_base_url, json=payload) LOG.debug('Set iBMC boot source override succeed') python-ibmcclient-0.2.5.1/ibmc_client/api/system/bios.py0000666000000000000000000000274713657260067021340 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.2 from ibmc_client.api import BaseApiClient from ibmc_client.constants import GET from ibmc_client.resources.system.bios import Bios class IBMCBiosClient(BaseApiClient): """iBMC BIOS API Client""" def __init__(self, connector, ibmc_client=None): """Initial a iBMC System Resource Client :param connector: iBMC http connector :param ibmc_client: a reference to global :class:`~ibmc_client.IBMCClient` object """ super(IBMCBiosClient, self).__init__(connector, ibmc_client) def get(self): # TODO (qianbiao.ng) should we detect resource odata id from root? # keep it hardcode here for now. uri = '%s/Bios' % self.connector.system_base_url resp = self.connector.request(GET, uri) return Bios(resp, ibmc_client=self.ibmc_client) python-ibmcclient-0.2.5.1/ibmc_client/api/system/storage.py0000666000000000000000000007513513676610333022045 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 import logging import time from collections import defaultdict from functools import cmp_to_key from itertools import groupby import six from ibmc_client import raid_utils, exceptions, constants from ibmc_client.api import BaseApiClient from ibmc_client.resources.system.storage import Storage LOG = logging.getLogger(__name__) class LogicalDisk(object): MAX_CAPACITY = -1 controller = None """controller model""" volume_name = None raid_level = None drives = None capacity_bytes = None span_number = None bootable = False share_physical_disks = False number_of_physical_disks = 0 use_shareable_disk_group = False def __init__(self, logical_disk): self._logical_disk = logical_disk self.controller = None # initialize volume name self.volume_name = logical_disk.get('volume_name', None) self.drives = [] self.capacity_bytes = None self.span_number = None # initialize bootable self.bootable = logical_disk.get('is_root_volume', False) self._size_gb = logical_disk.get('size_gb') self._controller_hint = logical_disk.get('controller') self._media_type = logical_disk.get('disk_type') self._protocol = logical_disk.get('interface_type') self._physical_disks = logical_disk.get('physical_disks') # initialize raid level _raid_level = logical_disk.get('raid_level') self.raid_setting = raid_utils.RAID_SETTINGS.get(_raid_level) if not self.raid_setting: raise exceptions.NotSupportedRaidLevel(_raid_level) # initialize capacity bytes if type(self._size_gb) == int: self.capacity_bytes = self._size_gb * 1024 * 1024 * 1024 else: self.capacity_bytes = self.MAX_CAPACITY # initialize physical disk settings self.share_physical_disks = logical_disk.get( 'share_physical_disks', False) # if user has specified physical disk number, use it. # (?) Else use min disks number as required by current raid level. self.number_of_physical_disks = logical_disk.get( 'number_of_physical_disks', None) if self.number_of_physical_disks: min_disk_count = self.raid_setting.get_min_disks() if self.number_of_physical_disks < min_disk_count: reason = ('number_of_physical_disks is small than min-disk-' 'count(%d) required by raid level %s' % (min_disk_count, self.raid_setting.key)) raise exceptions.InvalidLogicalDiskConfig( config=self._logical_disk, reason=reason) self.use_shareable_disk_group = False @property def is_jbod_mode(self): return self.raid_setting.name == raid_utils.JBOD @property def auto_scale(self): """whether use as much free space(size 'max') as possible :return: true if yes or false """ return self.capacity_bytes == self.MAX_CAPACITY @property def use_specified_disks(self): """whether user has specified the disks to be used :return: """ return self._physical_disks and len(self._physical_disks) > 0 def init_ctrl(self, controllers): # type: (list[Storage]) -> None """initialize storage controller for this logical disk :param controllers: a list controller object of iBMC """ if len(controllers) == 0: raise exceptions.NoRaidControllerFound() if not self._controller_hint: if len(controllers) != 1: raise exceptions.ControllerHintRequired() else: self.controller = controllers[0] else: # find storage matches hint or the every controller if no hint self.controller = next((ctrl for ctrl in controllers if ctrl.matches(self._controller_hint)), None) if not self.controller: raise exceptions.NoControllerMatchesHint( hint=self._controller_hint) if not self.controller.support_oob: ctrl_name = (self._controller_hint if self._controller_hint else self.controller.controller_name) raise exceptions.ControllerNotSupportOOB(controller=ctrl_name) if (self.raid_setting.name not in self.controller.supported_raid_levels and self.raid_setting.name != raid_utils.JBOD): raise exceptions.NotSupportedRaidLevel( self.raid_setting.key, controller=self._controller_hint) def init_disks( self, physical_disks, # type: list[raid_utils.PhysicalDisk] physical_disk_groups # type: list[raid_utils.PhysicalDiskGroup] ): """ :param physical_disks: a list of exists physical disk object :param physical_disk_groups: a list of exists physical disk group object :return: """ # disks that are excludable and matches required # "media type" and "protocol" excludable_disks = [ d for d in physical_disks if d.drive.matches(d.drive.id, media_type=self._media_type, protocol=self._protocol) and d.is_excludable] # non-share and use specified disks (2 cases, both size int and max) if not self.share_physical_disks and self.use_specified_disks: # for disk_hint in self._physical_disks: # disk = self.get_specified_disk(excludable_disks, disk_hint) # disk.mark_as_exclusive() # self.drives.append(disk.drive_id) # # TODO(qianbiao.ng) is disk validation required? # self.guess_span_number() specified_disks = [] for hint in self._physical_disks: disk = self.get_specified_disk(physical_disks, hint) if not disk.is_excludable: reason = ('Disk `%s` may has been used by other logical ' 'disk.' % hint) raise exceptions.InvalidLogicalDiskConfig( config=self._logical_disk, reason=reason) specified_disks.append(disk) try: solution = self.raid_setting.get_best_matched_disks( self.capacity_bytes, specified_disks, len(specified_disks)) if solution: self.span_number = solution.span for disk in solution.disks: disk.mark_as_exclusive() self.drives.append(disk.drive_id) else: raise exceptions.SpecifiedDisksHasNotEnoughSpace( size=self._size_gb, raid=self.raid_setting.key) except exceptions.IBMCClientError as e: raise exceptions.InvalidLogicalDiskConfig( config=self._logical_disk, reason=str(e)) # non-share and auto choose disks (2 cases, both size int and max) elif not self.share_physical_disks and not self.use_specified_disks: try: solution = self.raid_setting.get_best_matched_disks( self.capacity_bytes, excludable_disks, self.number_of_physical_disks) if solution: self.span_number = solution.span for disk in solution.disks: disk.mark_as_exclusive() self.drives.append(disk.drive_id) else: raise exceptions.LackOfDiskSpace() except exceptions.IBMCClientError as e: raise exceptions.InvalidLogicalDiskConfig( config=self._logical_disk, reason=str(e)) # share and use specified disks (2 cases, both size int and max) elif self.share_physical_disks and self.use_specified_disks: # we should find it from exits disk group first specified_disks = [self.get_specified_disk(physical_disks, hint) for hint in self._physical_disks] disk_group = self.find_disk_group_owns_disks( physical_disk_groups, specified_disks) if disk_group: disk_group.add_pending_capacity_bytes(self.capacity_bytes) self.drives = [disk_group.drives[0].drive_id] self.use_shareable_disk_group = True return # if no disk group matches, we need to use free disks for i in range(0, len(specified_disks)): disk = specified_disks[i] if not disk.is_excludable: reason = ('Disk `%s` may has been used by other logical ' 'disk.' % self._physical_disks[i]) raise exceptions.InvalidLogicalDiskConfig( config=self._logical_disk, reason=reason) try: solution = self.raid_setting.get_best_matched_disks( self.capacity_bytes, specified_disks, len(specified_disks)) if solution: self.use_shareable_solution(solution, physical_disk_groups) else: raise exceptions.LackOfDiskSpace() except exceptions.IBMCClientError as e: raise exceptions.InvalidLogicalDiskConfig( config=self._logical_disk, reason=str(e)) elif self.share_physical_disks and not self.use_specified_disks: # if any exists disk group matches, we would not compare it with # disk solutions. Use it directly. disk_group = self.raid_setting.get_best_matched_disk_group( self.capacity_bytes, physical_disk_groups) if disk_group: disk_group.add_pending_capacity_bytes(self.capacity_bytes) self.drives = [disk_group.drives[0].drive_id] self.use_shareable_disk_group = True return try: solution = self.raid_setting.get_best_matched_disks( self.capacity_bytes, excludable_disks, self.number_of_physical_disks) if solution: self.use_shareable_solution(solution, physical_disk_groups) else: raise exceptions.LackOfDiskSpace() except exceptions.IBMCClientError as e: raise exceptions.InvalidLogicalDiskConfig( config=self._logical_disk, reason=str(e)) def use_shareable_solution( self, solution, # type: raid_utils.RaidSolution physical_disk_groups # type: list[raid_utils.PhysicalDiskGroup] ): # type: (...) -> None """use a physical disks shareable solution - update drives & span number - mark used drives as exclusive - update physical disk group :param solution: :param physical_disk_groups: :return: """ self.span_number = solution.span drives = [disk.drive for disk in solution.disks] self.drives = [drive.drive_id for drive in drives] disk_group = raid_utils.PhysicalDiskGroup( drives, self.raid_setting, self.span_number) disk_group.add_used_capacity_bytes(self.capacity_bytes) physical_disk_groups.append(disk_group) for disk in solution.disks: disk.mark_as_exclusive() def find_disk_group_owns_disks( self, physical_disk_groups, # type: list[raid_utils.PhysicalDiskGroup] specified_physical_disks # type: list[raid_utils.PhysicalDisk] ): # type: (...) -> raid_utils.PhysicalDiskGroup """Find a disk group which owns all those physical disks. :param physical_disk_groups available physical disk groups :param specified_physical_disks specified physical disks :raises exceptions.InvalidLogicalDiskConfig when a disk group owns those physical disks exists, but it does not have enough capacity or it has a different raid-level. :return physical disk group if it's suitable to create the logical disk """ disk_id_str = ','.join([disk.drive.id for disk in specified_physical_disks]) LOG.info("Try to find disk-group owns disks %(disks)s", {'disks': disk_id_str}) disk_group = None share_disk_group = False for dg in physical_disk_groups: # TODO (qianbiao.ng) whether all disks should be present or a # subset disks is ok too? share_disk_group = all(disk.drive.id in dg.drive_id_list for disk in specified_physical_disks) if share_disk_group: disk_group = dg break if share_disk_group: try: disk_group.validate_if_suitable_for(self.capacity_bytes, self.raid_setting) LOG.info("Find a matched disk-group:: %(disk_group)s. Use it.", {'disk_group': str(disk_group)}) except exceptions.NotSuitablePhysicalDiskGroup as e: LOG.info("Find a disk-group:: %(disk_group)s owns specified " "disks. But it can not be used because:: %(reason)s", {'disk_group': str(disk_group), 'reason': str(e)}) raise exceptions.InvalidLogicalDiskConfig( config=self._logical_disk, reason=e.message) else: LOG.info("Could not find any disk-group owns disks%(disks)s", {'disks': disk_id_str}) return disk_group def get_specified_disk( self, physical_disks, # type: list[raid_utils.PhysicalDisk] disk_hint # type: str ): # type: (...) -> raid_utils.PhysicalDisk """get the physical disk specified by disk hint, media type and protocol. :param physical_disks: all physical disk object list :param disk_hint: indicates the disk hint specified by caller :raises: exceptions.NoDriveMatchesHint when non physical disk matches :return: the specified physical disk """ disk = next((disk for disk in physical_disks if disk.drive.matches(disk_hint, media_type=self._media_type, protocol=self._protocol)), None) if not disk: raise exceptions.NoDriveMatchesHint(hint=disk_hint, media_type=self._media_type, protocol=self._protocol) disk.hint = disk_hint return disk def guess_span_number(self): # pragma: no cover """(deprecated) guess span number """ if self.raid_setting.name in [raid_utils.RAID0, raid_utils.RAID1, raid_utils.RAID5, raid_utils.RAID6]: self.span_number = 1 elif self.raid_setting.name in [raid_utils.RAID50, raid_utils.RAID60]: for n in [2, 3, 5, 7]: if len(self.drives) % n == 0: self.span_number = n elif self.raid_setting.name == raid_utils.RAID10: self.span_number = len(self.drives) >> 1 def to_create_volume_payload(self): capacity_bytes = (None if self.capacity_bytes == self.MAX_CAPACITY else self.capacity_bytes) if not self.use_shareable_disk_group: payload = dict( storage_id=self.controller.id, volume_name=self.volume_name, raid_level=self.raid_setting.name, drives=self.drives, capacity_bytes=capacity_bytes, span=self.span_number, bootable=self.bootable) else: payload = dict( storage_id=self.controller.id, volume_name=self.volume_name, raid_level=None, drives=self.drives, capacity_bytes=capacity_bytes, span=None, bootable=self.bootable) return payload def __str__(self): return str(self._logical_disk) def sort_and_group_pending_logical_disk_list(logical_disks): # type: (list[LogicalDisk]) -> dict(str, list[LogicalDisk]) """ :rtype: dict(str, list[LogicalDisk]) :param logical_disks: :return: """ def compare_pending_logical_disks(logical_disk1, logical_disk2): # type: (LogicalDisk, LogicalDisk) -> int """ :param logical_disk1: :param logical_disk2: :return: """ # order by controller first if logical_disk1.controller.id != logical_disk2.controller.id: gt = logical_disk1.controller.id > logical_disk2.controller.id return 1 if gt else -1 # volumes with specified drives can be processed without cleaning if len(logical_disk1.drives) != len(logical_disk2.drives): return len(logical_disk1.drives) - len(logical_disk2.drives) # volume with 'MAX' size and empty drives should be processed at last if logical_disk1.capacity_bytes != logical_disk2.capacity_bytes: return logical_disk1.capacity_bytes - logical_disk2.capacity_bytes return 0 # pragma: no cover if six.PY2: # pragma: no cover _logical_disk_list = sorted( logical_disks, cmp=compare_pending_logical_disks, reverse=True) elif six.PY3: _logical_disk_list = sorted( logical_disks, key=cmp_to_key(compare_pending_logical_disks), reverse=True) grouped_logical_disks = dict( (key, list(it)) for key, it in groupby(_logical_disk_list, lambda it: it.controller.id) ) return grouped_logical_disks def build_disk_groups(ctrl): disk_groups = [] for volume in ctrl.volumes(): disk_group = next((dg for dg in disk_groups if dg.owns_volume(volume)), None) if disk_group: disk_group.add_used_capacity_bytes(volume.capacity_bytes) else: disk_group = raid_utils.PhysicalDiskGroup.from_volume( volume, ctrl.drives()) disk_groups.append(disk_group) return disk_groups class IBMCStorageClient(BaseApiClient): """iBMC storage API Client""" def __init__(self, connector, ibmc_client=None): """Initial a iBMC System storage Resource Client :param connector: iBMC http connector :param ibmc_client: a reference to global :class:`~ibmc_client.IBMCClient` object """ super(IBMCStorageClient, self).__init__(connector, ibmc_client) def list(self): # type: () -> list[Storage] """Get all storage controllers of this iBMC :return: a list of storage controller (:class:`~ibmc_client.resources.system.storage.Storage`) object """ url = '%s/Storages' % self.connector.system_base_url return self.load_odata_collection(url, Storage) def get(self, storage_id): # type: (str) -> Storage """get storage controller by raid storage id :param storage_id: indicates the id of storage :return: A storage controller(:class:`~ibmc_client.resources.system .storage.Storage`) object """ url = '%s/Storages/%s' % (self.connector.system_base_url, storage_id) return self.load_odata(url, Storage) def delete_all_raid_configuration(self): """Delete all RAID configuration. :return: """ LOG.info("Start delete all RAID configuration.") # waiting until storage ready self.waiting_storage_ready() storage_collection = self.list() for storage in storage_collection: if not storage.support_oob: raise exceptions.ControllerNotSupportOOB( controller=storage.controller_name) LOG.info("Start delete RAID configuration for %s.", storage.id) # we do not need to restore storage # restore RAID controller # storage.restore() # storage.set(copy_back=True, smarter_copy_back=True, jbod=False) # delete volume collection of RAID controller storage.delete_volume_collection() # restore all drives for drive in storage.drives(): drive.restore() LOG.info("Delete RAID configuration for %s done.", storage.id) if not storage_collection: LOG.info("No Storage present in this server.") LOG.info("Delete all RAID configuration done.") def apply_raid_configuration(self, logical_disks): """Apply RAID configuration. :param logical_disks: a list of JSON dictionaries which represents the logical disks to be created. The JSON dictionary should match the (ibmc_client.raid_config_schema.json) scheme. check https://docs.openstack.org/ironic/latest/admin/raid.html for details. A typical logical_disks may looks like:: [ { "size_gb": 50, "raid_level": "1+0", "controller": "RAID.Integrated.1-1", "volume_name": "root_volume", "is_root_volume": true, "physical_disks": [ "Disk.Bay.0:Encl.Int.0-1:RAID.Integrated.1-1", "Disk.Bay.1:Encl.Int.0-1:RAID.Integrated.1-1" ] }, { "size_gb": 100, "raid_level": "5", "controller": "RAID.Integrated.1-1", "volume_name": "data_volume", "physical_disks": [ "Disk.Bay.2:Encl.Int.0-1:RAID.Integrated.1-1", "Disk.Bay.3:Encl.Int.0-1:RAID.Integrated.1-1", "Disk.Bay.4:Encl.Int.0-1:RAID.Integrated.1-1" ] }, ..... ] :return: """ LOG.info('Start apply RAID configuration:: %(logical_disks)s', {'logical_disks': logical_disks}) # waiting until storage ready self.waiting_storage_ready() # load all controllers controllers = self.list() # prepare pending volume list pending_volume_list = [] for logical_disk in logical_disks: logical_disk = LogicalDisk(logical_disk) logical_disk.init_ctrl(controllers) pending_volume_list.append(logical_disk) groups = defaultdict(list) # type: dict(str, list[LogicalDisk]) for pending_volume in pending_volume_list: groups[pending_volume.controller.id].append(pending_volume) # validate volumes self.validate_pending_volumes(groups) # assign drives for not specified volumes for (ctrl_id, pending_volumes) in groups.items(): ctrl = next(ctrl for ctrl in controllers if ctrl.id == ctrl_id) jbod_mode = any(v.is_jbod_mode for v in pending_volumes) # handle JBOD mode if jbod_mode: ctrl.set(jbod=True) continue share_disk_enabled = any(v.share_physical_disks for v in pending_volumes) disk_groups = build_disk_groups(ctrl) if share_disk_enabled else [] physical_disks = [raid_utils.PhysicalDisk(drive) for drive in ctrl.drives()] # handle other RAID levels ordered_pending_volumes = [] """ step1:: handle volumes (2 cases) [x] share physical disks [o] specified physical disks [o] size "max|int" Notes:: - make sure all specified disks has not been used - make sure all specified disks will not be used later """ ordered_pending_volumes.extend([v for v in pending_volumes if not v.share_physical_disks and v.use_specified_disks]) """ step2:: handle volumes (1 cases) [x] share physical disks [x] specified physical disks [o] size "int" Notes:: - use disks which waste as less as better - make sure all specified disks has not been used - make sure all specified disks will not be used later """ ordered_pending_volumes.extend([v for v in pending_volumes if not v.share_physical_disks and not v.use_specified_disks and not v.auto_scale]) """ step3:: handle volumes (1 cases) [o] share physical disks [o] specified physical disks [o] size "int" """ ordered_pending_volumes.extend([v for v in pending_volumes if v.share_physical_disks and v.use_specified_disks and not v.auto_scale]) """ step4:: handle volumes (1 cases) [o] share physical disks [o] specified physical disks [o] size "max" """ ordered_pending_volumes.extend([v for v in pending_volumes if v.share_physical_disks and v.use_specified_disks and v.auto_scale]) """ step5:: handle volumes (1 cases) [o] share physical disks [x] specified physical disks [o] size "int" """ ordered_pending_volumes.extend([v for v in pending_volumes if v.share_physical_disks and not v.use_specified_disks and not v.auto_scale]) """ step6:: handle volumes (1 cases) [o] share physical disks [x] specified physical disks [o] size "max" Notes:: (?) Is this case really exists? """ ordered_pending_volumes.extend([v for v in pending_volumes if v.share_physical_disks and not v.use_specified_disks and v.auto_scale]) """ step7:: handle volumes (1 cases) [x] share physical disks [x] specified physical disks [o] size "max" """ ordered_pending_volumes.extend([ v for v in pending_volumes if (not v.share_physical_disks and not v.use_specified_disks and v.auto_scale)]) for pending_volume in ordered_pending_volumes: pending_volume.init_disks(physical_disks, disk_groups) for pending_volume in ordered_pending_volumes: self.ibmc_client.system.volume.create( **pending_volume.to_create_volume_payload()) time.sleep(constants.RAID_TASK_EFFECT_SECONDS) def waiting_storage_ready(self): # waiting util storage ready LOG.info('Waiting until storage ready.') while True: system = self.ibmc_client.system.get() try: if system.is_storage_ready: LOG.info('Storage is ready.') break LOG.info('Storage is not ready. waiting...') except exceptions.FeatureNotSupported: LOG.info('Query `IsStorageReady` feature is not supported, ' 'will treat it as ready now.') break time.sleep(30) @staticmethod def validate_pending_volumes(groups): for (ctrl_id, volumes) in groups.items(): # - (?) share disks & disks not specified but size 'max' # - size 'max' & disks not specified at most could appear once # unlimit_auto_scale_volumes = [v for v in volumes if v.auto_scale # and not v.use_specified_disks] # if len(unlimit_auto_scale_volumes) > 1: # reason = ('At most one logical disk config can be size-gb(' # '"max") but no physical-disks specified.') # raise InvalidLogicalDiskConfig( # config=unlimit_auto_scale_volumes[0], reason=reason) jbod_volumes = [v for v in volumes if v.is_jbod_mode] if 0 < len(jbod_volumes) != len(volumes): reason = 'JBOD mode could not work with other RAID level.' raise exceptions.InvalidLogicalDiskConfig( config=jbod_volumes[0], reason=reason) python-ibmcclient-0.2.5.1/ibmc_client/api/system/volume.py0000666000000000000000000001753513657260067021714 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 from ibmc_client.api import BaseApiClient from ibmc_client.constants import GET, PATCH, POST from ibmc_client.resources.system.storage import Volume from ibmc_client.resources.task import Task from ibmc_client.utils import remove_empty_from_dict # class CreateVolumePayload(object): # StorageId = None # VolumeName = None # VolumeRaidLevel = None # Drives = None # CapacityBytes = None # SpanNumber = None # Bootable = False # # def __init__(self, storage_id=None, volume_name=None, raid_level=None, # drives=None, capacity_bytes=None, span=None, bootable=None): # self.StorageId = storage_id # self.VolumeName = volume_name # self.VolumeRaidLevel = raid_level # self.CapacityBytes = capacity_bytes # self.Drives = drives # self.SpanNumber = span # self.Bootable = bootable # # def to_dict(self): # oem = remove_empty_from_dict({ # "VolumeName": self.VolumeName, # "VolumeRaidLevel": self.VolumeRaidLevel, # "Drives": self.Drives, # "SpanNumber": self.SpanNumber if self.SpanNumber > 1 else None # }) # # payload = { # "CapacityBytes": self.CapacityBytes, # "Oem": { # "Huawei": oem # } # } # return remove_empty_from_dict(payload) class IbmcVolumeClient(BaseApiClient): """iBMC volume API Client""" def __init__(self, connector, ibmc_client=None): """Initial a iBMC volume Resource Client :param connector: iBMC http connector :param ibmc_client: a reference to global :class:`~ibmc_client.IBMCClient` object """ super(IbmcVolumeClient, self).__init__(connector, ibmc_client) def get(self, storage_id, volume_id): """get volume by id :param storage_id: indicates the id of storage :param volume_id: indicates the id of volume :return: A Volume (:class:`~ibmc_client.resources.chassis.volume .Volume`) object """ url = '%s/Storages/%s/Volumes/%s' % (self.connector.system_base_url, storage_id, volume_id) resp = self.connector.request(GET, url) return Volume(resp, ibmc_client=self.ibmc_client) def list(self, storage_id): """get all volumes belongs to the storage :param storage_id: indicates the id of storage :return: A list of Volume (:class:`~ibmc_client.resources.chassis .volume.Volume`) object """ url = '%s/Storages/%s/Volumes' % (self.connector.system_base_url, storage_id) collection = self.connector.request(GET, url).json() members = collection.get('Members', []) return [self.load_odata(member, Volume) for member in members] def init(self, storage_id, volume_id, init_type): """init volume :param storage_id: indicates the id of storage :param volume_id: indicates the id of volume :param init_type: Indicates the initialization action of volume. Available Value Set: QuickInit, FullInit, CancelInit. - QuickInit: perform quick initialization. No task will be created - FullInit: perform complete initialization. A task will be created - CancelInit: cancel the initialization. No task will be created :return: :raises: a sub-class of IBMCClientError when http request failed """ url = '%s/Storages/%s/Volumes/%s/Actions/Volume.Initialize' % ( self.connector.system_base_url, storage_id, volume_id) payload = { "Type": init_type } self.connector.request(POST, url, json=payload) def create(self, storage_id=None, volume_name=None, raid_level=None, drives=None, capacity_bytes=None, span=None, bootable=None): """Create a new volume :param storage_id: indicates the storage id :param volume_name: indicates the name of to create volume :param raid_level: indicates the raid-level of to create volume :param drives: indicates the used drives of to create volume :param capacity_bytes: indicates the capacity bytes of to create volume :param span: indicates the span number of to create volume :param bootable: indicates whether the volume is a bootable volume :return: created volume id """ url = '%s/Storages/%s/Volumes' % (self.connector.system_base_url, storage_id) oem = remove_empty_from_dict({ "VolumeName": volume_name, "VolumeRaidLevel": raid_level, "Drives": drives, "SpanNumber": span if (span and span > 1) else None }) payload = remove_empty_from_dict({ "CapacityBytes": capacity_bytes, "Oem": { "Huawei": oem } }) resp = self.connector.request(POST, url, json=payload) task = self.ibmc_client.task.wait_task( Task(resp, ibmc_client=self.ibmc_client)) task.raise_if_failed() created_volume_odata_id = task.message_args[0] # set as boot volume if necessary if bootable: self.set_bootable(created_volume_odata_id, bootable) return created_volume_odata_id.split('/')[-1] def get_volume_odata_id(self, storage_id, volume_id): """set volume odata id :param storage_id: indicates the storage id :param volume_id: indicates the volume id :return: """ url = '%s/Storages/%s/Volumes/%s' % ( self.connector.system_base_url, storage_id, volume_id) return url def set_bootable(self, volume_odata_id, bootable): """set volume as boot disk :param volume_odata_id: indicates the volume odata id :param bootable: :return: """ payload = { "Oem": { "Huawei": { "BootEnable": bootable } } } url = self.connector.get_url(volume_odata_id) self.connector.request(PATCH, url, json=payload) def delete(self, storage_id, volume_id): """delete volume :param storage_id: indicates the storage id to delete :param volume_id: indicates the volume id to delete :return: volume deletion task with stable status """ url = '%s/Storages/%s/Volumes/%s' % (self.connector.system_base_url, storage_id, volume_id) return self.delete_by_odata_id(url) def delete_by_odata_id(self, odata_id): """delete volume by volume-odata-id :param odata_id: indicates the odata resource id of volume :return: volume deletion task with stable status """ resp = self.delete_odata(odata_id) task = self.ibmc_client.task.wait_task( Task(resp, ibmc_client=self.ibmc_client)) return task python-ibmcclient-0.2.5.1/ibmc_client/api/task/0000777000000000000000000000000013677000135017424 5ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/api/task/__init__.py0000666000000000000000000000000013657260067021535 0ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/api/task/task.py0000666000000000000000000000462313657260067020757 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 import logging from time import sleep from ibmc_client.api import BaseApiClient from ibmc_client import constants from ibmc_client.resources.task import Task LOG = logging.getLogger(__name__) class IbmcTaskClient(BaseApiClient): """iBMC TaskService API Client""" RECHECK_TASK_DELAY_IN_SECONDS = 3 def __init__(self, connector, ibmc_client=None): """Initial a iBMC TaskService Resource Client :param connector: iBMC http connector :param ibmc_client: a reference to global :class:`~ibmc_client.IBMCClient` object """ super(IbmcTaskClient, self).__init__(connector, ibmc_client) def get(self, task_id): url = '%s/Tasks/%s' % (self.connector.task_service_base_url, task_id) return self.load_odata(url, Task) def wait_task_by_id(self, task_id): """wait a task util it becomes stable. :param task_id: indicates id of task :return: stable task """ task = self.get(task_id) return self.wait_task(task) def wait_task(self, task): """wait a task util it becomes stable. :param task: task it self :return: stable task """ LOG.info("Wait task util processed, task: %s.", task) while True: if task.state in constants.TASK_STATUS_PROCESSING: LOG.info("%s is still processing, will reload %d seconds " "later.", task, self.RECHECK_TASK_DELAY_IN_SECONDS) sleep(self.RECHECK_TASK_DELAY_IN_SECONDS) task = self.get(task.id) elif task.state in constants.TASK_STATUS_PROCESSED: LOG.info("%s has been processed.", task) return task python-ibmcclient-0.2.5.1/ibmc_client/connector.py0000666000000000000000000002051013665701427020264 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. import logging from time import sleep import requests import six from ibmc_client import constants from ibmc_client import exceptions LOG = logging.getLogger(__name__) class Connector(object): """IBMC API connector base on requests""" # Default timeout in seconds for requests connect and read # http://docs.python-requests.org/en/master/user/advanced/#timeouts _DEFAULT_TIMEOUT = 60 def __init__(self, address, username, password, verify_ca): self.base_url = '%s/redfish/v1' % address self.address = address self._username = username self._password = password self._verify_ca = verify_ca self.session = None # Initial request session self._conn = requests.Session() self._conn.verify = verify_ca from ibmc_client import __version__ as version self._conn.headers.update({ 'User-Agent': 'python-ibmcclient - v%s' % version }) self._meta = self.request(constants.GET, self.base_url).json() @property def resource_id(self): return self._resource_id @property def system_base_url(self): return '%s/%s' % (self._meta['Systems']['@odata.id'], self._resource_id) @property def manager_base_url(self): return '%s/%s' % (self._meta['Managers']['@odata.id'], self._resource_id) @property def chassis_base_url(self): return '%s/%s' % (self._meta['Chassis']['@odata.id'], self._resource_id) @property def task_service_base_url(self): return self._meta['Tasks']['@odata.id'] @property def session_service_base_url(self): return self._meta['SessionService']['@odata.id'] @property def version(self): return self._meta['RedfishVersion'] # pragma: no cover def get_url(self, resource): """get absolute URL for odata resource :param resource: redfish resource :return: """ if isinstance(resource, six.string_types): path = resource elif isinstance(resource, dict) and resource.get("@odata.id", None): path = resource.get("@odata.id", None) if path.startswith(self.base_url): return path elif path.startswith('/redfish/v1'): return '%s%s' % (self.address, path) else: return '%s%s' % (self.base_url, path) def connect(self): if self.session is None: self._fetch_session() self._get_resource_id() def disconnect(self): try: session_path = '%(address)s%(location)s' % self.session self.request(constants.DELETE, session_path) except requests.exceptions.RequestException: # pragma: no cover # Failed to delete session, just ignore the errors now. # may be the session has expired. we can let it expired auto. LOG.warn('Failed to delete session.') def _fetch_session(self): """Fetch and cache a new session""" payload = { 'UserName': self._username, 'Password': self._password } create_session_url = '%s/Sessions' % self.session_service_base_url res = self.request(constants.POST, create_session_url, json=payload) # cache session token = res.headers.get(constants.HEADER_AUTH_TOKEN) location = res.headers.get('Location') self.session = dict(address=self.address, token=token, location=location) # update request credential header self._conn.headers.update({ constants.HEADER_AUTH_TOKEN: token, }) def _get_resource_id(self): """get resource id of server. - The value is 1 for a rack server. - The value is BladeN for a high-density server. N indicates the slot number of the server node. For example, Blade1. - The value is BladeN for a compute node or SwiN for a switch module in a blade server. N indicates the slot number of the compute node or switch module. """ managers_url = self.address + self._meta['Managers']['@odata.id'] res = self.request(constants.GET, managers_url).json() manager_odata_id = res['Members'][0]['@odata.id'] self._resource_id = manager_odata_id.split('/')[-1] def request(self, method, url, json=None, etag=None, headers=None, retry=False): try: url = self.get_url(url) return self._request(method, url, json=json, etag=etag, headers=headers) except requests.exceptions.RequestException as e: response = e.response if response is not None: if not retry and response.status_code: if response.status_code == 401: # If session expired, renew session then retry self._fetch_session() return self.request(method, url, json=json, etag=etag, headers=headers, retry=True) if response.status_code == 412: # If 412 pre-condition checking failed, # just retry after 10 seconds. sleep(10) return self.request(method, url, json=json, etag=etag, headers=headers, retry=True) LOG.warning('iBMC response -> %(method)s %(url)s, ' 'code: %(code)s, response: %(resp_txt)s', {'method': method, 'url': url, 'code': response.status_code, 'resp_txt': response.content}) raise exceptions.raise_for_response(method, url, response) else: raise exceptions.IBMCConnectionError(url=url, error=e) def _request(self, method, url, json=None, etag=None, headers=None): # If request method is PATCH or PUT, # "If-Match" header is required by iBMC redfish API. if method.upper() in [constants.PATCH, constants.PUT]: headers = headers or {} if not etag: res = self.request(constants.GET, url) headers.update({ constants.HEADER_IF_MATCH: res.headers.get(constants.HEADER_ETAG)}) else: headers.update({constants.HEADER_IF_MATCH: etag}) if method.upper() in [constants.POST, constants.PATCH, constants.PUT]: headers = headers or {} headers.update({constants.HEADER_CONTENT_TYPE: 'application/json'}) if url.endswith('/Sessions') and method == constants.POST: LOG.debug('iBMC request -> %(method)s %(url)s', {'method': method, 'url': url}) else: LOG.debug( 'iBMC request -> %(method)s %(url)s, payload:: %(payload)s', {'method': method, 'url': url, 'payload': json}) req = requests.Request(method, url, json=json, headers=headers) prepped = self._conn.prepare_request(req) res = self._conn.send(prepped, timeout=self._DEFAULT_TIMEOUT) res.raise_for_status() LOG.debug('iBMC response -> %(method)s %(url)s, code: %(code)s, ' 'content:: %(content)s', {'method': method, 'url': url, 'code': res.status_code, 'content': res.text}) return res python-ibmcclient-0.2.5.1/ibmc_client/constants.py0000666000000000000000000001270013657260067020311 0ustar 00000000000000# -*- coding: utf-8 -*- # Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.2 # RAID rsync task wait effect time seconds RAID_TASK_EFFECT_SECONDS = 20 # HTTP request method shortcut HEAD = 'HEAD' """http method HEAD""" GET = 'GET' """http method get""" POST = 'POST' """http method POST""" PATCH = 'PATCH' """http method PATCH""" PUT = 'PUT' """http method PUT""" DELETE = 'DELETE' # Redfish HTTP Headers HEADER_ETAG = 'ETag' """Redfish API HTTP header 'ETag'""" HEADER_AUTH_TOKEN = 'X-Auth-Token' """Redfish API HTTP header 'X-Auth-Token'""" HEADER_IF_MATCH = 'If-Match' """Redfish API HTTP header 'If-Match'""" HEADER_CONTENT_TYPE = 'Content-Type' """Redfish API HTTP header 'Content-Type'""" # System PowerState constants SYSTEM_POWER_STATE_ON = 'On' """The system is powered on""" SYSTEM_POWER_STATE_OFF = 'Off' """The system is powered off, although some components may continue to have AUX power such as management controller""" # Boot source target constants BOOT_SOURCE_TARGET_NONE = 'None' """Boot from the normal boot device""" BOOT_SOURCE_TARGET_PXE = 'Pxe' """Boot from the Pre-Boot EXecution (PXE) environment""" BOOT_SOURCE_TARGET_FLOPPY = 'Floppy' """Boot from the floppy disk drive""" BOOT_SOURCE_TARGET_CD = 'Cd' """Boot from the CD/DVD disc""" BOOT_SOURCE_TARGET_HDD = 'Hdd' """Boot from a hard drive""" BOOT_SOURCE_TARGET_BIOS_SETUP = 'BiosSetup' """Boot to the BIOS Setup Utility""" # Boot source mode constants BOOT_SOURCE_MODE_BIOS = 'Legacy' BOOT_SOURCE_MODE_UEFI = 'UEFI' # Boot source enabled constants BOOT_SOURCE_ENABLED_ONCE = 'Once' BOOT_SOURCE_ENABLED_CONTINUOUS = 'Continuous' BOOT_SOURCE_ENABLED_DISABLED = 'Disabled' # Reset action constants RESET_NMI = 'Nmi' RESET_ON = 'On' RESET_FORCE_OFF = 'ForceOff' RESET_GRACEFUL_SHUTDOWN = 'GracefulShutdown' RESET_FORCE_RESTART = 'ForceRestart' RESET_FORCE_POWER_CYCLE = 'ForcePowerCycle' # Task status TASK_STATUS_NEW = 'New' TASK_STATUS_STARTING = 'Starting' TASK_STATUS_RUNNING = 'Running' TASK_STATUS_SUSPENDED = 'Suspended' TASK_STATUS_INTERRUPTED = 'Interrupted' TASK_STATUS_PENDING = 'Pending' TASK_STATUS_STOPPING = 'Stopping' TASK_STATUS_COMPLETED = 'Completed' TASK_STATUS_KILLED = 'Killed' TASK_STATUS_EXCEPTION = 'Exception' TASK_STATUS_SERVICE = 'Service' TASK_STATUS_PROCESSING = (TASK_STATUS_NEW, TASK_STATUS_STARTING, TASK_STATUS_RUNNING, TASK_STATUS_SUSPENDED, TASK_STATUS_PENDING, TASK_STATUS_STOPPING) TASK_STATUS_PROCESSED = (TASK_STATUS_INTERRUPTED, TASK_STATUS_EXCEPTION, TASK_STATUS_KILLED, TASK_STATUS_COMPLETED) TASK_STATUS_FAILED = (TASK_STATUS_INTERRUPTED, TASK_STATUS_KILLED, TASK_STATUS_EXCEPTION) # Initial volume types VOLUME_INIT_QUICK = 'QuickInit' """perform quick initialization. No task will be created.""" VOLUME_INIT_FULL = 'FullInit' """perform complete initialization. A task will be created.""" VOLUME_INIT_CANCEL = 'CancelInit' """cancel the initialization. No task will be created.""" # Online 为某个虚拟磁盘的成员盘,可正常使用,处于在线状态。 # Unconfigured Good 磁盘状态正常,但不是虚拟磁盘的成员盘或热备盘。 # Hot Spare 被设置为热备盘。 # Failed 当“Online”状态或“Hot Spare”状态的磁盘出现不可恢复的错误时,会体现为此状态。 # Rebuild 硬盘正在进行数据重建,以保证虚拟磁盘的数据冗余性和完整性。 # Unconfigured Bad “Unconfigured Good”状态磁盘或未初始化的磁盘,出现 # 无法恢复的错误时,会体现为此状态。 # Missing “Online”状态的磁盘被拔出后,体现为此状态。 # Offline 为某个虚拟磁盘的成员盘,不可正常使用,处于离线状态。 # Shield State 物理磁盘在做诊断操作时的临时状态。 # Copyback 新盘正在替换故障成员盘。 # Optimal 虚拟磁盘状态良好,所有成员盘均在线。 # Degraded 虚拟磁盘状态异常,存在成员盘故障或离线的情况。 # Failed 虚拟磁盘故障。 # Partial Degraded 当RAID组中的物理硬盘故障或离线的数量未超过该RAID组级 # 别支持的最大故障硬盘的数量时,RAID组会体现为部分降级状态。 # drive firmware state DRIVE_FM_STATE_JBOD = 'JBOD' """drive firmware state:: JBOD""" DRIVE_FM_STATE_ONLINE = 'Online' """drive firmware state:: Online""" DRIVE_FM_STATE_HOTSPARE = 'HotSpareDrive' """drive firmware state:: Hot Spare""" DRIVE_FM_STATE_UNCONFIG_GOOD = 'UnconfiguredGood' """drive firmware state:: Unconfigured Good""" # drive hot spare type HOT_SPARE_NONE = 'None' """drive hot spare type None""" HOT_SPARE_GLOBAL = 'Global' """drive hot spare type Global""" HOT_SPARE_DEDICATED = 'Dedicated' """drive hot spare type Dedicated""" python-ibmcclient-0.2.5.1/ibmc_client/exceptions.py0000666000000000000000000002045213676604471020463 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. All Rights Reserved. # Copyright 2017 Red Hat, Inc. All Rights Reserved. # Modified upon https://github.com/openstack/sushy # # 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 logging from six.moves import http_client LOG = logging.getLogger(__name__) class IBMCClientError(Exception): """Basic exception for errors""" message = None def __init__(self, **kwargs): if self.message and kwargs: self.message = self.message % kwargs super(IBMCClientError, self).__init__(self.message) class IBMCClientOperationError(IBMCClientError): def __init__(self, error, **kwargs): self.message = error super(IBMCClientOperationError, self).__init__(**kwargs) class IBMCConnectionError(IBMCClientError): message = 'Unable to connect to %(url)s. Error: %(error)s' class MissingAttributeError(IBMCClientError): message = ('The attribute %(attribute)s is missing from the ' 'resource %(resource)s') class MalformedAttributeError(IBMCClientError): message = ('The attribute %(attribute)s is malformed in the ' 'resource %(resource)s: %(error)s') class MissingActionError(IBMCClientError): message = ('The action %(action)s is missing from the ' 'resource %(resource)s') class InvalidParameterValueError(IBMCClientError): message = ('The parameter "%(parameter)s" value "%(value)s" is invalid. ' 'Valid values are: %(valid_values)s') class NothingToApplyError(IBMCClientError): message = 'Nothing to apply, at least one property should be specified.' class ArchiveParsingError(IBMCClientError): message = 'Failed parsing archive "%(path)s": %(error)s' class IBMCHttpRequestError(IBMCClientError): """Basic exception for HTTP errors""" status_code = None """HTTP status code.""" body = None """Error JSON body, if present.""" code = 'Base.1.0.GeneralError' """Error code defined in the Redfish specification, if present.""" detail = None """Error message defined in the Redfish specification, if present.""" message = 'HTTP %(method)s %(url)s returned code %(code)s. %(error)s' def __init__(self, method, url, response): self.status_code = response.status_code try: body = response.json() except ValueError: LOG.warning('Error response from %(method)s %(url)s ' 'with status code %(code)s has no JSON body', {'method': method, 'url': url, 'code': self.status_code}) error = 'unknown error' else: self.body = body.get('error', {}) # TODO (qianbiao.ng): handle partial failure situation self.extended_info_list = self.body.get('@Message.ExtendedInfo', []) if len(self.extended_info_list) > 0: info = self.extended_info_list[0] error = ('[%(Severity)s] %(Message)s ' 'Resolution: %(Resolution)s') % info else: error = ('http status code: %d, http response: %s' % (self.status_code, self.body)) kwargs = {'method': method, 'url': url, 'code': self.status_code, 'error': error} LOG.info(('HTTP response for %(method)s %(url)s -> ' 'status code: %(code)s, error: %(error)s'), kwargs) super(IBMCHttpRequestError, self).__init__(**kwargs) class BadRequestError(IBMCHttpRequestError): pass class ResourceNotFoundError(IBMCHttpRequestError): # Overwrite the complex generic message with a simpler one. message = 'Resource %(url)s not found' class ServerSideError(IBMCHttpRequestError): pass class AccessError(IBMCHttpRequestError): pass class MissingXAuthToken(IBMCHttpRequestError): message = ('No X-Auth-Token returned from remote host when ' 'attempting to establish a session. Error: %(error)s') class NoControllerMatchesHint(IBMCClientError): message = ('No RAID storage controller matches hint %(hint)s. Please ' 'using storage-id, storage-name or storage-controller-name as ' 'controller hint.') class NoDriveMatchesHint(IBMCClientError): message = ('No available physical disk matches hint: %(hint)s, ' 'media-type: %(media_type)s, protocol: %(protocol)s. Please ' 'using HUAWEI drive-id, drive id, drive name or drive ' 'serial-number as physical disk hint.') def __init__(self, **kwargs): if kwargs['media_type'] is None: kwargs['media_type'] = 'any' if kwargs['protocol'] is None: kwargs['protocol'] = 'any' super(NoDriveMatchesHint, self).__init__(**kwargs) class NotSupportedRaidLevel(IBMCClientError): message = 'RAID level %(raid_level)s is supported.' def __init__(self, raid_level, controller=None): kwargs = {'raid_level': raid_level} if controller: kwargs.update(controller='controller') self.message = ('RAID level %(raid_level)s is supported by ' 'controller %(controller)s.') super(NotSupportedRaidLevel, self).__init__(**kwargs) class NoRaidControllerFound(IBMCClientError): message = 'No RAID storage controller found.' class ControllerHintRequired(IBMCClientError): message = ('Option `controller` is required because more than one RAID ' 'storage controller exists. Please review target-raid-config.') class TaskFailed(IBMCClientError): message = '%(message)s' class InvalidPhysicalDiskNumber(IBMCClientError): message = ('Invalid number_of_physical_disks option value %(' 'number_of_physical_disks)d, it could not work with ' 'raid-level %(raid)s.') class LackOfDiskSpace(IBMCClientError): message = ('There are not enough available disk space to create' ' this logical disk.') class SpecifiedDisksHasNotEnoughSpace(IBMCClientError): message = ('The specified physical disks do not have enough space ' 'to create a %(size)dG logical-disk(raid-level %(raid)s).') class InvalidLogicalDiskConfig(IBMCClientError): message = ('Logical-disk config `%(config)s` is invalid, reason: %(' 'reason)s') class NotSuitablePhysicalDiskGroup(IBMCClientError): message = '%(message)s' class ControllerNotSupportOOB(IBMCClientError): message = ('RAID controller `%(controller)s` does not support OOB ' 'management. Currently, ibmc RAID interface can only manage ' 'RAID controller which support OOB management.') class FeatureNotSupported(IBMCClientError): message = ('Feature is not supported by this iBMC server: %(feature)s, ' 'please check the version of this iBMC server.') def raise_for_response(method, url, response): """Raise a correct error class, if needed.""" if response.status_code < http_client.BAD_REQUEST: return elif response.status_code == http_client.NOT_FOUND: raise ResourceNotFoundError(method, url, response) elif response.status_code == http_client.BAD_REQUEST: raise BadRequestError(method, url, response) elif response.status_code in (http_client.UNAUTHORIZED, http_client.FORBIDDEN): raise AccessError(method, url, response) elif response.status_code >= http_client.INTERNAL_SERVER_ERROR: raise ServerSideError(method, url, response) else: raise IBMCHttpRequestError(method, url, response) python-ibmcclient-0.2.5.1/ibmc_client/raid_config_schema.json0000666000000000000000000001003613657260067022402 0ustar 00000000000000{ "$schema": "http://json-schema.org/draft-04/schema#", "title": "raid configuration json schema", "type": "object", "properties": { "logical_disks": { "type": "array", "items": { "type": "object", "properties": { "raid_level": { "type": "string", "enum": [ "JBOD", "0", "1", "2", "5", "6", "1+0", "5+0", "6+0" ], "description": "RAID level for the logical disk. Valid values are 'JBOD', '0', '1', '2', '5', '6', '1+0', '5+0' and '6+0'. Required." }, "size_gb": { "anyOf": [ { "type": "integer", "minimum": 0 }, { "type": "string", "enum": [ "MAX" ] } ], "description": "Size in GiB (Integer) for the logical disk. Use 'MAX' as size_gb if this logical disk is supposed to use the rest of the space available. Required." }, "volume_name": { "type": "string", "description": "Name of the volume to be created. If this is not specified, it will be auto-generated. Optional." }, "is_root_volume": { "type": "boolean", "description": "Specifies whether this disk is a root volume. By default, this is False. Optional." }, "share_physical_disks": { "type": "boolean", "description": "Specifies whether other logical disks can share physical disks with this logical disk. By default, this is False. Optional." }, "disk_type": { "type": "string", "enum": [ "hdd", "ssd" ], "description": "The type of disk preferred. Valid values are 'hdd' and 'ssd'. If this is not specified, disk type will not be a selection criterion for choosing backing physical disks. Optional." }, "interface_type": { "type": "string", "enum": [ "sata", "scsi", "sas" ], "description": "The interface type of disk. Valid values are 'sata', 'scsi' and 'sas'. If this is not specified, interface type will not be a selection criterion for choosing backing physical disks. Optional." }, "number_of_physical_disks": { "type": "integer", "minimum": 0, "exclusiveMinimum": true, "description": "Number of physical disks to use for this logical disk. By default, the driver uses the minimum number of disks required for that RAID level. Optional." }, "controller": { "type": "string", "description": "Controller to use for this logical disk. If not specified, the driver will choose a suitable RAID controller on the bare metal node. Optional." }, "physical_disks": { "anyOf": [ { "type": "array", "items": { "type": "string" } }, { "type": "array", "items": { "type": "object" }, "minItems": 2 } ], "description": "The physical disks to use for this logical disk. If not specified, the driver will choose suitable physical disks to use. Optional." } }, "required": [ "raid_level", "size_gb" ], "additionalProperties": false, "dependencies": { "physical_disks": [ "controller" ] } }, "minItems": 1 } }, "required": [ "logical_disks" ], "additionalProperties": false } python-ibmcclient-0.2.5.1/ibmc_client/raid_utils.py0000666000000000000000000006222213670321111020417 0ustar 00000000000000# Copyright 2020 HUAWEI, Inc. 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. # Version 0.0.3 import collections import logging import math from ibmc_client import exceptions from ibmc_client.resources.chassis import drive as DRIVE from ibmc_client.resources.system import storage LOG = logging.getLogger(__name__) # RAID Levels JBOD = 'JBOD' """No RAID, JBOD mode""" RAID0 = 'RAID0' """RAID Level 0, at least 2 drives is required.""" RAID1 = 'RAID1' """RAID Level 1, at least 1 drives is required.""" RAID5 = 'RAID5' """RAID Level 5, at least 3 drives is required. (N-1)""" RAID6 = 'RAID6' """RAID Level 6, (N-2)""" RAID10 = 'RAID10' """RAID Level 10, alias to RAID 0+1, at least 4 drives is required.""" RAID50 = 'RAID50' """RAID Level 50""" RAID60 = 'RAID60' """RAID Level 60""" # RAID types RAID_TYPE_SPANNED = 'Spanned' """RAID type spanned""" RAID_TYPE_NON_REDUNDANT = 'NonRedundant' """RAID type NonRedundant""" RAID_TYPE_MIRRORED = 'Mirrored' """RAID type Mirrored""" RAID_TYPE_STRIPED_WITH_PARITY = 'StripedWithParity' """RAID type Mirrored""" RAID_TYPE_RAW_DEVICE = 'RawDevice' """RAID type RawDevice""" class PhysicalDisk(object): """A model represents a real physical-disk(known as drive in iBMC) hardware """ drive = None drive_id = None protocol = None media_type = None capacity_bytes = None firmware_state = None used_by_volumes = None exclusive = None """indicates whether a physical disk is exclusive""" # pending_capacity_bytes = None # """indicates the pending bytes of uncommitted volumes""" def __init__(self, drive): # type: (DRIVE.Drive) -> None self.drive = drive self.drive_id = drive.drive_id self.protocol = drive.protocol self.media_type = drive.media_type self.capacity_bytes = drive.capacity_bytes self.firmware_state = drive.firmware_state self.used_by_volumes = drive.volume_odata_id_collection self.exclusive = False # def left_capacity_bytes(self): # return self.capacity_bytes - sum(self.pending_capacity_bytes) # @property # def is_shareable(self): # """whether this disk is shareable. # - not exclusive by any other unshareable pending volume # - firmware state is unconfig good # - left capacity bytes is great than 0. # # :return: # """ # return (not self.exclusive and self.drive.is_unconfig_good() and # self.left_capacity_bytes > 0) @property def is_excludable(self): """whether this disk is excludable. - not exclusive by any other unshareable pending volume - firmware state is unconfig good - not used by other shareable pending volume :return: true if yes else false """ # return (not self.exclusive and self.drive.is_unconfig_good() # and len(self.pending_capacity_bytes) == 0) return not self.exclusive and self.drive.is_unconfig_good() # def add_pending_capacity_bytes(self, capacity_bytes): # self.pending_capacity_bytes.append(capacity_bytes) def mark_as_exclusive(self): self.exclusive = True def __repr__(self): return str(self) def __str__(self): return "Disk%d(%s)" % (self.drive_id, self.media_type) class PhysicalDiskGroup(object): """A model represents a real physical-disk group. upon a physical-disk group, logical disks is created. """ drives = None """sorted drive(:class:`~ibmc_client.resources.chassis.drive.Drive`) object list that this disk group uses """ raid_setting = None # type: Raid span_number = None overhead = None capacity_bytes = None used_capacity_bytes_list = None pending_capacity_bytes_list = None def __init__(self, drives, raid_setting, span_number): # type: (list[DRIVE.Drive], Raid, int) -> None # TODO update used size self.drives = sorted(drives, key=lambda drive: drive.capacity_bytes) self.raid_setting = raid_setting self.span_number = span_number self.overhead = raid_setting.get_overhead_per_span() * span_number self.capacity_bytes = (self.drives[0].capacity_bytes * (len(self.drives) - self.overhead)) self.pending_capacity_bytes_list = [] self.used_capacity_bytes_list = [] @staticmethod def from_volume(volume, all_drives): # type: (storage.Volume, list[DRIVE.Drive]) -> PhysicalDiskGroup raid_setting = RAID_SETTINGS.get(volume.raid_level) drives = sorted( [drive for drive in all_drives if drive.odata_id in volume.drive_odata_id_collection], key=lambda drive: drive.capacity_bytes) span_number = volume.span_number disk_group = PhysicalDiskGroup(drives, raid_setting, span_number) disk_group.add_used_capacity_bytes(volume.capacity_bytes) return disk_group @property def used_capacity_bytes(self): return sum(self.used_capacity_bytes_list) @property def pending_capacity_bytes(self): return sum(self.pending_capacity_bytes_list) @property def left_capacity_bytes(self): return (self.capacity_bytes - self.used_capacity_bytes - self.pending_capacity_bytes) def has_capacity_for(self, target_capacity): """check whether this disk group has enough left capacity for target capacity. :param target_capacity: -1 for 'max'; int for bytes; :return: true if yes else false """ if target_capacity == -1: return self.left_capacity_bytes > 0 return self.left_capacity_bytes >= target_capacity def add_pending_capacity_bytes(self, target_capacity): if self.has_capacity_for(target_capacity): if target_capacity == -1: self.pending_capacity_bytes_list.append( self.left_capacity_bytes) else: self.pending_capacity_bytes_list.append(target_capacity) def owns_volume(self, volume): # type: (storage.Volume) -> bool """check whether current disk group owns a volume base on whether any disk of the volume belongs to disk group too. :param volume: :return: true if owns else false """ return any([drive.odata_id == volume.drive_odata_id_collection[0] for drive in self.drives]) def validate_if_suitable_for(self, target_capacity, raid): # type: (int, Raid) -> None """validate whether current physical disk group is suitable for target capacity with raid setting. :param target_capacity: indicates the required capacity :param raid: indicates the target raid setting :raises exceptions.NotSuitablePhysicalDiskGroup when a disk group does not have enough capacity or it has a different raid-level than required. """ # validate whether disk group has enough shareable capacity matches = self.has_capacity_for(target_capacity) if not matches: raise exceptions.NotSuitablePhysicalDiskGroup( message='Those physical disks does not have enough capacity.') # validate whether disk group has same raid level if self.raid_setting.name != raid.name: message = ('Those shareable physical disks has raid-level %s, ' 'could not be used for required raid-level %s.' % (self.raid_setting.key, raid.key)) raise exceptions.NotSuitablePhysicalDiskGroup(message=message) def add_used_capacity_bytes(self, used_capacity_bytes): self.used_capacity_bytes_list.append(used_capacity_bytes) @property def drive_id_list(self): return [drive.id for drive in self.drives] def is_better_than(self, target_capacity, other): # type: (int, PhysicalDiskGroup) -> bool """compare to other PhysicalDiskGroup to check whether is a better choice. :param target_capacity: :param other: indicates the other physical disk group to compare :return: true if current physical disk group is a better choice else false """ if target_capacity > 0: return self.waste_less_than(other) if target_capacity == -1: return self.left_great_than(other) def waste_less_than(self, other): """waste as less disk capacity as better :param other: :return: """ if other is None: return True if self.left_capacity_bytes < other.left_capacity_bytes: return True if self.left_capacity_bytes > other.left_capacity_bytes: return False return False def left_great_than(self, other): """as much left capacity bytes as better :param other: :return: """ if other is None: return True if self.left_capacity_bytes > other.left_capacity_bytes: return True if self.left_capacity_bytes < other.left_capacity_bytes: return False return False # pragma: no cover def log(self, find): fmt_kwargs = {'disk_group': str(self), 'left': self.left_capacity_bytes} if find: LOG.info('Find a better choice:: disk-group->%(disk_group)s, ' 'left-capacity-bytes: %(left)d', fmt_kwargs) else: LOG.info('Not a better choice:: disk-group->%(disk_group)s, ' 'left-capacity-bytes: %(left)d', fmt_kwargs) def __repr__(self): # pragma: no cover return str(self) def __str__(self): return "PhysicalDiskGroup(%s-%s)" % (self.raid_setting.name, ','.join(self.drive_id_list)) class RaidSolution(object): """RAID solution summary """ span = None disks = [] # type: list[PhysicalDisk] disks_count = None disks_total_bytes = None disks_waste_bytes = None disks_min_bytes = None raid_total_bytes = None def __init__(self, span, disks, overhead): # type: (int, list[PhysicalDisk], int) -> None self.span = span self.disks = sorted(disks, key=lambda d: d.capacity_bytes) self.disks_total_bytes = sum((_.capacity_bytes for _ in disks)) self.disks_min_bytes = (disks[0].capacity_bytes if len(disks) else 0) self.disks_count = len(disks) effect_disk_count = self.disks_count - overhead self.raid_total_bytes = self.disks_min_bytes * effect_disk_count self.disks_waste_bytes = (self.disks_total_bytes - self.disks_min_bytes * self.disks_count) def is_better_than(self, target_capacity, other): """compare to other solution :param target_capacity: :param other: indicates the other solution to compare :return: true if current solution is better choice else false """ if target_capacity > 0: return self.waste_less_than(other) if target_capacity == -1: return self.raid_capacity_great_than(other) def waste_less_than(self, other): """waste as less disk capacity as better if waste space is same then use as small disk capacity as better. :param other: :return: """ if other is None: return True if self.disks_waste_bytes < other.disks_waste_bytes: return True if self.disks_waste_bytes > other.disks_waste_bytes: return False if self.disks_total_bytes < other.disks_total_bytes: return True if self.disks_total_bytes > other.disks_total_bytes: return False if self.disks_count < other.disks_count: # pragma: no cover return True if self.disks_count > other.disks_count: # pragma: no cover return False return False def raid_capacity_great_than(self, other): """as much RAID volume capacity as better if capacity is same, then waste as less as better. :param other: :return: """ if other is None: return True if self.raid_total_bytes > other.raid_total_bytes: return True if self.raid_total_bytes < other.raid_total_bytes: return False return self.waste_less_than(other) def log(self, find): fmt_kwargs = {'span': self.span, 'waste': self.disks_waste_bytes, 'used': self.disks_total_bytes, 'disks': self.disks, 'effect': self.raid_total_bytes} if find: LOG.info('Find a better choice:: span->%(span)d, ' 'total-waste-bytes->%(waste)d, ' 'used-disks-total-bytes->%(used)d, ' 'raid-volume-bytes->%(effect)d, ' 'disks->%(disks)s', fmt_kwargs) else: LOG.info('Not a better choice:: span->%(span)d, ' 'total-waste-bytes->%(waste)d, ' 'used-disks-total-bytes->%(used)d, ' 'raid-volume-bytes->%(effect)d, ' 'disks->%(disks)s', fmt_kwargs) class Raid(object): key = None name = None min_disks = None max_disks = None overhead = None raid_type = None level = None # when raid-type is spanned raid_level = None span = None def __init__(self, key, name, raid_type, level, min_disks=None, max_disks=None, overhead=None, raid_level=None, span=None): self.key = key self.name = name self.min_disks = min_disks self.max_disks = max_disks self.overhead = overhead self.raid_type = raid_type self.level = level # when raid-type is spanned self.raid_level = raid_level self.span = span def get_min_disks(self): """get min required disks for all possible situations :return: min required disks """ if not self.is_spanned: return self.min_disks else: sub_raid_setting = RAID_SETTINGS.get(self.raid_level) return sub_raid_setting.min_disks * 2 def get_overhead_per_span(self): """get overhead for every span :return: """ if not self.is_spanned: return self.overhead else: sub_raid_setting = RAID_SETTINGS.get(self.raid_level) return sub_raid_setting.overhead @property def is_spanned(self): return self.raid_type == RAID_TYPE_SPANNED def get_best_matched_disk_group(self, target_capacity, physical_disk_groups): # type: (int, list[PhysicalDiskGroup]) -> PhysicalDiskGroup """get best matched physical disk group using waste least capacity strategy. :param target_capacity: indicates target required capacity :param physical_disk_groups: indicates available physical disk group list :return: """ LOG.info('Try to get best matched disk-group for ' 'volume(%(raid_level)s) with target capacity %(capacity)d ' 'using waste least strategy', {'raid_level': self.name, 'capacity': target_capacity}) best_choice = None for disk_group in physical_disk_groups: try: disk_group.validate_if_suitable_for(target_capacity, self) except exceptions.NotSuitablePhysicalDiskGroup as e: LOG.info("%(disk_group)s is not a choice:: %(reason)s", {'disk_group': str(disk_group), 'reason': str(e)}) pass else: better = disk_group.is_better_than(target_capacity, best_choice) disk_group.log(better) if better: best_choice = disk_group return best_choice def get_best_matched_disks(self, target_capacity, available_disks, disk_count_to_use): # type: (int, list[PhysicalDisk], int) -> RaidSolution """get best matched disks for target capacity size with current raid. :param target_capacity: target capacity :param available_disks: a list available physical disk :param disk_count_to_use: disk count to use if not None else auto choose :return: """ if self.name == JBOD: # pragma: no cover return None LOG.info('Calculate waste least disks for volume(%(raid_level)s) with' ' target capacity %(capacity)d, available disks: %(disks)s.', {'raid_level': self.name, 'capacity': target_capacity, 'disks': available_disks}) raid = RAID_SETTINGS.get(self.raid_level) if self.is_spanned else self available_span_list = [1] if not self.is_spanned else list(range(2, 9)) grouped_by_media_type = collections.defaultdict(list) for disk in available_disks: grouped_by_media_type[disk.media_type].append(disk) LOG.info('Group available disks by media type: %(disks)s', {'disks': str(grouped_by_media_type)}) is_specified_disk_count_legal = disk_count_to_use is None best_solution = None for (media_type, disks_by_media_type) in grouped_by_media_type.items(): LOG.info('Try to calculate for media type `%(media_type)s` now.', {'media_type': media_type}) for span in available_span_list: if disk_count_to_use: if disk_count_to_use % span != 0: LOG.info( 'Specified disk count number `%(disk_count)d` does' ' not match span number %(span)d, continue.', {'span': span, 'disk_count': disk_count_to_use}) continue disk_count_to_use_per_span = disk_count_to_use / span if (disk_count_to_use_per_span < raid.min_disks or disk_count_to_use_per_span > raid.max_disks): LOG.info( 'Specified disk count number `%(disk_count)d` ' 'does not match raid-level %(raid)s with ' 'span %(span)d, continue.', {'span': span, 'disk_count': disk_count_to_use, 'raid': self.key}) continue is_specified_disk_count_legal = True min_disks = (disk_count_to_use if disk_count_to_use else raid.min_disks * span) max_disks = (disk_count_to_use if disk_count_to_use else raid.max_disks * span) overhead = raid.overhead * span if min_disks > len(disks_by_media_type): LOG.info('Disk count(%(disk_count)d) is less than ' 'min-disks(%(min_disks)d), break current branch.', {'disk_count': len(disks_by_media_type), 'min_disks': min_disks}) break max_disk_count = min(max_disks, len(disks_by_media_type)) for required_disk_count in range( min_disks, max_disk_count + 1, span): LOG.info('Calculate for span:: %(span)d, disk-count:: %(' 'disk_count)d.', {'span': span, 'disk_count': required_disk_count}) if required_disk_count % span != 0: # pragma: no cover LOG.info( 'Disk count %(disk_count)d does not match span ' 'number %(span)d, continue.', {'span': span, 'disk_count': required_disk_count}) continue required_capacity = math.ceil( target_capacity / (required_disk_count - overhead)) matched_disks = [_ for _ in disks_by_media_type if _.capacity_bytes >= required_capacity] if len(matched_disks) < required_disk_count: LOG.info('Not enough disks has required capacity ' '%(required_capacity)d, required %(' 'disk_count)d actual %(actual)d.', {'required_capacity': required_capacity, 'disk_count': required_disk_count, 'actual': len(matched_disks)}) continue # sort matched disks sorted_matched_disks = sorted( matched_disks, key=lambda d: d.capacity_bytes) cases = len(matched_disks) - required_disk_count + 1 for start in range(0, cases): end = start + required_disk_count possible_disks = sorted_matched_disks[start:end] solution = RaidSolution(span, possible_disks, overhead) better = solution.is_better_than(target_capacity, best_solution) solution.log(better) if better: best_solution = solution """ In waste less scene:: if all disk capacity is gt than min-required capacity. it means all disks will always in possible disks later. then the greater disk-count-per-span is, waste the more. """ if (len(matched_disks) == len(disks_by_media_type) and target_capacity > 0): break if not is_specified_disk_count_legal: raise exceptions.InvalidPhysicalDiskNumber( number_of_physical_disks=disk_count_to_use, raid=self.key) return best_solution RAID_SETTINGS = { 'JBOD': Raid(**{ 'key': 'JBOD', 'name': JBOD, 'min_disks': 1, 'max_disks': 1024, 'raid_type': RAID_TYPE_RAW_DEVICE, 'overhead': 0, 'level': -1 }), '0': Raid(**{ 'key': '0', 'name': RAID0, 'min_disks': 1, 'max_disks': 1024, 'raid_type': RAID_TYPE_NON_REDUNDANT, 'overhead': 0, 'level': 0 }), '1': Raid(**{ 'key': '1', 'name': RAID1, 'min_disks': 2, 'max_disks': 2, 'raid_type': RAID_TYPE_MIRRORED, 'overhead': 1, 'level': 1 }), '5': Raid(**{ 'key': '5', 'name': RAID5, 'min_disks': 3, 'max_disks': 1024, 'raid_type': RAID_TYPE_STRIPED_WITH_PARITY, 'overhead': 1, 'level': 5 }), '6': Raid(**{ 'key': '6', 'name': RAID6, 'min_disks': 3, 'max_disks': 1024, 'raid_type': RAID_TYPE_STRIPED_WITH_PARITY, 'overhead': 2, 'level': 6 }), '1+0': Raid(**{ 'key': '1+0', 'name': RAID10, 'raid_type': RAID_TYPE_SPANNED, 'raid_level': '1', 'span': lambda disk_count: disk_count >> 1, 'level': 10 }), '5+0': Raid(**{ 'key': '5+0', 'name': RAID50, 'raid_type': RAID_TYPE_SPANNED, 'raid_level': '5', 'span': 2, 'level': 50 }), '6+0': Raid(**{ 'key': '6+0', 'name': RAID60, 'raid_type': RAID_TYPE_SPANNED, 'raid_level': '6', 'span': 2, 'level': 60 }) } # add local raid name mapping for k in list(RAID_SETTINGS): RAID_SETTINGS[RAID_SETTINGS[k].name] = RAID_SETTINGS[k] """RAID settings""" python-ibmcclient-0.2.5.1/ibmc_client/resources/0000777000000000000000000000000013677000135017723 5ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/resources/__init__.py0000666000000000000000000000724013665701342022044 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.2 from pprint import pformat # Resource Property keys import ibmc_client as ibmc from ibmc_client import constants PROP_RESOURCE_ID = '@odata.id' """resource(Odata) Id of redfish resource""" PROP_COLLECTION_MEMBER_COUNT = 'Members@odata.count' """collection resource member count""" class BaseResource(object): """iBMC Resource Base Model""" _resp = None _json = None _oem = None etag = None def __init__(self, resp, ibmc_client=None): # type: (dict, ibmc.IBMCClient) -> None """Initial a iBMC resource :param resp: redfish resource HTTP response from redfish API :param ibmc_client: a reference to global :class:`~ibmc_client.IBMCClient` object """ self._ibmc_client = ibmc_client self._connector = self._ibmc_client.connector if ibmc_client else None self.refresh(resp) def extra_init_action(self): """Per Resource customer init action :return: """ pass def refresh(self, resp): self._resp = resp self._json = resp.json() self._oem = (self._json['Oem']['Huawei'] if self._json.get('Oem') else None) self.etag = resp.headers.get(constants.HEADER_ETAG) self.extra_init_action() @property def odata_id(self): """get odata id of current resource :return: odata id of current resource """ return self._json.get(PROP_RESOURCE_ID) def get_action_uri(self, action_name): actions = self._json.get('Actions', None) _action_name = '#' + action_name if _action_name in actions: return actions[_action_name]['target'] elif actions.get('Oem', None): actions = actions['Oem']['Huawei'] if _action_name in actions: return actions[_action_name]['target'] return None # pragma: no cover def to_str(self): """ Returns the string representation of the model """ return pformat(self._json) # pragma: no cover def __repr__(self): """ For `print` and `pprint` """ return self.to_str() class CollectionResource(BaseResource): """iBMC System Resource Model""" @property def count(self): """get all resource odata id of a collection :return: """ return self._json.get(PROP_COLLECTION_MEMBER_COUNT, 0) @property def resources(self): """get all resource odata id of a collection :return: """ return [member.get(PROP_RESOURCE_ID) for member in self._json.get('Members', [])] class Status(object): """iBMC Resource Status Model""" def __init__(self, status): self._status = status @property def state(self): return self._status.get('State', None) @property def health(self): return self._status.get('Health', None) python-ibmcclient-0.2.5.1/ibmc_client/resources/chassis/0000777000000000000000000000000013677000135021360 5ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/resources/chassis/__init__.py0000666000000000000000000000233513657260067023506 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 import logging from ibmc_client.resources import BaseResource, PROP_RESOURCE_ID LOG = logging.getLogger(__name__) class Chassis(BaseResource): """iBMC Chassis Resource Model""" @property def drives(self): _drives = [] drive_links = self._json.get('Links', {}).get('Drives', []) for drive_link in drive_links: url = drive_link.get(PROP_RESOURCE_ID) drive_id = url.split('/')[-1] drive = self._ibmc_client.chassis.drive.get(drive_id) _drives.append(drive) return _drives python-ibmcclient-0.2.5.1/ibmc_client/resources/chassis/drive.py0000666000000000000000000001317413661154734023061 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 import logging from ibmc_client import utils, constants from ibmc_client.resources import BaseResource, Status, PROP_RESOURCE_ID LOG = logging.getLogger(__name__) class Drive(BaseResource): """iBMC chassis Drive Resource Model""" @property def drive_id(self): """get Oem drive id :return: drive id """ return self._oem.get('DriveID', None) @property def id(self): """get drive id :return: drive id """ return self._json.get('Id', None) @property def name(self): """get drive name :return: drive name """ return self._json.get('Name', None) @property def model(self): """get drive model :return: drive model """ return self._json.get('Model', None) @property def protocol(self): """get drive protocol typical protocol: SATA, SAS, SCSI. :return: drive protocol """ return self._json.get('Protocol', None) @property def media_type(self): """get drive Media Type typical media types: HDD, SSD :return: drive Media Type """ return self._json.get('MediaType', None) @property def manufacturer(self): """get drive Manufacturer :return: drive Manufacturer """ return self._json.get('Manufacturer', None) @property def serial_number(self): """get drive serial number :return: drive serial number """ return self._json.get('SerialNumber', None) @property def status(self): """get drive status :return: drive status """ return Status(self._json.get('Status', {})) @property def firmware_state(self): """get drive firmware state :return: drive firmware state """ return self._oem.get('FirmwareStatus', None) @property def hotspare_type(self): """get drive hot spare type :return: drive hot spare type """ return self._json.get('HotspareType', None) @property def capacity_bytes(self): """get drive capacity bytes :return: drive capacity bytes """ return self._json.get('CapacityBytes', None) @property def volume_odata_id_collection(self): """get volume odata id collection which this drive belongs to :return: volume odata id collection """ odata_collection = self._json.get('Links', {}).get('Volumes', []) return [odata.get(PROP_RESOURCE_ID) for odata in odata_collection] def has_fm_state(self, state): return self.firmware_state == state def is_unconfig_good(self): return self.has_fm_state(constants.DRIVE_FM_STATE_UNCONFIG_GOOD) def matches(self, hint, media_type=None, protocol=None): hint_matches = hint and hint in ( self.id, self.name, self.serial_number, str(self.drive_id)) media_matched = not media_type or ( media_type.lower() == self.media_type.lower()) protocol_matches = not protocol or ( protocol.lower() == self.protocol.lower()) return hint_matches and media_matched and protocol_matches def set(self, firmware_state=None, hotspare_type=None): """update drive :param firmware_state: indicates firmware state to update :param hotspare_type: indicates hotspare type to update :return: """ oem = ({"Huawei": {"FirmwareStatus": firmware_state}} if firmware_state else None) payload = utils.remove_empty_from_dict({ "HotspareType": hotspare_type, "Oem": oem }) if payload: resp = self._connector.request(constants.PATCH, self.odata_id, json=payload, etag=self.etag) self.refresh(resp) def restore(self): """restore drive, include operations: - [o] set hot-spare type to None if current state is HotSpareDrive - [x] (deprecated) set disk state to UnconfiguredGood if current state is JBOD :return: """ LOG.info('Start to restore drive %s.', self.id) settings = {} if self.firmware_state == constants.DRIVE_FM_STATE_HOTSPARE: settings['hotspare_type'] = constants.HOT_SPARE_NONE # do not need to restore driver status for JBOD # if self.firmware_state == constants.DRIVE_FM_STATE_JBOD: # settings['firmware_state'] = # constants.DRIVE_FM_STATE_UNCONFIG_GOOD if settings: self.set(**settings) logging.info('drive %s has been restored, restore settings:: %s.', self.id, str(settings)) else: LOG.info('drive %s has nothing to restore.', self.id) python-ibmcclient-0.2.5.1/ibmc_client/resources/system/0000777000000000000000000000000013677000135021247 5ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/resources/system/__init__.py0000666000000000000000000000344513676773266023413 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.2 import logging from ibmc_client import exceptions from ibmc_client.resources import BaseResource from ibmc_client.resources.system.boot_source_override import BootSourceOverride LOG = logging.getLogger(__name__) class System(BaseResource): """iBMC System Resource Model""" @property def power_state(self): return self._json['PowerState'] @property def boot_source_override(self): _boot = self._json['Boot'] return BootSourceOverride(_boot) @property def boot_sequence(self): if self._json.get('Bios', None): # v5 series server return self._ibmc_client.system.bios.get().boot_sequence else: # V3 series server _seq = self._oem['BootupSequence'] return _seq @property def is_storage_ready(self): key = 'StorageConfigReady' # only supported in latest ibmc if key in self._oem: return self._oem['StorageConfigReady'] == 1 else: feature = 'get StorageConfigReady attribute from System Resource' raise exceptions.FeatureNotSupported(feature=feature) python-ibmcclient-0.2.5.1/ibmc_client/resources/system/bios.py0000666000000000000000000000311113657260067022563 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.2 from ibmc_client import constants from ibmc_client.resources import BaseResource _BOOT_SEQUENCE_MAP = { 'HardDiskDrive': constants.BOOT_SOURCE_TARGET_HDD, 'DVDROMDrive': constants.BOOT_SOURCE_TARGET_CD, 'PXE': constants.BOOT_SOURCE_TARGET_PXE, } class Bios(BaseResource): """iBMC System Resource Model""" def extra_init_action(self): self._attrs = self._json['Attributes'] def __init__(self, resp, ibmc_client=None): """Initial a iBMC System BIOS resource :param resp: bios attribute resource HTTP response """ super(Bios, self).__init__(resp, ibmc_client=ibmc_client) @property def boot_sequence(self): # v5 series server keys = [k for k in self._attrs.keys() if k.startswith('BootTypeOrder')] seq = [self._attrs.get(t) for t in sorted(keys)] return [_BOOT_SEQUENCE_MAP.get(t, t) for t in seq] python-ibmcclient-0.2.5.1/ibmc_client/resources/system/boot_source_override.py0000666000000000000000000000242013657260067026053 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.2 class BootSourceOverride(object): """iBMC Boot Source Override Resource Model""" def __init__(self, json): """Initial a iBMC BootSourceOverride resource """ self._json = json @property def target(self): return self._json['BootSourceOverrideTarget'] @property def enabled(self): return self._json['BootSourceOverrideEnabled'] @property def mode(self): return self._json['BootSourceOverrideMode'] @property def supported_boot_devices(self): return self._json['BootSourceOverrideTarget@Redfish.AllowableValues'] python-ibmcclient-0.2.5.1/ibmc_client/resources/system/storage.py0000666000000000000000000002463213665701254023303 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 import logging from time import sleep from ibmc_client import utils, exceptions, constants from ibmc_client.resources import BaseResource, Status, PROP_RESOURCE_ID from ibmc_client.resources.chassis.drive import Drive LOG = logging.getLogger(__name__) class Storage(BaseResource): """iBMC System storage controller Resource Model""" ACTION_RESTORE = 'Storage.RestoreStorageControllerDefaultSettings' _drives = None _volumes = None _controller = None def extra_init_action(self): self._drives = None self._controller = self._json['StorageControllers'][0] self._oem = (self._controller['Oem']['Huawei'] if self._controller.get('Oem') else None) @property def id(self): """get Storage id :return: Storage id """ return self._json.get('Id', None) @property def name(self): """get Storage name :return: Storage name """ return self._json.get('Name', None) @property def controller_name(self): """get storage controller name :return: storage controller name """ return self._controller.get('Name', None) @property def model(self): """get storage controller model :return: storage controller model """ return self._controller.get('Model', None) @property def supported_raid_levels(self): """get storage controller Supported RAID Levels :return: storage controller Supported RAID Levels """ return self._oem.get('SupportedRAIDLevels', []) @property def support_oob(self): """get whether storage controller support OOB TODO(turnbig) compatibility between iBMC versions :return: true if support else false """ return self._oem.get('OOBSupport', False) @property def is_jbod_mode(self): """get whether storage controller is in jbod mode :return: true if jbod mode else false """ return self._oem.get('JBODState', False) @property def status(self): """get storage controller status :return: storage controller status """ return Status(self._controller.get('Status', {})) def drives(self, force_reload=False): # type: (bool) -> list[Drive] """get physical drives of this controller :return: physical drives """ # cache physical drives if not force_reload and self._drives: return self._drives drive_collection = self._json.get('Drives', []) self._drives = [self._ibmc_client.load_odata(_, Drive) for _ in drive_collection] return self._drives def volumes(self, force_reload=False): # type: (bool) -> list[Volume] """get logical volumes of this controller :return: logical volumes """ # cache physical drives if not force_reload and self._volumes: return self._volumes volume_collection_odata_id = self._json.get('Volumes') self._volumes = self._ibmc_client.load_odata_collection( volume_collection_odata_id, Volume) return self._volumes def summary(self): return { "Id": self.id, "Name": self.name, "ControllerName": self.controller_name, "Model": self.model, "SupportedRAIDLevels": self.supported_raid_levels, "OOBSupport": self.support_oob, "JBOD": self.is_jbod_mode, "PhysicalDisks": [ {"Id": drive.id, "Name": drive.name, "DriveId": drive.drive_id, "SerialNumber": drive.serial_number, "FirmwareStatus": drive.firmware_state, "CapacityBytes": utils.human_readable_byte( drive.capacity_bytes)} for drive in self.drives() ], "LogicalDisks": [ {"Id": volume.id, "Name": volume.name, "VolumeName": volume.volume_oem_name, "RaidLevel": volume.raid_level, "SpanNumber": volume.span_number, "Bootable": volume.bootable, "CapacityBytes": utils.human_readable_byte( volume.capacity_bytes), "PhysicalDisks": [ odata_id.split('/')[-1] for odata_id in volume.drive_odata_id_collection ]} for volume in self.volumes() ] } def matches(self, hint): """Check whether current storage matches the hint Notes:: if the hint is None or empty, it matches any controller. :param hint: a string could be storage id, storage name, storage controller name :return: true if matches else false """ return hint and hint in (self.id, self.name, self.controller_name) def set(self, copy_back=None, smarter_copy_back=None, jbod=None): """apply settings to storage :param copy_back: :bool: Indicates Whether copy back is enabled. :param smarter_copy_back: Indicates Whether SMART error copy back is enable. Before enabling this function, enable CopyBack first. :param jbod: Indicates Whether JBOD is enable. :return: A storage controller(:class:`~ibmc_client.resources.system .storage.Storage`) object """ settings = utils.remove_empty_from_dict({ "CopyBackState": copy_back, "SmarterCopyBackState": smarter_copy_back, "JBODState": jbod }) if not settings: raise exceptions.NothingToApplyError() payload = { "StorageControllers": [ { "Oem": { "Huawei": settings } } ] } resp = self._connector.request(constants.PATCH, self.odata_id, json=payload, etag=self.etag) self.refresh(resp) def restore(self): """restore RAID storage """ restore_url = self.get_action_uri(self.ACTION_RESTORE) self._connector.request(constants.POST, restore_url, json={}) def delete_volume_collection(self): """delete volume collection of a storage :return: """ LOG.info("Start delete volumes for storage:: %s.", self.id) volume_collection_url = self._json.get('Volumes', {}).get( PROP_RESOURCE_ID) volume_collection = self._ibmc_client.load_collection_resource( volume_collection_url) for volume_odata_id in volume_collection.resources: LOG.info("Start delete volume:: %s.", volume_odata_id) task = self._ibmc_client.system.volume.delete_by_odata_id( volume_odata_id) task.raise_if_failed() LOG.info("Delete volume:: %s done.", volume_odata_id) if volume_collection.count == 0: # pragma: no cover LOG.info("No volume present in this storage:: %s", self.id) else: # sleep some seconds to make sure the deletion has completely # take effect. sleep(constants.RAID_TASK_EFFECT_SECONDS) LOG.info("Delete volumes for storage:: %s done.", self.id) class Volume(BaseResource): """iBMC system volume Resource Model""" @property def id(self): """get volume id :return: volume id """ return self._json.get('Id', None) @property def name(self): """get volume name :return: volume name """ return self._json.get('Name', None) @property def status(self): """get volume status :return: volume status """ return Status(self._json.get('Status', {})) @property def capacity_bytes(self): """get volume capacity bytes :return: volume capacity bytes """ return self._json.get('CapacityBytes', None) @property def volume_oem_name(self): """get volume OEM name :return: volume OEM name """ return self._oem.get('VolumeName', None) @property def raid_level(self): """get volume raid level :return: volume raid level """ return self._oem.get('VolumeRaidLevel', None) @property def span_number(self): """get volume span number :return: volume span number """ return self._oem.get('SpanNumber', None) @property def drive_number_per_span(self): """get volume drive number per span :return: volume drive number per span """ return self._oem.get('NumDrivePerSpan', None) @property def bootable(self): """get whether volume bootable or not :return: true if bootable else false default None """ return self._oem.get('BootEnable', None) @property def bgi_enabled(self): """get whether volume BGI is enabled :return: true if enabled else false default None """ return self._oem.get('BGIEnable', None) @property def drive_odata_id_collection(self): """get drive odata id collection that belongs to this volume :return: drive odata id collection (list[str]) """ odata_collection = self._json.get('Links', {}).get('Drives', []) return [odata.get(PROP_RESOURCE_ID) for odata in odata_collection] python-ibmcclient-0.2.5.1/ibmc_client/resources/task/0000777000000000000000000000000013677000135020665 5ustar 00000000000000python-ibmcclient-0.2.5.1/ibmc_client/resources/task/__init__.py0000666000000000000000000001055313657260067023014 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 from ibmc_client.constants import TASK_STATUS_FAILED from ibmc_client.exceptions import TaskFailed from ibmc_client.resources import BaseResource class Task(BaseResource): """iBMC System storage controller Resource Model""" def raise_if_failed(self): if self.state in TASK_STATUS_FAILED: raise TaskFailed(message=self.friendly_failed_message) @property def id(self): """get task id :return: task id """ return self._json.get('Id', None) @property def name(self): """get task name :return: task name """ return self._json.get('Name', None) @property def state(self): """get task state :return: task state """ return self._json.get('TaskState', None) @property def start_time(self): """get task start time :return: task start time """ return self._json.get('StartTime', None) @property def end_time(self): """get task end time :return: task end time """ return self._json.get('EndTime', None) @property def percentage(self): """get task progress percentage :return: task progress percentage """ return self._oem.get('TaskPercentage', None) @property def messages(self): """get task progress result messages :return: task progress result messages default {} if empty """ messages = self._json.get('Messages', {}) # when messages is empty, ibmc will return []. # if type(messages) is list: # return {} if type(messages) is dict: return messages return {} @property def message_id(self): """get task progress result message id :return: task progress result message id """ return self.messages.get('MessageId', None) @property def message(self): """get task progress result message :return: task progress result message """ return self.messages.get('Message', None) @property def message_args(self): """get task progress result message args :return: task progress result message args """ return self.messages.get('MessageArgs', None) @property def resolution(self): """get task progress result resolution suggest :return: task progress result resolution suggest """ return self.messages.get('Resolution', None) @property def severity(self): """get task progress result severity :return: task progress result severity """ return self.messages.get('Severity', None) @property def friendly_failed_message(self): """get task progress failed description :return: task progress failed description """ display = ("[%(severity)s] Task(%(name)s)'s final state is %(state)s. " "Reason:: '%(message)s' Resolution:: '%(resolution)s'") return display % {'severity': self.severity, 'name': self.name, 'state': self.state, 'message': self.message, 'resolution': self.resolution} def to_str(self): """ Returns the string representation of the model """ return ("Task[id=%(id)s, Name=%(name)s, status=%(status)s, percent=" "%(percentage)s], start-time=%(start_time)s]" % {'id': self.id, 'name': self.name, 'status': self.state, 'percentage': self.percentage, 'start_time': self.start_time}) python-ibmcclient-0.2.5.1/ibmc_client/utils.py0000666000000000000000000000326113660727242017434 0ustar 00000000000000# Copyright 2019 HUAWEI, Inc. 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. # Version 0.0.3 def remove_empty_from_dict(original): """get a new dict which removes keys with empty value :param dict original: original dict, should not be None :return: a new dict which removes keys with empty values """ return dict((k, v) for k, v in original.items() if v is not None and v != '' and v != [] and v != {}) def str2bool(v): """str bool value to python Boolean :param v: :return: """ return v.lower() in ("yes", "true", "t", "1") def human_readable_byte(size_in_byte, suffix='B'): # type: (int, str) -> str """convert int size in byte to human readable size with unit. :param size_in_byte: indicates size in bytes :param suffix: suffix append to size unit :return: human readable size """ for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: if abs(size_in_byte) < 1024.0: return "%3.1f%s%s" % (size_in_byte, unit, suffix) size_in_byte /= 1024.0 return "%.1f%s%s" % (size_in_byte, 'Y', suffix) python-ibmcclient-0.2.5.1/python_ibmcclient.egg-info/0000777000000000000000000000000013677000135020645 5ustar 00000000000000python-ibmcclient-0.2.5.1/python_ibmcclient.egg-info/PKG-INFO0000666000000000000000000001010113677000135021733 0ustar 00000000000000Metadata-Version: 2.1 Name: python-ibmcclient Version: 0.2.5.1 Summary: HUAWEI iBMC client Home-page: https://github.com/IamFive/python-ibmcclient Author: QianBiao NG Author-email: iampurse@vip.qq.com License: UNKNOWN Project-URL: Bug Reports, https://github.com/IamFive/python-ibmcclient/issues Project-URL: Source, https://github.com/IamFive/python-ibmcclient Description: ================= python-ibmcclient ================= .. image:: https://travis-ci.org/IamFive/python-ibmcclient.svg?branch=master :target: https://travis-ci.org/IamFive/python-ibmcclient python-ibmcclient is a Python library to communicate with HUAWEI `iBMC` based systems. The goal of the library is to be extremely simple, small, have as few dependencies as possible and be very conservative when dealing with BMCs by access HTTP REST API provided by HUAWEI `iBMC` based systems. Currently, the scope of the library has been limited to supporting `OpenStack Ironic ibmc driver`_ Requirements ============ Python 2.7 and 3.4+ Installation ------------ From PyPi: .. code-block:: bash $ pip install python-ibmcclient or .. code-block:: bash $ easy_install python-ibmcclient Or from source: .. code-block:: bash $ python setup.py install Getting Started --------------- Please follow the `Installation`_ and then run the following: .. code-block:: python from __future__ import print_function from pprint import pprint import ibmc_client from ibmc_client import constants # ibmc server address = "https://example.ibmc.com" # credential username = "username" password = "password" # disable certification verify verify = False with ibmc_client.connect(address, username, password, verify) as client: # get system system = client.system.get() print('Power State: ') pprint(system.power_state) print('Boot Sequence: ') pprint(system.boot_sequence) print('Boot Source Override:' ) pprint(system.boot_source_override) # reset system client.system.reset(constants.RESET_FORCE_RESTART) # set boot source override client.system.set_boot_source(constants.BOOT_SOURCE_TARGET_PXE, constants.BOOT_SOURCE_MODE_BIOS, constants.BOOT_SOURCE_ENABLED_ONCE) .. _OpenStack Ironic ibmc driver: https://github.com/openstack/ironic-specs/blob/master/specs/approved/ibmc-driver.rst Keywords: HUAWEI iBMC redfish API client Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Description-Content-Type: text/x-rst Provides-Extra: dev Provides-Extra: test python-ibmcclient-0.2.5.1/python_ibmcclient.egg-info/SOURCES.txt0000666000000000000000000000207713677000135022537 0ustar 00000000000000CHANGELOG.md LICENSE.txt MANIFEST.in README.rst requirements.txt setup.cfg setup.py ibmc_client/__init__.py ibmc_client/connector.py ibmc_client/constants.py ibmc_client/exceptions.py ibmc_client/raid_config_schema.json ibmc_client/raid_utils.py ibmc_client/utils.py ibmc_client/api/__init__.py ibmc_client/api/chassis/__init__.py ibmc_client/api/chassis/drive.py ibmc_client/api/system/__init__.py ibmc_client/api/system/bios.py ibmc_client/api/system/storage.py ibmc_client/api/system/volume.py ibmc_client/api/task/__init__.py ibmc_client/api/task/task.py ibmc_client/resources/__init__.py ibmc_client/resources/chassis/__init__.py ibmc_client/resources/chassis/drive.py ibmc_client/resources/system/__init__.py ibmc_client/resources/system/bios.py ibmc_client/resources/system/boot_source_override.py ibmc_client/resources/system/storage.py ibmc_client/resources/task/__init__.py python_ibmcclient.egg-info/PKG-INFO python_ibmcclient.egg-info/SOURCES.txt python_ibmcclient.egg-info/dependency_links.txt python_ibmcclient.egg-info/requires.txt python_ibmcclient.egg-info/top_level.txtpython-ibmcclient-0.2.5.1/python_ibmcclient.egg-info/dependency_links.txt0000666000000000000000000000000113677000135024713 0ustar 00000000000000 python-ibmcclient-0.2.5.1/python_ibmcclient.egg-info/requires.txt0000666000000000000000000000015513677000135023246 0ustar 00000000000000requests>=2.14.2 six>=1.10.0 [:python_version < "3.5"] typing [dev] check-manifest flake8 [test] coverage python-ibmcclient-0.2.5.1/python_ibmcclient.egg-info/top_level.txt0000666000000000000000000000002213677000135023371 0ustar 00000000000000ibmc_client tests python-ibmcclient-0.2.5.1/requirements.txt0000666000000000000000000000012213657463625016737 0ustar 00000000000000requests>=2.14.2 # Apache-2.0 six>=1.10.0 # MIT typing; python_version < "3.5" python-ibmcclient-0.2.5.1/setup.cfg0000666000000000000000000000016513677000136015265 0ustar 00000000000000[metadata] license_files = LICENSE.txt [bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 python-ibmcclient-0.2.5.1/setup.py0000666000000000000000000000444313677000066015163 0ustar 00000000000000# io.open is needed for projects that support Python 2.7 # It ensures open() defaults to text mode with universal newlines, # and accepts an argument to specify the text encoding # Python 3 only projects can skip this import from io import open from os import path from setuptools import setup, find_packages __version__ = '0.2.5.1' here = path.abspath(path.dirname(__file__)) with open(path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() with open(path.join(here, 'requirements.txt'), encoding='utf-8') as f: requires = f.read().splitlines() setup( name='python-ibmcclient', version=__version__, description='HUAWEI iBMC client', long_description=long_description, long_description_content_type='text/x-rst', url='https://github.com/IamFive/python-ibmcclient', author='QianBiao NG', author_email='iampurse@vip.qq.com', classifiers=[ # Optional # How mature is this project? Common values are # 3 - Alpha # 4 - Beta # 5 - Production/Stable 'Development Status :: 4 - Beta', # Indicate who your project is intended for 'Intended Audience :: Developers', "Intended Audience :: System Administrators", "Operating System :: OS Independent", 'Topic :: Software Development :: Libraries :: Python Modules', 'License :: OSI Approved :: Apache Software License', "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", ], keywords='HUAWEI iBMC redfish API client', packages=find_packages(exclude=['contrib', 'docs', 'tests']), # Required install_requires=requires, extras_require={ # Optional 'dev': ['check-manifest', 'flake8'], 'test': ['coverage'], }, project_urls={ # Optional 'Bug Reports': 'https://github.com/IamFive/python-ibmcclient/issues', 'Source': 'https://github.com/IamFive/python-ibmcclient', }, )