bolt 0.17.2 → 0.18.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.

Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/bolt-modules/boltlib/lib/puppet/datatypes/result.rb +2 -0
  3. data/bolt-modules/boltlib/lib/puppet/datatypes/resultset.rb +2 -0
  4. data/bolt-modules/boltlib/lib/puppet/datatypes/target.rb +2 -0
  5. data/bolt-modules/boltlib/lib/puppet/functions/add_facts.rb +30 -0
  6. data/bolt-modules/boltlib/lib/puppet/functions/facts.rb +30 -0
  7. data/bolt-modules/boltlib/lib/puppet/functions/fail_plan.rb +4 -2
  8. data/bolt-modules/boltlib/lib/puppet/functions/file_upload.rb +2 -0
  9. data/bolt-modules/boltlib/lib/puppet/functions/get_targets.rb +2 -0
  10. data/bolt-modules/boltlib/lib/puppet/functions/run_command.rb +2 -0
  11. data/bolt-modules/boltlib/lib/puppet/functions/run_plan.rb +3 -2
  12. data/bolt-modules/boltlib/lib/puppet/functions/run_script.rb +2 -1
  13. data/bolt-modules/boltlib/lib/puppet/functions/run_task.rb +2 -0
  14. data/bolt-modules/boltlib/lib/puppet/functions/set_var.rb +8 -0
  15. data/bolt-modules/boltlib/lib/puppet/functions/vars.rb +9 -1
  16. data/exe/bolt +1 -0
  17. data/exe/bolt-inventory-pdb +1 -0
  18. data/lib/bolt.rb +2 -0
  19. data/lib/bolt/cli.rb +40 -34
  20. data/lib/bolt/config.rb +9 -2
  21. data/lib/bolt/error.rb +2 -0
  22. data/lib/bolt/executor.rb +2 -0
  23. data/lib/bolt/inventory.rb +36 -28
  24. data/lib/bolt/inventory/group.rb +14 -10
  25. data/lib/bolt/logger.rb +2 -0
  26. data/lib/bolt/node/errors.rb +2 -0
  27. data/lib/bolt/node/output.rb +2 -0
  28. data/lib/bolt/notifier.rb +2 -0
  29. data/lib/bolt/outputter.rb +2 -0
  30. data/lib/bolt/outputter/human.rb +28 -34
  31. data/lib/bolt/outputter/json.rb +7 -5
  32. data/lib/bolt/pal.rb +66 -30
  33. data/lib/bolt/puppetdb.rb +2 -0
  34. data/lib/bolt/puppetdb/client.rb +5 -3
  35. data/lib/bolt/puppetdb/config.rb +2 -0
  36. data/lib/bolt/result.rb +2 -0
  37. data/lib/bolt/result_set.rb +2 -0
  38. data/lib/bolt/target.rb +2 -0
  39. data/lib/bolt/transport/base.rb +4 -2
  40. data/lib/bolt/transport/local.rb +2 -0
  41. data/lib/bolt/transport/local/shell.rb +2 -0
  42. data/lib/bolt/transport/orch.rb +3 -1
  43. data/lib/bolt/transport/ssh.rb +3 -1
  44. data/lib/bolt/transport/ssh/connection.rb +3 -1
  45. data/lib/bolt/transport/winrm.rb +9 -2
  46. data/lib/bolt/transport/winrm/connection.rb +11 -3
  47. data/lib/bolt/util.rb +9 -7
  48. data/lib/bolt/version.rb +3 -1
  49. data/lib/bolt_ext/puppetdb_inventory.rb +3 -2
  50. data/modules/aggregate/lib/puppet/functions/aggregate/count.rb +2 -0
  51. data/modules/aggregate/lib/puppet/functions/aggregate/nodes.rb +2 -0
  52. data/modules/canary/lib/puppet/functions/canary/merge.rb +2 -0
  53. data/modules/canary/lib/puppet/functions/canary/random_split.rb +2 -0
  54. data/modules/canary/lib/puppet/functions/canary/skip.rb +2 -0
  55. metadata +37 -35
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
  require 'bolt/util'
3
5
  require 'bolt/target'
@@ -5,7 +7,7 @@ require 'bolt/inventory/group'
5
7
 
6
8
  module Bolt
7
9
  class Inventory
8
- ENVIRONMENT_VAR = 'BOLT_INVENTORY'.freeze
10
+ ENVIRONMENT_VAR = 'BOLT_INVENTORY'
9
11
 
10
12
  class ValidationError < Bolt::Error
11
13
  attr_accessor :path
@@ -41,11 +43,7 @@ module Bolt
41
43
  def self.from_config(config)
42
44
  if ENV.include?(ENVIRONMENT_VAR)
43
45
  begin
44
- # rubocop:disable YAMLLoad
45
- data = YAML.load(ENV[ENVIRONMENT_VAR])
46
- # In older releases of psych SyntaxError is not a subclass of Exception
47
- rescue Psych::SyntaxError
48
- raise Bolt::CLIError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
46
+ data = YAML.safe_load(ENV[ENVIRONMENT_VAR])
49
47
  rescue Psych::Exception
50
48
  raise Bolt::CLIError, "Could not parse inventory from $#{ENVIRONMENT_VAR}"
51
49
  end
@@ -56,7 +54,6 @@ module Bolt
56
54
  inventory = new(data, config)
57
55
  inventory.validate
58
56
  inventory.collect_groups
59
- inventory.add_localhost
60
57
  inventory
61
58
  end
62
59
 
@@ -68,6 +65,7 @@ module Bolt
68
65
  @groups = Group.new(data.merge('name' => 'all'))
69
66
  @group_lookup = {}
70
67
  @target_vars = {}
68
+ @target_facts = {}
71
69
  end
72
70
 
73
71
  def validate
@@ -79,16 +77,6 @@ module Bolt
79
77
  @group_lookup = @groups.collect_groups
80
78
  end
81
79
 
82
- def add_localhost
83
- # Append a 'localhost' group if not already present.
84
- unless @group_lookup.include?('localhost') || @groups.node_names.include?('localhost')
85
- @groups.nodes['localhost'] = {
86
- 'name' => 'localhost',
87
- 'config' => { 'transport' => 'local' }
88
- }
89
- end
90
- end
91
-
92
80
  def get_targets(targets)
93
81
  targets = expand_targets(targets)
94
82
  targets = if targets.is_a? Array
@@ -108,6 +96,15 @@ module Bolt
108
96
  @target_vars[target.name]
109
97
  end
110
98
 
99
+ def add_facts(target, new_facts = {})
100
+ @logger.warn("No facts to add") if new_facts.empty?
101
+ set_facts(target.name, new_facts)
102
+ end
103
+
104
+ def facts(target)
105
+ @target_facts[target.name]
106
+ end
107
+
111
108
  #### PRIVATE ####
112
109
  #
113
110
  # For debugging only now
@@ -119,19 +116,22 @@ module Bolt
119
116
  # Pass a target to get_targets for a public version of this
120
117
  # Should this reconfigure configured targets?
121
118
  def update_target(target)
122
- data = @groups.data_for(target.name) || {}
119
+ data = @groups.data_for(target.name)
123
120
 
124
- unless data['config']
125
- @logger.debug("Did not find #{target.name} in inventory")
126
- data['config'] = {}
121
+ unless data
122
+ data = {}
123
+ data['config'] = { 'transport' => 'local' } if target.name == 'localhost'
127
124
  end
128
125
 
129
- unless data['vars']
130
- @logger.debug("Did not find any variables for #{target.name} in inventory")
131
- data['vars'] = {}
126
+ unless data['config']
127
+ @logger.debug("Did not find config for #{target.name} in inventory")
128
+ data['config'] = {}
132
129
  end
133
130
 
134
- set_vars_from_hash(target.name, data['vars'])
131
+ # These should only get set from the inventory if they have not yet
132
+ # been instantiated
133
+ set_vars_from_hash(target.name, data['vars']) unless @target_vars[target.name]
134
+ set_facts(target.name, data['facts']) unless @target_facts[target.name]
135
135
 
136
136
  # Use Config object to ensure config section is treated consistently with config file
137
137
  conf = @config.deep_clone
@@ -183,15 +183,23 @@ module Bolt
183
183
  end
184
184
  private :expand_targets
185
185
 
186
- def set_vars_from_hash(target_name, data)
186
+ def set_vars_from_hash(tname, data)
187
187
  if data
188
188
  # Instantiate empty vars hash in case no vars are defined
189
- @target_vars[target_name] = @target_vars[target_name] || {}
189
+ @target_vars[tname] ||= {}
190
190
  # Assign target new merged vars hash
191
191
  # This is essentially a copy-on-write to maintain the immutability of @target_vars
192
- @target_vars[target_name] = @target_vars[target_name].merge(data).freeze
192
+ @target_vars[tname] = @target_vars[tname].merge(data).freeze
193
193
  end
194
194
  end
195
195
  private :set_vars_from_hash
196
+
197
+ def set_facts(tname, hash)
198
+ if hash
199
+ @target_facts[tname] ||= {}
200
+ @target_facts[tname] = Bolt::Util.deep_merge(@target_facts[tname], hash).freeze
201
+ end
202
+ end
203
+ private :set_facts
196
204
  end
197
205
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bolt
2
4
  class Inventory
3
5
  # Group is a specific implementation of Inventory based on nested
@@ -10,18 +12,17 @@ module Bolt
10
12
  @name = data['name']
11
13
  @nodes = {}
12
14
 
13
- if data['nodes']
14
- data['nodes'].each do |n|
15
- n = { 'name' => n } if n.is_a? String
16
- if @nodes.include? n['name']
17
- @logger.warn("Ignoring duplicate node in #{@name}: #{n}")
18
- else
19
- @nodes[n['name']] = n
20
- end
15
+ data['nodes']&.each do |n|
16
+ n = { 'name' => n } if n.is_a? String
17
+ if @nodes.include? n['name']
18
+ @logger.warn("Ignoring duplicate node in #{@name}: #{n}")
19
+ else
20
+ @nodes[n['name']] = n
21
21
  end
22
22
  end
23
23
 
24
24
  @vars = data['vars'] || {}
25
+ @facts = data['facts'] || {}
25
26
  @config = data['config'] || {}
26
27
  @groups = if data['groups']
27
28
  data['groups'].map { |g| Group.new(g) }
@@ -89,8 +90,7 @@ module Bolt
89
90
  end
90
91
 
91
92
  # The data functions below expect and return nil or a hash of the schema
92
- # { 'config' => Hash , 'vars' => Hash, groups => Array }
93
- # As we add more options beyond config this schema will grow
93
+ # { 'config' => Hash , 'vars' => Hash, 'facts' => Hash, groups => Array }
94
94
  def data_for(node_name)
95
95
  data_merge(group_collect(node_name), node_collect(node_name))
96
96
  end
@@ -99,6 +99,7 @@ module Bolt
99
99
  if (data = @nodes[node_name])
100
100
  { 'config' => data['config'] || {},
101
101
  'vars' => data['vars'] || {},
102
+ 'facts' => data['facts'] || {},
102
103
  # groups come from group_data
103
104
  'groups' => [] }
104
105
  end
@@ -107,12 +108,14 @@ module Bolt
107
108
  def group_data
108
109
  { 'config' => @config,
109
110
  'vars' => @vars,
111
+ 'facts' => @facts,
110
112
  'groups' => [@name] }
111
113
  end
112
114
 
113
115
  def empty_data
114
116
  { 'config' => {},
115
117
  'vars' => {},
118
+ 'facts' => {},
116
119
  'groups' => [] }
117
120
  end
118
121
 
@@ -127,6 +130,7 @@ module Bolt
127
130
  # are assigned a new hash, rather than merging the existing value
128
131
  # with the value meant to replace it
129
132
  'vars' => data2['vars'].merge(data1['vars']),
133
+ 'facts' => Bolt::Util.deep_merge(data1['facts'], data2['facts']),
130
134
  'groups' => data2['groups'] + data1['groups']
131
135
  }
132
136
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logging'
2
4
 
3
5
  module Bolt
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bolt
2
4
  class Node
3
5
  class BaseError < Bolt::Error
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
  require 'bolt/result'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'concurrent'
2
4
 
3
5
  module Bolt
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bolt
2
4
  class Outputter
3
5
  def self.for_format(format)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'terminal-table'
2
4
  module Bolt
3
5
  class Outputter
@@ -74,25 +76,25 @@ module Bolt
74
76
  def print_summary(results, elapsed_time)
75
77
  ok_set = results.ok_set
76
78
  unless ok_set.empty?
77
- @stream.puts format('Successful on %d node%s: %s',
78
- ok_set.size,
79
- ok_set.size == 1 ? '' : 's',
80
- ok_set.names.join(','))
79
+ @stream.puts format('Successful on %<size>d node%<plural>s: %<names>s',
80
+ size: ok_set.size,
81
+ plural: ok_set.size == 1 ? '' : 's',
82
+ names: ok_set.names.join(','))
81
83
  end
82
84
 
83
85
  error_set = results.error_set
84
86
  unless error_set.empty?
85
87
  @stream.puts colorize(:red,
86
- format('Failed on %d node%s: %s',
87
- error_set.size,
88
- error_set.size == 1 ? '' : 's',
89
- error_set.names.join(',')))
88
+ format('Failed on %<size>d node%<plural>s: %<names>s',
89
+ size: error_set.size,
90
+ plural: error_set.size == 1 ? '' : 's',
91
+ names: error_set.names.join(',')))
90
92
  end
91
93
 
92
- @stream.puts format('Ran on %d node%s in %.2f seconds',
93
- results.size,
94
- results.size == 1 ? '' : 's',
95
- elapsed_time)
94
+ @stream.puts format('Ran on %<size>d node%<plural>s in %<elapsed>.2f seconds',
95
+ size: results.size,
96
+ plural: results.size == 1 ? '' : 's',
97
+ elapsed: elapsed_time)
96
98
  end
97
99
 
98
100
  def print_table(results)
@@ -113,9 +115,9 @@ module Bolt
113
115
  # @param [Hash] A hash representing the task
114
116
  def print_task_info(task)
115
117
  # Building lots of strings...
116
- pretty_params = ""
117
- task_info = ""
118
- usage = "bolt task run --nodes, -n <node-name> #{task['name']}"
118
+ pretty_params = +""
119
+ task_info = +""
120
+ usage = +"bolt task run --nodes, -n <node-name> #{task['name']}"
119
121
 
120
122
  if task['parameters']
121
123
  replace_data_type(task['parameters'])
@@ -143,21 +145,13 @@ module Bolt
143
145
  # @param [Hash] A hash representing the plan
144
146
  def print_plan_info(plan)
145
147
  # Building lots of strings...
146
- pretty_params = ""
147
- plan_info = ""
148
- usage = "bolt plan run #{plan['name']}"
149
-
150
- if plan['parameters']
151
- plan['parameters'].each do |p|
152
- name = p['name']
153
- pretty_params << "- #{name}: #{p['type']}\n"
154
- usage << if p.include?('default_value')
155
- # TODO: print the default value when available
156
- " [#{name}=<value>]"
157
- else
158
- " #{name}=<value>"
159
- end
160
- end
148
+ pretty_params = +""
149
+ plan_info = +""
150
+ usage = +"bolt plan run #{plan['name']}"
151
+
152
+ plan['parameters']&.each do |name, p|
153
+ pretty_params << "- #{name}: #{p['type']}\n"
154
+ usage << (p.include?('default_value') ? " [#{name}=<value>]" : " #{name}=<value>")
161
155
  end
162
156
 
163
157
  plan_info << "\n#{plan['name']}"
@@ -184,10 +178,10 @@ module Bolt
184
178
  end
185
179
  end
186
180
 
187
- def fatal_error(e)
188
- @stream.puts(colorize(:red, e.message))
189
- if e.is_a? Bolt::RunFailure
190
- @stream.puts ::JSON.pretty_generate(e.result_set)
181
+ def fatal_error(err)
182
+ @stream.puts(colorize(:red, err.message))
183
+ if err.is_a? Bolt::RunFailure
184
+ @stream.puts ::JSON.pretty_generate(err.result_set)
191
185
  end
192
186
  end
193
187
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bolt
2
4
  class Outputter
3
5
  class JSON < Bolt::Outputter
@@ -32,9 +34,9 @@ module Bolt
32
34
  @stream.puts "],\n"
33
35
  @preceding_item = false
34
36
  @items_open = false
35
- @stream.puts format('"node_count": %d, "elapsed_time": %d }',
36
- results.size,
37
- elapsed_time)
37
+ @stream.puts format('"node_count": %<size>d, "elapsed_time": %<elapsed>d }',
38
+ size: results.size,
39
+ elapsed: elapsed_time)
38
40
  end
39
41
 
40
42
  def print_table(results)
@@ -55,10 +57,10 @@ module Bolt
55
57
  @stream.puts result.to_json
56
58
  end
57
59
 
58
- def fatal_error(e)
60
+ def fatal_error(err)
59
61
  @stream.puts "],\n" if @items_open
60
62
  @stream.puts '"_error": ' if @object_open
61
- @stream.puts e.to_json
63
+ @stream.puts err.to_json
62
64
  @stream.puts '}' if @object_open
63
65
  end
64
66
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bolt/executor'
2
4
  require 'bolt/error'
3
5
 
@@ -46,6 +48,10 @@ module Bolt
46
48
  _pcore_init_hash.to_json(opts)
47
49
  end
48
50
  end
51
+
52
+ unless Puppet.settings.global_defaults_initialized?
53
+ Puppet.initialize_settings
54
+ end
49
55
  end
50
56
 
51
57
  # Create a top-level alias for TargetSpec so that users don't have to
@@ -63,18 +69,16 @@ module Bolt
63
69
  # Runs a block in a PAL script compiler configured for Bolt. Catches
64
70
  # exceptions thrown by the block and re-raises them ensuring they are
65
71
  # Bolt::Errors since the script compiler block will squash all exceptions.
66
- def in_bolt_compiler(opts = [])
67
- Puppet.initialize_settings(opts)
72
+ def in_bolt_compiler
68
73
  r = Puppet::Pal.in_tmp_environment('bolt', modulepath: full_modulepath(@config[:modulepath]), facts: {}) do |pal|
69
74
  pal.with_script_compiler do |compiler|
70
75
  add_target_spec(compiler)
71
76
  begin
72
77
  yield compiler
73
78
  rescue Puppet::PreformattedError => err
74
- # Puppet sometimes rescues exceptions notes the location and reraises
75
- # For now return the original error. Exception cause support was added in Ruby 2.1
76
- # so we fall back to reporting the error we got for Ruby 2.0.
77
- if err.respond_to?(:cause) && err.cause
79
+ # Puppet sometimes rescues exceptions notes the location and reraises.
80
+ # Return the original error.
81
+ if err.cause
78
82
  if err.cause.is_a? Bolt::Error
79
83
  err.cause
80
84
  else
@@ -107,8 +111,8 @@ module Bolt
107
111
 
108
112
  def in_plan_compiler(executor, inventory)
109
113
  with_bolt_executor(executor, inventory) do
110
- with_puppet_settings do |opts|
111
- in_bolt_compiler(opts) do |compiler|
114
+ with_puppet_settings do
115
+ in_bolt_compiler do |compiler|
112
116
  yield compiler
113
117
  end
114
118
  end
@@ -123,13 +127,16 @@ module Bolt
123
127
  end
124
128
  end
125
129
 
130
+ # TODO: PUP-8553 should replace this
126
131
  def with_puppet_settings
127
132
  Dir.mktmpdir('bolt') do |dir|
128
133
  cli = []
129
134
  Puppet::Settings::REQUIRED_APP_SETTINGS.each do |setting|
130
135
  cli << "--#{setting}" << dir
131
136
  end
132
- yield cli
137
+ Puppet.settings.send(:clear_everything_for_tests)
138
+ Puppet.initialize_settings(cli)
139
+ yield
133
140
  end
134
141
  end
135
142
 
@@ -143,6 +150,37 @@ module Bolt
143
150
  end
144
151
  end
145
152
 
153
+ def parse_params(type, object_name, params)
154
+ in_bolt_compiler do |compiler|
155
+ if type == 'task'
156
+ param_spec = compiler.task_signature(object_name)&.task_hash&.dig('parameters')
157
+ elsif type == 'plan'
158
+ plan = compiler.plan_signature(object_name)
159
+ param_spec = plan_hash(object_name, plan) if plan
160
+ end
161
+ param_spec ||= {}
162
+
163
+ params.each_with_object({}) do |(name, str), acc|
164
+ type = param_spec.dig(name, 'type')
165
+ begin
166
+ parsed = JSON.parse(str, quirks_mode: true)
167
+ # The type may not exist if the module is remote on orch or if a task
168
+ # defines no parameters. Since we treat no parameters as Any we
169
+ # should parse everything in this case
170
+ acc[name] = if type && !type.instance?(parsed)
171
+ str
172
+ else
173
+ parsed
174
+ end
175
+ rescue JSON::ParserError
176
+ # This value may not be assignable in which case run_* will error
177
+ acc[name] = str
178
+ end
179
+ acc
180
+ end
181
+ end
182
+ end
183
+
146
184
  def get_task_info(task_name)
147
185
  task = in_bolt_compiler do |compiler|
148
186
  compiler.task_signature(task_name)
@@ -161,6 +199,20 @@ module Bolt
161
199
  end
162
200
  end
163
201
 
202
+ # This coverts a plan signature object into a format approximating
203
+ # the task_hash of a task_signature
204
+ def plan_hash(plan_name, plan)
205
+ elements = plan.params_type.elements || {}
206
+ parameters = elements.each_with_object({}) do |param, acc|
207
+ acc[param.name] = { 'type' => param.value_type }
208
+ acc[param.name]['default_value'] = nil if param.key_type.is_a?(Puppet::Pops::Types::POptionalType)
209
+ end
210
+ {
211
+ 'name' => plan_name,
212
+ 'parameters' => parameters
213
+ }
214
+ end
215
+
164
216
  def get_plan_info(plan_name)
165
217
  plan = in_bolt_compiler do |compiler|
166
218
  compiler.plan_signature(plan_name)
@@ -169,34 +221,18 @@ module Bolt
169
221
  if plan.nil?
170
222
  raise Bolt::CLIError, Bolt::Error.unknown_plan(plan_name)
171
223
  end
172
-
173
- elements = plan.params_type.elements
174
- {
175
- 'name' => plan_name,
176
- 'parameters' =>
177
- unless elements.nil? || elements.empty?
178
- elements.map { |e|
179
- p = {
180
- 'name' => e.name,
181
- 'type' => e.value_type
182
- }
183
- # TODO: when the default value can be obtained use the actual value instead of nil
184
- p['default_value'] = nil if e.key_type.is_a?(Puppet::Pops::Types::POptionalType)
185
- p
186
- }
187
- end
188
- }
224
+ plan_hash(plan_name, plan)
189
225
  end
190
226
 
191
- def run_task(object, targets, params, executor, inventory, &eventblock)
227
+ def run_task(task_name, targets, params, executor, inventory, &eventblock)
192
228
  in_task_compiler(executor, inventory) do |compiler|
193
- compiler.call_function('run_task', object, targets, params, &eventblock)
229
+ compiler.call_function('run_task', task_name, targets, params, &eventblock)
194
230
  end
195
231
  end
196
232
 
197
- def run_plan(object, params, executor = nil, inventory = nil)
233
+ def run_plan(plan_name, params, executor = nil, inventory = nil)
198
234
  in_plan_compiler(executor, inventory) do |compiler|
199
- compiler.call_function('run_plan', object, params)
235
+ compiler.call_function('run_plan', plan_name, params)
200
236
  end
201
237
  end
202
238
  end