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