diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..44c2b1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +release.tgz +build +build/** diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..30dad5b --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2020 Cobin Bluth + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..859a414 --- /dev/null +++ b/Readme.md @@ -0,0 +1,57 @@ +# gsh : The Missing Host Manager + +`gsh` intends to offer a new way to manage sets of hosts and a collection of accompanying scripts and files. +You can organize your inventory of hosts with groups and labels. You can add scripts and files (like configs) to your inventory. Then, `gsh` works over ssh, and you can execute scripts and/or copy files en masse to your groups of servers. +See TODO section below for future plans. + +### Requirements + +This should work on any linux host, like your workstation. Mac os builds are being tested. +Access to some ssh servers is needed to use all the features of `gsh`. Currently only bash is supported, with plans for more languages. + +### Installation + +#### A) Use Go Install +To build with go install: +```bash +$ go install github.com/cbluth/gsh +``` + +#### B) Clone via git, and build in docker + +To build in docker, golang not need to be installed: +```bash +$ git clone https://github.com/cbluth/gsh && cd gsh +$ ./build.sh # this will make a `release.tgz` file +$ tar -xzvf release.tgz # makes linux and mac builds +$ sudo cp ./build/gsh-linux /usr/local/bin/gsh +$ sudo chmod a+rx /usr/local/bin/gsh +``` + +#### C) Clone via git, and build locally + + +To build with go build: +```bash +$ git clone https://github.com/cbluth/gsh && cd gsh +$ go build . -o gsh +$ sudo cp gsh /usr/local/bin/gsh +$ sudo chmod a+rx /usr/local/bin/gsh +``` + +## How To Use It?? + +You can use `gsh` to execute scripts over ssh, and manage an inventory of servers.
+For usage, see [the docs](docs/) + +## Hack or Contribute +This project intentionally avoids importing 3rd party libraries, and tries to stick to the basic libraries provided by golang. +All contributions and bug reports are welcome, please just open an issue on the github issues page. +This is a new project, with lots of issues, if you see any bugs, please report them! :) + +To hack on this project, git clone to anywhere, and edit the code directly. +After applying edits to the codebase, you can use `./gsh.sh` to compile and run `gsh` via go, with the same arguments as running the program normally; eg: `./gsh.sh make host dev0 address=dev0.local` + +## License + +This project is licensed under the MIT License, see the [LICENSE](LICENSE) file for details. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..c163340 --- /dev/null +++ b/build.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# +# builds the project in temp docker env +# +set -e + +PROJECT="$(grep module go.mod | awk '{print $2}')" +SCRIPTPATH="$(dirname "$(readlink -f "${0}")")" +DOCKERTAG="${PROJECT}-build:tmp" +BINARY="${PROJECT}" +SILENT="${1}" # ./build.sh -s +DOCKERFILE="$(cat << EOF +FROM golang:latest as build +SHELL ["/bin/bash", "-c"] +WORKDIR /build +ADD . /build +RUN GOOS=linux GOARCH=amd64 go build \ + -a \ + -o /build/${PROJECT}-linux \ + . +RUN GOOS=darwin GOARCH=amd64 go build \ + -a \ + -o /build/${PROJECT}-darwin \ + . +FROM golang:latest +COPY --from=build /build/${PROJECT}-linux /build/ +COPY --from=build /build/${PROJECT}-darwin /build/ +CMD ["bash", "-c", "tar -cv /build/ | gzip"] +EOF +)" + +spinner() +{ + # http://fitnr.com/showing-a-bash-spinner.html + local pid=$1 + local delay=0.15 + local spinstr='|/-\' + while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do + local temp=${spinstr#?} + printf " [%c] " "$spinstr" + local spinstr=$temp${spinstr%"$temp"} + sleep $delay + printf "\b\b\b\b\b\b" + done + printf " \b\b\b\b" +} + +build() +{ + pushd "${SCRIPTPATH}/" > /dev/null 2>&1 + echo + echo "Using temporary docker build environment..." + echo + docker build \ + -t "${DOCKERTAG}" \ + -f - \ + . <<< "${DOCKERFILE}" + docker run --rm "${DOCKERTAG}" > "release.tgz" + # docker rmi "${DOCKERTAG}" + # chmod a+rx "${BINARY}" + echo + # echo "Removed temporary build environment" + # echo + popd > /dev/null 2>&1 + echo -n "Build time:" +} + +echo -n "Building... " +if [[ ! "${SILENT}" == "-s" ]] ; then + ( + time build + echo + # echo "${PROJECT^^} project executable: ${HOME}/.local/bin/${BINARY}" + # echo + ) & +else + (build) > /dev/null 2>&1 & +fi + +spinner $! + +# cp "${BINARY}" "${HOME}/.local/bin/" + +echo "Done!" diff --git a/cli.go b/cli.go new file mode 100644 index 0000000..b43e6e0 --- /dev/null +++ b/cli.go @@ -0,0 +1,439 @@ +package main + +import ( + "fmt" + "log" + "os" + "strings" + "strconv" + "io/ioutil" +) + + +const ( + lsHeader string = `╔%s╕ %s +║ %s └%s╮ +╿ `+undStart+`%s`+undStop+` : `+undStart+`Labels`+undStop+` +` + undStart string = "\033[4m" + undStop string = "\033[0m" +) + +func cli() error { + labels, err := getArgs() + if err != nil { + return err + } + m := mapLabels(labels) + switch m["action"] { + case "set": + { + return set(labels) + } + case "show": + { + return show(labels) + } + case "delete": + { + return del(labels) + } + case "execute": + { + // err = exec(labels[1:]) + // tmp() + return execute(labels) + } + case "copy": + { + tmp() + } + default: + { + tmp() + } + } + return nil +} + +// func action +func tmp() { +} + +// TODO: this whole thing needs a serious cleanup +func getArgs() ([]label, error) { + labels := []label{} + seen := map[string]bool{} + action := "" + t := "" + args := os.Args[1:] + for i, arg := range args { + if i == 0 { + switch arg { + case "make", "set", "add": + { + arg = "set" + } + case "show", "list", "get", "ls": + { + arg = "show" + } + case "rm", "remove", "del", "delete": + { + if len(os.Args) < 4 { + log.Fatalln("need name to delete") + } + arg = "delete" + } + case "exec", "run", "execute", "-n", "-g", "-su", "-sq": + { + arg = "execute" + } + case "copy", "cp": + { + arg = "copy" + } + default: + {} + } + labels = append(labels, label{Key: "action", Value: arg}) + action = arg + continue + } + if action == "execute" { + lbs := getExecuteArgs(args) + return lbs, nil + } else if i == 1 { + switch arg { + case "host", "hosts", "node", "nodes": + { + arg = "host" + } + case "group", "groups": + { + arg = "group" + } + case "script", "scripts": + { + arg = "script" + } + case "file", "files": + { + arg = "file" + } + default: + { + log.Fatalln("something went wrong: type") + } + } + labels = append(labels, label{Key: "type", Value: arg}) + t = arg + continue + } + if i == 2 && action == "show" && t == "group" { + db, err := openDB() + if err != nil { + return labels, err + } + q := []label{ + {Key: "action", Value: "show"}, + {Key: "type", Value: t}, + {Key: "name", Value: arg}, + } + result, err := db.get(q) + if err == nil { + lbs := delKey(result[0], "name") + lbs = delKey(lbs, "type") + labels = append(lbs, label{Key: "type", Value: "host"}) + } + break + } + if i == 2 && action != "show" { + labels = append(labels, label{Key: "name", Value: arg}) + // continue + } + if i == 2 && action == "show" && t == "host" && !strings.Contains(arg, "=") { + arg = "name="+arg + // continue + // break + } + if strings.Contains(arg, "=") { + s := strings.Split(arg, "=") + if !seen[s[0]] { + labels = append(labels, label{Key: s[0], Value: s[1]}) + } + seen[s[0]] = true + continue + } + if i == 3 && (t == "script" || t == "file") && action == "set" { + labels = append(labels, label{Key: "path", Value: arg}) + continue + } + } + // log.Println(labels) + m := mapLabels(labels) + err := *new(error) + if m["action"] == "set" && (m["type"] == "file" || m["type"] == "script") { + labels, err = replaceLabels(labels) + } + return labels, err +} + +func cliPrint(gs ...[]label) { + block1 := "" + block2 := "" + info := "" + m := mapLabels(gs[0]) + // log.Println(gs) + switch m["action"] { + case "set": + { + info = "Set!" + switch m["type"] { + case "host": + { + block1 = strings.Repeat("═", 6) + block2 = strings.Repeat("─", 10) + } + case "group": + { + block1 = strings.Repeat("═", 7) + block2 = strings.Repeat("─", 9) + } + case "script": + { + block1 = strings.Repeat("═", 8) + block2 = strings.Repeat("─", 8) + } + case "file": + { + block1 = strings.Repeat("═", 6) + block2 = strings.Repeat("─", 10) + } + } + } + case "show": + { + total := 0 + sm := mapLabels(gs[0]) + if !(len(gs) == 1 && sm["name"] == "NONE") { + total = len(gs) + } + info = "Total: " + strconv.Itoa(total) + switch m["type"] { + case "host": + { + block1 = strings.Repeat("═", 7) + block2 = strings.Repeat("─", 9) + } + case "group": + { + block1 = strings.Repeat("═", 8) + block2 = strings.Repeat("─", 8) + } + case "script": + { + block1 = strings.Repeat("═", 9) + block2 = strings.Repeat("─", 7) + } + case "file": + { + block1 = strings.Repeat("═", 7) + block2 = strings.Repeat("─", 9) + } + } + if !strings.HasSuffix(m["type"], "s") { + m["type"] = m["type"] + "s" + } + } + case "delete": + { + info = "Deleted!" + sm := mapLabels(gs[0]) + if sm["name"] == "NONE" { + info = "Not found" + } + switch m["type"] { + case "host": + { + block1 = strings.Repeat("═", 6) + block2 = strings.Repeat("─", 10) + } + case "group": + { + block1 = strings.Repeat("═", 7) + block2 = strings.Repeat("─", 9) + } + case "script": + { + block1 = strings.Repeat("═", 8) + block2 = strings.Repeat("─", 8) + } + case "file": + { + block1 = strings.Repeat("═", 6) + block2 = strings.Repeat("─", 10) + } + } + } + case "execute": + { + // err = exec(labels[1:]) + tmp() + } + case "copy": + { + tmp() + } + default: + { + log.Fatalln("something went wrong: action") + } + } + fmt.Printf( + lsHeader, + block1, + info, + strings.Title(strings.ToLower(m["type"])), + block2, + "Name", + ) + n := 0 + for _, l := range gs { + ml := mapLabels(l) + front := "├" + if n == len(gs) - 1 { + front = "└" + } + n++ + fmt.Println(front, ml["name"], ":", printLabels(l)) + } +} + +func printLabels(ls []label) string { + s := "" + seen := map[string]bool{} + for _, l := range ls { + if l.Key == "action" || l.Key == "type" || l.Key == "name" || l.Key == "publickey" { + continue + } + if seen[l.Key] { + continue + } else { + seen[l.Key] = true + k := l.Key + v := l.Value + if k == "revision" || k == "hostkey" { + if len(v) > 7 { + v = v[:8] + } + } + s += k + "=" + v + " " + } + } + return strings.TrimSpace(s) +} + +/// ISSUE HERE +func replaceLabels(labels []label) ([]label, error) { + m := mapLabels(labels) + // log.Println("MAP:", m) + // log.Println("LABELS:", labels) + revision, err := addFile(m["path"]) + if err != nil { + log.Fatalln(err) + } + // m["revision"] = revision + if m["type"] == "script" { + lang, err := getSheBang(m["path"]) + if err != nil { + return []label{}, err + } + // m["lang"] = lang + labels = append([]label{ + {Key: "lang", Value: lang}, + {Key: "revision", Value: revision}, + }, + labels..., + ) + // log.Println("predel:", labels) + labels = delKey(labels, "path") + // log.Println("postdel:", labels) + } + // log.Println("replaced:", labels) + return labels, nil +} + +func getSheBang(path string) (string, error) { + lang := "" + b, err := ioutil.ReadFile(path) + if err != nil { + return "", err + } + for i, s := range strings.Split(strings.Split(string(b), "\n")[0], " ") { + if i == 0 && !strings.Contains(s, `#!/`) { + break + } + if strings.Contains(s, "/bin/env") { + continue + } + if strings.Contains(s, "/") { + spl := strings.Split(s, "/") + lang = strings.TrimSpace(spl[len(spl)-1]) + } else { + lang = strings.TrimSpace(s) + } + } + return lang, nil +} + +func getScriptHashBang(args []string) ([]string, error) { + rev := "" + lang := "" + for _, arg := range args { + if strings.Contains(arg, "revision=") && len(arg) == 17 { + rev = strings.Split(arg, "=")[1] + } + } + if rev == "" { + return args, fmt.Errorf("%s", "revision not found") + } + dir := getDir() +"/db/" + rev[:1] + "/" + rev[1:2] + files, err := ioutil.ReadDir(dir) + if err != nil { + return args, err + } + for _, fi := range files { + if strings.HasPrefix(fi.Name(), rev) { + fBytes, err := ioutil.ReadFile(dir+"/"+fi.Name()) + if err != nil { + log.Println(err) + return args, err + } + for i, s := range strings.Split(strings.Split(string(fBytes), "\n")[0], " ") { + if i == 0 && !strings.Contains(s, `#!/`) { + break + } + if strings.Contains(s, "/bin/env") { + continue + } + if strings.Contains(s, "/") { + spl := strings.Split(s, "/") + lang = strings.TrimSpace(spl[len(spl)-1]) + } else { + lang = strings.TrimSpace(s) + } + } + } + } + if lang != "" { + args = append( + args[:3], + append( + []string{"lang="+lang}, + args[3:]..., + )..., + ) + } + return args, nil +} diff --git a/db.go b/db.go new file mode 100644 index 0000000..af7aebb --- /dev/null +++ b/db.go @@ -0,0 +1,361 @@ +package main + +import ( + "encoding/json" + "io" + "io/ioutil" + "log" + "os" + "os/user" + "sort" + "strings" + + // "strings" + "crypto/sha256" + "encoding/hex" + "sync" +) + +type ( + db struct { + dir string + hosts map[string][]label + groups map[string][]label + scripts map[string][]label + files map[string][]label + sync.RWMutex + } + label struct { + Key string `json:"key"` + Value string `json:"value"` + } + // lbls []label + dbjson struct { + dir string + Hosts []gtype `json:"hosts"` + Groups []gtype `json:"groups"` + Scripts []gtype `json:"scripts"` + Files []gtype `json:"files"` + sync.RWMutex + } + gtype struct { + Name string `json:"name"` + Labels []label `json:"labels"` + } +) + +func openDB() (*db, error) { + d := &db{ + dir: getDir(), + hosts: map[string][]label{}, + groups: map[string][]label{}, + scripts: map[string][]label{}, + files: map[string][]label{}, + } + err := d.load() + if err != nil { + return nil, err + } + return d, nil +} + + +func (db *db) load() (error) { + path := db.dir + "/db.json" + b := []byte{} + err := *new(error) + dbjson := &dbjson{} + db.Lock() + defer db.Unlock() + if _, err = os.Stat(path) ; err == nil { + b, err = ioutil.ReadFile(path) + if err != nil { + return err + } + err = json.Unmarshal(b, dbjson) + if err != nil { + return err + } + } else if os.IsNotExist(err) { + b, err := json.MarshalIndent(dbjson, "", " ") + if err != nil { + return err + } + err = ioutil.WriteFile(path, b, 0644) + if err != nil { + return err + } + } + for _, g := range dbjson.Hosts { + db.hosts[g.Name] = g.Labels + } + for _, g := range dbjson.Groups { + db.groups[g.Name] = g.Labels + // db.groups = append( + // db.groups, + // append( + // []label{ + // label{ + // Key: "name", + // Value: g.Name, + // }, + // }, + // g.Labels..., + // ), + // ) + } + for _, g := range dbjson.Scripts { + db.scripts[g.Name] = g.Labels + } + for _, g := range dbjson.Files { + db.files[g.Name] = g.Labels + } + return nil +} + +func getDir() string { + usr, err := user.Current() + if err != nil { + log.Fatalln(err) + } + dir := usr.HomeDir + "/.gsh" + if _, err := os.Stat(dir+"/db") ; os.IsNotExist(err) { + err = os.MkdirAll(dir+"/db", os.ModePerm) + if err != nil { + log.Fatalln(err) + } + } + return dir +} + +func mapLabels(labels []label) map[string]string { + m := map[string]string{} + for _, l := range labels { + m[l.Key] = l.Value + } + return m +} + +func (db *db) close() error { + path := db.dir + "/db.json" + db.Lock() + dbjson := &dbjson{} + for name, labels := range db.hosts { + dbjson.Hosts = append(dbjson.Hosts, gtype{ + Name: name, + Labels: labels, + }) + } + for name, labels := range db.groups { + dbjson.Groups = append(dbjson.Groups, gtype{ + Name: name, + Labels: labels, + }) + } + for name, labels := range db.scripts { + dbjson.Scripts = append(dbjson.Scripts, gtype{ + Name: name, + Labels: labels, + }) + } + for name, labels := range db.files { + dbjson.Files = append(dbjson.Files, gtype{ + Name: name, + Labels: labels, + }) + } + b, err := json.MarshalIndent(dbjson, "", " ") + if err != nil { + return err + } + err = ioutil.WriteFile(path, b, 0644) + if err != nil { + return err + } + return err +} + +func sortResults(in [][]label) [][]label { + names := []string{} + for _, line := range in { + m := mapLabels(line) + names = append(names, m["name"]) + } + sort.Strings(names) + out := [][]label{} + for _, name := range names { + for _, line := range in { + m := mapLabels(line) + if name == m["name"] { + out = append(out, line) + } + } + } + return out +} + +func addFile(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + h := sha256.New() + _, err = io.Copy(h, f) + if err != nil { + return "", err + } + err = f.Close() + if err != nil { + return "", err + } + filename := hex.EncodeToString(h.Sum(nil)) + f, err = os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + dir := getDir() +"/db/" + filename[:1] + if _, err := os.Stat(dir) ; os.IsNotExist(err) { + err = os.MkdirAll(dir, os.ModePerm) + if err != nil { + return "", err + } + } + dst, err := os.Create(dir + "/" + filename) + if err != nil { + return "", err + } + defer dst.Close() + // TODO: include filesize in labels + _, err = io.Copy(dst, f) + if err != nil { + return "", err + } + return filename, nil +} + +func delKey(labels []label, key string) []label { + m := mapLabels(labels) + delete(m, key) + new := []label{} + for _, l := range labels { + if _, ok := m[l.Key] ; ok { + new = append(new, l) + } + } + return new +} + +// func (db *db) queryHosts(query []label) [][]label { +// db.RLock() +// defer db.RUnlock() +// queryMap := mapLabels(query) +// return [][]label{{}} +// } + +func (db *db) hasType(t, n string) bool { + db.RLock() + defer db.RUnlock() + switch t { + case "script": + { + for name := range db.scripts { + if name == n { + return true + } + } + } + case "host": + { + for name := range db.hosts { + if name == n { + return true + } + } + } + case "group": + { + for name := range db.groups { + if name == n { + return true + } + } + } + case "file": + { + for name := range db.files { + if name == n { + return true + } + } + } + } + return false +} + +func (db *db) mapType(t, n string) map[string]string { + db.RLock() + defer db.RUnlock() + switch t { + case "script": + { + for name, labels := range db.scripts { + if name == n { + m := mapLabels(labels) + m["name"] = n + return m + } + } + } + case "host": + { + for name, labels := range db.hosts { + if name == n { + m := mapLabels(labels) + m["name"] = n + return m + } + } + } + case "group": + { + for name, labels := range db.groups { + if name == n { + m := mapLabels(labels) + m["name"] = n + return m + } + } + } + case "file": + { + for name, labels := range db.files { + if name == n { + m := mapLabels(labels) + m["name"] = n + return m + } + } + } + } + return map[string]string{} +} + +func (db *db) getFile(revision string) ([]byte, error) { + dir := db.dir + "/db/" + revision[:1] + "/" + files, err := ioutil.ReadDir(dir) + b := []byte{} + if err != nil { + // log.Println(err) + return []byte{}, err + } + for _, f := range files { + if strings.HasPrefix(f.Name(), revision) { + b, err = ioutil.ReadFile(dir+f.Name()) + if err != nil { + return []byte{}, err + } + } + } + return b, nil +} \ No newline at end of file diff --git a/delete.go b/delete.go new file mode 100644 index 0000000..1f8eb7c --- /dev/null +++ b/delete.go @@ -0,0 +1,52 @@ +package main + +func del(labels []label) error { + db, err := openDB() + if err != nil { + return err + } + defer db.close() + info, err := db.del(labels) + if err == nil { + cliPrint(info) + } + return err +} + +func (db *db) del(labels []label) ([]label, error) { + m := mapLabels(labels) + info, err := db.get( + []label{ + {Key: "name", Value: m["name"]}, + {Key: "type", Value: m["type"]}, + {Key: "action", Value: "delete"}, + }, + ) + if err != nil { + return []label{}, err + } + db.Lock() + defer db.Unlock() + switch m["type"] { + case "host": + { + delete(db.hosts, m["name"]) + } + case "file": + { + delete(db.files, m["name"]) + // db.cleanOrphans() + } + case "group": + { + delete(db.groups, m["name"]) + } + case "script": + { + delete(db.scripts, m["name"]) + // db.cleanOrphans() + } + } + // log.Println(info[0]) + return info[0], err +} \ No newline at end of file diff --git a/docs/Readme.md b/docs/Readme.md new file mode 100644 index 0000000..b872e77 --- /dev/null +++ b/docs/Readme.md @@ -0,0 +1,2 @@ +# gsh Documentation + diff --git a/docs/copy.md b/docs/copy.md new file mode 100644 index 0000000..7a429dc --- /dev/null +++ b/docs/copy.md @@ -0,0 +1,34 @@ +# Copy (WIP) + +Aliases: +- `copy` +- `cp` + +## Copying Files and Scripts to Hosts and Groups +You can copy files or scripts to sets of hosts.
+ +```bash +$ # For copying scripts/files to sets of hosts +$ gsh cp <-n|-g> [dst=/target/destination] [chmod=ug+rw,o-a] +``` + +## Examples: + +```bash +$ # copying a file to host demo1 +$ gsh copy file sshrc -n demo1 dst=/etc/sshrc chmod=a+rx +╔══════╕ File Copied! +║ File └──────────╮ +╿ Name : Labels +└ sshrc : scope=host target=demo1 dst=/etc/sshrc chmod=a+rx +2020/08/10 17:06:42 +``` +```bash +$ # copying a file to group +$ gsh copy file nginx.conf -g group1 dst=/etc/nginx/nginx.conf +╔══════╕ File Copied! +║ File └──────────╮ +╿ Name : Labels +└ nginx.conf : scope=group target=group1 dst=/etc/nginx/nginx.conf +2020/08/10 17:06:42 +``` diff --git a/docs/delete.md b/docs/delete.md new file mode 100644 index 0000000..e2357eb --- /dev/null +++ b/docs/delete.md @@ -0,0 +1,58 @@ +# Delete + +Aliases: +- `rm` +- `remove` +- `del` +- `delete` + +## Deleting Hosts, Groups, Scripts, and Files +You can remove or delete items from your inventory after adding them.
+These are the types you can remove from your inventory with `delete`: +- [`Hosts`](hosts.md) +- [`Groups`](groups.md) +- [`Scripts`](scripts.md) +- [`Files`](files.md) + +The caveat here is that you can only supply a `` and a `` + +```bash +$ # For Hosts, Groups, Scripts, and Files +$ gsh delete +``` +## Examples: + +```bash +$ # deleting a host +$ gsh del host demo1 +╔══════╕ Deleted! +║ Host └──────────╮ +╿ Name : Labels +└ demo1 : address=demo1.local demo=true +``` +```bash +$ # deleting a group +$ gsh delete group demo +╔═══════╕ Deleted! +║ Group └─────────╮ +╿ Name : Labels +└ demo : demo=true +``` +```bash +$ # deleting a script +$ gsh delete script cpuinfo +╔════════╕ Deleted! +║ Script └────────╮ +╿ Name : Labels +└ cpuinfo : lang=bash revision=8f0aaf02 +``` +```bash +$ # deleting a file +$ gsh delete file nginx.conf +╔══════╕ Deleted! +║ File └──────────╮ +╿ Name : Labels +└ nginx.conf : filename=nginx.cfg revision=6fea02c9 class=webserver +``` + +^^ Here are four examples, one for each type of item you can delete from your inventory. diff --git a/docs/execute.md b/docs/execute.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/files.md b/docs/files.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/groups.md b/docs/groups.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/hosts.md b/docs/hosts.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/labels.md b/docs/labels.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/make.md b/docs/make.md new file mode 100644 index 0000000..ee92129 --- /dev/null +++ b/docs/make.md @@ -0,0 +1,61 @@ +# Make + +Aliases: +- `make` +- `set` +- `add` + +## Making Hosts, Groups, Scripts, and Files +Your inventory is empty until you start adding items.
+These are the types you can add to your inventory with `make`: +- [`Hosts`](hosts.md) +- [`Groups`](groups.md) +- [`Scripts`](scripts.md) +- [`Files`](files.md) + +Additionally, you can append [`labels`](labels.md) to any of these. + +```bash +$ # For Hosts and Groups +$ gsh make [key=value...] +``` +```bash +$ # For Scripts and Files +$ gsh make [key=value...] +``` +## Examples: + +```bash +$ # setting host labels +$ gsh make host dev0 address=dev0.local develop=true project=foo +╔══════╕ Set! +║ Host └──────────╮ +╿ Name : Labels +└ dev0 : address=dev0.local develop=true project=foo +``` +```bash +$ # making a group +$ gsh make group develop develop=true +╔═══════╕ Set! +║ Group └─────────╮ +╿ Name : Labels +└ develop : develop=true +``` +```bash +$ # adding a script will calculate the sha256sum of the script +$ gsh make script hostname ./scripts/hostname.sh some=label another=label +╔════════╕ Set! +║ Script └────────╮ +╿ Name : Labels +└ hostname : lang=bash revision=da5070f9 some=label another=label +``` +```bash +$ # adding a file will also calculate the sha256sum +$ gsh make file nginx.conf ./configs/nginx.cfg class=webserver +╔══════╕ Set! +║ File └──────────╮ +╿ Name : Labels +└ nginx.conf : filename=nginx.cfg revision=6fea02c9 class=webserver +``` + +^^ Here are four examples, one for each type of item you can add to your inventory. diff --git a/docs/scripts.md b/docs/scripts.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/show.md b/docs/show.md new file mode 100644 index 0000000..e69de29 diff --git a/execute.go b/execute.go new file mode 100644 index 0000000..99e4dcf --- /dev/null +++ b/execute.go @@ -0,0 +1,114 @@ +package main + +import ( + "fmt" + "log" +) + +// action=execute +// type=script +// scope=host/group +// name=script_name +// target=host1/grp1 +// sudo=true +// [{action execute} {type script} {scope -n} {target d1-node} {sudo squash} {name hostname}] +func execute(labels []label) error { + // log.Println(labels) + m := mapLabels(labels) + db, err := openDB() + if err != nil { + return err + } + t := "" // type + if m["scope"] == "-n" { + t = "host" + } + if m["scope"] == "-g" { + t = "group" + } + if t == "" { + return fmt.Errorf("%s: %s", "scope unclear", m["scope"]) + } + q := []label{ + {Key: "action", Value: "execute"}, + {Key: "type", Value: t}, + {Key: "name", Value: m["target"]}, + } + lbs := []label{} + result, err := db.get(q) + if err == nil { + // cliPrint(result...) + // log.Println(result) + lbs = delKey(result[0], "name") + lbs = delKey(lbs, "name") + lbs = delKey(lbs, "type") + lbs = append(lbs, label{Key: "type", Value: "host"}) + // log.Println(v ...interface{}) + result, err = db.get(lbs) + if err != nil { + return err + } + // log.Println(result) + } + hasScript := db.hasType("script", m["name"]) + hasScope := db.hasType(t, m["target"]) + if hasScope && hasScript { + mt := db.mapType("script", m["name"]) + log.Printf("Executing script %s revision=%s ...", m["name"], mt["revision"][:8]) + for i, l := range result { + ml := mapLabels(l) + log.Printf("%v= Host: %s | Address: %s | User: %s | sudo: %s", i+1, ml["name"], ml["address"], usr, m["sudo"]) + scr, err := db.getFile(mt["revision"]) + if err != nil { + return err + } + stdout, stderr, err := runScript(string(scr), []string{}, host{Host: ml["address"], Port: 22}, false) + if len(stdout) > 1 { + fmt.Println(stdout) + } + if len(stderr) > 1 { + fmt.Println("script err:", stderr) + } + if err != nil { + fmt.Println(err) + } + } + } + // log.Println(result) + return nil +} + +func getExecuteArgs(args []string) []label { + scope := "" + // scriptName := "" + sudo := "false" + target := "" + for i, arg := range args { + if arg == "-n" || arg == "-g" { + scope = arg + target = args[i+1] + break + } + // log.Println(arg) + } + for _, arg := range args { + if arg == "-su" { + sudo = "raise" + } + if arg == "-sq" { + // dont use sudo if script has enabled + sudo = "squash" + } + } + execArgs := []label{ + {Key: "action", Value: "execute"}, + {Key: "type", Value: "script"}, + {Key: "scope", Value: scope}, + {Key: "target", Value: target}, + {Key: "sudo", Value: sudo}, + {Key: "name", Value: args[len(args)-1]}, + } + // execArgs = append(execArgs, label{}) + // os.Exit(0) + return execArgs +} \ No newline at end of file diff --git a/get.go b/get.go new file mode 100644 index 0000000..7dd8116 --- /dev/null +++ b/get.go @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: MIT + +package main + +// import "log" + +// import "log" + +func show(labels []label) error { + db, err := openDB() + if err != nil { + return err + } + result, err := db.get(labels) + if err == nil { + cliPrint(result...) + } + return err +} + +func (db *db) get(query []label) ([][]label, error) { + db.RLock() + defer db.RUnlock() + queryMap := mapLabels(query) + query = delKey(query, "type") + query = delKey(query, "action") + results := [][]label{} + // log.Println(queryMap) + // log.Println(query) + switch queryMap["type"] { + case "script": + { + for name, labels := range db.scripts { + // log.Println(name, labels) + // log.Println(queryMap) + match := map[string]bool{} + script := mapLabels(labels) + script["name"] = name + for k := range queryMap { + if k == "revision" { + if queryMap[k] == script[k][:8] { + match[k] = true + } + } else { + if queryMap[k] == script[k] { + match[k] = true + } + } + + } + if len(match) == len(query) { + labels = append( + []label{ + {Key:"name",Value:name}, + {Key:"type",Value:queryMap["type"]}, + {Key:"action",Value:queryMap["action"]}, + }, + labels...) + results = append(results, labels) + } + } + results = sortResults(results) + } + case "host": + { + for name, labels := range db.hosts { + match := map[string]bool{} + script := mapLabels(labels) + script["name"] = name + for k := range queryMap { + if queryMap[k] == script[k] { + match[k] = true + } + } + if len(match) == len(query) { + labels = append( + []label{ + {Key:"name",Value:name}, + {Key:"type",Value:queryMap["type"]}, + {Key:"action",Value:queryMap["action"]}, + }, + labels...) + results = append(results, labels) + } + } + results = sortResults(results) + // return results, nil + } + case "group": + { + for name, labels := range db.groups { + match := map[string]bool{} + script := mapLabels(labels) + script["name"] = name + for k := range queryMap { + if queryMap[k] == script[k] { + match[k] = true + } + } + if len(match) == len(query) { + labels = append( + []label{ + {Key:"name",Value:name}, + {Key:"type",Value:queryMap["type"]}, + {Key:"action",Value:queryMap["action"]}, + }, + labels...) + results = append(results, labels) + } + } + results = sortResults(results) + // return results, nil + } + case "file": + { + for name, labels := range db.files { + match := map[string]bool{} + script := mapLabels(labels) + script["name"] = name + for k := range queryMap { + if queryMap[k] == script[k] { + match[k] = true + } + } + if len(match) == len(query) { + labels = append( + []label{ + {Key:"name",Value:name}, + {Key:"type",Value:queryMap["type"]}, + {Key:"action",Value:queryMap["action"]}, + }, + labels...) + results = append(results, labels) + } + } + results = sortResults(results) + // return results, nil + } + } + // log.Println(results) + if len(results) == 0 { + results = append(results, []label{ + {Key:"name",Value:"NONE"}, + // {Key:"result",Value:"0"}, + {Key:"type",Value:queryMap["type"]}, + {Key:"action",Value:queryMap["action"]}, + }) + } + return results, nil +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4ed25a3 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/cbluth/gsh + +go 1.13 + +require golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b1681a5 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/gsh.sh b/gsh.sh new file mode 100755 index 0000000..56c9fdc --- /dev/null +++ b/gsh.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +cd "$(dirname "$(readlink -f "${0}")")" > /dev/null 2>&1 +go run . "${@}" diff --git a/main.go b/main.go new file mode 100644 index 0000000..c601e19 --- /dev/null +++ b/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" +) + +func main() { + err := cli() + if err != nil { + log.Fatalln(err) + } + log.Println() +} diff --git a/scripts/cpu.sh b/scripts/cpu.sh new file mode 100644 index 0000000..cf44327 --- /dev/null +++ b/scripts/cpu.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# cat /proc/cpuinfo | grep processor +cat /proc/cpuinfo diff --git a/scripts/hostname.sh b/scripts/hostname.sh new file mode 100755 index 0000000..f108702 --- /dev/null +++ b/scripts/hostname.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +set -e + +hostname -s diff --git a/scripts/uptime.sh b/scripts/uptime.sh new file mode 100755 index 0000000..4c63091 --- /dev/null +++ b/scripts/uptime.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +uptime + diff --git a/scripts/whoami.sh b/scripts/whoami.sh new file mode 100755 index 0000000..5edb868 --- /dev/null +++ b/scripts/whoami.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +whoami + diff --git a/set.go b/set.go new file mode 100644 index 0000000..1f49944 --- /dev/null +++ b/set.go @@ -0,0 +1,70 @@ +package main + +import ( + "log" +) + +func set(labels []label) error { + db, err := openDB() + if err != nil { + return err + } + defer db.close() + // log.Println("lbls1:", labels) + err = db.set(labels) + // log.Println("lbls2:", labels) + + if err == nil { + cliPrint(labels) + } + // log.Println("lbls3:", labels) + + return err +} + +func (db *db) set(labels []label) error { + // log.Println("front:", labels) + db.Lock() + defer db.Unlock() + m := mapLabels(labels) + labels = delKey(labels, "name") + labels = delKey(labels, "action") + labels = delKey(labels, "type") + // newlabels := labels + // delKey(&labels, "name") + // delKey(&labels, "type") + // delKey(&labels, "action") + // newlabels = delKey(newlabels, "path") + // delKey2(&newlabels, "name") + // delKey2(&newlabels, "type") + // delKey2(&newlabels, "action") + + switch m["type"] { + case "host": + { + db.hosts[m["name"]] = labels + } + case "file": + { + db.files[m["name"]] = labels + // db.cleanOrphans() + } + case "group": + { + db.groups[m["name"]] = labels + } + case "script": + { + // delKey(&labels, "path") + // log.Println(labels) + db.scripts[m["name"]] = labels + // db.cleanOrphans() + } + } + // log.Println("end:", labels) + return nil +} + +func tmp2() { + log.Println() +} \ No newline at end of file diff --git a/ssh.go b/ssh.go new file mode 100644 index 0000000..97d41bf --- /dev/null +++ b/ssh.go @@ -0,0 +1,108 @@ +package main + +import ( + "bytes" + // "fmt" + "io/ioutil" + "log" + // "net" + "os" + "os/user" + "strconv" + + // "strings" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/knownhosts" + // "golang.org/x/crypto/ssh/knownhosts" +) + +var ( + sshKey = getLocalSSHKey() + usr = getUser() +) + +type ( + host struct { + Host string + Port int + } +) + +func runScript(script string, args []string, host host, sudo bool) (string, string, error) { + hkcb, err := knownhosts.New(getHome()+"/.ssh/known_hosts") + if err != nil { + return "", "", err + } + signer, err := ssh.ParsePrivateKey(sshKey) + if err != nil { + return "", "", err + } + cfg := &ssh.ClientConfig{ + User: getUser(), + Auth: []ssh.AuthMethod{ + ssh.Password(os.Getenv("SSHPASS")), + ssh.PublicKeys(signer), + // ssh.PublicKeys(signers ...ssh.Signer) + }, + HostKeyCallback: hkcb, + } + conn, err := ssh.Dial("tcp", host.String(), cfg) + if err != nil { + log.Fatalln(err) + } + defer conn.Close() + session, err := conn.NewSession() + if err != nil { + log.Fatalln(err) + } + defer session.Close() + stderr := &bytes.Buffer{} + stdout := &bytes.Buffer{} + session.Stdin = bytes.NewReader([]byte(script)) + session.Stderr = stderr + session.Stdout = stdout + cmd := "env bash -s --" + if sudo { + cmd = "sudo " + cmd + } + if len(args) > 0 { + for _, arg := range args { + cmd = cmd + " " + arg + } + } + err = session.Run(cmd) + return stdout.String(), stderr.String(), err +} + +func getLocalSSHKey() []byte { + u, err := user.Current() + if err != nil { + log.Fatalln(err) + } + b, err := ioutil.ReadFile(u.HomeDir + "/.ssh/id_rsa") + if err != nil { + log.Fatalln(err) + } + return b +} + +func getUser() string { + u, err := user.Current() + if err != nil { + log.Fatalln(err) + } + return u.Username +} + +func getHome() string { + u, err := user.Current() + if err != nil { + log.Fatalln(err) + } + return u.HomeDir +} + +func (h host) String() string { + return h.Host + ":" + strconv.Itoa(h.Port) +} \ No newline at end of file