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 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: []