snapback 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -7
- data/README.md +6 -11
- data/bin/snapback +51 -67
- data/lib/snapback.rb +130 -0
- data/lib/snapback/cli/commit.rb +72 -0
- data/lib/snapback/cli/create.rb +157 -0
- data/lib/snapback/cli/drop.rb +94 -0
- data/lib/snapback/cli/install.rb +179 -0
- data/lib/snapback/cli/mount.rb +97 -0
- data/lib/snapback/cli/rollback.rb +161 -0
- data/lib/snapback/cli/snapshot.rb +155 -0
- data/lib/snapback/cli/unmount.rb +86 -0
- data/lib/snapback/configuration/Configuration_0_0_3.rb +99 -0
- data/lib/snapback/configuration_loader.rb +24 -0
- data/lib/snapback/filesystem.rb +39 -0
- data/lib/snapback/mysql/client_control.rb +125 -0
- data/lib/snapback/mysql/service_control.rb +14 -0
- data/lib/snapback/transaction.rb +25 -10
- data/lib/snapback/version.rb +3 -0
- metadata +110 -67
- data/lib/snapback/app/commit.rb +0 -35
- data/lib/snapback/app/create.rb +0 -117
- data/lib/snapback/app/drop.rb +0 -51
- data/lib/snapback/app/install.rb +0 -156
- data/lib/snapback/app/mount.rb +0 -62
- data/lib/snapback/app/rollback.rb +0 -63
- data/lib/snapback/app/snapshot.rb +0 -74
- data/lib/snapback/app/unmount.rb +0 -39
- data/lib/snapback/database.rb +0 -102
- data/lib/snapback/dsl.rb +0 -208
- data/lib/snapback/options.rb +0 -85
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
---
|
2
|
-
SHA1:
|
3
|
-
|
4
|
-
|
5
|
-
SHA512:
|
6
|
-
|
7
|
-
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 143e6d3560aafaca95946154237916b3b02dca1c
|
4
|
+
data.tar.gz: 41ffc96d8acc093da21be2818ac50a824f84f1ff
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 80faaf5da96a475a10dc9922798e39576138848cfbcabb704ad8a6039db35d2d27cb6d9c20d0b3aa6be2602f3436c42175a3303948190c834668a1a8b3bb5f8e
|
7
|
+
data.tar.gz: 671d610a7cd87dcaf4df7dd7700b261e71dd6c39e0b9ab3d611f52cfca9497b360eabe802dbcbed5e64fe0d73b57fe03b5ab9a5c3b4dd1d73502f31e8e836156
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Snapback
|
2
2
|
|
3
|
-
Version 0.
|
3
|
+
Version 0.0.3
|
4
4
|
|
5
|
-
Create MySQL snapshots for easy development rollback.
|
5
|
+
Create MySQL snapshots for easy development rollback. Only recommended for use on virtual machines, not recommended for production environments.
|
6
6
|
|
7
7
|
## Project Details
|
8
8
|
|
@@ -53,8 +53,8 @@ The gem should also automatically install the following gems:
|
|
53
53
|
You must then enable multiple tablespaces.
|
54
54
|
Add a line to the [mysqld] section of your MySQL my.cnf:
|
55
55
|
|
56
|
-
[mysqld]
|
57
|
-
innodb_file_per_table
|
56
|
+
[mysqld]
|
57
|
+
innodb_file_per_table
|
58
58
|
|
59
59
|
### Configuration
|
60
60
|
|
@@ -72,11 +72,6 @@ Restart AppArmor and MySQL
|
|
72
72
|
service apparmor restart
|
73
73
|
service mysql restart
|
74
74
|
|
75
|
-
### Usage
|
76
|
-
|
77
|
-
Only recommended for use on virtual machines.
|
78
|
-
Not recommended for production environments.
|
79
|
-
|
80
75
|
## Setup
|
81
76
|
|
82
77
|
To start using this application, you must run this once:
|
@@ -144,7 +139,7 @@ E.g.: Stop using the database "camera"
|
|
144
139
|
|
145
140
|
If you're getting an error message similar to following:
|
146
141
|
|
147
|
-
File descriptor ? (
|
142
|
+
File descriptor ? (~/.snapback.yml) leaked on lvcreate invocation.
|
148
143
|
|
149
144
|
You can circumvent this error by adding the following environmental variable before your command:
|
150
145
|
|
@@ -152,7 +147,7 @@ You can circumvent this error by adding the following environmental variable bef
|
|
152
147
|
|
153
148
|
For example:
|
154
149
|
|
155
|
-
sudo LVM_SUPPRESS_FD_WARNINGS=1
|
150
|
+
sudo LVM_SUPPRESS_FD_WARNINGS=1 snapback create camera --size 100M
|
156
151
|
|
157
152
|
## Licence
|
158
153
|
|
data/bin/snapback
CHANGED
@@ -1,82 +1,66 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
require 'gli'
|
3
|
+
require 'colorize'
|
4
|
+
require 'snapback'
|
2
5
|
|
3
|
-
|
4
|
-
require 'yaml'
|
5
|
-
require 'optparse'
|
6
|
-
require 'pp'
|
6
|
+
include GLI::App
|
7
7
|
|
8
|
-
require 'snapback/
|
9
|
-
require 'snapback/
|
10
|
-
require 'snapback/
|
11
|
-
require 'snapback/
|
8
|
+
require 'snapback/cli/install'
|
9
|
+
require 'snapback/cli/create'
|
10
|
+
require 'snapback/cli/drop'
|
11
|
+
require 'snapback/cli/snapshot'
|
12
|
+
require 'snapback/cli/commit'
|
13
|
+
require 'snapback/cli/rollback'
|
14
|
+
require 'snapback/cli/mount'
|
15
|
+
require 'snapback/cli/unmount'
|
12
16
|
|
13
|
-
|
14
|
-
# Ensure user is root
|
15
|
-
if Process.uid != 0 then
|
16
|
-
raise "Must run as root"
|
17
|
-
end
|
18
|
-
rescue
|
19
|
-
$stderr.puts "#{$!.to_s.red}"
|
20
|
-
exit
|
21
|
-
end
|
17
|
+
program_desc 'Create database snapshots using logical volume management (LVM)'
|
22
18
|
|
23
|
-
|
24
|
-
$options = Snapback::Options.parse(ARGV)
|
19
|
+
version Snapback::VERSION
|
25
20
|
|
26
|
-
|
27
|
-
|
28
|
-
require "snapback/app/install"
|
29
|
-
Snapback::App::Install.instance.go
|
21
|
+
desc 'Show debugging messages'
|
22
|
+
switch [:q,:quiet]
|
30
23
|
|
31
|
-
|
32
|
-
|
33
|
-
|
24
|
+
desc 'Location of the snapback configuration file'
|
25
|
+
flag :config, :default_value => '~/.snapback.yml'
|
26
|
+
|
27
|
+
pre do |global,command,options,args|
|
28
|
+
# Pre logic here
|
29
|
+
# Return true to proceed; false to abort and not call the
|
30
|
+
# chosen command
|
31
|
+
# Use skips_pre before a command to skip this block
|
32
|
+
# on that command only
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
rescue
|
39
|
-
raise "Could not load configuration file: #{$options[:config]}"
|
34
|
+
# Check the user is running as root
|
35
|
+
if Process.uid != 0 then
|
36
|
+
raise "You must run snapback as #{"root".colorize(:red)}"
|
40
37
|
end
|
41
38
|
|
42
|
-
#
|
43
|
-
|
44
|
-
$database.hostname = $config['mysql']['hostname']
|
45
|
-
$database.username = $config['mysql']['username']
|
46
|
-
$database.password = $config['mysql']['password']
|
47
|
-
$database.connect
|
39
|
+
# Normalise the configuration filename
|
40
|
+
global[:config] = File.expand_path(global[:config])
|
48
41
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
Snapback
|
53
|
-
when "snapshot"
|
54
|
-
require "snapback/app/snapshot"
|
55
|
-
Snapback::App::Snapshot.instance.go
|
56
|
-
when "commit"
|
57
|
-
require "snapback/app/commit"
|
58
|
-
Snapback::App::Commit.instance.go
|
59
|
-
when "rollback"
|
60
|
-
require "snapback/app/rollback"
|
61
|
-
Snapback::App::Rollback.instance.go
|
62
|
-
when "drop"
|
63
|
-
require "snapback/app/drop"
|
64
|
-
Snapback::App::Drop.instance.go
|
65
|
-
when "mount"
|
66
|
-
require "snapback/app/mount"
|
67
|
-
Snapback::App::Mount.instance.go
|
68
|
-
when "unmount"
|
69
|
-
require "snapback/app/unmount"
|
70
|
-
Snapback::App::Unmount.instance.go
|
42
|
+
if global[:quiet] then
|
43
|
+
Snapback.set_quiet
|
44
|
+
else
|
45
|
+
Snapback.set_verbose
|
71
46
|
end
|
72
|
-
rescue
|
73
|
-
puts ""
|
74
|
-
$stderr.puts "#{$!.to_s.red}"
|
75
|
-
puts "Use -v (--verbose) to view entire process".red if !$options[:verbose]
|
76
|
-
puts "Rolling back".red
|
77
|
-
puts ""
|
78
47
|
|
79
|
-
|
48
|
+
# Annouce the function
|
49
|
+
puts "Snapback is about to #{command.name.to_s.colorize(:green)}" if Snapback.verbose?
|
50
|
+
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
post do |global,command,options,args|
|
55
|
+
# Post logic here
|
56
|
+
# Use skips_post before a command to skip this
|
57
|
+
# block on that command only
|
58
|
+
end
|
59
|
+
|
60
|
+
on_error do |exception|
|
61
|
+
# Error logic here
|
62
|
+
# return false to skip default error handling
|
63
|
+
true
|
80
64
|
end
|
81
65
|
|
82
|
-
|
66
|
+
exit run(ARGV)
|
data/lib/snapback.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
require 'open4'
|
3
|
+
|
4
|
+
require 'snapback/transaction'
|
5
|
+
require 'snapback/filesystem'
|
6
|
+
|
7
|
+
# Command
|
8
|
+
|
9
|
+
module Snapback
|
10
|
+
@@verbose = false
|
11
|
+
|
12
|
+
def self.set_quiet
|
13
|
+
@@verbose = true
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.set_verbose
|
17
|
+
@@verbose = false
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.quiet?
|
21
|
+
@@verbose == true
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.verbose?
|
25
|
+
@@verbose == false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def run_command description, command = "", &block
|
30
|
+
if Snapback.verbose?
|
31
|
+
print "#{description}".to_s.ljust(72)
|
32
|
+
STDOUT.flush
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
if block_given? then
|
37
|
+
result = block.call
|
38
|
+
|
39
|
+
if result then
|
40
|
+
return_ok
|
41
|
+
else
|
42
|
+
return_no
|
43
|
+
end
|
44
|
+
else
|
45
|
+
err = ""
|
46
|
+
status = Open4::popen4(command) do |pid, stdin, stdout, stderr|
|
47
|
+
err = stderr.read
|
48
|
+
end
|
49
|
+
|
50
|
+
if status != 0 then
|
51
|
+
raise err
|
52
|
+
end
|
53
|
+
|
54
|
+
return_ok
|
55
|
+
end
|
56
|
+
rescue Exception => e
|
57
|
+
return_failed
|
58
|
+
raise $! # rethrow
|
59
|
+
end
|
60
|
+
|
61
|
+
return result
|
62
|
+
end
|
63
|
+
|
64
|
+
# Output status
|
65
|
+
|
66
|
+
def return_ok
|
67
|
+
return nil if Snapback.quiet?
|
68
|
+
puts "[#{" OK ".colorize(:green)}]"
|
69
|
+
end
|
70
|
+
|
71
|
+
def return_no
|
72
|
+
return nil if Snapback.quiet?
|
73
|
+
puts "[#{" NO ".colorize(:red)}]"
|
74
|
+
end
|
75
|
+
|
76
|
+
def return_failed
|
77
|
+
return nil if Snapback.quiet?
|
78
|
+
puts "[#{"FAILED".colorize(:red)}]"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Ask
|
82
|
+
|
83
|
+
def ask_int question, max
|
84
|
+
number = nil
|
85
|
+
|
86
|
+
while true
|
87
|
+
print "#{question}: "
|
88
|
+
|
89
|
+
number = $stdin.gets.chomp
|
90
|
+
|
91
|
+
if !Integer(number) then
|
92
|
+
puts "The value you entered is not a number."
|
93
|
+
puts ""
|
94
|
+
next
|
95
|
+
end
|
96
|
+
|
97
|
+
number = number.to_i
|
98
|
+
|
99
|
+
if number < 1 || number > max then
|
100
|
+
puts "Please enter a number between 1 and #{max}."
|
101
|
+
puts ""
|
102
|
+
next
|
103
|
+
end
|
104
|
+
|
105
|
+
break
|
106
|
+
end
|
107
|
+
|
108
|
+
number
|
109
|
+
end
|
110
|
+
|
111
|
+
def ask_string question
|
112
|
+
print "#{question}: "
|
113
|
+
$stdin.gets.chomp
|
114
|
+
end
|
115
|
+
|
116
|
+
def ask_confirm question
|
117
|
+
while true
|
118
|
+
print "#{question} [Y/N]: "
|
119
|
+
|
120
|
+
confirmed = $stdin.gets.chomp.upcase
|
121
|
+
|
122
|
+
if confirmed.eql? 'Y' then
|
123
|
+
return true
|
124
|
+
elsif confirmed.eql? 'N' then
|
125
|
+
return false
|
126
|
+
else
|
127
|
+
puts "Please enter either Y or N."
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'snapback/configuration_loader'
|
2
|
+
require 'snapback/mysql/service_control'
|
3
|
+
|
4
|
+
desc 'Commit the working copy of the database'
|
5
|
+
arg_name 'database [, ...]'
|
6
|
+
command :commit do |c|
|
7
|
+
c.action do |global_options,options,args|
|
8
|
+
# Ensure all flags and switches are present
|
9
|
+
help_now!('database(s) required') if args.empty?
|
10
|
+
|
11
|
+
# Load the configuration
|
12
|
+
config = Snapback::ConfigurationLoader.factory global_options[:config]
|
13
|
+
|
14
|
+
# Connect to MySQL
|
15
|
+
mysql_client = config.mysql_client
|
16
|
+
|
17
|
+
# For each database
|
18
|
+
args.each do |database|
|
19
|
+
|
20
|
+
# Start the transaction
|
21
|
+
Snapback::Transaction.new do
|
22
|
+
run_command "Selecting database: #{database}" do
|
23
|
+
mysql_client.database_select(database)
|
24
|
+
end
|
25
|
+
|
26
|
+
vg_name = config.lvm_volume_group
|
27
|
+
lv_name = "#{config.lvm_database_prefix}-#{database}"
|
28
|
+
lv_path = "/dev/#{vg_name}/#{lv_name}"
|
29
|
+
|
30
|
+
lv_exists = run_command "Checking logical volume exists" do
|
31
|
+
File.exists?(lv_path)
|
32
|
+
end
|
33
|
+
|
34
|
+
if !lv_exists then
|
35
|
+
raise "Logical volume #{lv_path.colorize(:red)} does not exist"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Flush
|
39
|
+
run_command "Flush tables with read lock" do
|
40
|
+
mysql_client.flush_tables
|
41
|
+
end
|
42
|
+
|
43
|
+
# Stop MySQL
|
44
|
+
run_command "Stop MySQL server" do
|
45
|
+
Snapback::MySQL::ServiceControl.stop
|
46
|
+
end
|
47
|
+
|
48
|
+
revert do
|
49
|
+
run_command "Start MySQL server" do
|
50
|
+
Snapback::MySQL::ServiceControl.start
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Commit LVM
|
55
|
+
run_command "Commit logical volume" do
|
56
|
+
exec "lvremove -f #{lv_path}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Start MySQL
|
60
|
+
run_command "Start MySQL server" do
|
61
|
+
Snapback::MySQL::ServiceControl.start
|
62
|
+
end
|
63
|
+
|
64
|
+
revert do
|
65
|
+
run_command "Stop MySQL server" do
|
66
|
+
Snapback::MySQL::ServiceControl.stop
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
desc 'Create a new snapback compliant database'
|
2
|
+
arg_name 'database [, ...]'
|
3
|
+
command :create do |c|
|
4
|
+
|
5
|
+
c.desc 'Disk size of database to create'
|
6
|
+
c.flag [:s, :size]
|
7
|
+
|
8
|
+
c.action do |global_options,options,args|
|
9
|
+
help_now!('parameter -s for size is required') if options[:s].nil?
|
10
|
+
help_now!('database(s) required') if args.empty?
|
11
|
+
|
12
|
+
# Load the configuration
|
13
|
+
config = Snapback::ConfigurationLoader.factory global_options[:config]
|
14
|
+
|
15
|
+
# Connect to MySQL
|
16
|
+
mysql_client = config.mysql_client
|
17
|
+
|
18
|
+
args.each do |database|
|
19
|
+
|
20
|
+
# Start the transaction
|
21
|
+
Snapback::Transaction.new do
|
22
|
+
|
23
|
+
vg_name = config.lvm_volume_group
|
24
|
+
lv_name = "#{config.lvm_database_prefix}-#{database}"
|
25
|
+
lv_path = "/dev/#{vg_name}/#{lv_name}"
|
26
|
+
|
27
|
+
mount_database_directory = config.filesystem_mount_directory(database)
|
28
|
+
mysql_database_directory = "#{mysql_client.get_data_directory}/#{database}"
|
29
|
+
|
30
|
+
database_exists = run_command "Checking database exists: #{database}" do
|
31
|
+
mysql_client.database_exists?(database)
|
32
|
+
end
|
33
|
+
|
34
|
+
if !database_exists then
|
35
|
+
run_command "Creating database: #{database}" do
|
36
|
+
mysql_client.database_create(database)
|
37
|
+
end
|
38
|
+
|
39
|
+
revert do
|
40
|
+
run_command "Dropping database: #{database}" do
|
41
|
+
mysql_client.db_drop(database)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
lv_available = run_command "Checking logical volume name is available" do
|
47
|
+
!File.exists?(lv_path)
|
48
|
+
end
|
49
|
+
|
50
|
+
if !lv_available then
|
51
|
+
raise "Logical volume #{lv_path.colorize(:red)} already exist"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Flush
|
55
|
+
run_command "Flush tables with read lock" do
|
56
|
+
mysql_client.flush_tables
|
57
|
+
end
|
58
|
+
|
59
|
+
# Stop MySQL
|
60
|
+
run_command "Stop MySQL server" do
|
61
|
+
Snapback::MySQL::ServiceControl.stop
|
62
|
+
end
|
63
|
+
|
64
|
+
revert do
|
65
|
+
run_command "Start MySQL server" do
|
66
|
+
Snapback::MySQL::ServiceControl.start
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Logical volume management
|
71
|
+
run_command "Create logical volume",
|
72
|
+
"lvcreate -L #{options[:s]} -n #{lv_name} #{vg_name}"
|
73
|
+
|
74
|
+
revert do
|
75
|
+
run_command "Removing logical volume",
|
76
|
+
"lvremove -f #{vg_name}/#{lv_name}"
|
77
|
+
end
|
78
|
+
|
79
|
+
run_command "Format logical volume filesystem",
|
80
|
+
"mkfs.ext4 #{lv_path}"
|
81
|
+
|
82
|
+
# Create a new directory for where the MySQL database can be mounted
|
83
|
+
# mkdir /mnt/mysql/{dbName};
|
84
|
+
|
85
|
+
mount_diectory_exists = run_command "Checking mount directory: #{mount_database_directory}" do
|
86
|
+
File.directory? mount_database_directory
|
87
|
+
end
|
88
|
+
|
89
|
+
if !mount_diectory_exists then
|
90
|
+
run_command "Make mount directory",
|
91
|
+
"mkdir #{mount_database_directory}"
|
92
|
+
|
93
|
+
revert do
|
94
|
+
run_command "Removing mount directory",
|
95
|
+
"rm -rf #{$mount_database_directory}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
run_command "Changing permissions of mount directory",
|
100
|
+
"chmod 0777 #{mount_database_directory}"
|
101
|
+
|
102
|
+
# Mount the new logical volume
|
103
|
+
run_command "Mounting logical volume",
|
104
|
+
"mount #{lv_path} #{mount_database_directory}"
|
105
|
+
|
106
|
+
revert do
|
107
|
+
run_command "Unmounting logical volume",
|
108
|
+
"umount #{mount_database_directory}"
|
109
|
+
end
|
110
|
+
|
111
|
+
# Move the contents of the database to the new logical volume
|
112
|
+
Snapback::Filesystem.move_mysql_files(mysql_database_directory, mount_database_directory)
|
113
|
+
|
114
|
+
revert do
|
115
|
+
Snapback::Filesystem.move_mysql_files(mount_database_directory, mysql_database_directory)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Remove the folder in the MySQL data directory
|
119
|
+
|
120
|
+
run_command "Remove mysql database directory",
|
121
|
+
"rm -rf #{mysql_database_directory}"
|
122
|
+
|
123
|
+
revert do
|
124
|
+
run_command "Re-creating MySQL database directory",
|
125
|
+
"mkdir #{mysql_database_directory}"
|
126
|
+
end
|
127
|
+
|
128
|
+
# Symbolic-link the MySQL data directory to the new logical volume
|
129
|
+
run_command "Linking mysql data directory",
|
130
|
+
"ln -s #{mount_database_directory} #{mysql_database_directory}"
|
131
|
+
|
132
|
+
revert do
|
133
|
+
run_command "Unlinking mysql data directory",
|
134
|
+
"unlink #{mysql_database_directory}"
|
135
|
+
end
|
136
|
+
|
137
|
+
# Change the permissions & ownership to MySQL
|
138
|
+
run_command "Changing owner to mysql: #{mysql_database_directory}",
|
139
|
+
"chown -R mysql:mysql #{mysql_database_directory}"
|
140
|
+
|
141
|
+
run_command "Changing owner to mysql: #{mount_database_directory}",
|
142
|
+
"chown -R mysql:mysql #{mount_database_directory}"
|
143
|
+
|
144
|
+
# Start MySQL
|
145
|
+
run_command "Starting MySQL server" do
|
146
|
+
Snapback::MySQL::ServiceControl.start
|
147
|
+
end
|
148
|
+
|
149
|
+
revert do
|
150
|
+
run_command "Stopping MySQL server" do
|
151
|
+
Snapback::MySQL::ServiceControl.stop
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|