snapback 0.0.1 → 0.0.3
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.
- 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
|