├── .gitignore ├── VERSION ├── Gopkg.toml ├── Dockerfile ├── main.go ├── .circleci └── config.yml ├── Makefile ├── README.md ├── Gopkg.lock ├── CONTRIBUTING.md ├── cmd └── root.go ├── exec ├── run.go └── run_test.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | [[constraint]] 2 | branch = "master" 3 | name = "github.com/gliderlabs/ssh" 4 | 5 | [[constraint]] 6 | branch = "master" 7 | name = "github.com/mitchellh/go-homedir" 8 | 9 | [[constraint]] 10 | branch = "master" 11 | name = "github.com/spf13/cobra" 12 | 13 | [[constraint]] 14 | name = "github.com/spf13/viper" 15 | version = "1.0.0" 16 | 17 | [[constraint]] 18 | branch = "master" 19 | name = "golang.org/x/crypto" 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | MAINTAINER Ricardo Aravena 3 | 4 | ENV PATH $PATH:/go/bin:/usr/local/go/bin 5 | ENV GOPATH /go 6 | 7 | RUN apk add --no-cache ca-certificates 8 | 9 | COPY . /go/src/github.com/raravena80/sshrunner 10 | 11 | RUN set -x \ 12 | && apk add --no-cache --virtual .deps \ 13 | gcc libc-dev git libgcc go \ 14 | && cd /go/src/github.com/raravena80/sshrunner \ 15 | && go get ./... \ 16 | && CGO_ENABLED=0 go build -o /usr/bin/sshrunner . \ 17 | && apk del .deps \ 18 | && rm -rf /go \ 19 | && echo "Build Finished." 20 | 21 | ENTRYPOINT [ "sshrunner" ] 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017-2018 Ricardo Aravena 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import "github.com/raravena80/sshrunner/cmd" 18 | 19 | func main() { 20 | cmd.Execute() 21 | } 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: /go/src/github.com/raravena80/sshrunner 5 | docker: 6 | - image: circleci/golang:1.10 7 | environment: 8 | TEST_RESULTS: /tmp/test-results 9 | DEP_VERSION: 0.3.2 10 | steps: 11 | - checkout 12 | - run: 13 | name: Test Results Dir 14 | command: mkdir -p $TEST_RESULTS 15 | - run: 16 | name: Get Godep 17 | command: | 18 | curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep 19 | chmod +x $GOPATH/bin/dep 20 | - run: 21 | name: Get all dependencies 22 | command: | 23 | go get github.com/jstemmer/go-junit-report 24 | go get golang.org/x/tools/cmd/cover 25 | go get github.com/mattn/goveralls 26 | go get github.com/raravena80/gotestsshagent 27 | dep ensure 28 | - run: 29 | name: Run unit tests and coverage 30 | command: | 31 | trap "go-junit-report <${TEST_RESULTS}/go-test.out > ${TEST_RESULTS}/go-test-report.xml" EXIT 32 | make circle COVERALLS_TOKEN=$COVERALLS_TOKEN | tee ${TEST_RESULTS}/go-test.out 33 | - run: make 34 | - store_test_results: 35 | path: /tmp/test-results 36 | 37 | workflows: 38 | version: 2 39 | build: 40 | jobs: 41 | - build 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOPKG_BASE := github.com/raravena80/sshrunner 2 | GOPKGS := $(shell go list ./... | grep -v /vendor/) 3 | GOPKG_COVERS := $(shell go list ./... | grep -v '^$(GOPKG_BASE)/vendor/' | grep -v '^$(GOPKG_BASE)$$' | sed "s|^$(GOPKG_BASE)/|cover/|" | sed 's/$$/.cover/') 4 | COVER_MODE := atomic 5 | FIRST_GOPATH=$(shell go env GOPATH | cut -d: -f1) 6 | GOFILES = $(shell find . -name '*.go' -not -path './vendor/*') 7 | # Define in your CI system as an env var 8 | COVERALLS_TOKEN ?= undefined 9 | 10 | VERSION_DESCRIBE := $(shell git describe --tags --always) 11 | VERSION := $(shell cat VERSION) 12 | GITCOMMIT := $(shell git rev-parse --short HEAD) 13 | GITUNTRACKEDCHANGES := $(shell git status --porcelain --untracked-files=no) 14 | ifneq ($(GITUNTRACKEDCHANGES),) 15 | GITCOMMIT := $(GITCOMMIT)-dirty 16 | endif 17 | CTIMEVAR=-X $(GOPKG_BASE)/cmd.Version=$(VERSION) 18 | GO_LDFLAGS=-ldflags "-w $(CTIMEVAR)" 19 | GO_LDFLAGS_STATIC=-ldflags "-w $(CTIMEVAR) -extldflags -static" 20 | 21 | default: build 22 | 23 | cover/%.cover: % 24 | mkdir -p $(dir $@) 25 | go test -v -race -coverprofile=$@ -covermode=$(COVER_MODE) ./$< 26 | 27 | sshserverstart: 28 | echo 'Start SSH Test Server' 29 | gotestsshd & > /dev/null 2>&1 30 | 31 | sshserverstop: 32 | echo 'Stop SSH Test Server' 33 | pkill -9 gotestsshd 34 | 35 | cover/all: $(GOPKG_COVERS) 36 | echo mode: $(COVER_MODE) > $@ 37 | for f in $(GOPKG_COVERS); do test -f $$f && sed 1d $$f >> $@ || true; done 38 | 39 | goveralls: cover/all 40 | $(FIRST_GOPATH)/bin/goveralls -coverprofile=cover/all -service=circle-ci \ 41 | -repotoken $(COVERALLS_TOKEN) || echo "not sending to coveralls" 42 | 43 | circle: goveralls 44 | 45 | workdir: 46 | mkdir -p workdir 47 | 48 | build: workdir/sshrunner 49 | 50 | build-native: $(GOFILES) 51 | go build $(GO_LDFLAGS) -o workdir/sshrunner . 52 | 53 | workdir/sshrunner: $(GOFILES) 54 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build $(GO_LDFLAGS) -o workdir/sshrunner.linux.amd64 . 55 | 56 | test: test-all 57 | 58 | test-all: 59 | @go test -v $(GOPKGS) 60 | 61 | clean: 62 | rm -rf cover workdir 63 | 64 | .PHONY: default test goveralls circle build 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sshrunner [![CircleCI Build Status](https://circleci.com/gh/raravena80/sshrunner.svg?style=shield)](https://circleci.com/gh/raravena80/sshrunner) [![Coverage Status](https://coveralls.io/repos/github/raravena80/sshrunner/badge.svg?branch=master)](https://coveralls.io/github/raravena80/sshrunner?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/raravena80/sshrunner)](https://goreportcard.com/report/github.com/raravena80/sshrunner) [![Documentation](https://godoc.org/github.com/raravena80/sshrunner?status.svg)](http://godoc.org/github.com/raravena80/sshrunner) [![Apache Licensed](https://img.shields.io/badge/license-Apache2.0-blue.svg)](https://raw.githubusercontent.com/raravena80/sshrunner/master/LICENSE) 2 | Run commands across servers using ssh 3 | 4 | ## Usage 5 | ``` 6 | Sshrunner runs an ssh command across multiple servers 7 | 8 | Usage: 9 | sshrunner [flags] 10 | 11 | Flags: 12 | -s, --agentsocket string Socket for the ssh agent (default "/private/tmp/com.apple.launchd.xxx/Listeners") 13 | -c, --command string Command to run 14 | --config string config file (default is $HOME/.sshrunner.yaml) 15 | -h, --help help for sshrunner 16 | -k, --key string Ssh key to use for authentication, full path (default "/Users/raravena/.ssh/id_rsa") 17 | -m, --machines strings Hosts to run command on 18 | -p, --port int Ssh port to connect to (default 22) 19 | -a, --useagent Use agent for authentication 20 | -u, --user string User to run the command as (default "raravena") 21 | ``` 22 | 23 | ## Examples 24 | 25 | Makes /tmp/tmpdir in 17.2.2.2 and 17.2.3.2: 26 | ``` 27 | $ sshrunner -c "mkdir /tmp/tmpdir" -m 17.2.2.2,17.2.3.2 28 | ``` 29 | 30 | Creates /tmp/file file in host1 and host2 31 | ``` 32 | $ sshrunner -c "touch /tmp/file" -m host1,host2 33 | ``` 34 | 35 | Moves a file and creates another one in host1 and host2: 36 | ``` 37 | $ sshrunner -c "mv /tmp/file1 /tmp/file2; touch /tmp/file3" -m host1,host2 38 | ``` 39 | 40 | Runs with default in `~/.sshrunner.yaml` 41 | ``` 42 | $ sshrunner 43 | ``` 44 | 45 | ## Config 46 | 47 | Sample `~/.sshrunner.yaml` 48 | 49 | ``` 50 | sshrunner: 51 | user: ubuntu 52 | key: /Users/username/.ssh/id_rsa 53 | useagent: true 54 | machines: 55 | - 172.1.1.1 56 | - 172.1.1.2 57 | - 172.1.1.3 58 | - 172.1.1.4 59 | - 172.1.1.5 60 | command: sudo rm -f /var/log/syslog.* 61 | ``` 62 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/anmitsu/go-shlex" 7 | packages = ["."] 8 | revision = "648efa622239a2f6ff949fed78ee37b48d499ba4" 9 | 10 | [[projects]] 11 | name = "github.com/fsnotify/fsnotify" 12 | packages = ["."] 13 | revision = "629574ca2a5df945712d3079857300b5e4da0236" 14 | version = "v1.4.2" 15 | 16 | [[projects]] 17 | branch = "master" 18 | name = "github.com/gliderlabs/ssh" 19 | packages = ["."] 20 | revision = "ce31f3cc47feee0c38db7ecfaa154026929ffbda" 21 | 22 | [[projects]] 23 | branch = "master" 24 | name = "github.com/hashicorp/hcl" 25 | packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] 26 | revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" 27 | 28 | [[projects]] 29 | name = "github.com/inconshreveable/mousetrap" 30 | packages = ["."] 31 | revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" 32 | version = "v1.0" 33 | 34 | [[projects]] 35 | name = "github.com/magiconair/properties" 36 | packages = ["."] 37 | revision = "d419a98cdbed11a922bf76f257b7c4be79b50e73" 38 | version = "v1.7.4" 39 | 40 | [[projects]] 41 | branch = "master" 42 | name = "github.com/mitchellh/go-homedir" 43 | packages = ["."] 44 | revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" 45 | 46 | [[projects]] 47 | branch = "master" 48 | name = "github.com/mitchellh/mapstructure" 49 | packages = ["."] 50 | revision = "06020f85339e21b2478f756a78e295255ffa4d6a" 51 | 52 | [[projects]] 53 | name = "github.com/pelletier/go-toml" 54 | packages = ["."] 55 | revision = "16398bac157da96aa88f98a2df640c7f32af1da2" 56 | version = "v1.0.1" 57 | 58 | [[projects]] 59 | name = "github.com/spf13/afero" 60 | packages = [".","mem"] 61 | revision = "ec3a3111d1e1bdff38a61e09d5a5f5e974905611" 62 | version = "v1.0.1" 63 | 64 | [[projects]] 65 | name = "github.com/spf13/cast" 66 | packages = ["."] 67 | revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" 68 | version = "v1.1.0" 69 | 70 | [[projects]] 71 | branch = "master" 72 | name = "github.com/spf13/cobra" 73 | packages = ["."] 74 | revision = "ccaecb155a2177302cb56cae929251a256d0f646" 75 | 76 | [[projects]] 77 | branch = "master" 78 | name = "github.com/spf13/jwalterweatherman" 79 | packages = ["."] 80 | revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b" 81 | 82 | [[projects]] 83 | name = "github.com/spf13/pflag" 84 | packages = ["."] 85 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" 86 | version = "v1.0.0" 87 | 88 | [[projects]] 89 | name = "github.com/spf13/viper" 90 | packages = ["."] 91 | revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" 92 | version = "v1.0.0" 93 | 94 | [[projects]] 95 | branch = "master" 96 | name = "golang.org/x/crypto" 97 | packages = ["curve25519","ed25519","ed25519/internal/edwards25519","ssh","ssh/agent","ssh/testdata"] 98 | revision = "95a4943f35d008beabde8c11e5075a1b714e6419" 99 | 100 | [[projects]] 101 | branch = "master" 102 | name = "golang.org/x/sys" 103 | packages = ["unix"] 104 | revision = "83801418e1b59fb1880e363299581ee543af32ca" 105 | 106 | [[projects]] 107 | branch = "master" 108 | name = "golang.org/x/text" 109 | packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"] 110 | revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" 111 | 112 | [[projects]] 113 | branch = "v2" 114 | name = "gopkg.in/yaml.v2" 115 | packages = ["."] 116 | revision = "287cf08546ab5e7e37d55a84f7ed3fd1db036de5" 117 | 118 | [solve-meta] 119 | analyzer-name = "dep" 120 | analyzer-version = 1 121 | inputs-digest = "6f0ad9a07eac7a998912c7a84531031117efd9569ae48d955c9ffee394c786cd" 122 | solver-name = "gps-cdcl" 123 | solver-version = 1 124 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at raravena80@gmail.com. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017-2018 Ricardo Aravena 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | homedir "github.com/mitchellh/go-homedir" 22 | "github.com/raravena80/sshrunner/exec" 23 | "github.com/spf13/cobra" 24 | "github.com/spf13/viper" 25 | ) 26 | 27 | var ( 28 | cfgFile string 29 | command string 30 | user string 31 | key string 32 | agentsocket string 33 | port int 34 | machines []string 35 | Version string 36 | ) 37 | 38 | // RootCmd Main root initializer command 39 | var RootCmd = &cobra.Command{ 40 | Use: "sshrunner", 41 | Short: "Sshrunner runs ssh commands across multiple servers", 42 | Long: `Sshrunner runs ssh commands across multiple servers 43 | 44 | For example: 45 | $ sshrunner -c "mkdir /tmp/tmpdir" -m 17.2.2.2,17.2.3.2 46 | 47 | Makes /tmp/tmpdir in 17.2.2.2 and 17.2.3.2 (It can also take dns names) 48 | `, 49 | // Bare app run 50 | Run: func(cmd *cobra.Command, args []string) { 51 | var options []func(*exec.Options) 52 | options = append(options, 53 | exec.Machines(viper.GetStringSlice("sshrunner.machines"))) 54 | options = append(options, 55 | exec.User(viper.GetString("sshrunner.user"))) 56 | options = append(options, 57 | exec.Port(viper.GetInt("sshrunner.port"))) 58 | options = append(options, 59 | exec.Cmd(viper.GetString("sshrunner.command"))) 60 | options = append(options, 61 | exec.Key(viper.GetString("sshrunner.key"))) 62 | options = append(options, 63 | exec.UseAgent(viper.GetBool("sshrunner.useagent"))) 64 | exec.Run(options...) 65 | }, 66 | Version: Version, 67 | } 68 | 69 | // Execute adds all child commands to the root command and sets flags appropriately. 70 | // This is called by main.main(). It only needs to happen once to the rootCmd. 71 | func Execute() { 72 | if err := RootCmd.Execute(); err != nil { 73 | fmt.Println(err) 74 | os.Exit(1) 75 | } 76 | } 77 | 78 | func init() { 79 | cobra.OnInitialize(initConfig) 80 | 81 | curUser := os.Getenv("LOGNAME") 82 | sshKey := os.Getenv("HOME") + "/.ssh/id_rsa" 83 | 84 | // Persistent flags 85 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sshrunner.yaml)") 86 | 87 | // Local flags 88 | RootCmd.Flags().StringSliceVarP(&machines, "machines", "m", []string{}, "Hosts to run command on") 89 | viper.BindPFlag("sshrunner.machines", RootCmd.Flags().Lookup("machines")) 90 | RootCmd.Flags().IntVarP(&port, "port", "p", 22, "Ssh port to connect to") 91 | viper.BindPFlag("sshrunner.port", RootCmd.Flags().Lookup("port")) 92 | RootCmd.Flags().StringVarP(&command, "command", "c", "", "Command to run") 93 | viper.BindPFlag("sshrunner.command", RootCmd.Flags().Lookup("command")) 94 | RootCmd.Flags().StringVarP(&user, "user", "u", curUser, "User to run the command as") 95 | viper.BindPFlag("sshrunner.user", RootCmd.Flags().Lookup("user")) 96 | RootCmd.Flags().StringVarP(&key, "key", "k", sshKey, "Ssh key to use for authentication, full path") 97 | viper.BindPFlag("sshrunner.key", RootCmd.Flags().Lookup("key")) 98 | RootCmd.Flags().BoolP("useagent", "a", false, "Use agent for authentication") 99 | viper.BindPFlag("sshrunner.useagent", RootCmd.Flags().Lookup("useagent")) 100 | RootCmd.Flags().StringVarP(&agentsocket, "agentsocket", "s", os.Getenv("SSH_AUTH_SOCK"), "Socket for the ssh agent") 101 | viper.BindPFlag("sshrunner.agentsocket", RootCmd.Flags().Lookup("agentsocket")) 102 | 103 | } 104 | 105 | // initConfig reads in config file and ENV variables if set. 106 | func initConfig() { 107 | if cfgFile != "" { 108 | // Use config file from the flag. 109 | viper.SetConfigFile(cfgFile) 110 | } else { 111 | // Find home directory. 112 | home, err := homedir.Dir() 113 | if err != nil { 114 | fmt.Println(err) 115 | os.Exit(1) 116 | } 117 | 118 | // Search config in home directory with name ".sshrunner" (without extension). 119 | viper.AddConfigPath(home) 120 | viper.SetConfigName(".sshrunner") 121 | } 122 | 123 | viper.AutomaticEnv() // read in environment variables that match 124 | 125 | // If a config file is found, read it in. 126 | if err := viper.ReadInConfig(); err == nil { 127 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /exec/run.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017-2018 Ricardo Aravena 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package exec 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "golang.org/x/crypto/ssh" 21 | "golang.org/x/crypto/ssh/agent" 22 | "io/ioutil" 23 | "net" 24 | "os" 25 | "time" 26 | ) 27 | 28 | // Options Type that holds all the options for sshrunner 29 | type Options struct { 30 | machines []string 31 | port int 32 | user string 33 | cmd string 34 | key string 35 | agentSocket string 36 | useAgent bool 37 | } 38 | 39 | type executeResult struct { 40 | result string 41 | err error 42 | } 43 | 44 | // User Sets the user option 45 | func User(u string) func(*Options) { 46 | return func(e *Options) { 47 | e.user = u 48 | } 49 | } 50 | 51 | // Port Sets the port option 52 | func Port(p int) func(*Options) { 53 | return func(e *Options) { 54 | e.port = p 55 | } 56 | } 57 | 58 | // Cmd Sets the command to be run option 59 | func Cmd(c string) func(*Options) { 60 | return func(e *Options) { 61 | e.cmd = c 62 | } 63 | } 64 | 65 | // Machines Sets the list of machines where to run the command 66 | func Machines(m []string) func(*Options) { 67 | return func(e *Options) { 68 | e.machines = m 69 | } 70 | } 71 | 72 | // Key Sets the ssh key filename used to authenticate 73 | func Key(k string) func(*Options) { 74 | return func(e *Options) { 75 | e.key = k 76 | } 77 | } 78 | 79 | // UseAgent Sets whether we want to use the ssh agent to authenticate 80 | func UseAgent(u bool) func(*Options) { 81 | return func(e *Options) { 82 | e.useAgent = u 83 | } 84 | } 85 | 86 | // AgentSocket Sets the agent socket file 87 | // Default is the value for env var SSH_AUTH_SOCK 88 | func AgentSocket(s string) func(*Options) { 89 | return func(e *Options) { 90 | e.agentSocket = s 91 | } 92 | } 93 | 94 | func makeSigner(keyname string) (signer ssh.Signer, err error) { 95 | fp, err := os.Open(keyname) 96 | if err != nil { 97 | return 98 | } 99 | defer fp.Close() 100 | 101 | buf, _ := ioutil.ReadAll(fp) 102 | signer, _ = ssh.ParsePrivateKey(buf) 103 | return 104 | } 105 | 106 | func makeKeyring(key, socket string, useAgent bool) ssh.AuthMethod { 107 | signers := []ssh.Signer{} 108 | 109 | if useAgent == true { 110 | aConn, _ := net.Dial("unix", socket) 111 | sshAgent := agent.NewClient(aConn) 112 | aSigners, _ := sshAgent.Signers() 113 | for _, signer := range aSigners { 114 | signers = append(signers, signer) 115 | } 116 | } 117 | 118 | if key != "" { 119 | keys := []string{key} 120 | 121 | for _, keyname := range keys { 122 | signer, err := makeSigner(keyname) 123 | if err == nil { 124 | signers = append(signers, signer) 125 | } 126 | } 127 | } 128 | return ssh.PublicKeys(signers...) 129 | } 130 | 131 | func executeCmd(opt Options, hostname string, config *ssh.ClientConfig) executeResult { 132 | 133 | port := fmt.Sprint(opt.port) 134 | conn, err := ssh.Dial("tcp", hostname+":"+port, config) 135 | 136 | if err != nil { 137 | return executeResult{result: "Connection refused", 138 | err: err} 139 | } 140 | 141 | session, _ := conn.NewSession() 142 | defer session.Close() 143 | 144 | var stdoutBuf bytes.Buffer 145 | session.Stdout = &stdoutBuf 146 | err = session.Run(opt.cmd) 147 | 148 | return executeResult{result: hostname + ":\n" + stdoutBuf.String(), 149 | err: err} 150 | } 151 | 152 | // Run Main function that kicks off the run 153 | func Run(options ...func(*Options)) bool { 154 | opt := Options{} 155 | for _, option := range options { 156 | option(&opt) 157 | } 158 | 159 | done := make(chan bool, len(opt.machines)) 160 | 161 | config := &ssh.ClientConfig{ 162 | User: opt.user, 163 | Auth: []ssh.AuthMethod{ 164 | makeKeyring( 165 | opt.key, 166 | opt.agentSocket, 167 | opt.useAgent), 168 | }, 169 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 170 | Timeout: 5 * time.Second, 171 | } 172 | 173 | for _, m := range opt.machines { 174 | go func(hostname string) { 175 | // we’ll write results into the buffered channel of strings 176 | res := executeCmd(opt, hostname, config) 177 | if res.err == nil { 178 | fmt.Print(res.result) 179 | done <- true 180 | } else { 181 | fmt.Println(res.result, "\n", res.err) 182 | done <- false 183 | 184 | } 185 | }(m) 186 | } 187 | 188 | retval := true 189 | 190 | for i := 0; i < len(opt.machines); i++ { 191 | if !<-done { 192 | retval = false 193 | } 194 | } 195 | return retval 196 | } 197 | -------------------------------------------------------------------------------- /exec/run_test.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2017-2018 Ricardo Aravena 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package exec 16 | 17 | import ( 18 | "fmt" 19 | glssh "github.com/gliderlabs/ssh" 20 | "golang.org/x/crypto/ssh" 21 | "golang.org/x/crypto/ssh/agent" 22 | "golang.org/x/crypto/ssh/testdata" 23 | "io" 24 | "io/ioutil" 25 | "math/rand" 26 | "net" 27 | "os" 28 | "reflect" 29 | "testing" 30 | "time" 31 | ) 32 | 33 | type mockSSHKey struct { 34 | keyname string 35 | content []byte 36 | privkey agent.AddedKey 37 | pubkey ssh.PublicKey 38 | } 39 | 40 | var ( 41 | testPrivateKeys map[string]interface{} 42 | testSigners map[string]ssh.Signer 43 | testPublicKeys map[string]ssh.PublicKey 44 | sshAgentSocket string 45 | ) 46 | 47 | func init() { 48 | var err error 49 | 50 | n := len(testdata.PEMBytes) 51 | testSigners = make(map[string]ssh.Signer, n) 52 | testPrivateKeys = make(map[string]interface{}, n) 53 | testPublicKeys = make(map[string]ssh.PublicKey, n) 54 | 55 | for t, k := range testdata.PEMBytes { 56 | testPrivateKeys[t], err = ssh.ParseRawPrivateKey(k) 57 | if err != nil { 58 | panic(fmt.Sprintf("Unable to parse test key %s: %v", t, err)) 59 | } 60 | testSigners[t], err = ssh.NewSignerFromKey(testPrivateKeys[t]) 61 | if err != nil { 62 | panic(fmt.Sprintf("Unable to create signer for test key %s: %v", t, err)) 63 | } 64 | testPublicKeys[t] = testSigners[t].PublicKey() 65 | } 66 | 67 | randomStr := fmt.Sprintf("%v", rand.Intn(5000)) 68 | socketFile := "/tmp/gosocket" + randomStr + ".sock" 69 | setupSSHAgent(socketFile) 70 | time.Sleep(2 * time.Second) 71 | startSSHServer() 72 | } 73 | 74 | func setupSSHAgent(socketFile string) { 75 | done := make(chan string, 1) 76 | a := agent.NewKeyring() 77 | go func(done chan<- string) { 78 | ln, err := net.Listen("unix", socketFile) 79 | if err != nil { 80 | panic(fmt.Sprintf("Couldn't create socket for tests %v", err)) 81 | } 82 | defer ln.Close() 83 | // Need to wait until the socket is setup 84 | firstTime := true 85 | for { 86 | if firstTime == true { 87 | done <- socketFile 88 | firstTime = false 89 | } 90 | c, err := ln.Accept() 91 | if err != nil { 92 | panic(fmt.Sprintf("Couldn't accept connection to agent tests %v", err)) 93 | } 94 | defer c.Close() 95 | go func(c io.ReadWriter) { 96 | err = agent.ServeAgent(a, c) 97 | if err != nil { 98 | fmt.Sprintf("Couldn't serve ssh agent for tests %v", err) 99 | } 100 | 101 | }(c) 102 | } 103 | 104 | }(done) 105 | sshAgentSocket = <-done 106 | } 107 | 108 | func addKeytoSSHAgent(key agent.AddedKey) { 109 | aConn, _ := net.Dial("unix", sshAgentSocket) 110 | sshAgent := agent.NewClient(aConn) 111 | sshAgent.Add(key) 112 | } 113 | 114 | func removeKeyfromSSHAgent(key ssh.PublicKey) { 115 | aConn, _ := net.Dial("unix", sshAgentSocket) 116 | sshAgent := agent.NewClient(aConn) 117 | sshAgent.Remove(key) 118 | } 119 | 120 | func startSSHServer() { 121 | done := make(chan bool, 1) 122 | go func(done chan<- bool) { 123 | glssh.Handle(func(s glssh.Session) { 124 | authorizedKey := ssh.MarshalAuthorizedKey(s.PublicKey()) 125 | io.WriteString(s, fmt.Sprintf("public key used by %s:\n", s.User())) 126 | s.Write(authorizedKey) 127 | s.Exit(0) 128 | }) 129 | 130 | publicKeyOption := glssh.PublicKeyAuth(func(ctx glssh.Context, key glssh.PublicKey) bool { 131 | for _, pubk := range testPublicKeys { 132 | if glssh.KeysEqual(key, pubk) { 133 | return true 134 | } 135 | } 136 | return false 137 | }) 138 | 139 | fmt.Println("starting ssh server on port 2222...") 140 | done <- true 141 | panic(glssh.ListenAndServe(":2222", nil, publicKeyOption)) 142 | }(done) 143 | <-done 144 | } 145 | 146 | func TestMakeSigner(t *testing.T) { 147 | tests := []struct { 148 | name string 149 | key mockSSHKey 150 | expected ssh.Signer 151 | }{ 152 | {name: "Basic key signer with valid rsa key", 153 | key: mockSSHKey{ 154 | keyname: "/tmp/mockkey", 155 | content: testdata.PEMBytes["rsa"], 156 | }, 157 | expected: testSigners["rsa"], 158 | }, 159 | {name: "Basic key signer with valid dsa key", 160 | key: mockSSHKey{ 161 | keyname: "/tmp/mockkey", 162 | content: testdata.PEMBytes["dsa"], 163 | }, 164 | expected: testSigners["dsa"], 165 | }, 166 | {name: "Basic key signer with valid ecdsa key", 167 | key: mockSSHKey{ 168 | keyname: "/tmp/mockkey", 169 | content: testdata.PEMBytes["ecdsa"], 170 | }, 171 | expected: testSigners["ecdsa"], 172 | }, 173 | {name: "Basic key signer with valid user key", 174 | key: mockSSHKey{ 175 | keyname: "/tmp/mockkey", 176 | content: testdata.PEMBytes["user"], 177 | }, 178 | expected: testSigners["user"], 179 | }, 180 | } 181 | 182 | for _, tt := range tests { 183 | t.Run(tt.name, func(t *testing.T) { 184 | // Write content of the key to the keyname file 185 | ioutil.WriteFile(tt.key.keyname, tt.key.content, 0644) 186 | returned, _ := makeSigner(tt.key.keyname) 187 | if !reflect.DeepEqual(returned, tt.expected) { 188 | t.Errorf("Value received: %v expected %v", returned, tt.expected) 189 | } 190 | os.Remove(tt.key.keyname) 191 | }) 192 | } 193 | } 194 | 195 | func TestMakeKeyring(t *testing.T) { 196 | tests := []struct { 197 | name string 198 | useagent bool 199 | key mockSSHKey 200 | expected ssh.AuthMethod 201 | }{ 202 | {name: "Basic key ring with valid rsa key", 203 | useagent: false, 204 | key: mockSSHKey{ 205 | keyname: "/tmp/mockkey", 206 | content: testdata.PEMBytes["rsa"], 207 | }, 208 | expected: ssh.PublicKeys(testSigners["rsa"]), 209 | }, 210 | {name: "Basic key ring with valid dsa key", 211 | useagent: false, 212 | key: mockSSHKey{ 213 | keyname: "/tmp/mockkey11", 214 | content: testdata.PEMBytes["dsa"], 215 | }, 216 | expected: ssh.PublicKeys(testSigners["dsa"]), 217 | }, 218 | {name: "Basic key ring with valid ecdsa key", 219 | useagent: false, 220 | key: mockSSHKey{ 221 | keyname: "/tmp/mockkey12", 222 | content: testdata.PEMBytes["ecdsa"], 223 | }, 224 | expected: ssh.PublicKeys(testSigners["ecdsa"]), 225 | }, 226 | {name: "Basic key ring with valid user key", 227 | useagent: false, 228 | key: mockSSHKey{ 229 | keyname: "/tmp/mockkey13", 230 | content: testdata.PEMBytes["user"], 231 | }, 232 | expected: ssh.PublicKeys(testSigners["user"]), 233 | }, 234 | {name: "Basic key ring agent with valid rsa key", 235 | useagent: true, 236 | key: mockSSHKey{ 237 | keyname: "", 238 | content: testdata.PEMBytes["rsa"], 239 | privkey: agent.AddedKey{PrivateKey: testPrivateKeys["rsa"]}, 240 | pubkey: testPublicKeys["rsa"], 241 | }, 242 | expected: ssh.PublicKeys(testSigners["rsa"]), 243 | }, 244 | {name: "Basic key ring agent with valid dsa key", 245 | useagent: true, 246 | key: mockSSHKey{ 247 | keyname: "", 248 | content: testdata.PEMBytes["dsa"], 249 | privkey: agent.AddedKey{PrivateKey: testPrivateKeys["dsa"]}, 250 | pubkey: testPublicKeys["dsa"], 251 | }, 252 | expected: ssh.PublicKeys(testSigners["dsa"]), 253 | }, 254 | {name: "Basic key ring agent with valid ecdsa key", 255 | useagent: true, 256 | key: mockSSHKey{ 257 | keyname: "", 258 | content: testdata.PEMBytes["ecdsa"], 259 | privkey: agent.AddedKey{PrivateKey: testPrivateKeys["ecdsa"]}, 260 | pubkey: testPublicKeys["ecdsa"], 261 | }, 262 | expected: ssh.PublicKeys(testSigners["ecdsa"]), 263 | }, 264 | } 265 | for _, tt := range tests { 266 | t.Run(tt.name, func(t *testing.T) { 267 | if tt.useagent == true { 268 | addKeytoSSHAgent(tt.key.privkey) 269 | } 270 | // Write content of the key to the keyname file 271 | if tt.key.keyname != "" { 272 | ioutil.WriteFile(tt.key.keyname, tt.key.content, 0644) 273 | } 274 | returned := makeKeyring(tt.key.keyname, sshAgentSocket, tt.useagent) 275 | // DeepEqual always returns false for functions unless nil 276 | // hence converting to string to compare 277 | check1 := reflect.ValueOf(returned).String() 278 | check2 := reflect.ValueOf(tt.expected).String() 279 | if !reflect.DeepEqual(check1, check2) { 280 | t.Errorf("Value received: %v expected %v", returned, tt.expected) 281 | } 282 | if tt.useagent == true { 283 | removeKeyfromSSHAgent(tt.key.pubkey) 284 | } 285 | if tt.key.keyname != "" { 286 | os.Remove(tt.key.keyname) 287 | } 288 | }) 289 | } 290 | } 291 | 292 | func TestRun(t *testing.T) { 293 | tests := []struct { 294 | name string 295 | machines []string 296 | user string 297 | cmd string 298 | key mockSSHKey 299 | port int 300 | useagent bool 301 | expected bool 302 | }{ 303 | {name: "Basic with valid rsa key", 304 | machines: []string{"localhost"}, 305 | port: 2222, 306 | cmd: "ls", 307 | user: "testuser", 308 | key: mockSSHKey{ 309 | keyname: "/tmp/mockkey21", 310 | content: testdata.PEMBytes["rsa"], 311 | }, 312 | useagent: false, 313 | expected: true, 314 | }, 315 | {name: "Basic with valid rsa key wrong hostname", 316 | machines: []string{"bogushost"}, 317 | port: 2222, 318 | cmd: "ls", 319 | user: "testuser", 320 | key: mockSSHKey{ 321 | keyname: "/tmp/mockkey22", 322 | content: testdata.PEMBytes["rsa"], 323 | }, 324 | useagent: false, 325 | expected: false, 326 | }, 327 | {name: "Basic with valid rsa key wrong port", 328 | machines: []string{"localhost"}, 329 | port: 2223, 330 | cmd: "ls", 331 | user: "testuser", 332 | key: mockSSHKey{ 333 | keyname: "/tmp/mockkey23", 334 | content: testdata.PEMBytes["rsa"], 335 | }, 336 | useagent: false, 337 | expected: false, 338 | }, 339 | {name: "Basic with valid rsa key Google endpoint", 340 | machines: []string{"www.google.com"}, 341 | port: 22, 342 | cmd: "ls", 343 | user: "testuser", 344 | key: mockSSHKey{ 345 | keyname: "/tmp/mockkey24", 346 | content: testdata.PEMBytes["rsa"], 347 | }, 348 | useagent: false, 349 | expected: false, 350 | }, 351 | } 352 | for _, tt := range tests { 353 | t.Run(tt.name, func(t *testing.T) { 354 | if tt.useagent == true { 355 | addKeytoSSHAgent(tt.key.privkey) 356 | } 357 | // Write content of the key to the keyname file 358 | if tt.key.keyname != "" { 359 | ioutil.WriteFile(tt.key.keyname, tt.key.content, 0644) 360 | } 361 | returned := Run(Machines(tt.machines), 362 | User(tt.user), 363 | Port(tt.port), 364 | Cmd(tt.cmd), 365 | Key(tt.key.keyname), 366 | UseAgent(tt.useagent), 367 | AgentSocket(sshAgentSocket)) 368 | 369 | if !(returned == tt.expected) { 370 | t.Errorf("Value received: %v expected %v", returned, tt.expected) 371 | } 372 | 373 | if tt.useagent == true { 374 | removeKeyfromSSHAgent(tt.key.pubkey) 375 | } 376 | if tt.key.keyname != "" { 377 | os.Remove(tt.key.keyname) 378 | } 379 | }) 380 | } 381 | } 382 | 383 | func TestTearDown(t *testing.T) { 384 | tests := []struct { 385 | name string 386 | id string 387 | }{ 388 | {name: "Teardown SSH Agent", 389 | id: "sshAgentTdown"}, 390 | } 391 | for _, tt := range tests { 392 | t.Run(tt.name, func(t *testing.T) { 393 | if tt.id == "sshAgentTdown" { 394 | os.Remove(sshAgentSocket) 395 | } 396 | 397 | }) 398 | 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------