blob: 5accab901fcc3df30859d146cbf6c4390ef32679 [file] [log] [blame]
package actions
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path"
"strings"
"cloud.google.com/go/storage"
"cos.googlesource.com/cos/tools.git/src/pkg/cos"
"cos.googlesource.com/cos/tools.git/src/pkg/dkms"
"cos.googlesource.com/cos/tools.git/src/pkg/fs"
"cos.googlesource.com/cos/tools.git/src/pkg/gcs"
"cos.googlesource.com/cos/tools.git/src/pkg/utils"
"github.com/golang/glog"
"github.com/spf13/cobra"
"google.golang.org/api/option"
)
func FetchKernelVersion() (string, error) {
kernelVersion := os.Getenv("KERNEL_VERSION")
if kernelVersion != "" {
return kernelVersion, nil
}
cmd := exec.Command("uname", "-r")
out, err := cmd.CombinedOutput()
if err == nil {
glog.Info(string(out))
} else {
glog.Error(string(out))
return "", err
}
return strings.Trim(string(out), "\n"), nil
}
// FetchKernelVersionFromHeaders downloads the COS kernel headers for the
// given build id and board to a temporary directory and reads the kernel
// release version from include/config/kernel.release.
func FetchKernelVersionFromHeaders(buildId, board string) (string, error) {
ctx := context.Background()
client, err := storage.NewClient(ctx, option.WithoutAuthentication())
if err != nil {
return "", err
}
prefix := path.Join(buildId, board)
downloader := cos.NewGCSDownloader(client, nil, "", prefix, "", "")
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
return "", err
}
defer os.RemoveAll(tmpDir)
glog.Infof("installing kernel headers to determine kernel version")
if err := dkms.InstallKernelHeaders(ctx, downloader, tmpDir); err != nil {
return "", err
}
versionPath := path.Join(tmpDir, "include/config/kernel.release")
versionBytes, err := os.ReadFile(versionPath)
if err != nil {
return "", err
}
version := strings.TrimSpace(string(versionBytes))
return version, nil
}
func FetchArch() (string, error) {
arch := os.Getenv("ARCH")
if arch != "" {
return arch, nil
}
cmd := exec.Command("uname", "-m")
out, err := cmd.CombinedOutput()
if err == nil {
glog.Info(string(out))
} else {
glog.Error(string(out))
return "", err
}
return strings.Trim(string(out), "\n"), nil
}
func ParsePackageNameAndVersion(name, version string) (string, string, error) {
if name == "" {
return "", "", errors.New("must provide a package name and version as <package>/<version>")
}
parts := strings.Split(name, "/")
name = parts[0]
if name == "" {
return "", "", errors.New("package name must not be empty")
}
if version == "" {
if len(parts) != 2 {
return "", "", errors.New("must provide a package and version as <package>/<version>")
}
version = parts[1]
if version == "" {
return "", "", errors.New("package version must not be empty")
}
} else {
if len(parts) != 1 {
return "", "", errors.New("cannot provide explicit package version and <package>/<version> simultaneously")
}
}
return name, version, nil
}
func initPackage(pkg *dkms.Package) error {
name, version, err := ParsePackageNameAndVersion(pkg.Name, pkg.Version)
if err != nil {
return err
}
pkg.Name = name
pkg.Version = version
if pkg.KernelVersion == "" {
kernelVersion, err := FetchKernelVersion()
if err != nil {
return fmt.Errorf("could not determine kernel version: %v", err)
}
pkg.KernelVersion = kernelVersion
} else if pkg.KernelVersion == "cos-default" {
kernelVersion, err := FetchKernelVersionFromHeaders(pkg.BuildId, pkg.Board)
if err != nil {
return fmt.Errorf("could not determine kernel version from headers: %v", err)
}
pkg.KernelVersion = kernelVersion
}
if pkg.Arch == "" {
arch, err := FetchArch()
if err != nil {
return fmt.Errorf("could not determine arch: %v", err)
}
pkg.Arch = arch
}
if pkg.Trees.Source == "" {
pkg.Trees.Source = "/usr/src"
}
if pkg.Trees.Install == "" {
pkg.Trees.Install = fmt.Sprintf("/lib/modules/%s", pkg.KernelVersion)
}
if pkg.Trees.Kernel == "" {
pkg.Trees.Kernel = fmt.Sprintf("/lib/modules/%s/build", pkg.KernelVersion)
}
if pkg.Trees.KernelModules == "" {
pkg.Trees.KernelModules = pkg.Trees.Install
}
if fs.IsDir(pkg.SourceDir()) {
config, err := dkms.LoadConfig(pkg)
if err != nil {
glog.Warningf("error loading dkms config: %v", err)
} else {
pkg.Config = config
}
}
return nil
}
func parseKeyCertPaths(options *dkms.Options) {
privateKeyPath := os.Getenv("MODULES_SIGN_KEY")
if privateKeyPath == "" {
privateKeyPath = "/var/lib/dkms/mok.key"
}
certificatePath := os.Getenv("MODULES_SIGN_CERT")
if certificatePath == "" {
certificatePath = "/var/lib/dkms/mok.cert"
}
options.PrivateKeyPath = privateKeyPath
options.CertificatePath = certificatePath
}
func FetchLatestCompatiblePackageVersion(pkg *dkms.Package, cache *gcs.GCSBucket) (string, error) {
versions, err := dkms.CompatiblePackageVersions(pkg)
if err != nil {
return "", err
}
if cache != nil {
ctx := context.Background()
cachedVersions, err := dkms.CachedCompatiblePackageVersions(ctx, pkg, cache)
if err != nil {
return "", err
}
versions = append(versions, cachedVersions...)
}
if len(versions) == 0 {
if cache != nil {
return "", fmt.Errorf("could not find any usable versions of the specified package in the local or cache DKMS tree; make sure the package is added first before using --latest")
} else {
return "", fmt.Errorf("could not find any usable versions of the specified package in the local DKMS tree; make sure the package is added first before using --latest")
}
}
latestVersion := versions[0]
for _, version := range versions[1:] {
if dkms.CompareVersions(latestVersion, version) < 0 {
latestVersion = version
}
}
return latestVersion, nil
}
func InitPackage(pkg *dkms.Package, cache *gcs.GCSBucket, latest bool) error {
// We need all of the other package fields initialized before we can
// determine the latest version of the package, so we fill in a
// placeholder value of the version so that the rest of the
// initialization works as normal
if latest {
if pkg.Version != "" {
return fmt.Errorf("cannot specify explicit package version (%s) and --latest simultaneously", pkg.Version)
}
pkg.Version = "placeholder"
}
if err := initPackage(pkg); err != nil {
return err
}
// After the other fields are initialized, we can get the actual latest
// package version.
if latest {
version, err := FetchLatestCompatiblePackageVersion(pkg, cache)
if err != nil {
return err
}
pkg.Version = version
}
glog.Infof("using package version %s", pkg.Version)
return nil
}
func InitCache(gcsPath, buildId, board string) (*gcs.GCSBucket, error) {
if gcsPath == "" {
gcsPath = os.Getenv("GCS_BUCKET")
}
if gcsPath == "" {
return nil, nil
}
if gcsPath == "cos-default" {
bucket := cos.DefaultCOSArtifactsBucket()
gcsPath = fmt.Sprintf("gs://%s/%s/%s", bucket, buildId, board)
}
ctx := context.Background()
client, err := storage.NewClient(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create storage client: %v", err)
}
return gcs.GCSBucketFromPath(client, gcsPath)
}
func parsePositionalArgs(args []string, pkg *dkms.Package) error {
if len(args) >= 1 {
if pkg.Name != "" {
return fmt.Errorf("cannot specify package name as both positional argument and flag")
}
pkg.Name = args[0]
}
if len(args) >= 2 {
if pkg.Trees.Source != "" {
return fmt.Errorf("cannot specify source tree as both positional argument and flag")
}
pkg.Trees.Source = args[1]
}
if len(args) > 2 {
return fmt.Errorf("expected at most 2 positional arguments; received %d", len(args))
}
return nil
}
// InitCOSValues initializes the package values which are specific to COS,
// namely BuildId and Board.
func InitCOSValues(pkg *dkms.Package, lsbReleasePath string) {
if pkg.BuildId == "" {
pkg.BuildId = os.Getenv("BUILD_ID")
}
if pkg.Board == "" {
pkg.Board = os.Getenv("BOARD")
}
if pkg.BuildId != "" && pkg.Board != "" {
return
}
lsbReleaseVars, err := utils.LoadEnvFromFile("", lsbReleasePath)
if err != nil {
glog.Warningf("could not read lsb-release file at %s: %v", err)
}
if pkg.BuildId == "" {
pkg.BuildId = lsbReleaseVars["CHROMEOS_RELEASE_VERSION"]
}
if pkg.Board == "" {
pkg.Board = lsbReleaseVars["CHROMEOS_RELEASE_BOARD"]
}
}
// ParseArgs parses the cobra positional arguments and flags to populate the
// options, pkg, and RootGCSBucket variables.
func ParseArgs(cmd *cobra.Command, args []string) error {
if err := parsePositionalArgs(args, RootPackage); err != nil {
return err
}
parseKeyCertPaths(RootOptions)
InitCOSValues(RootPackage, RootOptions.LSBReleasePath)
var err error
RootGCSBucket, err = InitCache(RootOptions.GCSBucket, RootPackage.BuildId, RootPackage.Board)
if err != nil {
return err
}
if err := InitPackage(RootPackage, RootGCSBucket, RootOptions.Latest); err != nil {
return err
}
glog.V(2).Infof("options: %v", RootOptions)
glog.V(2).Infof("package: %v", RootPackage)
glog.V(2).Infof("trees: %v", RootTrees)
glog.V(2).Infof("gcs bucket: %v", RootGCSBucket)
return nil
}