testlab 1.4.4 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|