beaker 0.0.0 → 1.0.0

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.
Files changed (98) hide show
  1. checksums.yaml +8 -8
  2. data/.travis.yml +8 -0
  3. data/README.md +6 -6
  4. data/beaker.gemspec +6 -2
  5. data/lib/beaker.rb +1 -1
  6. data/lib/beaker/answers.rb +34 -7
  7. data/lib/beaker/answers/version20.rb +124 -0
  8. data/lib/beaker/answers/version28.rb +21 -0
  9. data/lib/beaker/answers/version30.rb +24 -5
  10. data/lib/beaker/cli.rb +55 -41
  11. data/lib/beaker/command.rb +2 -2
  12. data/lib/beaker/dsl/helpers.rb +320 -106
  13. data/lib/beaker/dsl/install_utils.rb +202 -81
  14. data/lib/beaker/dsl/roles.rb +40 -0
  15. data/lib/beaker/host.rb +28 -20
  16. data/lib/beaker/host/unix.rb +7 -4
  17. data/lib/beaker/host/unix/pkg.rb +42 -12
  18. data/lib/beaker/host/windows.rb +9 -5
  19. data/lib/beaker/host/windows/group.rb +1 -1
  20. data/lib/beaker/host/windows/pkg.rb +41 -8
  21. data/lib/beaker/hypervisor.rb +23 -10
  22. data/lib/beaker/hypervisor/aixer.rb +15 -19
  23. data/lib/beaker/hypervisor/blimper.rb +71 -72
  24. data/lib/beaker/hypervisor/fusion.rb +11 -10
  25. data/lib/beaker/hypervisor/solaris.rb +17 -23
  26. data/lib/beaker/hypervisor/vagrant.rb +27 -12
  27. data/lib/beaker/hypervisor/vcloud.rb +154 -138
  28. data/lib/beaker/hypervisor/vcloud_pooled.rb +97 -0
  29. data/lib/beaker/hypervisor/vsphere.rb +8 -5
  30. data/lib/beaker/hypervisor/vsphere_helper.rb +43 -33
  31. data/lib/beaker/network_manager.rb +16 -12
  32. data/lib/beaker/options/command_line_parser.rb +199 -0
  33. data/lib/beaker/options/hosts_file_parser.rb +39 -0
  34. data/lib/beaker/options/options_file_parser.rb +45 -0
  35. data/lib/beaker/options/options_hash.rb +294 -0
  36. data/lib/beaker/options/parser.rb +288 -0
  37. data/lib/beaker/options/pe_version_scraper.rb +35 -0
  38. data/lib/beaker/options/presets.rb +70 -0
  39. data/lib/beaker/shared.rb +2 -1
  40. data/lib/beaker/shared/host_handler.rb +7 -2
  41. data/lib/beaker/shared/repetition.rb +1 -0
  42. data/lib/beaker/shared/timed.rb +14 -0
  43. data/lib/beaker/test_case.rb +2 -38
  44. data/lib/beaker/test_suite.rb +11 -25
  45. data/lib/beaker/utils/repo_control.rb +6 -8
  46. data/lib/beaker/utils/setup_helper.rb +9 -20
  47. data/spec/beaker/answers_spec.rb +109 -0
  48. data/spec/beaker/command_spec.rb +2 -2
  49. data/spec/beaker/dsl/assertions_spec.rb +1 -3
  50. data/spec/beaker/dsl/helpers_spec.rb +519 -84
  51. data/spec/beaker/dsl/install_utils_spec.rb +265 -16
  52. data/spec/beaker/dsl/roles_spec.rb +31 -10
  53. data/spec/beaker/host/windows/group_spec.rb +55 -0
  54. data/spec/beaker/host_spec.rb +130 -40
  55. data/spec/beaker/hypervisor/aixer_spec.rb +34 -0
  56. data/spec/beaker/hypervisor/blimper_spec.rb +77 -0
  57. data/spec/beaker/hypervisor/fusion_spec.rb +26 -0
  58. data/spec/beaker/hypervisor/hypervisor_spec.rb +66 -0
  59. data/spec/beaker/hypervisor/solaris_spec.rb +39 -0
  60. data/spec/beaker/hypervisor/vagrant_spec.rb +105 -0
  61. data/spec/beaker/hypervisor/vcloud_pooled_spec.rb +60 -0
  62. data/spec/beaker/hypervisor/vcloud_spec.rb +70 -0
  63. data/spec/beaker/hypervisor/vsphere_helper_spec.rb +162 -0
  64. data/spec/beaker/hypervisor/vsphere_spec.rb +76 -0
  65. data/spec/beaker/options/command_line_parser_spec.rb +25 -0
  66. data/spec/beaker/options/data/LATEST +1 -0
  67. data/spec/beaker/options/data/badyaml.cfg +21 -0
  68. data/spec/beaker/options/data/hosts.cfg +21 -0
  69. data/spec/beaker/options/data/opts.txt +6 -0
  70. data/spec/beaker/options/hosts_file_parser_spec.rb +30 -0
  71. data/spec/beaker/options/options_file_parser_spec.rb +23 -0
  72. data/spec/beaker/options/options_hash_spec.rb +111 -0
  73. data/spec/beaker/options/parser_spec.rb +172 -0
  74. data/spec/beaker/options/pe_version_scaper_spec.rb +15 -0
  75. data/spec/beaker/options/presets_spec.rb +24 -0
  76. data/spec/beaker/puppet_command_spec.rb +54 -21
  77. data/spec/beaker/shared/error_handler_spec.rb +40 -0
  78. data/spec/beaker/shared/host_handler_spec.rb +104 -0
  79. data/spec/beaker/shared/repetition_spec.rb +72 -0
  80. data/spec/beaker/test_suite_spec.rb +3 -16
  81. data/spec/beaker/utils/ntp_control_spec.rb +42 -0
  82. data/spec/beaker/utils/repo_control_spec.rb +168 -0
  83. data/spec/beaker/utils/setup_helper_spec.rb +82 -0
  84. data/spec/beaker/utils/validator_spec.rb +58 -0
  85. data/spec/helpers.rb +97 -0
  86. data/spec/matchers.rb +39 -0
  87. data/spec/mock_blimpy.rb +48 -0
  88. data/spec/mock_fission.rb +60 -0
  89. data/spec/mock_vsphere.rb +310 -0
  90. data/spec/mock_vsphere_helper.rb +183 -0
  91. data/spec/mocks.rb +83 -0
  92. data/spec/spec_helper.rb +8 -1
  93. metadata +106 -13
  94. data/beaker.rb +0 -10
  95. data/lib/beaker/options_parsing.rb +0 -323
  96. data/lib/beaker/test_config.rb +0 -148
  97. data/spec/beaker/options_parsing_spec.rb +0 -37
  98. data/spec/mocks_and_helpers.rb +0 -34
@@ -0,0 +1,39 @@
1
+ module Beaker
2
+ module Options
3
+ #A set of functions to parse hosts files
4
+ module HostsFileParser
5
+
6
+ # Read the contents of the hosts.cfg into an OptionsHash, merge the 'CONFIG' section into the OptionsHash, return OptionsHash
7
+ # @param [String] hosts_file_path The path to the hosts file
8
+ #
9
+ # @example
10
+ # hosts_hash = HostsFileParser.parse_hosts_file('sample.cfg')
11
+ # hosts_hash == {:HOSTS=>{:"pe-ubuntu-lucid"=>{:roles=>["agent", "dashboard", "database", "master"], ... }
12
+ #
13
+ # @return [OptionsHash] The contents of the hosts file as an OptionsHash
14
+ # @raise [ArgumentError] Raises if hosts_file_path is not a path to a file, or is not a valid YAML file
15
+ def self.parse_hosts_file(hosts_file_path)
16
+ hosts_file_path = File.expand_path(hosts_file_path)
17
+ unless File.exists?(hosts_file_path)
18
+ raise ArgumentError, "Required host file '#{hosts_file_path}' does not exist!"
19
+ end
20
+ host_options = Beaker::Options::OptionsHash.new
21
+ begin
22
+ host_options = host_options.merge(YAML.load_file(hosts_file_path))
23
+ rescue Psych::SyntaxError => e
24
+ raise ArgumentError, "#{hosts_file_path} is not a valid YAML file\n\t#{e}"
25
+ end
26
+
27
+ # Make sure the roles array is present for all hosts
28
+ host_options['HOSTS'].each_key do |host|
29
+ host_options['HOSTS'][host]['roles'] ||= []
30
+ end
31
+ if host_options.has_key?('CONFIG')
32
+ host_options = host_options.merge(host_options.delete('CONFIG'))
33
+ end
34
+ host_options
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ require 'open-uri'
2
+ module Beaker
3
+ module Options
4
+ #A set of functions to read options files
5
+ module OptionsFileParser
6
+
7
+ # Eval the contents of options_file_path, return as an OptionsHash
8
+ #
9
+ # Options file is assumed to contain extra options stored in a Hash
10
+ #
11
+ # ie,
12
+ # {
13
+ # :debug => true,
14
+ # :tests => "test.rb",
15
+ # }
16
+ #
17
+ # @param [String] options_file_path The path to the options file
18
+ #
19
+ # @example
20
+ # options_hash = OptionsFileParser.parse_options_file('sample.cfg')
21
+ # options_hash == {:debug=>true, :tests=>"test.rb", :pre_suite=>["pre-suite.rb"], :post_suite=>"post_suite1.rb,post_suite2.rb"}
22
+ #
23
+ # @return [OptionsHash] The contents of the options file as an OptionsHash
24
+ # @raise [ArgumentError] Raises if options_file_path is not a path to a file
25
+ # @note Since the options_file is Eval'ed, any other Ruby commands will also be executed, this can be used
26
+ # to set additional environment variables
27
+ def self.parse_options_file(options_file_path)
28
+ result = Beaker::Options::OptionsHash.new
29
+ if options_file_path
30
+ options_file_path = File.expand_path(options_file_path)
31
+ unless File.exists?(options_file_path)
32
+ raise ArgumentError, "Specified options file '#{options_file_path}' does not exist!"
33
+ end
34
+ # This eval will allow the specified options file to have access to our
35
+ # scope. It is important that the variable 'options_file_path' is
36
+ # accessible, because some existing options files (e.g. puppetdb) rely on
37
+ # that variable to determine their own location (for use in 'require's, etc.)
38
+ result = result.merge(eval(File.read(options_file_path)))
39
+ end
40
+ result
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,294 @@
1
+ module Beaker
2
+ module Options
3
+
4
+ # A hash that treats Symbol and String keys interchangeably
5
+ # and recursively merges hashes
6
+ class OptionsHash < Hash
7
+
8
+ # The dividor between elements when OptionsHash is dumped
9
+ DIV = ' '
10
+
11
+ # The end of line when dumping
12
+ EOL = "\n"
13
+
14
+ # Get value for given key, search for both k as String and k as Symbol,
15
+ # if not present return nil
16
+ #
17
+ # @param [Object] k The key to find, searches for both k as String
18
+ # and k as Symbol
19
+ #
20
+ # @example Use this method to return the value for a given key
21
+ # a['key'] = 'value'
22
+ # a['key'] == a[:key] == 'value'
23
+ #
24
+ # @return [nil, Object] Return the Object found at given key,
25
+ # or nil if no Object found
26
+ def [] k
27
+ super(k.to_s) || super(k.to_sym)
28
+ end
29
+
30
+ # Set Symbol key to Object value
31
+ # @param [Object] k The key to associated with the value,
32
+ # converted to Symbol key
33
+ # @param [Object] v The value to store in the ObjectHash
34
+ #
35
+ # @example Use this method to set the value for a key
36
+ # a['key'] = 'value'
37
+ #
38
+ # @return [Object] Return the Object value just stored
39
+ def []=k,v
40
+ super(k.to_sym, v)
41
+ end
42
+
43
+ # Determine if type of ObjectHash is pe, defaults to true
44
+ #
45
+ # @example Use this method to return the value for a given key
46
+ # a['type'] = 'pe'
47
+ # a.is_pe? == true
48
+ #
49
+ # @return [Boolean]
50
+ def is_pe?
51
+ self[:type] ? self[:type] =~ /pe/ : true
52
+ end
53
+
54
+ # Determine if key is stored in ObjectHash
55
+ # @param [Object] k The key to find in ObjectHash, searches for
56
+ # both k as String and k as Symbol
57
+ #
58
+ # @example Use this method to set the value for a key
59
+ # a['key'] = 'value'
60
+ # a.has_key[:key] == true
61
+ #
62
+ # @return [Boolean]
63
+ def has_key? k
64
+ super(k.to_s) || super(k.to_sym)
65
+ end
66
+
67
+ # Determine key=>value entry in OptionsHash, remove both value at
68
+ # String key and value at Symbol key
69
+ #
70
+ # @param [Object] k The key to delete in ObjectHash,
71
+ # deletes both k as String and k as Symbol
72
+ #
73
+ # @example Use this method to set the value for a key
74
+ # a['key'] = 'value'
75
+ # a.delete[:key] == 'value'
76
+ #
77
+ # @return [Object, nil] The Object deleted at value,
78
+ # nil if no Object deleted
79
+ def delete k
80
+ super(k.to_s) || super(k.to_sym)
81
+ end
82
+
83
+ # Recursively merge and OptionsHash with an OptionsHash or Hash
84
+ #
85
+ # @param [OptionsHash] base The hash to merge into
86
+ # @param [OptionsHash, Hash] hash The hash to merge from
87
+ #
88
+ # @example
89
+ # base = { :key => { :subkey1 => 'subval', :subkey2 => 'subval' } }
90
+ # hash = { :key => { :subkey1 => 'newval'} }
91
+ #
92
+ # rmerge(base, hash)
93
+ # #=> {:key =>
94
+ # {:subkey1 => 'newval',
95
+ # :subkey2 => 'subval'}}
96
+ #
97
+ # @return [OptionsHash] The combined bash and hash
98
+ def rmerge base, hash
99
+ return base unless hash.is_a?(Hash) || hash.is_a?(OptionsHash)
100
+ hash.each do |key, v|
101
+ if (base[key].is_a?(Hash) || base[key].is_a?(OptionsHash)) && (hash[key].is_a?(Hash) || has[key].is_a?(OptionsHash))
102
+ rmerge(base[key], hash[key])
103
+ elsif hash[key].is_a?(Hash)
104
+ base[key] = OptionsHash.new.merge(hash[key])
105
+ else
106
+ base[key]= hash[key]
107
+ end
108
+ end
109
+ base
110
+ end
111
+
112
+ # Recursively merge self with an OptionsHash or Hash
113
+ #
114
+ # @param [OptionsHash, Hash] hash The hash to merge from
115
+ #
116
+ # @example
117
+ # base = { :key => { :subkey1 => 'subval', :subkey2 => 'subval' } }
118
+ # hash = { :key => { :subkey1 => 'newval'} }
119
+ #
120
+ # base.merge(hash)
121
+ # #=> {:key =>
122
+ # {:subkey1 => 'newval',
123
+ # :subkey2 => 'subval' }
124
+ #
125
+ # @return [OptionsHash] The combined hash
126
+ def merge hash
127
+ rmerge(self, hash)
128
+ end
129
+
130
+ # Helper for formatting collections
131
+ # Computes the indentation level for elements of the collection
132
+ # Yields indentation to block to so the caller can create
133
+ # map of element strings
134
+ # Places delimiters in the correct location
135
+ # Joins everything with correct EOL
136
+ #
137
+ #
138
+ # !@visibility private
139
+ def as_coll( opening, closing, in_lvl, in_inc, &block )
140
+ delim_indent = in_inc * in_lvl
141
+ elem_indent = in_inc * (in_lvl + 1)
142
+
143
+ open_brace = opening
144
+ close_brace = delim_indent + closing
145
+
146
+ fmtd_coll = block.call( elem_indent )
147
+ str_coll = fmtd_coll.join( ',' + EOL )
148
+
149
+ return open_brace + EOL + str_coll + EOL + close_brace
150
+ end
151
+
152
+ # Pretty prints a collection
153
+ #
154
+ # @param [Enumerable] collection The collection to be printed
155
+ # @param [Integer] in_lvl The level of indentation
156
+ # @param [String] in_inc The increment to indent
157
+ #
158
+ # @example
159
+ # base = {:key => { :subkey1 => 'subval', :subkey2 => ['subval'] }}
160
+ # self.fmt_collection( base )
161
+ # #=> '{
162
+ # "key": {
163
+ # "subkey": "subval",
164
+ # "subkey2": [
165
+ # "subval"
166
+ # ]
167
+ # }
168
+ # }'
169
+ #
170
+ # @return [String] The collection as a pretty JSON object
171
+ def fmt_collection( collection, in_lvl = 0, in_inc = DIV )
172
+ if collection.respond_to? :each_pair
173
+ string = fmt_assoc( collection, in_lvl, in_inc )
174
+ else
175
+ string = fmt_list( collection, in_lvl, in_inc )
176
+ end
177
+
178
+ return string
179
+ end
180
+
181
+ # Pretty prints an associative collection
182
+ #
183
+ # @param [#each_pair] coll The collection to be printed
184
+ # @param [Integer] in_lvl The level of indentation
185
+ # @param [String] in_inc The increment to indent
186
+ #
187
+ # @example
188
+ # base = { :key => 'value', :key2 => 'value' }
189
+ # self.fmt_assoc( base )
190
+ # #=> '{
191
+ # "key": "value",
192
+ # "key2": "value"
193
+ # }'
194
+ #
195
+ # @return [String] The collection as a pretty JSON object
196
+ def fmt_assoc( coll, in_lvl = 0, in_inc = DIV )
197
+ if coll.empty?
198
+ return '{}'
199
+ else
200
+ as_coll '{', '}', in_lvl, in_inc do |elem_indent|
201
+ coll.map do |key, value|
202
+ assoc_line = elem_indent + '"' + key.to_s + '"' + ': '
203
+ assoc_line += fmt_value( value, in_lvl, in_inc )
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ # Pretty prints a list collection
210
+ #
211
+ # @param [#each] coll The collection to be printed
212
+ # @param [Integer] in_lvl The level of indentation
213
+ # @param [String] in_inc The increment to indent
214
+ #
215
+ # @example
216
+ # base = [ 'first', 'second' ]
217
+ # self.fmt_list( base )
218
+ # #=> '[
219
+ # "first",
220
+ # "second"
221
+ # ]'
222
+ #
223
+ # @return [String] The collection as a pretty JSON object
224
+ def fmt_list( coll, in_lvl = 0, in_inc = DIV )
225
+ if coll.empty?
226
+ return '[]'
227
+ else
228
+ as_coll '[', ']', in_lvl, in_inc do |indent|
229
+ coll.map do |el|
230
+ indent + fmt_value( el, in_lvl, in_inc )
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ # Chooses between collection and primitive formatting
237
+ #
238
+ # !@visibility private
239
+ def fmt_value( value, in_lvl = 0, in_inc = DIV )
240
+ if value.kind_of? Enumerable and not value.is_a? String
241
+ fmt_collection( value, in_lvl + 1, in_inc )
242
+ else
243
+ fmt_basic( value )
244
+ end
245
+ end
246
+
247
+ # Pretty prints primitive JSON values
248
+ #
249
+ # @param [Object] value The collection to be printed
250
+ #
251
+ # @example
252
+ # self.fmt_value( 4 )
253
+ # #=> '4'
254
+ #
255
+ # @example
256
+ # self.fmt_value( true )
257
+ # #=> 'true'
258
+ #
259
+ # @example
260
+ # self.fmt_value( nil )
261
+ # #=> 'null'
262
+ #
263
+ # @example
264
+ # self.fmt_value( 'string' )
265
+ # #=> '"string"'
266
+ #
267
+ # @return [String] The value as a valid JSON primitive
268
+ def fmt_basic( value )
269
+ case value
270
+ when Numeric, TrueClass, FalseClass then value.to_s
271
+ when NilClass then "null"
272
+ else "\"#{value}\""
273
+ end
274
+ end
275
+
276
+ # Pretty print the options as JSON
277
+ #
278
+ # @example
279
+ # base = { :key => { :subkey1 => 'subval', :subkey2 => 'subval' } }
280
+ # base.dump
281
+ # #=> '{
282
+ # "key": {
283
+ # "subkey1": "subval",
284
+ # "subkey2": 2
285
+ # }
286
+ # }
287
+ #
288
+ # @return [String] The description of self
289
+ def dump
290
+ fmt_collection( self, 0, DIV )
291
+ end
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,288 @@
1
+ require 'yaml'
2
+
3
+ module Beaker
4
+ module Options
5
+ #An Object that parses, merges and normalizes all supported Beaker options and arguments
6
+ class Parser
7
+ GITREPO = 'git://github.com/puppetlabs'
8
+ #These options can have the form of arg1,arg2 or [arg] or just arg,
9
+ #should default to []
10
+ LONG_OPTS = [:helper, :load_path, :tests, :pre_suite, :post_suite, :install, :modules]
11
+ #These options expand out into an array of .rb files
12
+ RB_FILE_OPTS = [:tests, :pre_suite, :post_suite]
13
+
14
+ PARSE_ERROR = if RUBY_VERSION > '1.8.7'; then Psych::SyntaxError; else ArgumentError; end
15
+
16
+ #The OptionsHash of all parsed options
17
+ attr_accessor :options
18
+
19
+ # Raises an ArgumentError with associated message
20
+ # @param [String] msg The error message to be reported
21
+ # @raise [ArgumentError] Takes the supplied message and raises it as an ArgumentError
22
+ def parser_error msg = ""
23
+ raise ArgumentError, msg.to_s
24
+ end
25
+
26
+ # Returns the git repository used for git installations
27
+ # @return [String] The git repository
28
+ def repo
29
+ GITREPO
30
+ end
31
+
32
+ # Returns a description of Beaker's supported arguments
33
+ # @return [String] The usage String
34
+ def usage
35
+ @command_line_parser.usage
36
+ end
37
+
38
+ # Normalizes argument into an Array. Argument can either be converted into an array of a single value,
39
+ # or can become an array of multiple values by splitting arg over ','. If argument is already an
40
+ # array that array is returned untouched.
41
+ # @example
42
+ # split_arg([1, 2, 3]) == [1, 2, 3]
43
+ # split_arg(1) == [1]
44
+ # split_arg("1,2") == ["1", "2"]
45
+ # split_arg(nil) == []
46
+ # @param [Array, String] arg Either an array or a string to be split into an array
47
+ # @return [Array] An array of the form arg, [arg], or arg.split(',')
48
+ def split_arg arg
49
+ arry = []
50
+ if arg.is_a?(Array)
51
+ arry += arg
52
+ elsif arg =~ /,/
53
+ arry += arg.split(',')
54
+ else
55
+ arry << arg
56
+ end
57
+ arry
58
+ end
59
+
60
+ # Generates a list of files based upon a given path or list of paths.
61
+ #
62
+ # Looks recursively for .rb files in paths.
63
+ #
64
+ # @param [Array] paths Array of file paths to search for .rb files
65
+ # @return [Array] An Array of fully qualified paths to .rb files
66
+ # @raise [ArgumentError] Raises if no .rb files are found in searched directory or if
67
+ # no .rb files are found overall
68
+ def file_list(paths)
69
+ files = []
70
+ if not paths.empty?
71
+ paths.each do |root|
72
+ if File.file? root then
73
+ files << root
74
+ else
75
+ discover_files = Dir.glob(
76
+ File.join(root, "**/*.rb")
77
+ ).select { |f| File.file?(f) }
78
+ if discover_files.empty?
79
+ parser_error "empty directory used as an option (#{root})!"
80
+ end
81
+ files += discover_files
82
+ end
83
+ end
84
+ end
85
+ if files.empty?
86
+ parser_error "no .rb files found in #{paths.to_s}"
87
+ end
88
+ files
89
+ end
90
+
91
+ #Converts array of paths into array of fully qualified git repo URLS with expanded keywords
92
+ #
93
+ #Supports the following keywords
94
+ # PUPPET
95
+ # FACTER
96
+ # HIERA
97
+ # HIERA-PUPPET
98
+ #@example
99
+ # opts = ["PUPPET/3.1"]
100
+ # parse_git_repos(opts) == ["#{GITREPO}/puppet.git#3.1"]
101
+ #@param [Array] git_opts An array of paths
102
+ #@return [Array] An array of fully qualified git repo URLs with expanded keywords
103
+ def parse_git_repos(git_opts)
104
+ git_opts.map! { |opt|
105
+ case opt
106
+ when /^PUPPET\//
107
+ opt = "#{GITREPO}/puppet.git##{opt.split('/', 2)[1]}"
108
+ when /^FACTER\//
109
+ opt = "#{GITREPO}/facter.git##{opt.split('/', 2)[1]}"
110
+ when /^HIERA\//
111
+ opt = "#{GITREPO}/hiera.git##{opt.split('/', 2)[1]}"
112
+ when /^HIERA-PUPPET\//
113
+ opt = "#{GITREPO}/hiera-puppet.git##{opt.split('/', 2)[1]}"
114
+ end
115
+ opt
116
+ }
117
+ git_opts
118
+ end
119
+
120
+ #Constructor for Parser
121
+ #
122
+ def initialize
123
+ @command_line_parser = Beaker::Options::CommandLineParser.new
124
+ end
125
+
126
+ # Parses ARGV or provided arguments array, file options, hosts options and combines with environment variables and
127
+ # preset defaults to generate a Hash representing the Beaker options for a given test run
128
+ #
129
+ # Order of priority is as follows:
130
+ # 1. environment variables are given top priority
131
+ # 2. host file options
132
+ # 3. the 'CONFIG' section of the hosts file
133
+ # 4. ARGV or provided arguments array
134
+ # 5. options file values
135
+ # 6. default or preset values are given the lowest priority
136
+ #
137
+ # @param [Array] args ARGV or a provided arguments array
138
+ # @raise [ArgumentError] Raises error on bad input
139
+ def parse_args(args = ARGV)
140
+ #NOTE on argument precedence:
141
+ #
142
+ # Will use env, then hosts/config file, then command line, then file options
143
+ #
144
+ @options = Beaker::Options::Presets.presets
145
+ cmd_line_options = @command_line_parser.parse!(args)
146
+ file_options = Beaker::Options::OptionsFileParser.parse_options_file(cmd_line_options[:options_file])
147
+ # merge together command line and file_options
148
+ # overwrite file options with command line options
149
+ cmd_line_and_file_options = file_options.merge(cmd_line_options)
150
+ # merge command line and file options with defaults
151
+ # overwrite defaults with command line and file options
152
+ @options = @options.merge(cmd_line_and_file_options)
153
+
154
+ if not @options[:help]
155
+ #read the hosts file that contains the node configuration and hypervisor info
156
+ hosts_options = Beaker::Options::HostsFileParser.parse_hosts_file(@options[:hosts_file])
157
+ # merge in host file vars
158
+ # overwrite options (default, file options, command line, env) with host file options
159
+ @options = @options.merge(hosts_options)
160
+ # merge in env vars
161
+ # overwrite options (default, file options, command line, hosts file) with env
162
+ env_vars = Beaker::Options::Presets.env_vars
163
+ @options = @options.merge(env_vars)
164
+
165
+ normalize_args
166
+ end
167
+
168
+ @options
169
+ end
170
+
171
+ # Determine is a given file exists and is a valid YAML file
172
+ # @param [String] f The YAML file path to examine
173
+ # @param [String] msg An options message to report in case of error
174
+ # @raise [ArgumentError] Raise if file does not exist or is not valid YAML
175
+ def check_yaml_file(f, msg = "")
176
+ if not File.file?(f)
177
+ parser_error "#{f} does not exist (#{msg})"
178
+ end
179
+ begin
180
+ YAML.load_file(f)
181
+ rescue PARSE_ERROR => e
182
+ parser_error "#{f} is not a valid YAML file (#{msg})\n\t#{e}"
183
+ end
184
+ end
185
+
186
+ #Validate all merged options values for correctness
187
+ #
188
+ #Currently checks:
189
+ # - if a keyfile is provided then use it
190
+ # - paths provided to --test, --pre-suite, --post-suite provided lists of .rb files for testing
191
+ # - --type is one of 'pe' or 'git'
192
+ # - --fail-mode is one of 'fast', 'stop' or nil
193
+ # - if using blimpy hypervisor an EC2 YAML file exists
194
+ # - if using the aix, solaris, or vcloud hypervisors a .fog file exists
195
+ # - that one and only one master is defined per set of hosts
196
+ # - that solaris/windows/aix hosts are agent only for PE tests OR
197
+ # - that windows/aix host are agent only if type is not 'pe'
198
+ #
199
+ #@raise [ArgumentError] Raise if argument/options values are invalid
200
+ def normalize_args
201
+
202
+ #use the keyfile if present
203
+ if @options.has_key?(:keyfile)
204
+ @options[:ssh][:keys] = [@options[:keyfile]]
205
+ end
206
+
207
+ #split out arguments - these arguments can have the form of arg1,arg2 or [arg] or just arg
208
+ #will end up being normalized into an array
209
+ LONG_OPTS.each do |opt|
210
+ if @options.has_key?(opt)
211
+ @options[opt] = split_arg(@options[opt])
212
+ if RB_FILE_OPTS.include?(opt)
213
+ @options[opt] = file_list(@options[opt])
214
+ end
215
+ if opt == :install
216
+ @options[:install] = parse_git_repos(@options[:install])
217
+ end
218
+ else
219
+ @options[opt] = []
220
+ end
221
+ end
222
+
223
+ #check for valid type
224
+ if @options[:type] !~ /(pe)|(git)/
225
+ parser_error "--type must be one of pe or git, not '#{@options[:type]}'"
226
+ end
227
+
228
+ #check for valid fail mode
229
+ if not ["fast", "stop", nil].include?(@options[:fail_mode])
230
+ parser_error "--fail-mode must be one of fast, stop"
231
+ end
232
+
233
+ #check for config files necessary for different hypervisors
234
+ hypervisors = []
235
+ @options[:HOSTS].each_key do |name|
236
+ hypervisors << @options[:HOSTS][name][:hypervisor].to_s
237
+ end
238
+ hypervisors.uniq!
239
+ hypervisors.each do |visor|
240
+ if ['blimpy'].include?(visor)
241
+ check_yaml_file(@options[:ec2_yaml], "required by #{visor}")
242
+ end
243
+ if ['aix', 'solaris', 'vcloud'].include?(visor)
244
+ check_yaml_file(@options[:dot_fog], "required by #{visor}")
245
+ end
246
+ end
247
+
248
+ #check that roles of hosts make sense
249
+ # - must be one and only one master
250
+ roles = []
251
+ @options[:HOSTS].each_key do |name|
252
+ roles << @options[:HOSTS][name][:roles]
253
+ end
254
+ master = 0
255
+ roles.each do |role_array|
256
+ if role_array.include?('master')
257
+ master += 1
258
+ end
259
+ end
260
+ if master > 1 or master < 1
261
+ parser_error "One and only one host/node may have the role 'master', fix #{@options[:hosts_file]}"
262
+ end
263
+
264
+ #check that solaris/windows/el-4 boxes are only agents
265
+ @options[:HOSTS].each_key do |name|
266
+ host = @options[:HOSTS][name]
267
+ if (host[:platform] =~ /windows|el-4/) ||
268
+ (@options.is_pe? && host[:platform] =~ /solaris/)
269
+
270
+ test_host_roles(name, host)
271
+ end
272
+ end
273
+
274
+ end
275
+
276
+ private
277
+
278
+ # @api private
279
+ def test_host_roles(host_name, host_hash)
280
+ host_roles = host_hash[:roles]
281
+ if (host_roles - ['agent']).size != 0
282
+ parser_error "#{host_hash[:platform].to_s} box '#{host_name}' can only have role 'agent', has roles #{host_roles.to_s}"
283
+ end
284
+ end
285
+
286
+ end
287
+ end
288
+ end