testlab 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ *log
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -c -b -fd
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ testlab
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-1.9.3
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - 2.0.0
7
+
8
+ notifications:
9
+ irc: "irc.freenode.net#jovelabs"
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in testlab.gemspec
3
+ gem 'coveralls', :require => false
4
+
4
5
  gemspec
data/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
data/README.md CHANGED
@@ -1,29 +1,43 @@
1
+ [![Gem Version](https://badge.fury.io/rb/testlab.png)](http://badge.fury.io/rb/testlab)
2
+ [![Dependency Status](https://gemnasium.com/zpatten/testlab.png)](https://gemnasium.com/zpatten/testlab)
3
+ [![Build Status](https://secure.travis-ci.org/zpatten/testlab.png)](http://travis-ci.org/zpatten/testlab)
4
+ [![Coverage Status](https://coveralls.io/repos/zpatten/testlab/badge.png?branch=master)](https://coveralls.io/r/zpatten/testlab)
5
+ [![Code Climate](https://codeclimate.com/github/zpatten/testlab.png)](https://codeclimate.com/github/zpatten/testlab)
6
+
1
7
  # TestLab
2
8
 
3
- TODO: Write a gem description
9
+ A framework for building lightweight virtual laboratories using LXC
10
+
11
+ # RESOURCES
12
+
13
+ Source:
4
14
 
5
- ## Installation
15
+ * https://github.com/zpatten/testlab
6
16
 
7
- Add this line to your application's Gemfile:
17
+ Issues:
8
18
 
9
- gem 'testlab'
19
+ * https://github.com/zpatten/testlab/issues
10
20
 
11
- And then execute:
21
+ Documentation:
12
22
 
13
- $ bundle
23
+ * http://zpatten.github.com/testlab/
14
24
 
15
- Or install it yourself as:
25
+ # LICENSE
16
26
 
17
- $ gem install testlab
27
+ TestLab - A framework for building lightweight virtual laboratories using LXC
18
28
 
19
- ## Usage
29
+ * Author: Zachary Patten <zachary@jovelabs.com> [![endorse](http://api.coderwall.com/zpatten/endorsecount.png)](http://coderwall.com/zpatten)
30
+ * Copyright: Copyright (c) Zachary Patten
31
+ * License: Apache License, Version 2.0
20
32
 
21
- TODO: Write usage instructions here
33
+ Licensed under the Apache License, Version 2.0 (the "License");
34
+ you may not use this file except in compliance with the License.
35
+ You may obtain a copy of the License at
22
36
 
23
- ## Contributing
37
+ http://www.apache.org/licenses/LICENSE-2.0
24
38
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
39
+ Unless required by applicable law or agreed to in writing, software
40
+ distributed under the License is distributed on an "AS IS" BASIS,
41
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
42
+ See the License for the specific language governing permissions and
43
+ limitations under the License.
data/Rakefile CHANGED
@@ -1 +1,75 @@
1
- require "bundler/gem_tasks"
1
+ ################################################################################
2
+ #
3
+ # Author: Zachary Patten <zachary@jovelabs.net>
4
+ # Copyright: Copyright (c) Zachary Patten
5
+ # License: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+ ################################################################################
20
+
21
+ require 'bundler/gem_tasks'
22
+
23
+ ################################################################################
24
+
25
+ require 'rspec/core/rake_task'
26
+ RSpec::Core::RakeTask.new(:spec)
27
+ task :default => [:spec]
28
+ task :test => [:spec]
29
+
30
+ ################################################################################
31
+
32
+ require 'coveralls/rake/task'
33
+ Coveralls::RakeTask.new
34
+ task :coveralls => [:spec, 'coveralls:push']
35
+
36
+ ################################################################################
37
+
38
+ require 'yard'
39
+ require 'yard/rake/yardoc_task'
40
+
41
+ GEM_NAME = File.basename(Dir.pwd)
42
+ DOC_PATH = File.expand_path(File.join("..", "/", "#{GEM_NAME}.doc"))
43
+
44
+ namespace :doc do
45
+ YARD::Rake::YardocTask.new(:pages) do |t|
46
+
47
+ # t.files = ['lib/**/*.rb']
48
+ t.options = ['--verbose', '-o', DOC_PATH]
49
+ end
50
+
51
+ namespace :pages do
52
+
53
+ desc 'Generate and publish YARD Documentation to GitHub pages'
54
+ task :publish => ['doc:pages'] do
55
+ describe = %x(git describe).chomp
56
+ stats = %x(bundle exec yard stats).chomp
57
+
58
+ commit_message = Array.new
59
+ commit_message << "Generated YARD Documentation for #{GEM_NAME.upcase} #{describe}\n\n"
60
+ commit_message << stats
61
+
62
+ Dir.chdir(DOC_PATH) do
63
+ puts(%x{git add -Av})
64
+ puts(%x{git commit -m"#{commit_message.join}"})
65
+ puts(%x{git push origin gh-pages})
66
+ end
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+ desc 'Alias to doc:yard'
73
+ task 'doc' => 'doc:yard'
74
+
75
+ ################################################################################
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require 'pry'
3
+ require 'testlab'
4
+
5
+ tl = TestLab.new
6
+
7
+ ##
8
+ #
9
+ # Welcome to the Cucumber-Chef pry console!
10
+ #
11
+ ##
12
+ binding.pry
@@ -0,0 +1,218 @@
1
+ class TestLab
2
+
3
+ # Container Error Class
4
+ class ContainerError < TestLabError; end
5
+
6
+ # Container Class
7
+ #
8
+ # @author Zachary Patten <zachary@jovelabs.net>
9
+ class Container < ZTK::DSL::Base
10
+ STATUS_KEYS = %w(node_id id state distro release interfaces provisioner).map(&:to_sym)
11
+
12
+ belongs_to :node, :class_name => 'TestLab::Node'
13
+
14
+ attribute :provisioner
15
+ attribute :config
16
+
17
+ attribute :interfaces
18
+
19
+ attribute :distro
20
+ attribute :release
21
+ attribute :arch
22
+
23
+ attribute :persist
24
+
25
+ def initialize(*args)
26
+ super(*args)
27
+
28
+ @ui = TestLab.ui
29
+ @provisioner = self.provisioner.new(self.config) if !self.provisioner.nil?
30
+ end
31
+
32
+ def status
33
+ interfaces = self.interfaces.collect{ |key, value| "#{key}:#{value[:name]}:#{value[:ip]}" }.join(', ')
34
+
35
+ {
36
+ :id => self.id,
37
+ :state => self.state,
38
+ :distro => self.distro,
39
+ :release => self.release,
40
+ :interfaces => interfaces,
41
+ :provisioner => self.provisioner,
42
+ :node_id => self.node.id
43
+ }
44
+ end
45
+
46
+ # Our LXC Container class
47
+ def lxc
48
+ @lxc ||= self.node.lxc.container(self.id)
49
+ end
50
+
51
+ # Create the container
52
+ def create
53
+ @ui.logger.debug { "Container Create: #{self.id} " }
54
+
55
+ self.arch ||= detect_arch
56
+
57
+ self.lxc.config.clear
58
+
59
+ self.lxc.config['lxc.utsname'] = self.id
60
+
61
+ self.interfaces.each do |network, network_config|
62
+ n = Hash.new
63
+ n['lxc.network.type'] = :veth
64
+ n['lxc.network.flags'] = :up
65
+ n['lxc.network.link'] = TestLab::Network.first(network).bridge
66
+ n['lxc.network.name'] = (network_config[:name] || "eth0")
67
+ n['lxc.network.hwaddr'] = (network_config[:mac] || generate_mac)
68
+ n['lxc.network.ipv4'] = (network_config[:ip] || generate_ip)
69
+ self.lxc.config.networks << n
70
+ end
71
+
72
+ self.lxc.config.save
73
+
74
+ self.lxc.create(*create_args)
75
+ end
76
+
77
+ # Destroy the container
78
+ def destroy
79
+ @ui.logger.debug { "Container Destroy: #{self.id} " }
80
+
81
+ self.lxc.destroy
82
+ end
83
+
84
+ # Start the container
85
+ def up
86
+ @ui.logger.debug { "Container Up: #{self.id} " }
87
+
88
+ self.lxc.start
89
+ end
90
+
91
+ # Stop the container
92
+ def down
93
+ @ui.logger.debug { "Container Down: #{self.id} " }
94
+
95
+ self.lxc.stop
96
+ end
97
+
98
+ # Reload the container
99
+ def reload
100
+ @ui.logger.debug { "Container Reload: #{self.id} " }
101
+
102
+ self.down
103
+ self.up
104
+ end
105
+
106
+ # Does the container exist?
107
+ def exists?
108
+ @ui.logger.debug { "Container Exists?: #{self.id} " }
109
+
110
+ self.lxc.exists?
111
+ end
112
+
113
+ # State of the container
114
+ def state
115
+ self.lxc.state
116
+ end
117
+
118
+ ################################################################################
119
+
120
+ # Container Callback: after_create
121
+ def after_create
122
+ @ui.logger.debug { "Container Callback: After Create: #{self.id} " }
123
+ end
124
+
125
+ # Container Callback: after_up
126
+ def after_up
127
+ @ui.logger.debug { "Container Callback: After Up: #{self.id} " }
128
+
129
+ self.create
130
+ self.up
131
+ end
132
+
133
+ # Container Callback: before_down
134
+ def before_down
135
+ @ui.logger.debug { "Container Callback: Before Down: #{self.id} " }
136
+
137
+ self.down
138
+ self.destroy
139
+ end
140
+
141
+ # Container Callback: before_destroy
142
+ def before_destroy
143
+ @ui.logger.debug { "Container Callback: Before Destroy: #{self.id} " }
144
+ end
145
+
146
+ ################################################################################
147
+
148
+ # Method missing handler
149
+ def method_missing(method_name, *method_args)
150
+ @ui.logger.debug { "CONTAINER METHOD MISSING: #{method_name.inspect}(#{method_args.inspect})" }
151
+
152
+ if (defined?(@provisioner) && @provisioner.respond_to?(method_name))
153
+ @provisioner.send(method_name, [self, *method_args].flatten)
154
+ else
155
+ super(method_name, *method_args)
156
+ end
157
+ end
158
+
159
+ ################################################################################
160
+ private
161
+ ################################################################################
162
+
163
+ # Returns arguments for lxc-create based on our distro
164
+ def create_args
165
+ case self.distro.downcase
166
+ when "ubuntu" then
167
+ %W(-f /etc/lxc/#{self.id} -t #{self.distro} -- --release #{self.release} --arch #{arch})
168
+ when "fedora" then
169
+ %W(-f /etc/lxc/#{self.id} -t #{self.distro} -- --release #{self.release})
170
+ end
171
+ end
172
+
173
+ # Attempt to detect the architecture of the node our container is running on
174
+ def detect_arch
175
+ case self.distro.downcase
176
+ when "ubuntu" then
177
+ ((self.node.arch =~ /x86_64/) ? "amd64" : "i386")
178
+ when "fedora" then
179
+ ((self.node.arch =~ /x86_64/) ? "amd64" : "i686")
180
+ end
181
+ end
182
+
183
+ def generate_ip
184
+ octets = [ 192..192,
185
+ 168..168,
186
+ 0..254,
187
+ 1..254 ]
188
+ ip = Array.new
189
+ for x in 1..4 do
190
+ ip << octets[x-1].to_a[rand(octets[x-1].count)].to_s
191
+ end
192
+ ip.join(".")
193
+ end
194
+
195
+ def generate_mac
196
+ digits = [ %w(0),
197
+ %w(0),
198
+ %w(0),
199
+ %w(0),
200
+ %w(5),
201
+ %w(e),
202
+ %w(0 1 2 3 4 5 6 7 8 9 a b c d e f),
203
+ %w(0 1 2 3 4 5 6 7 8 9 a b c d e f),
204
+ %w(5 6 7 8 9 a b c d e f),
205
+ %w(3 4 5 6 7 8 9 a b c d e f),
206
+ %w(0 1 2 3 4 5 6 7 8 9 a b c d e f),
207
+ %w(0 1 2 3 4 5 6 7 8 9 a b c d e f) ]
208
+ mac = ""
209
+ for x in 1..12 do
210
+ mac += digits[x-1][rand(digits[x-1].count)]
211
+ mac += ":" if (x.modulo(2) == 0) && (x != 12)
212
+ end
213
+ mac
214
+ end
215
+
216
+ end
217
+
218
+ end
@@ -0,0 +1,15 @@
1
+ class TestLab
2
+
3
+ # Labfile Error Class
4
+ class LabfileError < TestLabError; end
5
+
6
+ # Labfile Class
7
+ #
8
+ # @author Zachary Patten <zachary@jovelabs.net>
9
+ class Labfile < ZTK::DSL::Base
10
+ has_many :nodes, :class_name => 'TestLab::Node'
11
+
12
+ attribute :config
13
+ end
14
+
15
+ end