// Copyright (c) 2017-2018 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // // Note: To add a new architecture, implement all identifiers beginning "arch". package main /* #include const int ioctl_KVM_CREATE_VM = KVM_CREATE_VM; const int ioctl_KVM_CHECK_EXTENSION = KVM_CHECK_EXTENSION; */ import "C" import ( "errors" "fmt" "os" "os/exec" "path/filepath" "regexp" "strings" "syscall" "github.com/kata-containers/runtime/pkg/katautils" vc "github.com/kata-containers/runtime/virtcontainers" "github.com/kata-containers/runtime/virtcontainers/pkg/oci" "github.com/sirupsen/logrus" "github.com/urfave/cli" ) type kernelModule struct { // description desc string // maps parameter names to values parameters map[string]string // if it is definitely required required bool } // nolint: structcheck, unused, deadcode type kvmExtension struct { // description desc string // extension identifier id int } type vmContainerCapableDetails struct { cpuInfoFile string requiredCPUFlags map[string]string requiredCPUAttribs map[string]string requiredKernelModules map[string]kernelModule } const ( moduleParamDir = "parameters" successMessageCapable = "System is capable of running " + project successMessageCreate = "System can currently create " + project successMessageVersion = "Version consistency of " + project + " is verified" failMessage = "System is not capable of running " + project kernelPropertyCorrect = "Kernel property value correct" // these refer to fields in the procCPUINFO file genericCPUFlagsTag = "flags" // nolint: varcheck, unused, deadcode genericCPUVendorField = "vendor_id" // nolint: varcheck, unused, deadcode genericCPUModelField = "model name" // nolint: varcheck, unused, deadcode ) // variables rather than consts to allow tests to modify them var ( procCPUInfo = "/proc/cpuinfo" sysModuleDir = "/sys/module" modProbeCmd = "modprobe" ) // variables rather than consts to allow tests to modify them var ( kvmDevice = "/dev/kvm" ) // getCPUInfo returns details of the first CPU read from the specified cpuinfo file func getCPUInfo(cpuInfoFile string) (string, error) { text, err := katautils.GetFileContents(cpuInfoFile) if err != nil { return "", err } cpus := strings.SplitAfter(text, "\n\n") trimmed := strings.TrimSpace(cpus[0]) if trimmed == "" { return "", fmt.Errorf("Cannot determine CPU details") } return trimmed, nil } // findAnchoredString searches haystack for needle and returns true if found func findAnchoredString(haystack, needle string) bool { if haystack == "" || needle == "" { return false } // Ensure the search string is anchored pattern := regexp.MustCompile(`\b` + needle + `\b`) return pattern.MatchString(haystack) } // getCPUFlags returns the CPU flags from the cpuinfo file specified func getCPUFlags(cpuinfo string) string { for _, line := range strings.Split(cpuinfo, "\n") { if strings.HasPrefix(line, cpuFlagsTag) { fields := strings.Split(line, ":") if len(fields) == 2 { return strings.TrimSpace(fields[1]) } } } return "" } // haveKernelModule returns true if the specified module exists // (either loaded or available to be loaded) func haveKernelModule(module string) bool { // First, check to see if the module is already loaded path := filepath.Join(sysModuleDir, module) if katautils.FileExists(path) { return true } // Now, check if the module is unloaded, but available. // And modprobe it if so. cmd := exec.Command(modProbeCmd, module) if output, err := cmd.CombinedOutput(); err != nil { kataLog.WithField("module", module).WithError(err).Warnf("modprobe insert module failed: %s", string(output)) return false } return true } // checkCPU checks all required CPU attributes modules and returns a count of // the number of CPU attribute errors (all of which are logged by this // function). The specified tag is simply used for logging purposes. func checkCPU(tag, cpuinfo string, attribs map[string]string) (count uint32) { if cpuinfo == "" { return 0 } for attrib, desc := range attribs { fields := logrus.Fields{ "type": tag, "name": attrib, "description": desc, } found := findAnchoredString(cpuinfo, attrib) if !found { kataLog.WithFields(fields).Errorf("CPU property not found") count++ continue } kataLog.WithFields(fields).Infof("CPU property found") } return count } func checkCPUFlags(cpuflags string, required map[string]string) uint32 { return checkCPU("flag", cpuflags, required) } func checkCPUAttribs(cpuinfo string, attribs map[string]string) uint32 { return checkCPU("attribute", cpuinfo, attribs) } // kernelParamHandler represents a function that allows kernel module // parameter errors to be ignored for special scenarios. // // The function is passed the following parameters: // // onVMM - `true` if the host is running under a VMM environment // fields - A set of fields showing the expected and actual module parameter values. // msg - The message that would be logged showing the incorrect kernel module // parameter. // // The function must return `true` if the kernel module parameter error should // be ignored, or `false` if it is a real error. // // Note: it is up to the function to add an appropriate log call if the error // should be ignored. type kernelParamHandler func(onVMM bool, fields logrus.Fields, msg string) bool // checkKernelModules checks all required kernel modules modules and returns a count of // the number of module errors (all of which are logged by this // function). Only fatal errors result in an error return. func checkKernelModules(modules map[string]kernelModule, handler kernelParamHandler) (count uint32, err error) { onVMM, err := vc.RunningOnVMM(procCPUInfo) if err != nil { return 0, err } for module, details := range modules { fields := logrus.Fields{ "type": "module", "name": module, "description": details.desc, } if !haveKernelModule(module) { kataLog.WithFields(fields).Error("kernel property not found") if details.required { count++ } continue } kataLog.WithFields(fields).Infof("kernel property found") for param, expected := range details.parameters { path := filepath.Join(sysModuleDir, module, moduleParamDir, param) value, err := katautils.GetFileContents(path) if err != nil { return 0, err } value = strings.TrimRight(value, "\n\r") fields["parameter"] = param fields["value"] = value if value != expected { fields["expected"] = expected msg := "kernel module parameter has unexpected value" if handler != nil { ignoreError := handler(onVMM, fields, msg) if ignoreError { continue } } kataLog.WithFields(fields).Error(msg) count++ } kataLog.WithFields(fields).Info(kernelPropertyCorrect) } } return count, nil } // genericHostIsVMContainerCapable checks to see if the host is theoretically capable // of creating a VM container. //nolint: unused,deadcode func genericHostIsVMContainerCapable(details vmContainerCapableDetails) error { cpuinfo, err := getCPUInfo(details.cpuInfoFile) if err != nil { return err } cpuFlags := getCPUFlags(cpuinfo) if cpuFlags == "" { return fmt.Errorf("Cannot find CPU flags") } // Keep a track of the error count, but don't error until all tests // have been performed! errorCount := uint32(0) count := checkCPUAttribs(cpuinfo, details.requiredCPUAttribs) errorCount += count count = checkCPUFlags(cpuFlags, details.requiredCPUFlags) errorCount += count count, err = checkKernelModules(details.requiredKernelModules, archKernelParamHandler) if err != nil { return err } errorCount += count if errorCount == 0 { return nil } return fmt.Errorf("ERROR: %s", failMessage) } var kataCheckCLICommand = cli.Command{ Name: checkCmd, Usage: "tests if system can run " + project, Flags: []cli.Flag{ cli.BoolFlag{ Name: "verbose, v", Usage: "display the list of checks performed", }, cli.BoolFlag{ Name: "strict, s", Usage: "perform strict checking", }, }, Action: func(context *cli.Context) error { verbose := context.Bool("verbose") if verbose { kataLog.Logger.SetLevel(logrus.InfoLevel) } ctx, err := cliContextToContext(context) if err != nil { return err } span, _ := katautils.Trace(ctx, "kata-check") defer span.Finish() runtimeConfig, ok := context.App.Metadata["runtimeConfig"].(oci.RuntimeConfig) if !ok { return errors.New("kata-check: cannot determine runtime config") } err = setCPUtype(runtimeConfig.HypervisorType) if err != nil { return err } details := vmContainerCapableDetails{ cpuInfoFile: procCPUInfo, requiredCPUFlags: archRequiredCPUFlags, requiredCPUAttribs: archRequiredCPUAttribs, requiredKernelModules: archRequiredKernelModules, } err = hostIsVMContainerCapable(details) if err != nil { return err } fmt.Println(successMessageCapable) if os.Geteuid() == 0 { err = archHostCanCreateVMContainer(runtimeConfig.HypervisorType) if err != nil { return err } fmt.Println(successMessageCreate) } strict := context.Bool("strict") if strict { err = checkVersionConsistencyInComponents(runtimeConfig) if err != nil { return err } fmt.Println(successMessageVersion) } return nil }, } func genericArchKernelParamHandler(onVMM bool, fields logrus.Fields, msg string) bool { param, ok := fields["parameter"].(string) if !ok { return false } // This option is not required when // already running under a hypervisor. if param == "unrestricted_guest" && onVMM { kataLog.WithFields(fields).Warn(kernelPropertyCorrect) return true } if param == "nested" { kataLog.WithFields(fields).Warn(msg) return true } // don't ignore the error return false } // genericKvmIsUsable determines if it will be possible to create a full virtual machine // by creating a minimal VM and then deleting it. func genericKvmIsUsable() error { flags := syscall.O_RDWR | syscall.O_CLOEXEC f, err := syscall.Open(kvmDevice, flags, 0) if err != nil { return err } defer syscall.Close(f) fieldLogger := kataLog.WithField("check-type", "full") fieldLogger.WithField("device", kvmDevice).Info("device available") vm, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(f), uintptr(C.ioctl_KVM_CREATE_VM), 0) if errno != 0 { if errno == syscall.EBUSY { fieldLogger.WithField("reason", "another hypervisor running").Error("cannot create VM") } return errno } defer syscall.Close(int(vm)) fieldLogger.WithField("feature", "create-vm").Info("feature available") return nil } // genericCheckKVMExtension allows to query about the specific kvm extensions // nolint: unused, deadcode func genericCheckKVMExtensions(extensions map[string]kvmExtension) (map[string]int, error) { results := make(map[string]int) flags := syscall.O_RDWR | syscall.O_CLOEXEC kvm, err := syscall.Open(kvmDevice, flags, 0) if err != nil { return results, err } defer syscall.Close(kvm) for name, extension := range extensions { fields := logrus.Fields{ "type": "kvm extension", "name": name, "description": extension.desc, "id": extension.id, } ret, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(kvm), uintptr(C.ioctl_KVM_CHECK_EXTENSION), uintptr(extension.id)) // Generally return value(ret) 0 means no and 1 means yes, // but some extensions may report additional information in the integer return value. if errno != 0 { kataLog.WithFields(fields).Error("is not supported") return results, errno } results[name] = int(ret) kataLog.WithFields(fields).Info("kvm extension is supported") } return results, nil } // checkVersionConsistencyInComponents checks version consistency in Kata Components. func checkVersionConsistencyInComponents(config oci.RuntimeConfig) error { proxyInfo := getProxyInfo(config) shimInfo, err := getShimInfo(config) if err != nil { return err } shimVersionInfo := shimInfo.Version runtimeVersionInfo := constructVersionInfo(version) // kata-proxy exists if proxyInfo.Type != string(vc.NoProxyType) { proxyVersionInfo := proxyInfo.Version if !versionEqual(proxyVersionInfo, runtimeVersionInfo) || !versionEqual(shimVersionInfo, runtimeVersionInfo) { return fmt.Errorf("there exists version inconsistency in kata components. kata-proxy: v%d.%d.%d, kata-shim: v%d.%d.%d, kata-runtime: v%d.%d.%d", proxyVersionInfo.Major, proxyVersionInfo.Minor, proxyVersionInfo.Patch, shimVersionInfo.Major, shimVersionInfo.Minor, shimVersionInfo.Patch, runtimeVersionInfo.Major, runtimeVersionInfo.Minor, runtimeVersionInfo.Patch) } } else { if !versionEqual(shimVersionInfo, runtimeVersionInfo) { return fmt.Errorf("there exists version inconsistency in kata components. kata-shim: v%d.%d.%d, kata-runtime: v%d.%d.%d", shimVersionInfo.Major, shimVersionInfo.Minor, shimVersionInfo.Patch, runtimeVersionInfo.Major, runtimeVersionInfo.Minor, runtimeVersionInfo.Patch) } } return nil }