ey-core 3.0.5 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -7
- data/bin/ey-core +1 -1
- data/ey-core.gemspec +15 -1
- data/lib/ey-core/cli.rb +12 -114
- data/lib/ey-core/cli/accounts.rb +9 -0
- data/lib/ey-core/cli/applications.rb +16 -0
- data/lib/ey-core/cli/console.rb +14 -0
- data/lib/ey-core/cli/current_user.rb +8 -0
- data/lib/ey-core/cli/deploy.rb +65 -0
- data/lib/ey-core/cli/environments.rb +17 -0
- data/lib/ey-core/cli/errors.rb +7 -0
- data/lib/ey-core/cli/init.rb +11 -0
- data/lib/ey-core/cli/login.rb +26 -0
- data/lib/ey-core/cli/logout.rb +14 -0
- data/lib/ey-core/cli/logs.rb +40 -0
- data/lib/ey-core/cli/recipes.rb +92 -0
- data/lib/ey-core/cli/recipes/apply.rb +34 -0
- data/lib/ey-core/cli/recipes/download.rb +26 -0
- data/lib/ey-core/cli/recipes/upload.rb +27 -0
- data/lib/ey-core/cli/scp.rb +11 -0
- data/lib/ey-core/cli/servers.rb +19 -0
- data/lib/ey-core/cli/ssh.rb +94 -0
- data/lib/ey-core/cli/status.rb +20 -0
- data/lib/ey-core/cli/subcommand.rb +114 -0
- data/lib/ey-core/cli/timeout_deploy.rb +28 -0
- data/lib/ey-core/cli/version.rb +8 -0
- data/lib/ey-core/cli/web.rb +10 -0
- data/lib/ey-core/cli/web/disable.rb +23 -0
- data/lib/ey-core/cli/web/enable.rb +23 -0
- data/lib/ey-core/cli/web/restart.rb +23 -0
- data/lib/ey-core/cli/whoami.rb +4 -0
- data/lib/ey-core/client.rb +9 -0
- data/lib/ey-core/client/mock.rb +1 -0
- data/lib/ey-core/client/real.rb +4 -4
- data/lib/ey-core/collections/deployments.rb +8 -0
- data/lib/ey-core/models/account.rb +1 -0
- data/lib/ey-core/models/deployment.rb +23 -0
- data/lib/ey-core/models/environment.rb +37 -0
- data/lib/ey-core/requests/change_environment_maintenance.rb +38 -0
- data/lib/ey-core/requests/create_environment.rb +3 -0
- data/lib/ey-core/requests/deploy_environment_application.rb +17 -0
- data/lib/ey-core/requests/get_deployment.rb +19 -0
- data/lib/ey-core/requests/get_deployments.rb +29 -0
- data/lib/ey-core/requests/get_token_by_login.rb +30 -0
- data/lib/ey-core/requests/restart_environment_app_servers.rb +38 -0
- data/lib/ey-core/requests/timeout_deployment.rb +27 -0
- data/lib/ey-core/requests/upload_recipes_for_environment.rb +28 -0
- data/lib/ey-core/version.rb +1 -1
- data/spec/deployments_spec.rb +24 -0
- data/spec/tokens_spec.rb +23 -1
- metadata +228 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc84632655feed3d40115fdf1417d1a2e203877e
|
4
|
+
data.tar.gz: 92b68a9e1c740d0ad32148486f8d9516976722f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e58ade18e6f14500f8f47446d20829777a438a44d89ed653a705d31edd5cabfbb5b8ec1955f7e798b4a1bbce6ce4c6cc4958148e32e82039fb948c39db54c7a
|
7
|
+
data.tar.gz: 6e24dfeaaa4781fa782f8cca1b6b44f0b95347c33a387c9a9a66c7a341ada2055eb88caa887f617062fd6d6bb4bf3e2d75b52b2e908b0fe3c669d4bc490054ee
|
data/Gemfile
CHANGED
@@ -3,12 +3,6 @@ source 'https://rubygems.org'
|
|
3
3
|
# Specify your gem's dependencies in ey-core.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
gem 'awesome_print', '~> 1.0'
|
7
|
-
gem 'faye', '~> 1.1'
|
8
|
-
gem 'oj'
|
9
|
-
gem 'oj_mimic_json'
|
10
|
-
gem 'pry-nav'
|
11
|
-
|
12
6
|
group :doc do
|
13
7
|
gem 'yard'
|
14
8
|
gem 'redcarpet'
|
@@ -21,7 +15,6 @@ group :test do
|
|
21
15
|
gem 'guard-rspec', '~> 4.2', require: false
|
22
16
|
gem 'hashie'
|
23
17
|
gem 'rack-test'
|
24
|
-
gem 'rake'
|
25
18
|
gem 'rspec', '~> 3.1'
|
26
19
|
gem 'simplecov'
|
27
20
|
gem 'timecop'
|
data/bin/ey-core
CHANGED
data/ey-core.gemspec
CHANGED
@@ -19,10 +19,24 @@ Gem::Specification.new do |gem|
|
|
19
19
|
gem.licenses = ["MIT"]
|
20
20
|
|
21
21
|
gem.add_dependency "addressable", "~> 2.2"
|
22
|
+
gem.add_dependency "awesome_print"
|
23
|
+
gem.add_dependency "belafonte"
|
22
24
|
gem.add_dependency "cistern", "~> 0.12"
|
25
|
+
gem.add_dependency "colorize"
|
23
26
|
gem.add_dependency "ey-hmac", "~> 2.0"
|
24
|
-
gem.add_dependency "
|
27
|
+
gem.add_dependency "escape"
|
25
28
|
gem.add_dependency "faraday", "~> 0.9"
|
26
29
|
gem.add_dependency "faraday_middleware", "~> 0.9"
|
30
|
+
gem.add_dependency "faye"
|
27
31
|
gem.add_dependency "mime-types", "~> 2.99" #maintain ruby 1.9 compatibility
|
32
|
+
gem.add_dependency "oj"
|
33
|
+
gem.add_dependency "oj_mimic_json"
|
34
|
+
gem.add_dependency "pry"
|
35
|
+
gem.add_dependency "sshkey", "~> 1.6"
|
36
|
+
gem.add_dependency "table_print"
|
37
|
+
|
38
|
+
gem.add_development_dependency "pry-nav"
|
39
|
+
gem.add_development_dependency "rspec", "~> 3.0"
|
40
|
+
gem.add_development_dependency "ffaker"
|
41
|
+
gem.add_development_dependency "rake"
|
28
42
|
end
|
data/lib/ey-core/cli.rb
CHANGED
@@ -3,124 +3,22 @@ require 'ostruct'
|
|
3
3
|
require 'ey-core'
|
4
4
|
require 'awesome_print'
|
5
5
|
require 'pry'
|
6
|
+
require 'belafonte'
|
7
|
+
require 'table_print'
|
8
|
+
require 'rubygems/package'
|
9
|
+
require 'escape'
|
6
10
|
|
7
11
|
Cistern.formatter = Cistern::Formatter::AwesomePrint
|
8
12
|
|
9
|
-
class Ey::Core::Cli
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
def self.say(*args)
|
16
|
-
self.stdout.puts(*args)
|
17
|
-
end
|
18
|
-
|
19
|
-
attr_reader :options, :action, :resource, :client
|
20
|
-
|
21
|
-
def initialize(args=ARGV)
|
22
|
-
@action, @options = parse(args)
|
23
|
-
end
|
24
|
-
|
25
|
-
def parse(args)
|
26
|
-
options = {}
|
27
|
-
|
28
|
-
action, resource_name, resource_id = OptionParser.new do |opts|
|
29
|
-
opts.banner = "Usage: ey-core ACTION RESOURCE RESOURCE_ID [options]"
|
30
|
-
|
31
|
-
opts.separator ""
|
32
|
-
opts.separator "Specific options:"
|
33
|
-
|
34
|
-
opts.on("-t", "--token [TOKEN]",
|
35
|
-
"Use specific token. Defaults to core url entry in ~/.ey-core yaml file") do |token|
|
36
|
-
options[:token] = token
|
37
|
-
end
|
38
|
-
|
39
|
-
opts.on("-u", "--url [URL]",
|
40
|
-
"Use specific core URL. Defaults to 'https://api.engineyard.com'") do |url|
|
41
|
-
options[:url] = url
|
42
|
-
end
|
43
|
-
|
44
|
-
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
45
|
-
options[:logger] = v ? Logger.new(STDOUT) : Logger.new(nil)
|
46
|
-
end
|
47
|
-
|
48
|
-
opts.on("-e", "--execute-command [COMMAND]", "Execute Command") do |c|
|
49
|
-
@execute_command = c
|
50
|
-
end
|
51
|
-
|
52
|
-
opts.separator ""
|
53
|
-
opts.separator "Common options:"
|
54
|
-
|
55
|
-
# No argument, shows at tail. This will print an options summary.
|
56
|
-
# Try it and see!
|
57
|
-
opts.on_tail("-h", "--help", "Show this message") do
|
58
|
-
puts opts
|
59
|
-
exit
|
60
|
-
end
|
61
|
-
|
62
|
-
# Another typical switch to print the version.
|
63
|
-
opts.on_tail("--version", "Show version") do
|
64
|
-
puts Ey::Core::VERSION
|
65
|
-
exit
|
66
|
-
end
|
67
|
-
end.parse!(args)
|
68
|
-
|
69
|
-
set_client(options)
|
70
|
-
|
71
|
-
set_resource(action, resource_name, resource_id) if resource_name && resource_id
|
72
|
-
|
73
|
-
[action, (options.to_hash || {})]
|
74
|
-
end
|
75
|
-
|
76
|
-
def run
|
77
|
-
public_send(action)
|
78
|
-
end
|
79
|
-
|
80
|
-
def console
|
81
|
-
if @execute_command
|
82
|
-
@client.instance_eval(@execute_command)
|
83
|
-
else
|
84
|
-
Pry.config.prompt = proc { |obj, nest_level, _| "ey-core:> " }
|
85
|
-
@client.pry
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def show
|
90
|
-
Ey::Core::Cli.say(resource.ai)
|
91
|
-
end
|
92
|
-
|
93
|
-
def destroy
|
94
|
-
response = resource.destroy!
|
95
|
-
|
96
|
-
if response.is_a?(Ey::Core::Client::Request)
|
97
|
-
response.wait_for!(response.service.timeout, response.service.poll_interval) do |request|
|
98
|
-
Ey::Core::Cli.say "Waiting ... #{Time.now}"
|
99
|
-
Ey::Core::Cli.say(response.ai) unless request.ready?
|
100
|
-
request.ready?
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
Ey::Core::Cli.say(response.ai)
|
105
|
-
end
|
106
|
-
|
107
|
-
def current_user
|
108
|
-
@current_user ||= @client.users.current
|
109
|
-
end
|
110
|
-
|
111
|
-
def set_client(options)
|
112
|
-
@client ||= Ey::Core::Client.new(options)
|
113
|
-
end
|
114
|
-
|
115
|
-
def set_resource(action, resource_name, resource_id)
|
116
|
-
action = 'show' if action == 'console'
|
117
|
-
action || raise(ArgumentError.new("Missing action"))
|
118
|
-
Ey::Core::Client.models.find { |m,_| m.to_s == resource_name } || raise(ArgumentError.new("Unknown resource: #{resource_name}"))
|
119
|
-
%w[show destroy].include?(action) || raise(ArgumentError.new("Unknown action: #{action}"))
|
14
|
+
class Ey::Core::Cli < Belafonte::App
|
15
|
+
title "Engineyard CLI"
|
16
|
+
summary "Successor to the engineyard gem"
|
120
17
|
|
121
|
-
|
122
|
-
|
18
|
+
require_relative "cli/subcommand"
|
19
|
+
Dir[File.dirname(__FILE__) + '/cli/*.rb'].each {|file| load file }
|
123
20
|
|
124
|
-
|
21
|
+
Ey::Core::Cli::Subcommand.descendants.each do |d|
|
22
|
+
mount d
|
125
23
|
end
|
126
|
-
end
|
24
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class Ey::Core::Cli::Accounts < Ey::Core::Cli::Subcommand
|
2
|
+
title "accounts"
|
3
|
+
summary "Retrieve a list of Engine Yard accounts that you have access to."
|
4
|
+
|
5
|
+
def handle
|
6
|
+
table_data = TablePrint::Printer.new(current_accounts, [{id: {width: 36}}, :name])
|
7
|
+
puts table_data.table_print
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Ey::Core::Cli::Applications < Ey::Core::Cli::Subcommand
|
2
|
+
title "applications"
|
3
|
+
summary "Retrieve a list of Engine Yard applications that you have access to."
|
4
|
+
option :account, short: 'c', long: 'account', description: 'Filter by account name or id', argument: 'Account'
|
5
|
+
|
6
|
+
def handle
|
7
|
+
applications = if option(:account)
|
8
|
+
core_account_for(options).applications.all
|
9
|
+
else
|
10
|
+
current_accounts.map(&:applications).flatten.sort_by(&:id)
|
11
|
+
end
|
12
|
+
|
13
|
+
table_data = TablePrint::Printer.new(applications, [{id: {width: 10}}, :name])
|
14
|
+
puts table_data.table_print
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Ey::Core::Cli::Console < Ey::Core::Cli::Subcommand
|
2
|
+
title "console"
|
3
|
+
summary "Start an interactive console"
|
4
|
+
option :execute_command, short: "e", long: "command", description: "Command to execute", argument: "command"
|
5
|
+
|
6
|
+
def handle
|
7
|
+
if command = option(:execute_command)
|
8
|
+
core_client.instance_eval(command)
|
9
|
+
else
|
10
|
+
Pry.config.prompt = proc { |obj, nest_level, _| "ey-core:> " }
|
11
|
+
core_client.pry
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class Ey::Core::Cli::Deploy < Ey::Core::Cli::Subcommand
|
2
|
+
title "deploy"
|
3
|
+
summary "Deploy your application"
|
4
|
+
|
5
|
+
option :environment, short: "e", long: "environment", description: "Name or id of the environment to deploy to.", argument: "Environment"
|
6
|
+
option :account, short: 'c', long: 'account', 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.', argument: 'Account name or id'
|
7
|
+
option :ref, short: "r", long: "ref", description: "A git reference to deploy.", argument: "ref"
|
8
|
+
option :migrate, short: "m", long: "migrate", description: "The migration command to run. This option has a 50 character limit.", argument: "migrate"
|
9
|
+
option :app, short: "a", long: "app", description: "Application name or ID to deploy. If :account is not specified, this will be the first app that matches the criteria in the accounts you have access to.", argument: "app"
|
10
|
+
|
11
|
+
switch :stream, long: "stream", description: "Stream deploy output to this console."
|
12
|
+
switch :verbose, short: "v", long: "verbose", description: "Stream deploy output to this console. Alias to stream for backwards compatibility."
|
13
|
+
switch :no_migrate, long: "no-migrate", description: "Skip migration."
|
14
|
+
|
15
|
+
def handle
|
16
|
+
%w(environment app).each { |option| raise "--#{option} is required" unless options[option.to_sym] }
|
17
|
+
operator, environment = core_operator_and_environment_for(self.options)
|
18
|
+
if operator.is_a?(Ey::Core::Client::Account)
|
19
|
+
abort <<-EOF
|
20
|
+
Found account #{operator.name} but requested account #{option(:account)}.
|
21
|
+
Use the ID of the account instead of the name.
|
22
|
+
This can be retrieved by running "ey accounts".
|
23
|
+
EOF
|
24
|
+
.red unless operator.name == option(:account) || operator.id == option(:account)
|
25
|
+
end
|
26
|
+
|
27
|
+
unless environment
|
28
|
+
abort "Unable to locate environment #{option[:environment]} in #{operator.name}".red
|
29
|
+
end
|
30
|
+
|
31
|
+
unless option(:account)
|
32
|
+
self.options.merge!(environment: environment)
|
33
|
+
end
|
34
|
+
|
35
|
+
app = core_application_for(self.options)
|
36
|
+
|
37
|
+
deploy_options = {}
|
38
|
+
deploy_options.merge!(ref: option(:ref)) if option(:ref)
|
39
|
+
deploy_options.merge!(migrate_command: option(:migrate)) if option(:migrate)
|
40
|
+
deploy_options.merge!(migrate_command: '') if switch_active?(:no_migrate)
|
41
|
+
request = environment.deploy(app, deploy_options)
|
42
|
+
|
43
|
+
puts <<-EOF
|
44
|
+
Deploy started to environment: #{environment.name} with application: #{app.name}
|
45
|
+
Request ID: #{request.id}
|
46
|
+
EOF
|
47
|
+
if switch_active?(:stream) || switch_active?(:verbose)
|
48
|
+
request.subscribe { |m| print m["message"] if m.is_a?(Hash) }
|
49
|
+
puts "" # fix console output from stream
|
50
|
+
else
|
51
|
+
request.wait_for { |r| r.ready? } # dont raise from ready!
|
52
|
+
end
|
53
|
+
|
54
|
+
if request.successful
|
55
|
+
puts "Deploy successful!".green
|
56
|
+
else
|
57
|
+
abort <<-EOF
|
58
|
+
Deploy failed!
|
59
|
+
Request output:
|
60
|
+
#{request.message}
|
61
|
+
EOF
|
62
|
+
.red
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Ey::Core::Cli::Environments < Ey::Core::Cli::Subcommand
|
2
|
+
title "environments"
|
3
|
+
summary "Retrieve a list of Engine Yard environments that you have access to."
|
4
|
+
option :account, short: 'c', long: 'account', description: 'Filter by account name or id', argument: 'Account'
|
5
|
+
|
6
|
+
def handle
|
7
|
+
environments = if option(:account)
|
8
|
+
core_account_for(options).environments.all
|
9
|
+
else
|
10
|
+
current_accounts.map(&:environments).flatten.sort_by(&:id)
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
table_data = TablePrint::Printer.new(environments, [{id: {width: 10}}, :name])
|
15
|
+
puts table_data.table_print
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
class Ey::Core::Cli::Errors
|
2
|
+
::Ey::Core::Cli::RecipesNotFound = Class.new(ArgumentError)
|
3
|
+
::Ey::Core::Cli::NoCommand = Class.new(ArgumentError)
|
4
|
+
::Ey::Core::Cli::NoRepository = Class.new(ArgumentError)
|
5
|
+
::Ey::Core::Cli::RecipesExist = Class.new(ArgumentError)
|
6
|
+
::Ey::Core::Cli::AmbiguousSearch = Class.new(ArgumentError)
|
7
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class Ey::Core::Cli::Init < Ey::Core::Cli::Subcommand
|
2
|
+
title "init"
|
3
|
+
summary "Deprecated"
|
4
|
+
description <<-DESC
|
5
|
+
The init command has been deprecated. We apologize for any inconvenience.
|
6
|
+
DESC
|
7
|
+
|
8
|
+
def handle
|
9
|
+
abort "This command is deprecated".red
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Ey::Core::Cli::Login < Ey::Core::Cli::Subcommand
|
2
|
+
title "login"
|
3
|
+
summary "Retrieve API token from Engine Yard Cloud"
|
4
|
+
|
5
|
+
def handle
|
6
|
+
email = ENV["EMAIL"] || ask("Email:")
|
7
|
+
password = ENV["PASSWORD"] || ask("Password:", echo: false)
|
8
|
+
|
9
|
+
token = unauthenticated_core_client.get_api_token(email, password).body["api_token"]
|
10
|
+
|
11
|
+
existing_token = core_yaml[core_url]
|
12
|
+
write_token = if existing_token && existing_token != token
|
13
|
+
puts "New token does not match existing token. Overwriting".yellow
|
14
|
+
true
|
15
|
+
elsif existing_token == token
|
16
|
+
puts "Token already exists".green
|
17
|
+
false
|
18
|
+
else
|
19
|
+
puts "Writing token".green
|
20
|
+
true
|
21
|
+
end
|
22
|
+
write_core_yaml(token) if write_token
|
23
|
+
rescue Ey::Core::Response::Unauthorized
|
24
|
+
abort "Invalid email or password".yellow
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Ey::Core::Cli::Logout < Ey::Core::Cli::Subcommand
|
2
|
+
title "logout"
|
3
|
+
summary "Remove your Engine Yard API token"
|
4
|
+
|
5
|
+
def handle
|
6
|
+
if core_yaml[core_url]
|
7
|
+
core_yaml.delete(core_url)
|
8
|
+
write_core_yaml
|
9
|
+
puts "Successfully removed API token from credentials file".green
|
10
|
+
else
|
11
|
+
puts "No API token found".yellow
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Ey::Core::Cli::Logs < Ey::Core::Cli::Subcommand
|
2
|
+
title "logs"
|
3
|
+
summary "Retrieve the latest logs for an environment"
|
4
|
+
description <<-DESC
|
5
|
+
Displays Engine Yard configuration logs for all servers in the environment. If
|
6
|
+
recipes were uploaded to the environment & run, their logs will also be
|
7
|
+
displayed beneath the main configuration logs.
|
8
|
+
DESC
|
9
|
+
|
10
|
+
option :environment, short: "e", long: "environment", description: "Name or id of the environment to deploy to.", argument: "Environment"
|
11
|
+
option :account, short: 'c', long: 'account', 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.', argument: 'Account name or id'
|
12
|
+
option :server, short: 's', long: 'server', description: "Only retrieve logs for the specified server", argument: "id or amazon_id"
|
13
|
+
|
14
|
+
def handle
|
15
|
+
operator, environment = core_operator_and_environment_for(options)
|
16
|
+
abort "Unable to find matching environment".red unless environment
|
17
|
+
|
18
|
+
servers = if option(:server)
|
19
|
+
[environment.servers.get(option(:server))] || environment.servers.all(provisioned_id: option(:server))
|
20
|
+
else
|
21
|
+
environment.servers.all
|
22
|
+
end
|
23
|
+
|
24
|
+
abort "No servers found".red if servers.empty?
|
25
|
+
|
26
|
+
servers.each do |server|
|
27
|
+
name = server.name ? "#{server.name} (#{server.role})" : server.role
|
28
|
+
|
29
|
+
if log = server.latest_main_log
|
30
|
+
puts "Main logs for #{name}:".green
|
31
|
+
puts log.contents
|
32
|
+
end
|
33
|
+
|
34
|
+
if log = server.latest_custom_log
|
35
|
+
puts "Custom logs for #{name}:".green
|
36
|
+
puts log.contents
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|