hocho 0.1.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +68 -0
- data/Rakefile +6 -0
- data/bin/hocho +4 -0
- data/hocho.gemspec +29 -0
- data/lib/hocho/command.rb +98 -0
- data/lib/hocho/config.rb +56 -0
- data/lib/hocho/drivers/base.rb +42 -0
- data/lib/hocho/drivers/bundler.rb +100 -0
- data/lib/hocho/drivers/itamae_ssh.rb +32 -0
- data/lib/hocho/drivers/ssh_base.rb +105 -0
- data/lib/hocho/drivers.rb +9 -0
- data/lib/hocho/host.rb +156 -0
- data/lib/hocho/inventory.rb +29 -0
- data/lib/hocho/inventory_providers/base.rb +9 -0
- data/lib/hocho/inventory_providers/file.rb +40 -0
- data/lib/hocho/inventory_providers.rb +9 -0
- data/lib/hocho/property_providers/base.rb +9 -0
- data/lib/hocho/property_providers.rb +9 -0
- data/lib/hocho/runner.rb +57 -0
- data/lib/hocho/utils/finder.rb +26 -0
- data/lib/hocho/utils/symbolize.rb +16 -0
- data/lib/hocho/version.rb +3 -0
- data/lib/hocho.rb +5 -0
- data/script/console +14 -0
- data/script/setup +7 -0
- metadata +173 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6f40a1aecd4cf8840d456cf93fdd5523910c0187
|
4
|
+
data.tar.gz: ee1d577aaae256bffa0b2f94ca66539b4db941eb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ba38f1f5837dd7095c8f78bc06d2adcd50f7a281ca96008704816bd0d9a5e03579ffca9c0ed70cf02fd102097e8f61e2a31829d9318728922969fd0481b71c8a
|
7
|
+
data.tar.gz: c5dd335f31ee64c2a51ddf7bfbabd341436efa8e43b63db30c6a67563904991692ecd4db9552d88f17f1240b4a8bc3b64a2747d6043ad1508e12d2d93bc32fbf
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Shota Fukumori (sora_h)
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# Hocho: an itamae wrapper
|
2
|
+
|
3
|
+
Hocho is a wrapper of the provisioning tool [itamae](https://github.com/itamae-kitchen/itamae).
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- `itamae ssh` support
|
8
|
+
- remote `itamae local` support on rsync+bundler
|
9
|
+
- Simple pluggable host inventory, discovery
|
10
|
+
|
11
|
+
## vs. other softwares
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'hocho'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install hocho
|
28
|
+
|
29
|
+
## Setup
|
30
|
+
|
31
|
+
``` yaml
|
32
|
+
# hocho.yml
|
33
|
+
inventory_providers:
|
34
|
+
file:
|
35
|
+
path: './hosts'
|
36
|
+
```
|
37
|
+
|
38
|
+
```
|
39
|
+
# ./hosts/test.yml
|
40
|
+
test.example.org:
|
41
|
+
# ssh_options:
|
42
|
+
# user: ...
|
43
|
+
properties:
|
44
|
+
run_list:
|
45
|
+
- roles/app/default.rb
|
46
|
+
```
|
47
|
+
|
48
|
+
```
|
49
|
+
$ hocho list
|
50
|
+
$ hocho show test.example.org
|
51
|
+
$ hocho apply test.example.org
|
52
|
+
```
|
53
|
+
|
54
|
+
## Development
|
55
|
+
|
56
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
57
|
+
|
58
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
59
|
+
|
60
|
+
## Contributing
|
61
|
+
|
62
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hocho.
|
63
|
+
|
64
|
+
|
65
|
+
## License
|
66
|
+
|
67
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
68
|
+
|
data/Rakefile
ADDED
data/bin/hocho
ADDED
data/hocho.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hocho/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hocho"
|
8
|
+
spec.version = Hocho::VERSION
|
9
|
+
spec.authors = ["sorah (Shota Fukumori)"]
|
10
|
+
spec.email = ["her@sorah.jp"]
|
11
|
+
|
12
|
+
spec.summary = %q{Server provisioning tool with itamae}
|
13
|
+
spec.homepage = "https://github.com/sorah/hocho"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "bin"
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "thor"
|
22
|
+
spec.add_dependency "itamae"
|
23
|
+
spec.add_dependency "net-ssh"
|
24
|
+
spec.add_dependency "hashie"
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
27
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
28
|
+
spec.add_development_dependency "rspec"
|
29
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'yaml'
|
3
|
+
require 'json'
|
4
|
+
require 'io/console'
|
5
|
+
require 'hocho/config'
|
6
|
+
require 'hocho/inventory'
|
7
|
+
require 'hocho/runner'
|
8
|
+
|
9
|
+
module Hocho
|
10
|
+
class Command < Thor
|
11
|
+
class_option :config, type: :string, desc: 'path to config file (default: ENV["HOCHO_CONFIG"] or ./hocho.yml)'
|
12
|
+
|
13
|
+
desc "list", ""
|
14
|
+
method_option :verbose, type: :boolean, default: false, alias: %w(-v)
|
15
|
+
method_option :format, enum: %w(yaml json), default: 'yaml'
|
16
|
+
def list
|
17
|
+
hosts = inventory.hosts
|
18
|
+
|
19
|
+
if options[:verbose]
|
20
|
+
hosts.each do |host|
|
21
|
+
config.property_providers.each { |_| _.determine(host) }
|
22
|
+
end
|
23
|
+
case options[:format]
|
24
|
+
when 'yaml'
|
25
|
+
puts hosts.map(&:to_h).to_yaml
|
26
|
+
when 'json'
|
27
|
+
puts hosts.map(&:to_h).to_json
|
28
|
+
end
|
29
|
+
else
|
30
|
+
case options[:format]
|
31
|
+
when 'yaml'
|
32
|
+
puts hosts.map(&:name).to_yaml
|
33
|
+
when 'json'
|
34
|
+
puts hosts.map(&:name).to_json
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "show NAME", ""
|
40
|
+
method_option :format, enum: %w(yaml json), default: 'yaml'
|
41
|
+
def show(name)
|
42
|
+
host = inventory.filter(name: name).first
|
43
|
+
if host
|
44
|
+
case options[:format]
|
45
|
+
when 'yaml'
|
46
|
+
puts host.to_h.to_yaml
|
47
|
+
when 'json'
|
48
|
+
puts host.to_h.to_json
|
49
|
+
end
|
50
|
+
else
|
51
|
+
raise "host name=#{name.inspect} not found"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
desc "apply HOST", "run itamae"
|
56
|
+
method_option :sudo, type: :boolean, default: false
|
57
|
+
method_option :dry_run, type: :boolean, default: false, alias: %w(-n)
|
58
|
+
method_option :driver, type: :string
|
59
|
+
def apply(name)
|
60
|
+
host = inventory.filter(name: name).first
|
61
|
+
unless host
|
62
|
+
raise "host name=#{name.inspect} not found"
|
63
|
+
end
|
64
|
+
|
65
|
+
if config[:ask_sudo_password] || options[:sudo]
|
66
|
+
print "sudo password: "
|
67
|
+
host.sudo_password = $stdin.noecho { $stdin.gets.chomp }
|
68
|
+
puts
|
69
|
+
end
|
70
|
+
|
71
|
+
Runner.new(
|
72
|
+
host,
|
73
|
+
driver: options[:driver],
|
74
|
+
base_dir: config[:itamae_dir] || '.',
|
75
|
+
initializers: config[:initializers] || [],
|
76
|
+
driver_options: config[:driver_options] || {},
|
77
|
+
).run(
|
78
|
+
dry_run: options[:dry_run],
|
79
|
+
)
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def inventory
|
85
|
+
@inventory ||= Hocho::Inventory.new(config.inventory_providers)
|
86
|
+
end
|
87
|
+
|
88
|
+
def config
|
89
|
+
@config ||= Hocho::Config.load(config_file).tap do |c|
|
90
|
+
Dir.chdir c.base_dir # XXX:
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def config_file
|
95
|
+
options[:config] || ENV['HOCHO_CONFIG'] || './hocho.yml'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/hocho/config.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'hocho/utils/symbolize'
|
2
|
+
require 'hocho/property_providers'
|
3
|
+
require 'hocho/inventory_providers'
|
4
|
+
require 'pathname'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module Hocho
|
8
|
+
class Config
|
9
|
+
DEFAULT_INVENTORY_PROVIDERS_CONFIG = [file: {path: './hosts.yml'}]
|
10
|
+
|
11
|
+
def self.load(path)
|
12
|
+
new YAML.load_file(path.to_s), base_dir: File.dirname(path.to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(hash, base_dir: '.')
|
16
|
+
@config = Hocho::Utils::Symbolize.keys_of(hash)
|
17
|
+
@base_dir = Pathname(base_dir)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :base_dir
|
21
|
+
|
22
|
+
def [](k)
|
23
|
+
@config[k]
|
24
|
+
end
|
25
|
+
|
26
|
+
def inventory_providers
|
27
|
+
@inventory_providers ||= begin
|
28
|
+
provider_specs = (@config[:inventory_providers] || DEFAULT_INVENTORY_PROVIDERS_CONFIG)
|
29
|
+
if provider_specs.kind_of?(Hash)
|
30
|
+
provider_specs = [provider_specs]
|
31
|
+
end
|
32
|
+
|
33
|
+
provider_specs.flat_map do |spec|
|
34
|
+
raise TypeError, 'config inventory_providers[] should be an Hash' unless spec.kind_of?(Hash)
|
35
|
+
spec.map do |name, options|
|
36
|
+
InventoryProviders.find(name).new(**options)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def property_providers
|
43
|
+
@property_providers ||= begin
|
44
|
+
provider_specs = (@config[:property_providers] || [])
|
45
|
+
raise TypeError, 'config property_providers should be an Array' unless provider_specs.kind_of?(Array)
|
46
|
+
provider_specs.flat_map do |spec|
|
47
|
+
raise TypeError, 'config property_providers[] should be an Hash' unless spec.kind_of?(Hash)
|
48
|
+
spec.map do |name, options|
|
49
|
+
PropertyProviders.find(name).new(**options)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Hocho
|
6
|
+
module Drivers
|
7
|
+
class Base
|
8
|
+
def initialize(host, base_dir: '.', initializers: [])
|
9
|
+
@host = host
|
10
|
+
@base_dir = base_dir
|
11
|
+
@initializers = initializers
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :host, :base_dir, :initializers
|
15
|
+
|
16
|
+
def run(dry_run: false)
|
17
|
+
raise NotImplementedError
|
18
|
+
end
|
19
|
+
|
20
|
+
def run_list
|
21
|
+
[*initializers, *host.run_list]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def node_json
|
27
|
+
host.attributes.to_json
|
28
|
+
end
|
29
|
+
|
30
|
+
def with_node_json_file
|
31
|
+
begin
|
32
|
+
f = Tempfile.new('node-json')
|
33
|
+
f.puts node_json
|
34
|
+
f.flush
|
35
|
+
yield f.path
|
36
|
+
ensure
|
37
|
+
f.close!
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'hocho/drivers/ssh_base'
|
2
|
+
|
3
|
+
module Hocho
|
4
|
+
module Drivers
|
5
|
+
class Bundler < SshBase
|
6
|
+
def initialize(host, base_dir: '.', initializers: [], itamae_options: [], bundle_without: [], bundle_path: nil, deploy_dir: nil, keep_synced_files: nil)
|
7
|
+
super host, base_dir: base_dir, initializers: initializers
|
8
|
+
|
9
|
+
@itamae_options = itamae_options
|
10
|
+
@bundle_without = bundle_without
|
11
|
+
@bundle_path = bundle_path
|
12
|
+
@deploy_dir = deploy_dir
|
13
|
+
@keep_synced_files = keep_synced_files
|
14
|
+
end
|
15
|
+
|
16
|
+
def keep_synced_files?
|
17
|
+
@keep_synced_files
|
18
|
+
end
|
19
|
+
|
20
|
+
def run(dry_run: false)
|
21
|
+
deploy
|
22
|
+
bundle_install
|
23
|
+
run_itamae(dry_run: dry_run)
|
24
|
+
ensure
|
25
|
+
cleanup
|
26
|
+
end
|
27
|
+
|
28
|
+
def deploy
|
29
|
+
ssh_cmd = ['ssh', *host.openssh_config.flat_map { |l| ['-o', "\"#{l}\""] }].join(' ')
|
30
|
+
rsync_cmd = [*%w(rsync -az --copy-links --copy-unsafe-links --delete --exclude=.git), '--rsh', ssh_cmd, '.', "#{host.hostname}:#{host_basedir}"]
|
31
|
+
|
32
|
+
puts "=> $ #{rsync_cmd.inspect}"
|
33
|
+
system(*rsync_cmd, chdir: base_dir) or raise 'failed to rsync'
|
34
|
+
end
|
35
|
+
|
36
|
+
def bundle_install
|
37
|
+
bundle_path_env = @bundle_path ? "BUNDLE_PATH=#{@bundle_path.shellescape} " : nil
|
38
|
+
check_exitstatus, check_exitsignal = ssh_run("cd #{host_basedir.shellescape} && #{bundle_path_env}bundle check", error: false)
|
39
|
+
return if check_exitstatus == 0
|
40
|
+
|
41
|
+
prepare_sudo do |sh, sudovars, sudocmd|
|
42
|
+
bundle_install = [host.bundler_cmd, 'install']
|
43
|
+
bundle_install.push('--path', @bundle_path) if @bundle_path
|
44
|
+
bundle_install.push('--without', [*@bundle_without].join(?:)) if @bundle_without
|
45
|
+
|
46
|
+
puts "=> #{host.name} # #{bundle_install.shelljoin}"
|
47
|
+
|
48
|
+
ssh_run("bash") do |c|
|
49
|
+
c.on_data do |c, data|
|
50
|
+
puts "[#{host.name}] #{data}"
|
51
|
+
end
|
52
|
+
c.on_extended_data do |c, _, data|
|
53
|
+
puts "[#{host.name}/ERR] #{data}"
|
54
|
+
end
|
55
|
+
|
56
|
+
c.send_data("cd #{host_basedir.shellescape}\n#{sudovars}\n#{sudocmd}#{bundle_install.shelljoin}\n")
|
57
|
+
c.eof!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def run_itamae(dry_run: false)
|
63
|
+
with_host_node_json_file do
|
64
|
+
itamae_cmd = ['itamae', 'local', '-j', host_node_json_path, *@itamae_options]
|
65
|
+
itamae_cmd.push('--dry-run') if dry_run
|
66
|
+
itamae_cmd.push('--color') if $stdout.tty?
|
67
|
+
itamae_cmd.push(*run_list)
|
68
|
+
|
69
|
+
prepare_sudo do |sh, sudovars, sudocmd|
|
70
|
+
puts "=> #{host.name} # #{host.bundler_cmd} exec #{itamae_cmd.shelljoin}"
|
71
|
+
ssh_run("bash") do |c|
|
72
|
+
c.on_data do |c, data|
|
73
|
+
puts "[#{host.name}] #{data}"
|
74
|
+
end
|
75
|
+
c.on_extended_data do |c, _, data|
|
76
|
+
puts "[#{host.name}/ERR] #{data}"
|
77
|
+
end
|
78
|
+
|
79
|
+
c.send_data("cd #{host_basedir.shellescape}\n#{sudovars}\n#{sudocmd}#{host.bundler_cmd} exec #{itamae_cmd.shelljoin}\n")
|
80
|
+
c.eof!
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def cleanup
|
87
|
+
return unless !keep_synced_files? || !deploy_dir
|
88
|
+
|
89
|
+
cmd = "rm -rf #{host_basedir.shellescape}"
|
90
|
+
puts "=> #{host.name} $ #{cmd}"
|
91
|
+
ssh_run(cmd, error: false)
|
92
|
+
end
|
93
|
+
|
94
|
+
def host_basedir
|
95
|
+
@deploy_dir || "#{host_tmpdir}/itamae"
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'hocho/drivers/base'
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
module Hocho
|
5
|
+
module Drivers
|
6
|
+
class ItamaeSsh < Base
|
7
|
+
def initialize(host, base_dir: '.', initializers: [], itamae_options: [])
|
8
|
+
super host, base_dir: base_dir, initializers: initializers
|
9
|
+
@itamae_options = itamae_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def run(dry_run: false)
|
13
|
+
with_node_json_file do |node_json|
|
14
|
+
env = {}.tap do |e|
|
15
|
+
e['SUDO_PASSWORD'] = host.sudo_password if host.sudo_password
|
16
|
+
end
|
17
|
+
cmd = ["itamae", "ssh", *@itamae_options, "-j", node_json, "-h", host.hostname]
|
18
|
+
|
19
|
+
cmd.push('-u', host.user) if host.user
|
20
|
+
cmd.push('-p', host.ssh_port.to_s) if host.ssh_port
|
21
|
+
cmd.push('--dry-run') if dry_run
|
22
|
+
cmd.push('--color') if $stdout.tty?
|
23
|
+
|
24
|
+
cmd.push(*run_list)
|
25
|
+
|
26
|
+
puts "=> $ #{cmd.shelljoin}"
|
27
|
+
system(env, *cmd, chdir: base_dir) or raise "itamae ssh failed"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'hocho/drivers/base'
|
2
|
+
require 'securerandom'
|
3
|
+
require 'shellwords'
|
4
|
+
|
5
|
+
module Hocho
|
6
|
+
module Drivers
|
7
|
+
class SshBase < Base
|
8
|
+
def ssh
|
9
|
+
host.ssh_connection
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def prepare_sudo(password = host.sudo_password)
|
15
|
+
raise "sudo password not present" if !host.nopasswd_sudo? && password.nil?
|
16
|
+
|
17
|
+
if host.nopasswd_sudo?
|
18
|
+
yield nil, nil, nil
|
19
|
+
return
|
20
|
+
end
|
21
|
+
|
22
|
+
passphrase_env_name = "HOCHO_PA_#{SecureRandom.hex(8).upcase}"
|
23
|
+
# password_env_name = "HOCHO_PB_#{SecureRandom.hex(8).upcase}"
|
24
|
+
|
25
|
+
temporary_passphrase = SecureRandom.base64(129).chomp
|
26
|
+
|
27
|
+
encrypted_password = IO.pipe do |r,w|
|
28
|
+
w.write temporary_passphrase
|
29
|
+
w.close
|
30
|
+
IO.popen([*%w(openssl enc -aes-128-cbc -pass fd:5 -a), 5 => r], "r+") do |io|
|
31
|
+
io.puts password
|
32
|
+
io.close_write
|
33
|
+
io.read.chomp
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
begin
|
38
|
+
temp_executable = ssh.exec!('mktemp').chomp
|
39
|
+
raise unless temp_executable.start_with?('/')
|
40
|
+
|
41
|
+
ssh_run("chmod 0700 #{temp_executable.shellescape}; cat > #{temp_executable.shellescape}; chmod +x #{temp_executable.shellescape}") do |ch|
|
42
|
+
ch.send_data("#!/bin/bash\nexec openssl enc -aes-128-cbc -d -a -pass env:#{passphrase_env_name} <<< #{encrypted_password.shellescape}\n")
|
43
|
+
ch.eof!
|
44
|
+
end
|
45
|
+
|
46
|
+
sh = "#{passphrase_env_name}=#{temporary_passphrase.shellescape} SUDO_ASKPASS=#{temp_executable.shellescape} sudo -A "
|
47
|
+
exp = "export #{passphrase_env_name}=#{temporary_passphrase.shellescape}\nexport SUDO_ASKPASS=#{temp_executable.shellescape}\n"
|
48
|
+
cmd = "sudo -A "
|
49
|
+
yield sh, exp, cmd
|
50
|
+
|
51
|
+
ensure
|
52
|
+
ssh_run("shred --remove #{temp_executable.shellescape}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def host_tmpdir
|
58
|
+
@host_tmpdir ||= begin
|
59
|
+
mktemp_cmd = %w(mktemp -d -t hocho-run-XXXXXXXXX).shelljoin
|
60
|
+
mktemp_cmd.prepend("TMPDIR=#{host.tmpdir.shellescape} ") if host.tmpdir
|
61
|
+
|
62
|
+
res = ssh.exec!(mktemp_cmd)
|
63
|
+
unless res.start_with?('/tmp')
|
64
|
+
raise "Failed to mktemp #{mktemp_cmd.inspect} -> #{res.inspect}"
|
65
|
+
end
|
66
|
+
res.chomp
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def host_node_json_path
|
71
|
+
"#{host_tmpdir}/node.json"
|
72
|
+
end
|
73
|
+
|
74
|
+
def with_host_node_json_file
|
75
|
+
ssh_run("umask 0077 && cat > #{host_node_json_path.shellescape}") do |c|
|
76
|
+
c.send_data "#{node_json}\n"
|
77
|
+
c.eof!
|
78
|
+
end
|
79
|
+
|
80
|
+
yield host_node_json_path
|
81
|
+
ensure
|
82
|
+
ssh.exec!("rm #{host_node_json_path.shellescape}")
|
83
|
+
end
|
84
|
+
|
85
|
+
def ssh_run(cmd, error: true)
|
86
|
+
exitstatus, exitsignal = nil
|
87
|
+
|
88
|
+
puts "$ #{cmd}"
|
89
|
+
cha = ssh.open_channel do |ch|
|
90
|
+
ch.exec(cmd) do |c, success|
|
91
|
+
raise "execution failed on #{host.name}: #{cmd.inspect}" if !success && error
|
92
|
+
|
93
|
+
c.on_request("exit-status") { |c, data| exitstatus = data.read_long }
|
94
|
+
c.on_request("exit-signal") { |c, data| exitsignal = data.read_long }
|
95
|
+
|
96
|
+
yield c if block_given?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
cha.wait
|
100
|
+
raise "execution failed on #{host.name} (status=#{exitstatus.inspect}, signal=#{exitsignal.inspect}): #{cmd.inspect}" if (exitstatus != 0 || exitsignal) && error
|
101
|
+
[exitstatus, exitsignal]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/hocho/host.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'hocho/utils/symbolize'
|
2
|
+
require 'hashie'
|
3
|
+
require 'net/ssh'
|
4
|
+
require 'net/ssh/proxy/command'
|
5
|
+
|
6
|
+
module Hocho
|
7
|
+
class Host
|
8
|
+
def initialize(name, provider: nil, properties: {}, tags: {}, ssh_options: nil, tmpdir: nil, sudo_password: nil)
|
9
|
+
@name = name
|
10
|
+
@provider = provider
|
11
|
+
self.properties = properties
|
12
|
+
@tags = tags
|
13
|
+
@override_ssh_options = ssh_options
|
14
|
+
@tmpdir = tmpdir
|
15
|
+
@sudo_password = sudo_password
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :name, :provider, :properties, :tmpdir
|
19
|
+
attr_writer :sudo_password
|
20
|
+
attr_accessor :tags
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
name: name,
|
25
|
+
provider: provider,
|
26
|
+
tags: tags.to_h,
|
27
|
+
properties: properties.to_h,
|
28
|
+
}.tap do |h|
|
29
|
+
h[:tmpdir] = tmpdir if tmpdir
|
30
|
+
h[:ssh_options] = @override_ssh_options if @override_ssh_options
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def properties=(other)
|
35
|
+
@properties = Hashie::Mash.new(other)
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_properties_from_providers(providers)
|
39
|
+
providers.each do |provider|
|
40
|
+
provider.determine(self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_list
|
45
|
+
properties[:run_list] || []
|
46
|
+
end
|
47
|
+
|
48
|
+
def attributes
|
49
|
+
properties[:attributes] || {}
|
50
|
+
end
|
51
|
+
|
52
|
+
def sudo_password
|
53
|
+
@sudo_password || properties[:sudo_password] || ENV['SUDO_PASSWORD']
|
54
|
+
end
|
55
|
+
|
56
|
+
def nopasswd_sudo?
|
57
|
+
!!properties[:nopasswd_sudo]
|
58
|
+
end
|
59
|
+
|
60
|
+
def ssh_options
|
61
|
+
(Net::SSH::Config.for(name) || {}).merge(Hocho::Utils::Symbolize.keys_of(properties[:ssh_options] || {})).merge(@override_ssh_options || {})
|
62
|
+
end
|
63
|
+
|
64
|
+
def openssh_config
|
65
|
+
ssh_options.flat_map do |key, value|
|
66
|
+
case key
|
67
|
+
when :encryption
|
68
|
+
"Ciphers #{[*value].join(?,)}"
|
69
|
+
when :compression
|
70
|
+
"Compression #{value}"
|
71
|
+
when :compression_level
|
72
|
+
"CompressionLevel #{value}"
|
73
|
+
when :timeout
|
74
|
+
"ConnectTimeout #{value}"
|
75
|
+
when :forward_agent
|
76
|
+
"ForwardAgent #{value ? 'yes' : 'no'}"
|
77
|
+
when :keys_only
|
78
|
+
"IdentitiesOnly #{value ? 'yes' : 'no'}"
|
79
|
+
when :global_known_hosts_file
|
80
|
+
"GlobalKnownHostsFile #{value}"
|
81
|
+
when :auth_methods
|
82
|
+
[].tap do |lines|
|
83
|
+
methods = value.dup
|
84
|
+
value.each do |val|
|
85
|
+
case val
|
86
|
+
when 'hostbased'
|
87
|
+
lines << "HostBasedAuthentication yes"
|
88
|
+
when 'password'
|
89
|
+
lines << "PasswordAuthentication yes"
|
90
|
+
when 'publickey'
|
91
|
+
lines << "PubkeyAuthentication yes"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
unless methods.empty?
|
95
|
+
lines << "PreferredAuthentications #{methods.join(?,)}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
when :host_key
|
99
|
+
"HostKeyAlgorithms #{[*value].join(?,)}"
|
100
|
+
when :host_key_alias
|
101
|
+
"HostKeyAlias #{value}"
|
102
|
+
when :host_name
|
103
|
+
"HostName #{value}"
|
104
|
+
when :keys
|
105
|
+
[*value].map do |val|
|
106
|
+
"IdentityFile #{val}"
|
107
|
+
end
|
108
|
+
when :hmac
|
109
|
+
"Macs #{[*value].join(?,)}"
|
110
|
+
when :port
|
111
|
+
"Port #{value}"
|
112
|
+
when :proxy
|
113
|
+
if value.kind_of?(Net::SSH::Proxy::Command)
|
114
|
+
"ProxyCommand #{value.command_line_template}"
|
115
|
+
else
|
116
|
+
"ProxyCommand #{value}"
|
117
|
+
end
|
118
|
+
when :rekey_limit
|
119
|
+
"RekeyLimit #{value}"
|
120
|
+
when :user
|
121
|
+
"User #{value}"
|
122
|
+
when :user_known_hosts_file
|
123
|
+
"UserKnownHostsFile #{value}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def hostname
|
129
|
+
ssh_options[:host_name] || name
|
130
|
+
end
|
131
|
+
|
132
|
+
def user
|
133
|
+
ssh_options[:user]
|
134
|
+
end
|
135
|
+
|
136
|
+
def ssh_port
|
137
|
+
ssh_options[:port]
|
138
|
+
end
|
139
|
+
|
140
|
+
def preferred_driver
|
141
|
+
properties[:preferred_driver] && properties[:preferred_driver].to_sym
|
142
|
+
end
|
143
|
+
|
144
|
+
def bundler_cmd
|
145
|
+
properties[:bundler_cmd] || 'bundle'
|
146
|
+
end
|
147
|
+
|
148
|
+
def ssh_connection
|
149
|
+
@ssh ||= make_ssh_connection
|
150
|
+
end
|
151
|
+
|
152
|
+
def make_ssh_connection
|
153
|
+
Net::SSH.start(name, nil, ssh_options)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hocho
|
2
|
+
class Inventory
|
3
|
+
def initialize(providers)
|
4
|
+
@providers = providers
|
5
|
+
end
|
6
|
+
|
7
|
+
def hosts
|
8
|
+
@hosts ||= @providers.flat_map(&:hosts)
|
9
|
+
end
|
10
|
+
|
11
|
+
def filter(filters)
|
12
|
+
filters = filters.map do |name, value|
|
13
|
+
[name.to_s, value.to_s.split(?,) { |_| /#{Regexp.escape(_).gsub(/\\*/,'.*')}/ }]
|
14
|
+
end.to_h
|
15
|
+
|
16
|
+
hosts.select do |host|
|
17
|
+
filters.all? do |name, conditions|
|
18
|
+
case name
|
19
|
+
when 'name'
|
20
|
+
conditions.any? { |c| host.name.match(c) }
|
21
|
+
else
|
22
|
+
v = (host.attributes[name] || host.attributes[name.to_sym] || host.tags[name] || host.tags[name.to_sym])
|
23
|
+
v && conditions.any? { |c| v.to_s.match(c) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'hocho/utils/symbolize'
|
2
|
+
require 'hocho/inventory_providers/base'
|
3
|
+
require 'hocho/host'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module Hocho
|
7
|
+
module InventoryProviders
|
8
|
+
class File < Base
|
9
|
+
def initialize(path:)
|
10
|
+
@path = path
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
def files
|
16
|
+
@files ||= case
|
17
|
+
when ::File.directory?(path)
|
18
|
+
Dir[::File.join(path, "*.yml")]
|
19
|
+
else
|
20
|
+
[path]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def hosts
|
25
|
+
@hosts ||= files.flat_map do |file|
|
26
|
+
content = Hocho::Utils::Symbolize.keys_of(YAML.load_file(file))
|
27
|
+
content.map do |name, value|
|
28
|
+
Host.new(
|
29
|
+
name.to_s,
|
30
|
+
provider: self.class,
|
31
|
+
properties: value[:properties],
|
32
|
+
tags: value[:tags],
|
33
|
+
ssh_options: value[:ssh_options]
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/hocho/runner.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'hocho/drivers'
|
2
|
+
|
3
|
+
module Hocho
|
4
|
+
class Runner
|
5
|
+
def initialize(host, driver: nil, check_ssh_port: false, base_dir: '.', initializers: [], driver_options: {})
|
6
|
+
@host = host
|
7
|
+
@driver = driver && driver.to_sym
|
8
|
+
@check_ssh_port = check_ssh_port
|
9
|
+
@base_dir = base_dir
|
10
|
+
@initializers = initializers
|
11
|
+
@driver_options = driver_options
|
12
|
+
|
13
|
+
@bundler_support = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :host, :driver, :base_dir, :initializers
|
17
|
+
|
18
|
+
def run(dry_run: false)
|
19
|
+
puts "Running using #{best_driver_name}"
|
20
|
+
driver_options = @driver_options[best_driver_name] || {}
|
21
|
+
best_driver.new(host, base_dir: base_dir, initializers: initializers, **driver_options).run(dry_run: dry_run)
|
22
|
+
end
|
23
|
+
|
24
|
+
# def check_ssh_port
|
25
|
+
# return unless @check_ssh_port
|
26
|
+
# end
|
27
|
+
|
28
|
+
def ssh
|
29
|
+
host.ssh_connection
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def best_driver_name
|
35
|
+
@best_driver_name ||= case
|
36
|
+
when @driver
|
37
|
+
@driver
|
38
|
+
when @host.preferred_driver
|
39
|
+
@host.preferred_driver
|
40
|
+
when !bundler_support?
|
41
|
+
:itamae_ssh
|
42
|
+
else
|
43
|
+
:bundler
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def best_driver
|
48
|
+
@best_driver ||= Hocho::Drivers.find(best_driver_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def bundler_support?
|
52
|
+
# ssh_askpass
|
53
|
+
return @bundler_support unless @bundler_support.nil?
|
54
|
+
@bundler_support = (ssh.exec!("#{host.bundler_cmd} -v") || '').match(/^Bundler version/)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Hocho
|
2
|
+
module Utils
|
3
|
+
module Finder
|
4
|
+
def self.find(const, prefix, name)
|
5
|
+
retried = false
|
6
|
+
constant_name = name.to_s.gsub(/\A.|_./) { |s| s[-1].upcase }
|
7
|
+
|
8
|
+
begin
|
9
|
+
const.const_get constant_name, false
|
10
|
+
rescue NameError
|
11
|
+
unless retried
|
12
|
+
begin
|
13
|
+
require "#{prefix}/#{name}"
|
14
|
+
rescue LoadError
|
15
|
+
end
|
16
|
+
|
17
|
+
retried = true
|
18
|
+
retry
|
19
|
+
end
|
20
|
+
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Hocho
|
2
|
+
module Utils
|
3
|
+
module Symbolize
|
4
|
+
def self.keys_of(obj)
|
5
|
+
case obj
|
6
|
+
when Hash
|
7
|
+
Hash[obj.map { |k, v| [k.is_a?(String) ? k.to_sym : k, keys_of(v)] }]
|
8
|
+
when Array
|
9
|
+
obj.map { |v| keys_of(v) }
|
10
|
+
else
|
11
|
+
obj
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/hocho.rb
ADDED
data/script/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "hocho"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/script/setup
ADDED
metadata
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hocho
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0.beta1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- sorah (Shota Fukumori)
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: itamae
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: net-ssh
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: hashie
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.10'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.10'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '10.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '10.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
description:
|
112
|
+
email:
|
113
|
+
- her@sorah.jp
|
114
|
+
executables:
|
115
|
+
- hocho
|
116
|
+
extensions: []
|
117
|
+
extra_rdoc_files: []
|
118
|
+
files:
|
119
|
+
- ".gitignore"
|
120
|
+
- ".rspec"
|
121
|
+
- ".travis.yml"
|
122
|
+
- Gemfile
|
123
|
+
- LICENSE.txt
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- bin/hocho
|
127
|
+
- hocho.gemspec
|
128
|
+
- lib/hocho.rb
|
129
|
+
- lib/hocho/command.rb
|
130
|
+
- lib/hocho/config.rb
|
131
|
+
- lib/hocho/drivers.rb
|
132
|
+
- lib/hocho/drivers/base.rb
|
133
|
+
- lib/hocho/drivers/bundler.rb
|
134
|
+
- lib/hocho/drivers/itamae_ssh.rb
|
135
|
+
- lib/hocho/drivers/ssh_base.rb
|
136
|
+
- lib/hocho/host.rb
|
137
|
+
- lib/hocho/inventory.rb
|
138
|
+
- lib/hocho/inventory_providers.rb
|
139
|
+
- lib/hocho/inventory_providers/base.rb
|
140
|
+
- lib/hocho/inventory_providers/file.rb
|
141
|
+
- lib/hocho/property_providers.rb
|
142
|
+
- lib/hocho/property_providers/base.rb
|
143
|
+
- lib/hocho/runner.rb
|
144
|
+
- lib/hocho/utils/finder.rb
|
145
|
+
- lib/hocho/utils/symbolize.rb
|
146
|
+
- lib/hocho/version.rb
|
147
|
+
- script/console
|
148
|
+
- script/setup
|
149
|
+
homepage: https://github.com/sorah/hocho
|
150
|
+
licenses:
|
151
|
+
- MIT
|
152
|
+
metadata: {}
|
153
|
+
post_install_message:
|
154
|
+
rdoc_options: []
|
155
|
+
require_paths:
|
156
|
+
- lib
|
157
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: 1.3.1
|
167
|
+
requirements: []
|
168
|
+
rubyforge_project:
|
169
|
+
rubygems_version: 2.6.3
|
170
|
+
signing_key:
|
171
|
+
specification_version: 4
|
172
|
+
summary: Server provisioning tool with itamae
|
173
|
+
test_files: []
|