hansolo 0.0.1.alpha.3 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -0
- data/README.md +79 -15
- data/bin/hansolo +2 -49
- data/bin/hansolo-databag +6 -0
- data/bin/hansolo-ssh +6 -0
- data/hansolo.gemspec +5 -0
- data/lib/hansolo/commands/base.rb +85 -0
- data/lib/hansolo/commands/data_bag.rb +82 -0
- data/lib/hansolo/commands/solo.rb +79 -0
- data/lib/hansolo/commands/ssh.rb +58 -0
- data/lib/hansolo/librarians/berkshelf.rb +15 -0
- data/lib/hansolo/librarians.rb +1 -0
- data/lib/hansolo/providers/aws/data_bags.rb +25 -0
- data/lib/hansolo/providers/aws/discovery.rb +58 -0
- data/lib/hansolo/providers/aws/solo.rb +27 -0
- data/lib/hansolo/providers/aws.rb +30 -0
- data/lib/hansolo/providers/default/data_bags.rb +29 -0
- data/lib/hansolo/providers/default/solo.rb +68 -0
- data/lib/hansolo/providers/default.rb +21 -0
- data/lib/hansolo/version.rb +1 -1
- data/lib/hansolo.rb +35 -199
- metadata +103 -5
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown --title hansolo --protected -M github-markup -M redcarpet lib/**/*.rb
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
NOTE: This is alpha code.
|
4
4
|
|
5
|
-
|
5
|
+
CLI tool to automate berkshelf and chef-solo deployment
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -18,25 +18,89 @@ Or install it yourself as:
|
|
18
18
|
|
19
19
|
$ gem install hansolo
|
20
20
|
|
21
|
-
##
|
21
|
+
## Usage
|
22
22
|
|
23
|
-
|
24
|
-
"urls": [ "vagrant@localhost:2222" ],
|
25
|
-
"runlist": [ "my_app::deploy" ],
|
26
|
-
"app":"my_app",
|
27
|
-
"keydir":"/Applications/Vagrant/embedded/gems/gems/vagrant-1.1.4/keys/vagrant",
|
28
|
-
"aws_access_key_id":"AAAAAAAAAAAAAAAAAAAA",
|
29
|
-
"aws_secret_access_key":"1111111111111111111111111111111111111111",
|
30
|
-
"aws_bucket_name":"acme-data_bags",
|
31
|
-
"aws_data_bag_keys":["my_app/stage/environment.json"]
|
32
|
-
}
|
23
|
+
`hansolo` provides three command line utilities for managing nodes with `chef-solo`.
|
33
24
|
|
34
|
-
|
25
|
+
* `hansolo`: runs `rsync` to copy cookbooks and data bags to the target nodes, generates a manifest and executes `chef-solo` against the generated manifest.
|
26
|
+
* `hansolo-databag`: Manages data bags.
|
27
|
+
* `hansolo-ssh`: SSHs into one of the target nodes.
|
28
|
+
|
29
|
+
To see what options can be supplied, run the command with `-h` or `--help`.
|
30
|
+
|
31
|
+
|
32
|
+
## `Hanfile` options
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
Hansolo.configure do |config|
|
36
|
+
# Path to SSH keys
|
37
|
+
config.keydir = '~/.ssh/chef'
|
38
|
+
|
39
|
+
# Gateway server if nodes are in a private network. Must be a valid ssh URI
|
40
|
+
# or URI instance.
|
41
|
+
config.gateway = 'ssh://user@gateway.example.com:20202'
|
42
|
+
|
43
|
+
# Name of the application
|
44
|
+
config.app = 'blog'
|
45
|
+
|
46
|
+
# Nodes to run `chef-solo` on. Can be a single or array of ssh URIs or URI
|
47
|
+
# instance.
|
48
|
+
config.target = 'ssh://user@blog.example.com'
|
49
|
+
|
50
|
+
# List of recipes to run.
|
51
|
+
config.runlist = ['recipe']
|
52
|
+
|
53
|
+
# Local path where cookbooks should be installed to using
|
54
|
+
# `Hansolo.librarian`. Defaults to `./tmp/data_bags`
|
55
|
+
config.cookbooks_path = '/tmp/chef/cookbooks'
|
56
|
+
|
57
|
+
# Local path where data bags will be written when using `hansolo-databag`.
|
58
|
+
# Defaults to `./tmp/cookbooks`
|
59
|
+
config.data_bags_path = '/tmp/chef/cookbooks'
|
60
|
+
|
61
|
+
# Command to run on the node after SSHing.
|
62
|
+
config.post_ssh_command = 'export RAILS_ENV=production; cd /srv/blog/current'
|
63
|
+
|
64
|
+
# Which chef cookbook manager to use. Currently, only `#berkshelf` is
|
65
|
+
# supported.
|
66
|
+
config.librarian = :berkshelf
|
67
|
+
|
68
|
+
# SSH options to use when running `rsync` or `hansolo-ssh`.
|
69
|
+
# Defaults to `-q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`.
|
70
|
+
config.ssh_options = '-vvv'
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
## Providers
|
75
|
+
|
76
|
+
`hansolo`'s behavior can be augmented by different providers by requiring them
|
77
|
+
in a `Hanfile`. Currently, AWS is the only provider provided.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
# Add AWS functionality to the toolset
|
81
|
+
require 'hansolo/providers/aws'
|
82
|
+
|
83
|
+
Hansolo.configure do |config|
|
84
|
+
# ...
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
### AWS Provider
|
89
|
+
|
90
|
+
The AWS provider augments `hansolo` to store data\_bags in S3 and adds the
|
91
|
+
ability to query EC2 for the IP address of the gateway and/or target nodes.
|
35
92
|
|
36
|
-
|
93
|
+
Data bags are stored in a bucket with the name `data_bags-:app`. The bucket is
|
94
|
+
created if it does not exist.
|
37
95
|
|
38
|
-
|
96
|
+
To have the IP address of the gateway queried, use the following URI scheme:
|
97
|
+
`<tag_name>://user@<value>:port`. The `<tag_name>` is the name of any tag on
|
98
|
+
the instance (e.g. `Name://user@bastion`).
|
39
99
|
|
100
|
+
To query instances, set `Hansolo.target` to a hash with the keys `:user`,
|
101
|
+
`:host`, and optionally `:port` (if not `22`). The `:host` key should be set to
|
102
|
+
the tag to query and the value should be the name of the application (e.g.
|
103
|
+
`role://user@api`).
|
40
104
|
|
41
105
|
## Contributing
|
42
106
|
|
data/bin/hansolo
CHANGED
@@ -1,53 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
$: << File.expand_path(File.join(__FILE__, '..', '..', 'lib'))
|
3
3
|
|
4
|
-
require 'hansolo'
|
5
|
-
require 'optparse'
|
4
|
+
require 'hansolo/commands/solo'
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
optparse = OptionParser.new do |opts|
|
10
|
-
opts.banner = Hansolo::Cli.banner
|
11
|
-
|
12
|
-
opts.on( '-h', '--help', 'Display this screen' ) do
|
13
|
-
puts opts
|
14
|
-
puts "\n"
|
15
|
-
puts Hansolo::Cli.help
|
16
|
-
exit
|
17
|
-
end
|
18
|
-
|
19
|
-
opts.on( '-c', '--config file', String, 'Path to config file') do |filename|
|
20
|
-
conf_options = JSON.parse(File.read(filename)) if filename != '' and File.exists?(filename)
|
21
|
-
end
|
22
|
-
|
23
|
-
opts.on( '-u', '--urls a,b,c', Array, "Comma-sep list of urls, e.g.: user@host:port/dest/path") do |o|
|
24
|
-
options[:urls] = o
|
25
|
-
end
|
26
|
-
|
27
|
-
opts.on( '-k', '--keydir s', String, "Your local ssh key directory") do |o|
|
28
|
-
options[:keydir] = o
|
29
|
-
end
|
30
|
-
|
31
|
-
opts.on( '-a', '--app s', String, "The application name") do |o|
|
32
|
-
options[:app] = o
|
33
|
-
end
|
34
|
-
|
35
|
-
opts.on( '-s', '--stage s', String, "The stage name") do |o|
|
36
|
-
options[:stage] = o
|
37
|
-
end
|
38
|
-
|
39
|
-
opts.on( '-r', '--runlist a,b,c', Array, "The runlist you want to effect on the target(s)") do |o|
|
40
|
-
options[:runlist] = o
|
41
|
-
end
|
42
|
-
end.parse!(ARGV)
|
43
|
-
|
44
|
-
unless conf_options.any?
|
45
|
-
default_conf_filename = File.expand_path(File.join(".",".hansolo.json"))
|
46
|
-
conf_options = JSON.parse(File.read(default_conf_filename)) if File.exists?(default_conf_filename)
|
47
|
-
end
|
48
|
-
|
49
|
-
|
50
|
-
opts = conf_options.merge(options).inject({}){|m,(k,v)| m[k.to_sym] = v; m}
|
51
|
-
|
52
|
-
h = Hansolo::Cli.new conf_options.merge(opts)
|
53
|
-
h.all!
|
6
|
+
Hansolo::Commands::Solo.run(ARGV)
|
data/bin/hansolo-databag
ADDED
data/bin/hansolo-ssh
ADDED
data/hansolo.gemspec
CHANGED
@@ -20,9 +20,14 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_dependency "aws-sdk"
|
22
22
|
spec.add_dependency "net-ssh"
|
23
|
+
spec.add_dependency "net-ssh-gateway"
|
23
24
|
spec.add_dependency "json"
|
25
|
+
spec.add_dependency "terminal-table"
|
26
|
+
spec.add_dependency "cocaine"
|
24
27
|
|
25
28
|
spec.add_development_dependency "bundler", "~> 1.3"
|
26
29
|
spec.add_development_dependency "rake"
|
27
30
|
spec.add_development_dependency "mocha"
|
31
|
+
spec.add_development_dependency "yard"
|
32
|
+
spec.add_development_dependency "redcarpet"
|
28
33
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'cocaine'
|
3
|
+
require 'net/ssh'
|
4
|
+
require 'net/ssh/gateway'
|
5
|
+
require 'hansolo'
|
6
|
+
require 'hansolo/providers/default'
|
7
|
+
|
8
|
+
module Hansolo
|
9
|
+
module Commands
|
10
|
+
# Responsible for taking in command line options and reading in `Hanfile`.
|
11
|
+
# Any unique command line options should be added in a subclass. Provides
|
12
|
+
# minimal helpers for executing commands.
|
13
|
+
class Base
|
14
|
+
include Providers::DefaultBehavior
|
15
|
+
|
16
|
+
# @!attribute [r] bastion
|
17
|
+
# @return [URI] attributes of the bastion server
|
18
|
+
attr_reader :bastion
|
19
|
+
|
20
|
+
# Run the command
|
21
|
+
# @see {#run}
|
22
|
+
def self.run(arguments)
|
23
|
+
new(arguments).run
|
24
|
+
end
|
25
|
+
|
26
|
+
# Sets up command
|
27
|
+
#
|
28
|
+
# * Loads the `Hanfile`
|
29
|
+
# * Parses command line arguments
|
30
|
+
# * Determines the {#bastion} if {Hansolo.gateway} is specified
|
31
|
+
def initialize(arguments)
|
32
|
+
load_hanfile!
|
33
|
+
|
34
|
+
setup_parser
|
35
|
+
parser.parse!(arguments)
|
36
|
+
|
37
|
+
determine_bastion if Hansolo.gateway
|
38
|
+
end
|
39
|
+
|
40
|
+
# Public interface to the command to be implemented in a subclass.
|
41
|
+
def run
|
42
|
+
raise NotImplementedError
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def exec(command)
|
48
|
+
Hansolo.logger.debug(command)
|
49
|
+
Kernel.exec(command)
|
50
|
+
end
|
51
|
+
|
52
|
+
def call(command)
|
53
|
+
Hansolo.logger.debug(command)
|
54
|
+
%x{#{command}}
|
55
|
+
end
|
56
|
+
|
57
|
+
def parser
|
58
|
+
@parser ||= OptionParser.new
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_hanfile!
|
62
|
+
load hanfile_path if File.exists?(hanfile_path)
|
63
|
+
end
|
64
|
+
|
65
|
+
def hanfile_path
|
66
|
+
@hanfile_path ||= File.expand_path('Hanfile')
|
67
|
+
end
|
68
|
+
|
69
|
+
def setup_parser
|
70
|
+
parser.on( '-h', '--help', 'display this screen' ) do
|
71
|
+
puts parser
|
72
|
+
exit
|
73
|
+
end
|
74
|
+
|
75
|
+
parser.on( '-t', '--target a,b,c', Array, "comma-sep list of urls, e.g.: user@host:port/dest/path") do |option|
|
76
|
+
Hansolo.target = option
|
77
|
+
end
|
78
|
+
|
79
|
+
parser.on( '-a', '--app s', String, "the application name") do |option|
|
80
|
+
Hansolo.app = option
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'terminal-table'
|
2
|
+
require 'hansolo/commands/base'
|
3
|
+
require 'hansolo/providers/default/data_bags'
|
4
|
+
|
5
|
+
module Hansolo
|
6
|
+
module Commands
|
7
|
+
class DataBag < Base
|
8
|
+
include Providers::DefaultBehavior::DataBags
|
9
|
+
|
10
|
+
attr_accessor :bag, :item, :changes
|
11
|
+
|
12
|
+
def run
|
13
|
+
changes.nil? ? print : write and print
|
14
|
+
end
|
15
|
+
|
16
|
+
def changes=(key_value_pairs)
|
17
|
+
@changes = key_value_pairs.inject({}) do |hash, pair|
|
18
|
+
key, value = pair.split('=', 2)
|
19
|
+
hash[key] = value
|
20
|
+
hash
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def read(content = item_content)
|
27
|
+
JSON.parse(content)
|
28
|
+
end
|
29
|
+
|
30
|
+
def all
|
31
|
+
data_bags.map { |key, content| [key, read(content)] }
|
32
|
+
end
|
33
|
+
|
34
|
+
def write
|
35
|
+
content = read.merge(changes).delete_if { |k, v| v.nil? || v.strip.empty? }
|
36
|
+
content['id'] ||= item
|
37
|
+
|
38
|
+
write_to_storage(content.to_json)
|
39
|
+
end
|
40
|
+
|
41
|
+
def print
|
42
|
+
if !bag.nil? && !item.nil?
|
43
|
+
rows = read
|
44
|
+
rows.delete('id')
|
45
|
+
|
46
|
+
terminal_table = Terminal::Table.new(rows: rows, headings: ['key', 'value'])
|
47
|
+
else
|
48
|
+
terminal_table = Terminal::Table.new do |table|
|
49
|
+
table.headings = ['key', 'value']
|
50
|
+
all.each_with_index do |(bag_and_item, content), i|
|
51
|
+
table.add_separator if i != 0
|
52
|
+
|
53
|
+
table.add_row [{ value: ' ', colspan: 2, alignment: :center, border_y: ' ' }]
|
54
|
+
table.add_row [{ value: "BAG/ITEM: #{bag_and_item}", colspan: 2, alignment: :center }]
|
55
|
+
|
56
|
+
table.add_separator
|
57
|
+
|
58
|
+
content.delete('id')
|
59
|
+
content.each do |k, v|
|
60
|
+
table.add_row [k, v]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
STDOUT.puts terminal_table
|
67
|
+
end
|
68
|
+
|
69
|
+
def setup_parser
|
70
|
+
super
|
71
|
+
|
72
|
+
parser.on('-b', '--data-bag-and-item BAG/ITEM', String, 'The data-bag and data-item, e.g. config/environment') do |option|
|
73
|
+
self.bag, self.item = option.split('/')
|
74
|
+
end
|
75
|
+
|
76
|
+
parser.on('--set CONFIG', Array, 'Set or unset (with an empty value) key-value pairs, e.g. foo=bar,key=value') do |option|
|
77
|
+
self.changes = option
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'hansolo/commands/base'
|
2
|
+
require 'hansolo/providers/default/solo'
|
3
|
+
|
4
|
+
module Hansolo
|
5
|
+
module Commands
|
6
|
+
class Solo < Base
|
7
|
+
include Providers::DefaultBehavior::Solo
|
8
|
+
|
9
|
+
# Puts cookbooks and data bags on the target nodes and runs `chef-solo`.
|
10
|
+
# Providers should implement the {#sync_data_bags} and {#sync_cookbooks}.
|
11
|
+
def run
|
12
|
+
sync_data_bags
|
13
|
+
|
14
|
+
Hansolo.librarian.install!
|
15
|
+
sync_cookbooks
|
16
|
+
|
17
|
+
execute_chef_solo
|
18
|
+
end
|
19
|
+
|
20
|
+
# SSH into each node to prepare and execute a `chef-solo` run.
|
21
|
+
def execute_chef_solo
|
22
|
+
threads = hosts.map do |host|
|
23
|
+
ssh = connect(host)
|
24
|
+
|
25
|
+
Thread.new do
|
26
|
+
ssh.exec! generate_manifest.command(manifest: manifest)
|
27
|
+
ssh.exec! generate_json.command(json: json)
|
28
|
+
|
29
|
+
ssh.exec! chef_solo do |channel, stream, line|
|
30
|
+
puts line
|
31
|
+
end
|
32
|
+
|
33
|
+
ssh.close
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
threads.map(&:join)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def chef_solo
|
43
|
+
'sudo chef-solo -c /tmp/solo.rb -j /tmp/deploy.json'
|
44
|
+
end
|
45
|
+
|
46
|
+
def connect(host)
|
47
|
+
if bastion.nil?
|
48
|
+
Net::SSH.new(host.host, host.user, port: host.port)
|
49
|
+
else
|
50
|
+
gateway.ssh(host.host, host.user, port: host.port)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate_manifest
|
55
|
+
Cocaine::CommandLine.new('echo', ':manifest > /tmp/solo.rb')
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate_json
|
59
|
+
Cocaine::CommandLine.new('echo', ':json > /tmp/deploy.json')
|
60
|
+
end
|
61
|
+
|
62
|
+
def manifest
|
63
|
+
<<-MANIFEST
|
64
|
+
file_cache_path '/tmp'
|
65
|
+
cookbook_path '/tmp/cookbooks'
|
66
|
+
data_bag_path '/tmp/data_bags'
|
67
|
+
MANIFEST
|
68
|
+
end
|
69
|
+
|
70
|
+
def json
|
71
|
+
{ :run_list => Hansolo.runlist }.to_json
|
72
|
+
end
|
73
|
+
|
74
|
+
def gateway
|
75
|
+
@gateway ||= Net::SSH::Gateway.new(bastion.host, bastion.user, port: bastion.port)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'hansolo/commands/base'
|
2
|
+
|
3
|
+
module Hansolo::Commands
|
4
|
+
class SSH < Base
|
5
|
+
def run
|
6
|
+
if bastion.nil?
|
7
|
+
exec(ssh.command(ssh_params))
|
8
|
+
else
|
9
|
+
exec(bastion_ssh.command(bastion_params))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def post_ssh_command
|
16
|
+
"#{Hansolo.post_ssh_command}; bash -i"
|
17
|
+
end
|
18
|
+
|
19
|
+
def ssh
|
20
|
+
Cocaine::CommandLine.new('ssh', ssh_params)
|
21
|
+
end
|
22
|
+
|
23
|
+
def ssh_options
|
24
|
+
options = ":user@:host #{Hansolo.ssh_options} -p :port"
|
25
|
+
options << ' -t :command' if Hansolo.post_ssh_command
|
26
|
+
options
|
27
|
+
end
|
28
|
+
|
29
|
+
def ssh_params
|
30
|
+
@ssh_params ||= begin
|
31
|
+
uri = hosts.sample
|
32
|
+
|
33
|
+
{
|
34
|
+
user: uri.user,
|
35
|
+
host: uri.host,
|
36
|
+
port: uri.port.to_s,
|
37
|
+
command: post_ssh_command
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def bastion_ssh
|
43
|
+
Cocaine::CommandLine.new('ssh', bastion_ssh_options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def bastion_ssh_options
|
47
|
+
"-A -l :bastion_user #{Hansolo.ssh_options} -p :bastion_port :bastion_host -t \"ssh #{ssh_options}\""
|
48
|
+
end
|
49
|
+
|
50
|
+
def bastion_params
|
51
|
+
@bastion_params ||= {
|
52
|
+
bastion_user: bastion.user,
|
53
|
+
bastion_port: bastion.port.to_s,
|
54
|
+
bastion_host: bastion.host
|
55
|
+
}.merge(ssh_params)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Hansolo::Librarians
|
2
|
+
module Berkshelf
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def install!
|
6
|
+
directory = Pathname.new("tmp/cookbooks/#{Hansolo.app}")
|
7
|
+
FileUtils.mkdir_p(directory)
|
8
|
+
|
9
|
+
files = Dir[directory.join('*')]
|
10
|
+
FileUtils.rm_rf(files)
|
11
|
+
|
12
|
+
system("berks install --path #{directory}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'hansolo/librarians/berkshelf'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Hansolo::Providers::AWS
|
2
|
+
module DataBags
|
3
|
+
def data_bags
|
4
|
+
objects = bucket.objects.with_prefix(Hansolo.app).to_a
|
5
|
+
objects.map do |o|
|
6
|
+
key = o.key.chomp('.json').sub("#{Hansolo.app}/", '')
|
7
|
+
[key, o.read]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def item_key
|
12
|
+
@item_key ||= "#{Hansolo.app}/#{bag}/#{item}.json"
|
13
|
+
end
|
14
|
+
|
15
|
+
def item_content
|
16
|
+
bucket.objects[item_key].read
|
17
|
+
rescue AWS::S3::Errors::NoSuchKey
|
18
|
+
"{}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def write_to_storage(content)
|
22
|
+
bucket.objects[item_key].write(content)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Hansolo
|
2
|
+
module Providers
|
3
|
+
module AWS
|
4
|
+
module Discovery
|
5
|
+
def ec2
|
6
|
+
@ec2 ||= ::AWS::EC2.new(Hansolo.aws_credentials)
|
7
|
+
end
|
8
|
+
|
9
|
+
def s3
|
10
|
+
@s3 ||= ::AWS::S3.new(Hansolo.aws_credentials)
|
11
|
+
end
|
12
|
+
|
13
|
+
def determine_bastion
|
14
|
+
@bastion = begin
|
15
|
+
uri = super
|
16
|
+
|
17
|
+
return uri if uri.scheme == 'ssh'
|
18
|
+
|
19
|
+
instance = instances_by_tag(uri.scheme.to_s, uri.host).first
|
20
|
+
raise ArgumentError, "no gateway with #{uri.scheme} #{uri.host} found" if instance.nil?
|
21
|
+
|
22
|
+
URI.parse("ssh://#{uri.user}@#{instance.public_ip_address}:#{uri.port || 22}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def hosts
|
27
|
+
@hosts ||= begin
|
28
|
+
target = Hansolo.target
|
29
|
+
return super unless target.is_a?(Hash)
|
30
|
+
|
31
|
+
target_instances = instances_by_tag(target[:host].to_s, Hansolo.app)
|
32
|
+
|
33
|
+
target_instances.map do |instance|
|
34
|
+
ip_address = instance.ip_address || instance.private_ip_address
|
35
|
+
URI.parse("ssh://#{target[:user]}@#{ip_address}:#{target[:port] || 22}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def bucket
|
43
|
+
@bucket ||= begin
|
44
|
+
name = Hansolo.bucket_name
|
45
|
+
|
46
|
+
bucket = s3.buckets[name]
|
47
|
+
bucket = s3.buckets.create(name) unless bucket.exists?
|
48
|
+
bucket
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def instances_by_tag(tag, value)
|
53
|
+
ec2.instances.tagged(tag).tagged_values(value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Hansolo::Providers::AWS
|
2
|
+
module Solo
|
3
|
+
def sync_data_bags
|
4
|
+
threads = hosts.map do |host|
|
5
|
+
Thread.new do
|
6
|
+
ssh = connect(host)
|
7
|
+
|
8
|
+
command = data_bag_items.inject([]) do |cmd, object|
|
9
|
+
path = Pathname.new('/tmp/data_bags').join(object.key)
|
10
|
+
|
11
|
+
cmd << "mkdir -p #{path.dirname}"
|
12
|
+
cmd << "echo '#{object.read}' > #{path}"
|
13
|
+
end
|
14
|
+
|
15
|
+
ssh.exec! command.join('; ')
|
16
|
+
ssh.close
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
threads.map(&:join)
|
21
|
+
end
|
22
|
+
|
23
|
+
def data_bag_items
|
24
|
+
bucket.objects.select { |o| o.key =~ /\.json$/ }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'hansolo'
|
3
|
+
require 'hansolo/providers/aws/data_bags'
|
4
|
+
require 'hansolo/providers/aws/discovery'
|
5
|
+
require 'hansolo/providers/aws/solo'
|
6
|
+
|
7
|
+
module Hansolo
|
8
|
+
class << self
|
9
|
+
attr_accessor :aws_access_key_id, :aws_secret_access_key, :bucket_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.aws_credentials
|
13
|
+
@aws_credentials ||= {
|
14
|
+
access_key_id: aws_access_key_id,
|
15
|
+
secret_access_key: aws_secret_access_key
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
class Commands::Base
|
20
|
+
include Providers::AWS::Discovery
|
21
|
+
end
|
22
|
+
|
23
|
+
class Commands::DataBag
|
24
|
+
include Providers::AWS::DataBags
|
25
|
+
end
|
26
|
+
|
27
|
+
class Commands::Solo
|
28
|
+
include Providers::AWS::Solo
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hansolo::Providers::DefaultBehavior
|
2
|
+
module DataBags
|
3
|
+
# Key-value pairs of the name of the data bag item to the item's content.
|
4
|
+
# @return [Hash]
|
5
|
+
def data_bags
|
6
|
+
@data_bags ||= Dir[Hansolo.data_bags_path.join('*', '**')].map { |path| [path.chomp('.json'), load_content(path)] }
|
7
|
+
end
|
8
|
+
|
9
|
+
# Path to the
|
10
|
+
def item_path
|
11
|
+
Hansolo.data_bags_path.join(bag, "#{item}.json")
|
12
|
+
end
|
13
|
+
|
14
|
+
def load_content(path)
|
15
|
+
File.read(path)
|
16
|
+
end
|
17
|
+
|
18
|
+
def item_content
|
19
|
+
load_content(item_path)
|
20
|
+
rescue
|
21
|
+
'{}'
|
22
|
+
end
|
23
|
+
|
24
|
+
def write_to_storage(content)
|
25
|
+
FileUtils.mkdir_p(item_path.dirname)
|
26
|
+
File.open(item_path, 'w') { |f| f.write content }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Hansolo::Providers::DefaultBehavior
|
2
|
+
module Solo
|
3
|
+
|
4
|
+
# `rsync` data bags to the node
|
5
|
+
def sync_data_bags
|
6
|
+
rsync_resource(:data_bags)
|
7
|
+
end
|
8
|
+
|
9
|
+
# `rsync` cookbooks to the node
|
10
|
+
def sync_cookbooks
|
11
|
+
rsync_resource(:cookbooks)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def rsync_resource(resource)
|
17
|
+
threads = hosts.map do |host|
|
18
|
+
Thread.new { call rsync.command(rsync_params(host, resource)) }
|
19
|
+
end
|
20
|
+
|
21
|
+
threads.map(&:join)
|
22
|
+
end
|
23
|
+
|
24
|
+
def rsync
|
25
|
+
Cocaine::CommandLine.new('rsync', rsync_options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def rsync_options
|
29
|
+
"--delete -av -e \"#{ssh_options}\" :source :destination"
|
30
|
+
end
|
31
|
+
|
32
|
+
def ssh_options
|
33
|
+
if !bastion.nil?
|
34
|
+
"ssh -A -l :bastion_user #{Hansolo.ssh_options} :bastion_host ssh -l :user #{Hansolo.ssh_options} -p :port"
|
35
|
+
else
|
36
|
+
"ssh -l :user #{Hansolo.ssh_options} -p :port"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def rsync_params(host, content)
|
41
|
+
params = {
|
42
|
+
user: host.user,
|
43
|
+
ssh_options: Hansolo.ssh_options,
|
44
|
+
port: host.port.to_s,
|
45
|
+
source: source(content),
|
46
|
+
destination: destination(host, content)
|
47
|
+
}
|
48
|
+
|
49
|
+
if !bastion.nil?
|
50
|
+
params.merge!(
|
51
|
+
bastion_user: bastion.user,
|
52
|
+
bastion_port: bastion.port.to_s,
|
53
|
+
bastion_host: bastion.host
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
params
|
58
|
+
end
|
59
|
+
|
60
|
+
def source(content)
|
61
|
+
"#{Hansolo.send("#{content}_path").join(Hansolo.app)}/"
|
62
|
+
end
|
63
|
+
|
64
|
+
def destination(host, content)
|
65
|
+
"#{host.user}@#{host.host}:/tmp/#{content}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Hansolo
|
2
|
+
module Providers
|
3
|
+
module DefaultBehavior
|
4
|
+
# Sets {Hansolo::Commands::Base#bastion}
|
5
|
+
# @return [URI, NilClass]
|
6
|
+
def determine_bastion
|
7
|
+
@bastion = case Hansolo.gateway
|
8
|
+
when String then URI.parse(Hansolo.gateway)
|
9
|
+
when URI then Hansolo.gateway
|
10
|
+
else raise ArgumentError, 'pass in a String or URI object'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Builds an array of `URI` instances representing target nodes
|
15
|
+
# @return [Array<URI>]
|
16
|
+
def hosts
|
17
|
+
@hosts ||= Array(Hansolo.target).map { |target| URI.parse(target) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/hansolo/version.rb
CHANGED
data/lib/hansolo.rb
CHANGED
@@ -1,210 +1,46 @@
|
|
1
|
-
require '
|
2
|
-
require
|
3
|
-
require '
|
1
|
+
require 'logger'
|
2
|
+
require "hansolo/version"
|
3
|
+
require 'hansolo/librarians'
|
4
4
|
|
5
5
|
module Hansolo
|
6
|
-
class
|
7
|
-
attr_accessor :keydir,
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@aws_secret_access_key = args[:aws_secret_access_key]
|
18
|
-
@aws_access_key_id = args[:aws_access_key_id]
|
19
|
-
|
20
|
-
if (@aws_secret_access_key && @aws_access_key_id && @aws_bucket_name && @aws_data_bag_keys)
|
21
|
-
@s3conn = AWS::S3.new(:access_key_id => args[:aws_access_key_id],
|
22
|
-
:secret_access_key => args[:aws_secret_access_key])
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.banner
|
27
|
-
"Usage: hansolo [OPTS]"
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.help
|
31
|
-
DATA.read
|
32
|
-
end
|
33
|
-
|
34
|
-
def tmpdir
|
35
|
-
'/tmp'
|
36
|
-
end
|
37
|
-
|
38
|
-
def all!
|
39
|
-
vendor_berkshelf!
|
40
|
-
rsync_cookbooks!
|
41
|
-
rsync_data_bags! if s3conn
|
42
|
-
solo!
|
43
|
-
end
|
44
|
-
|
45
|
-
def username(url)
|
46
|
-
@username ||= Util.parse_url(url)[:username]
|
47
|
-
end
|
48
|
-
|
49
|
-
def dest_cookbooks_dir(url)
|
50
|
-
File.join("/", "home", username(url), "cookbooks")
|
51
|
-
end
|
52
|
-
|
53
|
-
def dest_data_bags_dir(url)
|
54
|
-
File.join("/", "home", username(url), "data_bags")
|
55
|
-
end
|
56
|
-
|
57
|
-
def local_cookbooks_tmpdir
|
58
|
-
File.join(tmpdir, 'cookbooks.working')
|
59
|
-
end
|
60
|
-
|
61
|
-
def local_data_bags_tmpdir
|
62
|
-
File.join(tmpdir, 'data_bags.working')
|
63
|
-
end
|
64
|
-
|
65
|
-
def vendor_berkshelf!
|
66
|
-
Util.call_vendor_berkshelf(local_cookbooks_tmpdir)
|
67
|
-
end
|
68
|
-
|
69
|
-
def s3_bucket
|
70
|
-
s3_bucket = s3conn.buckets[aws_bucket_name]
|
71
|
-
if s3_bucket.exists?
|
72
|
-
s3_bucket
|
73
|
-
else
|
74
|
-
s3conn.buckets.create(aws_bucket_name)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
#def s3_key_name
|
79
|
-
#"#{app}/#{stage}/environment.json"
|
80
|
-
#end
|
81
|
-
|
82
|
-
#def s3_item
|
83
|
-
#s3_bucket.objects[s3_key_name]
|
84
|
-
#end
|
85
|
-
|
86
|
-
def rsync_cookbooks!
|
87
|
-
raise ArgumentError, "missing urls array and keydir" unless (urls && keydir)
|
88
|
-
urls.each do |url|
|
89
|
-
opts = Util.parse_url(url).merge(keydir: keydir, sourcedir: local_cookbooks_tmpdir, destdir: dest_cookbooks_dir(url))
|
90
|
-
Util.call_rsync(opts)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def rsync_data_bags!
|
95
|
-
# Grab JSON file from S3, and place it into a conventional place
|
96
|
-
Util.call("mkdir -p #{File.join(local_data_bags_tmpdir, 'app')}")
|
97
|
-
|
98
|
-
aws_data_bag_keys.each do |key_name|
|
99
|
-
item = s3_bucket.objects[key_name]
|
100
|
-
base_key_name = File.basename(key_name)
|
101
|
-
File.open(File.join(local_data_bags_tmpdir, 'app', base_key_name), 'w') do |f|
|
102
|
-
f.write item.read
|
103
|
-
end if item.exists?
|
104
|
-
end
|
105
|
-
|
106
|
-
urls.each do |url|
|
107
|
-
opts = Util.parse_url(url).merge(keydir: keydir, sourcedir: local_data_bags_tmpdir, destdir: dest_data_bags_dir(url))
|
108
|
-
Util.call_rsync(opts)
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def solo!
|
113
|
-
raise ArgumentError, "missing urls array and keydir" unless (urls && keydir)
|
114
|
-
urls.each { |url| Util.chef_solo(Util.parse_url(url).merge(keydir: keydir, cookbooks_dir: dest_cookbooks_dir(url), data_bags_dir: dest_data_bags_dir(url), runlist: runlist)) }
|
115
|
-
end
|
6
|
+
class << self
|
7
|
+
attr_accessor :keydir,
|
8
|
+
:gateway,
|
9
|
+
:app,
|
10
|
+
:target,
|
11
|
+
:runlist,
|
12
|
+
:cookbooks_path,
|
13
|
+
:data_bags_path,
|
14
|
+
:post_ssh_command,
|
15
|
+
:librarian,
|
16
|
+
:ssh_options
|
116
17
|
end
|
117
18
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
end
|
123
|
-
|
124
|
-
def self.call_vendor_berkshelf(tmpdir)
|
125
|
-
call("rm -rf #{tmpdir} && bundle exec berks install --path #{tmpdir}")
|
126
|
-
end
|
127
|
-
|
128
|
-
def self.call_rsync(args={})
|
129
|
-
cmd = "rsync -av -e 'ssh -l #{args[:username]} #{ssh_options(["-p #{args[:port]}", "-i #{args[:keydir]}"])}' "
|
130
|
-
cmd << "#{args[:sourcedir]}/ #{args[:username]}@#{args[:hostname]}:#{args[:destdir]}"
|
131
|
-
call cmd
|
132
|
-
end
|
133
|
-
|
134
|
-
def self.chef_solo(args={})
|
135
|
-
# on remote do:
|
136
|
-
# build a solo.rb
|
137
|
-
# build a tmp json file with the contents { "run_list": [ "recipe[my_app::default]" ] }
|
138
|
-
# chef-solo -c solo.rb -j tmp.json
|
19
|
+
LOGGER = Logger.new(STDOUT)
|
20
|
+
LOGGER.formatter = proc do |severity, datetime, progname, msg|
|
21
|
+
"* #{msg}\n"
|
22
|
+
end
|
139
23
|
|
140
|
-
|
141
|
-
|
142
|
-
puts ssh.exec! "echo '#{ { :run_list => args[:runlist] }.to_json }' > /tmp/deploy.json"
|
143
|
-
ssh.exec! 'PATH="$PATH:/opt/vagrant_ruby/bin" sudo chef-solo -l debug -c /tmp/solo.rb -j /tmp/deploy.json' do |ch, stream, line|
|
144
|
-
puts line
|
145
|
-
end
|
146
|
-
end
|
147
|
-
end
|
24
|
+
def self.configure
|
25
|
+
yield self
|
148
26
|
|
149
|
-
|
27
|
+
self.cookbooks_path ||= Pathname.new('tmp/cookbooks')
|
28
|
+
self.data_bags_path ||= Pathname.new('tmp/data_bags')
|
29
|
+
self.ssh_options ||= '-q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
|
30
|
+
end
|
150
31
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
"cookbook_path '#{cookbooks_dir}'",
|
155
|
-
"data_bag_path '#{data_bags_dir}'"
|
156
|
-
].join("\n")
|
157
|
-
end
|
32
|
+
def self.logger
|
33
|
+
LOGGER
|
34
|
+
end
|
158
35
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
end
|
36
|
+
def self.librarians
|
37
|
+
{
|
38
|
+
berkshelf: Librarians::Berkshelf
|
39
|
+
}
|
40
|
+
end
|
41
|
+
private_class_method :librarians
|
166
42
|
|
167
|
-
|
168
|
-
|
169
|
-
[
|
170
|
-
"-q",
|
171
|
-
"-o StrictHostKeyChecking=no",
|
172
|
-
"-o UserKnownHostsFile=/dev/null"
|
173
|
-
] + opts
|
174
|
-
).join(' ')
|
175
|
-
end
|
43
|
+
def self.librarian=(librarian)
|
44
|
+
@librarian = librarians[librarian]
|
176
45
|
end
|
177
46
|
end
|
178
|
-
|
179
|
-
require "hansolo/version"
|
180
|
-
|
181
|
-
__END__
|
182
|
-
This is a simple cli program to automate deploy using chef-solo and
|
183
|
-
berkshelf.
|
184
|
-
|
185
|
-
If you pass a filename, put in JSON for the configuration. So in .hansolo.json:
|
186
|
-
|
187
|
-
{ "keydir": "/Applications/Vagrant/embedded/gems/gems/vagrant-1.1.4/keys/vagrant" }
|
188
|
-
|
189
|
-
Then you can pass to the command as:
|
190
|
-
|
191
|
-
$ hansolo -c .hansolo.json
|
192
|
-
|
193
|
-
NOTE: Command-line args trump config settings.
|
194
|
-
|
195
|
-
Example Usage:
|
196
|
-
|
197
|
-
$ hansolo -s approval -t /tmp/myapp.cookbooks \
|
198
|
-
|
199
|
-
-k /Applications/Vagrant/embedded/gems/gems/vagrant-1.1.4/keys/vagrant \
|
200
|
-
|
201
|
-
-u user@host1:22/path,user@host2:22/path \
|
202
|
-
|
203
|
-
-r apt::default,myapp::deploy
|
204
|
-
|
205
|
-
$ hansolo -s approval -c .hansolo.json
|
206
|
-
|
207
|
-
$ hansolo -s approval
|
208
|
-
|
209
|
-
NOTE: You don't need to pass -c if you use the filename .hansolo.json. Passing -c
|
210
|
-
will override reading this default.
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hansolo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Brian Kaney
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-07-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: aws-sdk
|
@@ -43,6 +43,22 @@ dependencies:
|
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: net-ssh-gateway
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
46
62
|
- !ruby/object:Gem::Dependency
|
47
63
|
name: json
|
48
64
|
requirement: !ruby/object:Gem::Requirement
|
@@ -59,6 +75,38 @@ dependencies:
|
|
59
75
|
- - ! '>='
|
60
76
|
- !ruby/object:Gem::Version
|
61
77
|
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: terminal-table
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: cocaine
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
62
110
|
- !ruby/object:Gem::Dependency
|
63
111
|
name: bundler
|
64
112
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,22 +155,72 @@ dependencies:
|
|
107
155
|
- - ! '>='
|
108
156
|
- !ruby/object:Gem::Version
|
109
157
|
version: '0'
|
158
|
+
- !ruby/object:Gem::Dependency
|
159
|
+
name: yard
|
160
|
+
requirement: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ! '>='
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '0'
|
166
|
+
type: :development
|
167
|
+
prerelease: false
|
168
|
+
version_requirements: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ! '>='
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
- !ruby/object:Gem::Dependency
|
175
|
+
name: redcarpet
|
176
|
+
requirement: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
type: :development
|
183
|
+
prerelease: false
|
184
|
+
version_requirements: !ruby/object:Gem::Requirement
|
185
|
+
none: false
|
186
|
+
requirements:
|
187
|
+
- - ! '>='
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
110
190
|
description: Tool to automate deployment using chef-solo and berkshelf
|
111
191
|
email:
|
112
192
|
- brian@vermonster.com
|
113
193
|
executables:
|
114
194
|
- hansolo
|
195
|
+
- hansolo-databag
|
196
|
+
- hansolo-ssh
|
115
197
|
extensions: []
|
116
198
|
extra_rdoc_files: []
|
117
199
|
files:
|
118
200
|
- .gitignore
|
201
|
+
- .yardopts
|
119
202
|
- Gemfile
|
120
203
|
- LICENSE.txt
|
121
204
|
- README.md
|
122
205
|
- Rakefile
|
123
206
|
- bin/hansolo
|
207
|
+
- bin/hansolo-databag
|
208
|
+
- bin/hansolo-ssh
|
124
209
|
- hansolo.gemspec
|
125
210
|
- lib/hansolo.rb
|
211
|
+
- lib/hansolo/commands/base.rb
|
212
|
+
- lib/hansolo/commands/data_bag.rb
|
213
|
+
- lib/hansolo/commands/solo.rb
|
214
|
+
- lib/hansolo/commands/ssh.rb
|
215
|
+
- lib/hansolo/librarians.rb
|
216
|
+
- lib/hansolo/librarians/berkshelf.rb
|
217
|
+
- lib/hansolo/providers/aws.rb
|
218
|
+
- lib/hansolo/providers/aws/data_bags.rb
|
219
|
+
- lib/hansolo/providers/aws/discovery.rb
|
220
|
+
- lib/hansolo/providers/aws/solo.rb
|
221
|
+
- lib/hansolo/providers/default.rb
|
222
|
+
- lib/hansolo/providers/default/data_bags.rb
|
223
|
+
- lib/hansolo/providers/default/solo.rb
|
126
224
|
- lib/hansolo/version.rb
|
127
225
|
- tests/hansolo_test.rb
|
128
226
|
homepage: ''
|
@@ -141,9 +239,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
141
239
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
240
|
none: false
|
143
241
|
requirements:
|
144
|
-
- - ! '
|
242
|
+
- - ! '>='
|
145
243
|
- !ruby/object:Gem::Version
|
146
|
-
version:
|
244
|
+
version: '0'
|
147
245
|
requirements: []
|
148
246
|
rubyforge_project:
|
149
247
|
rubygems_version: 1.8.23
|