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.
@@ -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