visor-common 0.0.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.
- data/bin/visor-config +44 -0
- data/lib/common/config.rb +257 -0
- data/lib/common/exception.rb +34 -0
- data/lib/common/extensions/array.rb +27 -0
- data/lib/common/extensions/hash.rb +121 -0
- data/lib/common/extensions/logger.rb +9 -0
- data/lib/common/extensions/object.rb +27 -0
- data/lib/common/extensions/yaml.rb +29 -0
- data/lib/common/util.rb +103 -0
- data/lib/common/version.rb +5 -0
- data/lib/visor-common.rb +9 -0
- data/spec/lib/config_spec.rb +102 -0
- data/spec/lib/extensions/hash_spec.rb +138 -0
- data/spec/lib/util_spec.rb +42 -0
- metadata +95 -0
data/bin/visor-config
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# VISoR configuration command line interface script.
|
4
|
+
# Run <visor-config -h> to get more usage help.
|
5
|
+
|
6
|
+
require File.expand_path('../../lib/visor-common', __FILE__)
|
7
|
+
|
8
|
+
unless ARGV.empty?
|
9
|
+
puts %q[Usage: visor-config
|
10
|
+
|
11
|
+
This script will generate the following VISOR configuration and logging directories and files:
|
12
|
+
|
13
|
+
- ~/.visor The VISOR default configuration directory
|
14
|
+
- ~/.visor/visor-config.yml The VISOR configuration file template
|
15
|
+
- ~/.visor/logs The VISOR default logging directory
|
16
|
+
|
17
|
+
Run it without any arguments to generate the above directories and files.
|
18
|
+
|
19
|
+
]
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
|
23
|
+
dir = "#{ENV['HOME']}/.visor"
|
24
|
+
|
25
|
+
if Dir.exists? dir
|
26
|
+
puts "Directory #{dir} already exists, do you want to override it and its files? (y/n)"
|
27
|
+
if gets.chomp.downcase == "n"
|
28
|
+
puts "Aborting configurations override."
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
puts "\nGenerating VISOR configuration directories and files:\n\n"
|
34
|
+
sleep (0.5)
|
35
|
+
print "creating #{dir}..."
|
36
|
+
system "mkdir #{dir}"
|
37
|
+
puts " [DONE]"
|
38
|
+
print "creating #{dir}/logs..."
|
39
|
+
system "mkdir #{dir}/logs"
|
40
|
+
puts " [DONE]"
|
41
|
+
print "creating #{dir}/visor-config.yml..."
|
42
|
+
system "echo '#{Visor::Common::Config::CONFIG_TEMPLATE}' > #{dir}/visor-config.yml"
|
43
|
+
puts " [DONE]"
|
44
|
+
puts "\nAll configurations were successful. Now open and customize the VISOR configuration file at #{dir}/visor-config.yml\n\n"
|
@@ -0,0 +1,257 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Visor
|
5
|
+
module Common
|
6
|
+
|
7
|
+
# The Config module provides a set of utility functions to manipulate configuration
|
8
|
+
# files and Logging.
|
9
|
+
#
|
10
|
+
module Config
|
11
|
+
|
12
|
+
include Visor::Common::Exception
|
13
|
+
|
14
|
+
# Default possible configuration file directories
|
15
|
+
DEFAULT_CONFIG_DIRS = %w{. ~/.visor /etc/visor}
|
16
|
+
# Default configuration file name
|
17
|
+
DEFAULT_CONFIG_FILE = 'visor-config.yml'
|
18
|
+
# Default logs path
|
19
|
+
DEFAULT_LOG_PATH = '~/.visor/logs'
|
20
|
+
# Default log datetime format
|
21
|
+
DEFAULT_LOG_DATETIME = "%Y-%m-%d %H:%M:%S"
|
22
|
+
# Default log level
|
23
|
+
DEFAULT_LOG_LEVEL = Logger::INFO
|
24
|
+
# Configuration file template
|
25
|
+
CONFIG_TEMPLATE = %q[
|
26
|
+
# ========== Default always loaded configuration throughout VISOR sub-systems =====
|
27
|
+
#
|
28
|
+
default: &default
|
29
|
+
# Set the default log date time format
|
30
|
+
log_datetime_format: "%Y-%m-%d %H:%M:%S"
|
31
|
+
# Set the default log path
|
32
|
+
log_path: ~/.visor/logs
|
33
|
+
# VISoR access and secret key credentials, grab yours from $ visor-admin
|
34
|
+
access_key: XXXXXX
|
35
|
+
secret_key: XXXXXX
|
36
|
+
|
37
|
+
# ================================ VISoR Auth ==================================
|
38
|
+
#
|
39
|
+
visor_auth:
|
40
|
+
# Merge default configuration
|
41
|
+
<<: *default
|
42
|
+
# Address to bind the meta server
|
43
|
+
bind_host: 0.0.0.0
|
44
|
+
# Port to bind the meta server
|
45
|
+
bind_port: 4566
|
46
|
+
# Backend connection string (syntax: name://user:pass@host:port/database)
|
47
|
+
backend: mongodb://:@127.0.0.1:27017/visor
|
48
|
+
# backend: mysql://visor:passwd@127.0.0.1:3306/visor
|
49
|
+
# Log file name (available: filename or empty for STDOUT)
|
50
|
+
log_file: visor-auth-server.log
|
51
|
+
# Log level to start logging above events (available: DEBUG, INFO)
|
52
|
+
log_level: INFO
|
53
|
+
|
54
|
+
# ================================ VISoR Meta =====================================
|
55
|
+
#
|
56
|
+
visor_meta:
|
57
|
+
# Merge default configuration
|
58
|
+
<<: *default
|
59
|
+
# Address to bind the meta server
|
60
|
+
bind_host: 0.0.0.0
|
61
|
+
# Port to bind the meta server
|
62
|
+
bind_port: 4567
|
63
|
+
# Backend connection string (syntax: name://user:pass@host:port/database)
|
64
|
+
backend: mongodb://:@127.0.0.1:27017/visor
|
65
|
+
# backend: mysql://visor:passwd@127.0.0.1:3306/visor
|
66
|
+
# Log file name (available: filename or empty for STDOUT)
|
67
|
+
log_file: visor-meta-server.log
|
68
|
+
# Log level to start logging above events (available: DEBUG, INFO)
|
69
|
+
log_level: INFO
|
70
|
+
|
71
|
+
# ================================ VISoR Image ====================================
|
72
|
+
#
|
73
|
+
visor_image:
|
74
|
+
# Merge default configuration
|
75
|
+
<<: *default
|
76
|
+
# Address to bind the meta server
|
77
|
+
bind_host: 0.0.0.0
|
78
|
+
# Port to bind the meta server
|
79
|
+
bind_port: 4568
|
80
|
+
# Log file name (available: filename or empty for STDOUT)
|
81
|
+
log_file: visor-api-server.log
|
82
|
+
# Log level to start logging equal and above events (available: DEBUG, INFO)
|
83
|
+
log_level: INFO
|
84
|
+
|
85
|
+
# ============================== VISoR Image Backends =============================
|
86
|
+
#
|
87
|
+
visor_store:
|
88
|
+
# Default store backend (available: s3, cumulus, walrus, lunacloud, hdfs, file)
|
89
|
+
default: file
|
90
|
+
#
|
91
|
+
# FileSystem store backend settings
|
92
|
+
#
|
93
|
+
file:
|
94
|
+
# Default directory to store image files in
|
95
|
+
directory: ~/VMs/
|
96
|
+
#
|
97
|
+
# Amazon Simple Storage (S3) store backend settings
|
98
|
+
#
|
99
|
+
s3:
|
100
|
+
# The bucket to store images in, make sure you provide an already existing bucket
|
101
|
+
bucket: XXXXXX
|
102
|
+
# Access and secret key credentials, grab yours on your AWS account settings page
|
103
|
+
access_key: XXXXXX
|
104
|
+
secret_key: XXXXXX
|
105
|
+
#
|
106
|
+
# Nimbus Cumulus (Cumulus) store backend settings
|
107
|
+
#
|
108
|
+
cumulus:
|
109
|
+
# The Cumulus host address
|
110
|
+
host: XXXXXX
|
111
|
+
# The Cumulus port number
|
112
|
+
port: XXXXXX
|
113
|
+
# The bucket to store images in, make sure you provide an already existing bucket
|
114
|
+
bucket: XXXXXX
|
115
|
+
# Access and secret key credentials, grab yours with Nimbus cloud
|
116
|
+
access_key: XXXXXX
|
117
|
+
secret_key: XXXXXX
|
118
|
+
#
|
119
|
+
# Eucalyptus Walrus (Walrus) store backend settings
|
120
|
+
#
|
121
|
+
walrus:
|
122
|
+
# The Walrus host address
|
123
|
+
host: XXXXXX
|
124
|
+
# The Walrus port number
|
125
|
+
port: XXXXXX
|
126
|
+
# A
|
127
|
+
# The bucket to store images in, make sure you provide an already existing bucket
|
128
|
+
bucket: XXXXXX
|
129
|
+
# Access and secret key credentials, grab yours with Eucalyptus cloud
|
130
|
+
access_key: XXXXXX
|
131
|
+
secret_key: XXXXXX
|
132
|
+
#
|
133
|
+
# Lunacloud (Lunacloud) store backend settings
|
134
|
+
#
|
135
|
+
lunacloud:
|
136
|
+
# The Lunacloud host address
|
137
|
+
host: betalcs.lunacloud.com
|
138
|
+
# The Lunacloud port number
|
139
|
+
port: 80
|
140
|
+
# The bucket to store images in, make sure you provide an already existing bucket
|
141
|
+
bucket: XXXXXX
|
142
|
+
# Access and secret key credentials, grab yours with Lunacloud cloud
|
143
|
+
access_key: XXXXXX
|
144
|
+
secret_key: XXXXXX
|
145
|
+
#
|
146
|
+
# Apache Hadoop HDFS (HDFS) store backend settings
|
147
|
+
#
|
148
|
+
hdfs:
|
149
|
+
# The HDFS host address
|
150
|
+
host: XXXXXX
|
151
|
+
# The HDFS port number
|
152
|
+
port: XXXXXX
|
153
|
+
# The bucket to store images in
|
154
|
+
bucket: XXXXXX
|
155
|
+
# Access credentials
|
156
|
+
username: XXXXXX
|
157
|
+
]
|
158
|
+
|
159
|
+
# Ordered search for a VISoR configuration file on default locations and return the first matched.
|
160
|
+
#
|
161
|
+
# @param other_file [String] Other file to use instead of default config files.
|
162
|
+
#
|
163
|
+
# @return [nil, String] Returns nil if no valid config file was found or a string with the
|
164
|
+
# absolute path to the found configuration file.
|
165
|
+
#
|
166
|
+
def self.find_config_file(other_file = nil)
|
167
|
+
if other_file
|
168
|
+
file = File.expand_path(other_file)
|
169
|
+
File.exists?(file) ? file : nil
|
170
|
+
else
|
171
|
+
DEFAULT_CONFIG_DIRS.each do |dir|
|
172
|
+
file = File.join(File.expand_path(dir), DEFAULT_CONFIG_FILE)
|
173
|
+
return file if File.exists?(file)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Looks for a VISoR configuration file througth {#self.find_config_file} and returns a hash with
|
179
|
+
# all configuration settings or just a sub-system scoped settings.
|
180
|
+
#
|
181
|
+
# @param scope [String] Used to return just the settings about a specific sub-system.
|
182
|
+
# @param other_file [String] Other file to use instead of default config files.
|
183
|
+
#
|
184
|
+
# @return [Hash] Global or scoped settings.
|
185
|
+
#
|
186
|
+
# @raise [RuntimeError] If there is no configuration files or if errors occur during parsing.
|
187
|
+
#
|
188
|
+
#TODO: YAML.load_openstruct(File.read(file))
|
189
|
+
def self.load_config(scope = nil, other_file = nil)
|
190
|
+
file = find_config_file(other_file)
|
191
|
+
raise ConfigError, "Could not found any configuration file." unless file
|
192
|
+
begin
|
193
|
+
content = YAML.load_file(file).symbolize_keys
|
194
|
+
rescue => e
|
195
|
+
raise ConfigError, "Error while parsing the configuration file: #{e.message}."
|
196
|
+
end
|
197
|
+
config = scope ? content[scope] : content
|
198
|
+
config.merge(file: file)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Build and return a Logger instance for a given VISoR sub-system, based on configuration
|
202
|
+
# file options, which are validated througth {#self.validate_logger}.
|
203
|
+
#
|
204
|
+
# @param app_name [Symbol] The VISoR sub-system app name to build a log for.
|
205
|
+
# @option configs [Hash] Optional configuration options to override config file ones.
|
206
|
+
#
|
207
|
+
# @return [Logger] A logger instance already properly configured.
|
208
|
+
#
|
209
|
+
def self.build_logger(app_name, configs = nil)
|
210
|
+
conf = configs || load_config
|
211
|
+
|
212
|
+
raise ConfigError, "Cannot locate 'default' configuration group." unless conf[:default]
|
213
|
+
raise ConfigError, "Cannot locate '#{app_name}' configuration group." unless conf[app_name]
|
214
|
+
|
215
|
+
log_path = File.expand_path(conf[:default][:log_path] || DEFAULT_LOG_PATH)
|
216
|
+
log_datetime = conf[:default][:log_datetime_format] || DEFAULT_LOG_DATETIME
|
217
|
+
log_file = conf[app_name][:log_file] || STDOUT
|
218
|
+
log_level = conf[app_name][:log_level] || DEFAULT_LOG_LEVEL
|
219
|
+
|
220
|
+
begin
|
221
|
+
Dir.mkdir(log_path) unless Dir.exists?(log_path)
|
222
|
+
rescue => e
|
223
|
+
raise ConfigError, "Cannot create the 'default/log_path' directory: #{e.message}."
|
224
|
+
end
|
225
|
+
|
226
|
+
begin
|
227
|
+
output = log_file==STDOUT ? log_file : File.join(log_path, log_file)
|
228
|
+
log = Logger.new(output, 5, 1024*1024)
|
229
|
+
rescue => e
|
230
|
+
raise ConfigError, "Error while create the logger for #{output}: #{e.message}."
|
231
|
+
end
|
232
|
+
|
233
|
+
begin
|
234
|
+
log.datetime_format = log_datetime
|
235
|
+
log.level = get_log_level(log_level)
|
236
|
+
rescue => e
|
237
|
+
raise ConfigError, "Error while setting logger properties: #{e.message}."
|
238
|
+
end
|
239
|
+
log
|
240
|
+
end
|
241
|
+
|
242
|
+
private
|
243
|
+
|
244
|
+
def self.get_log_level(level)
|
245
|
+
case level
|
246
|
+
when 'DEBUG' then
|
247
|
+
Logger::DEBUG
|
248
|
+
when 'INFO' then
|
249
|
+
Logger::INFO
|
250
|
+
else
|
251
|
+
DEFAULT_LOG_LEVEL
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Visor
|
2
|
+
module Common
|
3
|
+
|
4
|
+
# The Exceptions module introduces a set of custom exceptions used along
|
5
|
+
# all VISoR sub-systems.
|
6
|
+
#
|
7
|
+
module Exception
|
8
|
+
|
9
|
+
# Raise if invalid data is provided within new metadata
|
10
|
+
class Invalid < ArgumentError; end
|
11
|
+
|
12
|
+
# Raise if no image meta or file path is not found
|
13
|
+
class NotFound < StandardError; end
|
14
|
+
|
15
|
+
# Raise on a configuration error
|
16
|
+
class ConfigError < RuntimeError; end
|
17
|
+
|
18
|
+
# Raise if provided store backend is not supported
|
19
|
+
class UnsupportedStore < RuntimeError; end
|
20
|
+
|
21
|
+
# Raise if a record or file is already stored
|
22
|
+
class Duplicated < RuntimeError; end
|
23
|
+
|
24
|
+
# Raise on an internal server error
|
25
|
+
class InternalError < RuntimeError; end
|
26
|
+
|
27
|
+
# Raise on error trying to manipulate image files
|
28
|
+
class Forbidden < RuntimeError; end
|
29
|
+
|
30
|
+
# Raise on error trying to update image files/meta
|
31
|
+
class ConflictError < RuntimeError; end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Visor
|
2
|
+
module Common
|
3
|
+
|
4
|
+
# The Module Extensions provides a set of functions to extend the Standard Core Libraries
|
5
|
+
# with custom usefull methods used allong all VISoR sub-systems.
|
6
|
+
#
|
7
|
+
module Extensions
|
8
|
+
#
|
9
|
+
# Extending Array class
|
10
|
+
#
|
11
|
+
module Array
|
12
|
+
|
13
|
+
# Convert each array element to an OpenStruct.
|
14
|
+
# Used for Hash.to_openstruct
|
15
|
+
#
|
16
|
+
# @return [Array] The array with elements converted to OpenStruct.
|
17
|
+
#
|
18
|
+
def to_openstruct
|
19
|
+
map { |el| el.to_openstruct }
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Array.send :include, Visor::Common::Extensions::Array
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Visor
|
4
|
+
module Common
|
5
|
+
|
6
|
+
# The Module Extensions provides a set of functions to extend the Standard Core Libraries
|
7
|
+
# with custom usefull methods used allong all VISoR sub-systems.
|
8
|
+
#
|
9
|
+
module Extensions
|
10
|
+
#
|
11
|
+
# Extending Hash class
|
12
|
+
#
|
13
|
+
module Hash
|
14
|
+
|
15
|
+
# Return a new hash with all keys converted to strings.
|
16
|
+
#
|
17
|
+
def stringify_keys
|
18
|
+
inject({}) do |acc, (k, v)|
|
19
|
+
key = Symbol === k ? k.to_s : k
|
20
|
+
value = Hash === v ? v.stringify_keys : v
|
21
|
+
acc[key] = value
|
22
|
+
acc
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Destructively convert all keys to strings.
|
27
|
+
#
|
28
|
+
def stringify_keys!
|
29
|
+
self.replace(self.stringify_keys)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return a new hash with all keys converted to symbols.
|
33
|
+
#
|
34
|
+
def symbolize_keys
|
35
|
+
inject({}) do |acc, (k, v)|
|
36
|
+
key = String === k ? k.to_sym : k
|
37
|
+
value = Hash === v ? v.symbolize_keys : v
|
38
|
+
acc[key] = value
|
39
|
+
acc
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Destructively convert all keys to symbols.
|
44
|
+
#
|
45
|
+
def symbolize_keys!
|
46
|
+
self.replace(self.symbolize_keys)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Validate all keys in a hash match *valid keys.
|
50
|
+
#
|
51
|
+
# @param [Array] valid_keys Valid keys.
|
52
|
+
#
|
53
|
+
# @raise [ArgumentError] On a mismatch.
|
54
|
+
#
|
55
|
+
def assert_valid_keys(*valid_keys)
|
56
|
+
unknown_keys = keys - valid_keys.flatten
|
57
|
+
raise ArgumentError, "Unknown fields: #{unknown_keys.join(", ")}" unless unknown_keys.empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Validate inclusions of some keys in a hash.
|
61
|
+
#
|
62
|
+
# @param [Array] inclusion_keys Keys that must be included in the hash.
|
63
|
+
#
|
64
|
+
# @raise [ArgumentError] If some key is not included.
|
65
|
+
#
|
66
|
+
def assert_inclusion_keys(*inclusion_keys)
|
67
|
+
inc = inclusion_keys.flatten - keys
|
68
|
+
raise ArgumentError, "These fields are required: #{inc.join(', ')}" unless inc.empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Validate non-inclusion of some keys in a hash.
|
72
|
+
#
|
73
|
+
# @param [Array] exclude_keys Keys that must be not be included in the hash.
|
74
|
+
#
|
75
|
+
# @raise [ArgumentError] If some key is included.
|
76
|
+
#
|
77
|
+
def assert_exclusion_keys(*exclude_keys)
|
78
|
+
exc = exclude_keys.flatten & keys
|
79
|
+
raise ArgumentError, "These fields are read-only: #{exc.join(', ')}" unless exc.empty?
|
80
|
+
end
|
81
|
+
|
82
|
+
# Validate value of some key in a hash.
|
83
|
+
#
|
84
|
+
# @param [Array] valid_values Values to assert against the given key.
|
85
|
+
# @param [Array] key The key to assert its value.
|
86
|
+
#
|
87
|
+
# @raise [ArgumentError] If some key is included.
|
88
|
+
#
|
89
|
+
def assert_valid_values_for(key, *valid_values)
|
90
|
+
unless valid_values.flatten.include?(self[key.to_sym]) || self[key].nil?
|
91
|
+
raise ArgumentError, "Invalid #{key.to_s} '#{self[key]}', available options: #{valid_values.join(', ')}"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Set some keys in a hash to a given value, possibly ignoring some keys.
|
96
|
+
#
|
97
|
+
# @param [Array] keys_to_set Keys to set its value.
|
98
|
+
# @param [Array] keys_to_ignore Keys to be ignored.
|
99
|
+
# @param [Array] to_value Value to set the wanted keys.
|
100
|
+
#
|
101
|
+
# @raise [ArgumentError] If some key is included.
|
102
|
+
#
|
103
|
+
def set_blank_keys_value_to(keys_to_set, keys_to_ignore, to_value)
|
104
|
+
keys_to_set.each { |k| self.merge!(k => to_value) unless self[k] || keys_to_ignore.include?(k) }
|
105
|
+
end
|
106
|
+
|
107
|
+
# Convert a Hash to a OpenStruct, so it can be accessed as options like: h[key] => h.key
|
108
|
+
#
|
109
|
+
# @return [OpenStruct] The resulting OpenStruct object.
|
110
|
+
#
|
111
|
+
def to_openstruct
|
112
|
+
mapped = {}
|
113
|
+
each { |key, value| mapped[key] = value.to_openstruct }
|
114
|
+
OpenStruct.new(mapped)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
Hash.send :include, Visor::Common::Extensions::Hash
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Visor
|
2
|
+
module Common
|
3
|
+
|
4
|
+
# The Module Extensions provides a set of functions to extend the Standard Core Libraries
|
5
|
+
# with custom usefull methods used allong all VISoR sub-systems.
|
6
|
+
#
|
7
|
+
module Extensions
|
8
|
+
#
|
9
|
+
# Extending Object class
|
10
|
+
#
|
11
|
+
module Object
|
12
|
+
|
13
|
+
# Pass from Hash.to_openstruct
|
14
|
+
#
|
15
|
+
# @return [self] The object itself.
|
16
|
+
#
|
17
|
+
def to_openstruct
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Object.send :include, Visor::Common::Extensions::Object
|
27
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Visor
|
2
|
+
module Common
|
3
|
+
|
4
|
+
# The Module Extensions provides a set of functions to extend the Standard Core Libraries
|
5
|
+
# with custom usefull methods used allong all VISoR sub-systems.
|
6
|
+
#
|
7
|
+
module Extensions
|
8
|
+
#
|
9
|
+
# Extending YAML library
|
10
|
+
#
|
11
|
+
module YAML
|
12
|
+
|
13
|
+
# Load a YAML source to to an OpenStruct object.
|
14
|
+
# Used for Hash.to_openstruct
|
15
|
+
#
|
16
|
+
# @param source [YAML] A file or a parsed YAML object.
|
17
|
+
#
|
18
|
+
# @return [OpenStruct] YAML file parsed to an OpenStruct object.
|
19
|
+
#
|
20
|
+
def self.load_openstruct(source)
|
21
|
+
self.load(source).to_openstruct
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
YAML.send :include, Visor::Common::Extensions::YAML
|
data/lib/common/util.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'openssl'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Visor
|
6
|
+
module Common
|
7
|
+
|
8
|
+
# The Util module provides a set of utility functions used along all VISoR sub-systems.
|
9
|
+
#
|
10
|
+
module Util
|
11
|
+
extend self
|
12
|
+
|
13
|
+
# Push a hash containing image metadata into an HTTP header.
|
14
|
+
# Each key value pair is pushed as a string of the form 'x-image-meta-<key>'.
|
15
|
+
#
|
16
|
+
# @param meta [Hash] The image metadata
|
17
|
+
# @param header [Hash] (nil) The HTTP headers hash
|
18
|
+
#
|
19
|
+
# @return [Hash] The header containing the metadata headers
|
20
|
+
#
|
21
|
+
def push_meta_into_headers(meta, headers = {})
|
22
|
+
meta.each { |k, v| headers["x-image-meta-#{k.to_s.downcase}"] = v.to_s }
|
23
|
+
headers
|
24
|
+
end
|
25
|
+
|
26
|
+
def pull_meta_from_headers(headers)
|
27
|
+
meta = {}
|
28
|
+
headers.each do |k, v|
|
29
|
+
if key = k.split(/x[_-]image[_-]meta[_-]/i)[1]
|
30
|
+
value = parse_value v
|
31
|
+
meta[key.downcase.to_sym] = value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
meta
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_value(string)
|
38
|
+
if is_integer?(string) then
|
39
|
+
Integer(string)
|
40
|
+
elsif is_float?(string) then
|
41
|
+
Float(object)
|
42
|
+
elsif is_date?(string) then
|
43
|
+
Time.parse(string)
|
44
|
+
else
|
45
|
+
string
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_integer?(object)
|
50
|
+
true if Integer(object) rescue false
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_float?(object)
|
54
|
+
true if Float(object) rescue false
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_date?(object)
|
58
|
+
regexp = /\d{4}[-\/]\d{1,2}[-\/]\d{1,2}\s\d{2}:\d{2}:\d{2}\s\W\d{4}/
|
59
|
+
object.match(regexp) ? true : false
|
60
|
+
end
|
61
|
+
|
62
|
+
def sign_request(access_key, secret_key, method, path, headers={})
|
63
|
+
headers['Date'] ||= Time.now.utc.httpdate
|
64
|
+
desc = canonical_description(method, path, headers)
|
65
|
+
signature = Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), secret_key, desc)).strip
|
66
|
+
|
67
|
+
headers['Authorization'] = "VISOR #{access_key}:#{signature}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def canonical_description(method, path, headers={})
|
71
|
+
attributes = {}
|
72
|
+
headers.each do |key, value|
|
73
|
+
key = key.downcase
|
74
|
+
attributes[key] = value.to_s.strip if key.match(/^x-image-meta-|^content-md5$|^content-type$|^date$/o)
|
75
|
+
end
|
76
|
+
|
77
|
+
attributes['content-type'] ||= ''
|
78
|
+
attributes['content-md5'] ||= ''
|
79
|
+
|
80
|
+
desc = "#{method}\n"
|
81
|
+
attributes.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
|
82
|
+
desc << (key.match(/^x-image-meta-/o) ? "#{key}:#{value}\n" : "#{value}\n")
|
83
|
+
end
|
84
|
+
desc << path.gsub(/\?.*$/, '')
|
85
|
+
end
|
86
|
+
|
87
|
+
def authorize(env, vas)
|
88
|
+
auth = env['headers']['Authorization']
|
89
|
+
raise Visor::Common::Exception::Forbidden, "Authorization not provided." unless auth
|
90
|
+
access_key = auth.scan(/\ (\w+):/).flatten.first
|
91
|
+
raise Visor::Common::Exception::Forbidden, "No access key found in Authorization." unless access_key
|
92
|
+
user = vas.get_user(access_key) rescue nil
|
93
|
+
raise Visor::Common::Exception::Forbidden, "No user found with access key '#{access_key}'." unless user
|
94
|
+
sign = sign_request(user[:access_key], user[:secret_key], env['REQUEST_METHOD'], env['REQUEST_PATH'], env['headers'])
|
95
|
+
raise Visor::Common::Exception::Forbidden, "Invalid authorization, signatures do not match." unless auth == sign
|
96
|
+
access_key
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
#sign_request('key', 'secret', 'GET', '/users/joaodrp', {'x-image-meta-name' => 'hi'})
|
data/lib/visor-common.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
$:.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require 'common/exception'
|
3
|
+
require 'common/config'
|
4
|
+
require 'common/util'
|
5
|
+
require 'common/extensions/object'
|
6
|
+
require 'common/extensions/array'
|
7
|
+
require 'common/extensions/yaml'
|
8
|
+
require 'common/extensions/hash'
|
9
|
+
require 'common/extensions/logger'
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Visor::Common
|
4
|
+
describe Config do
|
5
|
+
|
6
|
+
let(:file) { File.join(File.expand_path('~/.visor'), Config::DEFAULT_CONFIG_FILE) }
|
7
|
+
let(:some_file) { File.expand_path('~/some_file') }
|
8
|
+
let(:invalid_file) { '~/some_invalid_file' }
|
9
|
+
|
10
|
+
let(:sample_conf) { {default:
|
11
|
+
{log_datetime_format: "%Y-%m-%d %H:%M:%S",
|
12
|
+
log_path: "~/.visor/logs"},
|
13
|
+
visor_meta:
|
14
|
+
{bind_host: "0.0.0.0", bind_port: 4567,
|
15
|
+
backend: "mongodb://:@127.0.0.1:27017/visor",
|
16
|
+
log: {file: "visor-meta.log", level: "DEBUG"}}} }
|
17
|
+
|
18
|
+
before(:all) do
|
19
|
+
unless File.exists?(file)
|
20
|
+
File.open(file, 'w') do |f|
|
21
|
+
f.write(sample_conf.stringify_keys.to_yaml)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
File.open(some_file, 'w') do |f|
|
26
|
+
f.write(sample_conf.stringify_keys.to_yaml)
|
27
|
+
end
|
28
|
+
|
29
|
+
File.open(File.expand_path(invalid_file), 'w') do |f|
|
30
|
+
f.write('this will break YAML parsing')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
after(:all) do
|
35
|
+
#File.delete(some_file, File.expand_path(invalid_file))
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#find_config_file" do
|
39
|
+
it "should find a configuration file in default dirs if it exists" do
|
40
|
+
Config.find_config_file.should == file
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should get an existing config file as option" do
|
44
|
+
Config.find_config_file(some_file).should == some_file
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should return nil if no configuration file found" do
|
48
|
+
Config.find_config_file('fake_file').should be_nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#load_config" do
|
53
|
+
it "should raise if there is no configuration files" do
|
54
|
+
lambda { Config.load_config(nil, invalid_file) }.should raise_exception
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should return a valid non-empty hash" do
|
58
|
+
conf = Config.load_config
|
59
|
+
conf.should be_a Hash
|
60
|
+
conf.should_not be_empty
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should append the configuration file full path to hash" do
|
64
|
+
conf = Config.load_config
|
65
|
+
conf[:file].should_not be_nil
|
66
|
+
File.exists?(conf[:file]).should be_true
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should return scoped configuration" do
|
70
|
+
conf = Config.load_config :default
|
71
|
+
conf.keys.should == sample_conf[:default].keys << :file
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should raise an exception if an error occurs during parsing" do
|
75
|
+
lambda { Config.load_config(nil, invalid_file) }.should raise_exception
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#build_logger" do
|
80
|
+
it "should return a Logger for a specific app" do
|
81
|
+
log = Config.build_logger :visor_meta
|
82
|
+
log.should be_a Logger
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should raise an exception if an error occurs loading the config file" do
|
86
|
+
lambda { Config.build_logger :fake_scope }.should raise_exception
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should set the log level if provided" do
|
90
|
+
log = Config.build_logger :visor_meta, sample_conf
|
91
|
+
log.level.should == Logger::DEBUG
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should set the log level to the default if not provided" do
|
95
|
+
sample_conf[:visor_meta][:log].delete(:level)
|
96
|
+
log = Config.build_logger :visor_meta, sample_conf
|
97
|
+
log.level.should == Config::DEFAULT_LOG_LEVEL
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Visor::Common::Extensions
|
4
|
+
describe Hash do
|
5
|
+
|
6
|
+
let(:h_symbols) { {a: 1, b: 2, c: {d: 4, e: 5}} }
|
7
|
+
let(:h_strings) { {'a' => 1, 'b' => 2, 'c' => {'d' => 4, 'e' => 5}} }
|
8
|
+
|
9
|
+
let(:simple_hash) { {a: 1, b: 2, c: 3, d: 4, e: 5} }
|
10
|
+
let(:valid_keys) { [:a, :b, :c, :d, :e] }
|
11
|
+
let(:inclusion_keys) { [:a, :b, :c, :d, :e] }
|
12
|
+
let(:exclusion_keys) { [:f, :g] }
|
13
|
+
|
14
|
+
describe "#stringify_keys" do
|
15
|
+
it "should return a new hash" do
|
16
|
+
h_symbols.stringify_keys.should be_a Hash
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should return the hash with all keys stringified" do
|
20
|
+
h_symbols.stringify_keys.keys.each { |k| k.should be_a String }
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should succed at any hash depth" do
|
24
|
+
new = h_symbols.stringify_keys
|
25
|
+
new['c'].keys.each { |k| k.should be_a String }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#stringify_keys!" do
|
30
|
+
it "should replace the hash with all keys stringified" do
|
31
|
+
h_symbols.stringify_keys!
|
32
|
+
h_symbols.keys.each { |k| k.should be_a String }
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should succed at any hash depth" do
|
36
|
+
h_symbols.stringify_keys!
|
37
|
+
h_symbols['c'].keys.each { |k| k.should be_a String }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#symbolize_keys" do
|
42
|
+
it "should return a new hash" do
|
43
|
+
h_strings.symbolize_keys.should be_a Hash
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should return the hash with all keys symbolized" do
|
47
|
+
h_strings.symbolize_keys.keys.each { |k| k.should be_a Symbol }
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should succed at any hash depth" do
|
51
|
+
new = h_strings.symbolize_keys
|
52
|
+
new[:c].keys.each { |k| k.should be_a Symbol }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#symbolize_keys!" do
|
57
|
+
it "should replace the hash with all keys symbolized" do
|
58
|
+
h_strings.symbolize_keys!
|
59
|
+
h_strings.keys.each { |k| k.should be_a Symbol }
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should succed at any hash depth" do
|
63
|
+
h_strings.symbolize_keys!
|
64
|
+
h_strings[:c].keys.each { |k| k.should be_a Symbol }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#assert_valid_keys" do
|
69
|
+
it "should validate that all keys are valid" do
|
70
|
+
l = lambda { simple_hash.assert_valid_keys(simple_hash.keys) }
|
71
|
+
l.should_not raise_exception
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should raise if any key is not valid" do
|
75
|
+
l = lambda { simple_hash.assert_valid_keys([:x, :y, :z]) }
|
76
|
+
l.should raise_exception ArgumentError
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "#assert_inclusion_keys" do
|
81
|
+
it "should validate that all mandatory keys are present" do
|
82
|
+
l = lambda { simple_hash.assert_inclusion_keys(inclusion_keys) }
|
83
|
+
l.should_not raise_exception
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should raise if any mandatory keys are not present" do
|
87
|
+
l = lambda { simple_hash.assert_inclusion_keys(inclusion_keys << :new_one) }
|
88
|
+
l.should raise_exception ArgumentError
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#assert_exclusion_keys" do
|
93
|
+
it "should validate that all exclusion keys are not present" do
|
94
|
+
l = lambda { simple_hash.assert_exclusion_keys(exclusion_keys) }
|
95
|
+
l.should_not raise_exception
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should raise if any exclusion keys are present" do
|
99
|
+
l = lambda { simple_hash.assert_exclusion_keys(exclusion_keys << :a) }
|
100
|
+
l.should raise_exception ArgumentError
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#assert_valid_values_for" do
|
105
|
+
it "should validate that a given key has a given possible value" do
|
106
|
+
l = lambda { simple_hash.assert_valid_values_for(:a, [1, 2, 3]) }
|
107
|
+
l.should_not raise_exception
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should raise if any exclusion keys are present" do
|
111
|
+
l = lambda { simple_hash.assert_valid_values_for(:a, [4, 5, 6]) }
|
112
|
+
l.should raise_exception ArgumentError
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "#set_blank_keys_value_to" do
|
117
|
+
it "should fill blank/non existing keys to a given value" do
|
118
|
+
simple_hash.set_blank_keys_value_to([:x, :y, :z], [], 1)
|
119
|
+
[:x, :y, :z].each { |el| simple_hash[el].should == 1 }
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should ignore some fields if asked to" do
|
123
|
+
simple_hash.set_blank_keys_value_to([:x, :y, :z], [:z], 1)
|
124
|
+
[:x, :y].each { |el| simple_hash[el].should == 1 }
|
125
|
+
simple_hash[:z].should be_nil
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "#to_openstruct" do
|
130
|
+
it "should return an OpenStruct object from a hash one" do
|
131
|
+
os = simple_hash.to_openstruct
|
132
|
+
os.should be_a OpenStruct
|
133
|
+
os.a.should == simple_hash[:a]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Visor::Common
|
4
|
+
describe Util do
|
5
|
+
|
6
|
+
let(:date) { Time.now }
|
7
|
+
let(:meta) { {name: 'util_spec', architecture: 'i386', access: 'public', created_at: date} }
|
8
|
+
|
9
|
+
let(:headers) { {'X_IMAGE_META_NAME' => 'util_spec',
|
10
|
+
'X_IMAGE_META_ARCHITECTURE' => 'i386',
|
11
|
+
'X_IMAGE_META_ACCESS' => 'public',
|
12
|
+
'X_IMAGE_META_CREATED_AT' => date.to_s} }
|
13
|
+
|
14
|
+
describe "#push_meta_into_headers" do
|
15
|
+
it "should receive an hash and push it into another as HTTP headers" do
|
16
|
+
headers = Visor::Common::Util.push_meta_into_headers(meta)
|
17
|
+
headers.should be_a Hash
|
18
|
+
i = 0
|
19
|
+
headers.each do |k, v|
|
20
|
+
orig_key = meta.keys[i]
|
21
|
+
k.should == "x-image-meta-#{orig_key}"
|
22
|
+
v.should == meta[orig_key.to_sym].to_s
|
23
|
+
i+=1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#push_meta_into_headers" do
|
29
|
+
it "should receive an hash and pull HTTP headers to a new hash" do
|
30
|
+
hash = Visor::Common::Util.pull_meta_from_headers(headers)
|
31
|
+
hash.should be_a Hash
|
32
|
+
hash.keys.should == meta.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should ignore non image meta headers" do
|
36
|
+
hash = Visor::Common::Util.pull_meta_from_headers(headers.merge('X_EXTRA' => 'value'))
|
37
|
+
hash.should_not include(:X_EXTRA)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: visor-common
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- João Pereira
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: yard
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: The VISOR Common System, a set of utility methods.
|
47
|
+
email: joaodrp@gmail.com
|
48
|
+
executables:
|
49
|
+
- visor-config
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- lib/common/config.rb
|
54
|
+
- lib/common/exception.rb
|
55
|
+
- lib/common/extensions/array.rb
|
56
|
+
- lib/common/extensions/hash.rb
|
57
|
+
- lib/common/extensions/logger.rb
|
58
|
+
- lib/common/extensions/object.rb
|
59
|
+
- lib/common/extensions/yaml.rb
|
60
|
+
- lib/common/util.rb
|
61
|
+
- lib/common/version.rb
|
62
|
+
- lib/visor-common.rb
|
63
|
+
- spec/lib/config_spec.rb
|
64
|
+
- spec/lib/extensions/hash_spec.rb
|
65
|
+
- spec/lib/util_spec.rb
|
66
|
+
- bin/visor-config
|
67
|
+
homepage: http://cvisor.org
|
68
|
+
licenses: []
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 1.9.2
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 1.8.24
|
88
|
+
signing_key:
|
89
|
+
specification_version: 3
|
90
|
+
summary: ! 'VISOR: Virtual Images Management Service for Cloud Infrastructures'
|
91
|
+
test_files:
|
92
|
+
- spec/lib/config_spec.rb
|
93
|
+
- spec/lib/extensions/hash_spec.rb
|
94
|
+
- spec/lib/util_spec.rb
|
95
|
+
has_rdoc:
|