blob: a5c86e5d53313f1badba341692f651d5a1c9812a [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"context"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/config"
"go.chromium.org/luci/config/cfgclient"
"go.chromium.org/luci/config/validation"
configPB "go.chromium.org/luci/machine-db/api/config/v1"
"go.chromium.org/luci/machine-db/appengine/model"
"go.chromium.org/luci/machine-db/common"
)
// datacentersFilename is the name of the config file enumerating datacenter files.
const datacentersFilename = "datacenters.cfg"
// switchMaxPorts is the maximum number of ports a switch may have.
const switchMaxPorts = 65535
// importDatacenters fetches, validates, and applies datacenter configs.
func importDatacenters(c context.Context, configSet config.Set, platformIds map[string]int64) error {
cfg := &configPB.Datacenters{}
metadata := &config.Meta{}
if err := cfgclient.Get(c, configSet, datacentersFilename, cfgclient.ProtoText(cfg), metadata); err != nil {
return errors.Annotate(err, "failed to load %s", datacentersFilename).Err()
}
logging.Infof(c, "Found %s revision %q", datacentersFilename, metadata.Revision)
ctx := &validation.Context{Context: c}
ctx.SetFile(datacentersFilename)
validateDatacentersCfg(ctx, cfg)
if err := ctx.Finalize(); err != nil {
return errors.Annotate(err, "invalid config").Err()
}
// cfgs will be a map of datacenter config filename to Datacenter.
cfgs := make(map[string]*configPB.Datacenter, len(cfg.Datacenter))
// datacenters will be a slice of Datacenters.
datacenters := make([]*configPB.Datacenter, 0, len(cfg.Datacenter))
for _, datacenterFile := range cfg.Datacenter {
datacenter := &configPB.Datacenter{}
if err := cfgclient.Get(c, configSet, datacenterFile, cfgclient.ProtoText(datacenter), nil); err != nil {
return errors.Annotate(err, "failed to load datacenter config %q", datacenterFile).Err()
}
cfgs[datacenterFile] = datacenter
datacenters = append(datacenters, datacenter)
logging.Infof(c, "Found configured datacenter %q", datacenter.Name)
}
validateDatacenters(ctx, cfgs)
if err := ctx.Finalize(); err != nil {
return errors.Annotate(err, "invalid config").Err()
}
datacenterIds, err := model.EnsureDatacenters(c, datacenters)
if err != nil {
return errors.Annotate(err, "failed to ensure datacenters").Err()
}
rackIds, err := model.EnsureRacks(c, datacenters, datacenterIds)
if err != nil {
return errors.Annotate(err, "failed to ensure racks").Err()
}
err = model.EnsureSwitches(c, datacenters, rackIds)
if err != nil {
return errors.Annotate(err, "failed to ensure switches").Err()
}
kvmIds, err := model.EnsureKVMs(c, datacenters, platformIds, rackIds)
if err != nil {
return errors.Annotate(err, "failed to ensure KVMs").Err()
}
// KVMs must reference the rack they exist in, while racks may optionally reference
// a KVM that serves them. Because of this bidirectional foreign key constraint in
// the database, we must first ensure that everything about racks except their KVM
// matches the config, then ensure that KVMs match the config, then ensure the KVM
// referred to by each rack matches the config.
err = model.EnsureRackKVMs(c, datacenters, kvmIds)
if err != nil {
return errors.Annotate(err, "failed to ensure rack KVMs").Err()
}
return nil
}
// validateDatacentersCfg validates datacenters.cfg.
func validateDatacentersCfg(c *validation.Context, cfg *configPB.Datacenters) {
// Datacenter filenames must be unique.
// Keep records of which ones we've already seen.
files := stringset.New(len(cfg.Datacenter))
for _, file := range cfg.Datacenter {
switch {
case file == "":
c.Errorf("datacenter filenames are required and must be non-empty")
case !files.Add(file):
c.Errorf("duplicate filename %q", file)
}
}
}
// validateDatacenters validates the individual datacenter.cfg files referenced in datacenters.cfg.
func validateDatacenters(c *validation.Context, datacenters map[string]*configPB.Datacenter) {
// Datacenter, rack, and switch names must be unique.
// Keep records of ones we've already seen.
names := stringset.New(len(datacenters))
racks := stringset.New(0)
switches := stringset.New(0)
kvms := stringset.New(0)
for filename, dc := range datacenters {
c.SetFile(filename)
switch {
case dc.Name == "":
c.Errorf("datacenter names are required and must be non-empty")
case !names.Add(dc.Name):
c.Errorf("duplicate datacenter %q", dc.Name)
}
c.Enter("datacenter %q", dc.Name)
for _, rack := range dc.Rack {
switch {
case rack.Name == "":
c.Errorf("rack names are required and must be non-empty")
case !racks.Add(rack.Name):
c.Errorf("duplicate rack %q", rack.Name)
}
c.Enter("rack %q", rack.Name)
for _, s := range rack.Switch {
switch {
case s.Name == "":
c.Errorf("switch names are required and must be non-empty")
case !switches.Add(s.Name):
c.Errorf("duplicate switch %q", s.Name)
}
c.Enter("switch %q", s.Name)
switch {
case s.Ports < 1:
c.Errorf("switches must have at least one port")
case s.Ports > switchMaxPorts:
c.Errorf("switches must have at most %d ports", switchMaxPorts)
}
c.Exit()
}
c.Exit()
}
for _, kvm := range dc.Kvm {
switch {
case kvm.Name == "":
c.Errorf("KVM names are required and must be non-empty")
case !kvms.Add(kvm.Name):
c.Errorf("duplicate KVM %q", kvm.Name)
}
c.Enter("kvm %q", kvm.Name)
if !racks.Has(kvm.Rack) {
c.Errorf("rack %q does not exist", kvm.Rack)
}
_, err := common.ParseMAC48(kvm.MacAddress)
if err != nil {
c.Errorf("invalid MAC-48 address %q", kvm.MacAddress)
}
_, err = common.ParseIPv4(kvm.Ipv4)
if err != nil {
c.Errorf("invalid IPv4 address %q", kvm.Ipv4)
}
c.Exit()
}
for _, rack := range dc.Rack {
if rack.Kvm != "" {
c.Enter("rack %q", rack.Name)
if !kvms.Has(rack.Kvm) {
c.Errorf("KVM %q does not exist", rack.Kvm)
}
c.Exit()
}
}
c.Exit()
}
}