chef-workflow 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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