IBM Cloud: SoftLayer APIによるベアメタルサーバ管理

# はじめに
IBM Cloud ClassicのAPI機能を利用して、Classic環境におけるベアメタルサーバの管理をご紹介します。
多数のベアメタルサーバを管理していると定期的に発生するファームウェアバージョンアップ等の負担が大きいかと思います。本機能にて、ベアメタルサーバのバージョン確認及びそれらのアップデート操作をAPI等で実現する事が可能となります。
また、これらAPI機能を利用する事で、カスマイズした運用管理ツールに組み込み自身の運用に最適化した環境も構築する事が可能となります。
# 本稿で利用する環境
+ ベアメタルサーバ：IBM Classic環境にて払い出されたベアメタルサーバ
+ SoftLayer API インタフェース：Classic（旧SoftLayer）環境のAPI管理機能
+ Pythonスクリプト：SoftLayer APIサイトで公開されている「Hardware components and versions」を利用
# SoftLayer APIについて
最初に[APIリファレンス概要](https://cloud.ibm.com/docs/virtual-servers?topic=virtual-servers-api-reference&locale=ja)をご確認下さい。また、[SoftLayer API Getting Started](https://sldn.softlayer.com/article/getting-started/)にて動画説明やスライド資料が掲載されておりますので、SoftLayer APIの理解にご活用下さい。
SoftLayer APIにて実現できる機能は、こちらの[SoftLayer API Reference](https://sldn.softlayer.com/reference/softlayerapi/)にて検索、確認する事ができます。本Referenceに記載されているServiceとData Typeに関しては、少し分かりづらいので以下のQiita三部作記事をご参考下さい。
+ [ShinobiLayer: SoftLayer API 次の一歩: データ型オブジェクト(1)](https://qiita.com/testnin2/items/f04ca0cee9b2f0a8f830)
+ [ShinobiLayer: SoftLayer API 次の一歩: データ型オブジェクト(2)](https://qiita.com/testnin2/items/6be9b87571a99e01002a)
+ [ShinobiLayer: SoftLayer API 次の一歩: methodの実行方法と返り値の制御(1)](https://qiita.com/testnin2/items/92ea8f937c70c3a7e254)
# Pythonスクリプトについて
SoftLayer APIは、プロトコルとしてSOAP,XML-RPC,RESTをサポートしております。今回は、Pythonのスクリプトを利用して、XML-RPCを利用します。
こちらに[Pythonのサンプルスクリプト](https://sldn.softlayer.com/python/)が掲載されてます。今回は、ベアメタルサーバのハードウェア情報を収集表示、ファームウェアのアップデートを行いたいので、[Hardware components and versions](https://sldn.softlayer.com/python/hardware_components.py/)を利用します。以下、Pythonファイルとなります。
```:ch_hw_info.py
import SoftLayer
import click
from prettytable import PrettyTable


class HardwareComponents():

    def __init__(self):
        self.client = SoftLayer.create_client_from_env()

    def hardware_information(self, hostname):
        """
        Get the Hardware Server Information.
        """
        mask = 'mask[hardwareStatus,lastOperatingSystemReload,datacenter,billingItem[hourlyFlag],' \
               'lastTransaction[transactionGroup],processorPhysicalCoreAmount,memoryCapacity,' \
               'operatingSystem[softwareLicense[softwareDescription]]]'
        hardware_id = self.get_hardware_detail(hostname).get('id')
        result = self.client['SoftLayer_Hardware_Server'].getObject(id=hardware_id, mask=mask)

        table = PrettyTable()
        table.title = "Hardware Server details"
        table.field_names = ['name', 'value']
        table.align['name'] = 'r'
        table.align['value'] = 'l'

        table.add_row(['Name', result['fullyQualifiedDomainName']])
        table.add_row(['Status', result['hardwareStatus']['status']])
        table.add_row(['Id', result['id']])
        table.add_row(['Started', result['provisionDate']])
    #    table.add_row({'Reloaded', result['lastOperatingSystemReload']['modifyDate']})
        table.add_row(['MFR Serial #', result['manufacturerSerialNumber']])
        table.add_row(['Location', result['datacenter']['longName']])
        table.add_row(['Serial #', result['serialNumber']])
        table.add_row(['Billing', "Hourly" if result['billingItem']['hourlyFlag'] else "Monthly"])
        table.add_row(['Last transaction', result['lastTransaction']['transactionGroup']['name']])
        table.add_row(['cores', result['processorPhysicalCoreAmount']])
        table.add_row(['memory', result['memoryCapacity']])
    #    table.add_row(['public_ip', result['primaryIpAddress']])
        table.add_row(['private_ip', result['primaryBackendIpAddress']])
        table.add_row(['os', result['operatingSystem']['softwareLicense']['softwareDescription']['manufacturer']])
        table.add_row(['os_version', result['operatingSystem']['softwareLicense']['softwareDescription']['version']])
        print(table)

    def list_hardware_components(self, hostname):
        """
        Get the Hardware Server Components.
        """
        mask = 'mask[activeComponents[hardwareComponentModel[hardwareGenericComponentModel[hardwareComponentType]]]]'
        hardware_id = self.get_hardware_detail(hostname).get('id')
        result = self.client['SoftLayer_Hardware_Server'].getObject(id=hardware_id, mask=mask)

        table = PrettyTable()
        table.title = "Hardware Components"
        table.field_names = ['id', 'hardwareComponentType', 'capacity', 'manufacturer', 'name', 'version']

        capacity = None
        component_type = None
        for component in result['activeComponents']:
            if 'hardwareGenericComponentModel' in component['hardwareComponentModel']:
                capacity = component['hardwareComponentModel']['hardwareGenericComponentModel']['capacity']
                component_type = component['hardwareComponentModel']['hardwareGenericComponentModel'][
                    'hardwareComponentType']['type']
                table.add_row([component['hardwareComponentModel']['id'],
                               component_type,
                               capacity,
                               component['hardwareComponentModel']['manufacturer'],
                               component['hardwareComponentModel']['name'],
                               component['hardwareComponentModel']['version']
                               ])

        print(table)

    def list_hardware_component_firmware(self, hostname, component_id=None):
        """
        Get the last firmware version updated for a specific hardware component.
        """
        mask = 'mask[hardwareComponentModel[firmwares]]'
        hardware_id = self.get_hardware_detail(hostname).get('id')

        if component_id:
            result = self.client['SoftLayer_Hardware_Server'].getComponents(mask=mask, id=hardware_id)

            table = PrettyTable()
            table.title = "Hardware Component Firmware"
            table.field_names = ['id', 'createDate', 'hardwareComponentModelId', 'version']

            list_firmware = list()
            for component in result:
                if component_id == component['hardwareComponentModel']['id']:
                    for firmware in component['hardwareComponentModel']['firmwares']:
                        list_firmware.append(firmware)

            if not len(list_firmware) == 0:
                list_result = sorted(list_firmware, key=lambda x: x['createDate'])
                last_firmware = list_result[len(list_result) - 1]
                table.add_row([last_firmware['id'],
                               last_firmware['createDate'],
                               last_firmware['hardwareComponentModelId'],
                               last_firmware['version'],
                               ])

            print(table)
        else:
            print('The hardware component id is required.')

    def update_firmware(self, hostname, ipmi=0, raid_controller=0, bios=0, hard_drive=0):
        """
        Firmware Update.
        """
        result = None
        try:
            hardware_id = self.get_hardware_detail(hostname).get('id')
            result = self.client['SoftLayer_Hardware_Server'].createFirmwareUpdateTransaction(ipmi,
                                                                                              raid_controller,
                                                                                              bios,
                                                                                              hard_drive,
                                                                                              id=hardware_id)
        except SoftLayer.SoftLayerAPIError as e:
            print("Update failed. faultCode=%s, faultString=%s" % (
                e.faultCode, e.faultString))

        if result:
            print("Update firmware successfully")
        else:
            print("Update firmware failed")

    def get_hardware_detail(self, hostname):
        """
        Get an specific hardware server detail from the account through the hostname.
        """
        object_filter = {
            "hardware": {
                "hostname": {
                    "operation": hostname
                }
            }
        }
        user_detail = self.client['SoftLayer_Account'].getHardware(filter=object_filter)

        return user_detail[0]

@click.command()
@click.argument('hostname')
@click.option('--component-id', type=click.INT,
              help='Hardware Component Id')
@click.option('--ipmi', type=click.INT,
              help='Set this hardware Component to 1 (to upgrade) or 0 (not upgrade)')
@click.option('--raid-controller', type=click.INT,
              help='Set this hardware Component to 1 (to upgrade) or 0 (not upgrade)')
@click.option('--bios', type=click.INT,
              help='Set this hardware Component to 1 (to upgrade) or 0 (not upgrade)')
@click.option('--hard_drive', type=click.INT,
              help='Set this hardware Component to 1 (to upgrade) or 0 (not upgrade)')

def main(hostname, component_id, ipmi, raid_controller, bios, hard_drive):
    main = HardwareComponents()
    # Uncomment this to print out the API calls made.
    """
    Hardware Server Detail.
    """
    main.hardware_information(hostname)

    """
    Hardware Server Components.
    """
    main.list_hardware_components(hostname)

    """
    Show the last firmware version updated for a specific hardware component.
    Get the component_id from the second request.
    """
    main.list_hardware_component_firmware(hostname, component_id=component_id)

    """
    Update Firmware.
    Set the options impi, raid-controller, bios or hard-drive with "1" if you 
    want to update the component or "0" if you do not want to update.
    """
    main.update_firmware(hostname, ipmi=ipmi, raid_controller=raid_controller, bios=bios, hard_drive=hard_drive)

if __name__ == "__main__":
    main()
```
# SoftLayer APIアクセスの為の認証情報
アクセスの為の認証情報を確認します。
SL_USERNAMEは、「アカウントID_メールアドレス」となります。具体的には、Cloudコンソールの「IAM」→「ユーザー」部分に表示される[「VPNパスワード」](https://cloud.ibm.com/iam/users/IBMid-665002DIVK?tab=userdetails)欄に利用される「ユーザ名」と同じになります。

:::note warn
SL_API_KEYは、Classic InfrastructureのAPI Keyを利用します。IAM環境用ではなく、Classic環境用を利用しますのでご注意下さい。
:::

詳細は、[IBM Cloud Block Storage 大掃除－許可ホストのないものをリスト・削除する](https://qiita.com/6onoda/items/55c9cdf125017c43e9d5)をご参照下さい。
```:USERNAME/API Key環境変数設定
$export SL_USERNAME=XXXXXXX_masaharu.esaki@ibm.com
$export SL_API_KEY='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXe8b02e3d483e4a3b45c5'
```
# ベアメタルサーバ情報の表示
Classicベアメタルサーバの作成部分の説明は省略させて頂きます。
サーバ名「baremetal01-easki」についてハードウェア情報をPythonスクリプトで表示します。Pythonスクリプトをサーバ名を引数に実行します。
```:ハードウェア情報の表示
firmware_ver_check % python3 ch_hw_info.py baremetal01-easki
+-------------------------------------------------------+
|                Hardware Server details                |
+------------------+------------------------------------+
|             name | value                              |
+------------------+------------------------------------+
|             Name | baremetal01-easki.ibmcloud.private |
|           Status | ACTIVE                             |
|               Id | 3241136                            |
|          Started | 2025-02-19T18:37:21-06:00          |
|     MFR Serial # | C8150LK20NT0047                    |
|         Location | Tokyo 5                            |
|         Serial # | SL01HVUG                           |
|          Billing | Hourly                             |
| Last transaction | Firmware Update                    |
|            cores | 4                                  |
|           memory | 64                                 |
|       private_ip | 10.193.139.115                     |
|               os | CentOS                             |
|       os_version | 9-64                               |
+------------------+------------------------------------+
+-------------------------------------------------------------------------------------------------------------------+
|                                                Hardware Components                                                |
+------+-----------------------+----------+--------------+------------------------+---------------------------------+
|  id  | hardwareComponentType | capacity | manufacturer |          name          |             version             |
+------+-----------------------+----------+--------------+------------------------+---------------------------------+
| 1993 |       Hard Drive      |   4000   |   Seagate    |  Enterprise Capacity   |           ST4000NM0035          |
| 1388 |    Drive Controller   |    8     |     LSI      | Avago MegaRAID 9361-8i | SATA/SAS - MegaRAID SAS 9361-8i |
| 1993 |       Hard Drive      |   4000   |   Seagate    |  Enterprise Capacity   |           ST4000NM0035          |
| 3398 |      Network Card     |    4     |  SuperMicro  |     AOC-STG-i4T-O      |               2.00              |
| 3244 |          RAM          |    32    |    Hynix     | 32GB DDR4 2Rx8 NON REG |   32GB DDR4 2Rx8 2666 NON REG   |
| 3095 |      Motherboard      |    1     |  SuperMicro  |     X11SCW-F_R1.02     |    Intel Xeon SingleProc SATA   |
| 3147 |        Battery        |    1     |   Broadcom   |        49571-15        |                1                |
| 2369 |    Remote Mgmt Card   |    1     |    Aspeed    |   AST2500 - Onboard    |            IPMI - KVM           |
| 3244 |          RAM          |    32    |    Hynix     | 32GB DDR4 2Rx8 NON REG |   32GB DDR4 2Rx8 2666 NON REG   |
| 3970 |      Power Supply     |    1     |  SuperMicro  |      PWS-504P-1R       |               2.0               |
| 1871 |       Backplane       |    4     |  SuperMicro  |     BPN-SAS3-815TQ     |          4 Port Passive         |
| 3163 |       Processor       |   3.8    |    Intel     |    Xeon-CoffeeLake     |         E-2174G-Quadcore        |
| 852  |    Drive Controller   |    2     |  Mainboard   |        Onboard         |   SATAIII IDE/AHCI Controller   |
| 3970 |      Power Supply     |    1     |  SuperMicro  |      PWS-504P-1R       |               2.0               |
| 2509 |    Security Device    |    1     |  SuperMicro  |     AOM-TPM-9671V      |       Infineon TPM Module       |
+------+-----------------------+----------+--------------+------------------------+---------------------------------+
The hardware component id is required.
Update failed. faultCode=SoftLayer_Exception_MissingParameter, faultString=No firmware updates selected to be executed.
Update firmware failed
```
ファームウェアのバージョンを表示するには、コンポーネントIDとしてMotherboardのIDを指定します。
```:ファームウェアバージョン表示
firmware_ver_check % python3 ch_hw_info.py --component-id 3095 baremetal01-easki
                            << snip >>
                            
+-------------------------------------------------------------------------------+
|                          Hardware Component Firmware                          |
+--------+---------------------------+--------------------------+---------------+
|   id   |         createDate        | hardwareComponentModelId |    version    |
+--------+---------------------------+--------------------------+---------------+
| 281979 | 2024-08-06T07:57:25-07:00 |           3095           | 2.1 2-10-2023 |
+--------+---------------------------+--------------------------+---------------+
```
# ファームウェアのバージョンアップ
ファームウェアのバージョンアップを実施する場合は、「--ipmi」オプションを「１」に指定して実行します。
```:ファームウェアバージョンアップ実施失敗
firmware_ver_check % python3 ch_hw_info.py --component-id 3095 --ipmi 1 
                            << snip >>
                            
+-------------------------------------------------------------------------------+
|                          Hardware Component Firmware                          |
+--------+---------------------------+--------------------------+---------------+
|   id   |         createDate        | hardwareComponentModelId |    version    |
+--------+---------------------------+--------------------------+---------------+
| 281979 | 2024-08-06T07:57:25-07:00 |           3095           | 2.1 2-10-2023 |
+--------+---------------------------+--------------------------+---------------+
Update failed. faultCode=SoftLayer_Exception_Public, faultString=Server must be offline before firmware update.
```
ファームウェアのバージョンアップに際して、サーバの停止が必要な為、失敗したようです。curlコマンドでサーバを停止します。id（3241136）は、Hardware Server Detailsに表示されているIDを利用します。
```:サーバ停止
firmware_ver_check % curl --location 'https://api.softlayer.com/rest/v3.1/SoftLayer_Hardware_Server/3241136/powerOff' \
--header 'Authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXe8b02e3d483e4a3b45c5'
```
再度、Pythonファイルを実行すると、サーバのファームウェアアップデートが開始されます。
以下のようにデバイスリスト上で、ファームウェアのアップデート更新が実施されている事が確認できます。
![update1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354579/a49767d5-8309-453c-b7d2-fd1b772a2578.png)

![update2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354579/02f9111b-61bf-4fcb-a9b9-87efbe8bebc7.png)

その後、１時間３０分程度で、ファームウェアの更新が完了しました。![update3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/354579/94b2d2d0-634b-4c29-89d9-7e9a90b1b3da.png)




# まとめ
SoftLayer APIは、IBM Cloudコンソールのバックエンドとして稼働している環境となります。Cloud コンソールで提供していない機能、またはAPI操作を組み合わせてご利用環境の運用の自動化が可能となります。頻繁に実施する定形的な操作は、APIによる自動化が可能となります。是非、API Referenceをご参照頂き、ご利用環境に適した運用環境を構築下さい。
# 参考サイト
+ [APIリファレンス概要](https://cloud.ibm.com/docs/virtual-servers?topic=virtual-servers-api-reference&locale=ja)
+ [SoftLayer API Getting Started](https://sldn.softlayer.com/article/getting-started/)
+ [SoftLayer API Reference](https://sldn.softlayer.com/reference/softlayerapi/)
+ [ShinobiLayer: SoftLayer API 次の一歩: データ型オブジェクト(1)](https://qiita.com/testnin2/items/f04ca0cee9b2f0a8f830)
+ [ShinobiLayer: SoftLayer API 次の一歩: データ型オブジェクト(2)](https://qiita.com/testnin2/items/6be9b87571a99e01002a)
+ [ShinobiLayer: SoftLayer API 次の一歩: methodの実行方法と返り値の制御(1)](https://qiita.com/testnin2/items/92ea8f937c70c3a7e254)
+ [Hardware components and versionsサンプルスクリプト]()
+ [IBM Cloud Block Storage 大掃除－許可ホストのないものをリスト・削除する](https://qiita.com/6onoda/items/55c9cdf125017c43e9d5)