testlab 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -17,4 +17,3 @@ test/version_tmp
17
17
  tmp
18
18
  *log
19
19
  Vagrantfile
20
- testlab/
data/Gemfile CHANGED
@@ -1,5 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'coveralls', :require => false
4
-
5
3
  gemspec
data/bin/tl CHANGED
@@ -3,39 +3,15 @@ require 'gli'
3
3
  require 'testlab'
4
4
 
5
5
  include GLI::App
6
+ include TestLab::Utility::Misc
6
7
 
7
- program_desc %(TestLab #{TestLab::VERSION} - A framework for building lightweight virtual infrastructure using LXC)
8
+ version TestLab::VERSION
8
9
 
9
- version Testlab::VERSION
10
+ program_desc %(A framework for building lightweight virtual infrastructure using LXC)
11
+ program_long_desc %(Program Long Description)
10
12
 
11
- # desc 'Describe some switch here'
12
- # switch [:s,:switch]
13
-
14
- # desc 'Describe some flag here'
15
- # default_value 'the default'
16
- # arg_name 'The name of the argument'
17
- # flag [:f,:flagname]
18
-
19
- # desc 'Manage the test lab'
20
- # arg_name 'Describe arguments to lab here'
21
- # command :lab do |c|
22
-
23
- # # c.desc 'Describe a switch to lab'
24
- # # c.switch :s
25
-
26
- # # c.desc 'Describe a flag to lab'
27
- # # c.default_value 'default'
28
- # # c.flag :f
29
- # # c.action do |global_options,options,args|
30
-
31
- # # # Your command logic here
32
-
33
- # # # If you have any errors, just raise them
34
- # # # raise "that command made no sense"
35
-
36
- # # puts "lab command ran"
37
- # # end
38
- # end
13
+ sort_help :manually
14
+ default_command :help
39
15
 
40
16
  desc 'Create the test lab'
41
17
  command :create do |create|
@@ -90,7 +66,8 @@ desc 'Manage nodes'
90
66
  arg_name 'Describe arguments to node here'
91
67
  command :node do |c|
92
68
 
93
- c.desc 'Node ID'
69
+ c.desc 'Node ID or Name'
70
+ c.arg_name 'node'
94
71
  c.flag [:i, :id]
95
72
 
96
73
  c.desc 'Open an SSH console to a node'
@@ -117,7 +94,8 @@ desc 'Manage containers'
117
94
  arg_name 'Describe arguments to container here'
118
95
  command :container do |c|
119
96
 
120
- c.desc 'Container ID'
97
+ c.desc 'Container ID or Name'
98
+ c.arg_name 'container'
121
99
  c.flag [:i, :id]
122
100
 
123
101
  c.desc 'Open an SSH console to a container'
@@ -170,7 +148,7 @@ pre do |global,command,options,args|
170
148
  @ui = ZTK::UI.new(:logger => @logger)
171
149
  @testlab = TestLab.new(:ui => @ui)
172
150
 
173
- message = TestLab::Utility.format_message("TestLab v#{TestLab::VERSION} Loaded".black.bold)
151
+ message = format_message("TestLab v#{TestLab::VERSION} Loaded".black.bold)
174
152
  @testlab.ui.stdout.puts(message)
175
153
 
176
154
  true
@@ -183,16 +161,25 @@ post do |global,command,options,args|
183
161
  end
184
162
 
185
163
  on_error do |exception|
186
- # Error logic here
187
- # return false to skip default error handling
188
- @ui.stderr.puts(["EXCEPTION:".red.bold, exception.inspect.red].join(' '))
164
+ @ui.stderr.puts
165
+ @ui.stderr.puts(format_message(["ERROR:".red, exception.message.red.bold].join(' ')))
189
166
 
190
- @logger.fatal { exception.inspect }
191
- exception.backtrace.each do |line|
192
- @logger.logdev.write("#{line}\n")
193
- end
167
+ case exception
168
+ when GLI::BadCommandLine, GLI::UnknownCommand, GLI::UnknownCommandArgument, GLI::UnknownGlobalArgument then
169
+ command_regex = /Command '([\w]+)' /
170
+ command = exception.message.scan(command_regex).flatten.first
171
+
172
+ commands[:help] and commands[:help].execute({}, {}, (command.nil? ? [] : [command.to_s]))
194
173
 
195
- false
174
+ false
175
+ else
176
+ @logger.fatal { exception.inspect }
177
+ exception.backtrace.each do |line|
178
+ @logger.logdev.write("#{line}\n")
179
+ end
180
+
181
+ false
182
+ end
196
183
  end
197
184
 
198
185
  exit run(ARGV)
@@ -4,6 +4,11 @@ class TestLab
4
4
  module Actions
5
5
 
6
6
  # Create the container
7
+ #
8
+ # Builds the configuration for the container and sends a request to the
9
+ # LXC sub-system to create the container.
10
+ #
11
+ # @return [Boolean] True if successful.
7
12
  def create
8
13
  @ui.logger.debug { "Container Create: #{self.id} " }
9
14
 
@@ -22,33 +27,63 @@ class TestLab
22
27
 
23
28
  self.lxc.create(*create_args)
24
29
  end
30
+
31
+ true
25
32
  end
26
33
 
27
34
  # Destroy the container
35
+ #
36
+ # Sends a request to the LXC sub-system to destroy the container.
37
+ #
38
+ # @return [Boolean] True if successful.
28
39
  def destroy
29
40
  @ui.logger.debug { "Container Destroy: #{self.id} " }
30
41
 
31
42
  please_wait(:ui => @ui, :message => format_object_action(self, 'Destroy', :red)) do
32
43
  self.lxc.destroy
33
44
  end
45
+
46
+ true
34
47
  end
35
48
 
36
49
  # Start the container
50
+ #
51
+ # Sends a request to the LXC sub-system to bring the container online.
52
+ #
53
+ # @return [Boolean] True if successful.
37
54
  def up
38
55
  @ui.logger.debug { "Container Up: #{self.id} " }
39
56
 
57
+ (self.lxc.state == :not_created) and raise ContainerError, "We can not online a non-existant container!"
58
+
40
59
  please_wait(:ui => @ui, :message => format_object_action(self, 'Up', :green)) do
41
60
  self.lxc.start
61
+ self.lxc.wait(:running)
62
+
63
+ (self.lxc.state != :running) and raise ContainerError, "The container failed to online!"
42
64
  end
65
+
66
+ true
43
67
  end
44
68
 
45
69
  # Stop the container
70
+ #
71
+ # Sends a request to the LXC sub-system to take the container offline.
72
+ #
73
+ # @return [Boolean] True if successful.
46
74
  def down
47
75
  @ui.logger.debug { "Container Down: #{self.id} " }
48
76
 
77
+ (self.lxc.state == :not_created) and raise ContainerError, "We can not offline a non-existant container!"
78
+
49
79
  please_wait(:ui => @ui, :message => format_object_action(self, 'Down', :red)) do
50
80
  self.lxc.stop
81
+ self.lxc.wait(:stopped)
82
+
83
+ (self.lxc.state != :stopped) and raise ContainerError, "The container failed to offline!"
51
84
  end
85
+
86
+ true
52
87
  end
53
88
 
54
89
  end
@@ -3,11 +3,17 @@ class TestLab
3
3
 
4
4
  module ClassMethods
5
5
 
6
+ # Container domain list
7
+ #
8
+ # Returns an array of strings containing all the unique domains defined
9
+ # across all containers
10
+ #
11
+ # @return [Array<String>] A unique array of all defined domain names.
6
12
  def domains
7
13
  self.all.map do |container|
8
14
  container.domain ||= container.node.labfile.config[:domain]
9
15
  container.domain
10
- end.compact
16
+ end.compact.uniq
11
17
  end
12
18
 
13
19
  end
@@ -3,6 +3,12 @@ class TestLab
3
3
 
4
4
  module Generators
5
5
 
6
+ # Generate IP address
7
+ #
8
+ # Generates an RFC compliant private IP address.
9
+ #
10
+ # @return [String] A random, private IP address in the 192.168.0.1/24
11
+ # range.
6
12
  def generate_ip
7
13
  octets = [ 192..192,
8
14
  168..168,
@@ -15,6 +21,11 @@ class TestLab
15
21
  ip.join(".")
16
22
  end
17
23
 
24
+ # Generate MAC address
25
+ #
26
+ # Generates an RFC compliant private MAC address.
27
+ #
28
+ # @return [String] A random, private MAC address.
18
29
  def generate_mac
19
30
  digits = [ %w(0),
20
31
  %w(0),
@@ -3,24 +3,17 @@ class TestLab
3
3
 
4
4
  module Interface
5
5
 
6
- # Returns the IP of the container
7
- def ip
8
- TestLab::Utility.ip(self.primary_interface.last[:ip])
9
- end
10
-
11
- # Returns the CIDR of the container
12
- def cidr
13
- TestLab::Utility.cidr(self.primary_interface.last[:ip]).to_i
14
- end
15
-
16
- # Returns a BIND PTR record
17
- def ptr
18
- TestLab::Utility.ptr(self.primary_interface.last[:ip])
19
- end
20
-
6
+ # Container primary interface
7
+ #
8
+ # Returns the primary interface for the container. If the container has
9
+ # multiple interfaces, this is based on which ever interface is marked
10
+ # with the primary flag. If the container only has one interface, then
11
+ # it is returned.
12
+ #
13
+ # @return [TestLab::Interface] The primary interface for the container.
21
14
  def primary_interface
22
- if self.interfaces.any?{ |i,c| c[:primary] == true }
23
- self.interfaces.find{ |i,c| c[:primary] == true }
15
+ if self.interfaces.any?{ |i| i.primary == true }
16
+ self.interfaces.find{ |i| i.primary == true }
24
17
  else
25
18
  self.interfaces.first
26
19
  end
@@ -3,7 +3,13 @@ class TestLab
3
3
 
4
4
  module Lifecycle
5
5
 
6
- # Container Setup
6
+ # Setup the container
7
+ #
8
+ # Attempts to setup the container. We first create the container, then
9
+ # attempt to bring it online. Afterwards the containers provisioner is
10
+ # called.
11
+ #
12
+ # @return [Boolean] True if successful.
7
13
  def setup
8
14
  @ui.logger.debug { "Container Setup: #{self.id} " }
9
15
 
@@ -19,7 +25,13 @@ class TestLab
19
25
  true
20
26
  end
21
27
 
22
- # Container Teardown
28
+ # Teardown the container
29
+ #
30
+ # Attempts to teardown the container. We first call the provisioner
31
+ # teardown method defined for the container, if any. Next we attempt to
32
+ # offline the container. Afterwards the container is destroy.
33
+ #
34
+ # @return [Boolean] True if successful.
23
35
  def teardown
24
36
  @ui.logger.debug { "Container Teardown: #{self.id} " }
25
37
 
@@ -3,7 +3,9 @@ class TestLab
3
3
 
4
4
  module LXC
5
5
 
6
- # Our LXC Container class
6
+ # LXC::Container object
7
+ #
8
+ # Returns a *LXC::Container* class instance configured for this container.
7
9
  #
8
10
  # @return [LXC] An instance of LXC::Container configured for this
9
11
  # container.
@@ -11,12 +13,19 @@ class TestLab
11
13
  @lxc ||= self.node.lxc.container(self.id)
12
14
  end
13
15
 
14
- # SSH to the container
16
+ # ZTK:SSH object
17
+ #
18
+ # Returns a *ZTK:SSH* class instance configured for this container.
19
+ #
20
+ # @return [ZTK::SSH] An instance of ZTK::SSH configured for this
21
+ # container.
15
22
  def ssh(options={})
16
23
  self.node.container_ssh(self, options)
17
24
  end
18
25
 
19
26
  # Does the container exist?
27
+ #
28
+ # @return [Boolean] True if the containers exists, false otherwise.
20
29
  def exists?
21
30
  @ui.logger.debug { "Container Exists?: #{self.id} " }
22
31
 
@@ -25,7 +34,7 @@ class TestLab
25
34
 
26
35
  # Returns arguments for lxc-create based on our distro
27
36
  #
28
- # @return [Array] An array of arguments for lxc-create
37
+ # @return [Array<String>] An array of arguments for lxc-create
29
38
  def create_args
30
39
  case self.distro.downcase
31
40
  when "ubuntu" then
@@ -49,21 +58,26 @@ class TestLab
49
58
  end
50
59
  end
51
60
 
61
+ # LXC Network Configuration
62
+ #
52
63
  # Builds an array of hashes containing the lxc configuration options for
53
- # our networks
64
+ # our network interfaces.
65
+ #
66
+ # @return [Array<Hash>] An array of hashes defining the containers
67
+ # interfaces for use in configuring LXC.
54
68
  def build_lxc_network_conf(interfaces)
55
69
  networks = Array.new
56
70
 
57
- interfaces.each do |network, network_config|
71
+ interfaces.each do |interface|
58
72
  networks << Hash[
59
73
  'lxc.network.type' => :veth,
60
74
  'lxc.network.flags' => :up,
61
- 'lxc.network.link' => TestLab::Network.first(network).bridge,
62
- 'lxc.network.name' => network_config[:name],
63
- 'lxc.network.hwaddr' => network_config[:mac],
64
- 'lxc.network.ipv4' => network_config[:ip]
75
+ 'lxc.network.link' => interface.network.bridge,
76
+ 'lxc.network.name' => interface.name,
77
+ 'lxc.network.hwaddr' => interface.mac,
78
+ 'lxc.network.ipv4' => "#{interface.ip}/#{interface.cidr} #{interface.netmask}"
65
79
  ]
66
- if (network_config[:primary] == true) || (interfaces.count == 1)
80
+ if (interface.primary == true) || (interfaces.count == 1)
67
81
  networks.last.merge!('lxc.network.ipv4.gateway' => :auto)
68
82
  end
69
83
  end
@@ -3,7 +3,9 @@ class TestLab
3
3
 
4
4
  module MethodMissing
5
5
 
6
- # Method missing handler
6
+ # Provisioner method handler
7
+ #
8
+ # Proxies missing methods to the containers defined provisioner, if any.
7
9
  def method_missing(method_name, *method_args)
8
10
  @ui.logger.debug { "CONTAINER METHOD MISSING: #{method_name.inspect}(#{method_args.inspect})" }
9
11
 
@@ -3,14 +3,54 @@ class TestLab
3
3
 
4
4
  module Status
5
5
 
6
+ # Container IP
7
+ #
8
+ # Returns the IP of the container.
9
+ #
10
+ # @return [String] The containers IP address.
11
+ def ip
12
+ TestLab::Utility.ip(self.primary_interface.address)
13
+ end
14
+
15
+ # Container CIDR
16
+ #
17
+ # Returns the CIDR of the container
18
+ #
19
+ # @return [Integer] The containers CIDR address.
20
+ def cidr
21
+ TestLab::Utility.cidr(self.primary_interface.address).to_i
22
+ end
23
+
24
+ # Container BIND PTR Record
25
+ #
26
+ # Returns a BIND reverse-DNS PTR record.
27
+ #
28
+ # @return [String] The containers ARPA PTR record.
29
+ def ptr
30
+ TestLab::Utility.ptr(self.primary_interface.address)
31
+ end
32
+
33
+ # Container FQDN
34
+ #
35
+ # Returns the FQDN for the container.
36
+ #
37
+ # @return [String] The containers FQDN.
6
38
  def fqdn
7
39
  self.domain ||= self.node.labfile.config[:domain]
8
40
 
9
41
  [self.id, self.domain].join('.')
10
42
  end
11
43
 
44
+ # Container Status
45
+ #
46
+ # Returns a hash of status information for the container.
47
+ #
48
+ # @return [Hash] A hash of status information for the container.
12
49
  def status
13
- interfaces = self.interfaces.collect{ |network, network_config| "#{network}:#{network_config[:name]}:#{network_config[:ip]}" }.join(', ')
50
+ interfaces = self.interfaces.collect do |interface|
51
+ "#{interface.network_id}:#{interface.name}:#{interface.ip}/#{interface.cidr}"
52
+ end.join(', ')
53
+
14
54
  {
15
55
  :id => self.id,
16
56
  :fqdn => self.fqdn,
@@ -23,7 +63,11 @@ class TestLab
23
63
  }
24
64
  end
25
65
 
26
- # State of the container
66
+ # Container State
67
+ #
68
+ # What state the container is in.
69
+ #
70
+ # @return [Symbol] A symbol indicating the state of the container.
27
71
  def state
28
72
  self.lxc.state
29
73
  end
@@ -5,8 +5,59 @@ class TestLab
5
5
 
6
6
  # Container Class
7
7
  #
8
+ # This class represents the TestLab Container DSL object.
9
+ #
10
+ # @example A simple container definition with a single interface:
11
+ # container "server-west-1" do
12
+ # domain "west.zone"
13
+ #
14
+ # distro "ubuntu"
15
+ # release "precise"
16
+ #
17
+ # interface do
18
+ # network_id :west
19
+ # name :eth0
20
+ # address '10.11.0.254/16'
21
+ # mac '00:00:5e:48:e9:6f'
22
+ # end
23
+ # end
24
+ #
25
+ # @example Multiple interfaces can be defined as well:
26
+ # container "dual-nic" do
27
+ # distro "ubuntu"
28
+ # release "precise"
29
+ #
30
+ # interface do
31
+ # network_id :east
32
+ # name :eth0
33
+ # address '10.10.0.200/16'
34
+ # mac '00:00:5e:63:b5:9f'
35
+ # end
36
+ #
37
+ # interface do
38
+ # network_id :west
39
+ # primary true
40
+ # name :eth1
41
+ # address '10.11.0.200/16'
42
+ # mac '00:00:5e:08:63:df'
43
+ # end
44
+ # end
45
+ #
46
+ # The operating system is determined by the *distro* and *release* attributes.
47
+ # The hostname (container ID) is passed as a parameter to the container call.
48
+ # A *domain* may additionally be specified (overriding the global domain, if
49
+ # set). If the *domain* attributes is omited, then the global domain is use,
50
+ # again only if it is set. The hostname (container ID) and the domain will be
51
+ # joined together to form the FQDN of the container.
52
+ #
53
+ # @see TestLab::Interface
54
+ #
8
55
  # @author Zachary Patten <zachary@jovelabs.net>
9
56
  class Container < ZTK::DSL::Base
57
+
58
+ # An array of symbols of the various keys in our status hash.
59
+ #
60
+ # @see TestLab::Container::Status
10
61
  STATUS_KEYS = %w(node_id id fqdn state distro release interfaces provisioner).map(&:to_sym)
11
62
 
12
63
  # Sub-Modules
@@ -33,6 +84,7 @@ class TestLab
33
84
 
34
85
  # Associations and Attributes
35
86
  belongs_to :node, :class_name => 'TestLab::Node'
87
+ has_many :interfaces, :class_name => 'TestLab::Interface'
36
88
 
37
89
  attribute :provisioner
38
90
  attribute :config
@@ -43,8 +95,6 @@ class TestLab
43
95
  attribute :passwd
44
96
  attribute :keys
45
97
 
46
- attribute :interfaces
47
-
48
98
  attribute :distro
49
99
  attribute :release
50
100
  attribute :arch
@@ -0,0 +1,45 @@
1
+ class TestLab
2
+
3
+ # Interface Error Class
4
+ class InterfaceError < TestLabError; end
5
+
6
+ # Interface Class
7
+ #
8
+ # @author Zachary Patten <zachary@jovelabs.net>
9
+ class Interface < ZTK::DSL::Base
10
+
11
+ # Associations and Attributes
12
+ belongs_to :container, :class_name => 'TestLab::Container'
13
+ belongs_to :network, :class_name => 'TestLab::Network'
14
+
15
+ attribute :address
16
+ attribute :mac
17
+ attribute :name
18
+
19
+ attribute :primary
20
+
21
+ def initialize(*args)
22
+ super(*args)
23
+
24
+ @ui = TestLab.ui
25
+ end
26
+
27
+ def ip
28
+ TestLab::Utility.ip(self.address)
29
+ end
30
+
31
+ def cidr
32
+ TestLab::Utility.cidr(self.address)
33
+ end
34
+
35
+ def netmask
36
+ TestLab::Utility.netmask(self.address)
37
+ end
38
+
39
+ def ptr
40
+ TestLab::Utility.ptr(self.address)
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,4 @@
1
+ # Monkey Patch the String class so we can have some easy ANSI methods
2
+ class String
3
+ include ZTK::ANSI
4
+ end
@@ -26,7 +26,7 @@ class TestLab
26
26
  @ui.logger.debug { "Network Up: #{self.id} " }
27
27
 
28
28
  please_wait(:ui => @ui, :message => format_object_action(self, 'Up', :green)) do
29
- self.node.ssh.exec(%(sudo ifconfig #{self.bridge} #{self.ip} up), :silence => true, :ignore_exit_status => true)
29
+ self.node.ssh.exec(%(sudo ifconfig #{self.bridge} #{self.ip} netmask #{self.netmask} up), :silence => true, :ignore_exit_status => true)
30
30
  end
31
31
  end
32
32
 
@@ -5,12 +5,12 @@ class TestLab
5
5
 
6
6
  # BIND PTR Record
7
7
  def ptr
8
- TestLab::Utility.ptr(self.ip)
8
+ TestLab::Utility.ptr(self.address)
9
9
  end
10
10
 
11
11
  # Returns the ARPA network
12
12
  def arpa
13
- TestLab::Utility.arpa(self.ip)
13
+ TestLab::Utility.arpa(self.address)
14
14
  end
15
15
 
16
16
  end
@@ -4,7 +4,7 @@ class TestLab
4
4
  module ClassMethods
5
5
 
6
6
  def ips
7
- self.all.map(&:ip).collect{ |ip| TestLab::Utility.ip(ip) }.compact
7
+ self.all.map(&:address).collect{ |address| TestLab::Utility.ip(address) }.compact
8
8
  end
9
9
 
10
10
  end
@@ -5,7 +5,7 @@ class TestLab
5
5
 
6
6
  # Network status
7
7
  def status
8
- interface = "#{bridge}:#{ip}"
8
+ interface = "#{bridge}:#{self.address}"
9
9
  {
10
10
  :id => self.id,
11
11
  :node_id => self.node.id,
@@ -17,19 +17,27 @@ class TestLab
17
17
  }
18
18
  end
19
19
 
20
+ def ip
21
+ TestLab::Utility.ip(self.address)
22
+ end
23
+
24
+ def cidr
25
+ TestLab::Utility.cidr(self.address)
26
+ end
27
+
20
28
  # Returns the network mask
21
29
  def netmask
22
- TestLab::Utility.netmask(self.ip)
30
+ TestLab::Utility.netmask(self.address)
23
31
  end
24
32
 
25
33
  # Returns the network address
26
34
  def network
27
- TestLab::Utility.network(self.ip)
35
+ TestLab::Utility.network(self.address)
28
36
  end
29
37
 
30
38
  # Returns the broadcast address
31
39
  def broadcast
32
- TestLab::Utility.broadcast(self.ip)
40
+ TestLab::Utility.broadcast(self.address)
33
41
  end
34
42
 
35
43
  # Network Bridge State
@@ -27,10 +27,11 @@ class TestLab
27
27
 
28
28
  # Associations and Attributes
29
29
  belongs_to :node, :class_name => 'TestLab::Node'
30
+ has_many :interfaces, :class_name => 'TestLab::Interface'
30
31
 
32
+ attribute :address
31
33
  attribute :bridge
32
34
 
33
- attribute :ip
34
35
  attribute :config
35
36
 
36
37