dtk-node-agent 0.5.10
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.
- checksums.yaml +15 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +18 -0
- data/README.md +42 -0
- data/bin/dtk-node-agent +16 -0
- data/dtk-node-agent.gemspec +38 -0
- data/lib/config/install.config +12 -0
- data/lib/dtk-node-agent/installer.rb +142 -0
- data/lib/dtk-node-agent/version.rb +3 -0
- data/mcollective_additions/plugins/README.md +1 -0
- data/mcollective_additions/plugins/v1.2/agent/discovery.rb +39 -0
- data/mcollective_additions/plugins/v1.2/agent/get_log_fragment.ddl +15 -0
- data/mcollective_additions/plugins/v1.2/agent/get_log_fragment.rb +79 -0
- data/mcollective_additions/plugins/v1.2/agent/git_access.ddl +9 -0
- data/mcollective_additions/plugins/v1.2/agent/git_access.rb +79 -0
- data/mcollective_additions/plugins/v1.2/agent/netstat.ddl +9 -0
- data/mcollective_additions/plugins/v1.2/agent/netstat.rb +34 -0
- data/mcollective_additions/plugins/v1.2/agent/puppet_apply.ddl +9 -0
- data/mcollective_additions/plugins/v1.2/agent/puppet_apply.rb +630 -0
- data/mcollective_additions/plugins/v1.2/agent/rpcutil.ddl +204 -0
- data/mcollective_additions/plugins/v1.2/agent/rpcutil.rb +101 -0
- data/mcollective_additions/plugins/v1.2/facts/pbuilder_facts.rb +35 -0
- data/mcollective_additions/plugins/v2.2/agent/dev_manager.ddl +9 -0
- data/mcollective_additions/plugins/v2.2/agent/dev_manager.rb +69 -0
- data/mcollective_additions/plugins/v2.2/agent/discovery.rb +39 -0
- data/mcollective_additions/plugins/v2.2/agent/dtk_node_agent_git_client.rb +94 -0
- data/mcollective_additions/plugins/v2.2/agent/execute_tests.ddl +9 -0
- data/mcollective_additions/plugins/v2.2/agent/execute_tests.rb +64 -0
- data/mcollective_additions/plugins/v2.2/agent/get_log_fragment.ddl +15 -0
- data/mcollective_additions/plugins/v2.2/agent/get_log_fragment.rb +79 -0
- data/mcollective_additions/plugins/v2.2/agent/git_access.ddl +9 -0
- data/mcollective_additions/plugins/v2.2/agent/git_access.rb +72 -0
- data/mcollective_additions/plugins/v2.2/agent/netstat.ddl +9 -0
- data/mcollective_additions/plugins/v2.2/agent/netstat.rb +34 -0
- data/mcollective_additions/plugins/v2.2/agent/ps.ddl +9 -0
- data/mcollective_additions/plugins/v2.2/agent/ps.rb +37 -0
- data/mcollective_additions/plugins/v2.2/agent/puppet_apply.ddl +9 -0
- data/mcollective_additions/plugins/v2.2/agent/puppet_apply.rb +633 -0
- data/mcollective_additions/plugins/v2.2/agent/puppet_cancel.ddl +10 -0
- data/mcollective_additions/plugins/v2.2/agent/puppet_cancel.rb +78 -0
- data/mcollective_additions/plugins/v2.2/agent/rpcutil.ddl +204 -0
- data/mcollective_additions/plugins/v2.2/agent/rpcutil.rb +101 -0
- data/mcollective_additions/plugins/v2.2/agent/sync_agent_code.ddl +10 -0
- data/mcollective_additions/plugins/v2.2/agent/sync_agent_code.rb +85 -0
- data/mcollective_additions/plugins/v2.2/agent/tail.ddl +11 -0
- data/mcollective_additions/plugins/v2.2/agent/tail.rb +67 -0
- data/mcollective_additions/plugins/v2.2/connector/r8stomp.rb +238 -0
- data/mcollective_additions/plugins/v2.2/connector/stomp.rb +349 -0
- data/mcollective_additions/plugins/v2.2/connector/stomp_em.rb +191 -0
- data/mcollective_additions/plugins/v2.2/facts/pbuilder_facts.rb +35 -0
- data/mcollective_additions/plugins/v2.2/security/sshkey.ddl +9 -0
- data/mcollective_additions/plugins/v2.2/security/sshkey.rb +362 -0
- data/mcollective_additions/server.cfg +22 -0
- data/puppet_additions/modules/r8/lib/puppet/type/r8_export_file.rb +53 -0
- data/puppet_additions/modules/r8/manifests/export_variable.rb +10 -0
- data/puppet_additions/puppet_lib_base/puppet/indirector/catalog/r8_storeconfig_backend.rb +48 -0
- data/puppet_additions/puppet_lib_base/puppet/indirector/r8_storeconfig_backend.rb +4 -0
- data/puppet_additions/puppet_lib_base/puppet/indirector/resource/r8_storeconfig_backend.rb +72 -0
- data/src/etc/init.d/ec2-run-user-data +95 -0
- data/src/etc/logrotate.d/mcollective +10 -0
- data/src/etc/logrotate.d/puppet +7 -0
- metadata +189 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'puppet'
|
3
|
+
require 'grit'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
#TODO: move to be shared by agents
|
8
|
+
PuppetApplyLogDir = "/var/log/puppet"
|
9
|
+
ModulePath = "/etc/puppet/modules"
|
10
|
+
|
11
|
+
module MCollective
|
12
|
+
module Agent
|
13
|
+
class Puppet_cancel < RPC::Agent
|
14
|
+
def initialize()
|
15
|
+
super()
|
16
|
+
@log = Log.instance
|
17
|
+
@reply_data = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Amar:
|
22
|
+
# puppet_cancel agent gets 'task_id' in request
|
23
|
+
# And goes through list of live threads inside stomp/mcollective process.
|
24
|
+
# If thread with matching 'task_id' is found that thread is killed
|
25
|
+
# If thread with matching 'task_id' is not found, error is returned in response
|
26
|
+
#
|
27
|
+
def run_action
|
28
|
+
task_id = request[:top_task_id]
|
29
|
+
@log.info("Terminating puppet apply thread for task_id=#{task_id}")
|
30
|
+
|
31
|
+
ret ||= Response.new()
|
32
|
+
|
33
|
+
Thread.list.each do |t|
|
34
|
+
if t[:task_id] == task_id
|
35
|
+
t[:is_canceled] = true
|
36
|
+
t.kill
|
37
|
+
@log.info("Puppet apply thread for task_id=#{task_id} terminated.")
|
38
|
+
ret.set_status_succeeded!()
|
39
|
+
return ret
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
@log.info("Puppet apply thread for task_id=#{task_id} is not running on this node.")
|
44
|
+
ret.set_status_failed!()
|
45
|
+
error_info = { :error => { :message => "Puppet apply thread for task_id=#{task_id} is not running on the node." } }
|
46
|
+
ret.merge!(error_info)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
#TODO: this should be common accross Agents
|
51
|
+
class Response < Hash
|
52
|
+
def initialize(hash={})
|
53
|
+
super()
|
54
|
+
self.merge!(hash)
|
55
|
+
self[:status] = :unknown unless hash.has_key?(:status)
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_hash()
|
59
|
+
Hash.new.merge(self)
|
60
|
+
end
|
61
|
+
|
62
|
+
def failed?()
|
63
|
+
self[:status] == :failed
|
64
|
+
end
|
65
|
+
|
66
|
+
def set_status_failed!()
|
67
|
+
self[:status] = :failed
|
68
|
+
end
|
69
|
+
def set_status_succeeded!()
|
70
|
+
self[:status] = :succeeded
|
71
|
+
end
|
72
|
+
def set_dynamic_attributes!(dynamic_attributes)
|
73
|
+
self[:dynamic_attributes] = dynamic_attributes
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
@@ -0,0 +1,204 @@
|
|
1
|
+
metadata :name => "rpcutil",
|
2
|
+
:description => "General helpful actions that expose stats and internals to SimpleRPC clients",
|
3
|
+
:author => "R.I.Pienaar <rip@devco.net>",
|
4
|
+
:license => "Apache License, Version 2.0",
|
5
|
+
:version => "1.0",
|
6
|
+
:url => "http://marionette-collective.org/",
|
7
|
+
:timeout => 10
|
8
|
+
|
9
|
+
action "collective_info", :description => "Info about the main and sub collectives" do
|
10
|
+
display :always
|
11
|
+
|
12
|
+
output :main_collective,
|
13
|
+
:description => "The main Collective",
|
14
|
+
:display_as => "Main Collective"
|
15
|
+
|
16
|
+
output :collectives,
|
17
|
+
:description => "All Collectives",
|
18
|
+
:display_as => "All Collectives"
|
19
|
+
|
20
|
+
summarize do
|
21
|
+
aggregate summary(:collectives)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
action "inventory", :description => "System Inventory" do
|
26
|
+
display :always
|
27
|
+
|
28
|
+
output :agents,
|
29
|
+
:description => "List of agent names",
|
30
|
+
:display_as => "Agents"
|
31
|
+
|
32
|
+
output :facts,
|
33
|
+
:description => "List of facts and values",
|
34
|
+
:display_as => "Facts"
|
35
|
+
|
36
|
+
output :classes,
|
37
|
+
:description => "List of classes on the system",
|
38
|
+
:display_as => "Classes"
|
39
|
+
|
40
|
+
output :version,
|
41
|
+
:description => "MCollective Version",
|
42
|
+
:display_as => "Version"
|
43
|
+
|
44
|
+
output :main_collective,
|
45
|
+
:description => "The main Collective",
|
46
|
+
:display_as => "Main Collective"
|
47
|
+
|
48
|
+
output :collectives,
|
49
|
+
:description => "All Collectives",
|
50
|
+
:display_as => "All Collectives"
|
51
|
+
|
52
|
+
output :data_plugins,
|
53
|
+
:description => "List of data plugin names",
|
54
|
+
:display_as => "Data Plugins"
|
55
|
+
end
|
56
|
+
|
57
|
+
action "get_fact", :description => "Retrieve a single fact from the fact store" do
|
58
|
+
display :always
|
59
|
+
|
60
|
+
input :fact,
|
61
|
+
:prompt => "The name of the fact",
|
62
|
+
:description => "The fact to retrieve",
|
63
|
+
:type => :string,
|
64
|
+
:validation => '^[\w\-\.]+$',
|
65
|
+
:optional => false,
|
66
|
+
:maxlength => 40
|
67
|
+
|
68
|
+
output :fact,
|
69
|
+
:description => "The name of the fact being returned",
|
70
|
+
:display_as => "Fact"
|
71
|
+
|
72
|
+
output :value,
|
73
|
+
:description => "The value of the fact",
|
74
|
+
:display_as => "Value"
|
75
|
+
|
76
|
+
summarize do
|
77
|
+
aggregate summary(:value)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
action "daemon_stats", :description => "Get statistics from the running daemon" do
|
82
|
+
display :always
|
83
|
+
|
84
|
+
output :threads,
|
85
|
+
:description => "List of threads active in the daemon",
|
86
|
+
:display_as => "Threads"
|
87
|
+
|
88
|
+
output :agents,
|
89
|
+
:description => "List of agents loaded",
|
90
|
+
:display_as => "Agents"
|
91
|
+
|
92
|
+
output :pid,
|
93
|
+
:description => "Process ID of the daemon",
|
94
|
+
:display_as => "PID"
|
95
|
+
|
96
|
+
output :times,
|
97
|
+
:description => "Processor time consumed by the daemon",
|
98
|
+
:display_as => "Times"
|
99
|
+
|
100
|
+
output :validated,
|
101
|
+
:description => "Messages that passed security validation",
|
102
|
+
:display_as => "Security Validated"
|
103
|
+
|
104
|
+
output :unvalidated,
|
105
|
+
:description => "Messages that failed security validation",
|
106
|
+
:display_as => "Failed Security"
|
107
|
+
|
108
|
+
output :passed,
|
109
|
+
:description => "Passed filter checks",
|
110
|
+
:display_as => "Passed Filter"
|
111
|
+
|
112
|
+
output :filtered,
|
113
|
+
:description => "Didn't pass filter checks",
|
114
|
+
:display_as => "Failed Filter"
|
115
|
+
|
116
|
+
output :starttime,
|
117
|
+
:description => "Time the server started",
|
118
|
+
:display_as => "Start Time"
|
119
|
+
|
120
|
+
output :total,
|
121
|
+
:description => "Total messages received",
|
122
|
+
:display_as => "Total Messages"
|
123
|
+
|
124
|
+
output :replies,
|
125
|
+
:description => "Replies sent back to clients",
|
126
|
+
:display_as => "Replies"
|
127
|
+
|
128
|
+
output :configfile,
|
129
|
+
:description => "Config file used to start the daemon",
|
130
|
+
:display_as => "Config File"
|
131
|
+
|
132
|
+
output :version,
|
133
|
+
:description => "MCollective Version",
|
134
|
+
:display_as => "Version"
|
135
|
+
|
136
|
+
output :ttlexpired,
|
137
|
+
:description => "Messages that did pass TTL checks",
|
138
|
+
:display_as => "TTL Expired"
|
139
|
+
|
140
|
+
summarize do
|
141
|
+
aggregate summary(:version)
|
142
|
+
aggregate summary(:agents)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
action "agent_inventory", :description => "Inventory of all agents on the server" do
|
147
|
+
display :always
|
148
|
+
|
149
|
+
output :agents,
|
150
|
+
:description => "List of agents on the server",
|
151
|
+
:display_as => "Agents"
|
152
|
+
end
|
153
|
+
|
154
|
+
action "get_config_item", :description => "Get the active value of a specific config property" do
|
155
|
+
display :always
|
156
|
+
|
157
|
+
input :item,
|
158
|
+
:prompt => "Configuration Item",
|
159
|
+
:description => "The item to retrieve from the server",
|
160
|
+
:type => :string,
|
161
|
+
:validation => '^.+$',
|
162
|
+
:optional => false,
|
163
|
+
:maxlength => 50
|
164
|
+
|
165
|
+
output :item,
|
166
|
+
:description => "The config property being retrieved",
|
167
|
+
:display_as => "Property"
|
168
|
+
|
169
|
+
output :value,
|
170
|
+
:description => "The value that is in use",
|
171
|
+
:display_as => "Value"
|
172
|
+
|
173
|
+
summarize do
|
174
|
+
aggregate summary(:value)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
action "get_data", :description => "Get data from a data plugin" do
|
179
|
+
display :always
|
180
|
+
|
181
|
+
input :source,
|
182
|
+
:prompt => "Data Source",
|
183
|
+
:description => "The data plugin to retrieve information from",
|
184
|
+
:type => :string,
|
185
|
+
:validation => '^\w+$',
|
186
|
+
:optional => false,
|
187
|
+
:maxlength => 50
|
188
|
+
|
189
|
+
input :query,
|
190
|
+
:prompt => "Query",
|
191
|
+
:description => "The query argument to supply to the data plugin",
|
192
|
+
:type => :string,
|
193
|
+
:validation => '^.+$',
|
194
|
+
:optional => true,
|
195
|
+
:maxlength => 50
|
196
|
+
end
|
197
|
+
|
198
|
+
action "ping", :description => "Responds to requests for PING with PONG" do
|
199
|
+
display :always
|
200
|
+
|
201
|
+
output :pong,
|
202
|
+
:description => "The local timestamp",
|
203
|
+
:display_as => "Timestamp"
|
204
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module MCollective
|
2
|
+
module Agent
|
3
|
+
class Rpcutil<RPC::Agent
|
4
|
+
# Basic system inventory, same as the basic discovery agent
|
5
|
+
action "inventory" do
|
6
|
+
reply[:agents] = Agents.agentlist
|
7
|
+
reply[:facts] = PluginManager["facts_plugin"].get_facts
|
8
|
+
reply[:version] = MCollective.version
|
9
|
+
reply[:classes] = []
|
10
|
+
reply[:main_collective] = config.main_collective
|
11
|
+
reply[:collectives] = config.collectives
|
12
|
+
reply[:data_plugins] = PluginManager.grep(/_data$/)
|
13
|
+
|
14
|
+
cfile = Config.instance.classesfile
|
15
|
+
if File.exist?(cfile)
|
16
|
+
reply[:classes] = File.readlines(cfile).map {|i| i.chomp}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Retrieve a single fact from the node
|
21
|
+
action "get_fact" do
|
22
|
+
validate :fact, String
|
23
|
+
|
24
|
+
reply[:fact] = request[:fact]
|
25
|
+
reply[:value] = Facts[request[:fact]]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get the global stats for this mcollectied
|
29
|
+
action "daemon_stats" do
|
30
|
+
stats = PluginManager["global_stats"].to_hash
|
31
|
+
|
32
|
+
reply[:threads] = stats[:threads]
|
33
|
+
reply[:agents] = stats[:agents]
|
34
|
+
reply[:pid] = stats[:pid]
|
35
|
+
reply[:times] = stats[:times]
|
36
|
+
reply[:configfile] = Config.instance.configfile
|
37
|
+
reply[:version] = MCollective.version
|
38
|
+
|
39
|
+
reply.data.merge!(stats[:stats])
|
40
|
+
end
|
41
|
+
|
42
|
+
# Builds an inventory of all agents on teh machine
|
43
|
+
# including license, version and timeout information
|
44
|
+
action "agent_inventory" do
|
45
|
+
reply[:agents] = []
|
46
|
+
|
47
|
+
Agents.agentlist.sort.each do |target_agent|
|
48
|
+
agent = PluginManager["#{target_agent}_agent"]
|
49
|
+
actions = agent.methods.grep(/_agent/)
|
50
|
+
|
51
|
+
agent_data = {:agent => target_agent,
|
52
|
+
:license => "unknown",
|
53
|
+
:timeout => agent.timeout,
|
54
|
+
:description => "unknown",
|
55
|
+
:name => target_agent,
|
56
|
+
:url => "unknown",
|
57
|
+
:version => "unknown",
|
58
|
+
:author => "unknown"}
|
59
|
+
|
60
|
+
agent_data.merge!(agent.meta)
|
61
|
+
|
62
|
+
reply[:agents] << agent_data
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Retrieves a single config property that is in effect
|
67
|
+
action "get_config_item" do
|
68
|
+
validate :item, String
|
69
|
+
|
70
|
+
reply.fail! "Unknown config property #{request[:item]}" unless config.respond_to?(request[:item])
|
71
|
+
|
72
|
+
reply[:item] = request[:item]
|
73
|
+
reply[:value] = config.send(request[:item])
|
74
|
+
end
|
75
|
+
|
76
|
+
# Responds to PING requests with the local timestamp
|
77
|
+
action "ping" do
|
78
|
+
reply[:pong] = Time.now.to_i
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns all configured collectives
|
82
|
+
action "collective_info" do
|
83
|
+
config = Config.instance
|
84
|
+
reply[:main_collective] = config.main_collective
|
85
|
+
reply[:collectives] = config.collectives
|
86
|
+
end
|
87
|
+
|
88
|
+
action "get_data" do
|
89
|
+
validate :source, String
|
90
|
+
|
91
|
+
query = Data.ddl_transform_input(Data.ddl(request[:source]), request[:query].to_s)
|
92
|
+
|
93
|
+
data = Data[ request[:source] ].lookup(query)
|
94
|
+
|
95
|
+
data.keys.each do |key|
|
96
|
+
reply[key] = data[key]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'puppet'
|
3
|
+
require 'grit'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
AGENT_MCOLLECTIVE_LOCATION = "#{::MCollective::Config.instance.libdir}/mcollective/agent/"
|
8
|
+
|
9
|
+
module MCollective
|
10
|
+
module Agent
|
11
|
+
class Sync_agent_code < RPC::Agent
|
12
|
+
def initialize()
|
13
|
+
super()
|
14
|
+
@log = Log.instance
|
15
|
+
@reply_data = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def old_run_action
|
19
|
+
ret ||= Response.new()
|
20
|
+
#git_server_url = request[:git_server_url]
|
21
|
+
raise "git server is not set in facts" unless git_server_url = Facts["git-server"]
|
22
|
+
branch = request[:branch]
|
23
|
+
cmd_opts = {:raise => true, :timeout => 60}
|
24
|
+
|
25
|
+
begin
|
26
|
+
# Amar: if git repo dir exists pull code, otherwise (first converge case) clone agent's project from git
|
27
|
+
if File.directory?(AgentGitPath)
|
28
|
+
grit_repo = ::Grit::Repo.new(AgentGitPath)
|
29
|
+
grit_repo.git.send(:pull, cmd_opts)
|
30
|
+
@log.info("Latest agent code pulled from GIT Repositoy.")
|
31
|
+
else
|
32
|
+
clone_args = [git_server_url, AgentGitPath]
|
33
|
+
clone_args += ["-b", branch] if branch
|
34
|
+
::Grit::Git.new("").clone(cmd_opts, *clone_args)
|
35
|
+
@log.info("Agent project successfully cloned from GIT Repository.")
|
36
|
+
end
|
37
|
+
# Copy latest agents to mcollective agent's directory
|
38
|
+
agents = Dir.glob("#{AgentGitPath}/mcollective_additions/plugins/v2.2/agent/*")
|
39
|
+
FileUtils.cp_r(agents, AgentMcollectivePath)
|
40
|
+
@log.info("Agent files copied to mcollective destination.")
|
41
|
+
# System call to restart mcollective
|
42
|
+
system("sudo /etc/init.d/mcollective restart")
|
43
|
+
@log.info("mcollective system restart completed.")
|
44
|
+
|
45
|
+
ret.set_status_succeeded!()
|
46
|
+
rescue Exception => e
|
47
|
+
@log.error("Error in syncing agent's code with GIT Repository: '#{git_server_url}'. Error: #{e}")
|
48
|
+
ret.set_status_failed!()
|
49
|
+
error_info = { :errors => { :message => "Error in syncing agent's code with GIT Repository: '#{git_server_url}'." } }
|
50
|
+
ret.merge!(error_info)
|
51
|
+
end
|
52
|
+
|
53
|
+
return ret
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
#TODO: this should be common accross Agents
|
58
|
+
class Response < Hash
|
59
|
+
def initialize(hash={})
|
60
|
+
super()
|
61
|
+
self.merge!(hash)
|
62
|
+
self[:status] = :unknown unless hash.has_key?(:status)
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_hash()
|
66
|
+
Hash.new.merge(self)
|
67
|
+
end
|
68
|
+
|
69
|
+
def failed?()
|
70
|
+
self[:status] == :failed
|
71
|
+
end
|
72
|
+
|
73
|
+
def set_status_failed!()
|
74
|
+
self[:status] = :failed
|
75
|
+
end
|
76
|
+
def set_status_succeeded!()
|
77
|
+
self[:status] = :succeeded
|
78
|
+
end
|
79
|
+
def set_dynamic_attributes!(dynamic_attributes)
|
80
|
+
self[:dynamic_attributes] = dynamic_attributes
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|