blob: 0d8ca23fe00adaec0befc2e3194661080fc549b3 [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package controller
import (
"context"
"github.com/golang/protobuf/proto"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/grpc/grpcutil"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
ufspb "infra/unifiedfleet/api/v1/models"
ufsAPI "infra/unifiedfleet/api/v1/rpc"
"infra/unifiedfleet/app/model/inventory"
"infra/unifiedfleet/app/model/registration"
"infra/unifiedfleet/app/util"
)
// CreateVM creates a new vm in datastore.
func CreateVM(ctx context.Context, vm *ufspb.VM, nwOpt *ufsAPI.NetworkOption) (*ufspb.VM, error) {
f := func(ctx context.Context) error {
hc := getVMHistoryClient(vm)
machine, err := getMachineForHost(ctx, vm.GetMachineLseId())
if err != nil {
return err
}
if err := validateCreateVM(ctx, vm, nwOpt, machine); err != nil {
return errors.Annotate(err, "Validation error - Failed to create MachineLSE").Err()
}
vm.Zone = machine.GetLocation().GetZone().String()
if vm.ResourceState == ufspb.State_STATE_UNSPECIFIED {
vm.ResourceState = ufspb.State_STATE_REGISTERED
}
// Update ip configs
if nwOpt.GetVlan() != "" || nwOpt.GetIp() != "" {
if err := hc.netUdt.addVMHostHelper(ctx, nwOpt, vm); err != nil {
return errors.Annotate(err, "Fail to assign ip to vm %s", vm.GetName()).Err()
}
vm.ResourceState = ufspb.State_STATE_DEPLOYING
}
if _, err := inventory.BatchUpdateVMs(ctx, []*ufspb.VM{vm}); err != nil {
return errors.Annotate(err, "Failed to create vm %q", vm.GetName()).Err()
}
// Update states
if err := hc.stUdt.updateStateHelper(ctx, vm.ResourceState); err != nil {
return errors.Annotate(err, "Fail to update state to vm %s", vm.GetName()).Err()
}
hc.LogVMChanges(nil, vm)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
logging.Errorf(ctx, "Failed to create vm in datastore: %s", err)
return nil, err
}
return vm, nil
}
// UpdateVM updates an existing vm in datastore.
func UpdateVM(ctx context.Context, vm *ufspb.VM, mask *field_mask.FieldMask) (*ufspb.VM, error) {
f := func(ctx context.Context) error {
hc := getVMHistoryClient(vm)
// Get old/existing VM
oldVM, err := inventory.GetVM(ctx, vm.GetName())
if err != nil {
return errors.Annotate(err, "Fail to get existing vm by %s", vm.GetName()).Err()
}
// Validate input
if err := validateUpdateVM(ctx, oldVM, vm, mask); err != nil {
return errors.Annotate(err, "UpdateVM - validation failed").Err()
}
// Copy for logging
oldVMCopy := proto.Clone(oldVM).(*ufspb.VM)
// Copy the vlan/zone to vm OUTPUT only fields from already existing vm
vm.Zone = oldVM.GetZone()
vm.Vlan = oldVM.GetVlan()
// Partial update by field mask
if mask != nil && len(mask.Paths) > 0 {
vm, err = processVMUpdateMask(ctx, oldVM, vm, mask)
if err != nil {
return errors.Annotate(err, "UpdateVM - processing update mask failed").Err()
}
} else {
// This is for complete object input
if vm.MachineLseId == "" {
return status.Error(codes.InvalidArgument, "UpdateVM - machineLseId cannot be empty/nil.")
}
// Check if user provided new host to associate the vm
if vm.MachineLseId != oldVM.MachineLseId {
machine, err := getMachineForHost(ctx, vm.GetMachineLseId())
if err != nil {
return err
}
if err := util.CheckPermission(ctx, util.InventoriesUpdate, machine.GetRealm()); err != nil {
return err
}
// update the zone info for vm for vm table indexing
vm.Zone = machine.GetLocation().GetZone().String()
}
}
// update state
if err := hc.stUdt.updateStateHelper(ctx, vm.GetResourceState()); err != nil {
return errors.Annotate(err, "Fail to update state to vm %s", vm.GetName()).Err()
}
if _, err := inventory.BatchUpdateVMs(ctx, []*ufspb.VM{vm}); err != nil {
return errors.Annotate(err, "Failed to create vm %q", vm.GetName()).Err()
}
hc.LogVMChanges(oldVMCopy, vm)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
logging.Errorf(ctx, "Failed to create vm in datastore: %s", err)
return nil, err
}
return vm, nil
}
// UpdateVMHost updates the vm host(update ip assignment).
func UpdateVMHost(ctx context.Context, vmName string, nwOpt *ufsAPI.NetworkOption) (*ufspb.VM, error) {
var vm *ufspb.VM
var err error
f := func(ctx context.Context) error {
hc := getVMHistoryClient(&ufspb.VM{Name: vmName})
//Get VM
vm, err = GetVM(ctx, vmName)
if err != nil {
return err
}
// Validate the input
if err := validateUpdateVMHost(ctx, vm, nwOpt.GetVlan(), nwOpt.GetIp()); err != nil {
return err
}
// this is for logging changes
oldVM := proto.Clone(vm).(*ufspb.VM)
// Find free ip, set IP and DHCP config
if err := hc.netUdt.addVMHostHelper(ctx, nwOpt, vm); err != nil {
return errors.Annotate(err, "Fail to assign ip to vm %s", vm.Name).Err()
}
vm.ResourceState = ufspb.State_STATE_DEPLOYING
if err := hc.stUdt.updateStateHelper(ctx, vm.ResourceState); err != nil {
return errors.Annotate(err, "Fail to update state to vm %s", vm.GetName()).Err()
}
// update vm with new vlan info set in prev step by addVMHostHelper
if _, err := inventory.BatchUpdateVMs(ctx, []*ufspb.VM{vm}); err != nil {
return errors.Annotate(err, "Failed to update vm %q", vm.GetName()).Err()
}
hc.LogVMChanges(oldVM, vm)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
logging.Errorf(ctx, "Failed to assign IP to the VM: %s", err)
return nil, err
}
return vm, nil
}
// validateUpdateVMHost validates if an ip can be assigned to the VM
func validateUpdateVMHost(ctx context.Context, vm *ufspb.VM, vlanName, ipv4Str string) error {
// Check permission
// during partial update, vm object may not have lse info, so we get the old vm to get the lse
// and then machine to check the permission
// Get MachineLSE
machine, err := getMachineForHost(ctx, vm.GetMachineLseId())
if err != nil {
return err
}
// check permission
if err := util.CheckPermission(ctx, util.InventoriesUpdate, machine.GetRealm()); err != nil {
return err
}
if ipv4Str != "" {
if _, err := util.IPv4StrToInt(ipv4Str); err != nil {
return errors.Annotate(err, "Validate create host").Tag(grpcutil.InvalidArgumentTag).Err()
}
return nil
}
// Check if resources does not exist
return ResourceExist(ctx, []*Resource{GetVlanResource(vlanName)}, nil)
}
// DeleteVMHost deletes the dhcp/ip of a vm in datastore.
func DeleteVMHost(ctx context.Context, vmName string) error {
f := func(ctx context.Context) error {
hc := getVMHistoryClient(&ufspb.VM{Name: vmName})
//Get VM
oldVM, err := inventory.GetVM(ctx, vmName)
if err != nil {
return err
}
// Validate the input
if err := validateDeleteVM(ctx, oldVM); err != nil {
return err
}
if err := hc.netUdt.deleteDHCPHelper(ctx); err != nil {
return err
}
oldVMCopy := proto.Clone(oldVM).(*ufspb.VM)
oldVM.Vlan = ""
oldVM.Ip = ""
oldVM.ResourceState = ufspb.State_STATE_REGISTERED
if _, err := inventory.BatchUpdateVMs(ctx, []*ufspb.VM{oldVM}); err != nil {
return errors.Annotate(err, "Failed to update vm %q", vmName).Err()
}
hc.stUdt.updateStateHelper(ctx, oldVM.ResourceState)
hc.LogVMChanges(oldVMCopy, oldVM)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
logging.Errorf(ctx, "Failed to delete the vm dhcp: %s", err)
return err
}
return nil
}
// processVMUpdateMask process update field mask to get only specific update
// fields and return a complete vm object with updated and existing fields
func processVMUpdateMask(ctx context.Context, oldVM *ufspb.VM, vm *ufspb.VM, mask *field_mask.FieldMask) (*ufspb.VM, error) {
// update the fields in the existing vm
for _, path := range mask.Paths {
switch path {
case "machineLseId":
machine, err := getMachineForHost(ctx, vm.GetMachineLseId())
if err != nil {
return oldVM, err
}
// check permission for the new machine realm
if err := util.CheckPermission(ctx, util.InventoriesUpdate, machine.GetRealm()); err != nil {
return oldVM, err
}
oldVM.MachineLseId = vm.GetMachineLseId()
oldVM.Zone = machine.GetLocation().GetZone().String()
case "macAddress":
oldVM.MacAddress = vm.GetMacAddress()
case "resourceState":
oldVM.ResourceState = vm.GetResourceState()
case "osVersion":
if oldVM.GetOsVersion() == nil {
oldVM.OsVersion = &ufspb.OSVersion{
Value: vm.GetOsVersion().GetValue(),
}
} else {
oldVM.GetOsVersion().Value = vm.GetOsVersion().GetValue()
}
case "tags":
oldVM.Tags = mergeTags(oldVM.GetTags(), vm.GetTags())
case "description":
oldVM.Description = vm.Description
case "deploymentTicket":
oldVM.DeploymentTicket = vm.GetDeploymentTicket()
}
}
// return existing/old vm with new updated values
return oldVM, nil
}
// DeleteVM deletes a vm in datastore.
func DeleteVM(ctx context.Context, id string) error {
f := func(ctx context.Context) error {
hc := getVMHistoryClient(&ufspb.VM{Name: id})
//Get VM
vm, err := inventory.GetVM(ctx, id)
if err != nil {
return err
}
// Validate the input
if err := validateDeleteVM(ctx, vm); err != nil {
return err
}
if err := inventory.DeleteVM(ctx, id); err != nil {
return errors.Annotate(err, "Unable to delete vm %s", id).Err()
}
if err := hc.stUdt.deleteStateHelper(ctx); err != nil {
return err
}
if err := hc.netUdt.deleteDHCPHelper(ctx); err != nil {
return err
}
hc.LogVMChanges(vm, nil)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
logging.Errorf(ctx, "Failed to delete vm in datastore: %s", err)
return err
}
return nil
}
// ListVMs lists the vms
func ListVMs(ctx context.Context, pageSize int32, pageToken, filter string, keysOnly bool) ([]*ufspb.VM, string, error) {
var filterMap map[string][]interface{}
var err error
if filter != "" {
filterMap, err = getFilterMap(filter, inventory.GetVMIndexedFieldName)
if err != nil {
return nil, "", errors.Annotate(err, "Failed to read filter for listing vms").Err()
}
}
filterMap = resetStateFilter(filterMap)
filterMap = resetOSFilter(filterMap)
filterMap = resetZoneFilter(filterMap)
return inventory.ListVMs(ctx, pageSize, pageSize, pageToken, filterMap, keysOnly, nil)
}
// GetVM returns vm for the given id from datastore.
func GetVM(ctx context.Context, id string) (*ufspb.VM, error) {
return inventory.GetVM(ctx, id)
}
// BatchGetVMs returns a batch of vms from datastore.
func BatchGetVMs(ctx context.Context, ids []string) ([]*ufspb.VM, error) {
return inventory.BatchGetVMs(ctx, ids)
}
func getVMHistoryClient(m *ufspb.VM) *HistoryClient {
return &HistoryClient{
stUdt: &stateUpdater{
ResourceName: util.AddPrefix(util.VMCollection, m.Name),
},
netUdt: &networkUpdater{
Hostname: m.Name,
},
}
}
// validateCreateVM validates if a vm can be created
func validateCreateVM(ctx context.Context, vm *ufspb.VM, nwOpt *ufsAPI.NetworkOption, machine *ufspb.Machine) error {
// Check permission
if err := util.CheckPermission(ctx, util.InventoriesCreate, machine.GetRealm()); err != nil {
return err
}
// Aggregate resource to check if vm does not exist
if err := resourceAlreadyExists(ctx, []*Resource{GetVMResource(vm.Name)}, nil); err != nil {
return err
}
resourcesNotFound := make([]*Resource, 0)
if nwOpt.GetVlan() != "" {
resourcesNotFound = append(resourcesNotFound, GetVlanResource(nwOpt.GetVlan()))
}
if nwOpt.GetIp() != "" {
if _, err := util.IPv4StrToInt(nwOpt.GetIp()); err != nil {
return errors.Annotate(err, "Validate create vm").Tag(grpcutil.InvalidArgumentTag).Err()
}
}
// check if resources does not exist
if err := ResourceExist(ctx, resourcesNotFound, nil); err != nil {
return err
}
return nil
}
// validateUpdateVM validates if a vm can be updated
func validateUpdateVM(ctx context.Context, oldVM *ufspb.VM, vm *ufspb.VM, mask *field_mask.FieldMask) error {
// Check permission
machine, err := getMachineForHost(ctx, oldVM.GetMachineLseId())
if err != nil {
return err
}
if err := util.CheckPermission(ctx, util.InventoriesUpdate, machine.GetRealm()); err != nil {
return err
}
if vm.GetMachineLseId() != "" {
// check if resources does not exist
if err := ResourceExist(ctx, []*Resource{GetMachineLSEResource(vm.GetMachineLseId())}, nil); err != nil {
return err
}
}
return validateVMUpdateMask(vm, mask)
}
// validateVMUpdateMask validates the update mask for VM update
func validateVMUpdateMask(vm *ufspb.VM, mask *field_mask.FieldMask) error {
if mask != nil {
// validate the give field mask
for _, path := range mask.Paths {
switch path {
case "name":
return status.Error(codes.InvalidArgument, "validateVMUpdateMask - name cannot be updated, delete and create a new vm instead")
case "update_time":
return status.Error(codes.InvalidArgument, "validateVMUpdateMask - update_time cannot be updated, it is a output only field")
case "machineLseId":
if vm.MachineLseId == "" {
return status.Error(codes.InvalidArgument, "validateVMUpdateMask - machineLseId cannot be empty/nil.")
}
case "macAddress":
case "osVersion":
if vm.GetOsVersion() == nil {
return status.Error(codes.InvalidArgument, "validateUpdateVM - Osversion cannot be empty/nil.")
}
case "deploymentTicket":
case "tags":
case "description":
case "resourceState":
// valid fields, nothing to validate.
default:
return status.Errorf(codes.InvalidArgument, "validateUpdateVM - unsupported update mask path %q", path)
}
}
}
return nil
}
func validateDeleteVM(ctx context.Context, vm *ufspb.VM) error {
machine, err := getMachineForHost(ctx, vm.GetMachineLseId())
if err != nil {
return err
}
// Check permission
if err := util.CheckPermission(ctx, util.InventoriesDelete, machine.GetRealm()); err != nil {
return err
}
return nil
}
func getMachineForHost(ctx context.Context, lseName string) (*ufspb.Machine, error) {
// Get MachineLSE
lse, err := inventory.GetMachineLSE(ctx, lseName)
if err != nil {
return nil, errors.Annotate(err, "Fail to get host by %s", lseName).Err()
}
if len(lse.GetMachines()) == 0 {
return nil, errors.Annotate(err, "No machine for the host %s", lse.GetName()).Err()
}
//Get Machine
machine, err := registration.GetMachine(ctx, lse.GetMachines()[0])
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "machine %s not found", lse.GetMachines()[0])
}
return machine, nil
}