soloist-rvm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.pairs +14 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +121 -0
- data/Guardfile +17 -0
- data/LICENSE +20 -0
- data/README.md +116 -0
- data/Vagrantfile +21 -0
- data/bin/soloist +4 -0
- data/examples/Guardfile +13 -0
- data/examples/soloistrc +2 -0
- data/lib/soloist.rb +2 -0
- data/lib/soloist/cli.rb +78 -0
- data/lib/soloist/config.rb +118 -0
- data/lib/soloist/remote.rb +66 -0
- data/lib/soloist/remote_config.rb +62 -0
- data/lib/soloist/royal_crown.rb +56 -0
- data/lib/soloist/spotlight.rb +41 -0
- data/lib/soloist/version.rb +3 -0
- data/script/bootstrap.sh +54 -0
- data/script/ci.sh +3 -0
- data/soloist-rvm.gemspec +34 -0
- data/spec/helpers/net_ssh_test_helper.rb +8 -0
- data/spec/helpers/net_ssh_test_patch.rb +38 -0
- data/spec/lib/soloist/cli_spec.rb +171 -0
- data/spec/lib/soloist/config_spec.rb +196 -0
- data/spec/lib/soloist/remote_config_spec.rb +84 -0
- data/spec/lib/soloist/remote_spec.rb +135 -0
- data/spec/lib/soloist/royal_crown_spec.rb +76 -0
- data/spec/lib/soloist/spotlight_spec.rb +66 -0
- data/spec/spec_helper.rb +15 -0
- metadata +297 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
require "soloist/royal_crown"
|
2
|
+
require "tempfile"
|
3
|
+
|
4
|
+
module Soloist
|
5
|
+
class Config
|
6
|
+
attr_writer :solo_rb_path, :node_json_path
|
7
|
+
attr_reader :royal_crown
|
8
|
+
|
9
|
+
def self.from_file(royal_crown_path)
|
10
|
+
rc = Soloist::RoyalCrown.from_file(royal_crown_path)
|
11
|
+
new(rc)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(royal_crown)
|
15
|
+
@royal_crown = royal_crown
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_chef
|
19
|
+
exec(conditional_sudo("bash -c '#{chef_solo}'"))
|
20
|
+
end
|
21
|
+
|
22
|
+
def chef_solo
|
23
|
+
"chef-solo -c '#{solo_rb_path}' -l '#{log_level}'"
|
24
|
+
end
|
25
|
+
|
26
|
+
def as_solo_rb
|
27
|
+
<<-SOLO_RB
|
28
|
+
file_cache_path "#{chef_cache_path}"
|
29
|
+
cookbook_path #{cookbook_paths.inspect}
|
30
|
+
json_attribs "#{node_json_path}"
|
31
|
+
SOLO_RB
|
32
|
+
end
|
33
|
+
|
34
|
+
def as_node_json
|
35
|
+
compiled.node_attributes.to_hash.merge({ "recipes" => compiled.recipes })
|
36
|
+
end
|
37
|
+
|
38
|
+
def chef_cache_path
|
39
|
+
"/var/chef/cache".tap do |cache_path|
|
40
|
+
system(conditional_sudo("mkdir -p #{cache_path}")) \
|
41
|
+
unless File.directory?(cache_path)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def cookbook_paths
|
46
|
+
([royal_crown_cookbooks_directory] + compiled.cookbook_paths).map do |path|
|
47
|
+
File.expand_path(path, royal_crown_path)
|
48
|
+
end.uniq.select do |path|
|
49
|
+
File.directory?(path)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def solo_rb_path
|
54
|
+
@solo_rb_path ||= Tempfile.new(["solo", ".rb"]).tap do |file|
|
55
|
+
puts as_solo_rb if debug?
|
56
|
+
file.write(as_solo_rb)
|
57
|
+
file.close
|
58
|
+
end.path
|
59
|
+
end
|
60
|
+
|
61
|
+
def node_json_path
|
62
|
+
@node_json_path ||= Tempfile.new(["node", ".json"]).tap do |file|
|
63
|
+
puts JSON.pretty_generate(as_node_json) if debug?
|
64
|
+
file.write(JSON.dump(as_node_json))
|
65
|
+
file.close
|
66
|
+
end.path
|
67
|
+
end
|
68
|
+
|
69
|
+
def merge!(other)
|
70
|
+
royal_crown.recipes += other.royal_crown.recipes
|
71
|
+
royal_crown.cookbook_paths += other.royal_crown.cookbook_paths
|
72
|
+
royal_crown.node_attributes.merge!(other.royal_crown.node_attributes)
|
73
|
+
royal_crown.env_variable_switches.merge!(other.royal_crown.env_variable_switches)
|
74
|
+
end
|
75
|
+
|
76
|
+
def compiled
|
77
|
+
@compiled ||= royal_crown.dup.tap do |rc|
|
78
|
+
while rc["env_variable_switches"]
|
79
|
+
rc.delete("env_variable_switches").each do |variable, switch|
|
80
|
+
switch.each do |value, inner|
|
81
|
+
rc.merge!(inner) if ENV[variable] == value
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def log_level
|
89
|
+
ENV["LOG_LEVEL"] || "info"
|
90
|
+
end
|
91
|
+
|
92
|
+
def debug?
|
93
|
+
log_level == "debug"
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def conditional_sudo(command)
|
98
|
+
sudo_command = rvm? ? "rvmsudo_secure_path=1 rvmsudo" : "sudo"
|
99
|
+
root? ? command : "#{sudo_command} -E #{command}"
|
100
|
+
end
|
101
|
+
|
102
|
+
def root?
|
103
|
+
Process.uid == 0
|
104
|
+
end
|
105
|
+
|
106
|
+
def rvm?
|
107
|
+
`which rvm`.length > 0
|
108
|
+
end
|
109
|
+
|
110
|
+
def royal_crown_cookbooks_directory
|
111
|
+
File.expand_path("cookbooks", royal_crown_path)
|
112
|
+
end
|
113
|
+
|
114
|
+
def royal_crown_path
|
115
|
+
File.dirname(royal_crown.path)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require "net/ssh"
|
2
|
+
require "shellwords"
|
3
|
+
require "etc"
|
4
|
+
|
5
|
+
module Soloist
|
6
|
+
class RemoteError < RuntimeError; end
|
7
|
+
|
8
|
+
class Remote
|
9
|
+
attr_reader :user, :host, :key, :timeout, :stdout, :stderr, :exitstatus
|
10
|
+
attr_writer :connection
|
11
|
+
|
12
|
+
def self.from_uri(uri, key = "~/.ssh/id_rsa")
|
13
|
+
parsed = URI.parse("ssh://#{uri}")
|
14
|
+
new(parsed.user || Etc.getlogin, parsed.host, key)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(user, host, key, options = {})
|
18
|
+
@user = user
|
19
|
+
@host = host
|
20
|
+
@key = key
|
21
|
+
@timeout = options[:timeout] || 10000
|
22
|
+
end
|
23
|
+
|
24
|
+
def backtick(command)
|
25
|
+
@stdout = ""
|
26
|
+
@stderr = ""
|
27
|
+
exec(command)
|
28
|
+
@stdout
|
29
|
+
end
|
30
|
+
|
31
|
+
def system(command)
|
32
|
+
@stdout = STDOUT
|
33
|
+
@stderr = STDERR
|
34
|
+
exec(command)
|
35
|
+
exitstatus
|
36
|
+
end
|
37
|
+
|
38
|
+
def system!(*command)
|
39
|
+
system(*command).tap do |status|
|
40
|
+
raise RemoteError.new("#{command.join(" ")} exited #{status}") unless status == 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def upload(from, to, opts = "--exclude .git")
|
45
|
+
Kernel.system("rsync -e 'ssh -i #{key}' -avz --delete #{from} #{user}@#{host}:#{to} #{opts}")
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def connection
|
50
|
+
@connection ||= Net::SSH.start(host, user, :keys => [key], :timeout => timeout)
|
51
|
+
end
|
52
|
+
|
53
|
+
def exec(*command)
|
54
|
+
connection.open_channel do |channel|
|
55
|
+
channel.exec(*command) do |stream, success|
|
56
|
+
raise RemoteError.new("Could not run #{command.join(" ")}") unless success
|
57
|
+
stream.on_data { |_, data| stdout << data }
|
58
|
+
stream.on_extended_data { |_, type, data| stderr << data }
|
59
|
+
stream.on_request("exit-status") { |_, data| @exitstatus = data.read_long }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
connection.loop
|
63
|
+
@exitstatus ||= 0
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "soloist/config"
|
2
|
+
require "soloist/remote"
|
3
|
+
|
4
|
+
module Soloist
|
5
|
+
class RemoteConfig < Config
|
6
|
+
attr_reader :remote
|
7
|
+
|
8
|
+
def self.from_file(royal_crown_path, remote)
|
9
|
+
rc = Soloist::RoyalCrown.from_file(royal_crown_path)
|
10
|
+
new(rc, remote)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(royal_crown, remote)
|
14
|
+
@royal_crown = royal_crown
|
15
|
+
@remote = remote
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_chef
|
19
|
+
remote.system!(conditional_sudo(%(/bin/bash -lc "#{chef_solo}")))
|
20
|
+
end
|
21
|
+
|
22
|
+
def node_json_path
|
23
|
+
@node_json_path ||= File.expand_path("node.json", chef_config_path).tap do |path|
|
24
|
+
remote.system!(%(echo '#{JSON.dump(as_node_json)}' | #{conditional_sudo("tee #{path}")}))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def solo_rb_path
|
29
|
+
@solo_rb_path ||= File.expand_path("solo.rb", chef_config_path).tap do |path|
|
30
|
+
remote.system!(%(echo '#{as_solo_rb}' | #{conditional_sudo("tee #{path}")}))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def chef_cache_path
|
35
|
+
@chef_cache_path ||= "/var/chef/cache".tap do |cache_path|
|
36
|
+
remote.system!(conditional_sudo("/bin/mkdir -m 777 -p #{cache_path}"))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def chef_config_path
|
41
|
+
@chef_config_path ||= "/etc/chef".tap do |path|
|
42
|
+
remote.system!(conditional_sudo("/bin/mkdir -m 777 -p #{path}"))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def cookbook_paths
|
47
|
+
@cookbook_paths ||= ["/var/chef/cookbooks".tap do |remote_path|
|
48
|
+
remote.system!(conditional_sudo("/bin/mkdir -m 777 -p #{remote_path}"))
|
49
|
+
super.each { |path| remote.upload("#{path}/", remote_path) }
|
50
|
+
end]
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
def conditional_sudo(command)
|
55
|
+
root? ? command : "/usr/bin/sudo -E #{command}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def root?
|
59
|
+
remote.user == "root"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "hashie"
|
2
|
+
|
3
|
+
module Soloist
|
4
|
+
class RoyalCrown < Hashie::Trash
|
5
|
+
property :path
|
6
|
+
property :recipes, :default => []
|
7
|
+
property :cookbook_paths, :default => []
|
8
|
+
property :node_attributes, :default => Hashie::Mash.new,
|
9
|
+
:transform_with => lambda { |v| Hashie::Mash.new(v) }
|
10
|
+
property :env_variable_switches, :default => Hashie::Mash.new,
|
11
|
+
:transform_with => lambda { |v| Hashie::Mash.new(v) }
|
12
|
+
|
13
|
+
def node_attributes=(hash)
|
14
|
+
self["node_attributes"] = Hashie::Mash.new(hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
def env_variable_switches=(hash)
|
18
|
+
self["env_variable_switches"] = Hashie::Mash.new(hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_yaml
|
22
|
+
to_hash.tap do |hash|
|
23
|
+
hash.delete("path")
|
24
|
+
self.class.nilable_properties.each { |k| hash[k] = nil if hash[k].empty? }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def save
|
29
|
+
return self unless path
|
30
|
+
File.open(path, "w+") { |file| file.write(YAML.dump(to_yaml)) }
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def reload
|
35
|
+
self.class.from_file(path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.from_file(file_path)
|
39
|
+
new(read_config(file_path).merge("path" => file_path))
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.read_config(yaml_file)
|
43
|
+
content = File.read(yaml_file)
|
44
|
+
YAML.load(ERB.new(content).result).tap do |hash|
|
45
|
+
nilable_properties.each do |key|
|
46
|
+
hash.delete(key) if hash[key].nil?
|
47
|
+
end if hash
|
48
|
+
end || {}
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def self.nilable_properties
|
53
|
+
(properties - [:path]).map(&:to_s)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Soloist
|
4
|
+
class NotFound < RuntimeError; end
|
5
|
+
|
6
|
+
class Spotlight
|
7
|
+
attr_reader :pathname
|
8
|
+
|
9
|
+
def self.find(*file_names)
|
10
|
+
new(Dir.pwd).find(*file_names)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.find!(*file_names)
|
14
|
+
new(Dir.pwd).find!(*file_names)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(path)
|
18
|
+
@pathname = Pathname.new(path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def find(*file_names)
|
22
|
+
pathname.ascend do |path|
|
23
|
+
file_name = file_names.detect { |fn| path.join(fn).file? }
|
24
|
+
break path.join(file_name) if file_name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def find!(*file_names)
|
29
|
+
file_path = find(*file_names)
|
30
|
+
unless file_path
|
31
|
+
file_names = if file_names.length > 2
|
32
|
+
file_names[0...-1].join(", ") + " or " + file_names.last
|
33
|
+
else
|
34
|
+
file_names.join(" or ")
|
35
|
+
end
|
36
|
+
raise Soloist::NotFound.new("Could not find #{file_names}")
|
37
|
+
end
|
38
|
+
file_path
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/script/bootstrap.sh
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
echo "Updating package list"
|
6
|
+
sudo apt-get update > /dev/null
|
7
|
+
|
8
|
+
echo "Ensuring curl is available"
|
9
|
+
sudo apt-get install -y curl > /dev/null
|
10
|
+
|
11
|
+
echo "Setting up RVM"
|
12
|
+
|
13
|
+
user=$1
|
14
|
+
[ -z "$user" ] && user="ubuntu"
|
15
|
+
|
16
|
+
test -d /usr/local/rvm || curl -L https://get.rvm.io | sudo bash -s stable
|
17
|
+
|
18
|
+
test -e /usr/local/rvm || sudo tee /etc/profile.d/rvm.sh > /dev/null <<RVMSH_CONTENT
|
19
|
+
[[ -s "/usr/local/rvm/scripts/rvm" ]] && source "/usr/local/rvm/scripts/rvm"
|
20
|
+
RVMSH_CONTENT
|
21
|
+
|
22
|
+
test -x /usr/local/rvm || sudo chmod +x /etc/profile.d/rvm.sh
|
23
|
+
|
24
|
+
grep "^$user:" /etc/passwd > /dev/null || sudo useradd -m $user -G sudo,rvm,admin -s /bin/bash
|
25
|
+
|
26
|
+
test -e /etc/rvmrc || sudo tee /etc/rvmrc > /dev/null <<RVMRC_CONTENTS
|
27
|
+
rvm_install_on_use_flag=1
|
28
|
+
rvm_trust_rvmrcs_flag=1
|
29
|
+
rvm_gemset_create_on_use_flag=1
|
30
|
+
RVMRC_CONTENTS
|
31
|
+
|
32
|
+
echo "Detecting RVM requirements"
|
33
|
+
|
34
|
+
bash -lc 'rvm requirements' | tee /tmp/rvm-requirements > /dev/null
|
35
|
+
packages=`grep " ruby: /usr/bin/apt-get install" /tmp/rvm-requirements | sed "s/ ruby: \/usr\/bin\/apt-get install //g"`
|
36
|
+
|
37
|
+
echo "Detected RVM requirements: $packages"
|
38
|
+
|
39
|
+
selections=`dpkg --get-selections`
|
40
|
+
for package in $packages
|
41
|
+
do
|
42
|
+
if ! echo "$selections" | grep "^$package\s" > /dev/null
|
43
|
+
then
|
44
|
+
to_install="$to_install $package"
|
45
|
+
fi
|
46
|
+
done
|
47
|
+
|
48
|
+
if [ -z "$to_install" ]
|
49
|
+
then
|
50
|
+
echo "Satisfied RVM requirements"
|
51
|
+
else
|
52
|
+
echo "Installing missing RVM requirements: $to_install"
|
53
|
+
sudo apt-get install -y $to_install
|
54
|
+
fi
|
data/script/ci.sh
ADDED
data/soloist-rvm.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "soloist/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "soloist-rvm"
|
7
|
+
s.version = Soloist::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Austin Putman"]
|
10
|
+
s.email = ["austin@rawfingertips.com"]
|
11
|
+
s.homepage = "http://github.com/austinfromboston/soloist"
|
12
|
+
s.summary = "Soloist-rvm is a way of running chef-solo under rvm"
|
13
|
+
s.description = "Makes running chef-solo possible."
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency "chef"
|
21
|
+
s.add_dependency "librarian"
|
22
|
+
s.add_dependency "thor"
|
23
|
+
s.add_dependency "hashie", "~> 1.2"
|
24
|
+
s.add_dependency "net-ssh"
|
25
|
+
s.add_dependency "awesome_print"
|
26
|
+
|
27
|
+
s.add_development_dependency "rspec"
|
28
|
+
s.add_development_dependency "guard-rspec"
|
29
|
+
s.add_development_dependency "guard-bundler"
|
30
|
+
s.add_development_dependency "guard-shell"
|
31
|
+
s.add_development_dependency "rb-fsevent"
|
32
|
+
s.add_development_dependency "terminal-notifier-guard"
|
33
|
+
s.add_development_dependency "gem-release"
|
34
|
+
end
|