bolt 2.10.0 → 2.14.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bolt might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Puppetfile +1 -1
- data/bolt-modules/boltlib/lib/puppet/datatypes/applyresult.rb +2 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resourceinstance.rb +3 -2
- data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -0
- data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +2 -2
- data/bolt-modules/boltlib/lib/puppet/functions/apply_prep.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/get_resources.rb +1 -1
- data/bolt-modules/boltlib/lib/puppet/functions/resource.rb +52 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +65 -0
- data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +4 -2
- data/bolt-modules/boltlib/lib/puppet/functions/run_task_with.rb +8 -2
- data/bolt-modules/boltlib/lib/puppet/functions/set_resources.rb +65 -43
- data/lib/bolt/analytics.rb +21 -2
- data/lib/bolt/applicator.rb +8 -2
- data/lib/bolt/apply_inventory.rb +4 -0
- data/lib/bolt/apply_result.rb +1 -1
- data/lib/bolt/apply_target.rb +4 -0
- data/lib/bolt/bolt_option_parser.rb +5 -4
- data/lib/bolt/catalog.rb +81 -68
- data/lib/bolt/cli.rb +16 -5
- data/lib/bolt/config.rb +62 -29
- data/lib/bolt/config/transport/ssh.rb +130 -91
- data/lib/bolt/executor.rb +14 -1
- data/lib/bolt/inventory/group.rb +1 -1
- data/lib/bolt/inventory/inventory.rb +4 -0
- data/lib/bolt/inventory/target.rb +4 -0
- data/lib/bolt/pal.rb +3 -0
- data/lib/bolt/project.rb +55 -11
- data/lib/bolt/resource_instance.rb +10 -3
- data/lib/bolt/shell/bash.rb +1 -1
- data/lib/bolt/shell/powershell/snippets.rb +8 -0
- data/lib/bolt/transport/local/connection.rb +2 -1
- data/lib/bolt/transport/ssh/connection.rb +39 -0
- data/lib/bolt/transport/winrm/connection.rb +4 -0
- data/lib/bolt/version.rb +1 -1
- data/lib/bolt_spec/bolt_context.rb +1 -1
- data/lib/bolt_spec/run.rb +1 -1
- metadata +6 -5
@@ -12,94 +12,132 @@ module Bolt
|
|
12
12
|
# NOTE: All transport configuration options should have a corresponding schema definition
|
13
13
|
# in schemas/bolt-transport-definitions.json
|
14
14
|
OPTIONS = {
|
15
|
-
"cleanup"
|
16
|
-
|
17
|
-
|
18
|
-
"connect-timeout"
|
19
|
-
|
20
|
-
"copy-command"
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
"
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
"
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
"
|
101
|
-
|
102
|
-
|
15
|
+
"cleanup" => { type: TrueClass,
|
16
|
+
external: true,
|
17
|
+
desc: "Whether to clean up temporary files created on targets." },
|
18
|
+
"connect-timeout" => { type: Integer,
|
19
|
+
desc: "How long to wait when establishing connections." },
|
20
|
+
"copy-command" => { external: true,
|
21
|
+
desc: "Command to use when copying files using ssh-command. "\
|
22
|
+
"Bolt runs `<copy-command> <src> <dest>`. **This option is "\
|
23
|
+
"experimental.**" },
|
24
|
+
"disconnect-timeout" => { type: Integer,
|
25
|
+
desc: "How long to wait before force-closing a connection." },
|
26
|
+
"encryption-algorithms" => { type: Array,
|
27
|
+
desc: "List of encryption algorithms to use when establishing a "\
|
28
|
+
"connection with a target. Supported algorithms are "\
|
29
|
+
"defined by the Ruby net-ssh library and can be viewed "\
|
30
|
+
"[here](https://github.com/net-ssh/net-ssh#supported-algorithms). "\
|
31
|
+
"All supported, non-deprecated algorithms are available by default when "\
|
32
|
+
"this option is not used. To reference all default algorithms "\
|
33
|
+
"when using this option, add 'defaults' to the list of supported "\
|
34
|
+
"algorithms." },
|
35
|
+
"extensions" => { type: Array,
|
36
|
+
desc: "List of file extensions that are accepted for scripts or tasks on "\
|
37
|
+
"Windows. Scripts with these file extensions rely on the target's file "\
|
38
|
+
"type association to run. For example, if Python is installed on the "\
|
39
|
+
"system, a `.py` script runs with `python.exe`. The extensions `.ps1`, "\
|
40
|
+
"`.rb`, and `.pp` are always allowed and run via hard-coded "\
|
41
|
+
"executables." },
|
42
|
+
"host" => { type: String,
|
43
|
+
external: true,
|
44
|
+
desc: "Host name." },
|
45
|
+
"host-key-algorithms" => { type: Array,
|
46
|
+
desc: "List of host key algorithms to use when establishing a connection "\
|
47
|
+
"with a target. Supported algorithms are defined by the Ruby net-ssh "\
|
48
|
+
"library "\
|
49
|
+
"([docs](https://github.com/net-ssh/net-ssh#supported-algorithms)). "\
|
50
|
+
"All supported, non-deprecated algorithms are available by default when "\
|
51
|
+
"this option is not used. To reference all default algorithms "\
|
52
|
+
"using this option, add 'defaults' to the list of supported "\
|
53
|
+
"algorithms." },
|
54
|
+
"host-key-check" => { type: TrueClass,
|
55
|
+
external: true,
|
56
|
+
desc: "Whether to perform host key validation when connecting." },
|
57
|
+
"interpreters" => { type: Hash,
|
58
|
+
external: true,
|
59
|
+
desc: "A map of an extension name to the absolute path of an executable, "\
|
60
|
+
"enabling you to override the shebang defined in a task executable. "\
|
61
|
+
"The extension can optionally be specified with the `.` character "\
|
62
|
+
"(`.py` and `py` both map to a task executable `task.py`) and the "\
|
63
|
+
"extension is case sensitive. When a target's name is `localhost`, "\
|
64
|
+
"Ruby tasks run with the Bolt Ruby interpreter by default." },
|
65
|
+
"kex-algorithms" => { type: Array,
|
66
|
+
desc: "List of key exchange algorithms to use when establishing a "\
|
67
|
+
"connection to a target. Supported algorithms are defined by the "\
|
68
|
+
"Ruby net-ssh library "\
|
69
|
+
"([docs](https://github.com/net-ssh/net-ssh#supported-algorithms)). "\
|
70
|
+
"All supported, non-deprecated algorithms are available by default when "\
|
71
|
+
"this option is not used. To reference all default algorithms "\
|
72
|
+
"using this option, add 'defaults' to the list of supported "\
|
73
|
+
"algorithms." },
|
74
|
+
"load-config" => { type: TrueClass,
|
75
|
+
desc: "Whether to load system SSH configuration." },
|
76
|
+
"login-shell" => { type: String,
|
77
|
+
desc: "Which login shell Bolt should expect on the target. "\
|
78
|
+
"Supported shells are #{LOGIN_SHELLS.join(', ')}. "\
|
79
|
+
"**This option is experimental.**" },
|
80
|
+
"mac-algorithms" => { type: Array,
|
81
|
+
desc: "List of message authentication code algorithms to use when "\
|
82
|
+
"establishing a connection to a target. Supported algorithms are "\
|
83
|
+
"defined by the Ruby net-ssh library "\
|
84
|
+
"([docs](https://github.com/net-ssh/net-ssh#supported-algorithms)). "\
|
85
|
+
"All supported, non-deprecated algorithms are available by default when "\
|
86
|
+
"this option is not used. To reference all default algorithms "\
|
87
|
+
"using this option, add 'defaults' to the list of supported "\
|
88
|
+
"algorithms." },
|
89
|
+
"password" => { type: String,
|
90
|
+
desc: "Login password." },
|
91
|
+
"port" => { type: Integer,
|
92
|
+
external: true,
|
93
|
+
desc: "Connection port." },
|
94
|
+
"private-key" => { external: true,
|
95
|
+
desc: "Either the path to the private key file to use for authentication, or "\
|
96
|
+
"a hash with the key `key-data` and the contents of the private key." },
|
97
|
+
"proxyjump" => { type: String,
|
98
|
+
desc: "A jump host to proxy connections through, and an optional user to "\
|
99
|
+
"connect with." },
|
100
|
+
"run-as" => { type: String,
|
101
|
+
external: true,
|
102
|
+
desc: "A different user to run commands as after login." },
|
103
|
+
"run-as-command" => { type: Array,
|
104
|
+
external: true,
|
105
|
+
desc: "The command to elevate permissions. Bolt appends the user and command "\
|
106
|
+
"strings to the configured `run-as-command` before running it on the "\
|
107
|
+
"target. This command must not require an interactive password prompt, "\
|
108
|
+
"and the `sudo-password` option is ignored when `run-as-command` is "\
|
109
|
+
"specified. The `run-as-command` must be specified as an array." },
|
110
|
+
"script-dir" => { type: String,
|
111
|
+
external: true,
|
112
|
+
desc: "The subdirectory of the tmpdir to use in place of a randomized "\
|
113
|
+
"subdirectory for uploading and executing temporary files on the "\
|
114
|
+
"target. It's expected that this directory already exists as a subdir "\
|
115
|
+
"of tmpdir, which is either configured or defaults to `/tmp`." },
|
116
|
+
"ssh-command" => { external: true,
|
117
|
+
desc: "Command and flags to use when SSHing. This enables the external "\
|
118
|
+
"SSH transport which shells out to the specified command. "\
|
119
|
+
"**This option is experimental.**" },
|
120
|
+
"sudo-executable" => { type: String,
|
121
|
+
external: true,
|
122
|
+
desc: "The executable to use when escalating to the configured `run-as` "\
|
123
|
+
"user. This is useful when you want to escalate using the configured "\
|
124
|
+
"`sudo-password`, since `run-as-command` does not use `sudo-password` "\
|
125
|
+
"or support prompting. The command executed on the target is "\
|
126
|
+
"`<sudo-executable> -S -u <user> -p custom_bolt_prompt <command>`. "\
|
127
|
+
"**This option is experimental.**" },
|
128
|
+
"sudo-password" => { type: String,
|
129
|
+
external: true,
|
130
|
+
desc: "Password to use when changing users via `run-as`." },
|
131
|
+
"tmpdir" => { type: String,
|
132
|
+
external: true,
|
133
|
+
desc: "The directory to upload and execute temporary files on the target." },
|
134
|
+
"tty" => { type: TrueClass,
|
135
|
+
desc: "Request a pseudo tty for the session. This option is generally "\
|
136
|
+
"only used in conjunction with the `run-as` option when the sudoers "\
|
137
|
+
"policy requires a `tty`." },
|
138
|
+
"user" => { type: String,
|
139
|
+
external: true,
|
140
|
+
desc: "Login user." }
|
103
141
|
}.freeze
|
104
142
|
|
105
143
|
DEFAULTS = {
|
@@ -142,10 +180,11 @@ module Bolt
|
|
142
180
|
"Unsupported login-shell #{@config['login-shell']}. Supported shells are #{LOGIN_SHELLS.join(', ')}"
|
143
181
|
end
|
144
182
|
|
145
|
-
|
146
|
-
unless
|
183
|
+
%w[encryption-algorithms host-key-algorithms kex-algorithms mac-algorithms run-as-command].each do |opt|
|
184
|
+
next unless @config.key?(opt)
|
185
|
+
unless @config[opt].all? { |n| n.is_a?(String) }
|
147
186
|
raise Bolt::ValidationError,
|
148
|
-
"
|
187
|
+
"#{opt} must be an Array of Strings, received #{@config[opt].inspect}"
|
149
188
|
end
|
150
189
|
end
|
151
190
|
|
data/lib/bolt/executor.rb
CHANGED
@@ -34,7 +34,8 @@ module Bolt
|
|
34
34
|
|
35
35
|
def initialize(concurrency = 1,
|
36
36
|
analytics = Bolt::Analytics::NoopClient.new,
|
37
|
-
noop = false
|
37
|
+
noop = false,
|
38
|
+
modified_concurrency = false)
|
38
39
|
# lazy-load expensive gem code
|
39
40
|
require 'concurrent'
|
40
41
|
|
@@ -64,6 +65,9 @@ module Bolt
|
|
64
65
|
Concurrent.global_immediate_executor
|
65
66
|
end
|
66
67
|
@logger.debug { "Started with #{concurrency} max thread(s)" }
|
68
|
+
|
69
|
+
@concurrency = concurrency
|
70
|
+
@warn_concurrency = modified_concurrency
|
67
71
|
end
|
68
72
|
|
69
73
|
def transport(transport)
|
@@ -102,6 +106,15 @@ module Bolt
|
|
102
106
|
# defined by the transport. Yields each batch, along with the corresponding
|
103
107
|
# transport, to the block in turn and returns an array of result promises.
|
104
108
|
def queue_execute(targets)
|
109
|
+
if @warn_concurrency && targets.length > @concurrency
|
110
|
+
@warn_concurrency = false
|
111
|
+
@logger.warn("The ulimit is low, which may cause file limit issues. Default concurrency has been set to "\
|
112
|
+
"'#{@concurrency}' to mitigate those issues, which may cause Bolt to run slow. "\
|
113
|
+
"Disable this warning by configuring ulimit using 'ulimit -n <limit>' in your shell "\
|
114
|
+
"configuration, or by configuring Bolt's concurrency. "\
|
115
|
+
"See https://puppet.com/docs/bolt/latest/bolt_known_issues.html for details.")
|
116
|
+
end
|
117
|
+
|
105
118
|
targets.group_by(&:transport).flat_map do |protocol, protocol_targets|
|
106
119
|
transport = transport(protocol)
|
107
120
|
report_transport(transport, protocol_targets.count)
|
data/lib/bolt/inventory/group.rb
CHANGED
@@ -16,7 +16,7 @@ module Bolt
|
|
16
16
|
DATA_KEYS = %w[config facts vars features plugin_hooks].freeze
|
17
17
|
TARGET_KEYS = DATA_KEYS + %w[name alias uri]
|
18
18
|
GROUP_KEYS = DATA_KEYS + %w[name groups targets]
|
19
|
-
CONFIG_KEYS = Bolt::Config::
|
19
|
+
CONFIG_KEYS = Bolt::Config::CONFIG_IN_INVENTORY.keys
|
20
20
|
|
21
21
|
def initialize(input, plugins)
|
22
22
|
@logger = Logging.logger[self]
|
@@ -90,6 +90,10 @@ module Bolt
|
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
|
+
def resource(type, title)
|
94
|
+
resources[Bolt::ResourceInstance.format_reference(type, title)]
|
95
|
+
end
|
96
|
+
|
93
97
|
def plugin_hooks
|
94
98
|
# Merge plugin_hooks from the config file with any defined by the group
|
95
99
|
# or assigned dynamically to the target
|
data/lib/bolt/pal.rb
CHANGED
@@ -190,6 +190,7 @@ module Bolt
|
|
190
190
|
# versions of "core" types, which are already present on the agent,
|
191
191
|
# but may cause issues on Puppet 5 agents.
|
192
192
|
@original_modulepath,
|
193
|
+
@project,
|
193
194
|
pdb_client,
|
194
195
|
@hiera_config,
|
195
196
|
@max_compiles,
|
@@ -359,6 +360,7 @@ module Bolt
|
|
359
360
|
name = param.name
|
360
361
|
if signature_params.include?(name)
|
361
362
|
params[name] = { 'type' => param.types.first }
|
363
|
+
params[name]['sensitive'] = param.types.first =~ /\ASensitive(\[.*\])?\z/ ? true : false
|
362
364
|
params[name]['default_value'] = defaults[name] if defaults.key?(name)
|
363
365
|
params[name]['description'] = param.text unless param.text.empty?
|
364
366
|
else
|
@@ -390,6 +392,7 @@ module Bolt
|
|
390
392
|
param.type_expr
|
391
393
|
end
|
392
394
|
params[name] = { 'type' => type_str }
|
395
|
+
params[name]['sensitive'] = param.type_expr.instance_of?(Puppet::Pops::Types::PSensitiveType)
|
393
396
|
params[name]['default_value'] = param.value
|
394
397
|
params[name]['description'] = param.description if param.description
|
395
398
|
end
|
data/lib/bolt/project.rb
CHANGED
@@ -12,11 +12,22 @@ module Bolt
|
|
12
12
|
"tasks" => "An array of task names to whitelist. Whitelisted plans are included in `bolt task show` output"
|
13
13
|
}.freeze
|
14
14
|
|
15
|
-
attr_reader :path, :config_file, :inventory_file, :modulepath, :hiera_config,
|
16
|
-
:puppetfile, :rerunfile, :type, :resource_types
|
15
|
+
attr_reader :path, :data, :config_file, :inventory_file, :modulepath, :hiera_config,
|
16
|
+
:puppetfile, :rerunfile, :type, :resource_types, :warnings, :project_file
|
17
17
|
|
18
18
|
def self.default_project
|
19
|
-
|
19
|
+
create_project(File.expand_path(File.join('~', '.puppetlabs', 'bolt')), 'user')
|
20
|
+
# If homedir isn't defined use the system config path
|
21
|
+
rescue ArgumentError
|
22
|
+
create_project(system_path, 'system')
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.system_path
|
26
|
+
if Bolt::Util.windows?
|
27
|
+
File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'bolt', 'etc')
|
28
|
+
else
|
29
|
+
File.join('/etc', 'puppetlabs', 'bolt')
|
30
|
+
end
|
20
31
|
end
|
21
32
|
|
22
33
|
# Search recursively up the directory hierarchy for the Project. Look for a
|
@@ -26,9 +37,9 @@ module Bolt
|
|
26
37
|
def self.find_boltdir(dir)
|
27
38
|
dir = Pathname.new(dir)
|
28
39
|
if (dir + BOLTDIR_NAME).directory?
|
29
|
-
|
40
|
+
create_project(dir + BOLTDIR_NAME, 'embedded')
|
30
41
|
elsif (dir + 'bolt.yaml').file? || (dir + 'bolt-project.yaml').file?
|
31
|
-
|
42
|
+
create_project(dir, 'local')
|
32
43
|
elsif dir.root?
|
33
44
|
default_project
|
34
45
|
else
|
@@ -36,9 +47,25 @@ module Bolt
|
|
36
47
|
end
|
37
48
|
end
|
38
49
|
|
39
|
-
def
|
50
|
+
def self.create_project(path, type = 'option')
|
51
|
+
fullpath = Pathname.new(path).expand_path
|
52
|
+
project_file = File.join(fullpath, 'bolt-project.yaml')
|
53
|
+
data = Bolt::Util.read_optional_yaml_hash(File.expand_path(project_file), 'project')
|
54
|
+
new(data, path, type)
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(raw_data, path, type = 'option')
|
40
58
|
@path = Pathname.new(path).expand_path
|
41
|
-
@
|
59
|
+
@project_file = @path + 'bolt-project.yaml'
|
60
|
+
|
61
|
+
@warnings = []
|
62
|
+
if (@path + 'bolt.yaml').file? && project_file?
|
63
|
+
msg = "Project-level configuration in bolt.yaml is deprecated if using bolt-project.yaml. "\
|
64
|
+
"Transport config should be set in inventory.yaml, all other config should be set in "\
|
65
|
+
"bolt-project.yaml."
|
66
|
+
@warnings << { msg: msg }
|
67
|
+
end
|
68
|
+
|
42
69
|
@inventory_file = @path + 'inventory.yaml'
|
43
70
|
@modulepath = [(@path + 'modules').to_s, (@path + 'site-modules').to_s, (@path + 'site').to_s]
|
44
71
|
@hiera_config = @path + 'hiera.yaml'
|
@@ -47,9 +74,26 @@ module Bolt
|
|
47
74
|
@resource_types = @path + '.resource_types'
|
48
75
|
@type = type
|
49
76
|
|
50
|
-
|
51
|
-
|
52
|
-
|
77
|
+
tc = Bolt::Config::CONFIG_IN_INVENTORY.keys & raw_data.keys
|
78
|
+
if tc.any?
|
79
|
+
msg = "Transport configuration isn't supported in bolt-project.yaml. Ignoring keys #{tc}"
|
80
|
+
@warnings << { msg: msg }
|
81
|
+
end
|
82
|
+
|
83
|
+
@data = raw_data.reject { |k, _| Bolt::Config::CONFIG_IN_INVENTORY.keys.include?(k) }
|
84
|
+
|
85
|
+
# Once bolt.yaml deprecation is removed, this attribute should be removed
|
86
|
+
# and replaced with .project_file in lib/bolt/config.rb
|
87
|
+
@config_file = if (Bolt::Config::OPTIONS.keys & @data.keys).any?
|
88
|
+
if (@path + 'bolt.yaml').file?
|
89
|
+
msg = "bolt-project.yaml contains valid config keys, bolt.yaml will be ignored"
|
90
|
+
@warnings << { msg: msg }
|
91
|
+
end
|
92
|
+
@project_file
|
93
|
+
else
|
94
|
+
@path + 'bolt.yaml'
|
95
|
+
end
|
96
|
+
validate if project_file?
|
53
97
|
end
|
54
98
|
|
55
99
|
def to_s
|
@@ -67,7 +111,7 @@ module Bolt
|
|
67
111
|
end
|
68
112
|
alias == eql?
|
69
113
|
|
70
|
-
def
|
114
|
+
def project_file?
|
71
115
|
@project_file.file?
|
72
116
|
end
|
73
117
|
|
@@ -30,8 +30,7 @@ module Bolt
|
|
30
30
|
@target = resource_hash['target']
|
31
31
|
@type = resource_hash['type'].to_s.capitalize
|
32
32
|
@title = resource_hash['title']
|
33
|
-
|
34
|
-
@state = resource_hash['state'] || resource_hash['parameters'] || {}
|
33
|
+
@state = resource_hash['state'] || {}
|
35
34
|
@desired_state = resource_hash['desired_state'] || {}
|
36
35
|
@events = resource_hash['events'] || []
|
37
36
|
end
|
@@ -84,11 +83,19 @@ module Bolt
|
|
84
83
|
to_hash.to_json(opts)
|
85
84
|
end
|
86
85
|
|
86
|
+
def self.format_reference(type, title)
|
87
|
+
"#{type.capitalize}[#{title}]"
|
88
|
+
end
|
89
|
+
|
87
90
|
def reference
|
88
|
-
|
91
|
+
self.class.format_reference(@type, @title)
|
89
92
|
end
|
90
93
|
alias to_s reference
|
91
94
|
|
95
|
+
def [](attribute)
|
96
|
+
@state[attribute]
|
97
|
+
end
|
98
|
+
|
92
99
|
def add_event(event)
|
93
100
|
@events << event
|
94
101
|
end
|