testlab 0.2.1 → 0.3.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.
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