├── .gitignore ├── .travis.yml ├── Dockerfile ├── Makefile ├── Vagrantfile ├── repo.go ├── health.go ├── README.md ├── io.go ├── main.go └── yumfile.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.tar.gz 3 | .sass-cache/ 4 | .vagrant/ 5 | y10k 6 | Yumfile 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | 6 | install: make get-deps 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:7 2 | 3 | # install OS packages 4 | RUN yum install -y epel-release && \ 5 | yum clean all && yum makecache && \ 6 | yum install -y \ 7 | createrepo \ 8 | git \ 9 | golang \ 10 | make \ 11 | mercurial \ 12 | yum-utils 13 | 14 | # setup GOPATH and source directory 15 | RUN mkdir -p /go/{bin,pkg,src} /usr/src/y10k 16 | ENV GOPATH=/go PATH=$PATH:/go/bin 17 | 18 | # install package deps 19 | ADD Makefile /tmp/Makefile 20 | RUN cd /tmp && make get-deps 21 | 22 | # open shell in source dir 23 | CMD cd /usr/src/y10k; /bin/bash 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP = y10k 2 | APPVER = 0.3.0 3 | ARCH = $(shell uname -m) 4 | PACKAGE = $(APP)-$(APPVER).$(ARCH) 5 | TARBALL = $(PACKAGE).tar.gz 6 | 7 | GO = go 8 | RM = rm -f 9 | TAR = tar 10 | 11 | all: $(APP) 12 | 13 | $(APP): main.go io.go repo.go yumfile.go 14 | $(GO) build -x -o $(APP) 15 | 16 | get-deps: 17 | $(GO) get -u github.com/codegangsta/cli 18 | 19 | tar: $(APP) README.md 20 | mkdir $(PACKAGE) 21 | cp -r $(APP) README.md $(PACKAGE)/ 22 | $(TAR) -czf $(TARBALL) $(PACKAGE)/ 23 | $(RM) -r $(PACKAGE) 24 | 25 | clean: 26 | $(GO) clean 27 | $(RM) -f $(APP) $(TARBALL) 28 | 29 | docker-image: 30 | docker build -t cavaliercoder/y10k . 31 | 32 | docker-run: 33 | docker run -it --rm -v $(PWD):/usr/src/y10k cavaliercoder/y10k 34 | 35 | .PHONY: all get-deps tar clean docker-image docker-run 36 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | $script = <> /home/vagrant/.bashrc < 0 { 32 | msg = fmt.Sprintf("%s (v%s)", cmd.Path, matches[0][1]) 33 | } else { 34 | msg = string(out) 35 | } 36 | } 37 | 38 | Dprintf(" %-*s%s\n", colWidth, "yum:", msg) 39 | 40 | // check for rpm 41 | cmd = exec.Command("rpm", "--version") 42 | out, err = cmd.CombinedOutput() 43 | if err != nil { 44 | return err 45 | } else { 46 | // extract version string 47 | matches := rpmVersionPattern.FindAllStringSubmatch(string(out), -1) 48 | if len(matches) > 0 { 49 | msg = fmt.Sprintf("%s (v%s)", cmd.Path, matches[0][1]) 50 | } else { 51 | msg = string(out) 52 | } 53 | } 54 | 55 | Dprintf(" %-*s%s\n", colWidth, "rpm:", msg) 56 | 57 | // check for reposync 58 | cmd = exec.Command("reposync", "--help") 59 | _, err = cmd.CombinedOutput() 60 | if err != nil { 61 | return err 62 | } else { 63 | msg = cmd.Path 64 | } 65 | Dprintf(" %-*s%s\n", colWidth, "reposync:", msg) 66 | 67 | // check for createrepo 68 | cmd = exec.Command("createrepo", "--version") 69 | out, err = cmd.CombinedOutput() 70 | if err != nil { 71 | return err 72 | } else { 73 | // extract version string 74 | matches := createrepoVersionPattern.FindAllStringSubmatch(string(out), -1) 75 | if len(matches) > 0 { 76 | msg = fmt.Sprintf("%s (v%s)", cmd.Path, matches[0][1]) 77 | } else { 78 | msg = string(out) 79 | } 80 | } 81 | 82 | Dprintf(" %-*s%s\n", colWidth, "createrepo:", msg) 83 | 84 | // check for repoquery 85 | cmd = exec.Command("repoquery", "--version") 86 | out, err = cmd.CombinedOutput() 87 | if err != nil { 88 | return err 89 | } else { 90 | // extract version string 91 | matches := repoqueryVersionPattern.FindAllStringSubmatch(string(out), -1) 92 | if len(matches) > 0 { 93 | msg = fmt.Sprintf("%s (v%s)", cmd.Path, matches[0][1]) 94 | } else { 95 | msg = string(out) 96 | } 97 | } 98 | 99 | Dprintf(" %-*s%s\n", colWidth, "repoquery:", msg) 100 | 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # y10k [![Build Status](https://travis-ci.org/cavaliercoder/y10k.svg?branch=master)](https://travis-ci.org/cavaliercoder/y10k) 2 | 3 | *Simplified Yum repository management from the year 10,000 AD* 4 | 5 | y10k is a tool to deploy Yum/RPM repositories and mirrors in your local 6 | environment using settings described in a INI formatted `Yumfile`. 7 | 8 | It is a wrapper for `reposync` and `createrepo` but takes the hard work out of 9 | writing shell scripts for each of your mirrors. It also provides an abstraction 10 | to ease management with configuration management tools like Puppet and Chef. 11 | 12 | What about Pulp/Satellite/Other? I wanted a cron job that syncronizes my repos 13 | with the upstreams into a folder shared in Apache/nginx. I don't want to deploy 14 | a database, server, agents, configure channel registrations, etc. etc. 15 | 16 | y10k is inspired by tools such as Puppet's [R10K](https://github.com/puppetlabs/r10k) 17 | and Ruby's [Bundler](http://bundler.io/gemfile.html). 18 | 19 | Hey cool there's [documentation](http://cavaliercoder.github.io/y10k). 20 | 21 | Oh, and you can [download y10k](https://sourceforge.net/projects/y10k/files/latest/download) 22 | precompiled binaries. 23 | 24 | ## Usage 25 | 26 | ``` 27 | NAME: 28 | y10k - simplified yum mirror management 29 | 30 | USAGE: 31 | y10k [global options] command [command options] [arguments...] 32 | 33 | VERSION: 34 | 0.3.0 35 | 36 | AUTHOR(S): 37 | Ryan Armstrong 38 | 39 | COMMANDS: 40 | yumfile work with a Yumfile 41 | version print the version of y10k 42 | help, h Shows a list of commands or help for one command 43 | 44 | GLOBAL OPTIONS: 45 | --logfile, -l redirect output to a log file [$Y10K_LOGFILE] 46 | --quiet, -q less verbose 47 | --debug, -d print debug output [$Y10K_DEBUG] 48 | --tmppath, -t "/tmp/y10k" path to y10k temporary objects [$Y10K_TMPPATH] 49 | --help, -h show help 50 | --version, -v print the version 51 | 52 | ``` 53 | 54 | ## Yumfile format 55 | 56 | ```ini 57 | # 58 | # Global settings 59 | # 60 | pathprefix=/var/www/html/pub 61 | 62 | # 63 | # CentOS 7 x86_64 mirror 64 | # 65 | [centos-7-x86_64-base] 66 | name=CentOS 7 x86_64 Base 67 | mirrorlist=http://mirrorlist.centos.orgbroken/?release=7&arch=x86_64&repo=os 68 | localpath=centos/7/os/x86_64 69 | arch=x86_64 70 | 71 | [centos-7-x86_64-updates] 72 | name=CentOS 7 x86_64 Updates 73 | mirrorlist=http://mirrorlist.centos.org/?release=7&arch=x86_64&repo=updates 74 | localpath=centos/7/updates/x86_64 75 | arch=x86_64 76 | 77 | ``` 78 | 79 | ## License 80 | 81 | Y10K Copyright (C) 2014 Ryan Armstrong (ryan@cavaliercoder.com) 82 | 83 | This program is free software: you can redistribute it and/or modify it under 84 | the terms of the GNU General Public License as published by the Free Software 85 | Foundation, either version 3 of the License, or (at your option) any later 86 | version. 87 | 88 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 89 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 90 | PARTICULAR PURPOSE. See the GNU General Public License for more details. 91 | 92 | You should have received a copy of the GNU General Public License along with 93 | this program. If not, see http://www.gnu.org/licenses/. 94 | -------------------------------------------------------------------------------- /io.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | LOG_CAT_ERROR = iota 14 | LOG_CAT_WARN 15 | LOG_CAT_INFO 16 | LOG_CAT_DEBUG 17 | ) 18 | 19 | var ( 20 | cmd *exec.Cmd = nil 21 | logfileHandle *os.File = nil 22 | logger *log.Logger = nil 23 | ) 24 | 25 | func InitLogFile() { 26 | if LogFilePath == "" { 27 | return 28 | } 29 | 30 | f, err := os.OpenFile(LogFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 31 | PanicOn(err) 32 | 33 | logger = log.New(f, "", log.LstdFlags) 34 | } 35 | 36 | // CloseLogFile cleans up any file handles associates with the log file. 37 | func CloseLogFile() { 38 | if logfileHandle != nil { 39 | PanicOn(logfileHandle.Close()) 40 | } 41 | } 42 | 43 | // Logf prints output to a logfile with a category and timestamp 44 | func Logf(category int, format string, a ...interface{}) { 45 | var cat string 46 | switch category { 47 | case LOG_CAT_ERROR: 48 | cat = "ERROR" 49 | case LOG_CAT_WARN: 50 | cat = "WARNING" 51 | case LOG_CAT_INFO: 52 | cat = "INFO" 53 | case LOG_CAT_DEBUG: 54 | cat = "DEBUG" 55 | default: 56 | panic(fmt.Sprintf("Unrecognized log category: %s", category)) 57 | } 58 | 59 | logger.Printf("%s %s", cat, fmt.Sprintf(format, a...)) 60 | } 61 | 62 | // Printf prints output to STDOUT or the logfile 63 | func Printf(format string, a ...interface{}) { 64 | if logger == nil { 65 | fmt.Printf(format, a...) 66 | } else { 67 | Logf(LOG_CAT_INFO, format, a...) 68 | } 69 | } 70 | 71 | // Errorf prints an error message to log or STDOUT 72 | func Errorf(err error, format string, a ...interface{}) { 73 | if logger == nil { 74 | if err != nil { 75 | fmt.Fprintf(os.Stderr, "ERROR: %s: %s\n", fmt.Sprintf(format, a...), err.Error()) 76 | } else { 77 | fmt.Fprintf(os.Stderr, "ERROR: %s\n", fmt.Sprintf(format, a...)) 78 | } 79 | } else { 80 | if err != nil { 81 | Logf(LOG_CAT_ERROR, "%s: %s\n", fmt.Sprintf(format, a...), err.Error()) 82 | } else { 83 | Logf(LOG_CAT_ERROR, format, a...) 84 | } 85 | } 86 | } 87 | 88 | // Fatalf prints an error message to log or STDOUT and exits the program with 89 | // a non-zero exit code 90 | func Fatalf(err error, format string, a ...interface{}) { 91 | Errorf(err, format, a...) 92 | os.Exit(1) 93 | } 94 | 95 | // Dprintf prints verbose output only if debug mode is enabled 96 | func Dprintf(format string, a ...interface{}) { 97 | if DebugMode { 98 | if logger == nil { 99 | fmt.Fprintf(os.Stderr, fmt.Sprintf("DEBUG: %s", format), a...) 100 | } else { 101 | Logf(LOG_CAT_DEBUG, format, a...) 102 | } 103 | } 104 | } 105 | 106 | // Exec executes a system command and redirects the commands output to debug 107 | func Exec(path string, args ...string) error { 108 | if cmd != nil { 109 | return NewErrorf("Child process is aleady running (%s:%d)", cmd.Path, cmd.Process.Pid) 110 | } 111 | 112 | cmd = exec.Command(path, args...) 113 | defer func() { 114 | cmd = nil 115 | }() 116 | 117 | // parse stdout async 118 | stdout, err := cmd.StdoutPipe() 119 | if err != nil { 120 | return err 121 | } 122 | 123 | go func() { 124 | scanner := bufio.NewScanner(stdout) 125 | for scanner.Scan() { 126 | Dprintf("%s: %s\n", cmd.Path, scanner.Text()) 127 | } 128 | }() 129 | 130 | // attach to stderr 131 | stderr, err := cmd.StderrPipe() 132 | if err != nil { 133 | return err 134 | } 135 | 136 | go func() { 137 | scanner := bufio.NewScanner(stderr) 138 | for scanner.Scan() { 139 | Dprintf("%s: %s\n", cmd.Path, scanner.Text()) 140 | } 141 | }() 142 | 143 | // execute 144 | Dprintf("exec: %s %s\n", path, strings.Join(args, " ")) 145 | err = cmd.Start() 146 | if err != nil { 147 | return err 148 | } 149 | Dprintf("exec: started with PID: %d\n", cmd.Process.Pid) 150 | 151 | // wait for process to finish 152 | err = cmd.Wait() 153 | if err != nil { 154 | return err 155 | } 156 | Dprintf("exec: finished\n") 157 | cmd = nil 158 | 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/codegangsta/cli" 7 | "os" 8 | "os/signal" 9 | ) 10 | 11 | var ( 12 | QuietMode bool 13 | DebugMode bool 14 | YumfilePath string 15 | LogFilePath string 16 | TmpBasePath string 17 | TmpYumConfPath string 18 | TmpYumLogFile string 19 | TmpYumCachePath string 20 | ) 21 | 22 | func main() { 23 | // ensure logfile handle gets cleaned up 24 | defer CloseLogFile() 25 | 26 | // route request 27 | app := cli.NewApp() 28 | app.Name = "y10k" 29 | app.Version = "0.3.0" 30 | app.Author = "Ryan Armstrong" 31 | app.Email = "ryan@cavaliercoder.com" 32 | app.Usage = "simplified yum mirror management" 33 | app.Flags = []cli.Flag{ 34 | cli.StringFlag{ 35 | Name: "logfile, l", 36 | Usage: "redirect output to a log file", 37 | EnvVar: "Y10K_LOGFILE", 38 | }, 39 | cli.BoolFlag{ 40 | Name: "quiet, q", 41 | Usage: "less verbose", 42 | }, 43 | cli.BoolFlag{ 44 | Name: "debug, d", 45 | Usage: "print debug output", 46 | EnvVar: "Y10K_DEBUG", 47 | }, 48 | cli.StringFlag{ 49 | Name: "tmppath, t", 50 | Usage: "path to y10k temporary objects", 51 | Value: "/tmp/y10k", 52 | EnvVar: "Y10K_TMPPATH", 53 | }, 54 | } 55 | 56 | app.Commands = []cli.Command{ 57 | { 58 | Name: "yumfile", 59 | Usage: "work with a Yumfile", 60 | Flags: []cli.Flag{ 61 | cli.StringFlag{ 62 | Name: "file, f", 63 | Usage: "path to Yumfile", 64 | Value: "./Yumfile", 65 | }, 66 | }, 67 | Before: func(context *cli.Context) error { 68 | YumfilePath = context.String("file") 69 | return nil 70 | }, 71 | Subcommands: []cli.Command{ 72 | { 73 | Name: "validate", 74 | Usage: "validate a Yumfile's syntax", 75 | Action: ActionYumfileValidate, 76 | }, 77 | { 78 | Name: "list", 79 | Usage: "list repositories in a Yumfile", 80 | Action: ActionYumfileList, 81 | }, 82 | { 83 | Name: "sync", 84 | Usage: "syncronize repos described in a Yumfile", 85 | Action: ActionYumfileSync, 86 | }, 87 | }, 88 | }, 89 | { 90 | Name: "version", 91 | Usage: "print the version of y10k", 92 | Action: func(context *cli.Context) { 93 | fmt.Printf("%s version %s\n", app.Name, app.Version) 94 | }, 95 | }, 96 | } 97 | 98 | app.Before = func(context *cli.Context) error { 99 | // set globals from command line context 100 | QuietMode = context.GlobalBool("quiet") 101 | DebugMode = context.GlobalBool("debug") 102 | LogFilePath = context.GlobalString("logfile") 103 | 104 | TmpBasePath = context.GlobalString("tmppath") 105 | TmpYumConfPath = context.GlobalString("tmppath") + "/" + "yum.conf" 106 | TmpYumLogFile = context.GlobalString("tmppath") + "/" + "yum.log" 107 | TmpYumCachePath = context.GlobalString("tmppath") + "/" + "cache" 108 | 109 | // configure logging 110 | InitLogFile() 111 | 112 | return nil 113 | } 114 | 115 | // sig handler 116 | c := make(chan os.Signal, 1) 117 | signal.Notify(c, os.Interrupt) 118 | go func() { 119 | for _ = range c { 120 | Printf("Caught SIGINT/Ctrl-C. Cleaning up...\n") 121 | 122 | if cmd != nil { 123 | Printf("Attempting to terminate %s (PID: %d)...\n", cmd.Path, cmd.Process.Pid) 124 | cmd.Process.Kill() 125 | } 126 | 127 | Printf("Exiting\n") 128 | os.Exit(2) 129 | } 130 | }() 131 | 132 | app.Run(os.Args) 133 | } 134 | 135 | // ActionYumfileValidate processes the 'yumfile validate' command 136 | func ActionYumfileValidate(context *cli.Context) { 137 | yumfile, err := LoadYumfile(YumfilePath) 138 | PanicOn(err) 139 | Printf("Yumfile appears valid (%d repos)\n", len(yumfile.Repos)) 140 | } 141 | 142 | // ActionYumfileList processes the 'yumfile list' command 143 | func ActionYumfileList(context *cli.Context) { 144 | yumfile, err := LoadYumfile(YumfilePath) 145 | PanicOn(err) 146 | 147 | repoCount := len(yumfile.Repos) 148 | padding := (len(fmt.Sprintf("%d", repoCount)) * 2) + 1 149 | for i, repo := range yumfile.Repos { 150 | Printf("%*s %s -> %s\n", padding, fmt.Sprintf("%d/%d", i+1, repoCount), repo.ID, repo.LocalPath) 151 | } 152 | } 153 | 154 | // ActionYumfileSync processes the 'yumfile sync' command 155 | func ActionYumfileSync(context *cli.Context) { 156 | yumfile, err := LoadYumfile(YumfilePath) 157 | PanicOn(err) 158 | 159 | repo := context.Args().First() 160 | if repo == "" { 161 | // sync/update all repos in Yumfile 162 | if err := yumfile.SyncAll(); err != nil { 163 | Fatalf(err, "Error running Yumfile") 164 | } 165 | } else { 166 | // sync/update one repo in the Yumfile 167 | mirror := yumfile.GetRepoByID(repo) 168 | if mirror == nil { 169 | Fatalf(nil, "No such repo found in Yumfile: %s", repo) 170 | } 171 | 172 | if err := yumfile.Sync([]Repo{*mirror}); err != nil { 173 | Fatalf(err, "Error syncronizing repo '%s'", mirror.ID) 174 | } 175 | } 176 | } 177 | 178 | func PanicOn(err error) { 179 | if err != nil { 180 | Fatalf(err, "Fatal error") 181 | } 182 | } 183 | 184 | func NewErrorf(format string, a ...interface{}) error { 185 | return errors.New(fmt.Sprintf(format, a...)) 186 | } 187 | -------------------------------------------------------------------------------- /yumfile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | type Yumfile struct { 13 | Repos []Repo 14 | LocalPathPrefix string 15 | } 16 | 17 | var boolMap = map[bool]int{ 18 | true: 1, 19 | false: 0, 20 | } 21 | 22 | var ( 23 | sectionHeadPattern = regexp.MustCompile("^\\[(.*)\\]") 24 | keyValPattern = regexp.MustCompile("^(\\w+)\\s*=\\s*(.*)") 25 | commentPattern = regexp.MustCompile("(^$)|(^\\s+$)|(^#)|(^;)") 26 | ) 27 | 28 | // LoadYumfile loads a Yumfile from disk 29 | func LoadYumfile(path string) (*Yumfile, error) { 30 | Dprintf("Loading Yumfile: %s\n", path) 31 | 32 | yumfile := Yumfile{} 33 | 34 | // open file 35 | f, err := os.Open(path) 36 | if err != nil { 37 | return nil, err 38 | } 39 | defer f.Close() 40 | 41 | // read each line 42 | n := 0 43 | scanner := bufio.NewScanner(f) 44 | var repo *Repo = nil 45 | for scanner.Scan() { 46 | n++ 47 | s := scanner.Text() 48 | 49 | if matches := sectionHeadPattern.FindAllStringSubmatch(s, -1); len(matches) > 0 { 50 | // line is a [section header] 51 | id := matches[0][1] 52 | 53 | // append previous section 54 | if repo != nil { 55 | yumfile.Repos = append(yumfile.Repos, *repo) 56 | } 57 | 58 | // create new repo def 59 | repo = NewRepo() 60 | 61 | repo.YumfilePath = path 62 | repo.YumfileLineNo = n 63 | repo.ID = id 64 | } else if matches := keyValPattern.FindAllStringSubmatch(s, -1); len(matches) > 0 { 65 | // line is a key=val pair 66 | key := matches[0][1] 67 | val := matches[0][2] 68 | 69 | if repo == nil { 70 | // global key/val pair 71 | switch key { 72 | case "pathprefix": 73 | yumfile.LocalPathPrefix = val 74 | 75 | default: 76 | return nil, NewErrorf("Syntax error in Yumfile on line %d: Unknown key: %s", n, key) 77 | } 78 | } else { 79 | // add key/val to current repo 80 | switch key { 81 | case "localpath": 82 | repo.LocalPath = val 83 | 84 | case "arch": 85 | repo.Architecture = val 86 | 87 | case "newonly": 88 | if b, err := strToBool(val); err != nil { 89 | return nil, NewErrorf("Syntax error in Yumfile on line %d: %s", n, err.Error()) 90 | } else { 91 | repo.NewOnly = b 92 | } 93 | 94 | case "sources": 95 | if b, err := strToBool(val); err != nil { 96 | return nil, NewErrorf("Syntax error in Yumfile on line %d: %s", n, err.Error()) 97 | } else { 98 | repo.IncludeSources = b 99 | } 100 | 101 | case "deleteremoved": 102 | if b, err := strToBool(val); err != nil { 103 | return nil, NewErrorf("Syntax error in Yumfile on line %d: %s", n, err.Error()) 104 | } else { 105 | repo.DeleteRemoved = b 106 | } 107 | 108 | case "gpgcheck": 109 | if b, err := strToBool(val); err != nil { 110 | return nil, NewErrorf("Syntax error in Yumfile on line %d: %s", n, err.Error()) 111 | } else { 112 | repo.GPGCheck = b 113 | 114 | // pass through to yum 115 | repo.Parameters[key] = val 116 | } 117 | 118 | case "checksum": 119 | repo.Checksum = val 120 | 121 | case "groupfile": 122 | repo.Groupfile = val 123 | 124 | default: 125 | repo.Parameters[key] = val 126 | } 127 | } 128 | } else if commentPattern.MatchString(s) { 129 | // ignore line 130 | } else { 131 | return nil, NewErrorf("Syntax error in Yumfile on line %d: %s", n, s) 132 | } 133 | } 134 | 135 | // add last scanned repo 136 | if repo != nil { 137 | yumfile.Repos = append(yumfile.Repos, *repo) 138 | } 139 | 140 | // check for scan errors 141 | if err := scanner.Err(); err != nil { 142 | return nil, err 143 | } 144 | 145 | // validate 146 | if err = yumfile.Validate(); err != nil { 147 | return nil, err 148 | } 149 | 150 | return &yumfile, nil 151 | } 152 | 153 | // Validate ensures all Yumfile fields contain valid values 154 | func (c *Yumfile) Validate() error { 155 | for i, repo := range c.Repos { 156 | if err := repo.Validate(); err != nil { 157 | return err 158 | } 159 | 160 | // append path prefix to each repo 161 | if c.LocalPathPrefix != "" { 162 | c.Repos[i].LocalPath = fmt.Sprintf("%s/%s", c.LocalPathPrefix, repo.LocalPath) 163 | } 164 | 165 | // TODO: Prevent duplicate local paths and repo IDs 166 | } 167 | 168 | return nil 169 | } 170 | 171 | func (c *Yumfile) GetRepoByID(id string) *Repo { 172 | for _, repo := range c.Repos { 173 | if repo.ID == id { 174 | return &repo 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func (c *Yumfile) SyncAll() error { 182 | return c.Sync(c.Repos) 183 | } 184 | 185 | // Sync processes all repository mirrors defined in a Yumfile 186 | func (c *Yumfile) Sync(repos []Repo) error { 187 | //if err := c.installYumConf(repos); err != nil { 188 | // return err 189 | //} 190 | 191 | for _, repo := range repos { 192 | if err := c.installYumConf(&repo); err != nil { 193 | Errorf(err, "Failed to create yum.conf for %s", repo.ID) 194 | } else { 195 | if err := c.reposync(&repo); err != nil { 196 | Errorf(err, "Failed to download updates for %s", repo.ID) 197 | } else { 198 | if err := c.createrepo(&repo); err != nil { 199 | Errorf(err, "Failed to update repo database for %s", repo.ID) 200 | } 201 | } 202 | } 203 | } 204 | 205 | return nil 206 | } 207 | 208 | func (c *Yumfile) installYumConf(repo *Repo) error { 209 | Dprintf("Installing yum.conf file: %s\n", TmpYumConfPath) 210 | 211 | // create temp path 212 | if err := os.MkdirAll(TmpBasePath, 0750); err != nil { 213 | return err 214 | } 215 | 216 | // create config file 217 | f, err := os.Create(TmpYumConfPath) 218 | if err != nil { 219 | return err 220 | } 221 | defer f.Close() 222 | 223 | // global yum conf 224 | fmt.Fprintf(f, "[main]\n") 225 | fmt.Fprintf(f, "cachedir=%s\n", TmpYumCachePath) 226 | fmt.Fprintf(f, "debuglevel=10\n") 227 | fmt.Fprintf(f, "exactarch=0\n") 228 | fmt.Fprintf(f, "gpgcheck=0\n") 229 | fmt.Fprintf(f, "keepcache=0\n") 230 | fmt.Fprintf(f, "logfile=%s\n", TmpYumLogFile) 231 | fmt.Fprintf(f, "plugins=0\n") 232 | fmt.Fprintf(f, "reposdir=\n") 233 | fmt.Fprintf(f, "rpmverbosity=debug\n") 234 | fmt.Fprintf(f, "timeout=5\n") 235 | fmt.Fprintf(f, "\n") 236 | 237 | // append repo config 238 | fmt.Fprintf(f, "[%s]\n", repo.ID) 239 | for key, val := range repo.Parameters { 240 | fmt.Fprintf(f, "%s=%s\n", key, val) 241 | } 242 | fmt.Fprintf(f, "\n") 243 | 244 | return nil 245 | } 246 | 247 | func (c *Yumfile) reposync(repo *Repo) error { 248 | Printf("Syncronizing repo: %s\n", repo.ID) 249 | 250 | // compute args for reposync command 251 | args := []string{ 252 | fmt.Sprintf("--config=%s", TmpYumConfPath), 253 | fmt.Sprintf("--repoid=%s", repo.ID), 254 | "--norepopath", 255 | "--downloadcomps", 256 | "--download-metadata", 257 | } 258 | 259 | if QuietMode { 260 | args = append(args, "--quiet") 261 | } 262 | 263 | if repo.NewOnly { 264 | args = append(args, "--newest-only") 265 | } 266 | 267 | if repo.IncludeSources { 268 | args = append(args, "--source") 269 | } 270 | 271 | if repo.DeleteRemoved { 272 | args = append(args, "--delete") 273 | } 274 | 275 | if repo.GPGCheck { 276 | args = append(args, "--gpgcheck") 277 | } 278 | 279 | if repo.Architecture != "" { 280 | args = append(args, fmt.Sprintf("--arch=%s", repo.Architecture)) 281 | } 282 | 283 | if repo.LocalPath != "" { 284 | args = append(args, fmt.Sprintf("--download_path=%s", repo.LocalPath)) 285 | } else { 286 | args = append(args, fmt.Sprintf("--download_path=./%s", repo.ID)) 287 | } 288 | 289 | // execute and capture output 290 | if err := Exec("reposync", args...); err != nil { 291 | return err 292 | } 293 | 294 | return nil 295 | } 296 | 297 | func (c *Yumfile) createrepo(repo *Repo) error { 298 | Printf("Updating repo database: %s\n", repo.ID) 299 | 300 | // compute args for createrepo command 301 | args := []string{ 302 | "--update", 303 | "--database", 304 | "--checkts", 305 | fmt.Sprintf("--workers=%d", runtime.NumCPU()*2), 306 | } 307 | 308 | if QuietMode { 309 | args = append(args, "--quiet") 310 | } else { 311 | args = append(args, "--profile") 312 | } 313 | 314 | // debug switches 315 | if DebugMode { 316 | args = append(args, "--verbose") 317 | } 318 | 319 | if repo.Groupfile != "" { 320 | args = append(args, fmt.Sprintf("--groupfile=%s", repo.Groupfile)) 321 | } 322 | 323 | // non-default checksum type 324 | if repo.Checksum != "" { 325 | args = append(args, fmt.Sprintf("--checksum=%s", repo.Checksum)) 326 | } 327 | 328 | // path to create repo for 329 | if repo.LocalPath != "" { 330 | args = append(args, repo.LocalPath) 331 | } else { 332 | args = append(args, fmt.Sprintf("./%s", repo.ID)) 333 | } 334 | 335 | // execute and capture output 336 | if err := Exec("createrepo", args...); err != nil { 337 | return err 338 | } 339 | 340 | return nil 341 | } 342 | 343 | func strToBool(s string) (bool, error) { 344 | lc := strings.ToLower(s) 345 | 346 | switch lc { 347 | case "1", "true", "enabled", "yes": 348 | return true, nil 349 | 350 | case "0", "false", "disabled", "no": 351 | return false, nil 352 | } 353 | 354 | return false, NewErrorf("Invalid boolean value: %s", s) 355 | } 356 | --------------------------------------------------------------------------------