uberinstaller 1.0.0

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 (43) hide show
  1. data/.gitignore +20 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +66 -0
  5. data/Rakefile +7 -0
  6. data/TODO +14 -0
  7. data/bin/uberinstaller +6 -0
  8. data/lib/uberinstaller.rb +24 -0
  9. data/lib/uberinstaller/cli.rb +51 -0
  10. data/lib/uberinstaller/commander.rb +112 -0
  11. data/lib/uberinstaller/config.rb +55 -0
  12. data/lib/uberinstaller/exception.rb +40 -0
  13. data/lib/uberinstaller/exceptions/command_not_processable.rb +13 -0
  14. data/lib/uberinstaller/exceptions/invalid_folder.rb +13 -0
  15. data/lib/uberinstaller/exceptions/invalid_json.rb +13 -0
  16. data/lib/uberinstaller/exceptions/invalid_local_package.rb +13 -0
  17. data/lib/uberinstaller/exceptions/invalid_package.rb +13 -0
  18. data/lib/uberinstaller/exceptions/invalid_ppa.rb +13 -0
  19. data/lib/uberinstaller/exceptions/invalid_url.rb +13 -0
  20. data/lib/uberinstaller/exceptions/json_file_not_found.rb +13 -0
  21. data/lib/uberinstaller/exceptions/json_parse_error.rb +13 -0
  22. data/lib/uberinstaller/exceptions/missing_local_package.rb +13 -0
  23. data/lib/uberinstaller/exceptions/missing_url.rb +13 -0
  24. data/lib/uberinstaller/exceptions/multiple_local_files_not_supported.rb +13 -0
  25. data/lib/uberinstaller/exceptions/multiple_repositories_not_supported.rb +13 -0
  26. data/lib/uberinstaller/exceptions/no_preprocessor_exception.rb +13 -0
  27. data/lib/uberinstaller/exceptions/parser_argument_error.rb +13 -0
  28. data/lib/uberinstaller/exceptions/wrong_architecture.rb +13 -0
  29. data/lib/uberinstaller/exceptions/wrong_version.rb +13 -0
  30. data/lib/uberinstaller/installer.rb +284 -0
  31. data/lib/uberinstaller/logger.rb +113 -0
  32. data/lib/uberinstaller/package_manager.rb +27 -0
  33. data/lib/uberinstaller/package_managers/apt.rb +18 -0
  34. data/lib/uberinstaller/package_managers/base.rb +87 -0
  35. data/lib/uberinstaller/package_managers/dpkg.rb +15 -0
  36. data/lib/uberinstaller/package_managers/git.rb +15 -0
  37. data/lib/uberinstaller/parser.rb +51 -0
  38. data/lib/uberinstaller/platform.rb +103 -0
  39. data/lib/uberinstaller/runner.rb +218 -0
  40. data/lib/uberinstaller/utils.rb +13 -0
  41. data/lib/uberinstaller/version.rb +3 -0
  42. data/uberinstaller.gemspec +35 -0
  43. metadata +243 -0
@@ -0,0 +1,113 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'colored'
4
+ require 'logger'
5
+
6
+ module Uberinstaller
7
+
8
+ # Handle application log
9
+ module Loggable
10
+ # @!attribute [r] loggers
11
+ # Hash of available loggers ( one for class in which logger is invoked )
12
+ @loggers = {}
13
+ # @!attribute [rw] log_path
14
+ # Path in which log files are saved ( default STDOUT )
15
+ @log_path = STDOUT
16
+ # @!attribute [rw] level
17
+ # Log level. Can be one of Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR ( default Logger::ERROR )
18
+ @level = Logger::ERROR
19
+
20
+ # Global, memoized, lazy initialized instance of a logger
21
+ #
22
+ # This is the magical bit that gets mixed into your classes. Respond to Logger function.
23
+ #
24
+ # @return [Object] an instance of the logger class
25
+ def logger
26
+ classname = (self.is_a? Module) ? self : self.class.name
27
+ @logger ||= Loggable.logger_for(classname)
28
+ end
29
+
30
+ class << self
31
+ # @!attribute [rw] level
32
+ # Log output level. Can be one of Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR ( default Logger::ERROR )
33
+ # @!attribute [rw] log_path
34
+ # Path in which log files are saved ( default STDOUT )
35
+ attr_accessor :level, :log_path
36
+
37
+ # Return the logger for a specific Class. If the instance is not found, creates it.
38
+ #
39
+ # @param classname [String] the name of the class for which a logger instance must be retrieved
40
+ # @return [Object] the instance of the logger class for the specified Class
41
+ def logger_for(classname)
42
+ @loggers[classname] ||= configure_logger_for(classname)
43
+ end
44
+
45
+ # Create and configure a logger for the specified Class
46
+ #
47
+ # @param classname [String] the name of the class for which a logger instance must be retrieved
48
+ # @return [Object] the instance of the logger class for the specified Class
49
+ def configure_logger_for(classname)
50
+ # handle case in which log path does not exists
51
+ begin
52
+ logger = Logger.new(@log_path)
53
+ rescue Errno::ENOENT
54
+ FileUtils.mkdir_p File.dirname @log_path
55
+ retry
56
+ end
57
+
58
+ logger.progname = classname
59
+ logger.level = @level
60
+ logger.formatter = proc { |severity, datetime, progname, msg|
61
+ case severity
62
+ when 'DEBUG'
63
+ spaciator = " *"
64
+ after_space = ""
65
+ colored = "white"
66
+ extra = ""
67
+ when 'INFO'
68
+ spaciator = " **"
69
+ after_space = " "
70
+ colored = ""
71
+ extra = ""
72
+ when 'WARN'
73
+ spaciator = " ***"
74
+ after_space = " "
75
+ colored = "yellow"
76
+ extra = ""
77
+ when 'ERROR'
78
+ spaciator = " ****"
79
+ after_space = ""
80
+ colored = "red"
81
+ extra = ""
82
+ when 'FATAL'
83
+ spaciator = "*****"
84
+ after_space = ""
85
+ colored = "red"
86
+ extra = "bold"
87
+ else
88
+ spaciator = " "
89
+ after_space = ""
90
+ colored = ""
91
+ extra = ""
92
+ end
93
+
94
+ formatted_output = " #{spaciator} [#{severity}]#{after_space} [#{datetime}] -- #{msg} { #{progname} }\n"
95
+ if @log_path == STDOUT or @log_path == STDERR
96
+ if colored.empty?
97
+ formatted_output
98
+ else
99
+ if extra.empty?
100
+ formatted_output.send(colored)
101
+ else
102
+ formatted_output.send(colored).send(extra)
103
+ end
104
+ end
105
+ else
106
+ formatted_output
107
+ end
108
+ }
109
+ logger
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'uberinstaller/config'
4
+ require 'uberinstaller/logger'
5
+
6
+ module Uberinstaller
7
+ module PackageManager
8
+
9
+ # Create a new PackageManager instance based on the type
10
+ #
11
+ # @param type [String] the type of package manager to create instance for
12
+ def self.new(type)
13
+ case type
14
+ when 'git' then package_manager = 'Git'
15
+ when 'local' then package_manager = Uberinstaller::Config.local_package_manager
16
+ when 'remote' then package_manager = Uberinstaller::Config.remote_package_manager
17
+ end
18
+
19
+ ("Uberinstaller::PackageManager::" + package_manager).split('::').inject(Object) {|scope,name| scope.const_get(name)}.new
20
+ end
21
+ end
22
+ end
23
+
24
+ require 'uberinstaller/package_managers/base'
25
+ require 'uberinstaller/package_managers/apt'
26
+ require 'uberinstaller/package_managers/dpkg'
27
+ require 'uberinstaller/package_managers/git'
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'uberinstaller/package_manager'
4
+
5
+ module Uberinstaller
6
+ module PackageManager
7
+ # Apt-Get Package manager
8
+ class Apt < Base
9
+
10
+ def set_commands
11
+ @commands[:add_repository] = "apt-add-repository -y"
12
+ @commands[:install] = "DEBIAN_FRONTEND=gnome apt-get install -y"
13
+ @commands[:update] = "apt-get update"
14
+ @commands[:upgrade] = "apt-get upgrade"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,87 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'uberinstaller/config'
4
+ require 'uberinstaller/logger'
5
+
6
+ require 'open3'
7
+
8
+ module Uberinstaller
9
+ module PackageManager
10
+ class Base
11
+ include Loggable
12
+
13
+ attr_reader :commands
14
+
15
+ # Create the package manager
16
+ def initialize
17
+ @commands = Hash.new
18
+ @commands = {
19
+ :add_repository => nil,
20
+ :info => nil,
21
+ :install => nil,
22
+ :search => nil,
23
+ :update => nil,
24
+ :upgrade => nil
25
+ }
26
+
27
+ set_commands
28
+ end
29
+
30
+ # This method is a stub, here only for reference
31
+ #
32
+ # In every subclass of PackageManager::Base this method must be redefined
33
+ # specifying the package manager specific command ( see Apt and Dpkg for
34
+ # example )
35
+ def set_commands; end
36
+
37
+ # Print to log for debug purposes
38
+ #
39
+ # @param action [String] the action that is being performed
40
+ # @param args [Array] an array of arguments for the specified action
41
+ def debug(action, args = [])
42
+ logger.debug "action : #{action}"
43
+ logger.debug "args : #{args.join(', ')}" unless args.empty?
44
+ logger.debug "command: #{make_command(action, args)}"
45
+ end
46
+
47
+ # Creates a command putting together action and arguments
48
+ #
49
+ # @param action [String] the action that is being performed
50
+ # @param args [Array] an array of arguments for the specified action
51
+ def make_command(action, args = [])
52
+ command = @commands[action.to_sym]
53
+ command += " '" + args.join(' ') + "'" unless args.empty?
54
+
55
+ command
56
+ end
57
+
58
+ # All execution is handled dinamically via this function
59
+ #
60
+ # If the method called is in the @commands Hash, the specified action is
61
+ # performed, otherwise a NoMethodError exception
62
+ #
63
+ # @raise [NoMethodError] if the method specified is not available
64
+ def method_missing(m, *args, &block)
65
+ if @commands.has_key? m
66
+ debug m, args
67
+
68
+ unless Config.dry_run
69
+ Open3.popen3(make_command(m, args)) { |stdin, stdout, stderr, wait_thr|
70
+ pid = wait_thr.pid # pid of the started process.
71
+ logger.debug "Running pid: #{pid}"
72
+
73
+ logger.debug stdout.readlines
74
+
75
+ exit_status = wait_thr.value.to_i # Process::Status object returned.
76
+ logger.debug "Exit status: #{exit_status}"
77
+ logger.error 'Some error happended during execution:' unless exit_status == 0
78
+ logger.error stderr.readlines unless exit_status == 0
79
+ }
80
+ end
81
+ else
82
+ raise NoMethodError
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'uberinstaller/package_manager'
4
+
5
+ module Uberinstaller
6
+ module PackageManager
7
+ # Dpkg package manager
8
+ class Dpkg < Base
9
+
10
+ def set_commands
11
+ @commands[:install] = "dpkg -i"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'uberinstaller/package_manager'
4
+
5
+ module Uberinstaller
6
+ module PackageManager
7
+ # Git package manager ( a little bit of a hack really )
8
+ class Git < Base
9
+
10
+ def set_commands
11
+ @commands[:install] = "git clone"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'uberinstaller/logger'
4
+ require 'uberinstaller/exception'
5
+
6
+ require 'json'
7
+
8
+ module Uberinstaller
9
+ class Parser
10
+ include Loggable
11
+
12
+ # @!attribute [rw] file
13
+ # the file to be parsed
14
+ # @!attribute [r] data
15
+ # an Hash containing data after parsing
16
+ attr_accessor :file
17
+ attr_reader :data
18
+
19
+ # Create the parser
20
+ def initialize(file, perform_parse = true)
21
+ if File.exists?(file)
22
+ @file = file
23
+ else
24
+ raise Uberinstaller::Exception::ParserArgumentError, file
25
+ end
26
+
27
+ @json = nil
28
+ @data = nil
29
+
30
+ run if perform_parse
31
+ self
32
+ end
33
+
34
+ def debug
35
+ @json
36
+ end
37
+
38
+ def run
39
+ begin
40
+ @json = IO.read(@file)
41
+ # Comments are stripped out! FUCK YEAH!
42
+ @data = JSON.parse @json, :symbolize_names => true
43
+ rescue JSON::ParserError
44
+ raise Uberinstaller::Exception::JsonParseError, @file
45
+ else
46
+ @data
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,103 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'uberinstaller/logger'
4
+
5
+ require 'hash_keyword_args'
6
+
7
+ module Uberinstaller
8
+ # http://stackoverflow.com/questions/170956/how-can-i-find-which-operating-system-my-ruby-program-is-running-on
9
+ class Platform
10
+ include Loggable
11
+
12
+ # @!attribute [r] architecture
13
+ # OS architecture information
14
+ # @!attribute [r] lsb
15
+ # LSB module information
16
+ # @!attribute [r] uname
17
+ # `uname` calls results
18
+ attr_reader :architecture, :lsb, :uname
19
+
20
+ # Get platform, detect ubuntu, detect ubuntu version, save lsb params
21
+ #
22
+ # @param opts [Hash]
23
+ # :lsb => the file containing LSB information
24
+ def initialize(opts = {})
25
+ @opts = opts.keyword_args(:lsb => '/etc/lsb-release')
26
+
27
+ @lsb = nil
28
+ @uname = nil
29
+
30
+ get_lsb_informations
31
+ get_arch_informations
32
+
33
+ @architecture = @uname[:machine]
34
+ end
35
+
36
+ # Check if platform is Ubuntu
37
+ def is_ubuntu?
38
+ return @lsb[:id] == 'Ubuntu' if @lsb[:id]
39
+ logger.fatal 'lsb is not set, impossible to get OS information'
40
+ false
41
+ end
42
+
43
+ # Reverse of is_ubuntu?
44
+ def is_not_ubuntu?
45
+ !is_ubuntu?
46
+ end
47
+
48
+ # Reverse of is_64bit?
49
+ def is_32bit?
50
+ !is_64bit?
51
+ end
52
+
53
+ # Check if system is running 64 bit OS
54
+ def is_64bit?
55
+ return @uname[:machine] == 'x86_64' if @uname[:machine]
56
+ logger.fatal 'uname is not set, impossible to get machine information'
57
+ false
58
+ end
59
+
60
+ private
61
+ # Detect OS architecture information
62
+ #
63
+ # Using a call to `uname` try to detect architecture informations.
64
+ # `uname` must be available on the system
65
+ def get_arch_informations
66
+ @uname ||= Hash.new
67
+ IO.popen 'uname -m' do |io| @uname[:machine] = io.read.strip end
68
+ IO.popen 'uname -n' do |io| @uname[:host] = io.read.strip end
69
+ IO.popen 'uname -srv' do |io| @uname[:kernel] = io.read.strip end
70
+ end
71
+
72
+ # Get OS information from LSB
73
+ #
74
+ # LSB must be aavailable on the system
75
+ def get_lsb_informations
76
+ # http://stackoverflow.com/a/1236075/715002
77
+ IO.popen "cat #{@opts.lsb}" do |io|
78
+ io.each do |line|
79
+ unless line.include? 'cat:' # check for error
80
+ @lsb ||= Hash.new
81
+
82
+ if line.include? 'DISTRIB_ID'
83
+ @lsb[:id] = get_lsb_value line
84
+ elsif line.include? 'DISTRIB_RELEASE'
85
+ @lsb[:release] = get_lsb_value line
86
+ elsif line.include? 'DISTRIB_CODENAME'
87
+ @lsb[:codename] = get_lsb_value line
88
+ elsif line.include? 'DISTRIB_DESCRIPTION'
89
+ @lsb[:description] = get_lsb_value line
90
+ end
91
+ else
92
+ logger.fatal "Platform has no #{@opts.lsb}, so it is not supported"
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ # Handy method to retrieve values from LSB pairs
99
+ def get_lsb_value(string)
100
+ string.split('=')[1].strip
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,218 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'uberinstaller/logger'
4
+
5
+ module Uberinstaller
6
+ class Runner
7
+ include Loggable
8
+
9
+ # @!attribute [r] packages
10
+ # the list of packages after configuration file is parsed
11
+ # @!attribute [r] parser
12
+ # the parser class used to parse configuration file
13
+ # @!attribute [r] platform
14
+ # the platform on which UberInstaller is running
15
+ # @!attribute [r] unprocessed
16
+ # check if the execution has already been done
17
+ attr_reader :packages, :parser, :platform, :unprocessed
18
+
19
+ # Initialize the Runner class
20
+ #
21
+ # @param file [String] the file name to be used for this execution
22
+ def initialize(file)
23
+ logger.info "Processing JSON file: #{file}"
24
+
25
+ # check if element has already been processed
26
+ @unprocessed = true
27
+
28
+ @parser = Parser.new file
29
+
30
+ @platform = Platform.new
31
+
32
+ logger.warn "Platform is not Ubuntu, please report any inconvenient behaviour" unless platform.is_ubuntu?
33
+
34
+ verify_architecture
35
+ verify_os_version
36
+
37
+ # This dummy commander is used to launch before all and after all scripts
38
+ @global_commander = Commander.new("Dummy package", { :cmd => { :after => "all.sh", :before => "all.sh" }})
39
+
40
+ @packages = parser.data[:packages]
41
+
42
+ get_nested_json
43
+ end
44
+
45
+ def install
46
+ logger.info 'Installing packages...'
47
+
48
+ @packages.each do |p|
49
+ pkg_name = p[0].to_s
50
+ pkg = p[1]
51
+
52
+ installer = Installer.new(pkg_name, pkg)
53
+ commander = Commander.new(pkg_name, pkg)
54
+
55
+ logger.info "Installing #{pkg_name}"
56
+
57
+ commander.before
58
+
59
+ case pkg[:type]
60
+ when 'system'
61
+ begin
62
+ installer.install 'system'
63
+ rescue Exception => e
64
+ logger.error e.message
65
+
66
+ pkg[:errors] = Array.new # add array to store errors
67
+ pkg[:errors] << e.message
68
+ end
69
+ when 'git'
70
+ begin
71
+ installer.install 'git'
72
+ rescue Exception => e
73
+ logger.error e.message
74
+
75
+ pkg[:errors] = Array.new # add array to store errors
76
+ pkg[:errors] << e.message
77
+ end
78
+ when 'local'
79
+ begin
80
+ installer.install 'local'
81
+ rescue Exception::MultipleLocalFilesNotSupported => e
82
+ logger.error e.message
83
+
84
+ pkg[:errors] = Array.new # add array to store errors
85
+ pkg[:errors] << e.message
86
+ end
87
+ else
88
+ logger.error "#{pkg_name} :type is not supported"
89
+ end
90
+
91
+ commander.after
92
+ end
93
+
94
+ logger.info 'Executing after all commands...'
95
+ @global_commander.after
96
+ end
97
+
98
+ # Preprocess all packages performing validation
99
+ def preprocess
100
+ logger.info 'Executing before all commands...'
101
+ @global_commander.before
102
+
103
+ logger.info 'Preprocessing packages...'
104
+ @packages.each do |p|
105
+ pkg_name = p[0].to_s
106
+ pkg = p[1]
107
+
108
+ logger.info "Package: #{pkg_name}"
109
+ logger.debug "Package content: #{pkg}"
110
+
111
+ # set pkg installation type based on existing key in the package definition
112
+ pkg[:type] = get_package_type pkg
113
+
114
+ installer = Installer.new(pkg_name, pkg)
115
+
116
+ case pkg[:type]
117
+ when 'system'
118
+ begin
119
+ installer.preprocess 'system'
120
+ rescue Exception::InvalidPackage, Exception::InvalidPpa => e
121
+ logger.error e.message
122
+
123
+ pkg[:skip] = true
124
+ pkg[:errors] = Array.new # add array to store errors
125
+ pkg[:errors] << e.message
126
+ end
127
+ when 'git'
128
+ begin
129
+ installer.preprocess 'git'
130
+ rescue Exception::InvalidFolder, Exception::MissingUrl, Exception::InvalidUrl => e
131
+ logger.error e.message
132
+
133
+ pkg[:skip] = true
134
+ pkg[:errors] = Array.new # add array to store errors
135
+ pkg[:errors] << e.message
136
+ end
137
+ when 'local'
138
+ begin
139
+ installer.preprocess 'local'
140
+ rescue Exception::MissingLocalPackage, Exception::InvalidLocalPackage => e
141
+ logger.error e.message
142
+
143
+ pkg[:skip] = true
144
+ pkg[:errors] = Array.new # add array to store errors
145
+ pkg[:errors] << e.message
146
+ end
147
+ else
148
+ logger.error "#{pkg_name} :type is not supported"
149
+ end
150
+ end
151
+
152
+ PackageManager.new('remote').update
153
+ end
154
+
155
+ # Verify that platform architecture match the one specified in the config file
156
+ #
157
+ # @raise [Exception::WrongArchitecture] if the architecture do not match configuration file
158
+ def verify_architecture
159
+ if parser.data[:meta][:arch]
160
+ unless parser.data[:meta][:arch] == 'system'
161
+ logger.debug 'Verifying architecture...'
162
+
163
+ unless parser.data[:meta][:arch] == platform.architecture
164
+ raise Exception::WrongArchitecture, parser.data[:meta][:arch]
165
+ else
166
+ logger.info "Architecture match installation file requirements"
167
+ end
168
+ end
169
+ else
170
+ logger.warn "Installation file does not specify a required architecture"
171
+ end
172
+ end
173
+
174
+ # Verify that the OS version match the one specified in the config file
175
+ #
176
+ # @raise [Exception::WrongVersion] if the version do not match
177
+ def verify_os_version
178
+ raise Exception::WrongVersion, parser.data[:meta][:version] unless parser.data[:meta][:version] == platform.lsb[:codename]
179
+ end
180
+
181
+ private
182
+ def get_nested_json
183
+ nested_packages = Hash.new
184
+
185
+ @packages.each do |p|
186
+ pkg_name = p[0].to_s
187
+ pkg = p[1]
188
+
189
+ if pkg.has_key? :json
190
+ installer = Installer.new(pkg_name, pkg)
191
+
192
+ begin
193
+ installer.preprocess 'json'
194
+ rescue Exception::JsonFileNotFound, Exception::InvalidJson => e
195
+ logger.error e.message
196
+
197
+ pkg[:skip] = true
198
+ pkg[:errors] = Array.new # add array to store errors
199
+ pkg[:errors] << e.message
200
+ else
201
+ file = File.join Config.json_path, pkg[:json] + '.json'
202
+ parser = Parser.new(file)
203
+ data = parser.data[:packages].each { |p| p[1][:type] = get_package_type p[1] }
204
+ nested_packages.merge! data
205
+ end
206
+ end
207
+ end
208
+
209
+ @packages.merge! nested_packages
210
+ end
211
+
212
+ def get_package_type(pkg)
213
+ return 'system' if pkg.has_key? :system
214
+ return 'git' if pkg.has_key? :git
215
+ return 'local' if pkg.has_key? :local
216
+ end
217
+ end
218
+ end