plugin/step/install/linux/dnf/dnf.go (188 lines of code) (raw):
package dnf
/*
Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*/
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/facebookincubator/go2chef/util/temp"
"github.com/facebookincubator/go2chef/util"
"github.com/facebookincubator/go2chef"
"github.com/mitchellh/mapstructure"
)
// TypeNames for the three variants of this step plugin
const (
TypeName = "go2chef.step.install.linux.dnf"
YumTypeName = "go2chef.step.install.linux.yum"
RPMTypeName = "go2chef.step.install.linux.rpm"
)
var (
// DefaultPackageName is the default package name to use for Chef installation
DefaultPackageName = "chef"
)
/*
Step implements Chef installation via RHEL/Fedora DNF/YUM/RPM
If you provide a `source` config block, this plugin will download it and search for
an RPM based on `package_name` (and, if specified, `version`).
Example config for a Chef install
{
"type": "go2chef.step.install.linux.dnf",
"name": "install chef",
"package_name": "chef"
}
*/
type Step struct {
StepName string `mapstructure:"name"`
DNFBinary string `mapstructure:"dnf_binary"`
RPMBinary string `mapstructure:"rpm_binary"`
PackageName string `mapstructure:"package_name"`
Version string `mapstructure:"version"`
RPMCheckTimeoutSeconds int `mapstructure:"rpm_check_timeout_seconds"`
InstallTimeoutSeconds int `mapstructure:"install_timeout_seconds"`
installWithRPM bool
logger go2chef.Logger
source go2chef.Source
downloadPath string
packageRegex *regexp.Regexp
packageVersionRegex *regexp.Regexp
}
func (s *Step) String() string {
return "<" + TypeName + ":" + s.StepName + ">"
}
// SetName sets the name of this step instance
func (s *Step) SetName(str string) {
s.StepName = str
}
// Name gets the name of this step instance
func (s *Step) Name() string {
return s.StepName
}
// Type returns the type of this step instance
func (s *Step) Type() string {
return TypeName
}
// Download fetches resources required for this step's execution
func (s *Step) Download() error {
if s.source == nil {
return nil
}
if s.isInstalled() {
return nil
}
tmpdir, err := temp.Dir("", "go2chef-install")
if err != nil {
return err
}
if err := s.source.DownloadToPath(tmpdir); err != nil {
return err
}
s.downloadPath = tmpdir
return nil
}
// Execute performs the installation
func (s *Step) Execute() error {
installPackage := s.PackageName
if !s.isInstalled() {
if s.source != nil {
rpm, err := s.findRPM()
if err != nil {
s.logger.Errorf("step execution failed: could not find step RPM from %s", s.source.Name())
return err
}
installPackage = filepath.Join(s.downloadPath, rpm)
}
if s.installWithRPM {
return s.installChefRPM(installPackage)
}
return s.installChefDNF(installPackage)
}
s.logger.Infof("%s specified is already installed, not reinstalling", installPackage)
return nil
}
// LoaderForBinary provides an instantiation function for this step plugin specific to the passed binary
func LoaderForBinary(binary string) go2chef.StepLoader {
return func(config map[string]interface{}) (go2chef.Step, error) {
step := &Step{
StepName: "",
DNFBinary: "/usr/bin/" + binary,
RPMBinary: "/usr/bin/rpm",
PackageName: DefaultPackageName,
RPMCheckTimeoutSeconds: 60,
InstallTimeoutSeconds: 300,
installWithRPM: binary == "rpm",
logger: go2chef.GetGlobalLogger(),
source: nil,
downloadPath: "",
}
if err := mapstructure.Decode(config, step); err != nil {
return nil, err
}
source, err := go2chef.GetSourceFromStepConfig(config)
if err != nil {
return nil, err
}
step.source = source
reStr := fmt.Sprintf("^%s-.*.rpm$", step.PackageName)
regex, err := regexp.Compile(reStr)
if err != nil {
step.logger.Errorf("failed to compile package matching regex %s", reStr)
return nil, err
}
step.packageRegex = regex
vreStr := fmt.Sprintf("^%s-%s.*", step.PackageName, step.Version)
vRegex, err := regexp.Compile(vreStr)
if err != nil {
step.logger.Errorf("failed to compile package version match regex %s", vreStr)
}
step.packageVersionRegex = vRegex
return step, nil
}
}
var _ go2chef.Step = &Step{}
func init() {
go2chef.RegisterStep(TypeName, LoaderForBinary("dnf"))
go2chef.RegisterStep(YumTypeName, LoaderForBinary("yum"))
go2chef.RegisterStep(RPMTypeName, LoaderForBinary("rpm"))
}
func (s *Step) findRPM() (string, error) {
s.logger.Debugf(0, "searching for RPM in %s matching %s", s.downloadPath, s.packageRegex)
return util.MatchPath(s.downloadPath, s.packageRegex)
}
func (s *Step) isInstalled() bool {
installed := false
if s.Version != "" {
if err := s.checkInstalled(); err != nil {
switch err.(type) {
case *exec.ExitError:
installed = false
case *go2chef.ErrChefAlreadyInstalled:
s.logger.Infof("%s", err)
installed = true
}
}
}
return installed
}
func (s *Step) checkInstalled() error {
chkCtx, chkCtxCancel := context.WithTimeout(context.Background(), time.Duration(s.RPMCheckTimeoutSeconds)*time.Second)
defer chkCtxCancel()
// run rpm -q <package> to get current package
buf := &bytes.Buffer{}
cmd := exec.CommandContext(chkCtx, s.RPMBinary, "-q", s.PackageName)
cmd.Stdin = nil
cmd.Stderr = os.Stderr
cmd.Stdout = buf
if err := cmd.Run(); err != nil {
return err
}
inst := strings.TrimSpace(buf.String())
if s.packageVersionRegex.MatchString(inst) {
return &go2chef.ErrChefAlreadyInstalled{
Installed: inst,
Requested: s.packageVersionRegex.String(),
}
}
return nil
}
func (s *Step) installChefDNF(installPackage string) error {
instCtx, instCtxCancel := context.WithTimeout(context.Background(), time.Duration(s.InstallTimeoutSeconds)*time.Second)
defer instCtxCancel()
cmd := exec.CommandContext(instCtx, s.DNFBinary, "-y", "install", installPackage)
cmd.Stdin = nil
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return cmd.Run()
}
func (s *Step) installChefRPM(installPackage string) error {
instCtx, instCtxCancel := context.WithTimeout(context.Background(), time.Duration(s.InstallTimeoutSeconds)*time.Second)
defer instCtxCancel()
cmd := exec.CommandContext(instCtx, s.RPMBinary, "-Uvh", "--oldpackage", installPackage)
cmd.Stdin = nil
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return cmd.Run()
}