blob: bb63be4683485a06728d93c69b02a2f404e9ee2a [file] [log] [blame]
package util
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"strings"
"time"
"github.com/gofrs/flock"
"github.com/apache/trafficcontrol/cache-config/t3c-apply/config"
"github.com/apache/trafficcontrol/cache-config/t3cutil"
"github.com/apache/trafficcontrol/lib/go-log"
)
const OneWeek = 604800
type FileLock struct {
f_lock *flock.Flock
is_locked bool
}
type ServiceStatus int
const (
SvcNotRunning ServiceStatus = 0
SvcRunning ServiceStatus = 1
SvcUnknown ServiceStatus = 2
)
func (s ServiceStatus) String() string {
switch s {
case 0:
return "SvcNotRunning"
case 1:
return "SvcRunning"
case 2:
fallthrough
default:
return "SvcUnknown"
}
}
// Try to get a file lock, non-blocking.
func (f *FileLock) GetLock(lockFile string) bool {
f.f_lock = flock.New(lockFile)
is_locked, err := f.f_lock.TryLock()
f.is_locked = is_locked
if err != nil { // some OS error attempting to obtain a file lock
log.Errorf("unable to obtain a lock on %s\n", lockFile)
return false
}
if !f.is_locked { // another process is running.
return false
}
return f.is_locked
}
// Releases the file lock, if locked.
func (f *FileLock) Unlock() {
if f.is_locked {
f.f_lock.Unlock()
}
}
func DirectoryExists(dir string) (bool, os.FileInfo) {
info, err := os.Stat(dir)
if os.IsNotExist(err) {
return false, nil
}
return info.IsDir(), info
}
func ExecCommand(fullCommand string, arg ...string) ([]byte, int, error) {
var outbuf bytes.Buffer
var errbuf bytes.Buffer
cmd := exec.Command(fullCommand, arg...)
cmd.Stdout = &outbuf
cmd.Stderr = &errbuf
err := cmd.Run()
if err != nil {
return outbuf.Bytes(), cmd.ProcessState.ExitCode(),
errors.New("Error executing '" + fullCommand + "': " + errbuf.String())
}
return outbuf.Bytes(), cmd.ProcessState.ExitCode(), err
}
func FileExists(fn string) (bool, os.FileInfo) {
info, err := os.Stat(fn)
if os.IsNotExist(err) {
return false, nil
}
return !info.IsDir(), info
}
func ReadFile(fn string) ([]byte, error) {
var data []byte
info, err := os.Stat(fn)
if err != nil {
return nil, err
}
size := info.Size()
fd, err := os.Open(fn)
if err != nil {
return nil, err
}
data = make([]byte, size)
c, err := fd.Read(data)
if err != nil || int64(c) != size {
return nil, errors.New("unable to completely read from '" + fn + "': " + err.Error())
}
fd.Close()
return data, nil
}
func GetServiceStatus(name string) (ServiceStatus, int, error) {
var pid int = -1
var active bool = false
output, rc, err := ExecCommand("/usr/sbin/service", name, "status")
// service is down
if rc == 3 {
return SvcNotRunning, pid, nil
} else if err != nil {
return SvcUnknown, pid, errors.New("could not get status for service '" + name + "'\n")
}
lines := strings.Split(string(output), "\n")
for ii := range lines {
line := strings.TrimSpace(lines[ii])
if strings.Contains(line, "Active: active") {
active = true
}
if active && strings.Contains(line, "Main PID: ") {
fmt.Sscanf(line, "Main PID: %d", &pid)
}
}
if active {
return SvcRunning, pid, nil
} else {
return SvcNotRunning, pid, nil
}
}
// start or restart the service 'service'. cmd is 'start | restart'
func ServiceStart(service string, cmd string) (bool, error) {
log.Infof("ServiceStart called for '%s'\n", service)
svcStatus, pid, err := GetServiceStatus(service)
if err != nil {
return false, errors.New("Could not get status for '" + service + "' : " + err.Error())
} else if svcStatus == SvcRunning && cmd == "start" {
log.Infof("service '%s' is already running, pid: %d\n", service, pid)
} else {
_, rc, err := ExecCommand("/usr/sbin/service", service, cmd)
if err != nil {
return false, errors.New("Could not " + cmd + " the '" + service + "' service: " + err.Error())
} else if rc == 0 {
// service was sucessfully started
return true, nil
}
}
// not started, service is already running
return false, nil
}
func WriteFileWithOwner(fn string, data []byte, uid *int, gid *int, perm os.FileMode) (int, error) {
fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return 0, errors.New("unable to open '" + fn + "' for writing: " + err.Error())
}
c, err := fd.Write(data)
if err != nil {
return 0, errors.New("error writing to '" + fn + "': " + err.Error())
}
fd.Close()
if uid != nil && gid != nil {
err = os.Chown(fn, *uid, *gid)
if err != nil {
return 0, errors.New("error changing ownership on '" + fn + "': " + err.Error())
}
}
return c, nil
}
func PackageAction(cmdstr string, name string) (bool, error) {
var rc int = -1
var err error = nil
var result bool = false
switch cmdstr {
case "info":
_, rc, err = ExecCommand("/usr/bin/yum", "info", "-y", name)
case "install":
_, rc, err = ExecCommand("/usr/bin/yum", "install", "-y", name)
case "remove":
_, rc, err = ExecCommand("/usr/bin/yum", "remove", "-y", name)
}
if rc == 0 {
result = true
err = nil
}
return result, err
}
// runs the rpm command.
// if the return code from rpm == 0, then a valid package list is returned.
//
// if the return code is 1, the the 'name' queried for is not part of a
//
// package or is not installed.
//
// otherwise, if the return code is not 0 or 1 and error is set, a general
// rpm command execution error is assumed and the error is returned.
func PackageInfo(cmdstr string, name string) ([]string, error) {
var result []string
switch cmdstr {
case "cfg-files": // returns a list of the package configuration files.
output, rc, err := ExecCommand("/bin/rpm", "-q", "-c", name)
if rc == 1 { // rpm package for 'name' was not found.
return nil, nil
} else if rc == 0 { // add the package name the file belongs to.
log.Debugf("output from cfg-files query: %s\n", string(output))
files := strings.Split(string(output), "\n")
for ii := range files {
result = append(result, strings.TrimSpace(files[ii]))
}
log.Debugf("result length: %d, result: %s\n", len(result), string(output))
} else if err != nil {
return nil, err
}
case "file-query": // returns the rpm name that owns the file 'name'
output, rc, err := ExecCommand("/bin/rpm", "-q", "-f", name)
if rc == 1 { // file is not part of any package.
return nil, nil
} else if rc == 0 { // add the package name the file belongs to.
log.Debugf("output from file-query: %s\n", string(output))
result = append(result, string(strings.TrimSpace(string(output))))
log.Debugf("result length: %d, result: %s\n", len(result), string(output))
} else if err != nil {
return nil, err
}
case "pkg-provides": // returns the package name that provides 'name'
output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatprovides", name)
log.Debugf("pkg-provides - name: %s, output: %s\n", name, output)
if rc == 1 { // no package provides 'name'
return nil, nil
} else if rc == 0 {
pkgs := strings.Split(string(output), "\n")
for ii := range pkgs {
result = append(result, strings.TrimSpace(pkgs[ii]))
}
} else if err != nil {
return nil, errors.New("rpm -q --whatprovides '" + name + "' returned: " + err.Error())
}
case "pkg-query": // returns the package name for 'name'.
output, rc, err := ExecCommand("/bin/rpm", "-q", name)
if rc == 1 { // the package is not installed.
return nil, nil
} else if rc == 0 { // add the rpm name
result = append(result, string(strings.TrimSpace(string(output))))
} else if err != nil {
return nil, errors.New("rpm -q '" + name + "' returned: " + err.Error())
}
case "pkg-requires": // returns a list of packages that requires package 'name'
output, rc, err := ExecCommand("/bin/rpm", "-q", "--whatrequires", name)
if rc == 1 { // no package reuires package 'name'
return nil, nil
} else if rc == 0 {
pkgs := strings.Split(string(output), "\n")
for ii := range pkgs {
result = append(result, strings.TrimSpace(pkgs[ii]))
}
} else if err != nil {
return nil, errors.New("rpm -q --whatrequires '" + name + "' returned: " + err.Error())
}
}
return result, nil
}
func RandomDuration(max time.Duration) time.Duration {
rand.Seed(time.Now().UnixNano())
return time.Duration(rand.Int63n(int64(max)))
}
func CheckUser(cfg config.Cfg) bool {
userInfo, err := user.Current()
if err != nil {
log.Errorf("could not obtain the current user info: %s\n", err.Error())
return false
}
log.Infof("user check: report-only=%v service-action=%v install-packages=%v files=%v user='%v'\n", cfg.ReportOnly, cfg.ServiceAction, cfg.InstallPackages, cfg.Files, userInfo.Username)
// TODO remove check? Let people run as any user, if it succeeds? Warn?
if userInfo.Username != "root" && !cfg.ReportOnly {
if cfg.ServiceAction == t3cutil.ApplyServiceActionFlagRestart {
log.Errorf("Only the root user may restart services, current user: %s\n", userInfo.Username)
return false
} else if cfg.InstallPackages {
log.Errorf("Only the root user may install packages, current user: %s\n", userInfo.Username)
return false
} else if cfg.Files == t3cutil.ApplyFilesFlagAll {
// TODO remove? Why would reval be ok, but not other files?
log.Errorf("Only the root user may set non-reval files, current user: %s\n", userInfo.Username)
return false
}
}
return true
}
func CleanTmpDir(cfg config.Cfg) bool {
if cfg.ReportOnly {
log.Infoln("Running report only, not cleaning tmp directory")
return true
}
now := time.Now().Unix()
if len(config.TmpBase) == 0 || !strings.HasPrefix(config.TmpBase, "/") {
log.Errorf("config.TmpBase is misconfigured: '%s', refusing to remove any files or directories.", config.TmpBase)
return false
}
files, err := ioutil.ReadDir(config.TmpBase)
if err != nil {
log.Errorf("opening %s: %v\n", config.TmpBase, err)
return false
}
for _, f := range files {
// remove any directory and its contents under the config.TmpBase (/tmp/ort) that
// is more than a week old.
if f.IsDir() && (now-f.ModTime().Unix()) > OneWeek {
dir := filepath.Join(config.TmpBase, f.Name())
if dir == "/" {
log.Errorf("config.TmpBase is incorrectly configured, refusing to remove '/'. check config.TmpBase.")
return false
}
if f.Name() == "." || f.Name() == ".." {
continue
}
err = os.RemoveAll(dir)
if err != nil {
log.Errorf("could not remove '%s': %v\n", dir, err)
return false
}
}
}
return true
}
func MkDir(name string, reportOnly bool) bool {
return doMkDirWithOwner(name, reportOnly, nil, nil)
}
func MkDirWithOwner(name string, reportOnly bool, uid *int, gid *int) bool {
return doMkDirWithOwner(name, reportOnly, uid, gid)
}
func doMkDirWithOwner(name string, reportOnly bool, uid *int, gid *int) bool {
// Check if already exists
fileInfo, err := os.Stat(name)
if err == nil {
if fileInfo.IsDir() {
log.Debugf("the directory '%s' already exists", name)
return true
} else {
log.Errorf("there is a file named, '%s' that is not a directory", name)
return false
}
}
if reportOnly {
log.Infof("Reporting only: the directory %s does not exist and was not created", name)
return true
}
err = os.Mkdir(name, 0755)
if err != nil {
log.Errorf("unable to create the directory '%s': %v", name, err)
return false
}
if uid != nil && gid != nil {
err = os.Chown(name, *uid, *gid)
if err != nil {
log.Errorf("unable to chown directory uid/gid, '%s': %v", name, err)
return false
}
}
return true
}
func Touch(fn string) error {
myfile, err := os.Create(fn)
if err != nil {
return err
}
myfile.Close()
return nil
}
func UpdateMaxmind(maxmindlocation string, tsconfigdir string, reportonly bool) bool {
if maxmindlocation == "" {
return false
}
// Dont update for report mode
if reportonly {
log.Infof("Reporting: maxmind location '%v', reporting only and not modifying file\n", maxmindlocation)
return false
}
// Split url, get filename
url, err := url.Parse(maxmindlocation)
if err != nil {
log.Errorf("error parsing maxmind url: %v", err)
return false
}
urlpath := url.Path
fileName := path.Base(urlpath)
if fileName == "." || fileName == "/" {
log.Errorf("filename for maxmind from url invalid: %s", fileName)
return false
}
// Check if filename exists in ats etc
filePath := filepath.Join(tsconfigdir, "/", fileName)
stdOut, _, code := t3cutil.Do(`date`,
"+%a, %d %b %Y %T %Z",
"-u",
"-r",
filePath)
// Do a HEAD request to check for 200 or 304 depending on if we
// have an existing file or not.
client := &http.Client{}
req, err := http.NewRequest("HEAD", maxmindlocation, nil)
if err != nil {
log.Errorf("error creating head request %v", err)
return false
}
if code == 0 {
req.Header.Add("If-Modified-Since", strings.TrimSpace(string(stdOut)))
}
resp, err := client.Do(req)
if err != nil {
log.Errorf("error issuing client IMS request %v", err)
return false
}
if resp.StatusCode != 304 && resp.StatusCode != 200 {
log.Errorf("error requesting %s, code: %d", maxmindlocation, resp.StatusCode)
return false
}
// If we have a 304 then update the timestamp on the file on disk.
if resp.StatusCode == 304 {
dateStr := resp.Header.Get("Last-Modified")
if dateStr == "" {
dateStr = resp.Header.Get("Date")
}
_, _, code := t3cutil.Do(`touch`,
"-d",
dateStr,
filePath)
if code != 0 {
log.Errorf("error setting new date: %s, on %s", dateStr, filePath)
return false
}
log.Infof("received a 304 for maxmind database, updated disk file with new date: %s", dateStr)
return false
}
_, _, code = t3cutil.Do(`curl`,
"-so",
filePath,
maxmindlocation)
if code != 0 {
log.Errorf("Error downloading maxmind database")
return false
}
gunzip := exec.Command("bash", "-c", "gunzip < "+filePath+" > "+(strings.TrimSuffix(filePath, ".gz"))+".tmp")
err = gunzip.Run()
if err != nil {
log.Errorf("error running gunzip: %v\n", err)
return false
}
move := exec.Command("bash", "-c", "mv "+(strings.TrimSuffix(filePath, ".gz")+".tmp")+" "+strings.TrimSuffix(filePath, ".gz"))
err = move.Run()
if err != nil {
log.Errorf("error moving new maxmind database file: %v\n", err)
return false
}
log.Infof("Maxmind DB at %s successfully updated from %s", filePath, maxmindlocation)
return true
}