blob: b320a0c4aa2fa82967b230e29166716027c7f879 [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"
"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"
crimsonconfig "go.chromium.org/luci/machine-db/api/config/v1"
"google.golang.org/genproto/protobuf/field_mask"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"infra/libs/git"
"infra/libs/sheet"
ufspb "infra/unifiedfleet/api/v1/models"
ufsAPI "infra/unifiedfleet/api/v1/rpc"
"infra/unifiedfleet/app/config"
"infra/unifiedfleet/app/model/configuration"
ufsds "infra/unifiedfleet/app/model/datastore"
"infra/unifiedfleet/app/model/inventory"
"infra/unifiedfleet/app/util"
)
// CreateVlan creates a new vlan in datastore.
func CreateVlan(ctx context.Context, vlan *ufspb.Vlan) (*ufspb.Vlan, error) {
setRealmForVlan(vlan)
var ips []*ufspb.IP
var length int
var err error
var freeStartIP, freeEndIP string
f := func(ctx context.Context) error {
hc := getVlanHistoryClient(vlan)
if err := validateCreateVlan(ctx, vlan); err != nil {
return errors.Annotate(err, "CreateVlan - validation failed").Err()
}
ips, length, freeStartIP, freeEndIP, err = util.ParseVlan(vlan.GetName(), vlan.GetVlanAddress(), vlan.GetFreeStartIpv4Str(), vlan.GetFreeEndIpv4Str())
if err != nil {
return errors.Annotate(err, "CreateVlan").Err()
}
vlan.CapacityIp = int32(length)
vlan.ResourceState = ufspb.State_STATE_SERVING
vlan.VlanNumber = util.GetSuffixAfterSeparator(vlan.GetName(), ":")
vlan.FreeStartIpv4Str = freeStartIP
vlan.FreeEndIpv4Str = freeEndIP
if _, err = configuration.BatchUpdateVlans(ctx, []*ufspb.Vlan{vlan}); err != nil {
return err
}
hc.stUdt.updateStateHelper(ctx, ufspb.State_STATE_SERVING)
hc.LogVLANChanges(nil, vlan)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return nil, errors.Annotate(err, "CreateVlan - unable to create vlan %s", vlan.Name).Err()
}
// Cannot update more than 500 entities in one transaction
logging.Debugf(ctx, "Updating %d ips", len(ips))
for i := 0; ; i += util.OperationPageSize {
end := util.Min(i+util.OperationPageSize, len(ips))
configuration.BatchUpdateIPs(ctx, ips[i:end])
if i+util.OperationPageSize >= len(ips) {
break
}
}
return vlan, nil
}
// UpdateVlan updates vlan in datastore.
func UpdateVlan(ctx context.Context, vlan *ufspb.Vlan, mask *field_mask.FieldMask) (*ufspb.Vlan, error) {
// Only in case of full update
if mask == nil || len(mask.Paths) == 0 {
// Set the realm for vlan
setRealmForVlan(vlan)
}
f := func(ctx context.Context) error {
hc := getVlanHistoryClient(vlan)
oldVlan, err := configuration.GetVlan(ctx, vlan.GetName())
if err != nil {
return errors.Annotate(err, "UpdateVlan - fail to get old vlan").Err()
}
if err := validateUpdateVlan(ctx, oldVlan, vlan, mask); err != nil {
return errors.Annotate(err, "UpdateVlan - validation failed").Err()
}
// Copy for logging
oldVlanCopy := proto.Clone(oldVlan).(*ufspb.Vlan)
// Copy the not-allowed change fields
vlan.VlanAddress = oldVlan.GetVlanAddress()
vlan.CapacityIp = oldVlan.GetCapacityIp()
vlan.VlanNumber = oldVlan.GetVlanNumber()
if err := validateReservedIPs(ctx, vlan); err != nil {
return err
}
// Partial update by field mask
if mask != nil && len(mask.Paths) > 0 {
vlan, err = processVlanUpdateMask(oldVlan, vlan, mask)
if err != nil {
return errors.Annotate(err, "UpdateVlan - processing update mask failed").Err()
}
}
// update state
if err := hc.stUdt.updateStateHelper(ctx, vlan.GetResourceState()); err != nil {
return errors.Annotate(err, "Fail to update state to vlan %s", vlan.GetName()).Err()
}
// ip range of the vlan may be changed
if err := hc.netUdt.updateVlanAndIPTable(ctx, vlan); err != nil {
return err
}
if _, err := configuration.BatchUpdateVlans(ctx, []*ufspb.Vlan{vlan}); err != nil {
return errors.Annotate(err, "UpdateVlan - unable to batch update vlan %s", vlan.Name).Err()
}
hc.LogVLANChanges(oldVlanCopy, vlan)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return nil, errors.Annotate(err, "UpdateVlan - unable to update vlan %s", vlan.Name).Err()
}
return vlan, nil
}
// GetVlan returns vlan for the given id from datastore.
func GetVlan(ctx context.Context, id string) (*ufspb.Vlan, error) {
return configuration.GetVlan(ctx, id)
}
// BatchGetVlans returns a batch of vlans from datastore.
func BatchGetVlans(ctx context.Context, ids []string) ([]*ufspb.Vlan, error) {
return configuration.BatchGetVlans(ctx, ids)
}
// ListVlans lists the vlans
func ListVlans(ctx context.Context, pageSize int32, pageToken, filter string, keysOnly bool) ([]*ufspb.Vlan, string, error) {
var filterMap map[string][]interface{}
var err error
if filter != "" {
filterMap, err = getFilterMap(filter, configuration.GetVlanIndexedFieldName)
if err != nil {
return nil, "", errors.Annotate(err, "Failed to read filter for listing vlans").Err()
}
}
filterMap = resetStateFilter(filterMap)
filterMap = resetZoneFilter(filterMap)
return configuration.ListVlans(ctx, pageSize, pageToken, filterMap, keysOnly)
}
// DeleteVlan deletes the vlan in datastore
//
// For referential data intergrity,
// Delete if this Vlan is not referenced by other resources in the datastore.
// If there are any references, delete will be rejected and an error will be returned.
func DeleteVlan(ctx context.Context, id string) error {
f := func(ctx context.Context) error {
hc := getVlanHistoryClient(&ufspb.Vlan{Name: id})
vlan, err := configuration.GetVlan(ctx, id)
if err != nil {
return errors.Annotate(err, "fail to get old vlan").Err()
}
err = validateDeleteVlan(ctx, vlan)
if err != nil {
return errors.Annotate(err, "DeleteVlan - validation failed").Err()
}
if err := configuration.DeleteVlan(ctx, id); err != nil {
return err
}
hc.LogVLANChanges(nil, vlan)
return hc.SaveChangeEvents(ctx)
}
if err := datastore.RunInTransaction(ctx, f, nil); err != nil {
return errors.Annotate(err, "DeleteVlan - unable to delete vlan %s", id).Err()
}
ips, _ := configuration.QueryIPByPropertyName(ctx, map[string]string{
"vlan": id,
})
// Cannot update more than 500 entities in one transaction
if len(ips) > 0 {
logging.Debugf(ctx, "deleting %d ips for vlan %s", len(ips), id)
ipIDs := ufsAPI.ParseResources(ips, "Id")
for i := 0; ; i += util.OperationPageSize {
end := util.Min(i+util.OperationPageSize, len(ipIDs))
configuration.BatchDeleteIPs(ctx, ipIDs[i:end])
if i+util.OperationPageSize >= len(ipIDs) {
break
}
}
}
return nil
}
// ImportVlans implements the logic of importing vlans and related info to backend storage.
//
// The function will return:
// * all of the results of the operations that already run
// * the first error that it meets
//
// The function will stop at the very first error.
func ImportVlans(ctx context.Context, vlans []*crimsonconfig.VLAN, pageSize int) (*ufsds.OpResults, error) {
IPs := make([]*ufspb.IP, 0)
vs := make([]*ufspb.Vlan, len(vlans))
for i, vlan := range vlans {
vlanName := util.GetBrowserLabName(util.Int64ToStr(vlan.GetId()))
ips, length, freeStartIP, freeEndIP, err := util.ParseVlan(vlanName, vlan.GetCidrBlock(), "", "")
if err != nil {
return nil, err
}
for _, ip := range ips {
IPs = append(IPs, ip)
}
vs[i] = &ufspb.Vlan{
Name: vlanName,
Description: vlan.GetAlias(),
CapacityIp: int32(length),
VlanAddress: vlan.GetCidrBlock(),
ResourceState: util.ToState(vlan.GetState()),
FreeStartIpv4Str: freeStartIP,
FreeEndIpv4Str: freeEndIP,
VlanNumber: util.GetSuffixAfterSeparator(vlanName, ":"),
}
}
deleteNonExistingVlans(ctx, vs, pageSize)
allRes := make(ufsds.OpResults, 0)
logging.Infof(ctx, "Importing %d vlans", len(vs))
for i := 0; ; i += pageSize {
end := util.Min(i+pageSize, len(vs))
res, err := configuration.ImportVlans(ctx, vs[i:end])
allRes = append(allRes, *res...)
if err != nil {
return &allRes, err
}
if i+pageSize >= len(vs) {
break
}
}
deleteInvalidIPs(ctx, pageSize)
logging.Infof(ctx, "Importing %d ips", len(IPs))
for i := 0; ; i += pageSize {
end := util.Min(i+pageSize, len(IPs))
res, err := configuration.ImportIPs(ctx, IPs[i:end])
allRes = append(allRes, *res...)
if err != nil {
return &allRes, err
}
if i+pageSize >= len(IPs) {
break
}
}
return &allRes, nil
}
func deleteInvalidIPs(ctx context.Context, pageSize int) {
resp, err := configuration.GetAllIPs(ctx)
if err != nil {
logging.Debugf(ctx, "Fail to get all ips: %s", err.Error())
return
}
var toDeleteIP []string
for _, sr := range resp.Passed() {
s := sr.Data.(*ufspb.IP)
if s.GetIpv4Str() == "" {
toDeleteIP = append(toDeleteIP, s.GetId())
}
}
logging.Infof(ctx, "Deleting %d invalid ips ", len(toDeleteIP))
deleteByPage(ctx, toDeleteIP, pageSize, configuration.DeleteIPs)
}
func deleteNonExistingVlans(ctx context.Context, vlans []*ufspb.Vlan, pageSize int) (*ufsds.OpResults, error) {
resMap := make(map[string]bool)
for _, r := range vlans {
resMap[r.GetName()] = true
}
resp, err := configuration.GetAllVlans(ctx)
if err != nil {
return nil, err
}
var toDelete []string
var toDeleteIP []string
for _, sr := range resp.Passed() {
s := sr.Data.(*ufspb.Vlan)
if util.IsInBrowserZone(s.GetName()) || strings.Contains(s.GetName(), "browser-lab") {
if _, ok := resMap[s.GetName()]; !ok {
toDelete = append(toDelete, s.GetName())
ips, err := configuration.QueryIPByPropertyName(ctx, map[string]string{"vlan": s.GetName()})
if err != nil {
return nil, err
}
for _, ip := range ips {
toDeleteIP = append(toDeleteIP, ip.GetId())
}
}
}
}
logging.Infof(ctx, "Deleting %d non-existing ips ", len(toDeleteIP))
deleteByPage(ctx, toDeleteIP, pageSize, configuration.DeleteIPs)
logging.Infof(ctx, "Deleting %d non-existing vlans ", len(toDelete))
return deleteByPage(ctx, toDelete, pageSize, configuration.DeleteVlans), nil
}
// ImportOSVlans imports the logic of parse and save network infos.
func ImportOSVlans(ctx context.Context, sheetClient sheet.ClientInterface, gitClient git.ClientInterface, pageSize int) (*ufsds.OpResults, error) {
networkCfg := config.Get(ctx).GetCrosNetworkConfig()
allVlans := make([]*ufspb.Vlan, 0)
allIPs := make([]*ufspb.IP, 0)
allDhcps := make([]*ufspb.DHCPConfig, 0)
for _, cfg := range networkCfg.GetCrosNetworkTopology() {
logging.Debugf(ctx, "########### Parse %s ###########", cfg.GetName())
resp, err := sheetClient.Get(ctx, cfg.GetSheetId(), []string{"VLANs and Netblocks"})
if err != nil {
return nil, status.Error(codes.FailedPrecondition, err.Error())
}
topology, dupcatedVlan := util.ParseATLTopology(resp)
logging.Debugf(ctx, "Topology length %d", len(topology))
logging.Debugf(ctx, "Duplicated vlans found in topology:")
logVlans(ctx, dupcatedVlan)
conf, err := gitClient.GetFile(ctx, cfg.GetRemotePath())
if err != nil {
return nil, status.Error(codes.FailedPrecondition, err.Error())
}
parsed, err := util.ParseOSDhcpdConf(conf, topology)
if err != nil {
return nil, status.Error(codes.FailedPrecondition, err.Error())
}
logging.Debugf(ctx, "Duplicated vlans found in dhcp conf file:")
logVlans(ctx, parsed.DuplicatedVlans)
logging.Debugf(ctx, "Vlans not existing in pre-defined topology:")
logVlans(ctx, parsed.MismatchedVlans)
logging.Debugf(ctx, "Invalid dhcps without vlan:")
logDHCPs(ctx, parsed.DHCPsWithoutVlan)
logging.Debugf(ctx, "Duplicated ips found in dhcp conf file:")
logIPs(ctx, parsed.DuplicatedIPs)
logging.Debugf(ctx, "Get %d vlans, %d ips, %d dhcps for %s", len(parsed.ValidVlans), len(parsed.ValidIPs), len(parsed.ValidDHCPs), cfg.GetName())
allVlans = append(allVlans, parsed.ValidVlans...)
allIPs = append(allIPs, parsed.ValidIPs...)
allDhcps = append(allDhcps, parsed.ValidDHCPs...)
}
allRes := make(ufsds.OpResults, 0)
logging.Debugf(ctx, "Importing %d vlans", len(allVlans))
for i := 0; ; i += pageSize {
end := util.Min(i+pageSize, len(allVlans))
res, err := configuration.ImportVlans(ctx, allVlans[i:end])
allRes = append(allRes, *res...)
if err != nil {
return &allRes, err
}
if i+pageSize >= len(allVlans) {
break
}
}
logging.Debugf(ctx, "Importing %d ips", len(allIPs))
for i := 0; ; i += pageSize {
end := util.Min(i+pageSize, len(allIPs))
res, err := configuration.ImportIPs(ctx, allIPs[i:end])
allRes = append(allRes, *res...)
if err != nil {
return &allRes, err
}
if i+pageSize >= len(allIPs) {
break
}
}
logging.Debugf(ctx, "Importing %d ips", len(allDhcps))
for i := 0; ; i += pageSize {
end := util.Min(i+pageSize, len(allDhcps))
res, err := configuration.ImportDHCPConfigs(ctx, allDhcps[i:end])
allRes = append(allRes, *res...)
if err != nil {
return &allRes, err
}
if i+pageSize >= len(allDhcps) {
break
}
}
return &allRes, nil
}
func logVlans(ctx context.Context, vlans []*ufspb.Vlan) {
if vlans != nil && len(vlans) > 0 {
for _, v := range vlans {
logging.Debugf(ctx, "\tVlan %s (%s)", v.GetName(), v.GetVlanAddress())
}
return
}
logging.Debugf(ctx, "\tNot found")
}
func logDHCPs(ctx context.Context, dhcps []*ufspb.DHCPConfig) {
if dhcps != nil && len(dhcps) > 0 {
for _, v := range dhcps {
logging.Debugf(ctx, "\tHost %s (%s)", v.GetHostname(), v.GetIp())
}
return
}
logging.Debugf(ctx, "\tNot found")
}
func logIPs(ctx context.Context, ips []*ufspb.IP) {
if ips != nil && len(ips) > 0 {
for _, v := range ips {
logging.Debugf(ctx, "\tIP %s", v.GetId())
}
return
}
logging.Debugf(ctx, "\tNot found")
}
// ReplaceVlan replaces an old Vlan with new Vlan in datastore
//
// It does a delete of old vlan and create of new Vlan.
// All the steps are in done in a transaction to preserve consistency on failure.
// Before deleting the old Vlan, it will get all the resources referencing
// the old Vlan. It will update all the resources which were referencing
// the old Vlan(got in the last step) with new Vlan.
// Deletes the old Vlan.
// Creates the new Vlan.
// This will preserve data integrity in the system.
func ReplaceVlan(ctx context.Context, oldVlan *ufspb.Vlan, newVlan *ufspb.Vlan) (*ufspb.Vlan, error) {
// TODO(eshwarn) : implement replace after user testing the tool
return nil, nil
}
// validateCreateVlan validates if a vlan can be created
func validateCreateVlan(ctx context.Context, vlan *ufspb.Vlan) error {
if err := util.CheckPermission(ctx, util.NetworksCreate, vlan.GetRealm()); err != nil {
return err
}
if err := resourceAlreadyExists(ctx, []*Resource{GetVlanResource(vlan.Name)}, nil); err != nil {
return err
}
cidrBlock := vlan.GetVlanAddress()
if cidrBlock != "" {
vlans, err := configuration.QueryVlanByPropertyName(ctx, "cidr_block", cidrBlock, true)
if err != nil {
return err
}
if len(vlans) > 0 {
return status.Errorf(codes.InvalidArgument, "cidr block %s is already occupied by %s", cidrBlock, vlans[0].GetName())
}
}
if err := validateFreeIPV4Str(vlan.FreeEndIpv4Str); err != nil {
return err
}
if err := validateFreeIPV4Str(vlan.FreeStartIpv4Str); err != nil {
return err
}
return nil
}
// validateDeleteVlan validates if a Vlan can be deleted
//
// Checks if this Vlan(VlanID) 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 validateDeleteVlan(ctx context.Context, vlan *ufspb.Vlan) error {
if err := util.CheckPermission(ctx, util.NetworksDelete, vlan.GetRealm()); err != nil {
return err
}
machinelses, err := inventory.QueryMachineLSEByPropertyName(ctx, "vlan_id", vlan.GetName(), true)
if err != nil {
return err
}
if len(machinelses) > 0 {
return status.Errorf(codes.FailedPrecondition, "vlan %s is occupied by %d hosts, e.g. %#v", vlan.GetName(), len(machinelses), ufsAPI.ParseResources(machinelses, "Name"))
}
vms, err := inventory.QueryVMByPropertyName(ctx, "vlan_id", vlan.GetName(), true)
if err != nil {
return err
}
if len(vms) > 0 {
return status.Errorf(codes.FailedPrecondition, "vlan %s is occupied by %d vms, e.g. %#v", vlan.GetName(), len(vms), ufsAPI.ParseResources(vms, "Name"))
}
return nil
}
// validateUpdateVlan validates if a vlan can be updated
func validateUpdateVlan(ctx context.Context, oldVlan *ufspb.Vlan, vlan *ufspb.Vlan, mask *field_mask.FieldMask) error {
if err := util.CheckPermission(ctx, util.NetworksUpdate, oldVlan.GetRealm()); err != nil {
return err
}
if vlan.GetRealm() != "" && oldVlan.GetRealm() != vlan.GetRealm() {
if err := util.CheckPermission(ctx, util.NetworksUpdate, vlan.GetRealm()); err != nil {
return err
}
}
if err := validateVlanUpdateMask(ctx, vlan, mask); err != nil {
return err
}
if mask == nil {
if err := validateFreeIPV4Str(vlan.FreeEndIpv4Str); err != nil {
return err
}
if err := validateFreeIPV4Str(vlan.FreeStartIpv4Str); err != nil {
return err
}
}
return nil
}
func validateFreeIPV4Str(ipv4 string) error {
if ipv4 != "" {
if _, err := util.IPv4StrToInt(ipv4); err != nil {
return status.Errorf(codes.InvalidArgument, "free ip %s is an invalid IP", ipv4)
}
}
return nil
}
// validateVlanUpdateMask validates the update mask for vlan update
func validateVlanUpdateMask(ctx context.Context, vlan *ufspb.Vlan, 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, "validateVlanUpdateMask - name cannot be updated, delete and create a new vlan instead")
case "update_time":
return status.Error(codes.InvalidArgument, "validateVlanUpdateMask - update_time cannot be updated, it is a Output only field")
case "description":
case "resourceState":
case "cidr_block":
return status.Error(codes.InvalidArgument, "validateVlanUpdateMask - cidr_block cannot be updated, delete and create a new vlan instead")
case "tags":
// valid fields, nothing to validate.
case "reserved_ips":
case "zones":
case "free_start_ip":
if err := validateFreeIPV4Str(vlan.FreeStartIpv4Str); err != nil {
return err
}
case "free_end_ip":
if err := validateFreeIPV4Str(vlan.FreeEndIpv4Str); err != nil {
return err
}
default:
return status.Errorf(codes.InvalidArgument, "validateVlanUpdateMask - unsupported update mask path %q", path)
}
}
}
return nil
}
// processVlanUpdateMask process the update field masks to do partial update
//
// Return a complete vlan object with updated and existing fields
func processVlanUpdateMask(oldVlan *ufspb.Vlan, vlan *ufspb.Vlan, mask *field_mask.FieldMask) (*ufspb.Vlan, error) {
// update the fields in the existing/old vlan
for _, path := range mask.Paths {
switch path {
case "description":
oldVlan.Description = vlan.GetDescription()
case "resourceState":
oldVlan.ResourceState = vlan.GetResourceState()
case "reserved_ips":
oldVlan.ReservedIps = mergeIPs(oldVlan.ReservedIps, vlan.GetReservedIps())
case "zones":
oldVlan.Zones = mergeZones(oldVlan.GetZones(), vlan.GetZones())
setRealmForVlan(oldVlan)
case "free_start_ip":
oldVlan.FreeStartIpv4Str = vlan.FreeStartIpv4Str
case "free_end_ip":
oldVlan.FreeEndIpv4Str = vlan.FreeEndIpv4Str
case "tags":
oldVlan.Tags = mergeTags(oldVlan.GetTags(), vlan.GetTags())
}
}
return oldVlan, nil
}
func getVlanHistoryClient(m *ufspb.Vlan) *HistoryClient {
return &HistoryClient{
stUdt: &stateUpdater{
ResourceName: util.AddPrefix(util.VlanCollection, m.Name),
},
netUdt: &networkUpdater{
Hostname: m.Name,
},
}
}
func setRealmForVlan(vlan *ufspb.Vlan) {
if len(vlan.GetZones()) > 0 {
vlan.Realm = util.ToUFSRealm(vlan.GetZones()[0].String())
} else {
vlan.Realm = ""
}
}