occi-cli 4.0.0.alpha.1

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