После формирования структуры проекта можно перейти к наполнению логикой операции по сбору нагрузки на CPU.
В данном разделе описана логика сбора информации в рамках этой системной операции.
Данная операция отвечает за получение данных о нагрузке на CPU целевого устройства по протоколу Ssh
, либо от агента
, если он установлен на устройстве.
Для выполнения этой операции сервис на вход должен получить следующий запрос:
CollectLinuxCpuUtilisationRequest
:device
:- Тип параметра:
DeviceContent
- Описание: Данные по 1 устройству.
- Тип параметра:
Тип DeviceContent
:
device_id
:- Тип параметра:
string
- Описание: Идентификатор устройства.
- Тип параметра:
model_name
:- Тип параметра:
string
- Описание: Название модели устройства.
- Тип параметра:
vendor_name
:- Тип параметра:
string
- Описание: Название вендора устройства.
- Тип параметра:
connectors
:- Тип параметра:
RepeatedField<DeviceConnector>
- Описание: Идентификатор сетевого интерфейса.
- Тип параметра:
Тип DeviceConnector
:
device_network_id
:- Тип параметра:
string
- Описание: Идентификатор сетевого интерфейса устройства.
- Тип параметра:
address
:- Тип параметра:
string
- Описание: IP/FQDN адрес устройства.
- Тип параметра:
mac
:- Тип параметра:
string
- Описание: MAC-адрес устройства.
- Тип параметра:
credentials
:- Тип параметра:
RepeatedField<Credential>
- Описание: Учетные данные подключения.
- Тип параметра:
Тип Credential
:
protocol
:- Тип параметра:
ConnectorProtocol
- Описание: Протокол подключения.
- Тип параметра:
login
:- Тип параметра:
string
- Описание: Логин для подключения.
- Тип параметра:
password
:- Тип параметра:
string
- Описание: Пароль для подключения.
- Тип параметра:
port
:- Тип параметра:
int32
- Описание: Порт подключения.
- Тип параметра:
cipher
:- Тип параметра:
int32
- Описание: Шифрование (только для IPMI).
- Тип параметра:
version
:- Тип параметра:
uint32
- Описание: Версия протокола (только для 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).
- Тип параметра:
privateKey
:- Тип параметра:
string
- Описание: Private key (только для SNMP).
- Тип параметра:
security_level
:- Тип параметра:
string
- Описание: Уровень безопасности.
- Тип параметра:
Enum ConnectorProtocol
:
ConnectorProtocol
(0):- Описание: Невалидное значение.
CONNECTOR_PROTOCOL_IPMI
(1):- Описание: Ipmi протокол для проверки подключения.
CONNECTOR_PROTOCOL_REDFISH
(2):- Описание: Redfish протокол для проверки подключения.
CONNECTOR_PROTOCOL_SNMP
(3):- Описание: Snmp протокол для проверки подключения.
CONNECTOR_PROTOCOL_SSH
(4):- Описание: Ssh протокол для проверки подключения.
CONNECTOR_PROTOCOL_WMI
(5):- Описание: Wmi протокол для проверки подключения.
Из даных запроса, сервис интересуют следующие поля:
- DeviceContent->connectors - список возможных сетевых подключений
- DeviceContent->connectors->address - Адрес сервера
- DeviceContent->connectors->credentials - Список данных, необходимых для подключения
- DeviceContent->connectors->protocol - Протокол подключения. Обязательно должен быть CONNECTOR_PROTOCOL_SSH. Другие протоколы должны игнорироваться
- DeviceContent->connectors->login - Логин пользователя, под которым будет выполнятсья команда
- DeviceContent->connectors->password - Пароль пользователя, под которым будет выполнятсья команда
- DeviceContent->connectors->port - Порт подключения
Используя полученные данные, сервис выполняет на оборудовании следующую команду:
- top -b -n1 -1 -p0 -w 400", "top -b -n1 -w 400
Сервис выполняет вышеуказанные операции на оборудовании, затем обрабатывает полученные результаты и возвращает ответ на grpc запрос в виде следующего объекта:
Тип DeviceCpuUtilisation
:
device_identity
:- Тип параметра:
DeviceDataIdentity
- Описание: Описание источника сбора данных.
- Тип параметра:
unit_utilistaions
:- Тип параметра:
map<int32, CpuUnitUtilisation
- Описание: Данные о статусе каждого из процессоров/ядер. Ключ - id процессора.
- Тип параметра:
summary_utilisation
:- Тип параметра:
CpuUnitUtilisation
- Описание: Суммарные метрики потребления CPU.
- Тип параметра:
Тип CpuUnitUtilisation
:
total_using
:- Тип параметра:
int32
- Описание: Общий процент использования процессора.
- Тип параметра:
idle_time
:- Тип параметра:
int32
- Описание: Время простоя процессора, выраженное в процентах.
- Тип параметра:
user_using
:- Тип параметра:
google.protobuf.Int32Value
- Описание: Процент использования процессора пользовательскими процессами.
- Тип параметра:
system_using
:- Тип параметра:
google.protobuf.Int32Value
- Описание: Процент использования процессора системными процессами.
- Тип параметра:
nice_value_using
:- Тип параметра:
google.protobuf.Int32Value
- Описание: Процент времени, в течение которого CPU выполнял процессы, выставленные пользователем вручную (nice).
- Тип параметра:
io_waiting
:- Тип параметра:
google.protobuf.Int32Value
- Описание: Процент времени, потраченного на ожидание ввода-вывода.
- Тип параметра:
hw_service_interrupts
:- Тип параметра:
google.protobuf.Int32Value
- Описание: Процент времени, потраченного на обработку аппаратных прерываний.
- Тип параметра:
soft_service_interrupts
:- Тип параметра:
google.protobuf.Int32Value
- Описание: Процент времени, потраченного на обработку программных (системных) прерываний.
- Тип параметра:
steal_time
:- Тип параметра:
google.protobuf.Int32Value
- Описание: Процент времени, потраченного на выполнение задач в виртуальной машине (виртуализация).
- Тип параметра:
В качестве примера реализуем простейшее приложение, выполняющее одну системную операцию "Получить данные о нагрузке на CPU". Тестировать будем на виртуальной машине с операционной системой Ubuntu 12.3.0
Общий принцип работы микросервиса следующий:
- сервис получает grpc запрос
- вызывает соответствующие shell команды на оборудовании, данные которого пришли в запросе
- соответствующим образом обрабатывает
- возвращает результаты обратно
- Создайте проект из шаблона "Служба ASP.NET Core gRPC". Это будет базовый каркас проекта.
- Прото-файлы из шаблона не годятся т.к. не соответствуют кронтрактам EMS. Нужно заменить их на правильные. Удалите демонстрационные файлы из директории Protos и скопируйте туда proto файлы из директории project\SshExample\SshExample\Protos. Это уже подготовленные grpc объекты, описанные выше
- В директории service создайте файл SshCommandCaller.cs. В нем будет находиться логика обработки grpc-запроса и отправка ssh-запроса целевому оборудованию.
- Теперь нам нужны команды, с помощью которых можно получить данные нагрузке на CPU. В Linux под эти цели подходит команда TOP.
Добавьте в этот файл следующие константы:
private static readonly string[] _commands = { "top -b -n1 -1 -p0 -w 400", "top -b -n1 -w 400" };
private static string TopCpuPrefix = "%cpu";
_commands - список shell команд, которые будет запрашивать сервис у оборудования. TopCpuPrefix - префикс для более удобной обработки результатов
- Для более удобной передачи данных между методом вызова команд и методом обработки добавим record HandleResult. В нем будут храниться код возврата вызова и его результаты.
Добавьте в SshCommandCaller.cs следующий record
public record HandleResult(int Success, string Stdout, string Stderr);
- Следующим этапом нужен метод, который будет непосредственно вызывать shell команды на оборудовании. Суть метода, получить из объекта CollectLinuxCpuUtilisationRequest креды подключения к оборудовани, подключитсья к нему и выполнить shell команду. Можете написать свой метод, можете взять из примера. Метод в примере не учитывает случай, что пользователь может не передать данных для подсоединения или у устройства их может быть несколько. Этот метод просто берет первые возможные данные для подсоединения, подключается к устройству, выполняет команду и собирает объект HandleResult.
public static HandleResult CallSsh(CollectLinuxCpuUtilisationRequest request, string command)
{
DeviceConnector connection = request.Device.Connectors.First();
Credential creds=connection.Credentials.First();
using (SshClient client = new SshClient(connection.Address, creds.Port, creds.Login, creds.Password))
{
client.Connect();
SshCommand cmdRes = client.RunCommand(command);
if (cmdRes.ExitStatus == 0)
{
client.Disconnect();
return new HandleResult(cmdRes.ExitStatus,
cmdRes.Result, cmdRes.Error);
}
client.Disconnect();
return new HandleResult(cmdRes.ExitStatus,
cmdRes.Result, cmdRes.Error);
}
}
- Далее полученные данные нужно обработать. Этот метод должен заниматься анализом полученных в результате выполнения команд данных и сборкой ответа для пользователя.
Резельтатами выполнения двух команд top будут примерно следующие данные (разнятся от количества процессоров в машине и их нагруженности)
top - 17:55:32 up 1 min, 1 user, load average: 0.01, 0.01, 0.00
Tasks: 1 total, 1 running, 0 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
...
%Cpu19 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7806.4 total, 6860.3 free, 509.2 used, 436.8 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 7067.7 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
697 vvsurje+ 20 0 7656 3236 2872 R 0.0 0.0 0:00.00 top
top - 17:55:52 up 2 min, 1 user, load average: 0.01, 0.00, 0.00
Tasks: 35 total, 1 running, 34 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7806.4 total, 6858.4 free, 511.2 used, 436.9 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 7065.8 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
690 root 20 0 43696 37724 9956 S 13.3 0.5 0:00.75 python3
1 root 20 0 166004 11392 8352 S 0.0 0.1 0:00.21 systemd
...
Ценность для текущего случая представляют только строки с префиксом %Cpu.
Каждая строчка - это данные одного процессора. %Cpu(s) - общие, сводные, данные по всем процессорам.
По каждому процессору представлены данные в виде групп, разбитых символом ','. Каждая группа состоит из числа - значения,
и строкового идентификатора. Например "0.0 us" - значит, что пользователем в данный момент процессор не загружен.
Логика работы метода следующая. Нужно объединить все данные в одну строку. Разбить ее на подстроки по по символу '\n' (перевод строки).
Отфильтровывать всё лишнее, оставляя только строки, начинающиеся с %Cpu.
После этого обработать каждую строку и собрать объекты с данными по каждому процессору.
Вот пример реализации такого метода.
public static CollectLinuxCpuUtilizationResponse ProcessResponse(HandleResult[] results, CollectLinuxCpuUtilizationRequest request)
{
string response = "";
foreach (HandleResult res in results)
{
if (res.Success == 0)
{
response += res.Stdout;
}
}
CollectLinuxCpuUtilizationResponse statDeviceCpu = new()
{
CpuUtilization = new()
{
DeviceIdentity = new DeviceDataIdentity()
{
DeviceId = request.Device.DeviceId,
Source = ServiceSource.LinuxManager
},
SummaryUtilization = new()
}
};
IEnumerable<string> rows = response.ToLowerInvariant()
.Split('\n', StringSplitOptions.TrimEntries)
.Where(s => s.StartsWith(TopCpuPrefix))
.SelectMany(c => c
.Split(TopCpuPrefix, StringSplitOptions.TrimEntries)
.Where(d => !string.IsNullOrWhiteSpace(d))
.Select(s => TopCpuPrefix + s));
foreach (string item in rows)
{
int processorIdEnd = item.IndexOf(' ', TopCpuPrefix.Length);
if (processorIdEnd == -1)
{
continue;
}
string? strProcessorId = item[TopCpuPrefix.Length..processorIdEnd];
int processorId;
if (strProcessorId == "(s):")
{
processorId = -1;
}
else if (!int.TryParse(strProcessorId, out processorId))
{
continue;
}
CpuUnitUtilization cpu = new();
foreach (string entiry in item[(processorIdEnd + 3)..].Split(',', StringSplitOptions.TrimEntries))
{
string[] parts = entiry.Split(' ', 2, StringSplitOptions.TrimEntries);
if (parts.Length != 2)
{
continue;
}
string key = parts[1];
string strValue = parts[0];
if (float.TryParse(strValue, out float intValue))
{
switch (key)
{
case "us": cpu.UserUsing = (int)Math.Ceiling(intValue); break;
case "sy": cpu.SystemUsing = (int)Math.Ceiling(intValue); break;
case "ni": cpu.NiceValueUsing = (int)Math.Ceiling(intValue); break;
case "id": cpu.IdleTime = (int)Math.Ceiling(intValue); break;
case "wa": cpu.IoWaiting = (int)Math.Ceiling(intValue); break;
case "hi": cpu.HwServiceInterrupts = (int)Math.Ceiling(intValue); break;
case "si": cpu.SoftServiceInterrupts = (int)Math.Ceiling(intValue); break;
case "st": cpu.StealTime = (int)Math.Ceiling(intValue); break;
default: break;
}
}
}
if (processorId == -1)
{
statDeviceCpu.CpuUtilization.SummaryUtilization = cpu;
}
else
{
statDeviceCpu.CpuUtilization.UnitUtilistaions.Add(processorId, cpu);
}
}
return statDeviceCpu;
}
- Далее необходимо связать предыдущие методы одним управляющим методом, который соберет список команд и для каждой из них вызовет обработку в методе
CallSsh
, а затем передаст результаты в методProcessResponse
. Ниже представлен пример реализации данного метода:
public static CollectLinuxCpuUtilisationResponse GetCpuUtilisation(CollectLinuxCpuUtilisationRequest request)
{
HandleResult[] responses= _commands.Select(cmd=> CallSsh(request,cmd)).ToArray();
return ProcessResponse(responses, request);
}
- Далее необходимо реализовать GRPC сервис из прото-файлов, которые были добавлены в проект ранее.
В файле
GreeterService.cs
(был создан из шаблона) замените все содержимое на следующее:
namespace SshExample.Services;
public class LinuxManagerService : LinuxManager.LinuxManagerBase
{
public override Task<CollectLinuxCpuUtilisationResponse> CollectCpuUtilisation(CollectLinuxCpuUtilisationRequest request, ServerCallContext context)
{
return Task.FromResult(SshCommandCaller.GetCpuUtilisation(request));
}
}
- После сборки и запуска приложения, проверьте результат работы. Для этого вызовите метод
CollectCpuUtilisation
, например, в Postman.