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