hocho 0.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.0
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hocho.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/hocho ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'hocho/command'
3
+
4
+ Hocho::Command.start
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
@@ -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
@@ -0,0 +1,9 @@
1
+ require 'hocho/utils/finder'
2
+
3
+ module Hocho
4
+ module Drivers
5
+ def self.find(name)
6
+ Hocho::Utils::Finder.find(self, 'hocho/drivers', name)
7
+ end
8
+ end
9
+ 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,9 @@
1
+ module Hocho
2
+ module InventoryProviders
3
+ class Base
4
+ def hosts
5
+ raise NotImplementedError
6
+ end
7
+ end
8
+ end
9
+ 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
@@ -0,0 +1,9 @@
1
+ require 'hocho/utils/finder'
2
+
3
+ module Hocho
4
+ module InventoryProviders
5
+ def self.find(name)
6
+ Hocho::Utils::Finder.find(self, 'hocho/inventory_providers', name)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Hocho
2
+ module PropertyProviders
3
+ class Base
4
+ def determine(host)
5
+ raise NotImplementedError
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'hocho/utils/finder'
2
+
3
+ module Hocho
4
+ module PropertyProviders
5
+ def self.find(name)
6
+ Hocho::Utils::Finder.find(self, 'hocho/inventory_providers', name)
7
+ end
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Hocho
2
+ VERSION = "0.1.0.beta1"
3
+ end
data/lib/hocho.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "hocho/version"
2
+
3
+ module Hocho
4
+ # Your code goes here...
5
+ end
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
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
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: []