chef-workflow 0.1.0
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.
- data/.gitignore +20 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +161 -0
- data/Rakefile +8 -0
- data/bin/chef-workflow-bootstrap +149 -0
- data/chef-workflow.gemspec +27 -0
- data/lib/chef-workflow.rb +73 -0
- data/lib/chef-workflow/support/attr.rb +31 -0
- data/lib/chef-workflow/support/debug.rb +51 -0
- data/lib/chef-workflow/support/ec2.rb +170 -0
- data/lib/chef-workflow/support/general.rb +63 -0
- data/lib/chef-workflow/support/generic.rb +30 -0
- data/lib/chef-workflow/support/ip.rb +129 -0
- data/lib/chef-workflow/support/knife-plugin.rb +33 -0
- data/lib/chef-workflow/support/knife.rb +122 -0
- data/lib/chef-workflow/support/scheduler.rb +403 -0
- data/lib/chef-workflow/support/vagrant.rb +41 -0
- data/lib/chef-workflow/support/vm.rb +71 -0
- data/lib/chef-workflow/support/vm/chef_server.rb +31 -0
- data/lib/chef-workflow/support/vm/ec2.rb +146 -0
- data/lib/chef-workflow/support/vm/knife.rb +217 -0
- data/lib/chef-workflow/support/vm/vagrant.rb +95 -0
- data/lib/chef-workflow/version.rb +6 -0
- metadata +167 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'chef-workflow/support/generic'
|
3
|
+
|
4
|
+
#
|
5
|
+
# Vagrant configuration settings. Uses `GenericSupport`.
|
6
|
+
#
|
7
|
+
class VagrantSupport
|
8
|
+
# The default vagrant box we use for provisioning.
|
9
|
+
DEFAULT_VAGRANT_BOX = "http://files.vagrantup.com/precise32.box"
|
10
|
+
|
11
|
+
# the calculated box, currently taken from the box_url. Expect this to change.
|
12
|
+
attr_reader :box
|
13
|
+
|
14
|
+
# FIXME: support non-url boxes and ram configurations
|
15
|
+
def initialize(box_url=DEFAULT_VAGRANT_BOX)
|
16
|
+
self.box_url = box_url
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Set or retrieve the box_url. See #box_url=.
|
21
|
+
#
|
22
|
+
def box_url(arg=nil)
|
23
|
+
if arg
|
24
|
+
self.box_url = arg
|
25
|
+
end
|
26
|
+
|
27
|
+
@box_url
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Set the box_url. The box name is derived from the url currently.
|
32
|
+
#
|
33
|
+
def box_url=(url)
|
34
|
+
@box_url = url
|
35
|
+
@box = File.basename(url).gsub('\.box', '')
|
36
|
+
end
|
37
|
+
|
38
|
+
include GenericSupport
|
39
|
+
end
|
40
|
+
|
41
|
+
VagrantSupport.configure
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'chef-workflow/support/general'
|
4
|
+
require 'chef-workflow/support/attr'
|
5
|
+
require 'chef-workflow/support/debug'
|
6
|
+
|
7
|
+
#--
|
8
|
+
# XXX see the dynamic require at the bottom
|
9
|
+
#++
|
10
|
+
|
11
|
+
#
|
12
|
+
# This class mainly exists to track the run state of the Scheduler, and is kept
|
13
|
+
# simple so that the contents can be marshalled and restored from a file.
|
14
|
+
#
|
15
|
+
class VM
|
16
|
+
class << self
|
17
|
+
extend AttrSupport
|
18
|
+
fancy_attr :vm_file
|
19
|
+
end
|
20
|
+
|
21
|
+
include DebugSupport
|
22
|
+
extend AttrSupport
|
23
|
+
|
24
|
+
#
|
25
|
+
# If a file exists that contains a VM object, load it. Use VM.vm_file to
|
26
|
+
# control the location of this file.
|
27
|
+
#
|
28
|
+
def self.load_from_file
|
29
|
+
vm_file = GeneralSupport.singleton.vm_file
|
30
|
+
|
31
|
+
if File.file?(vm_file)
|
32
|
+
return Marshal.load(File.binread(vm_file || DEFAULT_VM_FILE))
|
33
|
+
end
|
34
|
+
|
35
|
+
return nil
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Save the marshalled representation to a file. Use VM.vm_file to control the
|
40
|
+
# location of this file.
|
41
|
+
#
|
42
|
+
def save_to_file
|
43
|
+
vm_file = GeneralSupport.singleton.vm_file
|
44
|
+
marshalled = Marshal.dump(self)
|
45
|
+
FileUtils.mkdir_p(File.dirname(vm_file))
|
46
|
+
File.binwrite(vm_file, marshalled)
|
47
|
+
end
|
48
|
+
|
49
|
+
# the vm groups and their provisioning lists.
|
50
|
+
attr_reader :groups
|
51
|
+
# the dependencies that each vm group depends on
|
52
|
+
attr_reader :dependencies
|
53
|
+
# the set of provisioned (solved) groups
|
54
|
+
attr_reader :provisioned
|
55
|
+
# the set of provisioning (working) groups
|
56
|
+
attr_reader :working
|
57
|
+
|
58
|
+
def clean
|
59
|
+
@groups = { }
|
60
|
+
@dependencies = { }
|
61
|
+
@provisioned = Set.new
|
62
|
+
@working = Set.new
|
63
|
+
end
|
64
|
+
|
65
|
+
alias initialize clean
|
66
|
+
end
|
67
|
+
|
68
|
+
# XXX require all the provisioners -- marshal will blow up unless this is done.
|
69
|
+
Dir[File.join(File.expand_path(File.dirname(__FILE__)), 'vm', '*')].each do |x|
|
70
|
+
require x
|
71
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'chef-workflow/support/knife'
|
2
|
+
require 'chef-workflow/support/knife-plugin'
|
3
|
+
require 'chef/knife/server_bootstrap_standalone'
|
4
|
+
|
5
|
+
class VM
|
6
|
+
class ChefServerProvisioner
|
7
|
+
include DebugSupport
|
8
|
+
include KnifePluginSupport
|
9
|
+
|
10
|
+
attr_accessor :name
|
11
|
+
|
12
|
+
def startup(*args)
|
13
|
+
ip = args.first.first #arg
|
14
|
+
|
15
|
+
raise "No IP to use for the chef server" unless ip
|
16
|
+
|
17
|
+
args = %W[--node-name test-chef-server --host #{ip}]
|
18
|
+
|
19
|
+
args += %W[--ssh-user #{KnifeSupport.singleton.ssh_user}] if KnifeSupport.singleton.ssh_user
|
20
|
+
args += %W[--ssh-password #{KnifeSupport.singleton.ssh_password}] if KnifeSupport.singleton.ssh_password
|
21
|
+
args += %W[--identity-file #{KnifeSupport.singleton.ssh_identity_file}] if KnifeSupport.singleton.ssh_identity_file
|
22
|
+
|
23
|
+
init_knife_plugin(Chef::Knife::ServerBootstrapStandalone, args).run
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def shutdown
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'chef-workflow/support/ec2'
|
2
|
+
require 'chef-workflow/support/ip'
|
3
|
+
require 'chef-workflow/support/debug'
|
4
|
+
require 'net/ssh'
|
5
|
+
require 'timeout'
|
6
|
+
|
7
|
+
class VM
|
8
|
+
class EC2Provisioner
|
9
|
+
include DebugSupport
|
10
|
+
|
11
|
+
attr_accessor :name
|
12
|
+
|
13
|
+
def initialize(name, number_of_servers)
|
14
|
+
@name = name
|
15
|
+
@number_of_servers = number_of_servers
|
16
|
+
@instance_ids = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def ssh_connection_check(ip)
|
20
|
+
Net::SSH.start(ip, KnifeSupport.singleton.ssh_user, { :keys => [KnifeSupport.singleton.ssh_identity_file] }) do |ssh|
|
21
|
+
ssh.open_channel do |ch|
|
22
|
+
ch.on_open_failed do
|
23
|
+
return false
|
24
|
+
end
|
25
|
+
|
26
|
+
ch.exec("exit 0") do
|
27
|
+
return true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
rescue
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
def ec2
|
36
|
+
EC2Support.singleton
|
37
|
+
end
|
38
|
+
|
39
|
+
def startup(*args)
|
40
|
+
aws_ec2 = ec2.ec2_obj
|
41
|
+
|
42
|
+
ec2.assert_security_groups
|
43
|
+
|
44
|
+
instances = aws_ec2.instances.create(
|
45
|
+
:count => @number_of_servers,
|
46
|
+
:image_id => ec2.ami,
|
47
|
+
:security_groups => ec2.security_groups,
|
48
|
+
:key_name => ec2.ssh_key,
|
49
|
+
:instance_type => ec2.instance_type
|
50
|
+
)
|
51
|
+
|
52
|
+
#
|
53
|
+
# instances isn't actually an array above -- see this url:
|
54
|
+
#
|
55
|
+
# https://github.com/aws/aws-sdk-ruby/issues/100
|
56
|
+
#
|
57
|
+
# Actually make it a real array here so it's useful.
|
58
|
+
#
|
59
|
+
|
60
|
+
if instances.kind_of?(Array)
|
61
|
+
new_instances = []
|
62
|
+
|
63
|
+
instances.each do |instance|
|
64
|
+
new_instances.push(instance)
|
65
|
+
end
|
66
|
+
|
67
|
+
instances = new_instances
|
68
|
+
else
|
69
|
+
instances = [instances]
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
# There are instances where AWS won't acknowledge a created instance
|
74
|
+
# right away. Let's make sure the API server knows they all exist before
|
75
|
+
# moving forward.
|
76
|
+
#
|
77
|
+
|
78
|
+
unresolved_instances = instances.dup
|
79
|
+
|
80
|
+
until unresolved_instances.empty?
|
81
|
+
instance = unresolved_instances.shift
|
82
|
+
|
83
|
+
unless (instance.status rescue nil)
|
84
|
+
if_debug(3) do
|
85
|
+
$stderr.puts "API server doesn't think #{instance.id} exists yet."
|
86
|
+
end
|
87
|
+
|
88
|
+
sleep 0.3
|
89
|
+
unresolved_instances.push(instance)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
ip_addresses = []
|
94
|
+
|
95
|
+
instances.each do |instance|
|
96
|
+
@instance_ids.push(instance.id)
|
97
|
+
end
|
98
|
+
|
99
|
+
begin
|
100
|
+
Timeout.timeout(ec2.provision_wait) do
|
101
|
+
until instances.empty?
|
102
|
+
instance = instances.shift
|
103
|
+
|
104
|
+
ready = false
|
105
|
+
|
106
|
+
if instance.status == :running
|
107
|
+
ready = ssh_connection_check(instance.ip_address)
|
108
|
+
unless ready
|
109
|
+
if_debug(3) do
|
110
|
+
$stderr.puts "Instance #{instance.id} running, but ssh isn't up yet."
|
111
|
+
end
|
112
|
+
end
|
113
|
+
else
|
114
|
+
if_debug(3) do
|
115
|
+
$stderr.puts "#{instance.id} isn't running yet -- scheduling for re-check"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if ready
|
120
|
+
ip_addresses.push(instance.ip_address)
|
121
|
+
IPSupport.singleton.assign_role_ip(name, instance.ip_address)
|
122
|
+
else
|
123
|
+
sleep 0.3
|
124
|
+
instances.push(instance)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
rescue TimeoutError
|
129
|
+
raise "instances timed out waiting for ec2"
|
130
|
+
end
|
131
|
+
|
132
|
+
return ip_addresses
|
133
|
+
end
|
134
|
+
|
135
|
+
def shutdown
|
136
|
+
aws_ec2 = ec2.ec2_obj
|
137
|
+
|
138
|
+
@instance_ids.each do |instance_id|
|
139
|
+
aws_ec2.instances[instance_id].terminate
|
140
|
+
end
|
141
|
+
|
142
|
+
IPSupport.singleton.delete_role(name)
|
143
|
+
return true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'chef-workflow/support/debug'
|
2
|
+
require 'chef-workflow/support/knife'
|
3
|
+
require 'chef-workflow/support/knife-plugin'
|
4
|
+
require 'chef/node'
|
5
|
+
require 'chef/search/query'
|
6
|
+
require 'chef/knife/bootstrap'
|
7
|
+
require 'chef/knife/client_delete'
|
8
|
+
require 'chef/knife/node_delete'
|
9
|
+
require 'timeout'
|
10
|
+
|
11
|
+
class VM
|
12
|
+
#
|
13
|
+
# The Knife Provisioner does three major things:
|
14
|
+
#
|
15
|
+
# * Bootstraps a series of machines living on IP addresses supplied to it
|
16
|
+
# * Ensures that they converged successfully (if not, raises and displays output)
|
17
|
+
# * Waits until chef has indexed their metadata
|
18
|
+
#
|
19
|
+
# On deprovision, it deletes the nodes and clients related to this server group.
|
20
|
+
#
|
21
|
+
# Machines are named as such: $server_group-$number, where $number starts at 0
|
22
|
+
# and increases with the number of servers requested. Your node names will be
|
23
|
+
# named this as well as the clients associated with them.
|
24
|
+
#
|
25
|
+
# It does as much of this as it can in parallel, but stalls the current thread
|
26
|
+
# until the subthreads complete. This allows is to work as quickly as possible
|
27
|
+
# in a 'serial' scheduling scenario as we know bootstrapping can always occur
|
28
|
+
# in parallel for the group.
|
29
|
+
#
|
30
|
+
class KnifeProvisioner
|
31
|
+
|
32
|
+
include DebugSupport
|
33
|
+
include KnifePluginSupport
|
34
|
+
|
35
|
+
# the username for SSH.
|
36
|
+
attr_accessor :username
|
37
|
+
# the password for SSH.
|
38
|
+
attr_accessor :password
|
39
|
+
# drive knife bootstrap's sudo functionality.
|
40
|
+
attr_accessor :use_sudo
|
41
|
+
# the ssh key to be used for SSH
|
42
|
+
attr_accessor :ssh_key
|
43
|
+
# the bootstrap template to be used.
|
44
|
+
attr_accessor :template_file
|
45
|
+
# the chef environment to be used.
|
46
|
+
attr_accessor :environment
|
47
|
+
# the port to contact for SSH
|
48
|
+
attr_accessor :port
|
49
|
+
# the list of IPs to provision.
|
50
|
+
attr_accessor :ips
|
51
|
+
# the run list of this server group.
|
52
|
+
attr_accessor :run_list
|
53
|
+
# the name of this server group.
|
54
|
+
attr_accessor :name
|
55
|
+
# perform the solr check to ensure the instance has converged and its
|
56
|
+
# metadata is ready for searching.
|
57
|
+
attr_accessor :solr_check
|
58
|
+
|
59
|
+
# constructor.
|
60
|
+
def initialize
|
61
|
+
@ips = []
|
62
|
+
@username = nil
|
63
|
+
@password = nil
|
64
|
+
@ssh_key = nil
|
65
|
+
@port = nil
|
66
|
+
@use_sudo = nil
|
67
|
+
@run_list = nil
|
68
|
+
@template_file = nil
|
69
|
+
@environment = nil
|
70
|
+
@node_names = []
|
71
|
+
@solr_check = true
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Runs the provisioner. Accepts an array of IP addresses as its first
|
76
|
+
# argument, intended to be provided by provisioners that ran before it as
|
77
|
+
# their return value.
|
78
|
+
#
|
79
|
+
# Will raise if the IPs are not supplied or the provisioner is not named with
|
80
|
+
# a server group.
|
81
|
+
#
|
82
|
+
def startup(*args)
|
83
|
+
@ips = args.first #argh
|
84
|
+
raise "This provisioner is unnamed, cannot continue" unless name
|
85
|
+
raise "This provisioner requires ip addresses which were not supplied" unless ips
|
86
|
+
|
87
|
+
@run_list ||= ["role[#{name}]"]
|
88
|
+
|
89
|
+
t = []
|
90
|
+
ips.each_with_index do |ip, index|
|
91
|
+
node_name = "#{name}-#{index}"
|
92
|
+
@node_names.push(node_name)
|
93
|
+
t.push bootstrap(node_name, ip)
|
94
|
+
end
|
95
|
+
|
96
|
+
t.each(&:join)
|
97
|
+
|
98
|
+
return solr_check ? check_nodes : true
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# Deprovisions the server group. Runs node delete and client delete on all
|
103
|
+
# nodes that were created by this provisioner.
|
104
|
+
#
|
105
|
+
def shutdown
|
106
|
+
t = []
|
107
|
+
|
108
|
+
@node_names.each do |node_name|
|
109
|
+
t.push(
|
110
|
+
Thread.new do
|
111
|
+
client_delete(node_name)
|
112
|
+
node_delete(node_name)
|
113
|
+
end
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
117
|
+
t.each(&:join)
|
118
|
+
|
119
|
+
return true
|
120
|
+
end
|
121
|
+
|
122
|
+
#
|
123
|
+
# Checks that the nodes have made it into the search index. Will block until
|
124
|
+
# all nodes in this server group are found, or a 60 second timeout is
|
125
|
+
# reached, at which point it will raise.
|
126
|
+
#
|
127
|
+
def check_nodes
|
128
|
+
q = Chef::Search::Query.new
|
129
|
+
unchecked_node_names = @node_names.dup
|
130
|
+
|
131
|
+
# this dirty hack turns 'role[foo]' into 'roles:foo', but also works on
|
132
|
+
# recipe[] too. Then joins the whole thing with AND
|
133
|
+
search_query = run_list.
|
134
|
+
map { |s| s.gsub(/\[/, 's:"').gsub(/\]/, '"') }.
|
135
|
+
join(" AND ")
|
136
|
+
|
137
|
+
Timeout.timeout(KnifeSupport.singleton.search_index_wait) do
|
138
|
+
until unchecked_node_names.empty?
|
139
|
+
node_name = unchecked_node_names.shift
|
140
|
+
if_debug(3) do
|
141
|
+
$stderr.puts "Checking search validity for node #{node_name}"
|
142
|
+
end
|
143
|
+
|
144
|
+
result = q.search(
|
145
|
+
:node,
|
146
|
+
search_query + %Q[ AND name:"#{node_name}"]
|
147
|
+
).first
|
148
|
+
|
149
|
+
unless result and result.count == 1 and result.first.name == node_name
|
150
|
+
unchecked_node_names << node_name
|
151
|
+
end
|
152
|
+
|
153
|
+
# unfortunately if this isn't here you might as well issue kill -9 to
|
154
|
+
# the rake process
|
155
|
+
sleep 0.3
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
return true
|
160
|
+
rescue Timeout::Error
|
161
|
+
raise "Bootstrapped nodes for #{name} did not appear in Chef search index after 60 seconds."
|
162
|
+
end
|
163
|
+
|
164
|
+
#
|
165
|
+
# Bootstraps a single node. Validates bootstrap by checking the node metadata
|
166
|
+
# directly and ensuring it made it into the chef server.
|
167
|
+
#
|
168
|
+
def bootstrap(node_name, ip)
|
169
|
+
args = []
|
170
|
+
|
171
|
+
args += %W[-x #{username}] if username
|
172
|
+
args += %W[-P #{password}] if password
|
173
|
+
args += %w[--sudo] if use_sudo
|
174
|
+
args += %W[-i #{ssh_key}] if ssh_key
|
175
|
+
args += %W[--template-file #{template_file}] if template_file
|
176
|
+
args += %W[-p #{port}] if port
|
177
|
+
args += %W[-E #{environment}] if environment
|
178
|
+
|
179
|
+
args += %W[-r #{run_list.join(",")}]
|
180
|
+
args += %W[-N '#{node_name}']
|
181
|
+
args += [ip]
|
182
|
+
|
183
|
+
bootstrap_cli = init_knife_plugin(Chef::Knife::Bootstrap, args)
|
184
|
+
|
185
|
+
Thread.new do
|
186
|
+
bootstrap_cli.run
|
187
|
+
# knife bootstrap is the honey badger when it comes to exit status.
|
188
|
+
# We can't rely on it, so we examine the run_list of the node instead
|
189
|
+
# to ensure it converged.
|
190
|
+
run_list_size = Chef::Node.load(node_name).run_list.to_a.size
|
191
|
+
unless run_list_size > 0
|
192
|
+
puts bootstrap_cli.ui.stdout.string
|
193
|
+
puts bootstrap_cli.ui.stderr.string
|
194
|
+
raise "bootstrap for #{node_name}/#{ip} wasn't successful."
|
195
|
+
end
|
196
|
+
if_debug(2) do
|
197
|
+
puts bootstrap_cli.ui.stdout.string
|
198
|
+
puts bootstrap_cli.ui.stderr.string
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
#
|
204
|
+
# Deletes a chef client.
|
205
|
+
#
|
206
|
+
def client_delete(node_name)
|
207
|
+
init_knife_plugin(Chef::Knife::ClientDelete, [node_name, '-y']).run
|
208
|
+
end
|
209
|
+
|
210
|
+
#
|
211
|
+
# Deletes a chef node.
|
212
|
+
#
|
213
|
+
def node_delete(node_name)
|
214
|
+
init_knife_plugin(Chef::Knife::NodeDelete, [node_name, '-y']).run
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|