backup-remote 0.0.2

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 (135) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +112 -0
  3. data/bin/backup-remote +5 -0
  4. data/lib/backup.rb +155 -0
  5. data/lib/backup/archive.rb +170 -0
  6. data/lib/backup/binder.rb +22 -0
  7. data/lib/backup/cleaner.rb +116 -0
  8. data/lib/backup/cli.rb +374 -0
  9. data/lib/backup/cloud_io/base.rb +41 -0
  10. data/lib/backup/cloud_io/cloud_files.rb +298 -0
  11. data/lib/backup/cloud_io/s3.rb +260 -0
  12. data/lib/backup/compressor/base.rb +35 -0
  13. data/lib/backup/compressor/bzip2.rb +39 -0
  14. data/lib/backup/compressor/custom.rb +53 -0
  15. data/lib/backup/compressor/gzip.rb +74 -0
  16. data/lib/backup/config.rb +121 -0
  17. data/lib/backup/config/dsl.rb +106 -0
  18. data/lib/backup/config/helpers.rb +143 -0
  19. data/lib/backup/database/base.rb +85 -0
  20. data/lib/backup/database/mongodb.rb +187 -0
  21. data/lib/backup/database/mysql.rb +192 -0
  22. data/lib/backup/database/openldap.rb +95 -0
  23. data/lib/backup/database/postgresql.rb +133 -0
  24. data/lib/backup/database/redis.rb +179 -0
  25. data/lib/backup/database/remote_mysql.rb +248 -0
  26. data/lib/backup/database/riak.rb +82 -0
  27. data/lib/backup/database/sqlite.rb +57 -0
  28. data/lib/backup/encryptor/base.rb +29 -0
  29. data/lib/backup/encryptor/gpg.rb +747 -0
  30. data/lib/backup/encryptor/open_ssl.rb +77 -0
  31. data/lib/backup/errors.rb +58 -0
  32. data/lib/backup/logger.rb +199 -0
  33. data/lib/backup/logger/console.rb +51 -0
  34. data/lib/backup/logger/fog_adapter.rb +29 -0
  35. data/lib/backup/logger/logfile.rb +133 -0
  36. data/lib/backup/logger/syslog.rb +116 -0
  37. data/lib/backup/model.rb +479 -0
  38. data/lib/backup/notifier/base.rb +128 -0
  39. data/lib/backup/notifier/campfire.rb +63 -0
  40. data/lib/backup/notifier/command.rb +102 -0
  41. data/lib/backup/notifier/datadog.rb +107 -0
  42. data/lib/backup/notifier/flowdock.rb +103 -0
  43. data/lib/backup/notifier/hipchat.rb +118 -0
  44. data/lib/backup/notifier/http_post.rb +117 -0
  45. data/lib/backup/notifier/mail.rb +249 -0
  46. data/lib/backup/notifier/nagios.rb +69 -0
  47. data/lib/backup/notifier/pagerduty.rb +81 -0
  48. data/lib/backup/notifier/prowl.rb +68 -0
  49. data/lib/backup/notifier/pushover.rb +74 -0
  50. data/lib/backup/notifier/ses.rb +105 -0
  51. data/lib/backup/notifier/slack.rb +148 -0
  52. data/lib/backup/notifier/twitter.rb +58 -0
  53. data/lib/backup/notifier/zabbix.rb +63 -0
  54. data/lib/backup/package.rb +55 -0
  55. data/lib/backup/packager.rb +107 -0
  56. data/lib/backup/pipeline.rb +128 -0
  57. data/lib/backup/remote/command.rb +82 -0
  58. data/lib/backup/splitter.rb +76 -0
  59. data/lib/backup/storage/base.rb +69 -0
  60. data/lib/backup/storage/cloud_files.rb +158 -0
  61. data/lib/backup/storage/cycler.rb +75 -0
  62. data/lib/backup/storage/dropbox.rb +212 -0
  63. data/lib/backup/storage/ftp.rb +112 -0
  64. data/lib/backup/storage/local.rb +64 -0
  65. data/lib/backup/storage/qiniu.rb +65 -0
  66. data/lib/backup/storage/rsync.rb +248 -0
  67. data/lib/backup/storage/s3.rb +156 -0
  68. data/lib/backup/storage/scp.rb +67 -0
  69. data/lib/backup/storage/sftp.rb +82 -0
  70. data/lib/backup/syncer/base.rb +70 -0
  71. data/lib/backup/syncer/cloud/base.rb +179 -0
  72. data/lib/backup/syncer/cloud/cloud_files.rb +83 -0
  73. data/lib/backup/syncer/cloud/local_file.rb +100 -0
  74. data/lib/backup/syncer/cloud/s3.rb +110 -0
  75. data/lib/backup/syncer/rsync/base.rb +54 -0
  76. data/lib/backup/syncer/rsync/local.rb +31 -0
  77. data/lib/backup/syncer/rsync/pull.rb +51 -0
  78. data/lib/backup/syncer/rsync/push.rb +205 -0
  79. data/lib/backup/template.rb +46 -0
  80. data/lib/backup/utilities.rb +224 -0
  81. data/lib/backup/version.rb +5 -0
  82. data/templates/cli/archive +28 -0
  83. data/templates/cli/compressor/bzip2 +4 -0
  84. data/templates/cli/compressor/custom +7 -0
  85. data/templates/cli/compressor/gzip +4 -0
  86. data/templates/cli/config +123 -0
  87. data/templates/cli/databases/mongodb +15 -0
  88. data/templates/cli/databases/mysql +18 -0
  89. data/templates/cli/databases/openldap +24 -0
  90. data/templates/cli/databases/postgresql +16 -0
  91. data/templates/cli/databases/redis +16 -0
  92. data/templates/cli/databases/riak +17 -0
  93. data/templates/cli/databases/sqlite +11 -0
  94. data/templates/cli/encryptor/gpg +27 -0
  95. data/templates/cli/encryptor/openssl +9 -0
  96. data/templates/cli/model +26 -0
  97. data/templates/cli/notifier/zabbix +15 -0
  98. data/templates/cli/notifiers/campfire +12 -0
  99. data/templates/cli/notifiers/command +32 -0
  100. data/templates/cli/notifiers/datadog +57 -0
  101. data/templates/cli/notifiers/flowdock +16 -0
  102. data/templates/cli/notifiers/hipchat +16 -0
  103. data/templates/cli/notifiers/http_post +32 -0
  104. data/templates/cli/notifiers/mail +24 -0
  105. data/templates/cli/notifiers/nagios +13 -0
  106. data/templates/cli/notifiers/pagerduty +12 -0
  107. data/templates/cli/notifiers/prowl +11 -0
  108. data/templates/cli/notifiers/pushover +11 -0
  109. data/templates/cli/notifiers/ses +15 -0
  110. data/templates/cli/notifiers/slack +22 -0
  111. data/templates/cli/notifiers/twitter +13 -0
  112. data/templates/cli/splitter +7 -0
  113. data/templates/cli/storages/cloud_files +11 -0
  114. data/templates/cli/storages/dropbox +20 -0
  115. data/templates/cli/storages/ftp +13 -0
  116. data/templates/cli/storages/local +8 -0
  117. data/templates/cli/storages/qiniu +12 -0
  118. data/templates/cli/storages/rsync +17 -0
  119. data/templates/cli/storages/s3 +16 -0
  120. data/templates/cli/storages/scp +15 -0
  121. data/templates/cli/storages/sftp +15 -0
  122. data/templates/cli/syncers/cloud_files +22 -0
  123. data/templates/cli/syncers/rsync_local +20 -0
  124. data/templates/cli/syncers/rsync_pull +28 -0
  125. data/templates/cli/syncers/rsync_push +28 -0
  126. data/templates/cli/syncers/s3 +27 -0
  127. data/templates/general/links +3 -0
  128. data/templates/general/version.erb +2 -0
  129. data/templates/notifier/mail/failure.erb +16 -0
  130. data/templates/notifier/mail/success.erb +16 -0
  131. data/templates/notifier/mail/warning.erb +16 -0
  132. data/templates/storage/dropbox/authorization_url.erb +6 -0
  133. data/templates/storage/dropbox/authorized.erb +4 -0
  134. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  135. metadata +1122 -0
@@ -0,0 +1,58 @@
1
+ # encoding: utf-8
2
+ require 'twitter'
3
+
4
+ module Backup
5
+ module Notifier
6
+ class Twitter < Base
7
+
8
+ ##
9
+ # Twitter consumer key credentials
10
+ attr_accessor :consumer_key, :consumer_secret
11
+
12
+ ##
13
+ # OAuth credentials
14
+ attr_accessor :oauth_token, :oauth_token_secret
15
+
16
+ def initialize(model, &block)
17
+ super
18
+ instance_eval(&block) if block_given?
19
+ end
20
+
21
+ private
22
+
23
+ ##
24
+ # Notify the user of the backup operation results.
25
+ #
26
+ # `status` indicates one of the following:
27
+ #
28
+ # `:success`
29
+ # : The backup completed successfully.
30
+ # : Notification will be sent if `on_success` is `true`.
31
+ #
32
+ # `:warning`
33
+ # : The backup completed successfully, but warnings were logged.
34
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
35
+ #
36
+ # `:failure`
37
+ # : The backup operation failed.
38
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
39
+ #
40
+ def notify!(status)
41
+ send_message(message.call(model, :status => status_data_for(status)))
42
+ end
43
+
44
+ # Twitter::Client will raise an error if unsuccessful.
45
+ def send_message(message)
46
+ client = ::Twitter::REST::Client.new do |config|
47
+ config.consumer_key = @consumer_key
48
+ config.consumer_secret = @consumer_secret
49
+ config.access_token = @oauth_token
50
+ config.access_token_secret = @oauth_token_secret
51
+ end
52
+
53
+ client.update(message)
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: utf-8
2
+
3
+ module Backup
4
+ module Notifier
5
+ class Zabbix < Base
6
+
7
+ attr_accessor :zabbix_host
8
+
9
+ attr_accessor :zabbix_port
10
+
11
+ attr_accessor :service_name
12
+
13
+ attr_accessor :service_host
14
+
15
+ attr_accessor :item_key
16
+
17
+ def initialize(model, &block)
18
+ super
19
+ instance_eval(&block) if block_given?
20
+
21
+ @zabbix_host ||= Config.hostname
22
+ @zabbix_port ||= 10051
23
+ @service_name ||= "Backup #{ model.trigger }"
24
+ @service_host ||= Config.hostname
25
+ @item_key ||= 'backup_status'
26
+ end
27
+
28
+ private
29
+
30
+ ##
31
+ # Notify the user of the backup operation results.
32
+ #
33
+ # `status` indicates one of the following:
34
+ #
35
+ # `:success`
36
+ # : The backup completed successfully.
37
+ # : Notification will be sent if `on_success` is `true`.
38
+ #
39
+ # `:warning`
40
+ # : The backup completed successfully, but warnings were logged.
41
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
42
+ #
43
+ # `:failure`
44
+ # : The backup operation failed.
45
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
46
+ #
47
+ def notify!(status)
48
+ send_message(message.call(model, :status => status_data_for(status)))
49
+ end
50
+
51
+ def send_message(message)
52
+ msg = [service_host, service_name, model.exit_status, message].join("\t")
53
+ cmd = "#{ utility(:zabbix_sender) }" +
54
+ " -z '#{ zabbix_host }'" +
55
+ " -p '#{ zabbix_port }'" +
56
+ " -s #{ service_host }" +
57
+ " -k #{ item_key }" +
58
+ " -o '#{ msg }'"
59
+ run("echo '#{ msg }' | #{ cmd }")
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ module Backup
4
+ class Package
5
+
6
+ ##
7
+ # The time when the backup initiated (in format: 2011.02.20.03.29.59)
8
+ attr_accessor :time
9
+
10
+ ##
11
+ # The trigger which initiated the backup process
12
+ attr_reader :trigger
13
+
14
+ ##
15
+ # Extension for the final archive file(s)
16
+ attr_accessor :extension
17
+
18
+ ##
19
+ # Set by the Splitter if the final archive was "chunked"
20
+ attr_accessor :chunk_suffixes
21
+
22
+ ##
23
+ # If true, the Cycler will not attempt to remove the package when Cycling.
24
+ attr_accessor :no_cycle
25
+
26
+ ##
27
+ # The version of Backup used to create the package
28
+ attr_reader :version
29
+
30
+ def initialize(model)
31
+ @trigger = model.trigger
32
+ @extension = 'tar'
33
+ @chunk_suffixes = Array.new
34
+ @no_cycle = false
35
+ @version = VERSION
36
+ end
37
+
38
+ def filenames
39
+ if chunk_suffixes.empty?
40
+ [basename]
41
+ else
42
+ chunk_suffixes.map {|suffix| "#{ basename }-#{ suffix }" }
43
+ end
44
+ end
45
+
46
+ def basename
47
+ "#{ trigger }.#{ extension }"
48
+ end
49
+
50
+ def time_as_object
51
+ Time.strptime(time, '%Y.%m.%d.%H.%M.%S')
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,107 @@
1
+ # encoding: utf-8
2
+
3
+ module Backup
4
+ module Packager
5
+ class Error < Backup::Error; end
6
+
7
+ class << self
8
+ include Utilities::Helpers
9
+
10
+ ##
11
+ # Build the final package for the backup model.
12
+ def package!(model)
13
+ @package = model.package
14
+ @encryptor = model.encryptor
15
+ @splitter = model.splitter
16
+ @pipeline = Pipeline.new
17
+
18
+ Logger.info "Packaging the backup files..."
19
+ procedure.call
20
+
21
+ if @pipeline.success?
22
+ Logger.info "Packaging Complete!"
23
+ else
24
+ raise Error, "Failed to Create Backup Package\n" +
25
+ @pipeline.error_messages
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ ##
32
+ # Builds a chain of nested Procs which adds each command to a Pipeline
33
+ # needed to package the final command to package the backup.
34
+ # This is done so that the Encryptor and Splitter have the ability
35
+ # to perform actions before and after the final command is executed.
36
+ # No Encryptors currently utilize this, however the Splitter does.
37
+ def procedure
38
+ stack = []
39
+
40
+ ##
41
+ # Initial `tar` command to package the temporary backup folder.
42
+ # The command's output will then be either piped to the Encryptor
43
+ # or the Splitter (if no Encryptor), or through `cat` into the final
44
+ # output file if neither are configured.
45
+ @pipeline.add(
46
+ "#{ utility(:tar) } -cf - " +
47
+ "-C '#{ Config.tmp_path }' '#{ @package.trigger }'",
48
+ tar_success_codes
49
+ )
50
+
51
+ ##
52
+ # If an Encryptor was configured, it will be called first
53
+ # to add the encryption utility command to be piped through,
54
+ # and amend the final package extension.
55
+ # It's output will then be either piped into a Splitter,
56
+ # or through `cat` into the final output file.
57
+ if @encryptor
58
+ stack << lambda do
59
+ @encryptor.encrypt_with do |command, ext|
60
+ @pipeline << command
61
+ @package.extension << ext
62
+ stack.shift.call
63
+ end
64
+ end
65
+ end
66
+
67
+ ##
68
+ # If a Splitter was configured, the `split` utility command will be
69
+ # added to the Pipeline to split the final output into multiple files.
70
+ # Once the Proc executing the Pipeline has completed and returns back
71
+ # to the Splitter, it will check the final output files to determine
72
+ # if the backup was indeed split.
73
+ # If so, it will set the package's chunk_suffixes. If not, it will
74
+ # remove the '-aa' suffix from the only file created by `split`.
75
+ #
76
+ # If no Splitter was configured, the final file output will be
77
+ # piped through `cat` into the final output file.
78
+ if @splitter
79
+ stack << lambda do
80
+ @splitter.split_with do |command|
81
+ @pipeline << command
82
+ stack.shift.call
83
+ end
84
+ end
85
+ else
86
+ stack << lambda do
87
+ outfile = File.join(Config.tmp_path, @package.basename)
88
+ @pipeline << "#{ utility(:cat) } > #{ outfile }"
89
+ stack.shift.call
90
+ end
91
+ end
92
+
93
+ ##
94
+ # Last Proc to be called runs the Pipeline the procedure built.
95
+ # Once complete, the call stack will unwind back through the
96
+ # preceeding Procs in the stack (if any)
97
+ stack << lambda { @pipeline.run }
98
+
99
+ stack.shift
100
+ end
101
+
102
+ def tar_success_codes
103
+ gnu_tar? ? [0, 1] : [0]
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,128 @@
1
+ # encoding: utf-8
2
+
3
+ module Backup
4
+ class Pipeline
5
+ class Error < Backup::Error; end
6
+
7
+ include Utilities::Helpers
8
+
9
+ attr_reader :stderr, :errors
10
+
11
+ def commands
12
+ @commands
13
+ end
14
+
15
+ def initialize
16
+ @commands = []
17
+ @success_codes = []
18
+ @errors = []
19
+ @stderr = ''
20
+ end
21
+
22
+ ##
23
+ # Adds a command to be executed in the pipeline.
24
+ # Each command will be run in the order in which it was added,
25
+ # with it's output being piped to the next command.
26
+ #
27
+ # +success_codes+ must be an Array of Integer exit codes that will
28
+ # be considered successful for the +command+.
29
+ def add(command, success_codes)
30
+ @commands << command
31
+ @success_codes << success_codes
32
+ end
33
+
34
+ ##
35
+ # Commands added using this method will only be considered successful
36
+ # if their exit status is 0.
37
+ #
38
+ # Use #add if successful exit status codes need to be specified.
39
+ def <<(command)
40
+ add(command, [0])
41
+ end
42
+
43
+ ##
44
+ # Runs the command line from `#pipeline` and collects STDOUT/STDERR.
45
+ # STDOUT is then parsed to determine the exit status of each command.
46
+ # For each command with a non-zero exit status, a SystemCallError is
47
+ # created and added to @errors. All STDERR output is set in @stderr.
48
+ #
49
+ # Note that there is no accumulated STDOUT from the commands themselves.
50
+ # Also, the last command should not attempt to write to STDOUT.
51
+ # Any output on STDOUT from the final command will be sent to STDERR.
52
+ # This in itself will not cause #run to fail, but will log warnings
53
+ # when all commands exit with non-zero status.
54
+ #
55
+ # Use `#success?` to determine if all commands in the pipeline succeeded.
56
+ # If `#success?` returns `false`, use `#error_messages` to get an error report.
57
+ def run
58
+ Open4.popen4(pipeline) do |pid, stdin, stdout, stderr|
59
+ pipestatus = stdout.read.gsub("\n", '').split(':').sort
60
+ pipestatus.each do |status|
61
+ index, exitstatus = status.split('|').map(&:to_i)
62
+ unless @success_codes[index].include?(exitstatus)
63
+ command = command_name(@commands[index])
64
+ @errors << SystemCallError.new(
65
+ "'#{ command }' returned exit code: #{ exitstatus }", exitstatus
66
+ )
67
+ end
68
+ end
69
+ @stderr = stderr.read.strip
70
+ end
71
+ Logger.warn(stderr_messages) if success? && stderr_messages
72
+ rescue Exception => err
73
+ raise Error.wrap(err, 'Pipeline failed to execute')
74
+ end
75
+
76
+ def success?
77
+ @errors.empty?
78
+ end
79
+
80
+ ##
81
+ # Returns a multi-line String, reporting all STDERR messages received
82
+ # from the commands in the pipeline (if any), along with the SystemCallError
83
+ # (Errno) message for each command which had a non-zero exit status.
84
+ def error_messages
85
+ @error_messages ||= (stderr_messages || '') +
86
+ "The following system errors were returned:\n" +
87
+ @errors.map {|err| "#{ err.class }: #{ err.message }" }.join("\n")
88
+ end
89
+
90
+ private
91
+
92
+ ##
93
+ # Each command is added as part of the pipeline, grouped with an `echo`
94
+ # command to pass along the command's index in @commands and it's exit status.
95
+ # The command's STDERR is redirected to FD#4, and the `echo` command to
96
+ # report the "index|exit status" is redirected to FD#3.
97
+ # Each command's STDOUT will be connected to the STDIN of the next subshell.
98
+ # The entire pipeline is run within a container group, which redirects
99
+ # FD#3 to STDOUT and FD#4 to STDERR so these can be collected.
100
+ # FD#1 is redirected to STDERR so that any output from the final command
101
+ # on STDOUT will generate warnings, since the final command should not
102
+ # attempt to write to STDOUT, as this would interfere with collecting
103
+ # the exit statuses.
104
+ #
105
+ # There is no guarantee as to the order of this output, which is why the
106
+ # command's index in @commands is passed along with it's exit status.
107
+ # And, if multiple commands output messages on STDERR, those messages
108
+ # may be interleaved. Interleaving of the "index|exit status" outputs
109
+ # should not be an issue, given the small byte size of the data being written.
110
+ def pipeline
111
+ parts = []
112
+ @commands.each_with_index do |command, index|
113
+ parts << %Q[{ #{ command } 2>&4 ; echo "#{ index }|$?:" >&3 ; }]
114
+ end
115
+ %Q[{ #{ parts.join(' | ') } } 3>&1 1>&2 4>&2]
116
+ end
117
+
118
+ def stderr_messages
119
+ @stderr_messages ||= @stderr.empty? ? false : <<-EOS.gsub(/^ +/, ' ')
120
+ Pipeline STDERR Messages:
121
+ (Note: may be interleaved if multiple commands returned error messages)
122
+
123
+ #{ @stderr }
124
+ EOS
125
+ end
126
+
127
+ end
128
+ end
@@ -0,0 +1,82 @@
1
+ require 'net/ssh'
2
+ require 'sshkit'
3
+ require 'sshkit/dsl'
4
+ require 'sshkit/sudo'
5
+
6
+
7
+ module Backup
8
+ module Remote
9
+
10
+ class Command
11
+
12
+ include SSHKit::DSL
13
+
14
+
15
+ def run_ssh_cmd(hostname, ssh_user, ssh_pass, cmd)
16
+ srv = ssh_user+'@'+hostname
17
+ all_servers = [srv]
18
+
19
+ output = ''
20
+
21
+ on all_servers do |srv|
22
+ as(user: ssh_user) do
23
+ #execute(cmd)
24
+ output = capture(cmd)
25
+ end
26
+
27
+ end
28
+
29
+ #
30
+ return { res: 1, output: output }
31
+ end
32
+
33
+ def run_ssh_cmd_sudo(hostname, ssh_user, ssh_pass, cmd, handler=nil)
34
+ host = SSHKit::Host.new("#{ssh_user}@#{hostname}")
35
+ host.password = ssh_pass
36
+
37
+ on host do |host|
38
+ execute("#{cmd}", interaction_handler: handler)
39
+ end
40
+
41
+ #
42
+ return {res: 1, output: ""}
43
+
44
+ rescue => e
45
+ {
46
+ res: 0,
47
+ error: e.message
48
+ }
49
+ end
50
+
51
+ def self.interaction_handler_pwd(user, pwd, host='')
52
+ {
53
+ "#{user}@#{host}'s password:" => "#{pwd}\n",
54
+ /#{user}@#{host}'s password: */ => "#{pwd}\n",
55
+ "password: " => "#{pwd}\n",
56
+ "password:" => "#{pwd}\n",
57
+ "Password: " => "#{pwd}\n",
58
+ }
59
+ end
60
+
61
+
62
+ def ssh_download_file(hostname, ssh_user, ssh_pass, remote_filename, dest_filename)
63
+ host = SSHKit::Host.new("#{ssh_user}@#{hostname}")
64
+ host.password = ssh_pass
65
+
66
+ on host do |host|
67
+ download! remote_filename, dest_filename
68
+ end
69
+
70
+ #
71
+ return {res: 1, output: ""}
72
+
73
+ rescue => e
74
+ {
75
+ res: 0,
76
+ error: e.message
77
+ }
78
+ end
79
+
80
+ end
81
+ end
82
+ end