gat 0.2.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.
Files changed (41) hide show
  1. data/History.txt +84 -0
  2. data/PostInstall.txt +22 -0
  3. data/README.txt +49 -0
  4. data/Rakefile +12 -0
  5. data/bin/gat +15 -0
  6. data/lib/gat.rb +323 -0
  7. data/lib/gat/action/base.rb +197 -0
  8. data/lib/gat/action/ruby_method.rb +35 -0
  9. data/lib/gat/action/shell_command.rb +185 -0
  10. data/lib/gat/boot.rb +63 -0
  11. data/lib/gat/checks.rb +136 -0
  12. data/lib/gat/debug.rb +29 -0
  13. data/lib/gat/dependence/argument.rb +62 -0
  14. data/lib/gat/dependence/base.rb +54 -0
  15. data/lib/gat/dependence/folder.rb +104 -0
  16. data/lib/gat/dependence/program.rb +36 -0
  17. data/lib/gat/email.rb +80 -0
  18. data/lib/gat/exceptions.rb +163 -0
  19. data/lib/gat/extends.rb +102 -0
  20. data/lib/gat/help.rb +79 -0
  21. data/lib/gat/interpreter.rb +93 -0
  22. data/lib/gat/launcher.rb +100 -0
  23. data/lib/gat/logger.rb +331 -0
  24. data/lib/gat/operation.rb +253 -0
  25. data/lib/gat/version.rb +20 -0
  26. data/lib/gatgets/dar_backup/dar_backup.rb +367 -0
  27. data/lib/gatgets/dar_backup/dar_backup.yml +387 -0
  28. data/lib/gatgets/dar_backup/launcher.rb +44 -0
  29. data/lib/gatgets/dar_backup/templates/list_backups.erb +31 -0
  30. data/lib/gatgets/dar_backup/templates/search.erb +33 -0
  31. data/lib/gatgets/gatgets_manager/gatgets_manager.rb +65 -0
  32. data/lib/gatgets/gatgets_manager/gatgets_manager.yml +71 -0
  33. data/lib/gatgets/gatgets_manager/templates/list.erb +9 -0
  34. data/lib/gatgets/synchronization/README +26 -0
  35. data/lib/gatgets/synchronization/launcher.rb +20 -0
  36. data/lib/gatgets/synchronization/launcher_cron.sh +69 -0
  37. data/lib/gatgets/synchronization/synchronization.rb +123 -0
  38. data/lib/gatgets/synchronization/synchronization.yml +144 -0
  39. data/test/test_gat.rb +36 -0
  40. data/test/test_helper.rb +3 -0
  41. metadata +131 -0
data/lib/gat/logger.rb ADDED
@@ -0,0 +1,331 @@
1
+ =begin
2
+ This file is part of GAT: http://gat.rubyforge.org
3
+
4
+ (c) 2008-2009 A.C. Gnoxys info@gnoxys.net
5
+
6
+ GAT (Gnoxys Administration Tools) is released under the GPL License 3 or higher.
7
+ You can get a copy of the license easily at http://www.gnu.org/licenses/gpl.html.
8
+
9
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
10
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
11
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
12
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
13
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
14
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16
+ =end
17
+
18
+ module Gat
19
+ class Logger
20
+
21
+ include Gat::Debug
22
+
23
+ attr_reader :gatget
24
+ attr_reader :global_types
25
+
26
+ attr_accessor :times
27
+ attr_accessor :entries
28
+
29
+ def initialize(gatget)
30
+ @global_types = { 'trace' => { 'description' => 'Traces debug logs', 'verbose' => 4 },
31
+ 'message' => { 'description' => 'Message by gat', 'verbose' => 3 },
32
+ 'warning' => { 'description' => 'Warnings', 'verbose' => 2 },
33
+ 'error' => { 'description' => 'Errors at Gatget', 'verbose' => 1 },
34
+ 'direct' => { 'description' => 'Direct Outputs', 'verbose' => 0 }
35
+ }
36
+
37
+ @gatget = gatget || raise(GatgetException.new("Logger needs a gatget object", "initialize_logger"))
38
+ @times = Hash.new
39
+ @times['init'] = Time.now
40
+ @entries = Array.new
41
+ end
42
+
43
+
44
+
45
+ # Log method
46
+ # Store logs entry into entries array instance attribute
47
+ def log(*args)
48
+ log_entry = LogEntry.new(self, *args)
49
+ direct_output?(log_entry)
50
+ @entries << log_entry
51
+ end
52
+
53
+
54
+ # Gatget Logger output
55
+ # When operation or whatever is finish, gatget logger throw the outputs
56
+ #
57
+ # 3 Standar types are defined
58
+ # - Template output: Template output works with ERB standard template rubye engine. Template is evalued at printed as direct log
59
+ # Template is searched at <gatget_path>/templates/<operation_name>.erb
60
+ #
61
+ # - Email output: Email output deliver log to default email config or raise Exception. Default email config must be stored at
62
+ # @gatget.config['email_config']. Multiple to address can be defined and smtp && sendmail deliver methods are avalaible
63
+ #
64
+ # - YML output: YML output consist in a dump of logger to a yml structure file, so, then, log can be easily interpreted. YML log must be stored
65
+ # at /var/log/gat/<gatget_name>_<operation_name>_<time>.yml, so, log folder must we writtable or exception will be launched.
66
+ #
67
+ # Gat is planned to have more custom outputs by yml config like
68
+ #
69
+ # At operation definition (gatget yml config)
70
+ #
71
+ # operations:
72
+ # example_operation:
73
+ # outputs: [ custom_template, custom_email]
74
+ #
75
+ # and then, at templates definition
76
+ #
77
+ # templates:
78
+ # custom_template:
79
+ # type: template
80
+ # config:
81
+ # path: <your custom template path>
82
+ # log_level: <direct|warning|whatever>
83
+ # custom_email:
84
+ # type: email
85
+ # config:
86
+ # <overwrite here default email config>
87
+ #
88
+ # But it is still on mind :D
89
+ #
90
+ def output(type)
91
+ case type
92
+ when 'template','email','yml'
93
+ send("output_#{ type }")
94
+ else
95
+ # Now, do nothing! :D
96
+ end
97
+ end
98
+
99
+ def to_yaml
100
+ entries_hash_array = []
101
+ @entries.each do |entry|
102
+ entries_hash_array << entry.to_hash
103
+ end
104
+ entries_hash_array.to_yaml
105
+ end
106
+
107
+ def human_sume
108
+ process_resume = "Process Resume:\n---------------\n\n"
109
+ process_resume << " - Times: \n * start: #{ self.gatget.times['init'] }\n * now: #{ Time.now }\n * secs: #{ Time.now - self.gatget.times['init']} \n * min: #{ (Time.now - self.gatget.times['init'])/60 } \n * hours: #{ ( Time.now - self.gatget.times['init'])/3600 } \n\n"
110
+ process_resume << " - Logger:\n * msgs: #{ self.entries.size }\n * warnings: #{ self.entries.select{|e| e.global_type == 'warning' }.size }\n * errors: #{ self.entries.select{|e| e.global_type == 'error' }.size }\n\n"
111
+ process_resume << " - Whole log: \n#{ self.entries.map{ |e| " * #{ e.inspect } \n" } }"
112
+ process_resume
113
+ end
114
+
115
+ private
116
+ # just output message if direct output is enabled by debug or by verbosity
117
+ def direct_output?(log_entry)
118
+ if @gatget.debug
119
+ print_debug_msg(log_entry.inspect)
120
+ else
121
+ $stdout.puts log_entry.output unless self.gatget.verbose < log_entry.verbose_level
122
+ end
123
+ end
124
+
125
+
126
+ # Standar template output
127
+ # See output for more information
128
+ def output_template
129
+ template_file = "#{ @gatget.path }/templates/#{ @gatget.operation.name }.erb"
130
+
131
+ # raise GatgetException if template not found
132
+ unless File.exists?(template_file)
133
+ raise GatgetException.new("Template for operation #{ @gatget.operation.name } not found at #{ template_file }", "run_outputs_template")
134
+ end
135
+
136
+ template_content = File.read(template_file)
137
+
138
+ # bind operation variable to template
139
+ operation = @gatget.operation
140
+ template = ERB.new(template_content, 0, "%<>")
141
+ @gatget.logger.log("direct", "operation_output_template", template.result(binding))
142
+ end
143
+
144
+ # Standar email output
145
+ # See output for more information
146
+ def output_email
147
+ # default email config is used
148
+ unless @gatget.config['email_config']
149
+ raise GatgetConfigException.new("Default email config is empty at @gatget.config['email_config']", "run_output_email")
150
+ end
151
+
152
+ if @gatget.operation.status == 'ok'
153
+ subject_prefix = 'OK'
154
+ elsif @gatget.operation.status == 'failed'
155
+ subject_prefix = 'ERROR'
156
+ else
157
+ subject_prefix = 'WARNING'
158
+ end
159
+
160
+ email_subject = "[GAT] [#{ @gatget.class }] [#{ @gatget.operation.name.camelize }] [#{ subject_prefix }]"
161
+
162
+ email_body = "#{ @gatget.logger.human_sume }"
163
+
164
+ @gatget.create_and_deliver_email(@gatget.config['email_config']['send_to'], { 'subject' => email_subject, 'body' => email_body }, @gatget.config['email_config'])
165
+ end
166
+
167
+ # Standar yml output
168
+ # See output for more information
169
+ def output_yml
170
+ yml_log = File.new("/var/log/gat/#{ self.gatget.name.underscore }_#{ self.gatget.operation.name }_#{ Time.now.strftime("%Y%m%d%H%M%S") }.yml", "w+")
171
+ yml_log.write(self.gatget.logger.to_yaml)
172
+ yml_log.close
173
+ end
174
+
175
+ end
176
+
177
+
178
+ class LogEntry
179
+
180
+ attr_reader :logger
181
+ attr_reader :global_type
182
+ attr_reader :category
183
+ attr_reader :message
184
+ attr_reader :options
185
+ attr_reader :verbose_level
186
+ attr_reader :time
187
+
188
+
189
+ def initialize(logger, global_type = "trace", category = "undefined", message = "Empty", *options)
190
+ @logger = logger
191
+ @time = Time.now
192
+ @global_type = global_type
193
+
194
+ unless logger.global_types.keys.include?(global_type)
195
+ raise GatgetException.new("Undefined log global_type #{ global_type }", "initialize_log_entry")
196
+ end
197
+
198
+ @category = category
199
+ @message = message
200
+ @options = options
201
+ @verbose_level = logger.global_types[global_type]['verbose']
202
+ end
203
+
204
+
205
+ def inspect
206
+ "#{ self.time.strftime("%Y-%m-%d %H:%M:%S") } | #{ self.global_type.ljust(20) } | #{ self.category.ljust(20) } => #{ self.message }"
207
+ end
208
+
209
+ def to_hash
210
+ { 'time' => self.time, 'global_type' => self.global_type, 'category' => self.category, 'message' => self.message, 'options' => self.options }
211
+ end
212
+
213
+ def output
214
+ self.message
215
+ end
216
+
217
+ end
218
+ end
219
+
220
+
221
+ =begin
222
+
223
+ module Logger
224
+
225
+
226
+ # This function must be as robust as possible, so we can not allow any exception here
227
+ def log(global_type = "trace", category = "undefined", message = "Empty", options = {})
228
+
229
+ # log goes always to gat::log
230
+
231
+ #gatget_object = self.class.name == 'Gat::Base' ? self : self.gatget
232
+ gatget_object = self
233
+
234
+ log_entry = Hash.new
235
+ log_entry['type'] = global_type
236
+ log_entry['category'] = category
237
+ log_entry['message'] = message
238
+ log_entry['position'] = @logger.size
239
+ gatget_object.logger << log_entry
240
+
241
+ # skip output on debug mode
242
+ if gatget_object.debug
243
+ log_to_debug(log_entry)
244
+ else
245
+
246
+ #gatget_stout_output = gatget_object.config['gatget_operations'][gatget_object.operation]['output'] || 'enabled'
247
+ #if gatget_stout_output == 'enabled'
248
+ # log_entry_level = get_debug_levels[log_entry['type']] || 5
249
+ # if self.verbose >= log_entry_level
250
+ # $stdout.puts format_output(log_entry)
251
+ # end
252
+ #end
253
+ end
254
+ end
255
+
256
+ def format_output(log_entry, format = nil)
257
+ gatget_format_output = format || self.config['gatget_operations'][self.operation]['output_format'] || 'template'
258
+
259
+ if gatget_format_output == 'template'
260
+
261
+ template_file = self.templates["stdout"] || GAT_ROOT + '/templates/stdout.template'
262
+ template = File.new(template_file, "r")
263
+ template_content = ""
264
+ while (line = template.gets)
265
+ template_content << line
266
+ end
267
+ template.close
268
+ ret = eval(template_content)
269
+ else
270
+ ret = " #{ log_entry['position'].to_s.ljust(3) } | #{ log_entry['type'].ljust(10).upcase } | #{ log_entry['category'].ljust(30).upcase } | #{ log_entry['message'] }\n"
271
+ end
272
+ ret
273
+ end
274
+
275
+
276
+ # debug purpuose method, just output all
277
+ def log_to_debug(log_entry)
278
+ print_debug_msg "#{ log_entry['position'] } ** #{ log_entry['category'] } » #{ log_entry['message'] } «"
279
+ end
280
+
281
+ def get_type_messages(type)
282
+ entries = []
283
+ @logger.map { |log_entry|
284
+ entries << log_entry if log_entry['type'] == type
285
+ }
286
+ entries
287
+ end
288
+
289
+
290
+
291
+ # construct a string with all the logger information
292
+ def build_whole_log(options = {})
293
+ whole_log = ''
294
+
295
+ if options['start_text']
296
+ whole_log << options['start_text']
297
+ end
298
+
299
+ if options['include_resume']
300
+ for entry_type in ['error', 'warning']
301
+ type_entries = get_type_messages(entry_type)
302
+
303
+ if type_entries and type_entries.any?
304
+ whole_log << "\n"
305
+ whole_log << "#{ entry_type }s at gatget (#{ type_entries.size })\n"
306
+ whole_log << "#{ '-' * entry_type.size }-------------#{ '-' * type_entries.size.to_s.size }-\n"
307
+ whole_log << "\n"
308
+
309
+ type_entries.each do |type_entry|
310
+ whole_log << format_output(type_entry, 'plain')
311
+ end
312
+ else
313
+ whole_log << "0 #{ entry_type }s at gatget"
314
+ end
315
+ end
316
+ end
317
+
318
+ # include all log
319
+ whole_log << "\n"
320
+ whole_log << "whole log\n"
321
+ whole_log << "---------\n"
322
+ self.logger.each do |log_entry|
323
+ whole_log << format_output(log_entry, 'plain')
324
+ whole_log << "\n"
325
+ end
326
+
327
+ whole_log
328
+ end
329
+ end
330
+ end
331
+ =end
@@ -0,0 +1,253 @@
1
+ =begin
2
+
3
+ :file => operation.rb
4
+ :author => (c) 2008-2009 A.C. Gnoxys info@gnoxys.net
5
+
6
+ ######################################################################################
7
+ ## This file is part of GAT: http://gat.rubyforge.org ##
8
+ ## ##
9
+ ## GAT (Gnoxys Administration Tools) is released under the GPL License 3 or higher. ##
10
+ ## You can get a copy of the license easily at http://www.gnu.org/licenses/gpl.html.##
11
+ ######################################################################################
12
+
13
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
14
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
17
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
18
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+
21
+ =end
22
+
23
+
24
+ module Gat
25
+
26
+ # Gat::Operation
27
+ #
28
+ # Operations are the pieces of a gatget. Operations means the whole procement when we want to make something
29
+ # Operations have the next config options at YML definition
30
+ #
31
+ # <operation_name> *must be uniq*
32
+ # description: <description text>
33
+ # Description of operation... It will be showed at gatget help
34
+ # users: [ <allowed users to run gat> ]
35
+ # Users that are allowed to run GatGet. If empty or missing, no restrictions are applied
36
+ # programs: [ <array of programs_names> ]
37
+ # Array of programs that are defined in YML config that operation will need
38
+ # arguments:[ <array of arguments_names> ]
39
+ # Array of arguments that are defined in YML config that operation will need
40
+ # folders:[ <array of folders_names> ]
41
+ # Array of folders that are defined in YML config that operation will need
42
+ # helpers:[ <array of helpers_names> ]
43
+ # Ruby methods that will be launched before actions starts
44
+ # actions:
45
+ # Array of actions that operation will launch
46
+ # outpus:
47
+ # Array of outputs that operation will print or make
48
+ # onfail:
49
+ # Ruby method that is launched if Operation fail_do_backup
50
+ #
51
+
52
+
53
+ class Operation
54
+
55
+ attr_reader :gatget
56
+
57
+ attr_reader :config
58
+ attr_reader :name
59
+ attr_reader :description
60
+
61
+ attr_reader :programs
62
+ attr_reader :arguments
63
+ attr_reader :folders
64
+
65
+ attr_reader :helpers
66
+
67
+ attr_reader :outputs
68
+ attr_reader :users
69
+
70
+ attr_accessor :actions
71
+ attr_accessor :status
72
+
73
+ def initialize(operation_name, operation_config, gatget)
74
+ @name = operation_name
75
+ @gatget = gatget
76
+ @description = operation_config['description'] || "#{ operation_name } at GatGet #{ self.class }"
77
+ @config = operation_config
78
+ @helpers = operation_config['helpers'] || []
79
+
80
+ @status = 'ok'
81
+
82
+ if operation_config['users'] and not valid_user(operation_config['users'])
83
+ raise GatgetException.new('Current user is not allowed to run gatget operation', 'valid_user')
84
+ end
85
+
86
+ for dependence in [ 'programs', 'arguments', 'folders' ]
87
+ self.instance_variable_set("@#{ dependence }", evalue_dependences(dependence))
88
+ end
89
+ end
90
+
91
+
92
+ def evalue_dependences(dependence_type)
93
+ # get named dependences
94
+ dependences = self.config[dependence_type] || []
95
+ evalued_dependences = Array.new
96
+ dependences.each do |dependence|
97
+ config = self.gatget.get_element_config(dependence_type, dependence)
98
+ dependence_object = Object.module_eval("Gat::Dependence::#{ dependence_type.gsub(/s$/,'').capitalize }").new(dependence, config, self)
99
+ dependence_object.evalue
100
+ evalued_dependences << dependence_object
101
+ end
102
+
103
+ evalued_dependences
104
+ end
105
+
106
+ # Execute operation.
107
+ # After set all dependences, operation is ready to run. Just do it
108
+ # By steps, operation will run:
109
+ #
110
+ # 1) Helpers
111
+ # 2) Actions
112
+ # 3) Outputs
113
+ #
114
+ # Each step, operation will check that state is ok, if not ok, operation is stopped
115
+ def execute
116
+ run_helpers if continue_operation('helpers')
117
+ ret = run_actions if continue_operation('actions')
118
+
119
+ self.gatget.logger.log("message", "operation", "Operation #{ self.name } Ended!")
120
+
121
+ run_outputs
122
+ ret
123
+ end
124
+
125
+
126
+ def get_action(action_name)
127
+ actions = self.actions.select { |a| a.name == action_name }
128
+ actions.first || raise(GatgetException.new("Action #{ action_name } not found at operation #{ self.name }", "get_actions"))
129
+ end
130
+
131
+ private
132
+ # Run Helpers
133
+ #
134
+ # Helpers are:
135
+ #
136
+ # - Ruby methods that are wrotten at controller gatget file and provides specific features to the gatget
137
+ # - Helpers can not stop or modify gatget operation status, only actions can do it
138
+ #
139
+ def run_helpers
140
+ self.helpers.each do |helper|
141
+ self.gatget.logger.log("trace", "helper", "Running helper #{ self.class } @#{ helper }")
142
+
143
+ self.gatget.send(helper)
144
+ end
145
+ end
146
+
147
+
148
+ def run_actions
149
+ action_result = nil
150
+ actions = self.config['actions'] || []
151
+
152
+ if actions.empty?
153
+ raise GatgetConfigException.new("Actions are empty for operation #{ self.name }", "run_actions")
154
+ end
155
+
156
+ self.actions = Array.new
157
+ actions.each do |action|
158
+ action_config = self.gatget.get_element_config('actions', action) || []
159
+
160
+ if not action_config['type'] or action_config['type'].empty?
161
+ raise GatgetConfigException.new("Action type missing or empty for operation #{ self.name }", "run_actions")
162
+ end
163
+
164
+ action_object = Object.module_eval("Gat::Action::#{ action_config['type'].camelize }").new(action, action_config, self)
165
+
166
+ action_result = action_object.execute
167
+ self.actions << action_object
168
+ end
169
+ action_result
170
+ end
171
+
172
+ def run_outputs
173
+ outputs = self.config['outputs'] || []
174
+
175
+ outputs.each do |output|
176
+ self.gatget.logger.output(output)
177
+ end
178
+ end
179
+
180
+
181
+ def valid_user(users)
182
+ users.empty? or users.include?(%x[ whoami ].strip)
183
+ end
184
+
185
+ def continue_operation(continue_with)
186
+ ret = true
187
+ if self.status == 'failed'
188
+ self.gatget.logger.log("message", "operation", "Operation #{ self.name } will not run #{ continue_with }. Status is #{ self.status }")
189
+ ret = false
190
+ end
191
+ ret
192
+ end
193
+
194
+
195
+ end
196
+
197
+ end
198
+
199
+
200
+
201
+ # gatget_name: Your underscore gatgetname
202
+
203
+ # gatget_description: Your Gatget Description
204
+
205
+ # gatget_config_variables: Variable configurations to be used accross the gatget
206
+
207
+ # gatget_arguments: Gatget needed Arguments, by stdin or pipe
208
+
209
+ # gatget_programs: Array of programs gatget will use
210
+
211
+ # gaget_outputs Outputs an operations will launch
212
+
213
+ # gatget_conditions Conditions to be checked
214
+ # < condition_name >
215
+ # description Condition description
216
+ # type Condition check Type
217
+ # onfail What happens when condition fail
218
+ # onfail_message Optional Message for onfail
219
+
220
+ # gatget_actions
221
+
222
+
223
+ # gatget_operations Operations gatget will suport
224
+ # < operation_name >
225
+ # description Operation description
226
+ # arguments Array of arguments
227
+ # stdout: Array of stdouts
228
+ # instructions Array of instructions to execute
229
+ # onfail On fail Method
230
+
231
+
232
+ =begin
233
+
234
+ Gatget instance methods
235
+ # log Array of logs entries RW
236
+
237
+ # config Gatget Structure config R
238
+ # times Gatget Process Times RW
239
+
240
+ # operation Gatget runned operations R
241
+
242
+ # dependences: All that is needed by the program to run.extrated from config
243
+ # programs: Array with all the programs... take by config['gatget_config']['programs ']
244
+ # folders Array of Hasesh with config['gatget_config'][¡folder_name'] => { 'path' => '<path>', 'tree' => [ <tree folder array> ] }
245
+ # static Hash of static variables to the script
246
+ # arguments Command Line Arguments by get || pipe
247
+ #
248
+
249
+ =end
250
+
251
+
252
+
253
+