ey-core 3.1.2 → 3.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.ruby-version +1 -1
- data/.travis.yml +1 -0
- data/Gemfile +0 -2
- data/examples/add_instance.rb +74 -0
- data/examples/boot_env.rb +60 -0
- data/examples/stop_env.rb +51 -0
- data/examples/terminate_instance.rb +58 -0
- data/lib/ey-core/cli/accounts.rb +14 -6
- data/lib/ey-core/cli/applications.rb +32 -12
- data/lib/ey-core/cli/console.rb +24 -10
- data/lib/ey-core/cli/current_user.rb +13 -5
- data/lib/ey-core/cli/deploy.rb +110 -52
- data/lib/ey-core/cli/environments.rb +34 -12
- data/lib/ey-core/cli/errors.rb +10 -6
- data/lib/ey-core/cli/help.rb +30 -0
- data/lib/ey-core/cli/helpers/archive.rb +70 -0
- data/lib/ey-core/cli/helpers/chef.rb +35 -0
- data/lib/ey-core/cli/helpers/core.rb +195 -0
- data/lib/ey-core/cli/helpers/deprecated.rb +39 -0
- data/lib/ey-core/cli/helpers/log_streaming.rb +41 -0
- data/lib/ey-core/cli/helpers/stream_printer.rb +42 -0
- data/lib/ey-core/cli/init.rb +11 -8
- data/lib/ey-core/cli/login.rb +33 -21
- data/lib/ey-core/cli/logout.rb +18 -10
- data/lib/ey-core/cli/logs.rb +57 -35
- data/lib/ey-core/cli/main.rb +52 -15
- data/lib/ey-core/cli/recipes.rb +5 -87
- data/lib/ey-core/cli/recipes/apply.rb +83 -43
- data/lib/ey-core/cli/recipes/download.rb +48 -22
- data/lib/ey-core/cli/recipes/main.rb +21 -0
- data/lib/ey-core/cli/recipes/upload.rb +56 -23
- data/lib/ey-core/cli/scp.rb +11 -8
- data/lib/ey-core/cli/servers.rb +37 -15
- data/lib/ey-core/cli/ssh.rb +127 -70
- data/lib/ey-core/cli/status.rb +54 -14
- data/lib/ey-core/cli/subcommand.rb +47 -108
- data/lib/ey-core/cli/timeout_deploy.rb +56 -26
- data/lib/ey-core/cli/version.rb +13 -5
- data/lib/ey-core/cli/web.rb +7 -7
- data/lib/ey-core/cli/web/disable.rb +46 -20
- data/lib/ey-core/cli/web/enable.rb +40 -17
- data/lib/ey-core/cli/web/main.rb +21 -0
- data/lib/ey-core/cli/web/restart.rb +34 -15
- data/lib/ey-core/cli/whoami.rb +11 -3
- data/lib/ey-core/mock/searching.rb +4 -0
- data/lib/ey-core/model.rb +5 -0
- data/lib/ey-core/models/deployment.rb +7 -0
- data/lib/ey-core/models/environment.rb +5 -0
- data/lib/ey-core/models/request.rb +2 -0
- data/lib/ey-core/models/user.rb +2 -0
- data/lib/ey-core/requests/get_servers.rb +1 -1
- data/lib/ey-core/response.rb +4 -0
- data/lib/ey-core/subscribable.rb +3 -3
- data/lib/ey-core/version.rb +1 -1
- data/spec/ey-core/cli/accounts_spec.rb +20 -0
- data/spec/ey-core/cli/recipes/apply_spec.rb +4 -17
- data/spec/ey-core/cli/recipes/download_spec.rb +93 -0
- data/spec/ey-core/cli/recipes/upload_spec.rb +80 -0
- data/spec/servers_spec.rb +15 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/support/cli_helpers.rb +38 -2
- metadata +116 -53
- checksums.yaml +0 -7
@@ -0,0 +1,42 @@
|
|
1
|
+
module Ey
|
2
|
+
module Core
|
3
|
+
module Cli
|
4
|
+
module Helpers
|
5
|
+
module StreamPrinter
|
6
|
+
|
7
|
+
def stream_print(opts)
|
8
|
+
yield Printer.new(opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
class Printer
|
12
|
+
def initialize(rows = {})
|
13
|
+
@rows = rows
|
14
|
+
end
|
15
|
+
def print(*vals)
|
16
|
+
unless @header_printed
|
17
|
+
header = []
|
18
|
+
separator = []
|
19
|
+
@rows.each do |k,v|
|
20
|
+
header << format(k, v)
|
21
|
+
separator << '-' * v
|
22
|
+
end
|
23
|
+
puts header.join("| ")
|
24
|
+
puts separator.join("|-")
|
25
|
+
@header_printed = true
|
26
|
+
end
|
27
|
+
line = []
|
28
|
+
vals.each_with_index do |v,index|
|
29
|
+
line << format(v, @rows.values[index])
|
30
|
+
end
|
31
|
+
puts line.join("| ")
|
32
|
+
end
|
33
|
+
def format(value, width)
|
34
|
+
TablePrint::FixedWidthFormatter.new(width).format(value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/ey-core/cli/init.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
summary "Deprecated"
|
4
|
-
description <<-DESC
|
5
|
-
The init command has been deprecated. We apologize for any inconvenience.
|
6
|
-
DESC
|
1
|
+
require 'ey-core/cli/subcommand'
|
2
|
+
require 'ey-core/cli/helpers/deprecated'
|
7
3
|
|
8
|
-
|
9
|
-
|
4
|
+
module Ey
|
5
|
+
module Core
|
6
|
+
module Cli
|
7
|
+
class Init < Subcommand
|
8
|
+
include Helpers::Deprecated
|
9
|
+
|
10
|
+
deprecate('init')
|
11
|
+
end
|
12
|
+
end
|
10
13
|
end
|
11
14
|
end
|
data/lib/ey-core/cli/login.rb
CHANGED
@@ -1,26 +1,38 @@
|
|
1
|
-
|
2
|
-
title "login"
|
3
|
-
summary "Retrieve API token from Engine Yard Cloud"
|
1
|
+
require 'ey-core/cli/subcommand'
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module Ey
|
4
|
+
module Core
|
5
|
+
module Cli
|
6
|
+
class Login < Subcommand
|
7
|
+
title "login"
|
8
|
+
summary "Retrieve API token from Engine Yard Cloud"
|
8
9
|
|
9
|
-
|
10
|
+
def handle
|
11
|
+
email = ENV["EMAIL"] || ask("Email: ")
|
12
|
+
password = ENV["PASSWORD"] || ask("Password: ") { |q| q.echo = false }
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
14
|
+
token = unauthenticated_core_client.
|
15
|
+
get_api_token(email, password).
|
16
|
+
body["api_token"]
|
17
|
+
|
18
|
+
existing_token = core_yaml[core_url]
|
19
|
+
write_token= if existing_token && existing_token != token
|
20
|
+
puts "New token does not match existing token. Overwriting".yellow
|
21
|
+
true
|
22
|
+
elsif existing_token == token
|
23
|
+
puts "Token already exists".green
|
24
|
+
false
|
25
|
+
else
|
26
|
+
puts "Writing token".green
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
write_core_yaml(token) if write_token
|
31
|
+
|
32
|
+
rescue Ey::Core::Response::Unauthorized
|
33
|
+
abort "Invalid email or password".yellow
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
25
37
|
end
|
26
38
|
end
|
data/lib/ey-core/cli/logout.rb
CHANGED
@@ -1,14 +1,22 @@
|
|
1
|
-
|
2
|
-
title "logout"
|
3
|
-
summary "Remove your Engine Yard API token"
|
1
|
+
require 'ey-core/cli/subcommand'
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
module Ey
|
4
|
+
module Core
|
5
|
+
module Cli
|
6
|
+
class Logout < Subcommand
|
7
|
+
title "logout"
|
8
|
+
summary "Remove your Engine Yard API token"
|
9
|
+
|
10
|
+
def handle
|
11
|
+
if core_yaml[core_url]
|
12
|
+
core_yaml.delete(core_url)
|
13
|
+
write_core_yaml
|
14
|
+
puts "Successfully removed API token from credentials file".green
|
15
|
+
else
|
16
|
+
puts "No API token found".yellow
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
12
20
|
end
|
13
21
|
end
|
14
22
|
end
|
data/lib/ey-core/cli/logs.rb
CHANGED
@@ -1,39 +1,61 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
1
|
+
require 'ey-core/cli/subcommand'
|
2
|
+
|
3
|
+
module Ey
|
4
|
+
module Core
|
5
|
+
module Cli
|
6
|
+
class Logs < Subcommand
|
7
|
+
title "logs"
|
8
|
+
summary "Retrieve the latest logs for an environment"
|
9
|
+
description <<-DESC
|
10
|
+
Displays Engine Yard configuration logs for all servers in the environment. If
|
11
|
+
recipes were uploaded to the environment & run, their logs will also be
|
12
|
+
displayed beneath the main configuration logs.
|
13
|
+
DESC
|
14
|
+
|
15
|
+
option :environment,
|
16
|
+
short: "e",
|
17
|
+
long: "environment",
|
18
|
+
description: "Name or id of the environment to deploy to.",
|
19
|
+
argument: "Environment"
|
20
|
+
|
21
|
+
option :account,
|
22
|
+
short: 'c',
|
23
|
+
long: 'account',
|
24
|
+
description: 'Name or ID of the account that the environment resides in. If no account is specified, the app will deploy to the first environment that meets the criteria, in the accounts you have access to.',
|
25
|
+
argument: 'Account name or id'
|
26
|
+
|
27
|
+
option :server,
|
28
|
+
short: 's',
|
29
|
+
long: 'server',
|
30
|
+
description: "Only retrieve logs for the specified server",
|
31
|
+
argument: "id or amazon_id"
|
32
|
+
|
33
|
+
def handle
|
34
|
+
operator, environment = core_operator_and_environment_for(options)
|
35
|
+
abort "Unable to find matching environment".red unless environment
|
36
|
+
|
37
|
+
servers = if option(:server)
|
38
|
+
[environment.servers.get(option(:server))] || environment.servers.all(provisioned_id: option(:server))
|
39
|
+
else
|
40
|
+
environment.servers.all
|
41
|
+
end
|
42
|
+
|
43
|
+
abort "No servers found".red if servers.empty?
|
44
|
+
|
45
|
+
servers.each do |server|
|
46
|
+
name = server.name ? "#{server.name} (#{server.role})" : server.role
|
47
|
+
|
48
|
+
if log = server.latest_main_log
|
49
|
+
puts "Main logs for #{name}:".green
|
50
|
+
puts log.contents
|
51
|
+
end
|
33
52
|
|
34
|
-
|
35
|
-
|
36
|
-
|
53
|
+
if log = server.latest_custom_log
|
54
|
+
puts "Custom logs for #{name}:".green
|
55
|
+
puts log.contents
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
37
59
|
end
|
38
60
|
end
|
39
61
|
end
|
data/lib/ey-core/cli/main.rb
CHANGED
@@ -1,28 +1,65 @@
|
|
1
|
-
require 'optparse'
|
2
|
-
require 'ostruct'
|
3
|
-
require 'ey-core'
|
1
|
+
#require 'optparse'
|
2
|
+
#require 'ostruct'
|
3
|
+
#require 'ey-core'
|
4
4
|
require 'ey-core/cli'
|
5
5
|
require 'awesome_print'
|
6
|
-
require 'pry'
|
7
6
|
require 'belafonte'
|
8
|
-
require 'table_print'
|
9
7
|
require 'rubygems/package'
|
10
8
|
require 'escape'
|
11
9
|
require 'highline/import'
|
12
10
|
|
13
|
-
|
11
|
+
require 'ey-core/cli/accounts'
|
12
|
+
require 'ey-core/cli/applications'
|
13
|
+
require 'ey-core/cli/console'
|
14
|
+
require 'ey-core/cli/current_user'
|
15
|
+
require 'ey-core/cli/deploy'
|
16
|
+
require 'ey-core/cli/environments'
|
17
|
+
require 'ey-core/cli/help'
|
18
|
+
require 'ey-core/cli/init'
|
19
|
+
require 'ey-core/cli/login'
|
20
|
+
require 'ey-core/cli/logout'
|
21
|
+
require 'ey-core/cli/logs'
|
22
|
+
require 'ey-core/cli/recipes'
|
23
|
+
require 'ey-core/cli/scp'
|
24
|
+
require 'ey-core/cli/servers'
|
25
|
+
require 'ey-core/cli/ssh'
|
26
|
+
require 'ey-core/cli/status'
|
27
|
+
require 'ey-core/cli/timeout_deploy'
|
28
|
+
require 'ey-core/cli/version'
|
29
|
+
require 'ey-core/cli/web'
|
30
|
+
require 'ey-core/cli/whoami'
|
14
31
|
|
32
|
+
Cistern.formatter = Cistern::Formatter::AwesomePrint
|
15
33
|
|
16
|
-
class Ey::Core::Cli::Main < Belafonte::App
|
17
|
-
title "Engineyard CLI"
|
18
|
-
summary "Successor to the engineyard gem"
|
19
34
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
35
|
+
module Ey
|
36
|
+
module Core
|
37
|
+
module Cli
|
38
|
+
class Main < Belafonte::App
|
39
|
+
title "Engineyard CLI"
|
40
|
+
summary "Successor to the engineyard gem"
|
24
41
|
|
25
|
-
|
26
|
-
|
42
|
+
mount Accounts
|
43
|
+
mount Applications
|
44
|
+
mount Console
|
45
|
+
mount CurrentUser
|
46
|
+
mount Deploy
|
47
|
+
mount Environments
|
48
|
+
mount Help
|
49
|
+
mount Init
|
50
|
+
mount Login
|
51
|
+
mount Logout
|
52
|
+
mount Logs
|
53
|
+
mount Recipes::Main
|
54
|
+
mount Scp
|
55
|
+
mount Servers
|
56
|
+
mount Ssh
|
57
|
+
mount Status
|
58
|
+
mount TimeoutDeploy
|
59
|
+
mount Version
|
60
|
+
mount Web::Main
|
61
|
+
mount Whoami
|
62
|
+
end
|
63
|
+
end
|
27
64
|
end
|
28
65
|
end
|
data/lib/ey-core/cli/recipes.rb
CHANGED
@@ -1,91 +1,9 @@
|
|
1
|
-
|
2
|
-
title "recipes"
|
3
|
-
summary "Chef specific commands"
|
1
|
+
require 'ey-core/cli/recipes/main'
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
|
11
|
-
def run_chef(type, environment)
|
12
|
-
request = environment.apply(type)
|
13
|
-
puts "Started #{type} chef run".green
|
14
|
-
request.wait_for { |r| r.ready? }
|
15
|
-
if request.successful
|
16
|
-
puts "#{type.capitalize} chef run completed".green
|
17
|
-
else
|
18
|
-
puts "#{type.capitalize} chef run failed".red
|
19
|
-
ap request
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def gzip(tarfile)
|
24
|
-
gz = StringIO.new("")
|
25
|
-
z = Zlib::GzipWriter.new(gz)
|
26
|
-
z.write tarfile.string
|
27
|
-
z.close # this is necessary!
|
28
|
-
|
29
|
-
# z was closed to write the gzip footer, so
|
30
|
-
# now we need a new StringIO
|
31
|
-
StringIO.new gz.string
|
32
|
-
end
|
33
|
-
|
34
|
-
def archive_directory(path)
|
35
|
-
tarfile = StringIO.new("")
|
36
|
-
Gem::Package::TarWriter.new(tarfile) do |tar|
|
37
|
-
Dir[File.join(path, "**/*")].each do |file|
|
38
|
-
mode = File.stat(file).mode
|
39
|
-
relative_file = "cookbooks/#{file.sub(/^#{Regexp::escape path}\/?/, '')}"
|
40
|
-
|
41
|
-
if File.directory?(file)
|
42
|
-
tar.mkdir relative_file, mode
|
43
|
-
else
|
44
|
-
tar.add_file relative_file, mode do |tf|
|
45
|
-
File.open(file, "rb") { |f| tf.write f.read }
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
tarfile.rewind
|
52
|
-
gzip(tarfile)
|
53
|
-
end
|
54
|
-
|
55
|
-
def upload_recipes(environment, path="cookbooks/")
|
56
|
-
recipes_path = Pathname.new(path)
|
57
|
-
|
58
|
-
if recipes_path.exist? && recipes_path.to_s.match(/\.(tgz|tar\.gz)/)
|
59
|
-
environment.upload_recipes(recipes_path)
|
60
|
-
elsif recipes_path.exist?
|
61
|
-
environment.upload_recipes(archive_directory(path))
|
62
|
-
else
|
63
|
-
raise Ey::Core::Cli::RecipesNotFound, "Recipes file not found: #{recipes_path}"
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def ungzip(tarfile)
|
68
|
-
z = Zlib::GzipReader.new(tarfile)
|
69
|
-
unzipped = StringIO.new(z.read)
|
70
|
-
z.close
|
71
|
-
unzipped
|
72
|
-
end
|
73
|
-
|
74
|
-
|
75
|
-
def untar(io, destination)
|
76
|
-
Gem::Package::TarReader.new io do |tar|
|
77
|
-
tar.each do |tarfile|
|
78
|
-
destination_file = File.join destination, tarfile.full_name
|
79
|
-
|
80
|
-
if tarfile.directory?
|
81
|
-
FileUtils.mkdir_p destination_file
|
82
|
-
else
|
83
|
-
destination_directory = File.dirname(destination_file)
|
84
|
-
FileUtils.mkdir_p destination_directory unless File.directory?(destination_directory)
|
85
|
-
File.open destination_file, "wb" do |f|
|
86
|
-
f.print tarfile.read
|
87
|
-
end
|
88
|
-
end
|
3
|
+
module Ey
|
4
|
+
module Core
|
5
|
+
module Cli
|
6
|
+
module Recipes
|
89
7
|
end
|
90
8
|
end
|
91
9
|
end
|
@@ -1,56 +1,96 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
summary "Apply changes to an environment"
|
4
|
-
option :account, short: "c", long: "account", description: "Name or id of account", argument: "account"
|
5
|
-
option :environment, short: "e", long: "environment", description: "Name or id of environment", argument: "environment"
|
1
|
+
require 'ey-core/cli/subcommand'
|
2
|
+
require 'ey-core/cli/helpers/chef'
|
6
3
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
module Ey
|
5
|
+
module Core
|
6
|
+
module Cli
|
7
|
+
module Recipes
|
8
|
+
class Apply < Subcommand
|
9
|
+
include Helpers::Chef
|
11
10
|
|
12
|
-
|
13
|
-
|
11
|
+
title "apply"
|
12
|
+
summary "Apply changes to an environment"
|
14
13
|
|
15
|
-
|
16
|
-
|
14
|
+
option :account,
|
15
|
+
short: "c",
|
16
|
+
long: "account",
|
17
|
+
description: "Name or id of account",
|
18
|
+
argument: "account"
|
17
19
|
|
18
|
-
|
20
|
+
option :environment,
|
21
|
+
short: "e",
|
22
|
+
long: "environment",
|
23
|
+
description: "Name or id of environment",
|
24
|
+
argument: "environment"
|
19
25
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
26
|
+
switch :main,
|
27
|
+
short: "m",
|
28
|
+
long: "main",
|
29
|
+
description: "Apply main recipes only"
|
24
30
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
'Only one of --main, --custom, --quick, and --full may be specified.'
|
30
|
-
)
|
31
|
-
end
|
32
|
-
end
|
31
|
+
switch :custom,
|
32
|
+
short: "u",
|
33
|
+
long: "custom",
|
34
|
+
description: "Apply custom recipes only"
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
switch :quick,
|
37
|
+
short: "q",
|
38
|
+
long: "quick",
|
39
|
+
description: "Quick chef run"
|
37
40
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
+
switch :full,
|
42
|
+
short: "f",
|
43
|
+
long: "full",
|
44
|
+
description: "Run main and custom chef"
|
41
45
|
|
42
|
-
|
43
|
-
|
44
|
-
end
|
46
|
+
def handle
|
47
|
+
validate_run_type_flags
|
45
48
|
|
46
|
-
|
47
|
-
|
48
|
-
custom: 'custom',
|
49
|
-
quick: 'quick'
|
50
|
-
}
|
51
|
-
end
|
49
|
+
operator, environment = core_operator_and_environment_for(options)
|
50
|
+
raise "Unable to find matching environment" unless environment
|
52
51
|
|
53
|
-
|
54
|
-
|
52
|
+
run_chef(run_type, environment)
|
53
|
+
|
54
|
+
if switch_active?(:full)
|
55
|
+
run_chef("custom", environment)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def validate_run_type_flags
|
61
|
+
if active_run_type_flags.length > 1
|
62
|
+
kernel.abort(
|
63
|
+
'Only one of --main, --custom, --quick, and --full may be specified.'
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def run_type
|
69
|
+
secondary_run_types[active_run_type] || default_run_type
|
70
|
+
end
|
71
|
+
|
72
|
+
def active_run_type
|
73
|
+
active_run_type_flags.first
|
74
|
+
end
|
75
|
+
|
76
|
+
def active_run_type_flags
|
77
|
+
[:main, :custom, :quick, :full].select {|switch|
|
78
|
+
switch_active?(switch)
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def secondary_run_types
|
83
|
+
{
|
84
|
+
custom: 'custom',
|
85
|
+
quick: 'quick'
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def default_run_type
|
90
|
+
'main'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
55
95
|
end
|
56
96
|
end
|