Выключение оборудования

После формирования структуры проекта можно перейти к наполнению логикой операции по выключению оборудования.

В данном разделе описана логика сбора информации в рамках этой системной операции.

Обзор операции

Данная операция отвечает за выключение питания целевого устройства по протоколу Redfish.

Для реализации операции, RPC будет иметь следующую сигнатуру:

  • rpc PowerOff(PowerOffBmcRequest) returns (PowerOffBmcResponse)

На вход модуль получает следующий запрос в формате Protocol buffers:

Тип PowerOffBmcRequest:

  • device:
    • Тип параметра: DeviceContent
    • Описание: Данные об устройстве.
  • mode:
    • Тип параметра: BmcPowerOffMode
    • Описание: Режим выключения оборудования.

Тип DeviceContent:

  • device_id:
    • Тип параметра: string
    • Описание: Идентификатор устройства.
  • model_name:
    • Тип параметра: string
    • Описание: Модель устройства.
  • vendor_name:
    • Тип параметра: string
    • Описание: Вендор устройства.
  • connectors:
    • Тип параметра: repeated DeviceConnector
    • Описание: Список интерфейсов подключения к устройству.

Тип DeviceConnector:

  • device_network_id:
    • Тип параметра: string
    • Описание: Идентификатор сетевого интерфейса устройства.
  • address:
    • Тип параметра: string
    • Описание: Адрес подключения (ip/fqdn).
  • mac:
    • Тип параметра: string
    • Описание: MAC-адрес устройства.
  • credentials:
    • Тип параметра: repeated Credential
    • Описание: Список данных подключения к устройству.

Тип Credential:

  • protocol:
    • Тип параметра: ConnectorProtocol
    • Описание: Протокол подключения.
  • login:
    • Тип параметра: string
    • Описание: Логин для подключения.
  • password:
    • Тип параметра: string
    • Описание: Пароль для подключения.
  • port:
    • Тип параметра: int32
    • Описание: Порт подключения.
  • cipher:
    • Тип параметра: int32
    • Описание: Шифрование (только для IPMI).
  • version:
    • Тип параметра: int32
    • Описание: Версия протокола (только для SNMP).
  • community:
    • Тип параметра: string
    • Описание: Community слово (только для SNMP).
  • security_name:
    • Тип параметра: string
    • Описание: Security name (только для SNMP).
  • context:
    • Тип параметра: string
    • Описание: Контекст подключения (только для SNMP).
  • auth_protocol:
    • Тип параметра: string
    • Описание: Auth protocol (только для SNMP).
  • auth_key:
    • Тип параметра: string
    • Описание: Auth key (только для SNMP).
  • private_protocol:
    • Тип параметра: string
    • Описание: Private protocol (только для SNMP).
  • private_key:
    • Тип параметра: string
    • Описание: Private key (только для SNMP).
  • security_level:
    • Тип параметра: string
    • Описание: Уровень безопасности.

Перечисление BmcPowerOffMode:

  • BMC_POWER_OFF_MODE_UNSPECIFIED:
    • Описание: Невалидное значение.
  • BMC_POWER_OFF_MODE_STANDARD:
    • Описание: Стандартный режим выключения оборудования.
  • BMC_POWER_OFF_MODE_SOFT:
    • Описание: Soft-режим выключения оборудования.

Структура Credential является общей для реализации операции выключения оборудования по разным протоколам, поэтому может содержать большее количество полей, чем поддерживает модуль расширения.

Для корректной работы сбора данных, устройство должно иметь хотя бы одно действительное подключение с протоколом Redfish (поле protocol списка credentials со значением CONNECTOR_PROTOCOL_REDFISH), остальные устройства модуль должен игнорировать.

Пример данных запроса:

{
  "device": {
    "device_id": "test",
    "connectors": [
      {
        "address": "192.168.1.55",
        "credentials": [
          {
            "login": "root",
            "password": "1qaz@WSX",
            "port": 443,
            "protocol": "CONNECTOR_PROTOCOL_REDFISH"
          }
        ]
      }
    ]
  },
  "mode": "BMC_POWER_OFF_MODE_STANDARD"
}

В качестве cообщения-ответа в RPC используется модель:

Тип PowerOffBmcResponse:

  • result:
    • Тип параметра: OperationResult
    • Описание: Результат выполнения операции управления.

Тип OperationResult:

  • device_id:
    • Тип параметра: string
    • Описание: Идентификатор устройства, на котором происходила операция.
  • state:
    • Тип параметра: OperationState
    • Описание: Тип результата выполнения операции.
  • output:
    • Тип параметра: string
    • Описание: Текстовое описание результата выполнения операции.

Перечисление OperationState:

  • OPERATION_STATE_UNSPECIFIED:
    • Описание: Невалидное значение.
  • OPERATION_STATE_SUCCESS:
    • Описание: Операция завершена успешно.
  • OPERATION_STATE_FAILED:
    • Описание: Выполнение операции завершилось с ошибкой.

С полной структурой данных вы можете ознакомиться в протофайлах.

Пример реализации

Реализация операции будет производиться на устройстве от производителя Huawei со следующими характеристикам:

  • Название ОС: -
  • Версия: -
  • Процессор: Intel(R) Xeon(R) Silver 4208 CPU @ 2.10GHz x 2
  • Объем памяти дисков: -
  • Суммарно установлено памяти (ОЗУ): 24 GB
  • Количество плашек ОЗУ: 3

Для реализации операции необходимо иметь возможность подключения к удаленному хосту по протоколу Redfish, для этого необходимо сформировать набор данных подключения.

Изучив документацию с официального сайта производителя Huawei становится понятно каким образом можно изменить статуса питания устройства по Redfish.

URI домашней страницы службы Redfish (другое название корневой адрес службы) можно получить с помощью следующего URI: https://<IP-адрес:порт>/redfish/v1

Для выполнения операции выключения питания устройства необходимо отправить POST запрос для страницы Systems/<System Id>/Actions/ComputerSystem.Reset, а итоговый адрес получения данных буде выглядит так - https://<IP-адрес:порт>/redfish/v1/Systems/<System Id>/Actions/ComputerSystem.Reset c телом:

{
  {
    "ResetType": "ForceOff"
  }
}

Подробнее по ссылке:

Процесс выключения устройства в обычном режиме, который состоит из получения содержимого страницы и выполнения POST-запроса:

private async Task<OperationResult> PowerOffRedfish(Credential creds, string address)
{
    string payloadHeader = $"authorization:Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes(creds.Login + ':' + creds.Password))}";

    string bodyTemplate = @$"{{""ResetType"": ""ForceOff""}}";

    ImmutableArray<string> members = await GetMembers("/redfish/v1/Systems/", payloadHeader, address, creds.Port);

    if (!members.Any())
    {
        return new()
        {
            Output = "Members page not found",
            State = OperationState.Failed
        };
    }

    string actionPath = members[0];

    if (!actionPath.EndsWith('/'))
    {
        actionPath += '/';
    }

    actionPath += "Actions/ComputerSystem.Reset";

    RedfishOperationResult result = await TryPost(actionPath, payloadHeader, address, creds.Port, bodyTemplate);

    return new()
    {
        Output = result.Success
            ? result.Result
            : result.Error,
        State = result.Success ? OperationState.Success : OperationState.Failed,
    };
}

Процесс отправки запроса получения содержимого страницы:

private static async Task<string?> TryGetJsonPage(string path, string authHeader, string ip, int port)
{
    try
    {
        string uri = (port == 80 || port / 100 == 50 ? "http" : "https") + "://" + ip + ':' + port + path;

        HttpRequestMessage httpRequestMessage = new()
        {
            RequestUri = new(uri),
        };

        string[] headers = authHeader.Split(':', 2);

        if (headers.Length == 2)
        {
            httpRequestMessage.Headers.Add(headers[0], headers[1]);
        }

        HttpClient httpClient = new(new HttpClientHandler()
        {
            ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true,
            AllowAutoRedirect = false,
        })
        {
            Timeout = TimeSpan.FromSeconds(100)
        };

        using HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        string result = await httpResponseMessage.Content.ReadAsStringAsync();

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            return result;
        }
    }
    catch
    {
        return null;
    }

    return null;
}

Процесс отправки POST-запроса на выключение устройства:

public async Task<RedfishOperationResult> TryPost(string path, string authHeader, string ip, int port, string body)
{
    string uri = (port == 80 ? "http" : "https") + "://" + ip + ':' + port + path;

    try
    {
        HttpRequestMessage httpRequestMessage = new()
        {
            RequestUri = new(uri),
            Method = HttpMethod.Post,
            Content = new StringContent(body)
        };

        httpRequestMessage.Content.Headers.ContentType = new(MediaTypeNames.Application.Json);

        string[] headerValues = authHeader.Split(':', 2);

        if (headerValues.Length == 2)
        {
            httpRequestMessage.Headers.TryAddWithoutValidation(headerValues[0], headerValues[1]);
        }

        HttpClient httpClient = new(new HttpClientHandler()
        {
            ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true,
            AllowAutoRedirect = false,
        })
        {
            Timeout = TimeSpan.FromSeconds(100)
        };

        using HttpResponseMessage httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        int statusCode = (int)httpResponseMessage.StatusCode;

        string result = await httpResponseMessage.Content.ReadAsStringAsync();

        if (statusCode >= 200 && statusCode < 300 && (!result.Contains("\"error\"") || result.Contains(".Success\"")))
        {
            return new(result, string.Empty, true, string.Empty);
        }
        else
        {
            return new(string.Empty, result, false, string.Empty);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("Error while patch to device {0}, {1}", uri, ex);

        return new(string.Empty, ex.Message, false, string.Empty);
    }
}

Пример готового проекта расположен в папке project