Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial commit

  • Loading branch information...
commit 4a7f2c783ef5521f4cc94fcd4e47d26a89aeb42a 0 parents
@jmoiron authored
Showing with 486 additions and 0 deletions.
  1. +1 −0  .gitignore
  2. +30 −0 README
  3. +13 −0 autobuild.sh
  4. +96 −0 cfg.go
  5. +162 −0 cm.go
  6. +184 −0 commands.go
1  .gitignore
@@ -0,0 +1 @@
+*.swp
30 README
@@ -0,0 +1,30 @@
+Usage: ./cm [options]:
+
+ --help, -h: show help
+ --version: show version
+ --verbose, -v: show more output
+
+cm is a (very) simple configuration manager designed to help keep and maintain
+system configs in an alternate overlay directory, which could then be separately
+backed up, distributed, or under version control. The optional file arguments
+given to cm can generally also be directories, in which case cm always behaves
+recursively. Usage follows typical version control interface, with 'cm' followed
+by a command and then arguments related to that command:
+
+ add <file> - add a file to management directory
+ rm <file> - remove file from management directory
+ show - show what files, if any, are added under cm
+ diff [file] - show diff between files under cm
+ sync [all] - sync cm overlay to cwd, or / if "all"
+ pkg [subcmd...] - package management subcommand
+
+cm can also keep a log of installed packages and install missing packages. This
+is done by using the command "pkg" (or "package") and using one of the following
+subcommands:
+
+ pkg add <name> - add package to management list
+ pkg rm <name> - remove package from management list
+ pkg sync - install packages from list
+ pkg diff [name] -
+ pkg show [name] - show packages and install status, matching optional name
+
13 autobuild.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+cur=`pwd`
+
+inotifywait -mqr --timefmt '%d/%m/%y %H:%M' --format '%T %w %f' \
+ -e modify ./ | while read date time dir file; do
+ ext="${file##*.}"
+ if [[ "$ext" = "go" ]]; then
+ echo "$file changed @ $time $date, rebuilding..."
+ go build
+ fi
+done
+
96 cfg.go
@@ -0,0 +1,96 @@
+package main
+
+// Configuration management part of cm
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+const (
+ CONFIG_PATH = "/opt/cm"
+ CONFIG_PATH_MODE = 0755
+)
+
+// return the overlay version of a path
+func C(path string) string { return filepath.Join(CONFIG_PATH, path) }
+
+// return the non-overlay version of a path
+func X(path string) string {
+ if strings.HasPrefix(path, CONFIG_PATH) {
+ return path[len(CONFIG_PATH):]
+ }
+ return path
+}
+
+// copy the file `dst` to the file `src`, creating any direcsrcries necessary
+func Copy(dst, src string) error {
+ if !isFile(src) {
+ return fmt.Errorf("Source path %s must be a file.", src)
+ }
+ ss, _ := os.Stat(src)
+ s, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer s.Close()
+ dir := filepath.Dir(dst)
+ err = os.MkdirAll(dir, 0755)
+ if err != nil {
+ return err
+ }
+ d, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, ss.Mode())
+ if err != nil {
+ return err
+ }
+ defer d.Close()
+ _, err = io.Copy(d, s)
+ return err
+}
+
+// return true if path exists and is a file, false otherwise
+func isFile(path string) bool {
+ fi, err := os.Stat(path)
+ if err == nil && !fi.IsDir() {
+ return true
+ }
+ if err != nil && !os.IsNotExist(err) {
+ fmt.Printf("%s\n", err.Error())
+ }
+ return false
+}
+
+// initialize config path if not present, return an error if either the dir
+// exists and is not writable or the dir does not exist and cannot be created
+func configPathInit() error {
+ fi, err := os.Stat(CONFIG_PATH)
+ // if it doesn't exist
+ if err != nil && os.IsNotExist(err) {
+ err = os.MkdirAll(CONFIG_PATH, CONFIG_PATH_MODE)
+ return err
+ // if it isn't a dir or symlink
+ } else if !fi.IsDir() && (fi.Mode()&os.ModeSymlink) == 0 {
+ return fmt.Errorf("Path %s must be directory or symlink.", CONFIG_PATH)
+ }
+ // if it exists, check for writability
+ for i := 0; ; i++ {
+ path := C(fmt.Sprintf("__testfile%d", i))
+ if isFile(path) {
+ continue
+ }
+ _, err := os.Create(path)
+ if err == nil {
+ os.Remove(path)
+ return nil
+ }
+ if os.IsPermission(err) {
+ return fmt.Errorf("Path %s must be writable.", CONFIG_PATH)
+ }
+ return err
+ }
+ return errors.New("Unknown error setting up config path.")
+}
162 cm.go
@@ -0,0 +1,162 @@
+package main
+
+import (
+ "fmt"
+ "github.com/jmoiron/go-pkg-optarg"
+ "os"
+)
+
+const (
+ VERSION = "0.1"
+ ABOUT = `
+cm is a (very) simple configuration manager designed to help keep and maintain
+system configs in an alternate overlay directory, which could then be separately
+backed up, distributed, or under version control. The optional file arguments
+given to cm can generally also be directories, in which case cm always behaves
+recursively. Usage follows typical version control interface, with 'cm' followed
+by a command and then arguments related to that command:
+
+ add <file> - add a file to management directory
+ rm <file> - remove file from management directory
+ show - show what files, if any, are added under cm
+ diff [file] - show diff between files under cm
+ sync [all] - sync cm overlay to cwd, or / if "all"
+ pkg [subcmd...] - package management subcommand
+
+cm can also keep a log of installed packages and install missing packages. This
+is done by using the command "pkg" (or "package") and using one of the following
+subcommands:
+
+ pkg add <name> - add package to management list
+ pkg rm <name> - remove package from management list
+ pkg sync - install packages from list
+ pkg diff [name] - show diff between list and installed, matching optional name
+ pkg show [name] - show packages and install status, matching optional name
+
+`
+ COMMAND_HELP = `
+Valid commands:
+
+ add <file> - add a file to management directory
+ rm <file> - remove file from management directory
+ show - show what files, if any, are added under cm
+ diff [file] - show diff between files under cm
+ sync [all] - sync cm overlay to cwd, or / if "all"
+ pkg add <name> - add package to management list
+ pkg rm <name> - remove package from management list
+ pkg sync - install packages from list
+ pkg diff [name] - show diff between list and installed, matching optional name
+ pkg show [name] - show packages and install status, matching optional name
+
+`
+)
+
+type Opts struct {
+ Verbose bool
+}
+
+var opts Opts
+
+func vPrintf(s string, x ...interface{}) {
+ if opts.Verbose {
+ fmt.Printf(s, x...)
+ }
+}
+
+func init() {
+ optarg.HeaderFmt = "%s"
+ optarg.Header("")
+ optarg.Add("h", "help", "show help", false)
+ optarg.Add("", "version", "show version", false)
+ optarg.Add("v", "verbose", "show more output", false)
+ for opt := range optarg.Parse() {
+ switch opt.Name {
+ case "help":
+ optarg.Usage()
+ fmt.Printf(ABOUT)
+ os.Exit(0)
+ case "version":
+ fmt.Printf("%s\n", VERSION)
+ os.Exit(0)
+ case "verbose":
+ opts.Verbose = opt.Bool()
+ }
+ }
+}
+
+func main() {
+ args := optarg.Remainder
+ if len(args) == 0 {
+ vPrintf("Nothing to do. Run `%s --help` for usage.\n", os.Args[0])
+ return
+ }
+ // sanity check, initialize the config path and confirm it's writable
+ err := configPathInit()
+ if err != nil {
+ fmt.Printf("Error: %s\n", err.Error())
+ return
+ }
+
+ cmd := args[0]
+ args = args[1:]
+
+ // work on cwd if no args are given
+ if len(args) == 0 {
+ args = append(args, ".")
+ }
+
+ switch cmd {
+ case "add":
+ // FIXME: symlink behavior?
+ for _, arg := range args {
+ err := Add(arg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error (Add): %s\n", err.Error())
+ }
+ }
+ case "rm", "remove":
+ for _, arg := range args {
+ err := Rm(arg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error (Rm): %s\n", err.Error())
+ }
+ }
+ case "show":
+ for _, arg := range args {
+ err := Show(arg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error (Show): %s\n", err.Error())
+ }
+ }
+ case "sync", "update":
+ for _, arg := range args {
+ err := Sync(arg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error (Sync): %s\n", err.Error())
+ }
+ }
+ case "diff":
+ for _, arg := range args {
+ err := Diff(arg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error (Diff): %s\n", err.Error())
+ }
+ }
+ case "pkg", "package":
+ subcmd := args[0]
+ if subcmd == "." {
+ fmt.Fprintf(os.Stderr, "Error: pkg command requires subcommand.\n")
+ return
+ }
+ args := args[1:]
+ switch subcmd {
+ default:
+ fmt.Fprintf(os.Stderr, "Error: pkg subcommand %s not recognized\n", cmd)
+ fmt.Fprintf(os.Stderr, COMMAND_HELP)
+ }
+ default:
+ fmt.Fprintf(os.Stderr, "Error: command %s not recognized\n", cmd)
+ fmt.Fprintf(os.Stderr, COMMAND_HELP)
+ }
+
+}
184 commands.go
@@ -0,0 +1,184 @@
+package main
+
+import (
+ _ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+// turn a path into a list of files. if the path denotes a file, the list
+// contains only that file. if it contains a directory, a list of that
+// directory is returned. all paths are absolute.
+func pathToFiles(path string) ([]string, error) {
+ files := []string{}
+ if isFile(path) {
+ files = append(files, path)
+ } else {
+ stat, err := os.Stat(path)
+ if err != nil {
+ return files, err
+ }
+ if stat.IsDir() {
+ fileinfos, err := ioutil.ReadDir(path)
+ if err != nil {
+ return files, err
+ }
+ for _, fi := range fileinfos {
+ if !fi.IsDir() {
+ files = append(files, filepath.Join(path, fi.Name()))
+ } else {
+ more, err := pathToFiles(filepath.Join(path, fi.Name()))
+ if err == nil {
+ newf := make([]string, len(files)+len(more))
+ copy(newf, files)
+ copy(newf[len(files):], more)
+ files = newf
+ }
+ }
+ }
+ }
+ }
+ return files, nil
+}
+
+// implement the Add command, handling one path at a time. if the path is
+// a directory, then copy all of the files over.
+func Add(path string) error {
+ var cfg string
+ files := []string{}
+ abs, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+
+ files, err = pathToFiles(abs)
+ if err != nil {
+ return err
+ }
+
+ for _, file := range files {
+ cfg = C(file)
+ err = Copy(cfg, file)
+ if err == nil {
+ fmt.Printf("%s -> %s\n", file, cfg)
+ } else {
+ fmt.Fprintf(os.Stderr, "%s could not be added: %s\n", file, err.Error())
+ }
+ }
+ return nil
+}
+
+// implement the `rm` command, handling one path at a time. the `rm` command
+// will remove files from the overlay not present in the actual working
+// filesystem.
+func Rm(path string) error {
+ abs, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+
+ dst := C(abs)
+ err = os.RemoveAll(dst)
+ if err == nil {
+ fmt.Printf("Removed %s\n", abs)
+ } else {
+ fmt.Fprintf(os.Stderr, "%s could not be removed: %s\n", abs, err.Error())
+ return err
+ }
+ return nil
+}
+
+// `show` command, handling one path at a time, shows what files exist under
+// the overlay's version of the path
+func Show(path string) error {
+ abs, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+
+ dst := C(abs)
+ cfgs, err := pathToFiles(dst)
+ if err != nil && !os.IsNotExist(err) {
+ fmt.Fprintf(os.Stderr, "Could not show %s: %s\n", dst, err.Error())
+ } else if err != nil {
+ return nil
+ }
+
+ for _, cfg := range cfgs {
+ fmt.Println(X(cfg))
+ }
+ return nil
+}
+
+// `sync` command, copies file or directory from the overlay to the actual
+// filesystem. will never delete files on filesystem, but always overwrites
+// existing ones.
+func Sync(path string) error {
+ abs, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+
+ src := C(abs)
+ cfgs, err := pathToFiles(src)
+ if err != nil && os.IsNotExist(err) {
+ return nil
+ } else if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not load files from %s: %s\n", src, err.Error())
+ return err
+ }
+
+ for _, cfg := range cfgs {
+ file := X(cfg)
+ err = Copy(file, cfg)
+ if err == nil {
+ fmt.Printf("%s -> %s\n", cfg, file)
+ } else {
+ fmt.Fprintf(os.Stderr, "%s could not be synced: %s\n", file, err.Error())
+ }
+ }
+ return nil
+}
+
+// `diff` command, diffs two files or two directories of files using the system
+// diff program with '-u'. this will only diff files which are present in the
+// overlay, and the diff shows what would happen with a `sync`
+func Diff(path string) error {
+ var cmd *exec.Cmd
+ abs, err := filepath.Abs(path)
+ if err != nil {
+ return err
+ }
+ diff := os.Getenv("CM_DIFF")
+ if len(diff) == 0 {
+ // user colordiff if available, else diff
+ diff, err = exec.LookPath("colordiff")
+ if err != nil {
+ diff, err = exec.LookPath("diff")
+ if err != nil {
+ return fmt.Errorf("Could not find suitable diff executable in your PATH.")
+ }
+ }
+ }
+
+ src := C(abs)
+ cfgs, err := pathToFiles(src)
+
+ if err != nil && os.IsNotExist(err) {
+ fmt.Fprintf(os.Stderr, "Requested file not found: %s: %s\n", src, err.Error())
+ } else if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not diff files from %s: %s\n", src, err.Error())
+ return err
+ }
+
+ for _, cfg := range cfgs {
+ cmd = exec.Command(diff, "-u", cfg, X(cfg))
+ output, _ := cmd.Output()
+ fmt.Print(string(output))
+ }
+
+ return nil
+}
Please sign in to comment.
Something went wrong with that request. Please try again.