beaker 0.0.0 → 1.0.0

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