testlab 0.0.1 → 0.0.2

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