Skip to content

Commit

Permalink
add install command and update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
paulczar committed Sep 17, 2017
1 parent 78869d6 commit 709c163
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 26 deletions.
71 changes: 69 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

Gosbile is a wrapper around Ansible designed to implement a stronger "Infrastructure as Code" mentality.

When building and developing [Bluebox Cloud](https://github.com/blueboxgroup/ursula) and [Cuttle](https://github.com/ibm/cuttle) we found that
we had hundreds of deployments that were very similar so we looked to optimize our deployment workflow to solve that. We built a tool called [ursula-cli](https://github.com/blueboxgroup/ursula-cli) in Python, this is a rewrite of it in Golang. It supports most of our commonly used features, and some more.

When running a playbook with Ansible it keys off your inventory file and then uses its path to try and find `group_vars`, `host_vars`, etc. For Infrastructure as Code to work well you need to keep both your Ansible playbooks and your inventory in source control. However we wanted to take a step further and be able to keep an ssh config, known hosts file, etc and have Ansible be aware of those things.

To do this we wrote a wrapper around Ansible that keys off an environment directory rather than the inventory file, and we keep all environments in git including their `ssh_config`, `ssh_known_hosts` and often a vagrantfile, heat template or ansible playbook for infrastructure provisioning. Gosible looks for these and if they exist tells Ansible to use them. This also means as an Operator I always know how to SSH into a given environment `$ ssh -F infra/ssh_config elk01`. It will also if prompted utilize a `requirements.txt` and a `virtualenv` directory in each environment to ensure the right version of ansible (and any other deps like boto or softlayer libs) are installed.

```
.
├── ansible.cfg
├── defaults.yml
├── bastion
│   ├── group_vars
│   │   ├── all.yml
│   ├── hosts
│   ├── host_vars
│   ├── ssh_config
│   ├── ssh_known_hosts
│   └── vagrant.yml
├── infra
│   ├── group_vars
│   │   ├── all.yml
│   ├── heat_stack.yml
│   ├── hosts
│   ├── host_vars
│   ├── requirements.txt
│   ├── ssh_config
│   ├── ssh_known_hosts
│   ├── vagrant.yml
│   └── virtualenv
```

Gosbile assumes you have a directory that describes the `environment` that you wish to run ansible over. This
`environment` path contains at minimum your inventory (`./hosts`) along with any variables (`./group_vars/*`, `./host_vars/*`, etc).
It can also include an ssh config file `./ssh_config`, and an ssh known hosts file `./ssh_known_hosts`.
Expand All @@ -10,8 +42,6 @@ It is loosely based off the Bluebox project [ursula-cli](https://github.com/blue

The goal is to allow you to store everything involved in creating and deploying your infrastructure in git right alongside your Ansible Playbooks/Roles/etc.



## Install

While we're early on in development the easiest way to install Gosible is by running the following:
Expand Down Expand Up @@ -60,6 +90,43 @@ PLAY RECAP *********************************************************************
default : ok=2 changed=0 unreachable=0 failed=0
```

### Install Ansible

Gosible understands how to install Ansible:

```
$ gosible help install
Gosible install will install ansible and/or any dependencies provided.
If you do not specify a virtualenv or requirements file it will attempt
to find them in your local path, or it will try to find them in your
environment if you provide one.
Usage:
gosible install [flags]
Flags:
-e, --environment string path to your gosible environment
-h, --help help for install
-r, --requirements string path to requirements.txt
-v, --virtualenv string Path to VirtualEnv to use
```

You can see it in action by using the example environment in `tests/funtional/environment`

```
$ gosible install -e tests/functional/environment
checking if tests/functional/environment/requirements.txt exists... yes
Using VirtualEnv: tests/functional/environment/virtualenv
Using Pip: tests/functional/environment/virtualenv/bin/pip
==> Running: tests/functional/environment/virtualenv/bin/pip install -r tests/functional/environment/requirements.txt
Requirement already satisfied: ansible in ./tests/functional/environment/virtualenv/lib/python2.7/site-packages (from -r tests/functional/environment/requirements.txt (line 1))
Requirement already satisfied: softlayer in ./tests/functional/environment/virtualenv/lib/python2.7/site-packages (from -r tests/functional/environment/requirements.txt (line 2))
Requirement already satisfied: PyYAML in ./tests/functional/environment/virtualenv/lib/python2.7/site-packages (from ansible->-r tests/functional/environment/requirements.txt (line 1))
...
...
```

### Ping

Sometimes you just want to check if all the hosts in your environment are contactable, If you do not specify an environment
Expand Down
132 changes: 111 additions & 21 deletions ansible/install.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,128 @@
package ansible

import (
"fmt"
"os"
"path/filepath"
)

// Options for installing ansible
type InstallOptions struct {
Version string
Method string
Sudo bool
}

// Options for installing ansible via pip
type InstallViaPip struct {
RequirementsTXT string
VirtualEnv string
InstallOptions
Path string
}

func InstallAnsible() error {
func InstallViaPip(options *InstallOptions) error {
// TODO
return nil
}
var (
virtualenvBinary string
virtualEnv string
pipBinary string
requirementsTXT string
err error
)

func installAnsibleFromPip() error {
// TODO
return nil
}
if options.Path != "" {
if _, err = os.Stat(options.Path); os.IsNotExist(err) {
return fmt.Errorf("The specified Path (%s) does not exist", options.Path)
}
}

func installAnsibleFromApt() error {
// TODO
return nil
}
cwd, _ := os.Getwd()

// if no requirements.txt is set we should check to see if we have one in
// dir in provided path or the current working dir, if so we can use that.
if options.RequirementsTXT == "" {
if options.Path != "" {
requirementsTXT = filepath.Join(options.Path, "requirements.txt")
} else {
requirementsTXT = filepath.Join(cwd, "requirements.txt")
}
if requirementsTXT != "" {
fmt.Printf("checking if %s exists... ", requirementsTXT)
if _, err = os.Stat(requirementsTXT); err == nil {
options.RequirementsTXT = requirementsTXT
fmt.Println("yes")
} else {
fmt.Println("no")
}
}
} else {
fmt.Printf("checking if %s exists... ", options.RequirementsTXT)
if _, err = os.Stat(options.RequirementsTXT); err == nil {
fmt.Println("yes")
} else {
fmt.Println("no")
return fmt.Errorf("(%s) does not exist", options.RequirementsTXT)
}
}

// if requesting virtualenv we need to make sure virtualenv binary exists
if options.VirtualEnv != "" {
virtualenvBinary = checkBinInPath("virtualenv")
if virtualenvBinary == "" {
return fmt.Errorf("unable to find virtualenv in path")
}
}

// if no virtualenv is set we should check to see if we have a virtualenv
// dir in provided path or the current working dir, if so we can use that.
if options.VirtualEnv == "" {
//fmt.Printf("you specified path %s\n", options.Path)
if options.Path != "" {
virtualEnv = filepath.Join(options.Path, "virtualenv")
options.VirtualEnv = virtualEnv
} else {
virtualEnv = filepath.Join(cwd, "virtualenv")
fmt.Printf("checking if %s exists... ", virtualEnv)
if _, err = os.Stat(virtualEnv); err == nil {
options.VirtualEnv = virtualEnv
fmt.Println("yes")
} else {
fmt.Println("no")
}
}
}

// if requesting virtualenv we need to make sure virtualenv binary exists
if options.VirtualEnv != "" {
virtualenvBinary = checkBinInPath("virtualenv")
if virtualenvBinary == "" {
return fmt.Errorf("unable to find virtualenv in path")
}
}

// if a virtualenv is specified make sure that pip is installed
// in it and use that, otherwise make sure its in path.
if options.VirtualEnv != "" {
fmt.Printf("Using VirtualEnv: %s\n", options.VirtualEnv)
pipBinary = filepath.Join(options.VirtualEnv, "bin", "pip")
if _, err = os.Stat(pipBinary); os.IsNotExist(err) {
err = runCmd(virtualenvBinary, []string{options.VirtualEnv})
if err != nil {
return err
}
}
} else {
pipBinary = checkBinInPath("pip")
}
if pipBinary == "" {
return fmt.Errorf("unable to find pip in path")
}
fmt.Printf("Using Pip: %s\n", pipBinary)

if options.RequirementsTXT != "" {
err = runCmd(pipBinary, []string{"install", "-r", options.RequirementsTXT})
if err != nil {
return err
}
} else {
err = runCmd(pipBinary, []string{"install", "ansible"})
if err != nil {
return err
}
}

func installAnsibleFromYum() error {
// TODO
return nil
}
15 changes: 12 additions & 3 deletions ansible/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,8 @@ func isEnvironment(path string) bool {
testInventory := filepath.Join(path, "hosts")
if _, err := os.Stat(testInventory); os.IsNotExist(err) {
return false
} else {
return true
}
return true
default:
return false
}
Expand Down Expand Up @@ -132,9 +131,19 @@ func init() {
// setEnvironmentVariables("ANSIBLE_STDOUT_CALLBACK","json")
}

func checkBinInPath(binary string) string {
location, err := exec.LookPath(binary)
if err != nil {
return ""
}
return location
}


// runCmd takes a command and args and runs it, streaming output to stdout
func runCmd(cmdName string, cmdArgs []string) error {

fmt.Printf("==> Running: %s %s\n", cmdName, strings.Join(cmdArgs," "))
//return fmt.Errorf("bye")
cmd := exec.Command(cmdName, cmdArgs...)
cmdReader, err := cmd.StdoutPipe()
if err != nil {
Expand Down
40 changes: 40 additions & 0 deletions cmd/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cmd

import (
//"os/exec"
"github.com/spf13/cobra"
"github.com/paulczar/gosible/ansible"
)

var installOptions = &ansible.InstallOptions{}

// runCmd represents the run command
var installCmd = &cobra.Command{
Use: "install",
Short: "install ansible via pip",
Long: `
Gosible install will install ansible and/or any dependencies provided.
If you do not specify a virtualenv or requirements file it will attempt
to find them in your local path, or it will try to find them in your
environment if you provide one.
`,
RunE: func(cmd *cobra.Command, args []string) error {
err := ansible.InstallViaPip(installOptions)
if err != nil {
return err
}
return nil
},
}

func init() {
RootCmd.AddCommand(installCmd)
// stops parsing flags after first unknown flag is found
installCmd.Flags().SetInterspersed(false)
installCmd.Flags().StringVarP(&installOptions.VirtualEnv, "virtualenv",
"v", "", "Path to VirtualEnv to use")
installCmd.Flags().StringVarP(&installOptions.RequirementsTXT, "requirements",
"r", "", "path to requirements.txt")
installCmd.Flags().StringVarP(&installOptions.Path, "environment",
"e", "", "path to your gosible environment")
}

0 comments on commit 709c163

Please sign in to comment.