occi-cli 4.0.0.alpha.1

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.
@@ -0,0 +1,388 @@
1
+ require 'ostruct'
2
+ require 'optparse'
3
+ require 'uri'
4
+
5
+ require 'occi/cli/resource_output_factory'
6
+
7
+ module Occi::Cli
8
+
9
+ class OcciOpts
10
+
11
+ AUTH_METHODS = [:x509, :basic, :digest, :none].freeze
12
+ MEDIA_TYPES = ["application/occi+json", "application/occi+xml", "text/plain,text/occi", "text/plain"].freeze
13
+ ACTIONS = [:list, :describe, :create, :delete, :trigger].freeze
14
+ LOG_OUTPUTS = [:stdout, :stderr].freeze
15
+
16
+ def self.parse(args, test_env = false)
17
+
18
+ @@quiet = test_env
19
+
20
+ options = OpenStruct.new
21
+
22
+ options.debug = false
23
+ options.verbose = false
24
+
25
+ options.log = {}
26
+ options.log[:out] = STDERR
27
+ options.log[:level] = Occi::Log::WARN
28
+
29
+ options.filter = nil
30
+ options.dump_model = false
31
+
32
+ options.interactive = false
33
+
34
+ options.endpoint = "https://localhost:3300/"
35
+
36
+ options.auth = {}
37
+ options.auth[:type] = "none"
38
+ options.auth[:user_cert] = ENV['HOME'] + "/.globus/usercred.pem"
39
+ options.auth[:ca_path] = "/etc/grid-security/certificates"
40
+ options.auth[:username] = "anonymous"
41
+ options.auth[:ca_file] = nil
42
+ options.auth[:voms] = nil
43
+
44
+ options.output_format = :plain
45
+
46
+ options.mixins = nil
47
+ options.links = nil
48
+ options.attributes = nil
49
+ options.context_vars = nil
50
+
51
+ # TODO: change media type back to occi+json after the rOCCI-server update
52
+ #options.media_type = "application/occi+json"
53
+ options.media_type = "text/plain,text/occi"
54
+
55
+ opts = OptionParser.new do |opts|
56
+ opts.banner = %{Usage: occi [OPTIONS]
57
+
58
+ Examples:
59
+ occi --interactive --endpoint https://localhost:3300/ --auth x509
60
+
61
+ occi --endpoint https://localhost:3300/ --action list --resource os_tpl --auth x509
62
+
63
+ occi --endpoint https://localhost:3300/ --action list --resource resource_tpl --auth x509
64
+
65
+ occi --endpoint https://localhost:3300/ --action describe --resource os_tpl#debian6 --auth x509
66
+
67
+ occi --endpoint https://localhost:3300/ --action create --resource compute --mixin os_tpl#debian6 --mixin resource_tpl#small --attributes title="My rOCCI VM" --auth x509
68
+
69
+ occi --endpoint https://localhost:3300/ --action delete --resource /compute/65sd4f654sf65g4-s5fg65sfg465sfg-sf65g46sf5g4sdfg --auth x509}
70
+
71
+ opts.separator ""
72
+ opts.separator "Options:"
73
+
74
+ opts.on("-i",
75
+ "--interactive",
76
+ "Run as an interactive client without additional arguments") do |interactive|
77
+ options.interactive = interactive
78
+ end
79
+
80
+ opts.on("-e",
81
+ "--endpoint URI",
82
+ String,
83
+ "OCCI server URI, defaults to '#{options.endpoint}'") do |endpoint|
84
+ options.endpoint = URI(endpoint).to_s
85
+ end
86
+
87
+ opts.on("-n",
88
+ "--auth METHOD",
89
+ AUTH_METHODS,
90
+ "Authentication method, defaults to '#{options.auth[:type]}'") do |auth|
91
+ options.auth[:type] = auth.to_s
92
+ end
93
+
94
+ opts.on("-u",
95
+ "--username USER",
96
+ String,
97
+ "Username for basic or digest authentication, defaults to '#{options.auth[:username]}'") do |username|
98
+ options.auth[:username] = username
99
+ end
100
+
101
+ opts.on("-p",
102
+ "--password PASSWORD",
103
+ String,
104
+ "Password for basic, digest and x509 authentication") do |password|
105
+ options.auth[:password] = password
106
+ options.auth[:user_cert_password] = password
107
+ end
108
+
109
+ opts.on("-c",
110
+ "--ca-path PATH",
111
+ String,
112
+ "Path to CA certificates directory, defaults to '#{options.auth[:ca_path]}'") do |ca_path|
113
+ raise ArgumentError, "Path specified in --ca-path is not a directory!" unless File.directory? ca_path
114
+ raise ArgumentError, "Path specified in --ca-path is not readable!" unless File.readable? ca_path
115
+
116
+ options.auth[:ca_path] = ca_path
117
+ end
118
+
119
+ opts.on("-f",
120
+ "--ca-file PATH",
121
+ String,
122
+ "Path to CA certificates in a file") do |ca_file|
123
+ raise ArgumentError, "File specified in --ca-file is not a file!" unless File.file? ca_file
124
+ raise ArgumentError, "File specified in --ca-file is not readable!" unless File.readable? ca_file
125
+
126
+ options.auth[:ca_file] = ca_file
127
+ end
128
+
129
+ opts.on("-F",
130
+ "--filter CATEGORY",
131
+ String,
132
+ "Category type identifier to filter categories from model, must be used together with the -m option") do |filter|
133
+ options.filter = filter
134
+ end
135
+
136
+ opts.on("-x",
137
+ "--user-cred FILE",
138
+ String,
139
+ "Path to user's x509 credentials, defaults to '#{options.auth[:user_cert]}'") do |user_cred|
140
+ raise ArgumentError, "File specified in --user-cred is not a file!" unless File.file? user_cred
141
+ raise ArgumentError, "File specified in --user-cred is not readable!" unless File.readable? user_cred
142
+
143
+ options.auth[:user_cert] = user_cred
144
+ end
145
+
146
+ opts.on("-X",
147
+ "--voms",
148
+ "Using VOMS credentials; modifies behavior of the X509 authN module") do |voms|
149
+
150
+ options.auth[:voms] = true
151
+ end
152
+
153
+ opts.on("-y",
154
+ "--media-type MEDIA_TYPE",
155
+ MEDIA_TYPES,
156
+ "Media type for client <-> server communication, defaults to '#{options.media_type}'") do |media_type|
157
+ options.media_type = media_type
158
+ end
159
+
160
+ opts.on("-r",
161
+ "--resource RESOURCE",
162
+ String,
163
+ "Resource to be queried (e.g. network, compute, storage etc.), required") do |resource|
164
+ options.resource = resource
165
+ end
166
+
167
+ opts.on("-t",
168
+ "--attributes ATTRS",
169
+ Array,
170
+ "Comma separated attributes for new resources such as title=\"Name\", required") do |attributes|
171
+ options.attributes = {}
172
+
173
+ attributes.each do |attribute|
174
+ ary = /^(.+?)=(.+)$/.match(attribute).to_a.drop 1
175
+ raise ArgumentError, "Attributes must always contain ATTR=VALUE pairs!" if ary.length != 2
176
+
177
+ options.attributes[ary[0].to_sym] = ary[1]
178
+ end
179
+ end
180
+
181
+ opts.on("-T",
182
+ "--context CTX_VARS",
183
+ Array,
184
+ "Comma separated context variables for new compute resources such as SSH_KEY=\"ssh-rsa dfsdf...adfdf== user@localhost\"") do |context|
185
+ options.context_vars = {}
186
+
187
+ context.each do |ctx|
188
+ ary = /^(.+?)=(.+)$/.match(ctx).to_a.drop 1
189
+ raise ArgumentError, "Context variables must always contain ATTR=VALUE pairs!" if ary.length != 2
190
+
191
+ options.context_vars[ary[0].to_sym] = ary[1]
192
+ end
193
+ end
194
+
195
+ opts.on("-a",
196
+ "--action ACTION",
197
+ ACTIONS,
198
+ "Action to be performed on the resource, required") do |action|
199
+ options.action = action
200
+ end
201
+
202
+ opts.on("-M",
203
+ "--mixin NAME",
204
+ String,
205
+ "Type and name of the mixin as TYPE#NAME (e.g. os_tpl#monitoring, resource_tpl#medium)") do |mixin|
206
+ parts = mixin.split("#")
207
+
208
+ raise "Unknown mixin format! Use TYPE#NAME!" unless parts.length == 2
209
+
210
+ options.mixins = {} if options.mixins.nil?
211
+ options.mixins[parts[0]] = [] if options.mixins[parts[0]].nil?
212
+ options.mixins[parts[0]] << parts[1]
213
+ end
214
+
215
+ opts.on("-j",
216
+ "--link URI",
217
+ String,
218
+ "Link specified resource to the resource being created, only for action CREATE and resource COMPUTE") do |link|
219
+ link_relative_path = URI(link).path
220
+
221
+ raise ArgumentError, "Specified link URI is not valid!" unless link_relative_path.start_with? '/'
222
+
223
+ options.links = [] if options.links.nil?
224
+ options.links << link_relative_path
225
+ end
226
+
227
+ opts.on("-g",
228
+ "--trigger-action TRIGGER",
229
+ String,
230
+ "Action to be triggered on the resource") do |trigger_action|
231
+ options.trigger_action = trigger_action
232
+ end
233
+
234
+ opts.on("-l",
235
+ "--log-to OUTPUT",
236
+ LOG_OUTPUTS,
237
+ "Log to the specified device, defaults to 'STDERR'") do |log_to|
238
+ options.log[:out] = STDOUT if log_to == :stdout or log_to == :STDOUT
239
+ end
240
+
241
+ opts.on("-o",
242
+ "--output-format FORMAT",
243
+ Occi::Cli::ResourceOutputFactory.allowed_formats,
244
+ "Output format, defaults to human-readable 'plain'") do |output_format|
245
+ options.output_format = output_format
246
+ end
247
+
248
+ opts.on_tail("-m",
249
+ "--dump-model",
250
+ "Contact the endpoint and dump its model, cannot be used with the interactive mode") do |dump_model|
251
+ options.dump_model = dump_model
252
+ end
253
+
254
+ opts.on_tail("-d",
255
+ "--debug",
256
+ "Enable debugging messages") do |debug|
257
+ options.debug = debug
258
+ options.log[:level] = Occi::Log::DEBUG
259
+ end
260
+
261
+ opts.on_tail("-b",
262
+ "--verbose",
263
+ "Be more verbose, less intrusive than debug mode") do |verbose|
264
+ options.verbose = verbose
265
+ options.log[:level] = Occi::Log::INFO unless options.log[:level] == Occi::Log::DEBUG
266
+ end
267
+
268
+ opts.on_tail("-h",
269
+ "--help",
270
+ "Show this message") do
271
+ if @@quiet
272
+ exit true
273
+ else
274
+ puts opts
275
+ exit! true
276
+ end
277
+ end
278
+
279
+ opts.on_tail("-v",
280
+ "--version",
281
+ "Show version") do
282
+ if @@quiet
283
+ exit true
284
+ else
285
+ puts Occi::Cli::VERSION
286
+ exit! true
287
+ end
288
+ end
289
+ end
290
+
291
+ begin
292
+ opts.parse!(args)
293
+ rescue Exception => ex
294
+ if @@quiet
295
+ exit false
296
+ else
297
+ puts ex.message.capitalize
298
+ puts opts
299
+ exit!
300
+ end
301
+ end
302
+
303
+ check_restrictions options, opts
304
+
305
+ options
306
+ end
307
+
308
+ private
309
+
310
+ def self.check_restrictions(options, opts)
311
+ if options.interactive && options.dump_model
312
+ if @@quiet
313
+ exit false
314
+ else
315
+ puts "You cannot use '--dump-model' and '--interactive' at the same time!"
316
+ puts opts
317
+ exit!
318
+ end
319
+ end
320
+
321
+ if !options.dump_model && options.filter
322
+ if @@quiet
323
+ exit false
324
+ else
325
+ puts "You cannot use '--filter' without '--dump-model'!"
326
+ puts opts
327
+ exit!
328
+ end
329
+ end
330
+
331
+ if options.voms && options.auth[:type] != "x509"
332
+ if @@quiet
333
+ exit false
334
+ else
335
+ puts "You cannot use '--voms' without '--auth x509'!"
336
+ puts opts
337
+ exit!
338
+ end
339
+ end
340
+
341
+ return if options.interactive || options.dump_model
342
+
343
+ mandatory = []
344
+
345
+ if options.action == :trigger
346
+ mandatory << :trigger_action
347
+ end
348
+
349
+ if options.action == :create
350
+ if !options.links.nil?
351
+ mandatory << :links
352
+ else
353
+ mandatory << :mixins
354
+ end
355
+
356
+ mandatory << :attributes
357
+ check_attrs = true
358
+ end
359
+
360
+ mandatory.concat [:resource, :action]
361
+
362
+ check_hash options, mandatory, opts
363
+
364
+ if check_attrs
365
+ mandatory = [:title]
366
+ check_hash options.attributes, mandatory, opts
367
+ end
368
+ end
369
+
370
+ def self.check_hash(hash, mandatory, opts)
371
+ if !hash.is_a? Hash
372
+ hash = hash.marshal_dump
373
+ end
374
+
375
+ missing = mandatory.select{ |param| hash[param].nil? }
376
+ if !missing.empty?
377
+ if @@quiet
378
+ exit false
379
+ else
380
+ puts "Missing required arguments: #{missing.join(', ')}"
381
+ puts opts
382
+ exit!
383
+ end
384
+ end
385
+ end
386
+ end
387
+
388
+ end
@@ -0,0 +1,95 @@
1
+ require 'json'
2
+ require 'erb'
3
+
4
+ module Occi::Cli
5
+
6
+ class ResourceOutputFactory
7
+
8
+ @@allowed_formats = [:json, :plain].freeze
9
+ @@allowed_resource_types = [:compute, :storage, :network, :os_tpl, :resource_tpl].freeze
10
+ @@allowed_data_types = [:locations, :resources].freeze
11
+
12
+ attr_reader :output_format
13
+
14
+ def initialize(output_format = :plain)
15
+ raise "Unsupported output format!" unless @@allowed_formats.include? output_format
16
+ @output_format = output_format
17
+ end
18
+
19
+ def format(data, data_type, resource_type)
20
+ raise "Data has to be in an array!" unless data.is_a? Array
21
+ raise "Unsupported resource type! Got: #{resource_type.to_s}" unless @@allowed_resource_types.include? resource_type
22
+ raise "Unsupported data type! Got: #{data_type.to_s}" unless @@allowed_data_types.include? data_type
23
+
24
+ # validate incoming data
25
+ if data_type == :resources
26
+ data.each do |occi_collection|
27
+ raise "I need the resources to be in a Collection!" unless occi_collection.respond_to? :as_json
28
+ end
29
+ end
30
+
31
+ # construct method name from data type and output format
32
+ method = (data_type.to_s + "_to_" + output_format.to_s).to_sym
33
+
34
+ ResourceOutputFactory.send method, data, resource_type
35
+ end
36
+
37
+ def self.allowed_formats
38
+ @@allowed_formats
39
+ end
40
+
41
+ def self.allowed_resource_types
42
+ @@allowed_resource_types
43
+ end
44
+
45
+ def self.allowed_data_types
46
+ @@allowed_data_types
47
+ end
48
+
49
+ def self.resources_to_json(occi_resources, resource_type)
50
+ # generate JSON document from an array of JSON objects
51
+ JSON.generate occi_resources
52
+ end
53
+
54
+ def self.resources_to_plain(occi_resources, resource_type)
55
+ # using ERB templates for known resource and mixin types
56
+ file = File.expand_path("..", __FILE__) + '/templates/' + resource_type.to_s + ".erb"
57
+ template = ERB.new File.new(file).read
58
+
59
+ formatted_output = ""
60
+
61
+ occi_resources.each do |occi_resource|
62
+ json_resource = occi_resource.as_json
63
+ formatted_output << template.result(binding) unless json_resource.nil? || json_resource.empty?
64
+ end
65
+
66
+ formatted_output
67
+ end
68
+
69
+ def self.locations_to_json(url_locations, resource_type)
70
+ # give all locatios a common key and convert to JSON
71
+ locations = { resource_type => [] }
72
+
73
+ url_locations.each do |location|
74
+ location = location.split("/").last if [:os_tpl, :resource_tpl].include? resource_type
75
+ locations[resource_type] << location
76
+ end
77
+
78
+ locations.to_json
79
+ end
80
+
81
+ def self.locations_to_plain(url_locations, resource_type)
82
+ # just an attempt to make the array more readable
83
+ output = "\n" + resource_type.to_s.capitalize + " locations:\n"
84
+
85
+ url_locations.each do |location|
86
+ location = location.split("/").last if [:os_tpl, :resource_tpl].include? resource_type
87
+ output << "\t" << location << "\n"
88
+ end
89
+
90
+ output
91
+ end
92
+
93
+ end
94
+
95
+ end
@@ -0,0 +1,18 @@
1
+ <%# We always get a JSON %>
2
+ COMPUTE:
3
+ ID: <%= json_resource.resources.first.attributes.occi.core.id %>
4
+ TITLE: <%= json_resource.resources.first.attributes.occi.core.title %>
5
+ STATE: <%= json_resource.resources.first.attributes.occi.compute.state %>
6
+ LINKS:
7
+ <% if json_resource.links %><% json_resource.links.each do |link| %>
8
+ LINK "<%= link.kind %>":
9
+ ID: <%= link.attributes.occi.core.id %>
10
+ TITLE: <%= link.attributes.occi.core.title %>
11
+ TARGET: <%= link.target %>
12
+ <% if link.attributes.occi.networkinterface %>
13
+ IP ADDRESS: <%= link.attributes.occi.networkinterface.address %>
14
+ MAC ADDRESS: <%= link.attributes.occi.networkinterface.mac %>
15
+ <% elsif link.attributes.occi.storagelink %>
16
+ MOUNT POINT: <%= link.attributes.occi.storagelink.deviceid %>
17
+ <% end %>
18
+ <% end %><% end %>
@@ -0,0 +1,9 @@
1
+ <%# We always get a JSON %>
2
+ NETWORK:
3
+ ID: <%= json_resource.resources.first.attributes.occi.core.id %>
4
+ TITLE: <%= json_resource.resources.first.attributes.occi.core.title %>
5
+ STATE: <%= json_resource.resources.first.attributes.occi.network.state %>
6
+ ALLOCATION: <%= json_resource.resources.first.attributes.occi.network.allocation %>
7
+ ADDRESS: <%= json_resource.resources.first.attributes.occi.network.address %>
8
+
9
+
@@ -0,0 +1,7 @@
1
+ <%# We always get a JSON %>
2
+ OS_TPL:
3
+ TITLE: <%= json_resource.title %>
4
+ TERM: <%= json_resource.term %>
5
+ LOCATION: <%= json_resource.location %>
6
+
7
+
@@ -0,0 +1,7 @@
1
+ <%# We always get a JSON %>
2
+ RESOURCE_TPL:
3
+ TITLE: <%= json_resource.title %>
4
+ TERM: <%= json_resource.term %>
5
+ LOCATION: <%= json_resource.location %>
6
+
7
+
@@ -0,0 +1,8 @@
1
+ <%# We always get a JSON %>
2
+ STORAGE:
3
+ ID: <%= json_resource.resources.first.attributes.occi.core.id %>
4
+ TITLE: <%= json_resource.resources.first.attributes.occi.core.title %>
5
+ STATE: <%= json_resource.resources.first.attributes.occi.storage.state%>
6
+ DESCRIPTION: <%= json_resource.resources.first.attributes.occi.core.summary %>
7
+
8
+
@@ -0,0 +1,5 @@
1
+ module Occi
2
+ module Cli
3
+ VERSION = "4.0.0.alpha.1" unless defined?(::Occi::Cli::VERSION)
4
+ end
5
+ end
data/lib/occi-cli.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'occi-api'
3
+
4
+ module Occi::Cli; end
5
+
6
+ require 'occi/cli/version'
7
+ require 'occi/cli/occi_opts'
8
+ require 'occi/cli/resource_output_factory'
9
+ require 'occi/cli/helpers'
data/occi-cli.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib/', __FILE__)
3
+ $:.unshift lib unless $:.include?(lib)
4
+
5
+ require 'occi/cli/version'
6
+
7
+ Gem::Specification.new do |gem|
8
+ gem.name = "occi-cli"
9
+ gem.version = Occi::Cli::VERSION
10
+ gem.authors = ["Florian Feldhaus","Piotr Kasprzak", "Boris Parak"]
11
+ gem.email = ["florian.feldhaus@gwdg.de", "piotr.kasprzak@gwdg.de", "xparak@mail.muni.cz"]
12
+ gem.description = %q{OCCI is a collection of classes to simplify the implementation of the Open Cloud Computing API in Ruby}
13
+ gem.summary = %q{OCCI toolkit}
14
+ gem.homepage = 'https://github.com/gwdg/rOCCI-cli'
15
+
16
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ gem.files = `git ls-files`.split("\n")
18
+ gem.test_files = `git ls-files -- {test,spec}/*`.split("\n")
19
+ gem.require_paths = ["lib"]
20
+ gem.extensions = 'ext/mkrf_conf.rb'
21
+
22
+ gem.add_dependency 'occi-api'
23
+ gem.add_dependency 'highline'
24
+
25
+ gem.add_development_dependency "rspec"
26
+ gem.add_development_dependency "rake"
27
+ gem.add_development_dependency "builder"
28
+ gem.add_development_dependency "simplecov"
29
+ gem.add_development_dependency "yard"
30
+ gem.add_development_dependency "yard-sinatra"
31
+ gem.add_development_dependency "yard-rspec"
32
+ gem.add_development_dependency "yard-cucumber"
33
+
34
+ gem.required_ruby_version = ">= 1.8.7"
35
+ end
@@ -0,0 +1,9 @@
1
+ # module Occi
2
+ # module Cli
3
+ # describe Helpers do
4
+
5
+ # it "does something"
6
+
7
+ # end
8
+ # end
9
+ # end
@@ -0,0 +1,57 @@
1
+ module Occi
2
+ module Cli
3
+ describe OcciOpts do
4
+
5
+ it "terminates without arguments" do
6
+ expect{Occi::Cli::OcciOpts.parse [], true}.to raise_error()
7
+ end
8
+
9
+ it "terminates when it encounters an unknown argument" do
10
+ expect{Occi::Cli::OcciOpts.parse ["--non-existent", "fake"], true}.to raise_error()
11
+ end
12
+
13
+ it "parses minimal number of required arguments without errors" do
14
+ expect{Occi::Cli::OcciOpts.parse ["--resource", "compute", "--action", "list"], true}.not_to raise_error()
15
+ end
16
+
17
+ it "parses resource and action arguments without errors" do
18
+ begin
19
+ parsed = Occi::Cli::OcciOpts.parse ["--resource", "compute", "--action", "list"], true
20
+ rescue SystemExit
21
+ fail
22
+ end
23
+
24
+ parsed.should_not be_nil
25
+
26
+ parsed.resource.should_not be_nil
27
+ parsed.action.should_not be_nil
28
+
29
+ parsed.resource.should eq("compute")
30
+ parsed.action.should eq(:list)
31
+ end
32
+
33
+ it "shows version"
34
+
35
+ it "shows help"
36
+
37
+ it "doesn't accept unsupported authN methods"
38
+
39
+ it "doesn't accept an invalid URI as an endpoint"
40
+
41
+ it "doesn't accept an invalid URI as a resource"
42
+
43
+ it "doesn't accept unsupported actions"
44
+
45
+ it "doesn't allow create actions without attributes present"
46
+
47
+ it "doesn't allow create actions without either link(s) or mixin(s)"
48
+
49
+ it "doesn't allow create actions without attribute 'title' present"
50
+
51
+ it "doesn't accept malformed mixins"
52
+
53
+ it "doesn't accept malformed attributes"
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ module Occi
2
+ module Cli
3
+ describe ResourceOutputFactory do
4
+
5
+ it "does something"
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'occi-cli'
3
+
4
+ # enable coverage reports
5
+ if ENV['COVERAGE']
6
+ require 'simplecov'
7
+ SimpleCov.start
8
+ end
9
+
10
+ RSpec.configure do |c|
11
+ # in RSpec 3 this will no longer be necessary.
12
+ c.treat_symbols_as_metadata_keys_with_true_values = true
13
+ end