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,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
|