testlab 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,127 @@
1
+ class TestLab
2
+
3
+ # Network Error Class
4
+ class NetworkError < TestLabError; end
5
+
6
+ # Network Class
7
+ #
8
+ # @author Zachary Patten <zachary@jovelabs.net>
9
+ class Network < ZTK::DSL::Base
10
+ STATUS_KEYS = %w(node_id id state interface).map(&:to_sym)
11
+
12
+ belongs_to :node, :class_name => 'TestLab::Node'
13
+
14
+ attribute :bridge
15
+
16
+ attribute :cidr
17
+ attribute :config
18
+
19
+ def initialize(*args)
20
+ super(*args)
21
+
22
+ @ui = TestLab.ui
23
+ end
24
+
25
+ # Network status
26
+ def status
27
+ interface = "#{bridge}:#{cidr}"
28
+ {
29
+ :id => self.id,
30
+ :node_id => self.node.id,
31
+ :state => self.state,
32
+ :interface => interface
33
+ }
34
+ end
35
+
36
+ ################################################################################
37
+
38
+ # Create the network
39
+ def create
40
+ @ui.logger.debug { "Network Create: #{self.id} " }
41
+
42
+ self.node.ssh.exec(%(sudo brctl addbr #{self.bridge}), :silence => true, :ignore_exit_status => true)
43
+ self.node.ssh.exec(%(sudo ifconfig #{self.bridge} #{self.cidr} down), :silence => true, :ignore_exit_status => true)
44
+ end
45
+
46
+ # Destroy the network
47
+ def destroy
48
+ @ui.logger.debug { "Network Destroy: #{self.id} " }
49
+
50
+ self.node.ssh.exec(%(sudo brctl delbr #{self.bridge}), :silence => true, :ignore_exit_status => true)
51
+ end
52
+
53
+ # Start the network
54
+ def up
55
+ @ui.logger.debug { "Network Up: #{self.id} " }
56
+
57
+ self.node.ssh.exec(%(sudo ifconfig #{self.bridge} up), :silence => true, :ignore_exit_status => true)
58
+ end
59
+
60
+ # Stop the network
61
+ def down
62
+ @ui.logger.debug { "Network Down: #{self.id} " }
63
+
64
+ self.node.ssh.exec(%(sudo ifconfig #{self.bridge} down), :silence => true, :ignore_exit_status => true)
65
+ end
66
+
67
+ ################################################################################
68
+
69
+ # Reload the network
70
+ def reload
71
+ @ui.logger.debug { "Network Reload: #{self.id} " }
72
+
73
+ self.down
74
+ self.up
75
+ end
76
+
77
+ ################################################################################
78
+
79
+ # State of the network
80
+ def state
81
+ output = self.node.ssh.exec(%(sudo ifconfig #{self.bridge} | grep 'MTU'), :silence => true, :ignore_exit_status => true).output.strip
82
+ if ((output =~ /UP/) && (output =~ /RUNNING/))
83
+ :running
84
+ else
85
+ :stopped
86
+ end
87
+ end
88
+
89
+ ################################################################################
90
+
91
+ # Network Callback: after_create
92
+ def after_create
93
+ @ui.logger.debug { "Network Callback: After Create: #{self.id} " }
94
+ end
95
+
96
+ # Network Callback: after_up
97
+ def after_up
98
+ @ui.logger.debug { "Network Callback: After Up: #{self.id} " }
99
+
100
+ self.create
101
+ self.up
102
+ end
103
+
104
+ # Network Callback: before_down
105
+ def before_down
106
+ @ui.logger.debug { "Network Callback: Before Down: #{self.id} " }
107
+
108
+ self.down
109
+ self.destroy
110
+ end
111
+
112
+ # Network Callback: before_destroy
113
+ def before_destroy
114
+ @ui.logger.debug { "Network Callback: Before Destroy: #{self.id} " }
115
+ end
116
+
117
+ ################################################################################
118
+
119
+ # Method missing handler
120
+ def method_missing(method_name, *method_args)
121
+ @ui.logger.debug { "NETWORK METHOD MISSING: #{method_name.inspect}(#{method_args.inspect})" }
122
+ super(method_name, *method_args)
123
+ end
124
+
125
+ end
126
+
127
+ end
@@ -0,0 +1,148 @@
1
+ require 'lxc'
2
+
3
+ class TestLab
4
+
5
+ # Node Error Class
6
+ class NodeError < TestLabError; end
7
+
8
+ # Node Class
9
+ #
10
+ # @author Zachary Patten <zachary@jovelabs.net>
11
+ class Node < ZTK::DSL::Base
12
+ STATUS_KEYS = %w(id instance_id state user ip port provider con net rtr).map(&:to_sym)
13
+
14
+ belongs_to :labfile, :class_name => 'TestLab::Lab'
15
+
16
+ has_many :routers, :class_name => 'TestLab::Router'
17
+ has_many :containers, :class_name => 'TestLab::Container'
18
+ has_many :networks, :class_name => 'TestLab::Network'
19
+
20
+ attribute :provider
21
+ attribute :config
22
+
23
+ def initialize(*args)
24
+ super(*args)
25
+
26
+ @ui = TestLab.ui
27
+ @provider = self.provider.new(self.config)
28
+ end
29
+
30
+ def status
31
+ {
32
+ :instance_id => @provider.instance_id,
33
+ :state => @provider.state,
34
+ :user => @provider.user,
35
+ :ip => @provider.ip,
36
+ :port => @provider.port,
37
+ :provider => @provider.class,
38
+ :con => self.containers.count,
39
+ :net => self.networks.count,
40
+ :rtr => self.routers.count
41
+ }
42
+ end
43
+
44
+ # SSH to the Node
45
+ def ssh(options={})
46
+ if (!defined?(@ssh) || @ssh.nil?)
47
+ @ssh ||= ZTK::SSH.new({:ui => @ui, :timeout => 1200, :silence => true}.merge(options))
48
+ @ssh.config do |c|
49
+ c.host_name = @provider.ip
50
+ c.user = @provider.user
51
+ c.keys = @provider.identity
52
+ end
53
+ end
54
+ @ssh
55
+ end
56
+
57
+ # SSH to a container running on the Node
58
+ def ssh_container(id, options={})
59
+ end
60
+
61
+ # Returns the LXC object for this Node
62
+ #
63
+ # This object is used to control containers on the node via it's provider
64
+ def lxc(options={})
65
+ if (!defined?(@lxc) || @lxc.nil?)
66
+ @lxc ||= LXC.new
67
+ @lxc.use_sudo = true
68
+ @lxc.use_ssh = self.ssh
69
+ end
70
+ @lxc
71
+ end
72
+
73
+ def arch
74
+ @arch ||= self.ssh.exec(%(uname -m)).output.strip
75
+ end
76
+
77
+ ################################################################################
78
+
79
+ # Callback: After Create
80
+ # Ensure our packages are installed.
81
+ def after_create
82
+ self.ssh.exec(%(sudo apt-get -qq -y --force-yes update))
83
+ self.ssh.exec(%(sudo apt-get -qq -y --force-yes install lxc bridge-utils debootstrap yum isc-dhcp-server bind9 ntpdate ntp))
84
+ end
85
+
86
+ # Callback: After Up
87
+ def after_up
88
+ end
89
+
90
+ ################################################################################
91
+
92
+ # Provides a generic interface for triggering our callback framework
93
+ def proxy_callbacks(action, objects, method_name, *method_args)
94
+ callback_method = "#{action}_#{method_name}".to_sym
95
+
96
+ objects.each do |object|
97
+ if object.respond_to?(callback_method)
98
+ object.send(callback_method, *method_args)
99
+ end
100
+ end
101
+ end
102
+
103
+ # Provides a generic interface for triggering our callback framework
104
+ def self_callbacks(action, method_name, *method_args)
105
+ callback_method = "#{action}_#{method_name}".to_sym
106
+
107
+ if self.respond_to?(callback_method)
108
+ self.send(callback_method, *method_args)
109
+ end
110
+ end
111
+
112
+ # Method missing handler
113
+ def method_missing(method_name, *method_args)
114
+ @ui.logger.debug { "NODE METHOD MISSING: #{method_name.inspect}(#{method_args.inspect})" }
115
+
116
+ if TestLab::Provider::PROXY_METHODS.include?(method_name)
117
+ result = nil
118
+ object_collections = [self.containers, self.routers, self.networks]
119
+
120
+ self_callbacks(:before, method_name, *method_args)
121
+
122
+ object_collections.each do |object_collection|
123
+ proxy_callbacks(:before, object_collection, method_name, *method_args)
124
+ end
125
+
126
+ @ui.logger.debug { "method_name == #{method_name.inspect}" }
127
+ if @provider.respond_to?(method_name)
128
+ @ui.logger.debug { "@provider.send(#{method_name.inspect}, #{method_args.inspect})" }
129
+ result = @provider.send(method_name, *method_args)
130
+ else
131
+ raise TestLab::ProviderError, "Your provider does not respond to the method '#{method_name}'!"
132
+ end
133
+
134
+ self_callbacks(:after, method_name, *method_args)
135
+
136
+ object_collections.reverse.each do |object_collection|
137
+ proxy_callbacks(:after, object_collection, method_name, *method_args)
138
+ end
139
+
140
+ result
141
+ else
142
+ super(method_name, *method_args)
143
+ end
144
+ end
145
+
146
+ end
147
+
148
+ end
@@ -0,0 +1,27 @@
1
+ class TestLab
2
+
3
+ # Provider Error Class
4
+ class ProviderError < TestLabError; end
5
+
6
+ # Provider Class
7
+ #
8
+ # @author Zachary Patten <zachary@jovelabs.net>
9
+ class Provider
10
+ PROXY_METHODS = %w(instance_id state user ip port create destroy up down reload status alive? dead? exists?).map(&:to_sym)
11
+
12
+ autoload :AWS, 'testlab/providers/aws'
13
+ autoload :Local, 'testlab/providers/local'
14
+ autoload :Vagrant, 'testlab/providers/vagrant'
15
+
16
+ class << self
17
+
18
+ # Returns the path to the gems provider templates
19
+ def template_dir
20
+ File.join(TestLab.gem_dir, "lib", "testlab", "providers", "templates")
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,20 @@
1
+ class TestLab
2
+
3
+ class Provider
4
+
5
+ # AWS Provider Error Class
6
+ class AWSError < ProviderError; end
7
+
8
+ # AWS Provider Class
9
+ #
10
+ # @author Zachary Patten <zachary@jovelabs.net>
11
+ class AWS
12
+
13
+ def initialize(ui=ZTK::UI.new)
14
+ @ui = ui
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ class TestLab
2
+
3
+ class Provider
4
+
5
+ # Local Provider Error Class
6
+ class LocalError < ProviderError; end
7
+
8
+ # Local Provider Class
9
+ #
10
+ # @author Zachary Patten <zachary@jovelabs.net>
11
+ class Local
12
+
13
+ def initialize(ui=ZTK::UI.new)
14
+ @ui = ui
15
+ end
16
+
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ #!/bin/env ruby
2
+ #
3
+ # Auto-generated by TestLab v<%= TestLab::VERSION %> on <%= Time.now.utc %> -- DO NOT EDIT!
4
+ #
5
+ # Vagrant v2 Configuration
6
+ ###########################
7
+
8
+ Vagrant.configure("2") do |config|
9
+ config.vm.define <%= @id.inspect %> do |testlab|
10
+ testlab.vm.hostname = <%= @hostname.inspect %>
11
+ testlab.vm.box = <%= @box.inspect %>
12
+ testlab.vm.box_url = <%= @box_url.inspect %>
13
+ testlab.vm.network(:private_network, :ip => <%= @ip.inspect %>)
14
+
15
+ testlab.vm.provider :virtualbox do |vb|
16
+ vb.name = <%= @hostname.inspect %>
17
+ vb.customize(["modifyvm", :id, "--cpus", <%= @cpus.inspect %>])
18
+ vb.customize(["modifyvm", :id, "--memory", <%= @memory.inspect %>])
19
+ end
20
+
21
+ testlab.ssh.username = <%= @user.inspect %>
22
+ testlab.ssh.port = <%= @port.inspect %>
23
+ testlab.ssh.forward_agent = true
24
+ end
25
+ end
@@ -0,0 +1,204 @@
1
+ class TestLab
2
+
3
+ class Provider
4
+
5
+ # Vagrant Provider Error Class
6
+ class VagrantError < ProviderError; end
7
+
8
+ # Vagrant Provider Class
9
+ #
10
+ # @author Zachary Patten <zachary@jovelabs.net>
11
+ class Vagrant
12
+
13
+ # States which indicate the VM is running
14
+ RUNNING_STATES = %w(running).map(&:to_sym)
15
+
16
+ # States which indicate the VM is shut down
17
+ SHUTDOWN_STATES = %w(aborted paused saved poweroff).map(&:to_sym)
18
+
19
+ # The state we report if we can not determine the VM state
20
+ UNKNOWN_STATE = :unknown
21
+
22
+ # A collection of all valid states the VM can be in
23
+ VALID_STATES = (RUNNING_STATES + SHUTDOWN_STATES).flatten
24
+
25
+ # A collection of all invalid states the VM can be in
26
+ INVALID_STATES = (%w(not_created).map(&:to_sym) + [UNKNOWN_STATE]).flatten
27
+
28
+ # A collection of all states the VM can be in
29
+ ALL_STATES = (VALID_STATES + INVALID_STATES).flatten
30
+
31
+ MSG_NO_LAB = %(We could not find a test lab!)
32
+
33
+ ################################################################################
34
+
35
+ def initialize(config={}, ui=nil)
36
+ @config = config
37
+ @ui = (ui || TestLab.ui)
38
+
39
+ # ensure our vagrant key is there
40
+ @config[:vagrant] ||= Hash.new
41
+
42
+ render_vagrantfile
43
+ end
44
+
45
+ ################################################################################
46
+
47
+ # Create the Vagrant instance
48
+ def create
49
+ self.up
50
+ end
51
+
52
+ # Destroy Vagrant-controlled VM
53
+ def destroy
54
+ !self.exists? and raise VagrantError, MSG_NO_LAB
55
+
56
+ self.down
57
+ self.vagrant_cli("destroy", "--force", self.instance_id)
58
+
59
+ self.state
60
+ end
61
+
62
+ ################################################################################
63
+
64
+ # Online Vagrant-controlled VM
65
+ def up
66
+ self.vagrant_cli("up", self.instance_id)
67
+ ZTK::TCPSocketCheck.new(:host => self.ip, :port => self.port, :wait => 120, :ui => @ui).wait
68
+
69
+ self.state
70
+ end
71
+
72
+ # Halt Vagrant-controlled VM
73
+ def down
74
+ !self.exists? and raise VagrantError, MSG_NO_LAB
75
+
76
+ self.vagrant_cli("halt", self.instance_id)
77
+
78
+ self.state
79
+ end
80
+
81
+ ################################################################################
82
+
83
+ # Reload Vagrant-controlled VM
84
+ def reload
85
+ self.down
86
+ self.up
87
+
88
+ self.state
89
+ end
90
+
91
+ ################################################################################
92
+
93
+ # Inquire the state of the Vagrant-controlled VM
94
+ def state
95
+ output = self.vagrant_cli("status | grep '#{self.instance_id}'").output
96
+ result = UNKNOWN_STATE
97
+ ALL_STATES.map{ |s| s.to_s.gsub('_', ' ') }.each do |state|
98
+ if output =~ /#{state}/
99
+ result = state.to_s.gsub(' ', '_')
100
+ break
101
+ end
102
+ end
103
+ result.to_sym
104
+ end
105
+
106
+ ################################################################################
107
+
108
+ # Does the Vagrant-controlled VM exist?
109
+ def exists?
110
+ (self.state != :not_created)
111
+ end
112
+
113
+ # Is the Vagrant-controlled VM alive?
114
+ def alive?
115
+ (self.exists? && (RUNNING_STATES.include?(self.state) rescue false))
116
+ end
117
+
118
+ # Is the Vagrant-controlled VM dead?
119
+ def dead?
120
+ !self.alive?
121
+ end
122
+
123
+ # START CORE CONFIG
124
+ ####################
125
+
126
+ def instance_id
127
+ (@config[:vagrant][:id] || "testlab-#{ENV['USER']}".downcase)
128
+ end
129
+
130
+ def user
131
+ (@config[:vagrant][:user] || "vagrant")
132
+ end
133
+
134
+ def identity
135
+ (@config[:vagrant][:identity] || File.join(ENV['HOME'], ".vagrant.d", "insecure_private_key"))
136
+ end
137
+
138
+ def ip
139
+ (@config[:vagrant][:ip] || "192.168.33.10")
140
+ end
141
+
142
+ def port
143
+ (@config[:vagrant][:port] || 22)
144
+ end
145
+
146
+ ##################
147
+ # END CORE CONFIG
148
+
149
+ def hostname
150
+ (@config[:vagrant][:hostname] || self.instance_id)
151
+ end
152
+
153
+ def box
154
+ (@config[:vagrant][:box] || "precise64")
155
+ end
156
+
157
+ def box_url
158
+ (@config[:vagrant][:box_url] || "http://files.vagrantup.com/precise64.box")
159
+ end
160
+
161
+ def cpus
162
+ (@config[:vagrant][:cpus] || 2)
163
+ end
164
+
165
+ def memory
166
+ (@config[:vagrant][:memory] || 2048)
167
+ end
168
+
169
+ ################################################################################
170
+
171
+ def vagrant_cli(*args)
172
+ @ui.logger.debug { "args == #{args.inspect}" }
173
+
174
+ command = TestLab.build_command("vagrant", *args)
175
+ @ui.logger.debug { "command == #{command.inspect}" }
176
+
177
+ ZTK::Command.new(:ui => @ui).exec(command, :silence => true)
178
+ end
179
+
180
+ def render_vagrantfile
181
+ context = {
182
+ :id => self.instance_id,
183
+ :ip => self.ip,
184
+ :hostname => self.hostname,
185
+ :user => self.user,
186
+ :port => self.port,
187
+ :cpus => self.cpus,
188
+ :memory => self.memory,
189
+ :box => self.box,
190
+ :box_url => self.box_url
191
+ }
192
+
193
+ vagrantfile_template = File.join(TestLab::Provider.template_dir, "vagrant", "Vagrantfile.erb")
194
+ vagrantfile = File.join(@config[:repo], "Vagrantfile")
195
+ IO.write(vagrantfile, ZTK::Template.render(vagrantfile_template, context))
196
+ end
197
+
198
+ ################################################################################
199
+
200
+
201
+ end
202
+
203
+ end
204
+ end
@@ -0,0 +1,25 @@
1
+ class TestLab
2
+
3
+ # Provisioner Error Class
4
+ class ProvisionerError < TestLabError; end
5
+
6
+ # Provisioner Class
7
+ #
8
+ # @author Zachary Patten <zachary@jovelabs.net>
9
+ class Provisioner
10
+
11
+ autoload :Shell, 'testlab/provisioners/shell'
12
+ autoload :Chef, 'testlab/provisioners/chef'
13
+
14
+ class << self
15
+
16
+ # Returns the path to the gems provisioner templates
17
+ def template_dir
18
+ File.join(TestLab.gem_dir, "lib", "testlab", "provisioners", "templates")
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,21 @@
1
+ class TestLab
2
+
3
+ class Provisioner
4
+
5
+ # Chef Provisioner Error Class
6
+ class ChefError < ProvisionerError; end
7
+
8
+ # Chef Provisioner Class
9
+ #
10
+ # @author Zachary Patten <zachary@jovelabs.net>
11
+ class Chef
12
+
13
+ def initialize(config={}, ui=nil)
14
+ @config = config
15
+ @ui = (ui || TestLab.ui)
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ class TestLab
2
+
3
+ class Provisioner
4
+
5
+ # Shell Provisioner Error Class
6
+ class ShellError < ProvisionerError; end
7
+
8
+ # Shell Provisioner Class
9
+ #
10
+ # @author Zachary Patten <zachary@jovelabs.net>
11
+ class Shell
12
+
13
+ def initialize(config={}, ui=nil)
14
+ @config = config
15
+ @ui = (ui || TestLab.ui)
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end