blob: 039bd043b853ca01f2a3aac240af757530f0b77d [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"
"fmt"
"strings"
"github.com/golang/protobuf/proto"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/gae/service/datastore"
"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/inventory"
"infra/unifiedfleet/app/model/registration"
ufsUtil "infra/unifiedfleet/app/util"
)
// CreateSwitch creates switch in datastore.
func CreateSwitch(ctx context.Context, s *ufspb.Switch) (*ufspb.Switch, error) {
// TODO(eshwarn): Add logic for Chrome OS
f := func(ctx context.Context) error {
hc := getSwitchHistoryClient(s)
// Get rack to associate the switch
rack, err := GetRack(ctx, s.GetRack())
if err != nil {
return err
}
// Validate the input
if err := validateCreateSwitch(ctx, s, rack); err != nil {
return err
}
// Fill the zone to switch OUTPUT only fields for indexing
s.Zone = rack.GetLocation().GetZone().String()
s.ResourceState = ufspb.State_STATE_SERVING
// Create a switch 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.BatchUpdateSwitches(ctx, []*ufspb.Switch{s}); err != nil {
return errors.Annotate(err, "Unable to create switch %s", s.Name).Err()
}
hc.LogSwitchChanges(nil, s)
// Update state
if err := hc.stUdt.updateStateHelper(ctx, ufspb.State_STATE_SERVING); err != nil {
return err
}
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
logging.Errorf(ctx, "Failed to create switch in datastore: %s", err)
return nil, err
}
return s, nil
}
// UpdateSwitch updates switch in datastore.
func UpdateSwitch(ctx context.Context, s *ufspb.Switch, mask *field_mask.FieldMask) (*ufspb.Switch, error) {
// TODO(eshwarn): Add logic for Chrome OS
f := func(ctx context.Context) error {
hc := getSwitchHistoryClient(s)
// Get old/existing switch
oldS, err := registration.GetSwitch(ctx, s.GetName())
if err != nil {
return errors.Annotate(err, "UpdateSwitch - get switch %s failed", s.GetName()).Err()
}
// Validate the input
if err := validateUpdateSwitch(ctx, oldS, s, mask); err != nil {
return errors.Annotate(err, "UpdateSwitch - validation failed").Err()
}
// Copy for logging
oldSwitchCopy := proto.Clone(oldS).(*ufspb.Switch)
// Fill the zone to switch OUTPUT only fields
s.Zone = oldS.GetZone()
// Partial update by field mask
if mask != nil && len(mask.Paths) > 0 {
s, err = processSwitchUpdateMask(ctx, oldS, s, mask)
if err != nil {
return errors.Annotate(err, "processing update mask failed").Err()
}
} else {
// This is for complete object input
if s.GetRack() == "" {
return status.Error(codes.InvalidArgument, "rack cannot be empty for updating a switch")
}
if oldS.GetRack() != s.GetRack() {
// User is trying to associate this switch with a different rack.
// Get rack to associate the switch
rack, err := GetRack(ctx, s.GetRack())
if err != nil {
return errors.Annotate(err, "UpdateSwitch - get rack %s failed", s.GetRack()).Err()
}
// check permission for the new rack realm
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsUpdate, rack.GetRealm()); err != nil {
return err
}
// Fill the zone to switch OUTPUT only fields for indexing
s.Zone = rack.GetLocation().GetZone().String()
}
}
// Update state
if err := hc.stUdt.updateStateHelper(ctx, s.GetResourceState()); err != nil {
return errors.Annotate(err, "Fail to update state to switch %s", s.GetName()).Err()
}
// Update switch entry
// we use this func as it is a non-atomic operation and can be used to
// run within a transaction. Datastore doesnt allow nested transactions.
if _, err := registration.BatchUpdateSwitches(ctx, []*ufspb.Switch{s}); err != nil {
return errors.Annotate(err, "UpdateSwitch - unable to batch update switch %s", s.Name).Err()
}
hc.LogSwitchChanges(oldSwitchCopy, s)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return nil, errors.Annotate(err, "UpdateSwitch - failed to update switch %s in datastore", s.Name).Err()
}
return s, nil
}
// processSwitchUpdateMask process update field mask to get only specific update
// fields and return a complete switch object with updated and existing fields
func processSwitchUpdateMask(ctx context.Context, oldSwitch *ufspb.Switch, s *ufspb.Switch, mask *field_mask.FieldMask) (*ufspb.Switch, error) {
// update the fields in the existing/old switch
for _, path := range mask.Paths {
switch path {
case "rack":
if oldSwitch.GetRack() != s.GetRack() {
// User is trying to associate this switch with a different rack.
// Get rack to associate the switch
rack, err := GetRack(ctx, s.GetRack())
if err != nil {
return oldSwitch, errors.Annotate(err, "UpdateSwitch - get rack %s failed", s.GetRack()).Err()
}
// check permission for the new rack realm
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsUpdate, rack.GetRealm()); err != nil {
return oldSwitch, err
}
oldSwitch.Rack = s.GetRack()
// Fill the zone to switch OUTPUT only fields for indexing
oldSwitch.Zone = rack.GetLocation().GetZone().String()
}
case "resourceState":
oldSwitch.ResourceState = s.GetResourceState()
case "description":
oldSwitch.Description = s.GetDescription()
case "capacity":
oldSwitch.CapacityPort = s.GetCapacityPort()
case "tags":
oldSwitch.Tags = mergeTags(oldSwitch.GetTags(), s.GetTags())
}
}
// return existing/old switch with new updated values
return oldSwitch, nil
}
// GetSwitch returns switch for the given id from datastore.
func GetSwitch(ctx context.Context, id string) (*ufspb.Switch, error) {
return registration.GetSwitch(ctx, id)
}
// BatchGetSwitches returns a batch of switches from datastore.
func BatchGetSwitches(ctx context.Context, ids []string) ([]*ufspb.Switch, error) {
return registration.BatchGetSwitches(ctx, ids)
}
// ListSwitches lists the switches
func ListSwitches(ctx context.Context, pageSize int32, pageToken, filter string, keysOnly bool) ([]*ufspb.Switch, string, error) {
var filterMap map[string][]interface{}
var err error
if filter != "" {
filterMap, err = getFilterMap(filter, registration.GetSwitchIndexedFieldName)
if err != nil {
return nil, "", errors.Annotate(err, "Failed to read filter for listing switches").Err()
}
}
filterMap = resetStateFilter(filterMap)
filterMap = resetZoneFilter(filterMap)
return registration.ListSwitches(ctx, pageSize, pageToken, filterMap, keysOnly)
}
// DeleteSwitch deletes the switch in datastore
//
// For referential data intergrity,
// 1. Validate if this switch is not referenced by other resources in the datastore.
// 2. Delete the switch
// 3. Get the rack associated with this switch
// 4. Update the rack by removing the association with this switch
func DeleteSwitch(ctx context.Context, id string) error {
return deleteSwitchHelper(ctx, id, true)
}
func deleteSwitchHelper(ctx context.Context, id string, inTransaction bool) error {
f := func(ctx context.Context) error {
hc := getSwitchHistoryClient(&ufspb.Switch{Name: id})
s, err := registration.GetSwitch(ctx, id)
if err != nil {
return errors.Annotate(err, "Unable to get switch").Err()
}
// Validate input
if err := validateDeleteSwitch(ctx, s); err != nil {
return errors.Annotate(err, "validation failed - Unable to delete switch %s", id).Err()
}
// Delete the switch
if err := registration.DeleteSwitch(ctx, id); err != nil {
return errors.Annotate(err, "delete failed - Unable to delete switch %s", id).Err()
}
// Update state
hc.stUdt.deleteStateHelper(ctx)
hc.LogSwitchChanges(s, nil)
return hc.SaveChangeEvents(ctx)
}
if inTransaction {
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
logging.Errorf(ctx, "Failed to delete switch in datastore: %s", err)
return err
}
return nil
}
return f(ctx)
}
// ReplaceSwitch replaces an old Switch with new Switch in datastore
//
// It does a delete of old switch and create of new Switch.
// All the steps are in done in a transaction to preserve consistency on failure.
// Before deleting the old Switch, it will get all the resources referencing
// the old Switch. It will update all the resources which were referencing
// the old Switch(got in the last step) with new Switch.
// Deletes the old Switch.
// Creates the new Switch.
// This will preserve data integrity in the system.
func ReplaceSwitch(ctx context.Context, oldSwitch *ufspb.Switch, newSwitch *ufspb.Switch) (*ufspb.Switch, error) {
// TODO(eshwarn) : implement replace after user testing the tool
return nil, nil
}
// RenameSwitch renames the switch and updates the associated machinelse in datastore
func RenameSwitch(ctx context.Context, oldSwitchName, newSwitchName string) (*ufspb.Switch, error) {
var s *ufspb.Switch
var err error
f := func(ctx context.Context) error {
// Get the old Switch
s, err = registration.GetSwitch(ctx, oldSwitchName)
if err != nil {
return errors.Annotate(err, "Unable to get switch %s", oldSwitchName).Err()
}
// Validate the input
if err := validateRenameSwitch(ctx, s, newSwitchName); err != nil {
return errors.Annotate(err, "validation failed").Err()
}
// Copy for logging
oldSwitchCopy := proto.Clone(s).(*ufspb.Switch)
hc := getSwitchHistoryClient(oldSwitchCopy)
if err := renameSwitchHelper(ctx, oldSwitchName, newSwitchName, hc); err != nil {
return err
}
// Delete the old switch
if err := registration.DeleteSwitch(ctx, oldSwitchName); err != nil {
return errors.Annotate(err, "unable to delete switch %s", oldSwitchName).Err()
}
// Create a new switch
s.Name = newSwitchName
if _, err = registration.BatchUpdateSwitches(ctx, []*ufspb.Switch{s}); err != nil {
return errors.Annotate(err, "unable to batch update switch %s", s.Name).Err()
}
hc.LogSwitchChanges(oldSwitchCopy, s)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return nil, errors.Annotate(err, "failed to rename switch: %s", oldSwitchName).Err()
}
return s, nil
}
// validateRenameSwitch validates if a Switch can be renamed
func validateRenameSwitch(ctx context.Context, oldSwitch *ufspb.Switch, newSwitchName string) error {
rack, err := registration.GetRack(ctx, oldSwitch.GetRack())
if err != nil {
return status.Errorf(codes.InvalidArgument, "rack %s not found", oldSwitch.GetRack())
}
// Check permission
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsUpdate, rack.GetRealm()); err != nil {
return err
}
// Check if new switch name already exists
if err := resourceAlreadyExists(ctx, []*Resource{GetSwitchResource(newSwitchName)}, nil); err != nil {
return err
}
return nil
}
func renameSwitchHelper(ctx context.Context, oldSwitchName, newSwitchName string, hc *HistoryClient) error {
// Update MachineLSE with new switch name
if err := updateIndexingForMachineLSE(ctx, "switch", oldSwitchName, newSwitchName, hc); err != nil {
return errors.Annotate(err, "failed to update indexing for hosts").Err()
}
// Update Nic with new switch name
if err := updateIndexingForNic(ctx, "switch", oldSwitchName, newSwitchName, hc); err != nil {
return errors.Annotate(err, "failed to update indexing for hosts").Err()
}
// Update Drac with new switch name
if err := updateIndexingForDrac(ctx, "switch", oldSwitchName, newSwitchName, hc); err != nil {
return errors.Annotate(err, "failed to update indexing for dracs").Err()
}
return nil
}
// validateDeleteSwitch validates if a Switch can be deleted
//
// Checks if this Switch(SwitchID) is not referenced by other resources in the datastore.
// If there are any other references, delete will be rejected and an error will be returned.
func validateDeleteSwitch(ctx context.Context, s *ufspb.Switch) error {
rack, err := registration.GetRack(ctx, s.GetRack())
if err != nil {
return errors.Annotate(err, "unable to get rack %s", s.GetRack()).Err()
}
// Check permission
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsDelete, rack.GetRealm()); err != nil {
return err
}
nics, err := registration.QueryNicByPropertyName(ctx, "switch_id", s.GetName(), true)
if err != nil {
return err
}
dracs, err := registration.QueryDracByPropertyName(ctx, "switch_id", s.GetName(), true)
if err != nil {
return err
}
machinelses, err := inventory.QueryMachineLSEByPropertyName(ctx, "switch_id", s.GetName(), true)
if err != nil {
return err
}
if len(nics) > 0 || len(dracs) > 0 || len(machinelses) > 0 {
var errorMsg strings.Builder
errorMsg.WriteString(fmt.Sprintf("Switch %s cannot be deleted because there are other resources which are referring to this Switch.", s.GetName()))
if len(nics) > 0 {
errorMsg.WriteString(fmt.Sprintf("\nNics referring to the Switch:\n"))
for _, nic := range nics {
errorMsg.WriteString(nic.Name + ", ")
}
}
if len(dracs) > 0 {
errorMsg.WriteString(fmt.Sprintf("\nDracs referring to the Switch:\n"))
for _, drac := range dracs {
errorMsg.WriteString(drac.Name + ", ")
}
}
if len(machinelses) > 0 {
errorMsg.WriteString(fmt.Sprintf("\nChromeOS hosts referring to the Switch:\n"))
for _, machinelse := range machinelses {
errorMsg.WriteString(machinelse.Name + ", ")
}
}
logging.Errorf(ctx, errorMsg.String())
return status.Errorf(codes.FailedPrecondition, errorMsg.String())
}
return nil
}
// validateCreateSwitch validates if a switch can be created
//
// check if the switch already exists
// check if the rack does not exist
func validateCreateSwitch(ctx context.Context, s *ufspb.Switch, rack *ufspb.Rack) error {
// Check permission
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsCreate, rack.GetRealm()); err != nil {
return err
}
// Check if switch already exists
if err := resourceAlreadyExists(ctx, []*Resource{GetSwitchResource(s.Name)}, nil); err != nil {
return err
}
return nil
}
// validateUpdateSwitch validates if a switch can be updated
//
// check if switch and rack does not exist
func validateUpdateSwitch(ctx context.Context, oldS *ufspb.Switch, s *ufspb.Switch, mask *field_mask.FieldMask) error {
rack, err := registration.GetRack(ctx, oldS.GetRack())
if err != nil {
return status.Errorf(codes.InvalidArgument, "rack %s not found", oldS.GetRack())
}
// Check permission
if err := ufsUtil.CheckPermission(ctx, ufsUtil.RegistrationsUpdate, rack.GetRealm()); err != nil {
return err
}
// Aggregate resource to check if rack does not exist
if s.GetRack() != "" {
resourcesNotFound := []*Resource{GetRackResource(s.GetRack())}
// check if resources does not exist
if err := ResourceExist(ctx, resourcesNotFound, nil); err != nil {
return err
}
}
return validateSwitchUpdateMask(s, mask)
}
// validateSwitchUpdateMask validates the update mask for switch update
func validateSwitchUpdateMask(s *ufspb.Switch, 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, "validateUpdateSwitch - name cannot be updated, delete and create a new switch instead")
case "update_time":
return status.Error(codes.InvalidArgument, "validateUpdateSwitch - update_time cannot be updated, it is a Output only field")
case "rack":
if s.GetRack() == "" {
return status.Error(codes.InvalidArgument, "rack cannot be empty for updating a switch")
}
case "capacity":
case "description":
case "tags":
case "resourceState":
// valid fields, nothing to validate.
default:
return status.Errorf(codes.InvalidArgument, "validateUpdateSwitch - unsupported update mask path %q", path)
}
}
}
return nil
}
func getSwitchHistoryClient(m *ufspb.Switch) *HistoryClient {
return &HistoryClient{
stUdt: &stateUpdater{
ResourceName: ufsUtil.AddPrefix(ufsUtil.SwitchCollection, m.Name),
},
netUdt: &networkUpdater{
Hostname: m.Name,
},
}
}