chef-workflow 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,31 @@
|
|
1
|
+
#
|
2
|
+
# Mixin to make exposing attr modification via `instance_eval` easier.
|
3
|
+
#
|
4
|
+
module AttrSupport
|
5
|
+
#
|
6
|
+
# Defines an attribute that is both a standard writer, but with an overloaded
|
7
|
+
# reader that accepts an optional argument. Equivalent to this code for `foo`:
|
8
|
+
#
|
9
|
+
# attr_writer :foo
|
10
|
+
#
|
11
|
+
# def foo(*args)
|
12
|
+
# if args.count > 0
|
13
|
+
# @foo = arg
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @foo
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
def fancy_attr(name)
|
20
|
+
class_eval <<-EOF
|
21
|
+
attr_writer :#{name}
|
22
|
+
def #{name}(*args)
|
23
|
+
if args.count > 0
|
24
|
+
@#{name} = args.first
|
25
|
+
end
|
26
|
+
|
27
|
+
@#{name}
|
28
|
+
end
|
29
|
+
EOF
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#
|
2
|
+
# mixin to assist with adding debug messages.
|
3
|
+
#
|
4
|
+
module DebugSupport
|
5
|
+
|
6
|
+
CHEF_WORKFLOW_DEBUG_DEFAULT = 2
|
7
|
+
|
8
|
+
#
|
9
|
+
# Conditionally executes based on the level of debugging requested.
|
10
|
+
#
|
11
|
+
# `CHEF_WORKFLOW_DEBUG` in the environment is converted to an integer. This
|
12
|
+
# integer is compared to the first argument. If it is higher than the first
|
13
|
+
# argument, the block supplied will execute.
|
14
|
+
#
|
15
|
+
# Optionally, if there is a `else_block`, this block will be executed if the
|
16
|
+
# condition is *not* met. This allows a slightly more elegant (if less ugly)
|
17
|
+
# variant of dealing with the situation where if debugging is on, do one
|
18
|
+
# thing, and if not, do something else.
|
19
|
+
#
|
20
|
+
# Examples:
|
21
|
+
#
|
22
|
+
# if_debug(1) do
|
23
|
+
# $stderr.puts "Here's a debug message"
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# This will print "here's a debug message" to standard error if debugging is
|
27
|
+
# set to 1 or greater.
|
28
|
+
#
|
29
|
+
# do_thing = lambda { run_thing }
|
30
|
+
# if_debug(2, &do_thing) do
|
31
|
+
# $stderr.puts "Doing this thing"
|
32
|
+
# do_thing.call
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# If debugging is set to 2 or higher, "Doing this thing" will be printed to
|
36
|
+
# standard error and then `run_thing` will be executed. If lower than 2 or
|
37
|
+
# off, will just execute `run_thing`.
|
38
|
+
#
|
39
|
+
def if_debug(minimum=1, else_block=nil)
|
40
|
+
$CHEF_WORKFLOW_DEBUG ||=
|
41
|
+
ENV.has_key?("CHEF_WORKFLOW_DEBUG") ?
|
42
|
+
ENV["CHEF_WORKFLOW_DEBUG"].to_i :
|
43
|
+
CHEF_WORKFLOW_DEBUG_DEFAULT
|
44
|
+
|
45
|
+
if $CHEF_WORKFLOW_DEBUG >= minimum
|
46
|
+
yield if block_given?
|
47
|
+
elsif else_block
|
48
|
+
else_block.call
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'aws'
|
2
|
+
require 'chef-workflow/support/generic'
|
3
|
+
require 'chef-workflow/support/general'
|
4
|
+
require 'chef-workflow/support/debug'
|
5
|
+
require 'chef-workflow/support/attr'
|
6
|
+
|
7
|
+
class EC2Support
|
8
|
+
extend AttrSupport
|
9
|
+
include DebugSupport
|
10
|
+
|
11
|
+
fancy_attr :access_key_id
|
12
|
+
fancy_attr :secret_access_key
|
13
|
+
fancy_attr :ami
|
14
|
+
fancy_attr :instance_type
|
15
|
+
fancy_attr :region
|
16
|
+
fancy_attr :ssh_key
|
17
|
+
fancy_attr :security_groups
|
18
|
+
fancy_attr :security_group_open_ports
|
19
|
+
fancy_attr :provision_wait
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
self.security_groups :auto
|
23
|
+
self.security_group_open_ports [22, 4000]
|
24
|
+
self.provision_wait 300
|
25
|
+
end
|
26
|
+
|
27
|
+
def ec2_obj
|
28
|
+
args =
|
29
|
+
if access_key_id and secret_access_key
|
30
|
+
{
|
31
|
+
:access_key_id => access_key_id,
|
32
|
+
:secret_access_key => secret_access_key
|
33
|
+
}
|
34
|
+
else
|
35
|
+
{
|
36
|
+
:access_key_id => ENV["AWS_ACCESS_KEY_ID"],
|
37
|
+
:secret_access_key => ENV["AWS_SECRET_ACCESS_KEY"]
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
ec2 = AWS::EC2.new(args)
|
42
|
+
ec2.regions[region]
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Only used if security_groups is set to auto. Returns the filename to
|
47
|
+
# marshal the automatically created security groups to.
|
48
|
+
#
|
49
|
+
def security_group_setting_path
|
50
|
+
File.join(GeneralSupport.singleton.workflow_dir, 'security-groups')
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Creates a security group and saves it to the security_group_setting_path.
|
55
|
+
#
|
56
|
+
def create_security_group
|
57
|
+
aws_ec2 = ec2_obj
|
58
|
+
|
59
|
+
name = nil
|
60
|
+
|
61
|
+
loop do
|
62
|
+
name = 'chef-workflow-' + (0..rand(10).to_i).map { rand(0..9).to_s }.join("")
|
63
|
+
|
64
|
+
if_debug(3) do
|
65
|
+
$stderr.puts "Seeing if security group name #{name} is taken"
|
66
|
+
end
|
67
|
+
|
68
|
+
break unless aws_ec2.security_groups[name].exists?
|
69
|
+
sleep 0.3
|
70
|
+
end
|
71
|
+
|
72
|
+
group = aws_ec2.security_groups.create(name)
|
73
|
+
|
74
|
+
security_group_open_ports.each do |port|
|
75
|
+
group.authorize_ingress(:tcp, port)
|
76
|
+
group.authorize_ingress(:udp, port)
|
77
|
+
end
|
78
|
+
|
79
|
+
group.authorize_ingress(:tcp, (0..65535), group)
|
80
|
+
group.authorize_ingress(:udp, (0..65535), group)
|
81
|
+
|
82
|
+
# XXX I think the name should be enough, but maybe this'll cause a problem.
|
83
|
+
File.binwrite(security_group_setting_path, Marshal.dump([name]))
|
84
|
+
return [name]
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Loads any stored security groups. Returns nil if it can't.
|
89
|
+
#
|
90
|
+
def load_security_group
|
91
|
+
Marshal.load(File.binread(security_group_setting_path)) rescue nil
|
92
|
+
end
|
93
|
+
|
94
|
+
#
|
95
|
+
# Ensures security groups exist.
|
96
|
+
#
|
97
|
+
# If @security_groups is :auto, creates one and sets it up with the
|
98
|
+
# security_group_open_ports on TCP and UDP.
|
99
|
+
#
|
100
|
+
# If @security_groups is an array of group names or a single group name,
|
101
|
+
# asserts they exist. If they do not exist, it raises.
|
102
|
+
#
|
103
|
+
def assert_security_groups
|
104
|
+
aws_ec2 = ec2_obj
|
105
|
+
|
106
|
+
if security_groups == :auto
|
107
|
+
loaded_groups = load_security_group
|
108
|
+
|
109
|
+
# this will make it hit the second block everytime from now on (and
|
110
|
+
# bootstrap it recursively)
|
111
|
+
if loaded_groups
|
112
|
+
self.security_groups loaded_groups
|
113
|
+
assert_security_groups
|
114
|
+
else
|
115
|
+
self.security_groups create_security_group
|
116
|
+
end
|
117
|
+
else
|
118
|
+
self.security_groups = [security_groups] unless security_groups.kind_of?(Array)
|
119
|
+
|
120
|
+
self.security_groups.each do |group|
|
121
|
+
#
|
122
|
+
# just retry this until it works -- some stupid flexible proxy in aws-sdk will bark about a missing method otherwise.
|
123
|
+
#
|
124
|
+
|
125
|
+
begin
|
126
|
+
aws_ec2.security_groups[group]
|
127
|
+
rescue
|
128
|
+
sleep 1
|
129
|
+
retry
|
130
|
+
end
|
131
|
+
|
132
|
+
raise "EC2 security group #{group} does not exist and it should." unless aws_ec2.security_groups[group]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def find_secgroup_running_instances(group_name)
|
138
|
+
# exponential complexity with API calls? NO PROBLEM
|
139
|
+
ec2_obj.instances.select do |i|
|
140
|
+
i.status != :terminated &&
|
141
|
+
i.security_groups.find { |s| s.name == group_name }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def destroy_security_group
|
146
|
+
if File.exist?(security_group_setting_path)
|
147
|
+
group_name = Marshal.load(File.binread(security_group_setting_path)).first
|
148
|
+
|
149
|
+
until (instances = find_secgroup_running_instances(group_name)).empty?
|
150
|
+
if_debug(1) do
|
151
|
+
$stderr.puts "Trying to destroy security group #{group_name}, but instances are still bound to it."
|
152
|
+
$stderr.puts instances.map(&:id).inspect
|
153
|
+
$stderr.puts "Terminating instances, sleeping, and trying again."
|
154
|
+
end
|
155
|
+
|
156
|
+
instances.each do |i|
|
157
|
+
i.terminate rescue nil
|
158
|
+
end
|
159
|
+
|
160
|
+
sleep 10
|
161
|
+
end
|
162
|
+
|
163
|
+
ec2_obj.security_groups.find { |g| g.name == group_name }.delete
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
include GenericSupport
|
168
|
+
end
|
169
|
+
|
170
|
+
EC2Support.configure
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'chef-workflow/support/generic'
|
2
|
+
require 'chef-workflow/support/attr'
|
3
|
+
require 'chef-workflow/support/vm/ec2'
|
4
|
+
require 'chef-workflow/support/vm/vagrant'
|
5
|
+
|
6
|
+
#
|
7
|
+
# General configuration, typically global to all chef-workflow related things.
|
8
|
+
# See `GenericSupport` for a rundown of usage.
|
9
|
+
#
|
10
|
+
class GeneralSupport
|
11
|
+
# Standard chef-workflow dir.
|
12
|
+
DEFAULT_CHEF_WORKFLOW_DIR = File.join(Dir.pwd, '.chef-workflow')
|
13
|
+
# Location of the VM database.
|
14
|
+
DEFAULT_CHEF_VM_FILE = File.join(DEFAULT_CHEF_WORKFLOW_DIR, 'vms')
|
15
|
+
# Location of the chef-server prison file (vagrant only).
|
16
|
+
DEFAULT_CHEF_SERVER_PRISON = File.join(DEFAULT_CHEF_WORKFLOW_DIR, 'chef-server')
|
17
|
+
|
18
|
+
extend AttrSupport
|
19
|
+
|
20
|
+
##
|
21
|
+
# :attr:
|
22
|
+
#
|
23
|
+
# configure the workflow directory
|
24
|
+
fancy_attr :workflow_dir
|
25
|
+
|
26
|
+
##
|
27
|
+
# :attr:
|
28
|
+
#
|
29
|
+
# configure the location of the vm file
|
30
|
+
fancy_attr :vm_file
|
31
|
+
|
32
|
+
##
|
33
|
+
# :attr:
|
34
|
+
#
|
35
|
+
# configure the location of the chef server prison.
|
36
|
+
fancy_attr :chef_server_prison
|
37
|
+
|
38
|
+
def initialize(opts={})
|
39
|
+
@workflow_dir = opts[:workflow_dir] || DEFAULT_CHEF_WORKFLOW_DIR
|
40
|
+
@vm_file = opts[:vm_file] || DEFAULT_CHEF_VM_FILE
|
41
|
+
@chef_server_prison = opts[:chef_server_prison] || DEFAULT_CHEF_SERVER_PRISON
|
42
|
+
machine_provisioner :vagrant
|
43
|
+
end
|
44
|
+
|
45
|
+
def machine_provisioner(*args)
|
46
|
+
if args.count > 0
|
47
|
+
@machine_provisioner = case args.first
|
48
|
+
when :ec2
|
49
|
+
VM::EC2Provisioner
|
50
|
+
when :vagrant
|
51
|
+
VM::VagrantProvisioner
|
52
|
+
else
|
53
|
+
args.first
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
@machine_provisioner
|
58
|
+
end
|
59
|
+
|
60
|
+
include GenericSupport
|
61
|
+
end
|
62
|
+
|
63
|
+
GeneralSupport.configure
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#
|
2
|
+
# mixin for supplying a consistent interface to singleton configuration classes.
|
3
|
+
#
|
4
|
+
|
5
|
+
module GenericSupport
|
6
|
+
#--
|
7
|
+
# it's cool; this isn't absolutely evil or anything.
|
8
|
+
#++
|
9
|
+
def self.included(klass)
|
10
|
+
class << klass
|
11
|
+
# The singleton object that is supplying the current configuration.
|
12
|
+
# Always reference this when working with classes that use this
|
13
|
+
# interface.
|
14
|
+
attr_reader :singleton
|
15
|
+
|
16
|
+
# circular references, oh my
|
17
|
+
attr_accessor :supported_class
|
18
|
+
|
19
|
+
#
|
20
|
+
# Configure the singleton. Instance evals a block that you can set stuff on.
|
21
|
+
#
|
22
|
+
def configure(&block)
|
23
|
+
@singleton ||= self.supported_class.new
|
24
|
+
@singleton.instance_eval(&block) if block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
klass.supported_class = klass
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'chef-workflow/support/generic'
|
4
|
+
require 'chef-workflow/support/attr'
|
5
|
+
|
6
|
+
ENV["TEST_CHEF_SUBNET"] ||= "10.10.10.0"
|
7
|
+
|
8
|
+
#
|
9
|
+
# IP allocation database. Uses `GenericSupport`.
|
10
|
+
#
|
11
|
+
class IPSupport < DelegateClass(Hash)
|
12
|
+
extend AttrSupport
|
13
|
+
|
14
|
+
##
|
15
|
+
# :attr:
|
16
|
+
#
|
17
|
+
# The subnet used for calculating assignable IP addresses. You really want to
|
18
|
+
# set `TEST_CHEF_SUBNET` in your environment instead of changing this.
|
19
|
+
#
|
20
|
+
fancy_attr :subnet
|
21
|
+
|
22
|
+
##
|
23
|
+
# :attr:
|
24
|
+
#
|
25
|
+
# The location of the ip database.
|
26
|
+
#
|
27
|
+
fancy_attr :ip_file
|
28
|
+
|
29
|
+
def initialize(subnet=ENV["TEST_CHEF_SUBNET"], ip_file=File.join(Dir.pwd, '.chef-workflow', 'ips'))
|
30
|
+
@subnet = subnet
|
31
|
+
reset
|
32
|
+
@ip_file = ip_file
|
33
|
+
super(@ip_assignment)
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Resets (clears) the IP database.
|
38
|
+
#
|
39
|
+
def reset
|
40
|
+
@ip_assignment = { }
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Loads the IP database from disk. Location is based on the `ip_file` accessor.
|
45
|
+
#
|
46
|
+
def load
|
47
|
+
if File.exist?(ip_file)
|
48
|
+
@ip_assignment = Marshal.load(File.binread(ip_file))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Saves the IP database to disk. Location is based on the `ip_file` accessor.
|
54
|
+
#
|
55
|
+
def write
|
56
|
+
FileUtils.mkdir_p(File.dirname(ip_file))
|
57
|
+
File.binwrite(ip_file, Marshal.dump(@ip_assignment))
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# Gets the next unallocated IP, given an IP to start with.
|
62
|
+
#
|
63
|
+
def next_ip(arg)
|
64
|
+
octets = arg.split(/\./, 4).map(&:to_i)
|
65
|
+
octets[3] += 1
|
66
|
+
raise "out of ips!" if octets[3] > 255
|
67
|
+
return octets.map(&:to_s).join(".")
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Gets the next un-used IP. This basically calls `next_ip` with knowledge of
|
72
|
+
# the database.
|
73
|
+
#
|
74
|
+
def unused_ip
|
75
|
+
ip = next_ip(@subnet)
|
76
|
+
|
77
|
+
while ip_used?(ip)
|
78
|
+
ip = next_ip(ip)
|
79
|
+
end
|
80
|
+
|
81
|
+
return ip
|
82
|
+
end
|
83
|
+
|
84
|
+
#
|
85
|
+
# Predicate to determine if an IP is in use.
|
86
|
+
#
|
87
|
+
def ip_used?(ip)
|
88
|
+
@ip_assignment.values.flatten.include?(ip)
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Appends an IP to a role.
|
93
|
+
#
|
94
|
+
def assign_role_ip(role, ip)
|
95
|
+
@ip_assignment[role] ||= []
|
96
|
+
@ip_assignment[role].push(ip)
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# Removes the role and all associated IPs.
|
101
|
+
#
|
102
|
+
def delete_role(role)
|
103
|
+
@ip_assignment.delete(role)
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Gets all the IPs for a role, as an array of strings.
|
108
|
+
#
|
109
|
+
def get_role_ips(role)
|
110
|
+
@ip_assignment[role] || []
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# Helper method for vagrant. Vagrant always occupies .1 of any subnet it
|
115
|
+
# configures host-only networking on. This takes care of doing that.
|
116
|
+
#
|
117
|
+
def seed_vagrant_ips
|
118
|
+
# vagrant requires that .1 be used by vagrant. ugh.
|
119
|
+
dot_one_ip = @subnet.gsub(/\.\d+$/, '.1')
|
120
|
+
unless ip_used?(dot_one_ip)
|
121
|
+
assign_role_ip("vagrant-reserved", dot_one_ip)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
include GenericSupport
|
126
|
+
end
|
127
|
+
|
128
|
+
IPSupport.configure
|
129
|
+
IPSupport.singleton.load
|