blob: 7d4eeeea1eda5d7dcf3270c894b826502b2f01ad [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"
crimson "go.chromium.org/luci/machine-db/api/crimson/v1"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
ufspb "infra/unifiedfleet/api/v1/models"
"infra/unifiedfleet/app/model/configuration"
ufsds "infra/unifiedfleet/app/model/datastore"
"infra/unifiedfleet/app/model/inventory"
"infra/unifiedfleet/app/model/registration"
ufsUtil "infra/unifiedfleet/app/util"
)
// CreateNic creates a new nic in datastore.
//
// Checks if the resources referenced by the Nic input already exists
// in the system before creating a new Nic
func CreateNic(ctx context.Context, nic *ufspb.Nic) (*ufspb.Nic, error) {
f := func(ctx context.Context) error {
hc := getNicHistoryClient(nic)
// Get browser machine to associate the nic
machine, err := getBrowserMachine(ctx, nic.GetMachine())
if err != nil {
return errors.Annotate(err, "CreateNic - failed to get machine %s", nic.GetMachine()).Err()
}
// Validate the input
if err := validateCreateNic(ctx, nic, machine); err != nil {
return errors.Annotate(err, "CreateNic - validation failed").Err()
}
// Fill the rack/zone to nic OUTPUT only fields for indexing nic table
nic.Rack = machine.GetLocation().GetRack()
nic.Zone = machine.GetLocation().GetZone().String()
// Create a nic entry
// we use this func as it is a non-atomic operation and can be used to
// run within a transaction to make it atomic. Datastore doesnt allow
// nested transactions.
if _, err = registration.BatchUpdateNics(ctx, []*ufspb.Nic{nic}); err != nil {
return errors.Annotate(err, "CreateNic - unable to batch update nic %s", nic.Name).Err()
}
hc.LogNicChanges(nil, nic)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return nil, errors.Annotate(err, "CreateNic - unable to create nic %s", nic.Name).Err()
}
return nic, nil
}
// UpdateNic updates nic in datastore.
//
// Checks if the resources referenced by the Nic input already exists
// in the system before updating a Nic
func UpdateNic(ctx context.Context, nic *ufspb.Nic, mask *field_mask.FieldMask) (*ufspb.Nic, error) {
f := func(ctx context.Context) error {
hc := getNicHistoryClient(nic)
// Get old/existing nic
oldNic, err := registration.GetNic(ctx, nic.GetName())
if err != nil {
return errors.Annotate(err, "UpdateNic - get nic %s failed", nic.GetName()).Err()
}
// Validate the input
if err := validateUpdateNic(ctx, oldNic, nic, mask); err != nil {
return errors.Annotate(err, "UpdateNic - validation failed").Err()
}
oldNicCopy := proto.Clone(oldNic).(*ufspb.Nic)
// Copy the rack/zone to nic OUTPUT only fields from already existing nic
nic.Rack = oldNic.GetRack()
nic.Zone = oldNic.GetZone()
// Partial update by field mask
if mask != nil && len(mask.Paths) > 0 {
nic, err = processNicUpdateMask(ctx, oldNic, nic, mask)
if err != nil {
return errors.Annotate(err, "UpdateNic - processing update mask failed").Err()
}
} else {
// This is for complete object input
if nic.GetMachine() == "" {
return status.Error(codes.InvalidArgument, "Machine cannot be empty for updating a drac")
}
}
// Check if user provided new machine to associate the nic
if nic.GetMachine() != oldNicCopy.GetMachine() {
newMachine, err := verifyNewMachine(ctx, nic.GetMachine())
if err != nil {
return err
}
// Fill the rack/zone to nic OUTPUT only fields
nic.Rack = newMachine.GetLocation().GetRack()
nic.Zone = newMachine.GetLocation().GetZone().String()
// Update corresponding lses/dhcps/mac address
if err := updateForMachineChange(ctx, nic, oldNicCopy.GetMachine(), nic.GetMachine(), newMachine.GetRealm(), hc); err != nil {
return err
}
} else if nic.GetMacAddress() != oldNicCopy.GetMacAddress() {
lses, err := inventory.QueryMachineLSEByPropertyName(ctx, "machine_ids", nic.GetMachine(), false)
if err != nil {
return err
}
if len(lses) > 0 {
logging.Infof(ctx, "updating corresponding dhcp record for host %q", lses[0].GetName())
// Set the correct hostname for the nic's corresponding dhcp configs
hc.netUdt.Hostname = lses[0].GetName()
if _, err := hc.netUdt.updateDHCPWithMac(ctx, nic.GetMacAddress()); err != nil {
return err
}
}
}
// Update nic entry
if _, err := registration.BatchUpdateNics(ctx, []*ufspb.Nic{nic}); err != nil {
return errors.Annotate(err, "UpdateNic - unable to batch update nic %s", nic.Name).Err()
}
hc.LogNicChanges(oldNicCopy, nic)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return nil, errors.Annotate(err, "UpdateNic - failed to update nic %s in datastore", nic.Name).Err()
}
return nic, nil
}
func verifyNewMachine(ctx context.Context, newMachineName string) (*ufspb.Machine, error) {
// Get browser machine to associate the nic
machine, err := getBrowserMachine(ctx, newMachineName)
if err != nil {
return nil, errors.Annotate(err, "UpdateNic - failed to get browser machine %s", newMachineName).Err()
}
// check permission for the new machine realm
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsUpdate, machine.GetRealm()); err != nil {
return nil, err
}
return machine, nil
}
func updateForMachineChange(ctx context.Context, nic *ufspb.Nic, oldMachineName, newMachineName, realm string, hc *HistoryClient) error {
logging.Infof(ctx, "updating corresponding machine lses")
lses, err := inventory.QueryMachineLSEByPropertyName(ctx, "machine_ids", newMachineName, false)
if err != nil {
return err
}
oldLses, err := inventory.QueryMachineLSEByPropertyName(ctx, "machine_ids", oldMachineName, false)
if err != nil {
return err
}
var newLseName string
var lsesToUpdate []*ufspb.MachineLSE
var lsesBeforeUpdate []*ufspb.MachineLSE
if len(lses) > 0 {
newLseName = lses[0].GetName()
lsesBeforeUpdate = append(lsesBeforeUpdate, proto.Clone(lses[0]).(*ufspb.MachineLSE))
lses[0].Nic = nic.GetName()
lsesToUpdate = append(lsesToUpdate, lses[0])
}
var oldLseName string
if len(oldLses) > 0 {
oldLseName = oldLses[0].GetName()
lsesBeforeUpdate = append(lsesBeforeUpdate, proto.Clone(oldLses[0]).(*ufspb.MachineLSE))
oldLses[0].Nic = ""
lsesToUpdate = append(lsesToUpdate, oldLses[0])
}
if len(lsesToUpdate) > 0 {
// check permission for potential updating LSE/dhcp
if err := ufsUtil.CheckPermission(ctx, ufsUtil.InventoriesUpdate, realm); err != nil {
return err
}
if _, err := inventory.BatchUpdateMachineLSEs(ctx, lsesToUpdate); err != nil {
return err
}
// log lse change event
for i, old := range lsesBeforeUpdate {
hc.LogMachineLSEChanges(old, lsesToUpdate[i])
}
}
// Update corresponding dhcp records
logging.Infof(ctx, "Updating dhcp record from %q to %q", oldLseName, newLseName)
if _, err := hc.netUdt.renameDHCP(ctx, oldLseName, newLseName, nic.GetMacAddress()); err != nil {
return err
}
return nil
}
// processNicUpdateMask process update field mask to get only specific update
// fields and return a complete nic object with updated and existing fields
func processNicUpdateMask(ctx context.Context, oldNic *ufspb.Nic, nic *ufspb.Nic, mask *field_mask.FieldMask) (*ufspb.Nic, error) {
// update the fields in the existing/old nic
for _, path := range mask.Paths {
switch path {
case "machine":
oldNic.Machine = nic.GetMachine()
case "macAddress":
oldNic.MacAddress = nic.GetMacAddress()
case "switch":
if oldNic.GetSwitchInterface() == nil {
oldNic.SwitchInterface = &ufspb.SwitchInterface{
Switch: nic.GetSwitchInterface().GetSwitch(),
}
} else {
oldNic.GetSwitchInterface().Switch = nic.GetSwitchInterface().GetSwitch()
}
case "portName":
if oldNic.GetSwitchInterface() == nil {
oldNic.SwitchInterface = &ufspb.SwitchInterface{
PortName: nic.GetSwitchInterface().GetPortName(),
}
} else {
oldNic.GetSwitchInterface().PortName = nic.GetSwitchInterface().GetPortName()
}
case "tags":
oldNic.Tags = mergeTags(oldNic.GetTags(), nic.GetTags())
}
}
// For partial update, validate switch interface just before updating in case
// before we checks the incompleted interface
if err := validateNicSwitchPort(ctx, oldNic.GetName(), oldNic.GetMachine(), oldNic.GetSwitchInterface()); err != nil {
return oldNic, err
}
// return existing/old nic with new updated values
return oldNic, nil
}
// GetNic returns nic for the given id from datastore.
func GetNic(ctx context.Context, id string) (*ufspb.Nic, error) {
return registration.GetNic(ctx, id)
}
// BatchGetNics returns a batch of nics from datastore.
func BatchGetNics(ctx context.Context, ids []string) ([]*ufspb.Nic, error) {
return registration.BatchGetNics(ctx, ids)
}
// ListNics lists the nics
func ListNics(ctx context.Context, pageSize int32, pageToken, filter string, keysOnly bool) ([]*ufspb.Nic, string, error) {
var filterMap map[string][]interface{}
var err error
if filter != "" {
filterMap, err = getFilterMap(filter, registration.GetNicIndexedFieldName)
if err != nil {
return nil, "", errors.Annotate(err, "Failed to read filter for listing nics").Err()
}
}
filterMap = resetZoneFilter(filterMap)
return registration.ListNics(ctx, pageSize, pageToken, filterMap, keysOnly)
}
// DeleteNic deletes the nic in datastore
func DeleteNic(ctx context.Context, id string) error {
return deleteNicHelper(ctx, id, true)
}
func deleteNicHelper(ctx context.Context, id string, inTransaction bool) error {
f := func(ctx context.Context) error {
hc := getNicHistoryClient(&ufspb.Nic{Name: id})
nic, err := registration.GetNic(ctx, id)
if err != nil {
return errors.Annotate(err, "DeleteNic - Unable to get nic").Err()
}
// Validate the input
if err := validateDeleteNic(ctx, nic); err != nil {
return errors.Annotate(err, "DeleteNic - validation failed").Err()
}
// Delete the nic
if err := registration.DeleteNic(ctx, id); err != nil {
return errors.Annotate(err, "DeleteNic - unable to delete nic %s", id).Err()
}
hc.LogNicChanges(nic, nil)
return hc.SaveChangeEvents(ctx)
}
if inTransaction {
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return errors.Annotate(err, "DeleteNic - failed to delete nic in datastore: %s", id).Err()
}
return nil
}
return f(ctx)
}
// ImportNetworkInterfaces creates or updates a batch of nics, dracs, and dhcps in datastore
func ImportNetworkInterfaces(ctx context.Context, nics []*crimson.NIC, dracs []*crimson.DRAC, machines []*crimson.Machine, pageSize int) (*ufsds.OpResults, error) {
var allRes ufsds.OpResults
newNics, newDracs, dhcps, _, _ := ufsUtil.ProcessNetworkInterfaces(nics, dracs, machines)
// Please note that the importing here is not in one transaction, which
// actually may cause data incompleteness. But as the importing job
// will be triggered periodically, such incompleteness that's caused by
// potential failure will be ignored.
deleteNonExistingNics(ctx, newNics, pageSize)
logging.Infof(ctx, "Importing %d nics", len(newNics))
for i := 0; ; i += pageSize {
end := ufsUtil.Min(i+pageSize, len(newNics))
res, err := registration.ImportNics(ctx, newNics[i:end])
allRes = append(allRes, *res...)
if err != nil {
return &allRes, err
}
if i+pageSize >= len(newNics) {
break
}
}
deleteNonExistingDracs(ctx, newDracs, pageSize)
logging.Infof(ctx, "Importing %d dracs", len(newDracs))
for i := 0; ; i += pageSize {
end := ufsUtil.Min(i+pageSize, len(newDracs))
res, err := registration.ImportDracs(ctx, newDracs[i:end])
allRes = append(allRes, *res...)
if err != nil {
return &allRes, err
}
if i+pageSize >= len(newDracs) {
break
}
}
logging.Infof(ctx, "Importing %d dhcps", len(dhcps))
for i := 0; ; i += pageSize {
end := ufsUtil.Min(i+pageSize, len(dhcps))
res, err := configuration.ImportDHCPConfigs(ctx, dhcps[i:end])
allRes = append(allRes, *res...)
if err != nil {
return &allRes, err
}
if i+pageSize >= len(dhcps) {
break
}
}
return &allRes, nil
}
func deleteNonExistingNics(ctx context.Context, nics []*ufspb.Nic, pageSize int) (*ufsds.OpResults, error) {
resMap := make(map[string]bool)
for _, r := range nics {
resMap[r.GetName()] = true
}
resp, err := registration.GetAllNics(ctx)
if err != nil {
return nil, err
}
var toDelete []string
for _, sr := range resp.Passed() {
s := sr.Data.(*ufspb.Nic)
if _, ok := resMap[s.GetName()]; !ok {
toDelete = append(toDelete, s.GetName())
}
}
logging.Infof(ctx, "Deleting %d non-existing nics", len(toDelete))
return deleteByPage(ctx, toDelete, pageSize, registration.DeleteNics), nil
}
func deleteNonExistingDracs(ctx context.Context, dracs []*ufspb.Drac, pageSize int) (*ufsds.OpResults, error) {
resMap := make(map[string]bool)
for _, r := range dracs {
resMap[r.GetName()] = true
}
resp, err := registration.GetAllDracs(ctx)
if err != nil {
return nil, err
}
var toDelete []string
for _, sr := range resp.Passed() {
s := sr.Data.(*ufspb.Drac)
if _, ok := resMap[s.GetName()]; !ok {
toDelete = append(toDelete, s.GetName())
}
}
logging.Infof(ctx, "Deleting %d non-existing dracs", len(toDelete))
logging.Infof(ctx, "Deleting %d non-existing drac-related dhcps", len(toDelete))
allRes := *deleteByPage(ctx, toDelete, pageSize, registration.DeleteDracs)
allRes = append(allRes, *deleteByPage(ctx, toDelete, pageSize, configuration.DeleteDHCPs)...)
return &allRes, nil
}
// ReplaceNic replaces an old Nic with new Nic in datastore
//
// It does a delete of old nic and create of new Nic.
// All the steps are in done in a transaction to preserve consistency on failure.
// Before deleting the old Nic, it will get all the resources referencing
// the old Nic. It will update all the resources which were referencing
// the old Nic(got in the last step) with new Nic.
// Deletes the old Nic.
// Creates the new Nic.
// This will preserve data integrity in the system.
func ReplaceNic(ctx context.Context, oldNic *ufspb.Nic, newNic *ufspb.Nic) (*ufspb.Nic, error) {
// TODO(eshwarn) : implement replace after user testing the tool
return nil, nil
}
// validateDeleteNic validates if a nic can be deleted
func validateDeleteNic(ctx context.Context, nic *ufspb.Nic) error {
machine, err := registration.GetMachine(ctx, nic.GetMachine())
if err != nil {
return errors.Annotate(err, "validateDeleteNic - unable to get machine %s", nic.GetMachine()).Err()
}
// Check permission
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsDelete, machine.GetRealm()); err != nil {
return err
}
// Get the machinelse associated with the nic
lses, err := inventory.QueryMachineLSEByPropertyName(ctx, "machine_ids", nic.GetMachine(), false)
if err != nil {
return errors.Annotate(err, "validateDeleteNic - failed to query host by machine %s", nic.GetMachine()).Err()
}
for _, lse := range lses {
if lse.GetNic() == nic.GetName() {
return status.Errorf(codes.InvalidArgument, "validateDeleteNic - nic %s is used by host %s", nic.GetName(), lse.GetName())
}
}
return nil
}
// validateCreateNic validates if a nic can be created
//
// check if the nic already exists
// checks if the machine and resources referenced by the nic does not exist
func validateCreateNic(ctx context.Context, nic *ufspb.Nic, machine *ufspb.Machine) error {
// Check permission
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsCreate, machine.GetRealm()); err != nil {
return err
}
// Check if nic already exists
if err := resourceAlreadyExists(ctx, []*Resource{GetNicResource(nic.Name)}, nil); err != nil {
return err
}
if err := validateMacAddress(ctx, nic.GetName(), nic.GetMacAddress()); err != nil {
return err
}
if err := validateNicSwitchPort(ctx, nic.GetName(), nic.GetMachine(), nic.GetSwitchInterface()); err != nil {
return err
}
// Aggregate resource to check if resources referenced by the nic does not exist
if switchID := nic.GetSwitchInterface().GetSwitch(); switchID != "" {
if err := ResourceExist(ctx, []*Resource{GetSwitchResource(switchID)}, nil); err != nil {
return err
}
}
return nil
}
// validateUpdateNic validates if a nic can be updated
//
// checks if nic, machine and resources referecned by the nic does not exist
func validateUpdateNic(ctx context.Context, oldNic *ufspb.Nic, nic *ufspb.Nic, mask *field_mask.FieldMask) error {
machine, err := registration.GetMachine(ctx, oldNic.GetMachine())
if err != nil {
return status.Errorf(codes.InvalidArgument, "machine %s not found", oldNic.GetMachine())
}
// Check permission
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsUpdate, machine.GetRealm()); err != nil {
return err
}
// Aggregate resource to check if does not exist
var resourcesNotFound []*Resource
// Aggregate resource to check if machine does not exist
if nic.GetMachine() != "" {
resourcesNotFound = append(resourcesNotFound, GetMachineResource(nic.GetMachine()))
}
// Aggregate resource to check if resources referenced by the nic does not exist
if switchID := nic.GetSwitchInterface().GetSwitch(); switchID != "" {
resourcesNotFound = append(resourcesNotFound, GetSwitchResource(switchID))
}
// check if resources does not exist
if err := ResourceExist(ctx, resourcesNotFound, nil); err != nil {
return err
}
// Check partial update first to avoid unnecessary validations
if err := validateNicUpdateMask(ctx, nic, mask); err != nil {
return err
}
if err := validateMacAddress(ctx, nic.GetName(), nic.GetMacAddress()); err != nil {
return err
}
if err := validateNicSwitchPort(ctx, nic.GetName(), nic.GetMachine(), nic.GetSwitchInterface()); err != nil {
return err
}
return nil
}
// validateNicUpdateMask validates the update mask for nic update
func validateNicUpdateMask(ctx context.Context, nic *ufspb.Nic, 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, "validateNicUpdateMask - name cannot be updated, delete and create a new nic instead")
case "update_time":
return status.Error(codes.InvalidArgument, "validateNicUpdateMask - update_time cannot be updated, it is a Output only field")
case "switch":
fallthrough
case "portName":
// Check switch interface validity in processNicUpdateMask later.
if nic.GetSwitchInterface() == nil {
return status.Error(codes.InvalidArgument, "validateNicUpdateMask - switch interface cannot be empty/nil.")
}
case "machine":
if nic.GetMachine() == "" {
status.Error(codes.InvalidArgument, "validateNicUpdateMask - machine cannot be empty")
}
case "macAddress":
if err := validateMacAddress(ctx, nic.GetName(), nic.GetMacAddress()); err != nil {
return err
}
case "tags":
// valid fields, nothing to validate.
default:
return status.Errorf(codes.InvalidArgument, "validateNicUpdateMask - unsupported update mask path %q", path)
}
}
}
return nil
}
func getNicHistoryClient(m *ufspb.Nic) *HistoryClient {
return &HistoryClient{
stUdt: &stateUpdater{
ResourceName: ufsUtil.AddPrefix(ufsUtil.NicCollection, m.Name),
},
netUdt: &networkUpdater{
Hostname: m.Name,
},
}
}
// RenameNic renames the nic and updates the associated machinelse in datastore
func RenameNic(ctx context.Context, oldNicName, newNicName string) (*ufspb.Nic, error) {
var nic *ufspb.Nic
var err error
f := func(ctx context.Context) error {
// Get the old Nic
nic, err = registration.GetNic(ctx, oldNicName)
if err != nil {
return errors.Annotate(err, "Unable to get nic %s", oldNicName).Err()
}
// Validate the input
if err := validateRenameNic(ctx, nic, newNicName); err != nil {
return errors.Annotate(err, "validation failed").Err()
}
// Copy for logging
oldNicCopy := proto.Clone(nic).(*ufspb.Nic)
hc := getNicHistoryClient(oldNicCopy)
// Update MachineLSE with new nic name
if err := updateIndexingForMachineLSE(ctx, "nic", oldNicName, newNicName, hc); err != nil {
return errors.Annotate(err, "failed to update indexing for hosts").Err()
}
// Delete the old nic
if err := registration.DeleteNic(ctx, oldNicName); err != nil {
return errors.Annotate(err, "unable to delete nic %s", oldNicName).Err()
}
// Create a new nic
nic.Name = newNicName
if _, err = registration.BatchUpdateNics(ctx, []*ufspb.Nic{nic}); err != nil {
return errors.Annotate(err, "unable to batch update nic %s", nic.Name).Err()
}
hc.LogNicChanges(oldNicCopy, nic)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return nil, errors.Annotate(err, "failed to rename nic: %s", oldNicName).Err()
}
return nic, nil
}
// validateRenameNic validates if a Nic can be renamed
func validateRenameNic(ctx context.Context, oldNic *ufspb.Nic, newNicName string) error {
machine, err := registration.GetMachine(ctx, oldNic.GetMachine())
if err != nil {
return status.Errorf(codes.InvalidArgument, "machine %s not found", oldNic.GetMachine())
}
// Check permission
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsUpdate, machine.GetRealm()); err != nil {
return err
}
// Check if new nic name already exists
if err := resourceAlreadyExists(ctx, []*Resource{GetNicResource(newNicName)}, nil); err != nil {
return err
}
return nil
}
// updateIndexingForNic updates indexing for Nic tables
// can be used inside a transaction
func updateIndexingForNic(ctx context.Context, property, oldValue, newValue string, hc *HistoryClient) error {
var nics []*ufspb.Nic
var err error
switch property {
case "switch":
nics, err = registration.QueryNicByPropertyName(ctx, "switch_id", oldValue, false)
if err != nil {
return errors.Annotate(err, "failed to query nics for switch %s", newValue).Err()
}
for _, nic := range nics {
oldNicCopy := proto.Clone(nic).(*ufspb.Nic)
nic.GetSwitchInterface().Switch = newValue
hc.LogNicChanges(oldNicCopy, nic)
}
}
if _, err := registration.BatchUpdateNics(ctx, nics); err != nil {
return errors.Annotate(err, "unable to batch update nics").Err()
}
return nil
}