blob: bc98fcf23a18aa31eb0ce3c5fc5e6064e24dcd02 [file] [log] [blame]
package main
/*
* 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 (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/apache/trafficcontrol/cache-config/t3c-apply/config"
"github.com/apache/trafficcontrol/cache-config/t3c-apply/torequest"
"github.com/apache/trafficcontrol/cache-config/t3c-apply/util"
"github.com/apache/trafficcontrol/cache-config/t3cutil"
"github.com/apache/trafficcontrol/lib/go-log"
tcutil "github.com/apache/trafficcontrol/lib/go-util"
)
// Version is the application version.
// This is overwritten by the build with the current project version.
var Version = "0.4"
// GitRevision is the git revision the application was built from.
// This is overwritten by the build with the current project version.
var GitRevision = "nogit"
const (
ExitCodeSuccess = 0
ExitCodeAlreadyRunning = 132
ExitCodeConfigFilesError = 133
ExitCodeConfigError = 134
ExitCodeGeneralFailure = 135
ExitCodePackagingError = 136
ExitCodeRevalidationError = 137
ExitCodeServicesError = 138
ExitCodeSyncDSError = 139
ExitCodeUserCheckError = 140
)
func runSysctl(cfg config.Cfg) {
if cfg.ReportOnly {
return
}
if cfg.ServiceAction == t3cutil.ApplyServiceActionFlagRestart {
_, rc, err := util.ExecCommand("/usr/sbin/sysctl", "-p")
if err != nil {
log.Errorln("sysctl -p failed")
} else if rc == 0 {
log.Debugf("sysctl -p ran succesfully.")
}
}
}
const LockFilePath = "/var/run/t3c.lock"
const LockFileRetryInterval = time.Second
const LockFileRetryTimeout = time.Minute
const FailureExitMsg = `CRITICAL FAILURE, ABORTING`
const PostConfigFailureExitMsg = `CRITICAL FAILURE AFTER SETTING CONFIG, ABORTING`
const SuccessExitMsg = `SUCCESS`
func main() {
os.Exit(LogPanic(Main))
}
// Main is the main function of t3c-apply.
// This is a separate function so defer statements behave as-expected.
// DO NOT call os.Exit within this function; return the code instead.
// Returns the application exit code.
func Main() int {
var syncdsUpdate torequest.UpdateStatus
var lock util.FileLock
cfg, err := config.GetCfg(Version, GitRevision)
if err != nil {
fmt.Println(err)
fmt.Println(FailureExitMsg)
return ExitCodeConfigError
} else if cfg == (config.Cfg{}) { // user used the --help option
return ExitCodeSuccess
}
log.Infoln("Trying to acquire app lock")
for lockStart := time.Now(); !lock.GetLock(LockFilePath); {
if time.Since(lockStart) > LockFileRetryTimeout {
log.Errorf("Failed to get app lock after %v seconds, another instance is running, exiting without running\n", int(LockFileRetryTimeout/time.Second))
log.Infoln(FailureExitMsg)
return ExitCodeAlreadyRunning
}
log.Errorf("Unable to acquire app lock retrying in: %v ", LockFileRetryInterval)
time.Sleep(LockFileRetryInterval)
}
log.Infoln("Acquired app lock")
defer lock.Unlock()
// Note failing to load old metadata is not fatal!
// oldMetaData must always be checked for nil before usage!
oldMetaData, err := LoadMetaData(cfg)
if err != nil {
log.Errorln("Failed to load old metadata file, metadata-dependent functionality disabled: " + err.Error())
}
// Note we only write the metadata file after acquiring the app lock.
// We don't want to write a metadata file if we didn't run because another t3c-apply
// was already running.
metaData := t3cutil.NewApplyMetaData()
metaData.ServerHostName = cfg.CacheHostName
t3cutil.WriteActionLog(t3cutil.ActionLogActionApplyStart, t3cutil.ActionLogStatusSuccess, metaData)
if cfg.UseGit == config.UseGitYes {
triedMakingRepo, err := util.EnsureConfigDirIsGitRepo(cfg)
if err != nil {
log.Errorln("Ensuring config directory '" + cfg.TsConfigDir + "' is a git repo - config may not be a git repo! " + err.Error())
if triedMakingRepo {
t3cutil.WriteActionLog(t3cutil.ActionLogActionGitInit, t3cutil.ActionLogStatusFailure, metaData)
}
} else {
log.Infoln("Successfully ensured ATS config directory '" + cfg.TsConfigDir + "' is a git repo")
if triedMakingRepo {
t3cutil.WriteActionLog(t3cutil.ActionLogActionGitInit, t3cutil.ActionLogStatusSuccess, metaData)
}
}
} else {
log.Infoln("UseGit not 'yes', not creating git repo")
}
if cfg.UseGit == config.UseGitYes || cfg.UseGit == config.UseGitAuto {
//need to see if there is an old lock file laying around.
//older than 5 minutes
const gitMaxLockAgeMinutes = 5
const gitLock = ".git/index.lock"
gitLockFile := filepath.Join(cfg.TsConfigDir, gitLock)
oldLock, err := util.IsGitLockFileOld(gitLockFile, time.Now(), gitMaxLockAgeMinutes*time.Minute)
if err != nil {
log.Errorln("checking for git lock file: " + err.Error())
}
if oldLock {
log.Errorf("removing git lock file older than %dm", gitMaxLockAgeMinutes)
err := util.RemoveGitLock(gitLockFile)
if err != nil {
log.Errorf("couldn't remove git lock file: %v", err.Error())
}
}
log.Infoln("Checking git for safe directory config")
if err := util.GetGitConfigSafeDir(cfg); err != nil {
log.Warnln("error checking git for safe directory config: " + err.Error())
}
// commit anything someone else changed when we weren't looking,
// with a keyword indicating it wasn't our change
if err := util.MakeGitCommitAll(cfg, util.GitChangeNotSelf, true); err != nil {
log.Errorln("git committing existing changes, dir '" + cfg.TsConfigDir + "': " + err.Error())
t3cutil.WriteActionLog(t3cutil.ActionLogActionGitCommitInitial, t3cutil.ActionLogStatusFailure, metaData)
} else {
t3cutil.WriteActionLog(t3cutil.ActionLogActionGitCommitInitial, t3cutil.ActionLogStatusSuccess, metaData)
}
}
trops := torequest.NewTrafficOpsReq(cfg)
// if doing os checks, insure there is a 'systemctl' or 'service' and 'chkconfig' commands.
if !cfg.SkipOSCheck && cfg.SvcManagement == config.Unknown {
log.Errorln("OS checks are enabled and unable to find any know service management tools.")
}
// create and clean the config.TmpBase (/tmp/ort)
if !util.MkDir(config.TmpBase, cfg.ReportOnly) {
log.Errorln("mkdir TmpBase '" + config.TmpBase + "' failed, cannot continue")
log.Infoln(FailureExitMsg)
return ExitCodeGeneralFailure
} else if !util.CleanTmpDir(cfg) {
log.Errorln("CleanTmpDir failed, cannot continue")
log.Infoln(FailureExitMsg)
return ExitCodeGeneralFailure
}
log.Infoln(time.Now().Format(time.RFC3339))
if !util.CheckUser(cfg) {
lock.Unlock()
return ExitCodeUserCheckError
}
// if running in Revalidate mode, check to see if it's
// necessary to continue
if cfg.Files == t3cutil.ApplyFilesFlagReval {
syncdsUpdate, err = trops.CheckRevalidateState(false)
if err != nil {
log.Errorln("Checking revalidate state: " + err.Error())
return GitCommitAndExit(ExitCodeRevalidationError, FailureExitMsg, cfg, metaData, oldMetaData)
}
if syncdsUpdate == torequest.UpdateTropsNotNeeded {
log.Infoln("Checking revalidate state: returned UpdateTropsNotNeeded")
metaData.Succeeded = true
return GitCommitAndExit(ExitCodeRevalidationError, SuccessExitMsg, cfg, metaData, oldMetaData)
}
} else {
syncdsUpdate, err = trops.CheckSyncDSState(metaData)
if err != nil {
log.Errorln("Checking syncds state: " + err.Error())
return GitCommitAndExit(ExitCodeSyncDSError, FailureExitMsg, cfg, metaData, oldMetaData)
}
if !cfg.IgnoreUpdateFlag && cfg.Files == t3cutil.ApplyFilesFlagAll && syncdsUpdate == torequest.UpdateTropsNotNeeded {
// If touching remap.config fails, we want to still try to restart services
// But log a critical-post-config-failure, which needs logged right before exit.
postConfigFail := false
// check for maxmind db updates even if we have no other updates
if CheckMaxmindUpdate(cfg) {
// We updated the db so we should touch and reload
trops.RemapConfigReload = true
path := cfg.TsConfigDir + "/remap.config"
_, rc, err := util.ExecCommand("/usr/bin/touch", path)
if err != nil {
log.Errorf("failed to update the remap.config for reloading: %s\n", err.Error())
postConfigFail = true
} else if rc == 0 {
log.Infoln("updated the remap.config for reloading.")
}
if err := trops.StartServices(&syncdsUpdate, metaData); err != nil {
log.Errorln("failed to start services: " + err.Error())
metaData.PartialSuccess = true
return GitCommitAndExit(ExitCodeServicesError, PostConfigFailureExitMsg, cfg, metaData, oldMetaData)
}
}
finalMsg := SuccessExitMsg
if postConfigFail {
finalMsg = PostConfigFailureExitMsg
}
metaData.Succeeded = true
return GitCommitAndExit(ExitCodeSuccess, finalMsg, cfg, metaData, oldMetaData)
}
}
if cfg.Files != t3cutil.ApplyFilesFlagAll {
// make sure we got the data necessary to check packages
log.Infoln("======== Didn't get all files, no package processing needed or possible ========")
metaData.InstalledPackages = oldMetaData.InstalledPackages
} else {
log.Infoln("======== Start processing packages ========")
err = trops.ProcessPackages()
if err != nil {
log.Errorf("Error processing packages: %s\n", err)
return GitCommitAndExit(ExitCodePackagingError, FailureExitMsg, cfg, metaData, oldMetaData)
}
metaData.InstalledPackages = t3cutil.PackagesToMetaData(trops.Pkgs)
// check and make sure packages are enabled for startup
err = trops.CheckSystemServices()
if err != nil {
log.Errorf("Error verifying system services: %s\n", err.Error())
return GitCommitAndExit(ExitCodeServicesError, FailureExitMsg, cfg, metaData, oldMetaData)
}
}
log.Debugf("Preparing to fetch the config files for %s, files: %s, syncdsUpdate: %s\n", cfg.CacheHostName, cfg.Files, syncdsUpdate)
err = trops.GetConfigFileList()
if err != nil {
log.Errorf("Getting config file list: %s\n", err)
return GitCommitAndExit(ExitCodeConfigFilesError, FailureExitMsg, cfg, metaData, oldMetaData)
}
syncdsUpdate, err = trops.ProcessConfigFiles(metaData)
if err != nil {
log.Errorf("Error while processing config files: %s\n", err.Error())
t3cutil.WriteActionLog(t3cutil.ActionLogActionUpdateFilesAll, t3cutil.ActionLogStatusFailure, metaData)
} else {
t3cutil.WriteActionLog(t3cutil.ActionLogActionUpdateFilesAll, t3cutil.ActionLogStatusSuccess, metaData)
}
// check for maxmind db updates
// If we've updated also reload remap to reload the plugin and pick up the new database
if CheckMaxmindUpdate(cfg) {
trops.RemapConfigReload = true
}
if trops.RemapConfigReload == true {
cfg, ok := trops.GetConfigFile("remap.config")
_, rc, err := util.ExecCommand("/usr/bin/touch", cfg.Path)
if err != nil {
log.Errorf("failed to update the remap.config for reloading: %s\n", err.Error())
} else if rc == 0 && ok == true {
log.Infoln("updated the remap.config for reloading.")
}
}
if err := trops.StartServices(&syncdsUpdate, metaData); err != nil {
log.Errorln("failed to start services: " + err.Error())
metaData.PartialSuccess = true
return GitCommitAndExit(ExitCodeServicesError, PostConfigFailureExitMsg, cfg, metaData, oldMetaData)
}
// start 'teakd' if installed.
if trops.IsPackageInstalled("teakd") {
svcStatus, pid, err := util.GetServiceStatus("teakd")
if err != nil {
log.Errorf("not starting 'teakd', error getting 'teakd' run status: %s\n", err)
} else if svcStatus == util.SvcNotRunning {
running, err := util.ServiceStart("teakd", "start")
if err != nil {
log.Errorf("'teakd' was not started: %s\n", err)
} else if running {
log.Infoln("service 'teakd' started.")
} else if svcStatus == util.SvcRunning {
log.Infof("service 'teakd' was already running, pid: %v\n", pid)
}
}
}
// reload sysctl
if trops.SysCtlReload == true {
runSysctl(cfg)
}
trops.PrintWarnings()
if err := trops.UpdateTrafficOps(&syncdsUpdate); err != nil {
log.Errorf("failed to update Traffic Ops: %s\n", err.Error())
}
metaData.Succeeded = true
return GitCommitAndExit(ExitCodeSuccess, SuccessExitMsg, cfg, metaData, oldMetaData)
}
func LogPanic(f func() int) (exitCode int) {
defer func() {
if err := recover(); err != nil {
log.Errorf("panic: (err: %v) stacktrace:\n%s\n", err, tcutil.Stacktrace())
log.Infoln(FailureExitMsg)
exitCode = ExitCodeGeneralFailure
return
}
}()
return f()
}
// GitCommitAndExit attempts to git commit all changes, and logs any error.
// It then logs exitMsg at the Info level, and returns exitCode.
// This is a helper function, to reduce the duplicated commit-log-return into a single line.
func GitCommitAndExit(exitCode int, exitMsg string, cfg config.Cfg, metaData *t3cutil.ApplyMetaData, oldMetaData *t3cutil.ApplyMetaData) int {
// metadata isn't actually part of git, but we always want to write it before committing to git, so this is the right place
// files previously dropped never become "unmanaged",
// and if we delete them they're removed from oldMetaData as well as the new,
// so add the old files to the new metadata.
// This is especially important for reval runs, which don't add all files.
metaData.OwnedFilePaths = t3cutil.CombineOwnedFilePaths(metaData, oldMetaData)
if len(metaData.InstalledPackages) == 0 {
metaData.InstalledPackages = oldMetaData.InstalledPackages
}
WriteMetaData(cfg, metaData)
success := exitCode == ExitCodeSuccess
if cfg.UseGit == config.UseGitYes || cfg.UseGit == config.UseGitAuto {
if err := util.MakeGitCommitAll(cfg, util.GitChangeIsSelf, success); err != nil {
log.Errorln("git committing existing changes, dir '" + cfg.TsConfigDir + "': " + err.Error())
// nil metadata to prevent modifying the file after the final git commit
t3cutil.WriteActionLog(t3cutil.ActionLogActionGitCommitFinal, t3cutil.ActionLogStatusFailure, nil)
} else {
// nil metadata to prevent modifying the file after the final git commit
t3cutil.WriteActionLog(t3cutil.ActionLogActionGitCommitFinal, t3cutil.ActionLogStatusSuccess, nil)
}
}
if metaData.Succeeded {
// nil metadata to prevent modifying the file after the final git commit
t3cutil.WriteActionLog(t3cutil.ActionLogActionApplyEnd, t3cutil.ActionLogStatusSuccess, nil)
} else {
// nil metadata to prevent modifying the file after the final git commit
t3cutil.WriteActionLog(t3cutil.ActionLogActionApplyEnd, t3cutil.ActionLogStatusFailure, nil)
}
log.Infoln(exitMsg)
return exitCode
}
// CheckMaxmindUpdate will (if a url is set) check for a db on disk.
// If it exists, issue an IMS to determine if it needs to update the db.
// If no file or if an update is needed to be done it is downloaded and unpacked.
func CheckMaxmindUpdate(cfg config.Cfg) bool {
// Check if we have a URL for a maxmind db
// If we do, test if the file exists, do IMS based on disk time
// and download and unpack as needed
retresult := false
if cfg.MaxMindLocation != "" {
// Check if the maxmind db needs to be updated before reload
MaxMindList := strings.Split(cfg.MaxMindLocation, ",")
for _, v := range MaxMindList {
result := util.UpdateMaxmind(v, cfg.TsConfigDir, cfg.ReportOnly)
if result {
log.Infoln("maxmind database was updated from " + v)
} else {
log.Infoln("maxmind database not updated. Either not needed or curl/gunzip failure: " + v)
}
if result {
// If we've seen any database updates then return true to update ATS
retresult = true
}
}
} else {
log.Infoln(("maxmindlocation is empty, not checking for DB update"))
}
return retresult
}
const MetaDataFileName = `t3c-apply-metadata.json`
const MetaDataFileMode = 0600
// WriteMetaData writes the metaData file.
//
// The metadata file is written in the ATS config directory, so it's versioned
// with git.
//
// On error, an error is written to the log, but no error is returned.
func WriteMetaData(cfg config.Cfg, metaData *t3cutil.ApplyMetaData) {
bts, err := metaData.Format()
if err != nil {
log.Errorln("formatting metadata file: " + err.Error())
return
}
metaDataFilePath := GetMetaDataFilePath(cfg)
if err := ioutil.WriteFile(metaDataFilePath, bts, MetaDataFileMode); err != nil {
log.Errorln("writing metadata file '" + metaDataFilePath + "': " + err.Error())
return
}
}
func LoadMetaData(cfg config.Cfg) (*t3cutil.ApplyMetaData, error) {
metaDataFilePath := GetMetaDataFilePath(cfg)
bts, err := ioutil.ReadFile(metaDataFilePath)
if err != nil {
return nil, errors.New("reading metadata file '" + metaDataFilePath + "': " + err.Error())
}
metaData := &t3cutil.ApplyMetaData{}
if err := json.Unmarshal(bts, &metaData); err != nil {
return nil, errors.New("unmarshalling metadata file: " + err.Error())
}
return metaData, nil
}
func GetMetaDataFilePath(cfg config.Cfg) string {
return filepath.Join(cfg.TsConfigDir, MetaDataFileName)
}