Сбор перечня виртуальных машин

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

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

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

// Сервис HypervisorManager реализует логику взаимодействия и сбора информации с гипервизоров
service HypervisorManager  {
  // Получение данных для заведения гипервизора
  rpc CollectHypervisorInitialContent(CollectHypervisorInitialContentRequest) returns (CollectHypervisorInitialContentResponse);
}

/**
 * Контракт запроса для rpc CollectHypervisorInitialContent
 */
message CollectHypervisorInitialContentRequest {
  // Входные данные для сбора информации
  DeviceInitialInput device_input = 1;
}

/**
 * Контракт ответа для rpc CollectHypervisorInitialContent
 */
message CollectHypervisorInitialContentResponse {
  // Инфомрация, необходимая при заведении
  DeviceInitialContent content = 2;
}

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

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

Реализация операции будет производиться на ESXI версии 7.0 запущенной на оборудовании Gagar>n oracul gen. 1.

Дополняем уже имеющийся шаблон реализацией RPC CollectHypervisorInitialContent.

В ходе обработки запроса на языке golang для гипервизора ESXI мы воспользуемся библиотекой govmomi для взаимодействия с VMware vSphere APIs и сбора информации с гипервизора.

Данный подход основан на примере.

Для начала необходимо получить данные для подключения к гипервизору по протоколу Esxi:

creds, err := findCreds(req.DeviceInput.Credentials, pb.InitialProtocol_INITIAL_PROTOCOL_ESXI)
if err != nil {
  return nil, fmt.Errorf("find creds: %s", err)
}

Функция для получения данных подключения:

func findCreds(in []*pb.InitialCredential, protocol pb.InitialProtocol) (*pb.InitialCredential, error) {
  if len(in) == 0 {
    return nil, fmt.Errorf("device creds list is empty")
  }

  for _, creds := range in {
    if creds.Protocol == protocol {
      if creds.Login == "" || creds.Password == "" {
        return nil, fmt.Errorf("login or password can not be empty")
      }

      return creds, nil
    }
  }

  return nil, fmt.Errorf("creds not found")
}

Попробуем создать клиента к гипервизору для проверки подключения, используем переменную ошибки далее в конечном парсинге ответа:

func (r *microservice) CollectVirtualMachinesList(ctx context.Context, req *pb.CollectHypervisorInitialContentRequest) (*pb.CollectHypervisorInitialContentResponse, error) {
  // ...

  _, err = createClient(ctx, req.DeviceInput.Address, creds.Login, creds.Password)

  // ...
}

func createClient(ctx context.Context, address, login, password string) (*govmomi.Client, error) {
  if strings.TrimSpace(address) == "" {
    return nil, fmt.Errorf("hypervisor address can not be empty")
  }

  u, err := soap.ParseURL(address)
  if err != nil {
    return nil, fmt.Errorf("parse URL [%s]: %s", address, err)
  }

  // TODO UserPassword() method UNRECOMMENDED
  u.User = url.UserPassword(login, password)

  return govmomi.NewClient(ctx, u, true)
}

Далее попробуем запросить данные с гипервизора с помощью функции collectData:

func (r *microservice) CollectVirtualMachinesList(ctx context.Context, req *pb.CollectHypervisorInitialContentRequest) (*pb.CollectHypervisorInitialContentResponse, error) {
  // ...

  collectData(ctx, req.DeviceInput.Address, creds.Login, creds.Password, &hostSystems, "HostSystem", "hardware", "summary", "config")

  // ...
}

func collectData(
  ctx context.Context,
  addr, login, passw string,
  dst any,
  mainPath string,
  subPaths ...string,
) error {
  c, err := createClient(ctx, addr, login, passw)
  if err != nil {
    return fmt.Errorf("connect: %s", err)
  }

  m := view.NewManager(c.Client)
  v, err := m.CreateContainerView(ctx, c.ServiceContent.RootFolder, []string{mainPath}, true)
  if err != nil {
    return fmt.Errorf("create container view: %s", err)
  }
  defer v.Destroy(ctx) //nolint:all

  if err = v.Retrieve(ctx, []string{mainPath}, subPaths, dst); err != nil {
    return fmt.Errorf("retrieve: %s", err)
  }

  return nil
}

В завершении парсим данные из полученного ответа, формируем структуру для ответа по RPC (на данный момент обрабатываем только первую хостовую систему в массиве):

func parseInitialContent(hs []mo.HostSystem, checkErr error) *pb.CollectHypervisorInitialContentResponse {
  out := pb.CollectHypervisorInitialContentResponse{
    Content: &pb.DeviceInitialContent{},
  }

  if checkErr != nil {
    out.Content.CheckResult = false
    out.Content.Output = checkErr.Error()
    return &out
  }

  out.Content.CheckResult = true

  h := hs[0]
  if h.Hardware != nil {
    sn := strings.TrimSpace(h.Hardware.SystemInfo.SerialNumber)
    if sn != "" {
      out.Content.SerialNumbers = append(out.Content.SerialNumbers, sn)
    }

    model := strings.TrimSpace(h.Hardware.SystemInfo.Model)
    if model != "" {
      out.Content.Models = append(out.Content.Models, model)
    }

    vendor := strings.TrimSpace(h.Hardware.SystemInfo.Vendor)
    if model != "" {
      out.Content.Vendors = append(out.Content.Vendors, vendor)
    }
  }

  if h.Config != nil {
    operationSystemVersion := h.Config.Product.Version
    operationSystem := h.Config.Product.FullName
    operationSystemArchitecture := h.Config.Product.OsType
    hostname := ""
    if h.Summary.Host != nil {
      hostname = h.Summary.Host.Value
    }

    out.Content.OperationSystems = append(out.Content.OperationSystems, &pb.InitialOperationSystem{
      OperationSystem:             operationSystem,
      OperationSystemVersion:      operationSystemVersion,
      OperationSystemArchitecture: operationSystemArchitecture,
      Hostname:                    hostname,
    })
  }

  return &out
}

В завершении отправляем полученный результат как ответ по RPC.

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