beaker 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.simplecov +14 -0
  5. data/DOCUMENTING.md +167 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +17 -0
  8. data/README.md +332 -0
  9. data/Rakefile +121 -0
  10. data/beaker.gemspec +42 -0
  11. data/beaker.rb +10 -0
  12. data/bin/beaker +9 -0
  13. data/lib/beaker.rb +36 -0
  14. data/lib/beaker/answers.rb +29 -0
  15. data/lib/beaker/answers/version28.rb +104 -0
  16. data/lib/beaker/answers/version30.rb +194 -0
  17. data/lib/beaker/cli.rb +113 -0
  18. data/lib/beaker/command.rb +241 -0
  19. data/lib/beaker/command_factory.rb +21 -0
  20. data/lib/beaker/dsl.rb +85 -0
  21. data/lib/beaker/dsl/assertions.rb +87 -0
  22. data/lib/beaker/dsl/helpers.rb +625 -0
  23. data/lib/beaker/dsl/install_utils.rb +299 -0
  24. data/lib/beaker/dsl/outcomes.rb +99 -0
  25. data/lib/beaker/dsl/roles.rb +97 -0
  26. data/lib/beaker/dsl/structure.rb +63 -0
  27. data/lib/beaker/dsl/wrappers.rb +100 -0
  28. data/lib/beaker/host.rb +193 -0
  29. data/lib/beaker/host/aix.rb +15 -0
  30. data/lib/beaker/host/aix/file.rb +16 -0
  31. data/lib/beaker/host/aix/group.rb +35 -0
  32. data/lib/beaker/host/aix/user.rb +32 -0
  33. data/lib/beaker/host/unix.rb +54 -0
  34. data/lib/beaker/host/unix/exec.rb +15 -0
  35. data/lib/beaker/host/unix/file.rb +16 -0
  36. data/lib/beaker/host/unix/group.rb +40 -0
  37. data/lib/beaker/host/unix/pkg.rb +22 -0
  38. data/lib/beaker/host/unix/user.rb +32 -0
  39. data/lib/beaker/host/windows.rb +44 -0
  40. data/lib/beaker/host/windows/exec.rb +18 -0
  41. data/lib/beaker/host/windows/file.rb +15 -0
  42. data/lib/beaker/host/windows/group.rb +36 -0
  43. data/lib/beaker/host/windows/pkg.rb +26 -0
  44. data/lib/beaker/host/windows/user.rb +32 -0
  45. data/lib/beaker/hypervisor.rb +37 -0
  46. data/lib/beaker/hypervisor/aixer.rb +52 -0
  47. data/lib/beaker/hypervisor/blimper.rb +123 -0
  48. data/lib/beaker/hypervisor/fusion.rb +56 -0
  49. data/lib/beaker/hypervisor/solaris.rb +65 -0
  50. data/lib/beaker/hypervisor/vagrant.rb +118 -0
  51. data/lib/beaker/hypervisor/vcloud.rb +175 -0
  52. data/lib/beaker/hypervisor/vsphere.rb +80 -0
  53. data/lib/beaker/hypervisor/vsphere_helper.rb +200 -0
  54. data/lib/beaker/logger.rb +167 -0
  55. data/lib/beaker/network_manager.rb +73 -0
  56. data/lib/beaker/options_parsing.rb +323 -0
  57. data/lib/beaker/result.rb +55 -0
  58. data/lib/beaker/shared.rb +15 -0
  59. data/lib/beaker/shared/error_handler.rb +17 -0
  60. data/lib/beaker/shared/host_handler.rb +46 -0
  61. data/lib/beaker/shared/repetition.rb +28 -0
  62. data/lib/beaker/ssh_connection.rb +198 -0
  63. data/lib/beaker/test_case.rb +225 -0
  64. data/lib/beaker/test_config.rb +148 -0
  65. data/lib/beaker/test_suite.rb +288 -0
  66. data/lib/beaker/utils.rb +7 -0
  67. data/lib/beaker/utils/ntp_control.rb +42 -0
  68. data/lib/beaker/utils/repo_control.rb +92 -0
  69. data/lib/beaker/utils/setup_helper.rb +77 -0
  70. data/lib/beaker/utils/validator.rb +27 -0
  71. data/spec/beaker/command_spec.rb +94 -0
  72. data/spec/beaker/dsl/assertions_spec.rb +104 -0
  73. data/spec/beaker/dsl/helpers_spec.rb +230 -0
  74. data/spec/beaker/dsl/install_utils_spec.rb +70 -0
  75. data/spec/beaker/dsl/outcomes_spec.rb +43 -0
  76. data/spec/beaker/dsl/roles_spec.rb +86 -0
  77. data/spec/beaker/dsl/structure_spec.rb +60 -0
  78. data/spec/beaker/dsl/wrappers_spec.rb +52 -0
  79. data/spec/beaker/host_spec.rb +95 -0
  80. data/spec/beaker/logger_spec.rb +117 -0
  81. data/spec/beaker/options_parsing_spec.rb +37 -0
  82. data/spec/beaker/puppet_command_spec.rb +128 -0
  83. data/spec/beaker/ssh_connection_spec.rb +39 -0
  84. data/spec/beaker/test_case_spec.rb +6 -0
  85. data/spec/beaker/test_suite_spec.rb +44 -0
  86. data/spec/mocks_and_helpers.rb +34 -0
  87. data/spec/spec_helper.rb +15 -0
  88. metadata +359 -0
@@ -0,0 +1,299 @@
1
+ require 'pathname'
2
+
3
+ module Beaker
4
+ module DSL
5
+ #
6
+ # This module contains methods to help cloning, extracting git info,
7
+ # ordering of Puppet packages, and installing ruby projects that
8
+ # contain an `install.rb` script.
9
+ module InstallUtils
10
+
11
+ # The default install path
12
+ SourcePath = "/opt/puppet-git-repos"
13
+
14
+ # A regex to know if the uri passed is pointing to a git repo
15
+ GitURI = %r{^(git|https?|file)://|^git@}
16
+
17
+ # Github's ssh signature for cloning via ssh
18
+ GitHubSig = 'github.com,207.97.227.239 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=='
19
+
20
+ # @param [String] uri A uri in the format of <git uri>#<revision>
21
+ # the `git://`, `http://`, `https://`, and ssh
22
+ # (if cloning as the remote git user) protocols
23
+ # are valid for <git uri>
24
+ #
25
+ # @example Usage
26
+ # project = extract_repo_info_from 'git@github.com:puppetlabs/SuperSecretSauce#what_is_justin_doing'
27
+ #
28
+ # puts project[:name]
29
+ # #=> 'SuperSecretSauce'
30
+ #
31
+ # puts project[:rev]
32
+ # #=> 'what_is_justin_doing'
33
+ #
34
+ # @return [Hash{Symbol=>String}] Returns a hash containing the project
35
+ # name, repository path, and revision
36
+ # (defaults to HEAD)
37
+ #
38
+ # @api dsl
39
+ def extract_repo_info_from uri
40
+ project = {}
41
+ repo, rev = uri.split('#', 2)
42
+ project[:name] = Pathname.new(repo).basename('.git').to_s
43
+ project[:path] = repo
44
+ project[:rev] = rev || 'HEAD'
45
+ return project
46
+ end
47
+
48
+ # Takes an array of package info hashes (like that returned from
49
+ # {#extract_repo_info_from}) and sorts the `puppet`, `facter`, `hiera`
50
+ # packages so that puppet's dependencies will be installed first.
51
+ #
52
+ # @!visibility private
53
+ def order_packages packages_array
54
+ puppet = packages_array.select {|e| e[:name] == 'puppet' }
55
+ puppet_depends_on = packages_array.select do |e|
56
+ e[:name] == 'hiera' or e[:name] == 'facter'
57
+ end
58
+ depends_on_puppet = (packages_array - puppet) - puppet_depends_on
59
+ [puppet_depends_on, puppet, depends_on_puppet].flatten
60
+ end
61
+
62
+ # @param [Host] host An object implementing {Beaker::Hosts}'s
63
+ # interface.
64
+ # @param [String] path The path on the remote [host] to the repository
65
+ # @param [Hash{Symbol=>String}] repository A hash representing repo
66
+ # info like that emitted by
67
+ # {#extract_repo_info_from}
68
+ #
69
+ # @example Getting multiple project versions
70
+ # versions = [puppet_repo, facter_repo, hiera_repo].inject({}) do |vers, repo_info|
71
+ # vers.merge(find_git_repo_versions(host, '/opt/git-puppet-repos', repo_info) )
72
+ # end
73
+ # @return [Hash] Executes git describe on [host] and returns a Hash
74
+ # with the key of [repository[:name]] and value of
75
+ # the output from git describe.
76
+ #
77
+ # @note This requires the helper methods:
78
+ # * {Beaker::DSL::Structure#step}
79
+ # * {Beaker::DSL::Helpers#on}
80
+ #
81
+ # @api dsl
82
+ def find_git_repo_versions host, path, repository
83
+ version = {}
84
+ step "Grab version for #{repository[:name]}" do
85
+ on host, "cd #{path}/#{repository[:name]} && " +
86
+ "git describe || true" do
87
+ version[repository[:name]] = stdout.chomp
88
+ end
89
+ end
90
+ version
91
+ end
92
+
93
+ #
94
+ # @see #find_git_repo_versions
95
+ def install_from_git host, path, repository
96
+ name = repository[:name]
97
+ repo = repository[:path]
98
+ rev = repository[:rev]
99
+ target = "#{path}/#{name}"
100
+
101
+ step "Clone #{repo} if needed" do
102
+ on host, "test -d #{path} || mkdir -p #{path}"
103
+ on host, "test -d #{target} || git clone #{repo} #{target}"
104
+ end
105
+
106
+ step "Update #{name} and check out revision #{rev}" do
107
+ commands = ["cd #{target}",
108
+ "remote rm origin",
109
+ "remote add origin #{repo}",
110
+ "fetch origin",
111
+ "clean -fdx",
112
+ "checkout -f #{rev}"]
113
+ on host, commands.join(" && git ")
114
+ end
115
+
116
+ step "Install #{name} on the system" do
117
+ # The solaris ruby IPS package has bindir set to /usr/ruby/1.8/bin.
118
+ # However, this is not the path to which we want to deliver our
119
+ # binaries. So if we are using solaris, we have to pass the bin and
120
+ # sbin directories to the install.rb
121
+ install_opts = ''
122
+ install_opts = '--bindir=/usr/bin --sbindir=/usr/sbin' if
123
+ host['platform'].include? 'solaris'
124
+
125
+ on host, "cd #{target} && " +
126
+ "if [ -f install.rb ]; then " +
127
+ "ruby ./install.rb #{install_opts}; " +
128
+ "else true; fi"
129
+ end
130
+ end
131
+
132
+ def do_install hosts, version, path, pre_30, options = {}
133
+ #convenience methods for installation
134
+ ########################################################
135
+ def installer_cmd(host, version, installer)
136
+ if host['platform'] =~ /windows/
137
+ "cd #{host['working_dir']} && msiexec.exe /qn /i puppet-enterprise-#{version}.msi"
138
+ else
139
+ "cd #{host['working_dir']}/#{host['dist']} && ./#{installer}"
140
+ end
141
+ end
142
+ def link_exists?(link)
143
+ require "net/http"
144
+ require "open-uri"
145
+ url = URI.parse(link)
146
+ Net::HTTP.start(url.host, url.port) do |http|
147
+ return http.head(url.request_uri).code == "200"
148
+ end
149
+ end
150
+ def fetch_puppet(hosts, version, path)
151
+ local = File.directory?(path)
152
+ hosts.each do |host|
153
+ filename = ""
154
+ extension = ""
155
+ if host['platform'] =~ /windows/
156
+ filename = "puppet-enterprise-#{version}"
157
+ extension = ".msi"
158
+ else
159
+ filename = "#{host['dist']}"
160
+ extension = ""
161
+ if local
162
+ extension = File.exists?("#{path}/#{filename}.tar.gz") ? ".tar.gz" : ".tar"
163
+ else
164
+ extension = link_exists?("#{path}/#{filename}.tar.gz") ? ".tar.gz" : ".tar"
165
+ end
166
+ end
167
+ if local
168
+ if not File.exists?("#{path}/#{filename}#{extension}")
169
+ raise "attempting installation on #{host}, #{path}/#{filename}#{extension} does not exist"
170
+ end
171
+ scp_to host, "#{path}/#{filename}#{extension}", "#{host['working_dir']}/#{filename}#{extension}"
172
+ else
173
+ if not link_exists?("#{path}/#{filename}#{extension}")
174
+ raise "attempting installation on #{host}, #{path}/#{filename}#{extension} does not exist"
175
+ end
176
+ on host, "cd #{host['working_dir']}; curl #{path}/#{filename}#{extension} -o #{filename}#{extension}"
177
+ end
178
+ if extension =~ /gz/
179
+ on host, "cd #{host['working_dir']}; gunzip #{filename}#{extension}"
180
+ end
181
+ if extension =~ /tar/
182
+ on host, "cd #{host['working_dir']}; tar -xvf #{filename}.tar"
183
+ end
184
+ end
185
+ end
186
+ ########################################################
187
+ #start installation steps here
188
+ options[:installer] = 'puppet-enterprise-installer' unless options[:installer]
189
+ options[:type] = :install unless options[:type]
190
+ hostcert='uname | grep -i sunos > /dev/null && hostname || hostname -s'
191
+ master_certname = on(master, hostcert).stdout.strip
192
+ answers = Beaker::Answers.answers(version, hosts, master_certname, options)
193
+ special_nodes = [master, database, dashboard].uniq
194
+ real_agents = agents - special_nodes
195
+
196
+ # Set PE distribution for all the hosts, create working dir
197
+ use_all_tar = ENV['PE_USE_ALL_TAR'] == 'true'
198
+ hosts.each do |host|
199
+ platform = use_all_tar ? 'all' : host['platform']
200
+ host['dist'] = "puppet-enterprise-#{version}-#{platform}"
201
+ host['working_dir'] = "/tmp/" + Time.new.strftime("%Y-%m-%d_%H.%M.%S") #unique working dirs make me happy
202
+ on host, "mkdir #{host['working_dir']}"
203
+ end
204
+
205
+ fetch_puppet(hosts, version, path)
206
+
207
+ hosts.each do |host|
208
+ # Database host was added in 3.0. Skip it if installing an older version
209
+ next if host == database and host != master and host != dashboard and pre_30
210
+ if host['platform'] =~ /windows/
211
+ on host, "#{installer_cmd(host, version, options[:installer])} PUPPET_MASTER_SERVER=#{master} PUPPET_AGENT_CERTNAME=#{host}"
212
+ else
213
+ create_remote_file host, "#{host['working_dir']}/answers", Beaker::Answers.answer_string(host, answers)
214
+
215
+ on host, "#{installer_cmd(host, version, options[:installer])} -a #{host['working_dir']}/answers"
216
+ end
217
+ end
218
+
219
+
220
+ # If we're installing a version less than 3.0, ignore the database host
221
+ install_hosts = hosts.dup
222
+ install_hosts.delete(database) if pre_30 and database != master and database != dashboard
223
+
224
+ # On each agent, we ensure the certificate is signed then shut down the agent
225
+ install_hosts.each do |host|
226
+ sign_certificate(host)
227
+ stop_agent(host)
228
+ end
229
+
230
+ # Wait for PuppetDB to be totally up and running
231
+ sleep_until_puppetdb_started(database) unless pre_30
232
+
233
+ # Run the agent once to ensure everything is in the dashboard
234
+ install_hosts.each do |host|
235
+ on host, puppet_agent('-t'), :acceptable_exit_codes => [0,2]
236
+
237
+ # Workaround for PE-1105 when deploying 3.0.0
238
+ # The installer did not respect our database host answers in 3.0.0,
239
+ # and would cause puppetdb to be bounced by the agent run. By sleeping
240
+ # again here, we ensure that if that bounce happens during an upgrade
241
+ # test we won't fail early in the install process.
242
+ if version == '3.0.0' and host == database
243
+ sleep_until_puppetdb_started(database)
244
+ end
245
+ end
246
+
247
+ install_hosts.each do |host|
248
+ wait_for_host_in_dashboard(host)
249
+ end
250
+
251
+ if pre_30
252
+ task = 'nodegroup:add_all_nodes group=default'
253
+ else
254
+ task = 'defaultgroup:ensure_default_group'
255
+ end
256
+ on dashboard, "/opt/puppet/bin/rake -sf /opt/puppet/share/puppet-dashboard/Rakefile #{task} RAILS_ENV=production"
257
+
258
+ # Now that all hosts are in the dashbaord, run puppet one more
259
+ # time to configure mcollective
260
+ on install_hosts, puppet_agent('-t'), :acceptable_exit_codes => [0,2]
261
+ end
262
+
263
+ #is version a < version b
264
+ #3.0.0-160-gac44cfb is greater than 3.0.0, and 2.8.2
265
+ def version_is_less a, b
266
+ a = a.split('-')[0].split('.')
267
+ b = b.split('-')[0].split('.')
268
+ (0...a.length).each do |i|
269
+ if i < b.length
270
+ if a[i] < b[i]
271
+ return true
272
+ elsif a[i] > b[i]
273
+ return false
274
+ end
275
+ else
276
+ return false
277
+ end
278
+ end
279
+ return false
280
+ end
281
+
282
+ def install_pe version, path
283
+ pre_30 = version_is_less(version, '3.0')
284
+ step "Install #{version} PE on #{path}"
285
+ do_install hosts, version, path, pre_30
286
+ end
287
+
288
+ def upgrade_pe version, path, from
289
+ pre_30 = version_is_less(version, '3.0')
290
+ if pre_30
291
+ do_install(hosts, version, path, pre_30, :type => :upgrade, :installer => 'puppet-enterprise-upgrader', :from => from)
292
+ else
293
+ do_install(hosts, version, path, pre_30, :type => :upgrade, :from => from)
294
+ end
295
+ end
296
+
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,99 @@
1
+ module Beaker
2
+ module DSL
3
+ # This module includes dsl helpers for setting the state of a test case.
4
+ # They do not need inclusion if using third party test runner. The
5
+ # Exception classes that they raise however should be defined as other
6
+ # DSL helpers will raise them as needed. See individual DSL modules
7
+ # for their specific dependencies. A class that mixes in this module
8
+ # must have a method #logger which will yield an object that responds to
9
+ # #notify and #warn. NOTE: the interface to logger may change shortly and
10
+ # {Beaker::Logger} should be consulted for the appropriate
11
+ # interface.
12
+ #
13
+ # Simply these methods log a message and raise the appropriate Exception
14
+ # The exceptions are are caught by {Beaker::TestCase} and are
15
+ # designed to allow some degree of freedom from the individual third
16
+ # party test runners that could be used.
17
+ module Outcomes
18
+
19
+ # Raise this class if it is determined that a test case should not
20
+ # be executed because the feature in question is still a
21
+ # "Work in Progress"
22
+ class PendingTest < Exception; end
23
+
24
+ # Raise this class if execution should be stopped because the test
25
+ # is not applicable within a given environment.
26
+ class SkipTest < Exception; end
27
+
28
+ # Raise this class if some criteria has been met that proves a failure.
29
+ class FailTest < Exception; end
30
+
31
+ # Raise this class if execution should stop because enough criteria has
32
+ # shown itself to pass the test.
33
+ class PassTest < Exception; end
34
+
35
+
36
+ # Raises FailTest Exception and logs an error message
37
+ #
38
+ # @param [String] msg An optional message to log
39
+ # @raise [FailTest]
40
+ # @api dsl
41
+ def fail_test msg = nil
42
+ message = formatted_message( msg, 'Failed' )
43
+ logger.warn( [message, logger.pretty_backtrace].join("\n") )
44
+
45
+ raise( FailTest, message )
46
+ end
47
+
48
+ # Raises PassTest Exception and logs a message
49
+ #
50
+ # @param [String] msg An optional message to log
51
+ # @raise [PassTest]
52
+ # @api dsl
53
+ def pass_test msg = nil
54
+ message = formatted_message( msg, 'Passed' )
55
+ logger.notify( message )
56
+
57
+ raise( PassTest, message )
58
+ end
59
+
60
+ # Raises PendingTest Exception and logs an error message
61
+ #
62
+ # @param [String] msg An optional message to log
63
+ # @raise [PendingTest]
64
+ # @api dsl
65
+ def pending_test msg = nil
66
+ message = formatted_message( msg, 'is Pending' )
67
+ logger.warn( message )
68
+
69
+ raise( PendingTest, message )
70
+ end
71
+
72
+ # Raises SkipTest Exception and logs a message
73
+ #
74
+ # @param [String] msg An optional message to log
75
+ # @raise [SkipTest]
76
+ # @api dsl
77
+ def skip_test msg = nil
78
+ message = formatted_message( msg, 'was Skipped' )
79
+ logger.notify( message )
80
+
81
+ raise( SkipTest, message )
82
+ end
83
+
84
+ # Formats an optional message or self appended by a state, either
85
+ # bracketted in newlines
86
+ #
87
+ # @param [String, nil] message The message (or nil) to format
88
+ # @param [String] default_str The string to be appended to self if
89
+ # message is nil
90
+ #
91
+ # @return [String] A prettier string with helpful info
92
+ # @!visibility private
93
+ def formatted_message(message, default_str )
94
+ msg = message ? "\n#{message}\n" : "\n#{self} #{default_str}.\n"
95
+ return msg
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,97 @@
1
+ module Beaker
2
+ module DSL
3
+ #
4
+ # Identifying hosts.
5
+ #
6
+ # This aids in reaching common subsets of hosts in a testing matrix.
7
+ #
8
+ # It requires the class it is mixed into to provide the attribute
9
+ # `hosts` which contain the hosts to search, these should implement
10
+ # {Beaker::Host}'s interface. They, at least, must have #[]
11
+ # and #to_s available and provide an array when #[]('roles') is called.
12
+ #
13
+ # Also the constant {FailTest} needs to be defined it will be raised
14
+ # in error conditions
15
+ #
16
+ # @api dsl
17
+ module Roles
18
+
19
+ # The hosts for which ['roles'] include 'agent'
20
+ #
21
+ # @return [Array<Host>] May be empty
22
+ #
23
+ # @example Basic usage
24
+ # agents.each do |agent|
25
+ # ...test each agent in turn...
26
+ # end
27
+ #
28
+ def agents
29
+ hosts_as 'agent'
30
+ end
31
+
32
+ # The host for which ['roles'] include 'master'
33
+ #
34
+ # @return [Array<Host>]
35
+ # @raise [Beaker::DSL::Outcomes::FailTest] if there are less
36
+ # or more than 1 master is found.
37
+ #
38
+ # @example Basic usage
39
+ # on, master, 'cat /etc/puppet/puppet.conf'
40
+ #
41
+ def master
42
+ find_only_one :master
43
+ end
44
+
45
+ # The host for which ['roles'] include 'database'
46
+ #
47
+ # @return [Array<Host>]
48
+ # @raise [Beaker::DSL::Outcomes::FailTest] if there are less
49
+ # or more than 1 database is found.
50
+ #
51
+ # @example Basic usage
52
+ # on, agent, "curl -k http://#{database}:8080"
53
+ #
54
+ def database
55
+ find_only_one :database
56
+ end
57
+
58
+ # The host for which ['roles'] include 'dashboard'
59
+ #
60
+ # @return [Array<Host>]
61
+ # @raise [Beaker::DSL::Outcomes::FailTest] if there are less
62
+ # or more than 1 dashboard is found.
63
+ #
64
+ # @example Basic usage
65
+ # on, agent, "curl https://#{database}/nodes/#{agent}"
66
+ #
67
+ def dashboard
68
+ find_only_one :dashboard
69
+ end
70
+
71
+ # Select hosts that include a desired role from #hosts
72
+ #
73
+ # @param [String, Symbol] desired_role The role to select for
74
+ # @return [Array<Host>] The hosts that match
75
+ # desired_role, may be empty
76
+ #
77
+ # @example Basic usage
78
+ # hairy = hosts_as :yak
79
+ # hairy.each do |yak|
80
+ # on yak, 'shave'
81
+ # end
82
+ #
83
+ # @api public
84
+ def hosts_as(desired_role = nil)
85
+ hosts_with_role(hosts, desired_role)
86
+ end
87
+
88
+ # @param [Symbol, String] role The role to find a host for
89
+ # @return [Host] Returns the host, if one and only one is found
90
+ # @raise Raises a failure exception if one and only one host that matches
91
+ # the specified role is NOT found.
92
+ def find_only_one role
93
+ only_host_with_role(hosts, role)
94
+ end
95
+ end
96
+ end
97
+ end