chef 0.9.8.rc.0 → 0.9.8
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/README.rdoc +80 -44
- data/lib/chef/checksum.rb +7 -4
- data/lib/chef/client.rb +4 -0
- data/lib/chef/cookbook/cookbook_collection.rb +10 -9
- data/lib/chef/cookbook/file_system_file_vendor.rb +9 -8
- data/lib/chef/cookbook/file_vendor.rb +2 -1
- data/lib/chef/cookbook/metadata.rb +3 -0
- data/lib/chef/cookbook/remote_file_vendor.rb +3 -2
- data/lib/chef/cookbook/syntax_check.rb +8 -0
- data/lib/chef/cookbook_site_streaming_uploader.rb +5 -1
- data/lib/chef/cookbook_version.rb +25 -19
- data/lib/chef/exceptions.rb +3 -0
- data/lib/chef/file_access_control.rb +1 -0
- data/lib/chef/handler.rb +37 -1
- data/lib/chef/mixin/deep_merge.rb +6 -5
- data/lib/chef/mixin/recipe_definition_dsl_core.rb +2 -1
- data/lib/chef/mixin/shell_out.rb +1 -1
- data/lib/chef/mixin/template.rb +1 -1
- data/lib/chef/mixin/xml_escape.rb +2 -2
- data/lib/chef/monkey_patches/dir.rb +1 -1
- data/lib/chef/monkey_patches/string.rb +2 -1
- data/lib/chef/monkey_patches/tempfile.rb +5 -1
- data/lib/chef/provider/package/easy_install.rb +29 -22
- data/lib/chef/provider/template.rb +1 -94
- data/lib/chef/recipe.rb +16 -12
- data/lib/chef/rest.rb +17 -5
- data/lib/chef/run_context.rb +2 -1
- data/lib/chef/run_status.rb +27 -0
- data/lib/chef/runner.rb +5 -3
- data/lib/chef/shef.rb +3 -1
- data/lib/chef/shef/ext.rb +1 -0
- data/lib/chef/shef/model_wrapper.rb +1 -1
- data/lib/chef/shef/shef_rest.rb +1 -1
- data/lib/chef/shef/shef_session.rb +1 -0
- data/lib/chef/shell_out.rb +47 -19
- data/lib/chef/version.rb +1 -1
- metadata +6 -27
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
= Chef
|
2
2
|
|
3
|
-
|
3
|
+
== DESCRIPTION:
|
4
4
|
|
5
5
|
Chef is a configuration management tool designed to bring automation to your entire infrastructure.
|
6
6
|
|
@@ -12,7 +12,7 @@ This README focuses on developers who want to modify Chef source code. For users
|
|
12
12
|
|
13
13
|
* http://wiki.opscode.com/display/chef/Installing+Chef+from+HEAD
|
14
14
|
|
15
|
-
|
15
|
+
== DEVELOPMENT:
|
16
16
|
|
17
17
|
Before working on the code, if you plan to contribute your changes, you need to read the Opscode Contributing document.
|
18
18
|
|
@@ -24,84 +24,120 @@ You will also need to set up the repository with the appropriate branches. We do
|
|
24
24
|
|
25
25
|
Once your repository is set up, you can start working on the code. We do use BDD/TDD with RSpec and Cucumber, so you'll need to get a development environment running.
|
26
26
|
|
27
|
-
|
27
|
+
== ENVIRONMENT:
|
28
28
|
|
29
29
|
In order to have a development environment where changes to the Chef code can be tested, we'll need to install a few things after setting up the Git repository.
|
30
30
|
|
31
|
-
|
31
|
+
=== Non-Gem Dependencies
|
32
32
|
|
33
33
|
Install these via your platform's preferred method; for example apt, yum, ports, emerge, etc.
|
34
34
|
|
35
|
-
* Git
|
36
|
-
*
|
37
|
-
*
|
38
|
-
*
|
39
|
-
|
40
|
-
|
35
|
+
* Git[http://git-scm.com/]
|
36
|
+
* Erlang/OTP[http://www.erlang.org/]
|
37
|
+
* CouchDB[http://couchdb.apache.org/]
|
38
|
+
* RabbitMQ[http://www.rabbitmq.com/]
|
39
|
+
* GCC and C Standard Libraries, header files, etc. (i.e., build-essential on debian/ubuntu)
|
40
|
+
* Ruby development package
|
41
|
+
* libxml2 development package
|
41
42
|
|
43
|
+
=== Runtime Rubygem Dependencies
|
44
|
+
==== Chef Client and Solo
|
42
45
|
* ohai
|
43
|
-
*
|
44
|
-
*
|
45
|
-
*
|
46
|
-
*
|
46
|
+
* bunny
|
47
|
+
* erubis
|
48
|
+
* extlib
|
49
|
+
* highline
|
50
|
+
* json (1.4.2)
|
51
|
+
* mixlib-authentication
|
52
|
+
* mixlib-cli
|
53
|
+
* mixlib-config
|
54
|
+
* mixlib-log
|
55
|
+
* moneta
|
56
|
+
* rest-client
|
57
|
+
* uuidtools
|
47
58
|
* merb-core
|
48
|
-
* roman-merb_cucumber
|
49
|
-
* thin
|
50
59
|
|
51
|
-
|
60
|
+
==== Chef Server, WebUI and Solr
|
61
|
+
All of the above, plus the following:
|
62
|
+
* coderay
|
63
|
+
* haml
|
64
|
+
* libxml-ruby
|
65
|
+
* merb-assets
|
66
|
+
* merb-core
|
67
|
+
* merb-haml
|
68
|
+
* merb-helpers
|
69
|
+
* merb-param-protection
|
70
|
+
* ruby-openid
|
71
|
+
* thin
|
52
72
|
|
53
|
-
|
73
|
+
=== Development Rubygem Dependencies
|
74
|
+
* rake[http://rake.rubyforge.org/]
|
75
|
+
* rspec[http://rspec.info/]
|
76
|
+
* cucumber[http://cukes.info/]
|
54
77
|
|
55
|
-
|
78
|
+
Ohai is also by Opscode and available on GitHub, http://github.com/opscode/ohai/tree/master.
|
56
79
|
|
57
80
|
== Starting the Environment:
|
58
81
|
|
59
|
-
|
82
|
+
=== On Mac OS X:
|
83
|
+
For ease of debugging, Chef includes a script to start each of the required
|
84
|
+
daemons in a separate Terminal.app tab via applescript:
|
60
85
|
|
61
|
-
|
86
|
+
scripts/mac-dev-start features
|
62
87
|
|
63
|
-
|
88
|
+
=== On Linux and BSD
|
64
89
|
|
65
|
-
|
66
|
-
|
67
|
-
* Starts solr
|
68
|
-
* Starts chef-indexer.
|
69
|
-
* Starts chef-server on port 4000
|
90
|
+
run the dev:features rake task. You may need to run it as root depending on how
|
91
|
+
your system is configured.
|
70
92
|
|
93
|
+
rake dev:features
|
94
|
+
|
95
|
+
=== Daemons
|
96
|
+
After starting the environment, you should have the following processes running:
|
97
|
+
* couchdb listening on port 5984
|
98
|
+
* rabbitmq listening on port 5672
|
99
|
+
* solr listening on port 8983
|
100
|
+
* chef-solr-indexer connected as a client to rabbitmq
|
101
|
+
* chef-server listening on port 4000
|
102
|
+
* chef-server-webui listening on port 4040
|
71
103
|
|
72
104
|
You'll know its running when you see:
|
73
105
|
|
74
|
-
~
|
75
|
-
merb : worker (port 4000) ~
|
76
|
-
merb : worker (port 4000) ~
|
77
|
-
merb : worker (port 4000) ~ Successfully bound to port 4000
|
106
|
+
merb : chef-server (api) : worker (port 4000) ~ Starting Thin at port 4000
|
107
|
+
merb : chef-server (api) : worker (port 4000) ~ Using Thin adapter on host 0.0.0.0 and port 4000.
|
108
|
+
merb : chef-server (api) : worker (port 4000) ~ Successfully bound to port 4000
|
78
109
|
|
79
110
|
You'll want to leave this terminal running the dev environment.
|
80
111
|
|
81
|
-
|
112
|
+
=== Web Interface:
|
82
113
|
|
83
|
-
With the dev environment running, you can now access the web interface via http://localhost:
|
114
|
+
With the dev environment running, you can now access the web interface via http://localhost:4040/.
|
84
115
|
|
85
116
|
== Spec testing:
|
86
117
|
|
87
|
-
We use RSpec for unit/spec tests.
|
118
|
+
We use RSpec for unit/spec tests. It is not necessary to start the development
|
119
|
+
environment to run the specs--they are completely standalone.
|
88
120
|
|
89
121
|
rake spec
|
90
122
|
|
91
|
-
This doesn't actually use the development environment, because it does the testing on all the Chef internals. For integration/usage testing, we use Cucumber features.
|
92
|
-
|
93
123
|
== Integration testing:
|
94
124
|
|
95
|
-
We test integration with Cucumber.
|
125
|
+
We test integration with Cucumber. To run the full suite, run the rake task:
|
126
|
+
|
127
|
+
rake features
|
128
|
+
|
129
|
+
Subsets of the integration tests can be run with the various tasks in the
|
130
|
+
features namespace. To see the full list, run
|
131
|
+
|
132
|
+
rake -T
|
133
|
+
|
134
|
+
To run individual feature tests, you can take advantage of cucumber's tagging
|
135
|
+
support. Tag the feature you wish to run (tags are denoted with a leading `@'
|
136
|
+
sign), then use the cucumber command:
|
96
137
|
|
97
|
-
|
98
|
-
rake features:api # Run Features with Cucumber
|
99
|
-
rake features:client # Run Features with Cucumber
|
100
|
-
rake features:provider:package:macports # Run Features with Cucumber
|
101
|
-
rake features:provider:remote_file # Run Features with Cucumber
|
102
|
-
rake features:search # Run Features with Cucumber
|
138
|
+
cucumber -t @my_tag
|
103
139
|
|
104
|
-
|
140
|
+
== LINKS:
|
105
141
|
|
106
142
|
Source:
|
107
143
|
|
data/lib/chef/checksum.rb
CHANGED
@@ -18,10 +18,10 @@
|
|
18
18
|
require 'chef/log'
|
19
19
|
require 'uuidtools'
|
20
20
|
|
21
|
-
|
22
|
-
# Checksum for an individual file; e.g., used for sandbox/cookbook uploading
|
23
|
-
# to track which files the system already manages.
|
24
21
|
class Chef
|
22
|
+
# == Chef::Checksum
|
23
|
+
# Checksum for an individual file; e.g., used for sandbox/cookbook uploading
|
24
|
+
# to track which files the system already manages.
|
25
25
|
class Checksum
|
26
26
|
attr_accessor :checksum, :create_time
|
27
27
|
attr_accessor :couchdb_id, :couchdb_rev
|
@@ -42,7 +42,10 @@ class Chef
|
|
42
42
|
}
|
43
43
|
}
|
44
44
|
|
45
|
-
# Creates a new Chef::Checksum object.
|
45
|
+
# Creates a new Chef::Checksum object.
|
46
|
+
# === Arguments
|
47
|
+
# checksum::: the MD5 content hash of the file
|
48
|
+
# couchdb::: An instance of Chef::CouchDB
|
46
49
|
#
|
47
50
|
# === Returns
|
48
51
|
# object<Chef::Checksum>:: Duh. :)
|
data/lib/chef/client.rb
CHANGED
@@ -36,12 +36,16 @@ require 'chef/version'
|
|
36
36
|
require 'ohai'
|
37
37
|
|
38
38
|
class Chef
|
39
|
+
# == Chef::Client
|
40
|
+
# The main object in a Chef run. Preps a Chef::Node and Chef::RunContext,
|
41
|
+
# syncs cookbooks if necessary, and triggers convergence.
|
39
42
|
class Client
|
40
43
|
attr_accessor :node
|
41
44
|
attr_accessor :ohai
|
42
45
|
attr_accessor :rest
|
43
46
|
attr_accessor :runner
|
44
47
|
|
48
|
+
#--
|
45
49
|
# TODO: timh/cw: 5-19-2010: json_attribs should be moved to RunContext?
|
46
50
|
attr_reader :json_attribs
|
47
51
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
#--
|
2
2
|
# Author:: Tim Hinderliter (<tim@opscode.com>)
|
3
3
|
# Author:: Christopher Walters (<cw@opscode.com>)
|
4
4
|
# Copyright:: Copyright (c) 2010 Opscode, Inc.
|
@@ -19,15 +19,16 @@
|
|
19
19
|
|
20
20
|
require 'extlib'
|
21
21
|
|
22
|
-
# This class is the consistent interface for a node to obtain its
|
23
|
-
# cookbooks by name.
|
24
|
-
#
|
25
|
-
# This class is basically a glorified Hash, but since there are
|
26
|
-
# several ways this cookbook information is collected,
|
27
|
-
# (e.g. CookbookLoader for solo, hash of auto-vivified Cookbook
|
28
|
-
# objects for lazily-loaded remote cookbooks), it gets transformed
|
29
|
-
# into this.
|
30
22
|
class Chef
|
23
|
+
# == Chef::CookbookCollection
|
24
|
+
# This class is the consistent interface for a node to obtain its
|
25
|
+
# cookbooks by name.
|
26
|
+
#
|
27
|
+
# This class is basically a glorified Hash, but since there are
|
28
|
+
# several ways this cookbook information is collected,
|
29
|
+
# (e.g. CookbookLoader for solo, hash of auto-vivified Cookbook
|
30
|
+
# objects for lazily-loaded remote cookbooks), it gets transformed
|
31
|
+
# into this.
|
31
32
|
class CookbookCollection < Mash
|
32
33
|
|
33
34
|
# The input is a mapping of cookbook name to CookbookVersion objects. We
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
#--
|
2
2
|
# Author:: Christopher Walters (<cw@opscode.com>)
|
3
3
|
# Author:: Tim Hinderliter (<tim@opscode.com>)
|
4
4
|
# Copyright:: Copyright (c) 2010 Opscode, Inc.
|
@@ -19,15 +19,16 @@
|
|
19
19
|
|
20
20
|
require 'chef/cookbook/file_vendor'
|
21
21
|
|
22
|
-
# This FileVendor loads files from Chef::Config.cookbook_path. The
|
23
|
-
# thing that's sort of janky about this FileVendor implementation is
|
24
|
-
# that it basically takes only the cookbook's name from the manifest
|
25
|
-
# and throws the rest away then re-builds the list of files on the
|
26
|
-
# disk. This is due to the manifest not having the on-disk file
|
27
|
-
# locations, since in the chef-client case, that information is
|
28
|
-
# non-sensical.
|
29
22
|
class Chef
|
30
23
|
class Cookbook
|
24
|
+
# == Chef::Cookbook::FileSystemFileVendor
|
25
|
+
# This FileVendor loads files from Chef::Config.cookbook_path. The
|
26
|
+
# thing that's sort of janky about this FileVendor implementation is
|
27
|
+
# that it basically takes only the cookbook's name from the manifest
|
28
|
+
# and throws the rest away then re-builds the list of files on the
|
29
|
+
# disk. This is due to the manifest not having the on-disk file
|
30
|
+
# locations, since in the chef-client case, that information is
|
31
|
+
# non-sensical.
|
31
32
|
class FileSystemFileVendor < FileVendor
|
32
33
|
|
33
34
|
def initialize(manifest)
|
@@ -18,9 +18,10 @@
|
|
18
18
|
#
|
19
19
|
|
20
20
|
|
21
|
-
# This class handles fetching of cookbook files based on specificity.
|
22
21
|
class Chef
|
23
22
|
class Cookbook
|
23
|
+
# == Chef::Cookbook::FileVendor
|
24
|
+
# This class handles fetching of cookbook files based on specificity.
|
24
25
|
class FileVendor
|
25
26
|
|
26
27
|
def self.on_create(&block)
|
@@ -25,6 +25,9 @@ require 'chef/cookbook/metadata/version'
|
|
25
25
|
|
26
26
|
class Chef
|
27
27
|
class Cookbook
|
28
|
+
# == Chef::Cookbook::Metadata
|
29
|
+
# Chef::Cookbook::Metadata provides a convenient DSL for declaring metadata
|
30
|
+
# about Chef Cookbooks.
|
28
31
|
class Metadata
|
29
32
|
|
30
33
|
COMPARISON_FIELDS = [ :name, :description, :long_description, :maintainer,
|
@@ -18,10 +18,11 @@
|
|
18
18
|
|
19
19
|
require 'chef/cookbook/file_vendor'
|
20
20
|
|
21
|
-
# This FileVendor loads files by either fetching them from the local cache, or
|
22
|
-
# if not available, loading them from the remote server.
|
23
21
|
class Chef
|
24
22
|
class Cookbook
|
23
|
+
# == Chef::Cookbook::RemoteFileVendor
|
24
|
+
# This FileVendor loads files by either fetching them from the local cache, or
|
25
|
+
# if not available, loading them from the remote server.
|
25
26
|
class RemoteFileVendor < FileVendor
|
26
27
|
|
27
28
|
def initialize(manifest, rest, valid_cache_entries)
|
@@ -21,11 +21,16 @@ require 'chef/mixin/shell_out'
|
|
21
21
|
|
22
22
|
class Chef
|
23
23
|
class Cookbook
|
24
|
+
# == Chef::Cookbook::SyntaxCheck
|
25
|
+
# Encapsulates the process of validating the ruby syntax of files in Chef
|
26
|
+
# cookbooks.
|
24
27
|
class SyntaxCheck
|
25
28
|
include Chef::Mixin::ShellOut
|
26
29
|
|
27
30
|
attr_reader :cookbook_path
|
28
31
|
|
32
|
+
# Creates a new SyntaxCheck given the +cookbook_name+ and a +cookbook_path+.
|
33
|
+
# If no +cookbook_path+ is given, +Chef::Config.cookbook_path+ is used.
|
29
34
|
def self.for_cookbook(cookbook_name, cookbook_path=nil)
|
30
35
|
cookbook_path ||= Chef::Config.cookbook_path
|
31
36
|
unless cookbook_path
|
@@ -34,6 +39,9 @@ class Chef
|
|
34
39
|
new(File.join(cookbook_path, cookbook_name.to_s))
|
35
40
|
end
|
36
41
|
|
42
|
+
# Create a new SyntaxCheck object
|
43
|
+
# === Arguments
|
44
|
+
# cookbook_path::: the (on disk) path to the cookbook
|
37
45
|
def initialize(cookbook_path)
|
38
46
|
@cookbook_path = cookbook_path
|
39
47
|
end
|
@@ -22,8 +22,12 @@ require 'net/http'
|
|
22
22
|
require 'mixlib/authentication/signedheaderauth'
|
23
23
|
require 'openssl'
|
24
24
|
|
25
|
-
# inspired by from http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html
|
26
25
|
class Chef
|
26
|
+
# == Chef::CookbookSiteStreamingUploader
|
27
|
+
# A streaming multipart HTTP upload implementation. Used to upload cookbooks
|
28
|
+
# (in tarball form) to http://cookbooks.opscode.com
|
29
|
+
#
|
30
|
+
# inspired by http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html
|
27
31
|
class CookbookSiteStreamingUploader
|
28
32
|
|
29
33
|
DefaultHeaders = { 'accept' => 'application/json', 'x-chef-version' => ::Chef::VERSION }
|
@@ -24,9 +24,15 @@ require 'chef/resource_definition_list'
|
|
24
24
|
require 'chef/recipe'
|
25
25
|
require 'chef/cookbook/file_vendor'
|
26
26
|
|
27
|
-
# TODO: timh/cw: 5-24-2010: mutators for files (e.g., recipe_filenames=,
|
28
|
-
# recipe_filenames.insert) should dirty the manifest so it gets regenerated.
|
29
27
|
class Chef
|
28
|
+
# == Chef::CookbookVersion
|
29
|
+
# CookbookVersion is a model object encapsulating the data about a Chef
|
30
|
+
# cookbook. Chef supports maintaining multiple versions of a cookbook on a
|
31
|
+
# single server; each version is represented by a distinct instance of this
|
32
|
+
# class.
|
33
|
+
#--
|
34
|
+
# TODO: timh/cw: 5-24-2010: mutators for files (e.g., recipe_filenames=,
|
35
|
+
# recipe_filenames.insert) should dirty the manifest so it gets regenerated.
|
30
36
|
class CookbookVersion
|
31
37
|
include Chef::IndexQueue::Indexable
|
32
38
|
|
@@ -228,23 +234,23 @@ class Chef
|
|
228
234
|
# as well as describing cookbook metadata. The manifest follows a form
|
229
235
|
# like the following:
|
230
236
|
#
|
231
|
-
#
|
232
|
-
#
|
233
|
-
#
|
234
|
-
#
|
235
|
-
#
|
236
|
-
#
|
237
|
-
#
|
238
|
-
#
|
239
|
-
#
|
240
|
-
#
|
241
|
-
#
|
242
|
-
#
|
243
|
-
#
|
244
|
-
#
|
245
|
-
#
|
246
|
-
#
|
247
|
-
#
|
237
|
+
# {
|
238
|
+
# :cookbook_name = "apache2",
|
239
|
+
# :version = "1.0",
|
240
|
+
# :name = "Apache 2"
|
241
|
+
# :metadata = ???TODO: timh/cw: 5-24-2010: describe this format,
|
242
|
+
#
|
243
|
+
# :files => [
|
244
|
+
# {
|
245
|
+
# :name => "afile.rb",
|
246
|
+
# :path => "files/ubuntu-9.10/afile.rb",
|
247
|
+
# :checksum => "2222",
|
248
|
+
# :specificity => "ubuntu-9.10"
|
249
|
+
# },
|
250
|
+
# ],
|
251
|
+
# :templates => [ manifest_record1, ... ],
|
252
|
+
# ...
|
253
|
+
# }
|
248
254
|
def manifest
|
249
255
|
unless @manifest
|
250
256
|
generate_manifest
|
data/lib/chef/exceptions.rb
CHANGED
@@ -16,6 +16,9 @@
|
|
16
16
|
# limitations under the License.
|
17
17
|
|
18
18
|
class Chef
|
19
|
+
# == Chef::Exceptions
|
20
|
+
# Chef's custom exceptions are all contained within the Chef::Exceptions
|
21
|
+
# namespace.
|
19
22
|
class Exceptions
|
20
23
|
class Application < RuntimeError; end
|
21
24
|
class Cron < RuntimeError; end
|
data/lib/chef/handler.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
#--
|
2
2
|
# Author:: Adam Jacob (<adam@opscode.com>)
|
3
3
|
# Copyright:: Copyright (c) 2010 Opscode, Inc.
|
4
4
|
# License:: Apache License, Version 2.0
|
@@ -18,6 +18,42 @@
|
|
18
18
|
require 'forwardable'
|
19
19
|
|
20
20
|
class Chef
|
21
|
+
# == Chef::Handler
|
22
|
+
# The base class for an Exception or Notification Handler. Create your own
|
23
|
+
# handler by subclassing Chef::Handler. When a Chef run fails with an
|
24
|
+
# uncaught Exception, Chef will set the +run_status+ on your handler and call
|
25
|
+
# +report+
|
26
|
+
#
|
27
|
+
# ===Example:
|
28
|
+
#
|
29
|
+
# require 'net/smtp'
|
30
|
+
#
|
31
|
+
# module MyOrg
|
32
|
+
# class OhNoes < Chef::Handler
|
33
|
+
#
|
34
|
+
# def report
|
35
|
+
# # Create the email message
|
36
|
+
# message = "From: Your Name <your@mail.address>\n"
|
37
|
+
# message << "To: Destination Address <someone@example.com>\n"
|
38
|
+
# message << "Subject: Chef Run Failure\n"
|
39
|
+
# message << "Date: #{Time.now.rfc2822}\n\n"
|
40
|
+
#
|
41
|
+
# # The Node is available as +node+
|
42
|
+
# message << "Chef run failed on #{node.name}\n"
|
43
|
+
# # +run_status+ is a value object with all of the run status data
|
44
|
+
# message << "#{run_status.formatted_exception}\n"
|
45
|
+
# # Join the backtrace lines. Coerce to an array just in case.
|
46
|
+
# message << Array(backtrace).join("\n")
|
47
|
+
#
|
48
|
+
# # Send the email
|
49
|
+
# Net::SMTP.start('your.smtp.server', 25) do |smtp|
|
50
|
+
# smtp.send_message message, 'from@address', 'to@address'
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
21
57
|
class Handler
|
22
58
|
|
23
59
|
extend Forwardable
|