knife-solo 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ require 'pathname'
2
+
3
+ require 'chef/knife'
4
+ require 'chef/config'
5
+
6
+ require 'knife-solo/ssh_command'
7
+ require 'knife-solo/kitchen_command'
8
+
9
+ class Chef
10
+ class Knife
11
+ # Approach ported from spatula (https://github.com/trotter/spatula)
12
+ # Copyright 2009, Trotter Cashion
13
+ class Cook < Knife
14
+ include KnifeSolo::SshCommand
15
+ include KnifeSolo::KitchenCommand
16
+
17
+ banner "knife prepare [user@]hostname [json] (options)"
18
+
19
+ def run
20
+ super
21
+
22
+ check_syntax
23
+
24
+ Chef::Config.from_file('solo.rb')
25
+
26
+ rsync_kitchen
27
+
28
+ add_patches
29
+
30
+ logging_arg = "-l debug" if config[:verbosity] > 0
31
+ stream_command <<-BASH
32
+ sudo chef-solo -c #{chef_path}/solo.rb \
33
+ -j #{chef_path}/#{node_config} \
34
+ #{logging_arg}
35
+ BASH
36
+ end
37
+
38
+ def check_syntax
39
+ Dir["**/*.rb"].each do |recipe|
40
+ ok = system "ruby -c #{recipe} >/dev/null 2>&1"
41
+ raise "Syntax error in #{recipe}" if not ok
42
+ end
43
+
44
+ Dir["**/*.json"].each do |json|
45
+ begin
46
+ require 'json'
47
+ # parse without instantiating Chef classes
48
+ JSON.parse File.read(json), :create_additions => false
49
+ rescue => error
50
+ raise "Syntax error in #{json}: #{error.message}"
51
+ end
52
+ end
53
+ end
54
+
55
+ def node_config
56
+ @name_args[1] || super
57
+ end
58
+
59
+ def chef_path
60
+ Chef::Config.file_cache_path
61
+ end
62
+
63
+ def patch_path
64
+ Array(Chef::Config.cookbook_path).first + "/chef_solo_patches/libraries"
65
+ end
66
+
67
+ # TODO (mat): Let rsync write to /var/chef-solo with sudo somehow
68
+ def rsync_kitchen
69
+ system %Q{rsync -rlP --rsh="ssh #{ssh_args}" --delete --exclude '.*' ./ :#{chef_path}}
70
+ end
71
+
72
+ def add_patches
73
+ run_command "mkdir -p #{patch_path}"
74
+ Dir[Pathname.new(__FILE__).dirname.join("patches", "*.rb")].each do |patch|
75
+ system %Q{rsync -rlP --rsh="ssh #{ssh_args}" #{patch} :#{patch_path}}
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,29 @@
1
+ require 'chef/knife'
2
+
3
+ class Chef
4
+ class Knife
5
+ class Kitchen < Knife
6
+ include FileUtils
7
+
8
+ banner "knife kitchen NAME"
9
+
10
+ def run
11
+ name = @name_args.first
12
+ mkdir name
13
+ mkdir name + "/nodes"
14
+ mkdir name + "/roles"
15
+ mkdir name + "/data_bags"
16
+ mkdir name + "/site-cookbooks"
17
+ mkdir name + "/cookbooks"
18
+ File.open(name + "/solo.rb", 'w') do |f|
19
+ f << <<-RUBY.gsub(/^\s+/, '')
20
+ file_cache_path "/tmp/chef-solo"
21
+ data_bag_path "/tmp/chef-solo/data_bags"
22
+ cookbook_path [ "/tmp/chef-solo/site-cookbooks",
23
+ "/tmp/chef-solo/cookbooks" ]
24
+ RUBY
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,42 @@
1
+ # Taken from Vagrant's patch: https://gist.github.com/867960
2
+ # based on Brian Akins's patch: http://lists.opscode.com/sympa/arc/chef/2011-02/msg00000.html
3
+ if Chef::Config[:solo]
4
+ class Chef
5
+ module Mixin
6
+ module Language
7
+ def data_bag(bag)
8
+ @solo_data_bags = {} if @solo_data_bags.nil?
9
+ unless @solo_data_bags[bag]
10
+ @solo_data_bags[bag] = {}
11
+ data_bag_path = Chef::Config[:data_bag_path]
12
+ Dir.glob(File.join(data_bag_path, bag, "*.json")).each do |f|
13
+ item = JSON.parse(IO.read(f))
14
+ @solo_data_bags[bag][item['id']] = item
15
+ end
16
+ end
17
+ @solo_data_bags[bag].keys
18
+ end
19
+
20
+ def data_bag_item(bag, item)
21
+ data_bag(bag) unless ( !@solo_data_bags.nil? && @solo_data_bags[bag])
22
+ @solo_data_bags[bag][item]
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+
29
+ class Chef
30
+ class Recipe
31
+ def search(bag_name, query=nil)
32
+ Chef::Log.warn("Simplistic search patch, ignoring query of %s" % [query]) unless query.nil?
33
+ data_bag(bag_name.to_s).each do |bag_item_id|
34
+ bag_item = data_bag_item(bag_name.to_s, bag_item_id)
35
+ yield bag_item
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,117 @@
1
+ require 'chef/knife'
2
+ require 'knife-solo/ssh_command'
3
+ require 'knife-solo/kitchen_command'
4
+
5
+ class Chef
6
+ class Knife
7
+ # Approach ported from littlechef (https://github.com/tobami/littlechef)
8
+ # Copyright 2010, 2011, Miquel Torres <tobami@googlemail.com>
9
+ class Prepare < Knife
10
+ include KnifeSolo::SshCommand
11
+ include KnifeSolo::KitchenCommand
12
+
13
+ banner "knife prepare [user@]hostname (options)"
14
+
15
+ def run
16
+ super
17
+ send("#{distro[:type]}_gem_install")
18
+ generate_node_config
19
+ end
20
+
21
+ def generate_node_config
22
+ File.open(node_config, 'w') do |f|
23
+ f.print <<-JSON.gsub(/^\s+/, '')
24
+ { "run_list": [] }
25
+ JSON
26
+ end unless node_config.exist?
27
+ end
28
+
29
+ def package_list
30
+ @packages.join(' ')
31
+ end
32
+
33
+ # TODO (mat): integration test this, not a gem install
34
+ def emerge_gem_install
35
+ ui.msg("Installing required packages...")
36
+ run_command("sudo USE='-test' ACCEPT_KEYWORDS='~amd64' emerge -u chef")
37
+ end
38
+
39
+ def add_rpm_repos
40
+ ui.message("Adding EPEL and ELFF...")
41
+ repo_url = "http://download.fedora.redhat.com"
42
+ repo_path = "/pub/epel/5/i386/epel-release-5-4.noarch.rpm"
43
+ result = run_command("sudo rpm -Uvh #{repo_url}#{repo_path}")
44
+ installed = "package epel-release-5-4.noarch is already installed"
45
+ raise result.stderr unless result.success? || result.stdout.match(installed)
46
+
47
+ repo_url = "http://download.elff.bravenet.com"
48
+ repo_path = "/5/i386/elff-release-5-3.noarch.rpm"
49
+ result = run_command("sudo rpm -Uvh #{repo_url}#{repo_path}")
50
+ installed = "package elff-release-5-3.noarch is already installed"
51
+ raise result.stderr unless result.success? || result.stdout.match(installed)
52
+ end
53
+
54
+ # TODO (mat): integration test this, will probably break without sudo
55
+ def rpm_gem_install
56
+ ui.msg("Installing required packages...")
57
+ @packages = %w(ruby ruby-shadow gcc gcc-c++ ruby-devel wget rsync)
58
+ run_command("sudo yum -y install #{package_list}")
59
+ gem_install
60
+ end
61
+
62
+ def debian_gem_install
63
+ ui.msg "Updating apt caches..."
64
+ run_command("sudo apt-get update")
65
+
66
+ ui.msg "Installing required packages..."
67
+ @packages = %w(ruby ruby-dev libopenssl-ruby irb
68
+ build-essential wget ssl-cert rsync)
69
+ run_command <<-BASH
70
+ sudo DEBIAN_FRONTEND=noninteractive apt-get --yes install #{package_list}
71
+ BASH
72
+
73
+ gem_install
74
+ end
75
+
76
+ def gem_install
77
+ ui.msg "Installing rubygems from source..."
78
+ release = "rubygems-1.7.2"
79
+ file = "#{release}.tgz"
80
+ url = "http://production.cf.rubygems.org/rubygems/#{file}"
81
+ run_command("wget #{url}")
82
+ run_command("tar zxf #{file}")
83
+ run_command("cd #{release} && sudo ruby setup.rb --no-format-executable")
84
+ run_command("sudo rm -rf #{release} #{file}")
85
+ run_command("sudo gem install --no-rdoc --no-ri chef")
86
+ end
87
+
88
+ def issue
89
+ run_command("cat /etc/issue").stdout
90
+ end
91
+
92
+ def distro
93
+ @distro ||= case issue
94
+ when %r{Debian GNU/Linux 5}
95
+ {:type => "debian", :version => "lenny"}
96
+ when %r{Debian GNU/Linux 6}
97
+ {:type => "debian", :version => "squeeze"}
98
+ when %r{Debian GNU/Linux wheezy}
99
+ {:type => "debian", :version => "wheezy"}
100
+ when %r{Ubuntu}
101
+ version = run_command("lsb_release -cs").stdout.strip
102
+ {:type => "debian", :version => version}
103
+ when %r{CentOS}
104
+ {:type => "rpm", :version => "CentOS"}
105
+ when %r{Red Hat Enterprise Linux}
106
+ {:type => "rpm", :version => "Red Hat"}
107
+ when %r{Scientific Linux SL}
108
+ {:type => "rpm", :version => "Scientific Linux"}
109
+ when %r{This is \\n\.\\O (\\s \\m \\r) \\t}
110
+ {:type => "gentoo", :version => "Gentoo"}
111
+ else
112
+ raise "Contents of /etc/issue not recognized, please fork https://github.com/matschaffer/knife-solo and add support for your distro."
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1 @@
1
+ require 'knife-solo/info'
@@ -0,0 +1,3 @@
1
+ module KnifeSolo
2
+ VERSION = '0.0.3'
3
+ end
@@ -0,0 +1,18 @@
1
+ module KnifeSolo
2
+ module KitchenCommand
3
+ class OutOfKitchenError < StandardError
4
+ def message
5
+ "This command must be run inside a Chef solo kitchen."
6
+ end
7
+ end
8
+
9
+ def required_files
10
+ %w(nodes roles cookbooks data_bags solo.rb)
11
+ end
12
+
13
+ def run
14
+ all_present = required_files.inject(true) { |m, f| m && File.exists?(f) }
15
+ raise OutOfKitchenError.new unless all_present
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,161 @@
1
+ require 'pathname'
2
+
3
+ module KnifeSolo
4
+ module SshCommand
5
+ def self.included(other)
6
+ other.instance_eval do
7
+ deps do
8
+ require 'net/ssh'
9
+ end
10
+
11
+ option :ssh_config,
12
+ :short => "-F configfile",
13
+ :long => "--ssh-config-file configfile",
14
+ :description => "Alternate location for ssh config file"
15
+
16
+ option :ssh_password,
17
+ :short => "-P PASSWORD",
18
+ :long => "--ssh-password PASSWORD",
19
+ :description => "The ssh password"
20
+
21
+ option :ssh_identity,
22
+ :short => "-i FILE",
23
+ :long => "--ssh-identity FILE",
24
+ :description => "The ssh identity file"
25
+
26
+ option :ssh_port,
27
+ :short => "-p FILE",
28
+ :long => "--ssh-port FILE",
29
+ :description => "The ssh port"
30
+ end
31
+ end
32
+
33
+ def node_config
34
+ Pathname.new("nodes/#{host}.json")
35
+ end
36
+
37
+ def host_descriptor
38
+ return @host_descriptor if @host_descriptor
39
+ parts = @name_args.first.split('@')
40
+ @host_descriptor = {
41
+ :host => parts.pop,
42
+ :user => parts.pop
43
+ }
44
+ end
45
+
46
+ def user
47
+ host_descriptor[:user] || config_file_options[:user] || ENV['USER']
48
+ end
49
+
50
+ def host
51
+ host_descriptor[:host]
52
+ end
53
+
54
+ def ask_password
55
+ ui.ask("Enter the password for #{user}@#{host}: ") do |q|
56
+ q.echo = false
57
+ end
58
+ end
59
+
60
+ def password
61
+ config[:ssh_password] ||= ask_password
62
+ end
63
+
64
+ def try_connection
65
+ Net::SSH.start(host, user, connection_options) do |ssh|
66
+ ssh.exec!("true")
67
+ end
68
+ end
69
+
70
+ def config_file_options
71
+ Net::SSH::Config.for(host, config_files)
72
+ end
73
+
74
+ def connection_options
75
+ options = config_file_options
76
+ options[:port] = config[:ssh_port] if config[:ssh_port]
77
+ options[:password] = config[:ssh_password] if config[:ssh_password]
78
+ options
79
+ end
80
+
81
+ def config_files
82
+ Array(config[:ssh_config] || Net::SSH::Config.default_files)
83
+ end
84
+
85
+ def detect_authentication_method
86
+ return @detected if @detected
87
+ begin
88
+ try_connection
89
+ rescue Errno::ETIMEDOUT
90
+ raise "Unable to connect to #{host}"
91
+ rescue Net::SSH::AuthenticationFailed
92
+ # Ensure the password is set or ask for it immediately
93
+ password
94
+ end
95
+ @detected = true
96
+ end
97
+
98
+ def ssh_args
99
+ host_arg = [user, host].compact.join('@')
100
+ config_arg = "-F #{config[:ssh_config]}" if config[:ssh_config]
101
+ ident_arg = "-i #{config[:ssh_identity]}" if config[:ssh_identity]
102
+ port_arg = "-p #{config[:ssh_port]}" if config[:ssh_port]
103
+
104
+ [host_arg, config_arg, ident_arg, port_arg].compact.join(' ')
105
+ end
106
+
107
+ class ExecResult
108
+ attr_accessor :stdout, :stderr, :exit_code
109
+
110
+ def initialize
111
+ @stdout = ""
112
+ @stderr = ""
113
+ end
114
+
115
+ def success?
116
+ exit_code == 0
117
+ end
118
+ end
119
+
120
+ def stream_command(command)
121
+ run_command(command, :streaming => true)
122
+ end
123
+
124
+ def run_command(command, options={})
125
+ detect_authentication_method
126
+
127
+ result = ExecResult.new
128
+ command = command.sub(/^\s*sudo/, 'sudo -p \'knife sudo password: \'')
129
+ Net::SSH.start(host, user, connection_options) do |ssh|
130
+ ssh.open_channel do |channel|
131
+ channel.request_pty
132
+ channel.exec(command) do |ch, success|
133
+ raise "ssh.channel.exec failure" unless success
134
+
135
+ channel.on_data do |ch, data| # stdout
136
+ if data =~ /^knife sudo password: /
137
+ ch.send_data("#{password}\n")
138
+ else
139
+ ui.stdout << data if options[:streaming]
140
+ result.stdout << data
141
+ end
142
+ end
143
+
144
+ channel.on_extended_data do |ch, type, data|
145
+ next unless type == 1
146
+ ui.stderr << data if options[:streaming]
147
+ result.stderr << data
148
+ end
149
+
150
+ channel.on_request("exit-status") do |ch, data|
151
+ result.exit_code = data.read_long
152
+ end
153
+
154
+ end
155
+ ssh.loop
156
+ end
157
+ end
158
+ result
159
+ end
160
+ end
161
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knife-solo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mat Schaffer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-08-01 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70346489903680 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70346489903680
25
+ - !ruby/object:Gem::Dependency
26
+ name: mocha
27
+ requirement: &70346489903000 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70346489903000
36
+ - !ruby/object:Gem::Dependency
37
+ name: virtualbox
38
+ requirement: &70346489902460 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70346489902460
47
+ - !ruby/object:Gem::Dependency
48
+ name: chef
49
+ requirement: &70346489901760 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.10.0
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70346489901760
58
+ - !ruby/object:Gem::Dependency
59
+ name: net-ssh
60
+ requirement: &70346489901160 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 2.1.3
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70346489901160
69
+ description: Handles bootstrapping, running chef solo, rsyncing cookbooks etc
70
+ email: mat@schaffer.me
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/chef/knife/cook.rb
76
+ - lib/chef/knife/kitchen.rb
77
+ - lib/chef/knife/patches/data_bags_patch.rb
78
+ - lib/chef/knife/prepare.rb
79
+ - lib/knife-solo/info.rb
80
+ - lib/knife-solo/kitchen_command.rb
81
+ - lib/knife-solo/ssh_command.rb
82
+ - lib/knife-solo.rb
83
+ homepage: https://github.com/matschaffer/knife-solo
84
+ licenses: []
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ segments:
96
+ - 0
97
+ hash: -2883154116349428333
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ segments:
105
+ - 0
106
+ hash: -2883154116349428333
107
+ requirements: []
108
+ rubyforge_project: nowarning
109
+ rubygems_version: 1.8.6
110
+ signing_key:
111
+ specification_version: 3
112
+ summary: A collection of knife plugins for dealing with chef solo
113
+ test_files: []