testlab 1.4.4 → 1.5.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.
- checksums.yaml +15 -0
- data/features/step_definitions/testlab_steps.rb +4 -0
- data/features/support/Labfile.vagrant +2 -0
- data/features/testlab.feature +9 -2
- data/lib/commands/container.rb +3 -7
- data/lib/commands/network.rb +12 -12
- data/lib/testlab/container/actions.rb +11 -14
- data/lib/testlab/container/clone.rb +54 -10
- data/lib/testlab/container/io.rb +4 -8
- data/lib/testlab/container/provision.rb +8 -10
- data/lib/testlab/container/status.rb +9 -5
- data/lib/testlab/network/actions.rb +4 -8
- data/lib/testlab/network/provision.rb +2 -4
- data/lib/testlab/network/status.rb +11 -7
- data/lib/testlab/node/actions.rb +0 -8
- data/lib/testlab/node/provision.rb +0 -4
- data/lib/testlab/providers/vagrant.rb +0 -6
- data/lib/testlab/version.rb +1 -1
- data/spec/container_spec.rb +28 -1
- data/spec/network_spec.rb +1 -0
- metadata +5 -31
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ODBlNjc1YWRlMDE2YjI5ZDY0YWE5ZTU5M2FhMWJkYWE2Y2FlZTgwYQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YTkwZTliNWZjNDE5YzgzN2U2YTY1ZTJkZWFiZWRjNDQ4OWU1MTQ0MQ==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
YmUxMDdhOWQ4ZmRiZjBiYWU1NDBhYzFjYjdhNDRkNTBiZmZmODljN2VmMmQ1
|
10
|
+
NWE5NTUwNDUyMjMyNjc3YjUwNmQxYjM4ZDM4NWM3ZTQ2YjRkZTA0M2ZlYWY0
|
11
|
+
NWEyY2IwYmM1MjQ3ZDA5YjhhMDc2Y2YxY2MyOGVmNzVlYTQ2M2Q=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MmQ4ZGUxZGQzMGFkM2MyNWZkYTliYzMzYTMyMGY4YjY4MjRhOWQxZTRhNzNk
|
14
|
+
YzE4YTc4ODliNGRjOWQzOWYyZDE1NWRkYzM2MzY5OTE1ZmZkYTUxMzk2ZjI3
|
15
|
+
NGIxNmRkNWEyNWYyMjNiZDE3YzUxY2FlODg1MDAwYzM2MGE5N2U=
|
@@ -26,6 +26,10 @@ When /^I destroy the lab with "([^"]*)"$/ do |app_name|
|
|
26
26
|
testlab_cmd(app_name, %W(destroy))
|
27
27
|
end
|
28
28
|
|
29
|
+
When /^I bounce the lab with "([^"]*)"$/ do |app_name|
|
30
|
+
testlab_cmd(app_name, %W(bounce))
|
31
|
+
end
|
32
|
+
|
29
33
|
def testlab_cmd(app_name, *args)
|
30
34
|
args = args.join(' ')
|
31
35
|
step %(I run `#{app_name} --repo=#{TEST_REPO} --labfile=#{TEST_LABFILE} #{args}`)
|
@@ -25,6 +25,7 @@ node 'vagrant' do
|
|
25
25
|
|
26
26
|
network 'labnet' do
|
27
27
|
provisioners [
|
28
|
+
TestLab::Provisioner::Route,
|
28
29
|
TestLab::Provisioner::Bind
|
29
30
|
]
|
30
31
|
|
@@ -38,6 +39,7 @@ node 'vagrant' do
|
|
38
39
|
|
39
40
|
provisioners [
|
40
41
|
TestLab::Provisioner::Resolv,
|
42
|
+
TestLab::Provisioner::Bind,
|
41
43
|
TestLab::Provisioner::AptCacherNG,
|
42
44
|
TestLab::Provisioner::Apt
|
43
45
|
]
|
data/features/testlab.feature
CHANGED
@@ -25,6 +25,8 @@ Feature: TestLab command-line
|
|
25
25
|
Then the exit status should be 0
|
26
26
|
When I export the containers with "tl"
|
27
27
|
Then the exit status should be 0
|
28
|
+
When I bounce the lab with "tl"
|
29
|
+
Then the exit status should be 0
|
28
30
|
|
29
31
|
|
30
32
|
Scenario: TestLab import
|
@@ -34,6 +36,8 @@ Feature: TestLab command-line
|
|
34
36
|
Then the exit status should be 0
|
35
37
|
When I build the lab with "tl"
|
36
38
|
Then the exit status should be 0
|
39
|
+
When I bounce the lab with "tl"
|
40
|
+
Then the exit status should be 0
|
37
41
|
|
38
42
|
|
39
43
|
Scenario: TestLab clone
|
@@ -50,13 +54,13 @@ Feature: TestLab command-line
|
|
50
54
|
Then the exit status should be 0
|
51
55
|
|
52
56
|
When I build the containers with "tl"
|
53
|
-
Then the exit status should be
|
57
|
+
Then the exit status should be 1
|
54
58
|
|
55
59
|
When I bounce the containers with "tl"
|
56
60
|
Then the exit status should be 0
|
57
61
|
|
58
62
|
When I recycle the containers with "tl"
|
59
|
-
Then the exit status should be
|
63
|
+
Then the exit status should be 1
|
60
64
|
|
61
65
|
When I bounce the containers with "tl"
|
62
66
|
Then the exit status should be 0
|
@@ -85,6 +89,9 @@ Feature: TestLab command-line
|
|
85
89
|
When I bounce the containers with "tl"
|
86
90
|
Then the exit status should be 0
|
87
91
|
|
92
|
+
When I bounce the lab with "tl"
|
93
|
+
Then the exit status should be 0
|
94
|
+
|
88
95
|
|
89
96
|
Scenario: TestLab Demolish
|
90
97
|
When I demolish the lab with "tl"
|
data/lib/commands/container.rb
CHANGED
@@ -30,14 +30,10 @@ Displays the status of all containers or single/multiple containers if supplied
|
|
30
30
|
EOF
|
31
31
|
c.command :status do |status|
|
32
32
|
status.action do |global_options, options, args|
|
33
|
-
containers = iterate_objects_by_name(options[:name], TestLab::Container)
|
33
|
+
containers = iterate_objects_by_name(options[:name], TestLab::Container)
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
else
|
38
|
-
ZTK::Report.new(:ui => @testlab.ui).list(containers, TestLab::Container::STATUS_KEYS) do |container|
|
39
|
-
OpenStruct.new(container.status)
|
40
|
-
end
|
35
|
+
ZTK::Report.new(:ui => @testlab.ui).list(containers, TestLab::Container::STATUS_KEYS) do |container|
|
36
|
+
OpenStruct.new(container.status)
|
41
37
|
end
|
42
38
|
end
|
43
39
|
end
|
data/lib/commands/network.rb
CHANGED
@@ -30,14 +30,10 @@ Displays the status of all networks or single/multiple networks if supplied via
|
|
30
30
|
EOF
|
31
31
|
c.command :status do |status|
|
32
32
|
status.action do |global_options, options, args|
|
33
|
-
networks = iterate_objects_by_name(options[:name], TestLab::Network)
|
33
|
+
networks = iterate_objects_by_name(options[:name], TestLab::Network)
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
else
|
38
|
-
ZTK::Report.new(:ui => @testlab.ui).list(networks, TestLab::Network::STATUS_KEYS) do |network|
|
39
|
-
OpenStruct.new(network.status)
|
40
|
-
end
|
35
|
+
ZTK::Report.new(:ui => @testlab.ui).list(networks, TestLab::Network::STATUS_KEYS) do |network|
|
36
|
+
OpenStruct.new(network.status)
|
41
37
|
end
|
42
38
|
end
|
43
39
|
end
|
@@ -56,7 +52,6 @@ EOF
|
|
56
52
|
p = TestLab::Provisioner::Route.new({}, @ui)
|
57
53
|
p.on_network_up(network)
|
58
54
|
@testlab.ui.stdout.puts("Added routes successfully!".green.bold)
|
59
|
-
@testlab.ui.stdout.puts %x(netstat -nr | grep '#{network.node.ip}').strip
|
60
55
|
end
|
61
56
|
end
|
62
57
|
end
|
@@ -70,23 +65,28 @@ EOF
|
|
70
65
|
p = TestLab::Provisioner::Route.new({}, @ui)
|
71
66
|
p.on_network_down(network)
|
72
67
|
@testlab.ui.stdout.puts("Deleted routes successfully!".red.bold)
|
73
|
-
@testlab.ui.stdout.puts %x(netstat -nr | grep '#{network.node.ip}').strip
|
74
68
|
end
|
75
69
|
end
|
76
70
|
end
|
77
71
|
|
78
72
|
# ROUTE SHOW
|
79
73
|
#############
|
74
|
+
|
75
|
+
# Route show helper method because OSX sucks
|
76
|
+
def osx_network(net)
|
77
|
+
net.network.split('.').delete_if{ |o| o == '0' }.join('.')
|
78
|
+
end
|
79
|
+
|
80
80
|
route.desc 'Show routes to lab networks'
|
81
81
|
route.command :show do |show|
|
82
82
|
show.action do |global_options,options,args|
|
83
83
|
iterate_objects_by_name(options[:name], TestLab::Network) do |network|
|
84
|
-
@testlab.ui.stdout.puts("TestLab
|
84
|
+
@testlab.ui.stdout.puts("Routes for TestLab network '#{network.id}':".green.bold)
|
85
85
|
case RUBY_PLATFORM
|
86
86
|
when /darwin/ then
|
87
|
-
@testlab.ui.stdout.puts %x(netstat -nrf inet | grep '#{network.
|
87
|
+
@testlab.ui.stdout.puts %x(netstat -nrf inet | grep '#{osx_network(network)}/#{network.cidr}').strip
|
88
88
|
when /linux/ then
|
89
|
-
@testlab.ui.stdout.puts %x(netstat -nr | grep '#{network.
|
89
|
+
@testlab.ui.stdout.puts %x(netstat -nr | grep '#{network.network}').strip
|
90
90
|
end
|
91
91
|
end
|
92
92
|
end
|
@@ -12,8 +12,9 @@ class TestLab
|
|
12
12
|
def create
|
13
13
|
@ui.logger.debug { "Container Create: #{self.id}" }
|
14
14
|
|
15
|
-
|
16
|
-
|
15
|
+
self.node.alive? or return false
|
16
|
+
|
17
|
+
persistent_operation_check(:create)
|
17
18
|
|
18
19
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Create', :green)) do
|
19
20
|
configure
|
@@ -34,8 +35,7 @@ class TestLab
|
|
34
35
|
def destroy
|
35
36
|
@ui.logger.debug { "Container Destroy: #{self.id}" }
|
36
37
|
|
37
|
-
|
38
|
-
(self.state != :not_created) or return false
|
38
|
+
self.node.alive? or return false
|
39
39
|
|
40
40
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Destroy', :red)) do
|
41
41
|
self.lxc.destroy(%(-f))
|
@@ -55,8 +55,7 @@ class TestLab
|
|
55
55
|
def up
|
56
56
|
@ui.logger.debug { "Container Up: #{self.id}" }
|
57
57
|
|
58
|
-
|
59
|
-
(self.state != :running) or return false
|
58
|
+
self.node.alive? or return false
|
60
59
|
|
61
60
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Up', :green)) do
|
62
61
|
configure
|
@@ -66,19 +65,19 @@ class TestLab
|
|
66
65
|
self.node.exec(%(sudo arp --verbose --delete #{interface.ip}), :ignore_exit_status => true)
|
67
66
|
end
|
68
67
|
|
69
|
-
if self.
|
68
|
+
if self.is_ephemeral?
|
70
69
|
self.lxc_clone.start_ephemeral(clone_args)
|
71
70
|
else
|
72
71
|
self.lxc.start(%(--daemon))
|
73
72
|
end
|
74
73
|
|
75
|
-
(self.
|
74
|
+
(self.state != :running) and raise ContainerError, "The container failed to online!"
|
76
75
|
|
77
76
|
ZTK::TCPSocketCheck.new(:ui => @ui, :host => self.primary_interface.ip, :port => 22).wait
|
78
77
|
|
79
78
|
# If we are not in ephemeral mode we should attempt to provision our
|
80
79
|
# defined users.
|
81
|
-
if
|
80
|
+
if self.is_persistent?
|
82
81
|
self.users.each do |user|
|
83
82
|
user.provision
|
84
83
|
end
|
@@ -101,15 +100,14 @@ class TestLab
|
|
101
100
|
def down
|
102
101
|
@ui.logger.debug { "Container Down: #{self.id}" }
|
103
102
|
|
104
|
-
|
105
|
-
(self.state == :running) or return false
|
103
|
+
self.node.alive? or return false
|
106
104
|
|
107
105
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Down', :red)) do
|
108
106
|
|
109
107
|
self.lxc.stop
|
110
108
|
|
111
109
|
# If we are in ephemeral mode...
|
112
|
-
if self.
|
110
|
+
if self.is_ephemeral?
|
113
111
|
|
114
112
|
# IMPORTANT NOTE:
|
115
113
|
#
|
@@ -118,11 +116,10 @@ class TestLab
|
|
118
116
|
#
|
119
117
|
# If we are using a memory backed COW filesystem for the ephemeral
|
120
118
|
# clones then it will be released when the container is stopped.
|
121
|
-
|
122
119
|
self.persist and self.lxc.destroy(%(-f))
|
123
120
|
end
|
124
121
|
|
125
|
-
(self.
|
122
|
+
(self.state == :running) and raise ContainerError, "The container failed to offline!"
|
126
123
|
|
127
124
|
do_provisioner_callbacks(self, :down, @ui)
|
128
125
|
end
|
@@ -12,7 +12,7 @@ class TestLab
|
|
12
12
|
@ui.logger.debug { "Container Ephemeral: #{self.id}" }
|
13
13
|
|
14
14
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Ephemeral', :yellow)) do
|
15
|
-
self.to_ephemeral
|
15
|
+
is_persistent? and self.to_ephemeral
|
16
16
|
end
|
17
17
|
|
18
18
|
true
|
@@ -27,12 +27,49 @@ class TestLab
|
|
27
27
|
@ui.logger.debug { "Container Persistent: #{self.id}" }
|
28
28
|
|
29
29
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Persistent', :yellow)) do
|
30
|
-
self.
|
30
|
+
is_ephemeral? and self.to_persistent
|
31
31
|
end
|
32
32
|
|
33
33
|
true
|
34
34
|
end
|
35
35
|
|
36
|
+
# Persistent Operation Check
|
37
|
+
#
|
38
|
+
# Checks if the container is operating in ephemeral mode, and if it is
|
39
|
+
# raises an exception indicating the operation can not proceed.
|
40
|
+
#
|
41
|
+
# If the container is operating in persistent mode, no output is generated
|
42
|
+
# and true is returned indicating the operation can continue.
|
43
|
+
#
|
44
|
+
# @return [Boolean] True if the operation can continue; false otherwise.
|
45
|
+
def persistent_operation_check(action)
|
46
|
+
if is_ephemeral?
|
47
|
+
raise ContainerError, "You can not #{action} #{self.id} because it is currently in ephemeral mode!"
|
48
|
+
end
|
49
|
+
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Is Container Ephemeral?
|
54
|
+
#
|
55
|
+
# Returns true if the container is in ephemeral mode, false otherwise.
|
56
|
+
#
|
57
|
+
# @return [Boolean] Returns true if the container is ephemeral, false
|
58
|
+
# otherwise.
|
59
|
+
def is_ephemeral?
|
60
|
+
self.lxc_clone.exists?
|
61
|
+
end
|
62
|
+
|
63
|
+
# Is Container Persistent?
|
64
|
+
#
|
65
|
+
# Returns true if the container is in persistent mode, false otherwise.
|
66
|
+
#
|
67
|
+
# @return [Boolean] Returns true if the container is persistent, false
|
68
|
+
# otherwise.
|
69
|
+
def is_persistent?
|
70
|
+
!is_ephemeral?
|
71
|
+
end
|
72
|
+
|
36
73
|
# LXC::Container object
|
37
74
|
#
|
38
75
|
# Returns a *LXC::Container* class instance configured for the clone of
|
@@ -51,8 +88,12 @@ class TestLab
|
|
51
88
|
# occur.
|
52
89
|
#
|
53
90
|
# @return [Boolean] Returns true if successful.
|
54
|
-
def
|
55
|
-
if self.
|
91
|
+
def to_persistent
|
92
|
+
if self.is_ephemeral?
|
93
|
+
self_state = self.state
|
94
|
+
|
95
|
+
configure
|
96
|
+
|
56
97
|
self.lxc.stop
|
57
98
|
self.lxc.destroy(%(-f))
|
58
99
|
|
@@ -60,7 +101,8 @@ class TestLab
|
|
60
101
|
self.lxc_clone.clone(%W(-o #{self.lxc_clone.name} -n #{self.lxc.name}))
|
61
102
|
self.lxc_clone.destroy(%(-f))
|
62
103
|
|
63
|
-
|
104
|
+
# bring our container back online if it was running before the operation
|
105
|
+
(self_state == :running) and self.up
|
64
106
|
end
|
65
107
|
|
66
108
|
true
|
@@ -73,7 +115,11 @@ class TestLab
|
|
73
115
|
#
|
74
116
|
# @return [Boolean] Returns true if successful.
|
75
117
|
def to_ephemeral
|
76
|
-
if
|
118
|
+
if self.is_persistent?
|
119
|
+
self_state = self.state
|
120
|
+
|
121
|
+
configure
|
122
|
+
|
77
123
|
self.lxc_clone.stop
|
78
124
|
self.lxc_clone.destroy(%(-f))
|
79
125
|
|
@@ -81,10 +127,8 @@ class TestLab
|
|
81
127
|
self.lxc.clone(%W(-o #{self.lxc.name} -n #{self.lxc_clone.name}))
|
82
128
|
self.lxc.destroy(%(-f))
|
83
129
|
|
84
|
-
|
85
|
-
|
86
|
-
self.lxc.stop
|
87
|
-
self.persist and self.lxc.destroy(%(-f))
|
130
|
+
# bring our container back online if it was running before the operation
|
131
|
+
(self_state == :running) and self.up
|
88
132
|
end
|
89
133
|
|
90
134
|
true
|
data/lib/testlab/container/io.rb
CHANGED
@@ -11,7 +11,7 @@ class TestLab
|
|
11
11
|
def export(compression=9, local_file=nil)
|
12
12
|
@ui.logger.debug { "Container Export: #{self.id} " }
|
13
13
|
|
14
|
-
(self.
|
14
|
+
(self.state == :not_created) and raise ContainerError, 'You must create a container before you can export it!'
|
15
15
|
|
16
16
|
# Throw an exception if we are attempting to export a container in a
|
17
17
|
# ephemeral state.
|
@@ -49,9 +49,7 @@ EOF
|
|
49
49
|
self.node.download(remote_file, local_file)
|
50
50
|
end
|
51
51
|
|
52
|
-
@ui.stdout.puts
|
53
|
-
@ui.stdout.puts("Your shipping container is now exported and available at '#{local_file}'!".green.bold)
|
54
|
-
@ui.stdout.puts
|
52
|
+
@ui.stdout.puts(format_message("Your shipping container is now exported and available at '#{local_file}'!".green.bold))
|
55
53
|
|
56
54
|
true
|
57
55
|
end
|
@@ -63,7 +61,7 @@ EOF
|
|
63
61
|
@ui.logger.debug { "Container Import: #{self.id} " }
|
64
62
|
|
65
63
|
# Ensure we are not in ephemeral mode.
|
66
|
-
self.
|
64
|
+
self.persistent
|
67
65
|
|
68
66
|
self.down
|
69
67
|
self.destroy
|
@@ -97,9 +95,7 @@ du -sh #{self.lxc.container_root}
|
|
97
95
|
EOF
|
98
96
|
end
|
99
97
|
|
100
|
-
@ui.stdout.puts
|
101
|
-
@ui.stdout.puts("Your shipping container is now imported and available for use!".green.bold)
|
102
|
-
@ui.stdout.puts
|
98
|
+
@ui.stdout.puts(format_message("Your shipping container is now imported and available for use!".green.bold))
|
103
99
|
|
104
100
|
true
|
105
101
|
end
|
@@ -5,16 +5,14 @@ class TestLab
|
|
5
5
|
|
6
6
|
# Provision the container
|
7
7
|
#
|
8
|
-
# Attempts to provision the container.
|
9
|
-
#
|
10
|
-
# is called.
|
8
|
+
# Attempts to provision the container. Calls the containers defined
|
9
|
+
# provisioners provision methods.
|
11
10
|
#
|
12
11
|
# @return [Boolean] True if successful.
|
13
12
|
def provision
|
14
13
|
@ui.logger.debug { "Container Provision: #{self.id} " }
|
15
14
|
|
16
|
-
|
17
|
-
(self.state != :running) and return false
|
15
|
+
self.node.alive? or return false
|
18
16
|
|
19
17
|
please_wait(:ui => @ui, :message => format_object_action(self, :provision, :green)) do
|
20
18
|
do_provisioner_callbacks(self, :provision, @ui)
|
@@ -25,16 +23,16 @@ class TestLab
|
|
25
23
|
|
26
24
|
# Deprovision the container
|
27
25
|
#
|
28
|
-
# Attempts to deprovision the container.
|
29
|
-
# deprovision
|
30
|
-
# to offline the container. Afterwards the container is destroy.
|
26
|
+
# Attempts to deprovision the container. Calls the containers defined
|
27
|
+
# provisioners deprovision methods.
|
31
28
|
#
|
32
29
|
# @return [Boolean] True if successful.
|
33
30
|
def deprovision
|
34
31
|
@ui.logger.debug { "Container Deprovision: #{self.id} " }
|
35
32
|
|
36
|
-
|
37
|
-
|
33
|
+
self.node.alive? or return false
|
34
|
+
|
35
|
+
persistent_operation_check(:deprovision)
|
38
36
|
|
39
37
|
please_wait(:ui => @ui, :message => format_object_action(self, :deprovision, :red)) do
|
40
38
|
do_provisioner_callbacks(self, :deprovision, @ui)
|
@@ -70,8 +70,8 @@ class TestLab
|
|
70
70
|
#
|
71
71
|
# @return [Symbol] A symbol indicating the state of the container.
|
72
72
|
def state
|
73
|
-
if self.
|
74
|
-
|
73
|
+
if self.node.dead?
|
74
|
+
:unknown
|
75
75
|
else
|
76
76
|
self.lxc.state
|
77
77
|
end
|
@@ -82,10 +82,14 @@ class TestLab
|
|
82
82
|
# What mode the container is in.
|
83
83
|
# @return [Symbol] A symbol indicating the mode of the container.
|
84
84
|
def mode
|
85
|
-
if self.
|
86
|
-
:
|
85
|
+
if self.node.dead?
|
86
|
+
:unknown
|
87
87
|
else
|
88
|
-
|
88
|
+
if self.is_ephemeral?
|
89
|
+
:ephemeral
|
90
|
+
else
|
91
|
+
:persistent
|
92
|
+
end
|
89
93
|
end
|
90
94
|
end
|
91
95
|
|
@@ -7,8 +7,7 @@ class TestLab
|
|
7
7
|
def create
|
8
8
|
@ui.logger.debug { "Network Create: #{self.id} " }
|
9
9
|
|
10
|
-
|
11
|
-
(self.state == :not_created) or return false
|
10
|
+
self.node.alive? or return false
|
12
11
|
|
13
12
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Create', :green)) do
|
14
13
|
self.node.bootstrap(<<-EOF, :ignore_exit_status => true)
|
@@ -41,8 +40,7 @@ brctl setfd #{self.bridge} 0
|
|
41
40
|
def destroy
|
42
41
|
@ui.logger.debug { "Network Destroy: #{self.id} " }
|
43
42
|
|
44
|
-
|
45
|
-
(self.state != :not_created) or return false
|
43
|
+
self.node.alive? or return false
|
46
44
|
|
47
45
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Destroy', :red)) do
|
48
46
|
self.node.bootstrap(<<-EOF, :ignore_exit_status => true)
|
@@ -61,8 +59,7 @@ brctl delbr #{self.bridge}
|
|
61
59
|
def up
|
62
60
|
@ui.logger.debug { "Network Up: #{self.id} " }
|
63
61
|
|
64
|
-
|
65
|
-
# (self.state != :running) or return false
|
62
|
+
self.node.alive? or return false
|
66
63
|
|
67
64
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Up', :green)) do
|
68
65
|
self.node.bootstrap(<<-EOF, :ignore_exit_status => true)
|
@@ -80,8 +77,7 @@ ifconfig #{self.bridge} #{self.ip} netmask #{self.netmask} broadcast #{self.broa
|
|
80
77
|
def down
|
81
78
|
@ui.logger.debug { "Network Down: #{self.id} " }
|
82
79
|
|
83
|
-
|
84
|
-
# (self.state == :running) or return false
|
80
|
+
self.node.alive? or return false
|
85
81
|
|
86
82
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Down', :red)) do
|
87
83
|
self.node.bootstrap(<<-EOF, :ignore_exit_status => true)
|
@@ -7,8 +7,7 @@ class TestLab
|
|
7
7
|
def provision
|
8
8
|
@ui.logger.debug { "Network Provision: #{self.id} " }
|
9
9
|
|
10
|
-
|
11
|
-
(self.state != :running) and return false
|
10
|
+
self.node.alive? or return false
|
12
11
|
|
13
12
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Provision', :green)) do
|
14
13
|
do_provisioner_callbacks(self, :provision, @ui)
|
@@ -21,8 +20,7 @@ class TestLab
|
|
21
20
|
def deprovision
|
22
21
|
@ui.logger.debug { "Network Deprovision: #{self.id} " }
|
23
22
|
|
24
|
-
|
25
|
-
(self.state != :running) and return false
|
23
|
+
self.node.alive? or return false
|
26
24
|
|
27
25
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Deprovision', :red)) do
|
28
26
|
do_provisioner_callbacks(self, :deprovision, @ui)
|
@@ -63,15 +63,19 @@ class TestLab
|
|
63
63
|
|
64
64
|
# Network Bridge State
|
65
65
|
def state
|
66
|
-
|
67
|
-
|
68
|
-
:not_created
|
66
|
+
if self.node.dead?
|
67
|
+
:unknown
|
69
68
|
else
|
70
|
-
|
71
|
-
if (
|
72
|
-
:
|
69
|
+
exit_code = self.node.exec(%(sudo brctl show #{self.bridge} 2>&1 | grep -i 'No such device'), :ignore_exit_status => true).exit_code
|
70
|
+
if (exit_code == 0)
|
71
|
+
:not_created
|
73
72
|
else
|
74
|
-
:
|
73
|
+
output = self.node.exec(%(sudo ifconfig #{self.bridge} 2>&1 | grep 'MTU'), :ignore_exit_status => true).output
|
74
|
+
if ((output =~ /UP/) || (output =~ /RUNNING/))
|
75
|
+
:running
|
76
|
+
else
|
77
|
+
:stopped
|
78
|
+
end
|
75
79
|
end
|
76
80
|
end
|
77
81
|
end
|
data/lib/testlab/node/actions.rb
CHANGED
@@ -7,8 +7,6 @@ class TestLab
|
|
7
7
|
def create
|
8
8
|
@ui.logger.debug { "Node Create: #{self.id} " }
|
9
9
|
|
10
|
-
(self.state == :not_created) or return false
|
11
|
-
|
12
10
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Create', :green)) do
|
13
11
|
@provider.create
|
14
12
|
|
@@ -22,8 +20,6 @@ class TestLab
|
|
22
20
|
def destroy
|
23
21
|
@ui.logger.debug { "Node Destroy: #{self.id} " }
|
24
22
|
|
25
|
-
(self.state != :not_created) or return false
|
26
|
-
|
27
23
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Destroy', :red)) do
|
28
24
|
@provider.destroy
|
29
25
|
|
@@ -37,8 +33,6 @@ class TestLab
|
|
37
33
|
def up
|
38
34
|
@ui.logger.debug { "Node Up: #{self.id} " }
|
39
35
|
|
40
|
-
(self.state != :running) or return false
|
41
|
-
|
42
36
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Up', :green)) do
|
43
37
|
@provider.up
|
44
38
|
|
@@ -52,8 +46,6 @@ class TestLab
|
|
52
46
|
def down
|
53
47
|
@ui.logger.debug { "Node Down: #{self.id} " }
|
54
48
|
|
55
|
-
(self.state == :running) or return false
|
56
|
-
|
57
49
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Down', :red)) do
|
58
50
|
@provider.down
|
59
51
|
|
@@ -7,8 +7,6 @@ class TestLab
|
|
7
7
|
def provision
|
8
8
|
@ui.logger.debug { "Node Provision: #{self.id} " }
|
9
9
|
|
10
|
-
(self.state != :running) and return false
|
11
|
-
|
12
10
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Provision', :green)) do
|
13
11
|
do_provisioner_callbacks(self, :provision, @ui)
|
14
12
|
end
|
@@ -20,8 +18,6 @@ class TestLab
|
|
20
18
|
def deprovision
|
21
19
|
@ui.logger.debug { "Node Deprovision: #{self.id} " }
|
22
20
|
|
23
|
-
(self.state != :running) and return false
|
24
|
-
|
25
21
|
please_wait(:ui => @ui, :message => format_object_action(self, 'Deprovision', :red)) do
|
26
22
|
do_provisioner_callbacks(self, :deprovision, @ui)
|
27
23
|
end
|
@@ -29,8 +29,6 @@ class TestLab
|
|
29
29
|
# A collection of all states the VM can be in
|
30
30
|
ALL_STATES = (VALID_STATES + INVALID_STATES).flatten
|
31
31
|
|
32
|
-
MSG_NO_LAB = %(We could not find a test lab!)
|
33
|
-
|
34
32
|
################################################################################
|
35
33
|
|
36
34
|
def initialize(config={}, ui=nil)
|
@@ -50,8 +48,6 @@ class TestLab
|
|
50
48
|
|
51
49
|
# Destroy Vagrant-controlled VM
|
52
50
|
def destroy
|
53
|
-
!self.exists? and raise VagrantError, MSG_NO_LAB
|
54
|
-
|
55
51
|
self.alive? and self.down
|
56
52
|
self.exists? and self.vagrant_cli("destroy", "--force", self.instance_id)
|
57
53
|
|
@@ -71,8 +67,6 @@ class TestLab
|
|
71
67
|
|
72
68
|
# Halt Vagrant-controlled VM
|
73
69
|
def down(*args)
|
74
|
-
!self.exists? and raise VagrantError, MSG_NO_LAB
|
75
|
-
|
76
70
|
arguments = (%W(halt #{self.instance_id}) + args).flatten.compact
|
77
71
|
|
78
72
|
self.vagrant_cli(*arguments)
|
data/lib/testlab/version.rb
CHANGED
data/spec/container_spec.rb
CHANGED
@@ -52,8 +52,10 @@ describe TestLab::Container do
|
|
52
52
|
|
53
53
|
describe "#status" do
|
54
54
|
it "should return a hash of status information about the container" do
|
55
|
+
subject.node.stub(:state) { :running }
|
55
56
|
subject.lxc.stub(:state) { :not_created }
|
56
57
|
subject.lxc_clone.stub(:exists?) { false }
|
58
|
+
|
57
59
|
subject.status.should be_kind_of(Hash)
|
58
60
|
subject.status.should_not be_empty
|
59
61
|
end
|
@@ -61,7 +63,9 @@ describe TestLab::Container do
|
|
61
63
|
|
62
64
|
describe "#state" do
|
63
65
|
it "should return the state of the container" do
|
66
|
+
subject.node.stub(:dead?) { false }
|
64
67
|
subject.lxc.stub(:state) { :not_created }
|
68
|
+
subject.lxc_clone.stub(:exists?) { false }
|
65
69
|
subject.state.should == :not_created
|
66
70
|
end
|
67
71
|
end
|
@@ -166,6 +170,7 @@ describe TestLab::Container do
|
|
166
170
|
subject.lxc.stub(:state) { :stopped }
|
167
171
|
subject.lxc.stub(:destroy) { true }
|
168
172
|
subject.lxc_clone.stub(:exists?) { false }
|
173
|
+
subject.lxc_clone.stub(:destroy) { false }
|
169
174
|
subject.stub(:provisioners) { Array.new }
|
170
175
|
|
171
176
|
subject.destroy
|
@@ -175,15 +180,17 @@ describe TestLab::Container do
|
|
175
180
|
describe "#up" do
|
176
181
|
it "should up the container" do
|
177
182
|
subject.node.stub(:state) { :running }
|
183
|
+
|
178
184
|
subject.lxc.stub(:exists?) { true }
|
179
185
|
subject.lxc.stub(:start) { true }
|
180
186
|
subject.lxc.stub(:wait) { true }
|
181
187
|
subject.lxc.stub(:state) { :running }
|
182
188
|
subject.lxc.stub(:attach)
|
183
|
-
subject.stub(:provisioners) { Array.new }
|
184
189
|
|
185
190
|
subject.lxc_clone.stub(:exists?) { false }
|
186
191
|
|
192
|
+
subject.stub(:provisioners) { Array.new }
|
193
|
+
|
187
194
|
ZTK::TCPSocketCheck.any_instance.stub(:wait) { true }
|
188
195
|
|
189
196
|
subject.up
|
@@ -193,10 +200,14 @@ describe TestLab::Container do
|
|
193
200
|
describe "#down" do
|
194
201
|
it "should down the container" do
|
195
202
|
subject.node.stub(:state) { :running }
|
203
|
+
|
196
204
|
subject.lxc.stub(:exists?) { true }
|
197
205
|
subject.lxc.stub(:stop) { true }
|
198
206
|
subject.lxc.stub(:wait) { true }
|
199
207
|
subject.lxc.stub(:state) { :stopped }
|
208
|
+
|
209
|
+
subject.lxc_clone.stub(:exists?) { false }
|
210
|
+
|
200
211
|
subject.stub(:provisioners) { Array.new }
|
201
212
|
|
202
213
|
subject.down
|
@@ -207,8 +218,12 @@ describe TestLab::Container do
|
|
207
218
|
context "with no provisioner" do
|
208
219
|
it "should provision the container" do
|
209
220
|
subject.node.stub(:state) { :running }
|
221
|
+
|
210
222
|
subject.lxc.stub(:exists?) { true }
|
211
223
|
subject.lxc.stub(:state) { :stopped }
|
224
|
+
|
225
|
+
subject.lxc_clone.stub(:exists?) { false }
|
226
|
+
|
212
227
|
subject.stub(:provisioners) { Array.new }
|
213
228
|
|
214
229
|
subject.provision
|
@@ -220,8 +235,12 @@ describe TestLab::Container do
|
|
220
235
|
subject and (subject = TestLab::Container.first('server-shell'))
|
221
236
|
|
222
237
|
subject.node.stub(:state) { :running }
|
238
|
+
|
223
239
|
subject.lxc.stub(:exists?) { true }
|
224
240
|
subject.lxc.stub(:state) { :stopped }
|
241
|
+
|
242
|
+
subject.lxc_clone.stub(:exists?) { false }
|
243
|
+
|
225
244
|
subject.stub(:provisioners) { Array.new }
|
226
245
|
|
227
246
|
subject.provision
|
@@ -233,8 +252,12 @@ describe TestLab::Container do
|
|
233
252
|
context "with no provisioner" do
|
234
253
|
it "should deprovision the container" do
|
235
254
|
subject.node.stub(:state) { :running }
|
255
|
+
|
236
256
|
subject.lxc.stub(:exists?) { true }
|
237
257
|
subject.lxc.stub(:state) { :stopped }
|
258
|
+
|
259
|
+
subject.lxc_clone.stub(:exists?) { false }
|
260
|
+
|
238
261
|
subject.stub(:provisioners) { Array.new }
|
239
262
|
|
240
263
|
subject.deprovision
|
@@ -246,8 +269,12 @@ describe TestLab::Container do
|
|
246
269
|
subject and (subject = TestLab::Container.first('server-shell'))
|
247
270
|
|
248
271
|
subject.node.stub(:state) { :running }
|
272
|
+
|
249
273
|
subject.lxc.stub(:exists?) { true }
|
250
274
|
subject.lxc.stub(:state) { :stopped }
|
275
|
+
|
276
|
+
subject.lxc_clone.stub(:exists?) { false }
|
277
|
+
|
251
278
|
subject.stub(:provisioners) { Array.new }
|
252
279
|
|
253
280
|
subject.deprovision
|
data/spec/network_spec.rb
CHANGED
@@ -102,6 +102,7 @@ describe TestLab::Network do
|
|
102
102
|
|
103
103
|
describe "#state" do
|
104
104
|
it "should return the state of the bridge" do
|
105
|
+
subject.node.stub(:dead?) { false }
|
105
106
|
subject.node.ssh.stub(:exec) { OpenStruct.new(:output => " UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1") }
|
106
107
|
subject.state.should == :running
|
107
108
|
end
|
metadata
CHANGED
@@ -1,20 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: testlab
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
5
|
-
prerelease:
|
4
|
+
version: 1.5.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Zachary Patten
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2013-08-
|
11
|
+
date: 2013-08-14 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: gli
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
17
|
- - ! '>='
|
20
18
|
- !ruby/object:Gem::Version
|
@@ -22,7 +20,6 @@ dependencies:
|
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
24
|
- - ! '>='
|
28
25
|
- !ruby/object:Gem::Version
|
@@ -30,7 +27,6 @@ dependencies:
|
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: lxc
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
31
|
- - ! '>='
|
36
32
|
- !ruby/object:Gem::Version
|
@@ -38,7 +34,6 @@ dependencies:
|
|
38
34
|
type: :runtime
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
38
|
- - ! '>='
|
44
39
|
- !ruby/object:Gem::Version
|
@@ -46,7 +41,6 @@ dependencies:
|
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: ztk
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
45
|
- - ! '>='
|
52
46
|
- !ruby/object:Gem::Version
|
@@ -54,7 +48,6 @@ dependencies:
|
|
54
48
|
type: :runtime
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
52
|
- - ! '>='
|
60
53
|
- !ruby/object:Gem::Version
|
@@ -62,7 +55,6 @@ dependencies:
|
|
62
55
|
- !ruby/object:Gem::Dependency
|
63
56
|
name: activesupport
|
64
57
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
58
|
requirements:
|
67
59
|
- - ! '>='
|
68
60
|
- !ruby/object:Gem::Version
|
@@ -70,7 +62,6 @@ dependencies:
|
|
70
62
|
type: :runtime
|
71
63
|
prerelease: false
|
72
64
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
65
|
requirements:
|
75
66
|
- - ! '>='
|
76
67
|
- !ruby/object:Gem::Version
|
@@ -78,7 +69,6 @@ dependencies:
|
|
78
69
|
- !ruby/object:Gem::Dependency
|
79
70
|
name: bundler
|
80
71
|
requirement: !ruby/object:Gem::Requirement
|
81
|
-
none: false
|
82
72
|
requirements:
|
83
73
|
- - ! '>='
|
84
74
|
- !ruby/object:Gem::Version
|
@@ -86,7 +76,6 @@ dependencies:
|
|
86
76
|
type: :development
|
87
77
|
prerelease: false
|
88
78
|
version_requirements: !ruby/object:Gem::Requirement
|
89
|
-
none: false
|
90
79
|
requirements:
|
91
80
|
- - ! '>='
|
92
81
|
- !ruby/object:Gem::Version
|
@@ -94,7 +83,6 @@ dependencies:
|
|
94
83
|
- !ruby/object:Gem::Dependency
|
95
84
|
name: pry
|
96
85
|
requirement: !ruby/object:Gem::Requirement
|
97
|
-
none: false
|
98
86
|
requirements:
|
99
87
|
- - ! '>='
|
100
88
|
- !ruby/object:Gem::Version
|
@@ -102,7 +90,6 @@ dependencies:
|
|
102
90
|
type: :development
|
103
91
|
prerelease: false
|
104
92
|
version_requirements: !ruby/object:Gem::Requirement
|
105
|
-
none: false
|
106
93
|
requirements:
|
107
94
|
- - ! '>='
|
108
95
|
- !ruby/object:Gem::Version
|
@@ -110,7 +97,6 @@ dependencies:
|
|
110
97
|
- !ruby/object:Gem::Dependency
|
111
98
|
name: rake
|
112
99
|
requirement: !ruby/object:Gem::Requirement
|
113
|
-
none: false
|
114
100
|
requirements:
|
115
101
|
- - ! '>='
|
116
102
|
- !ruby/object:Gem::Version
|
@@ -118,7 +104,6 @@ dependencies:
|
|
118
104
|
type: :development
|
119
105
|
prerelease: false
|
120
106
|
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
none: false
|
122
107
|
requirements:
|
123
108
|
- - ! '>='
|
124
109
|
- !ruby/object:Gem::Version
|
@@ -126,7 +111,6 @@ dependencies:
|
|
126
111
|
- !ruby/object:Gem::Dependency
|
127
112
|
name: redcarpet
|
128
113
|
requirement: !ruby/object:Gem::Requirement
|
129
|
-
none: false
|
130
114
|
requirements:
|
131
115
|
- - ! '>='
|
132
116
|
- !ruby/object:Gem::Version
|
@@ -134,7 +118,6 @@ dependencies:
|
|
134
118
|
type: :development
|
135
119
|
prerelease: false
|
136
120
|
version_requirements: !ruby/object:Gem::Requirement
|
137
|
-
none: false
|
138
121
|
requirements:
|
139
122
|
- - ! '>='
|
140
123
|
- !ruby/object:Gem::Version
|
@@ -142,7 +125,6 @@ dependencies:
|
|
142
125
|
- !ruby/object:Gem::Dependency
|
143
126
|
name: aruba
|
144
127
|
requirement: !ruby/object:Gem::Requirement
|
145
|
-
none: false
|
146
128
|
requirements:
|
147
129
|
- - ! '>='
|
148
130
|
- !ruby/object:Gem::Version
|
@@ -150,7 +132,6 @@ dependencies:
|
|
150
132
|
type: :development
|
151
133
|
prerelease: false
|
152
134
|
version_requirements: !ruby/object:Gem::Requirement
|
153
|
-
none: false
|
154
135
|
requirements:
|
155
136
|
- - ! '>='
|
156
137
|
- !ruby/object:Gem::Version
|
@@ -158,7 +139,6 @@ dependencies:
|
|
158
139
|
- !ruby/object:Gem::Dependency
|
159
140
|
name: rspec
|
160
141
|
requirement: !ruby/object:Gem::Requirement
|
161
|
-
none: false
|
162
142
|
requirements:
|
163
143
|
- - ! '>='
|
164
144
|
- !ruby/object:Gem::Version
|
@@ -166,7 +146,6 @@ dependencies:
|
|
166
146
|
type: :development
|
167
147
|
prerelease: false
|
168
148
|
version_requirements: !ruby/object:Gem::Requirement
|
169
|
-
none: false
|
170
149
|
requirements:
|
171
150
|
- - ! '>='
|
172
151
|
- !ruby/object:Gem::Version
|
@@ -174,7 +153,6 @@ dependencies:
|
|
174
153
|
- !ruby/object:Gem::Dependency
|
175
154
|
name: yard
|
176
155
|
requirement: !ruby/object:Gem::Requirement
|
177
|
-
none: false
|
178
156
|
requirements:
|
179
157
|
- - ! '>='
|
180
158
|
- !ruby/object:Gem::Version
|
@@ -182,7 +160,6 @@ dependencies:
|
|
182
160
|
type: :development
|
183
161
|
prerelease: false
|
184
162
|
version_requirements: !ruby/object:Gem::Requirement
|
185
|
-
none: false
|
186
163
|
requirements:
|
187
164
|
- - ! '>='
|
188
165
|
- !ruby/object:Gem::Version
|
@@ -190,7 +167,6 @@ dependencies:
|
|
190
167
|
- !ruby/object:Gem::Dependency
|
191
168
|
name: coveralls
|
192
169
|
requirement: !ruby/object:Gem::Requirement
|
193
|
-
none: false
|
194
170
|
requirements:
|
195
171
|
- - ! '>='
|
196
172
|
- !ruby/object:Gem::Version
|
@@ -198,7 +174,6 @@ dependencies:
|
|
198
174
|
type: :development
|
199
175
|
prerelease: false
|
200
176
|
version_requirements: !ruby/object:Gem::Requirement
|
201
|
-
none: false
|
202
177
|
requirements:
|
203
178
|
- - ! '>='
|
204
179
|
- !ruby/object:Gem::Version
|
@@ -328,27 +303,26 @@ files:
|
|
328
303
|
homepage: http://hackers.lookout.com/testlab/
|
329
304
|
licenses:
|
330
305
|
- Apache 2.0
|
306
|
+
metadata: {}
|
331
307
|
post_install_message:
|
332
308
|
rdoc_options: []
|
333
309
|
require_paths:
|
334
310
|
- lib
|
335
311
|
required_ruby_version: !ruby/object:Gem::Requirement
|
336
|
-
none: false
|
337
312
|
requirements:
|
338
313
|
- - ! '>='
|
339
314
|
- !ruby/object:Gem::Version
|
340
315
|
version: '0'
|
341
316
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
342
|
-
none: false
|
343
317
|
requirements:
|
344
318
|
- - ! '>='
|
345
319
|
- !ruby/object:Gem::Version
|
346
320
|
version: '0'
|
347
321
|
requirements: []
|
348
322
|
rubyforge_project:
|
349
|
-
rubygems_version:
|
323
|
+
rubygems_version: 2.0.6
|
350
324
|
signing_key:
|
351
|
-
specification_version:
|
325
|
+
specification_version: 4
|
352
326
|
summary: A toolkit for building virtual computer labs
|
353
327
|
test_files:
|
354
328
|
- features/step_definitions/container_steps.rb
|