mirror/merlin/config/config.go

266 lines
8.5 KiB
Go

package config
import (
"os"
"path/filepath"
"gopkg.in/ini.v1"
"git.csclub.uwaterloo.ca/public/merlin/logger"
)
const (
DAILY = 86400
TWICE_DAILY = DAILY / 2
HOURLY = 3600
TWICE_HOURLY = HOURLY / 2
BI_HOURLY = HOURLY * 2
TRI_HOURLY = HOURLY * 3
TEN_MINUTELY = 600
FIVE_MINUTELY = 300
// could change this into a default_config
DEFAULT_MAX_JOBS = 6
DEFAULT_MAX_TIME = DAILY / 4
DEFAULT_SYNC_TYPE = "csc-sync-standard"
DEFAULT_FREQUENCY_STRING = "by-hourly"
DEFAULT_DOWNLOAD_DIR = "/mirror/root"
DEFAULT_PASSWORD_DIR = "/home/mirror/passwords"
DEFAULT_STATE_DIR = "/home/mirror/merlin/states"
DEFAULT_LOG_DIR = "/home/mirror/merlin/logs"
DEFAULT_RSYNC_LOG_DIR = "/home/mirror/merlin/logs-rsync"
DEFAULT_ZFSSYNC_LOG_DIR = "/home/mirror/merlin/logs-zfssync"
DEFAULT_SOCK_PATH = "/run/merlin.sock"
)
var frequencies = map[string]int{
"daily": DAILY,
"twice-daily": TWICE_DAILY,
"hourly": HOURLY,
"twice-hourly": TWICE_HOURLY,
"bi-hourly": BI_HOURLY,
"tri-hourly": TRI_HOURLY,
"ten-minutely": TEN_MINUTELY,
"five-minutely": FIVE_MINUTELY,
}
// Last job attempt statuses
const (
NOT_RUN_YET = iota
SUCCESS
FAILURE
TERMINATED // was killed by a signal
)
type SyncResult struct {
Name string
Exit int
}
type Config struct {
// the maximum number of jobs allowed to execute concurrently
MaxJobs int `ini:"max_jobs"`
// the IP addresses to use for rsync
IPv4Address string `ini:"ipv4_address"`
IPv6Address string `ini:"ipv6_address"`
// the default sync type
DefaultSyncType string `ini:"default_sync_type"`
// the default frequency string
DefaultFrequencyStr string `ini:"default_frequency"`
// the default MaxTime
DefaultMaxTime int `ini:"default_max_time"`
// the directory where rsync should download files
DownloadDir string `ini:"download_dir"`
// the directory where rsync passwords are stored
PasswordDir string `ini:"password_dir"`
// the directory where the state of each repo sync is saved
StateDir string `ini:"states_dir"`
// the directory where merlin will store the general logs for each repo
RepoLogDir string `ini:"repo_logs_dir"`
// the directory to store the rsync logs for each repo
RsyncLogDir string `ini:"rsync_logs_dir"`
// the directory to store the zfssync logs for each repo
ZfssyncLogDir string `ini:"zfssync_logs_dir"`
// the Unix socket path which arthur will use to communicate with us
SockPath string `ini:"sock_path"`
}
// make it more clear when full path should be used vs when just the file name is needed
type Repo struct {
// the name of this repo
Name string `ini:"-"`
// this should be one of "csc-sync-standard", etc.
SyncType string `ini:"sync_type"`
// a human-readable frequency, e.g. "bi-hourly"
FrequencyStr string `ini:"frequency"`
// the desired interval (in seconds) between successive runs
Frequency int `ini:"-"`
// the maximum time (in seconds) that each child process of this repo
// can for before being killed
MaxTime int `ini:"max_time"`
// where to download the files for this repo (relative to the download
// dir in the config)
LocalDir string `ini:"local_dir"`
// the remote host to rsync from
RsyncHost string `ini:"rsync_host"`
// the remote directory on the rsync host
RsyncDir string `ini:"rsync_dir"`
// the rsync user (optional)
RsyncUser string `ini:"rsync_user"`
// the file storing the password for rsync (optional)
PasswordFile string `ini:"password_file"`
// the file storing the repo sync state (used to override default)
StateFile string `ini:"state_file"`
// the full file path for general logging of this repo (used to override default)
RepoLogFile string `ini:"repo_log_file"`
// a reference to the general logger
Logger *logger.Logger `ini:"-"`
// the full file path for logging this repo's rsync (used to override default)
RsyncLogFile string `ini:"rsync_log_file"`
// the full file path for logging this repo's zfssync (used to override default)
ZfssyncLogFile string `ini:"zfssync_log_file"`
// the repo will write its name and status in a Result struct to DoneChan
// when it has finished a job (shared by all repos)
DoneChan chan<- SyncResult `ini:"-"`
// repos should stop syncing if StopChan is closed (shared by all repos)
StopChan chan struct{} `ini:"-"`
// a struct that stores the repo's status
State RepoState `ini:"-"`
}
// This should only be modified by the main thread
type RepoState struct {
// these are stored in the states folder
// whether this repo is running a job or not
IsRunning bool `ini:"is_running"`
// the Unix epoch timestamp at which this repo last attempted a job
LastAttemptStartTime int64 `ini:"last_attempt_time"`
// the number of seconds this repo ran for during its last attempted job
LastAttemptRunTime int64 `ini:"last_attempt_runtime"`
// whether the last attempt was successful or not
LastAttemptExit int `ini:"last_attempt_exit"`
}
var (
Conf Config
Repos []*Repo
RepoMap map[string]*Repo
)
// GetConfig reads the config from a JSON file, initializes default values,
// and initializes the non-configurable fields of each repo.
// It returns a Config.
func LoadConfig(configPath string, doneChan chan SyncResult, stopChan chan struct{}) {
// set default values then load config from file
newConf := Config{
MaxJobs: DEFAULT_MAX_JOBS,
DefaultSyncType: DEFAULT_SYNC_TYPE,
DefaultFrequencyStr: DEFAULT_FREQUENCY_STRING,
DefaultMaxTime: DEFAULT_MAX_TIME,
PasswordDir: DEFAULT_PASSWORD_DIR,
DownloadDir: DEFAULT_DOWNLOAD_DIR,
StateDir: DEFAULT_STATE_DIR,
RepoLogDir: DEFAULT_LOG_DIR,
RsyncLogDir: DEFAULT_RSYNC_LOG_DIR,
ZfssyncLogDir: DEFAULT_ZFSSYNC_LOG_DIR,
SockPath: DEFAULT_SOCK_PATH,
}
iniInfo, err := ini.Load(configPath)
panicIfErr(err)
err = iniInfo.MapTo(&newConf)
panicIfErr(err)
// check config for major errors
for _, dir := range []string{newConf.StateDir, newConf.RepoLogDir, newConf.RsyncLogDir, newConf.ZfssyncLogDir} {
err := os.MkdirAll(dir, 0755)
panicIfErr(err)
}
if newConf.IPv4Address == "" {
panic("Missing IPv4 address from config")
} else if newConf.IPv6Address == "" {
panic("Missing IPv6 address from config")
}
newRepos := make([]*Repo, 0)
for _, section := range iniInfo.Sections() {
repoName := section.Name()
if repoName == "DEFAULT" {
continue
}
// set the default values for the repo then load from file
// TODO: check if local_dir and repoName are always the same value
// TODO: check to ensure that every Repo.Name is unique (may already be done by ini)
repo := Repo{
Name: repoName,
SyncType: newConf.DefaultSyncType,
FrequencyStr: newConf.DefaultFrequencyStr,
MaxTime: newConf.DefaultMaxTime,
StateFile: filepath.Join(newConf.StateDir, repoName),
RepoLogFile: filepath.Join(newConf.RepoLogDir, repoName) + ".log",
RsyncLogFile: filepath.Join(newConf.RsyncLogDir, repoName) + "-rsync.log",
ZfssyncLogFile: filepath.Join(newConf.ZfssyncLogDir, repoName) + "-zfssync.log",
DoneChan: doneChan,
StopChan: stopChan,
}
err := section.MapTo(&repo)
panicIfErr(err)
// TODO: ensure that the parent dirs to the file also exist when touching
// or just remove the ability to override
touchFiles(
repo.StateFile,
repo.RepoLogFile,
repo.RsyncLogFile,
repo.ZfssyncLogFile,
)
repo.Logger = logger.NewLogger(repo.Name, repo.RepoLogFile)
repo.Frequency = frequencies[repo.FrequencyStr]
if repo.SyncType == "" {
panic("Missing sync type from " + repo.Name)
} else if repo.Frequency == 0 {
panic("Missing or invalid frequency for " + repo.Name)
}
repo.State = RepoState{
IsRunning: false,
LastAttemptStartTime: 0,
LastAttemptRunTime: 0,
LastAttemptExit: NOT_RUN_YET,
}
err = ini.MapTo(&repo.State, repo.StateFile)
panicIfErr(err)
repo.SaveState()
newRepos = append(newRepos, &repo)
}
if len(newRepos) == 0 {
panic("No repos found in config")
}
Conf = newConf
Repos = newRepos
RepoMap = make(map[string]*Repo)
for _, repo := range Repos {
RepoMap[repo.Name] = repo
}
}
// save the current state of the repo to a file
func (repo *Repo) SaveState() {
// repo.Logger.Debug("Saving state")
state_cfg := ini.Empty()
if err := ini.ReflectFrom(state_cfg, &repo.State); err != nil {
repo.Logger.Error(err.Error())
}
file, err := os.OpenFile(repo.StateFile, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
repo.Logger.Error(err.Error())
}
if _, err := state_cfg.WriteTo(file); err != nil {
repo.Logger.Error(err.Error())
}
// repo.Logger.Debug("Saved state")
}