Skip to content

Commit

Permalink
[ FEATURE ] AddedRun Ansible Playbook functionality (#1)
Browse files Browse the repository at this point in the history
* added Ansible playbook functionallity
* updated readme & cleaned up flags structs
  • Loading branch information
ZeljkoBenovic authored Aug 30, 2022
1 parent 2c43737 commit c86cab2
Show file tree
Hide file tree
Showing 9 changed files with 398 additions and 93 deletions.
50 changes: 40 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@ User can load a bash script or define a single command, that will execute on all

## Usage

### Parameters (flags)
* `aws-profile` - AWS profile as defined in *aws credentials* file. Default: `default`
* `aws-zone` - AWS zone in which EC2 instances reside. Default: `eu-central-1`
* `cmd` - one-liner bash command that will be executed on EC2 instances.
* `instances` - instance IDs, separated by comma (,). This is a mandatory flag.
* `log-level` - the level of logging output (info, debug, error). Default: `info`
* `output` - a file name to write the output result of a command/script. Default: `console output`
* `script` - the location of bash script file that will run on EC2 instances.

### AWS credentials
AWS credentials can be pulled from environment variables or from aws credentials file.
To define a which profile from credentials file should be used, set `aws-profile` flag. By default, it is set to `default`.
Expand All @@ -27,12 +18,51 @@ Environment variables with credentials that can be set:
* `AWS_SECRET_ACCESS_KEY` - the access key secret
* `AWS_SESSION_TOKEN` - the session token (optional)

### Example

### General Parameters
* `aws-profile` - AWS profile as defined in *aws credentials* file. Default: `default`
* `aws-zone` - AWS zone in which EC2 instances reside. Default: `eu-central-1`
* `instances` - instance IDs, separated by comma (,). This is a mandatory flag.
* `log-level` - the level of logging output (info, debug, error). Default: `info`
* `output` - a file name to write the output result of a command/script. Default: `console output`
* `mode` - switch between modes - Bash script or Ansible playbook. Default: `bash`

### Running Bash scripts
* `cmd` - one-liner bash command that will be executed on EC2 instances.
* `script` - the location of bash script file that will run on EC2 instances.
* `mode` - for running bash scripts `mode` can be omitted as the default value is `bash`

If both `cmd` and `script` flags are defined, `script` will take precedence, and `cmd` will be disregarded.

#### Example

```bash
aws-commander -instances i-0bf9c273c67f684a0,i-011c9b3e3607a63b5,i-0e53e37f7b34517f5,i-0f02ca10faf8f349e -cmd "cd /tmp && ls -lah" -aws-profile test-account
```

### Running Ansible Playbook
* `playbook` - the location of Ansible playbook that will be executed on EC2 instances.
* `dryrun` - when set to true, Ansible playbook will run and the output will be shown, but
no data will be changed.
* `mode` - for running Ansible playbook `mode` must be set to `ansible`

#### Ansible prerequisites
Every EC2 instance, that should run Ansible playbook, must have Ansible already installed.
If Ansible is not installed, the deployment will fail.
You can use `bash` mode to simply install Ansible from your OS package manager before running the playbook.

#### Example
```bash
## if Ansible is not installed on host - install Ansible
aws-commander -instances i-0bf9c273c67f684a0,i-011c9b3e3607a63b5,i-0e53e37f7b34517f5,i-0f02ca10faf8f349e -cmd "sudo apt install -y ansible" -aws-profile test-account -aws-zone us-west-2
## run playbook
aws-commander -instances i-0bf9c273c67f684a0,i-011c9b3e3607a63b5,i-0e53e37f7b34517f5,i-0f02ca10faf8f349e -mode ansible -playbook scripts/nodes-restart.yaml -aws-profile test-account -aws-zone us-west-2
```

#### Missing features
Currently, running the Ansible playbook from a remote location via URL / S3 is not supported.
It will be supported in the future release.

## License

Copyright 2022 Trapesys
Expand Down
115 changes: 90 additions & 25 deletions framework/adapters/left/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,50 +10,115 @@ import (
)

type Adapter struct {
flags *cmd.Flags
logger hclog.Logger

buffInstanceFlag string
}

func NewAdapter() ports.ICmd {
return &Adapter{
flags: &cmd.Flags{
AwsZone: new(string),
BashScriptLocation: new(string),
LogLevel: new(string),
OutputLocation: new(string),
FreeFormCmd: new(string),
AwsProfile: new(string),
},
}
return &Adapter{}
}

func (a *Adapter) GetFlags() cmd.Flags {
flag.StringVar(a.flags.AwsZone, "aws-zone", "eu-central-1", "aws zone where instances reside")
flag.StringVar(a.flags.BashScriptLocation, "script", "", "the location of the script to run")
flag.StringVar(&a.buffInstanceFlag, "instances", "", "instance IDs, separated by comma (,)")
flag.StringVar(a.flags.LogLevel, "log-level", "info", "log output level")
flag.StringVar(a.flags.OutputLocation, "output", "", "the location of file to write json output "+
"(default: output to console)")
flag.StringVar(a.flags.FreeFormCmd, "cmd", "", "freeform command, a single line bash command to be executed")
flag.StringVar(a.flags.AwsProfile, "aws-profile", "default", "aws credentials profile")
flag.StringVar(cmd.UserFlags.AwsZone.ValueString,
cmd.UserFlags.AwsZone.Name,
cmd.UserFlags.AwsZone.DefaultString,
cmd.UserFlags.AwsZone.Usage,
)
flag.StringVar(cmd.UserFlags.BashScriptLocation.ValueString,
cmd.UserFlags.BashScriptLocation.Name,
cmd.UserFlags.BashScriptLocation.DefaultString,
cmd.UserFlags.BashScriptLocation.Usage,
)
flag.StringVar(&a.buffInstanceFlag,
cmd.UserFlags.InstanceIDs.Name,
cmd.UserFlags.InstanceIDs.DefaultString,
cmd.UserFlags.InstanceIDs.Usage,
)
flag.StringVar(cmd.UserFlags.LogLevel.ValueString,
cmd.UserFlags.LogLevel.Name,
cmd.UserFlags.LogLevel.DefaultString,
cmd.UserFlags.LogLevel.Usage,
)
flag.StringVar(cmd.UserFlags.OutputLocation.ValueString,
cmd.UserFlags.OutputLocation.Name,
cmd.UserFlags.OutputLocation.DefaultString,
cmd.UserFlags.OutputLocation.Usage,
)
flag.StringVar(cmd.UserFlags.FreeFormCmd.ValueString,
cmd.UserFlags.FreeFormCmd.Name,
cmd.UserFlags.FreeFormCmd.DefaultString,
cmd.UserFlags.FreeFormCmd.Usage,
)
flag.StringVar(cmd.UserFlags.AwsProfile.ValueString,
cmd.UserFlags.AwsProfile.Name,
cmd.UserFlags.AwsProfile.DefaultString,
cmd.UserFlags.AwsProfile.Usage,
)
flag.StringVar(cmd.UserFlags.Mode.ValueString,
cmd.UserFlags.Mode.Name,
cmd.UserFlags.Mode.DefaultString,
cmd.UserFlags.Mode.Usage,
)
flag.StringVar(cmd.UserFlags.AnsiblePlaybook.ValueString,
cmd.UserFlags.AnsiblePlaybook.Name,
cmd.UserFlags.AnsiblePlaybook.DefaultString,
cmd.UserFlags.AnsiblePlaybook.Usage,
)
flag.BoolVar(cmd.UserFlags.AnsibleDryRun.ValueBool,
cmd.UserFlags.AnsibleDryRun.Name,
cmd.UserFlags.AnsibleDryRun.DefaultBool,
cmd.UserFlags.AnsibleDryRun.Usage,
)
flag.Parse()

a.checkFlags()

cmd.UserFlags.InstanceIDs.ValueStringArr = append(cmd.UserFlags.InstanceIDs.ValueStringArr,
strings.Split(a.buffInstanceFlag, ",")...)

return cmd.UserFlags
}

func (a *Adapter) WithLogger(logger hclog.Logger) ports.ICmd {
a.logger = logger.Named("cmd")

return a
}

func (a Adapter) isAllowedMode() bool {
for _, mode := range cmd.UserFlags.Mode.AllowedValuesStr {
if *cmd.UserFlags.Mode.ValueString == mode {
return true
}
}

return false
}

func (a *Adapter) checkFlags() {
// check if Instance ID is defined
if a.buffInstanceFlag == "" {
a.logger.Error("instance IDs not defined")
flag.PrintDefaults()

os.Exit(1)
}

a.flags.InstanceIDs = append(a.flags.InstanceIDs, strings.Split(a.buffInstanceFlag, ",")...)
// check if modes are allowed
if !a.isAllowedMode() {
a.logger.Error("only bash script and ansible playbook modes types are supported")
flag.PrintDefaults()

return *a.flags
}
os.Exit(1)
}

func (a *Adapter) WithLogger(logger hclog.Logger) ports.ICmd {
a.logger = logger.Named("cmd")
// check if ansible playbook is defined
if *cmd.UserFlags.Mode.ValueString == "ansible" &&
*cmd.UserFlags.AnsiblePlaybook.ValueString == "" {
a.logger.Error("running in Ansible mode but no Ansible Playbook file defined!")
flag.PrintDefaults()

return a
os.Exit(1)
}
}
11 changes: 11 additions & 0 deletions framework/adapters/left/localfs/localfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ func (a Adapter) ReadBashScript(bashScriptLocation string) string {
return string(fileBytes)
}

func (a Adapter) ReadAnsiblePlaybook(playbookLocation string) string {
// TODO: check if is yaml or yml file
fileBytes, err := os.ReadFile(playbookLocation)
if err != nil {
a.logger.Error("could not read file", "file", playbookLocation, "err", err.Error())
os.Exit(1)
}

return string(fileBytes)
}

func (a Adapter) WriteRunCommandOutput(cmdOutput ssm.Instances, outputLocation string) error {
jsonBuff, err := json.MarshalIndent(cmdOutput, "", " ")
if err != nil {
Expand Down
73 changes: 73 additions & 0 deletions framework/adapters/right/ssm/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package ssm

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
)

type commandType string

var generateCommand map[commandType]func() *ssm.SendCommandInput

// these should reflect flag options for -mode flag - bash by default
const (
bashScript commandType = "bash"
ansiblePlaybook commandType = "ansible"
)

// prepareCommand sets the function which initializes command based on the flag input
func (a *Adapter) prepareCommand() map[commandType]func() *ssm.SendCommandInput {
generateCommand = map[commandType]func() *ssm.SendCommandInput{
bashScript: a.runBashScript,
ansiblePlaybook: a.runAnsiblePlaybook,
}

return generateCommand
}

// runBashScript initializes AWS-RunShellScript document which will run a bash script on nodes
func (a *Adapter) runBashScript() *ssm.SendCommandInput {
// if we have instance IDs, else if we have tags
if a.instanceIDs != nil {
return &ssm.SendCommandInput{
DocumentName: aws.String("AWS-RunShellScript"),
DocumentVersion: aws.String("$LATEST"),
InstanceIds: a.instanceIDs,
Parameters: a.commands,
TimeoutSeconds: aws.Int64(300),
}
} else if a.targets != nil {
return &ssm.SendCommandInput{
DocumentName: aws.String("AWS-RunShellScript"),
DocumentVersion: aws.String("$LATEST"),
Targets: a.targets,
Parameters: a.commands,
TimeoutSeconds: aws.Int64(300),
}
}

return nil
}

func (a *Adapter) runAnsiblePlaybook() *ssm.SendCommandInput {
// if we have instance IDs, else if we have tags
if a.instanceIDs != nil {
return &ssm.SendCommandInput{
DocumentName: aws.String("AWS-RunAnsiblePlaybook"),
DocumentVersion: aws.String("$LATEST"),
InstanceIds: a.instanceIDs,
Parameters: a.commands,
TimeoutSeconds: aws.Int64(300),
}
} else if a.targets != nil {
return &ssm.SendCommandInput{
DocumentName: aws.String("AWS-RunAnsiblePlaybook"),
DocumentVersion: aws.String("$LATEST"),
Targets: a.targets,
Parameters: a.commands,
TimeoutSeconds: aws.Int64(300),
}
}

return nil
}
Loading

0 comments on commit c86cab2

Please sign in to comment.