vagrant-aws 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +65 -0
- data/Rakefile +10 -0
- data/lib/vagrant-aws.rb +14 -0
- data/lib/vagrant-aws/action/create.rb +56 -0
- data/lib/vagrant-aws/action/create_image.rb +106 -0
- data/lib/vagrant-aws/action/create_sshkey.rb +39 -0
- data/lib/vagrant-aws/action/deregister_image.rb +27 -0
- data/lib/vagrant-aws/action/populate_ssh.rb +41 -0
- data/lib/vagrant-aws/action/prepare_provisioners.rb +127 -0
- data/lib/vagrant-aws/action/resume.rb +20 -0
- data/lib/vagrant-aws/action/suspend.rb +20 -0
- data/lib/vagrant-aws/action/terminate.rb +21 -0
- data/lib/vagrant-aws/box.rb +20 -0
- data/lib/vagrant-aws/box_collection.rb +15 -0
- data/lib/vagrant-aws/command.rb +186 -0
- data/lib/vagrant-aws/config.rb +38 -0
- data/lib/vagrant-aws/environment.rb +79 -0
- data/lib/vagrant-aws/errors.rb +29 -0
- data/lib/vagrant-aws/middleware.rb +53 -0
- data/lib/vagrant-aws/system.rb +24 -0
- data/lib/vagrant-aws/version.rb +3 -0
- data/lib/vagrant-aws/vm.rb +94 -0
- data/lib/vagrant_init.rb +4 -0
- data/locales/en.yml +64 -0
- data/test/test_helper.rb +24 -0
- data/test/vagrant-aws/action/create_image_test.rb +63 -0
- data/test/vagrant-aws/action/create_ssh_key_test.rb +43 -0
- data/test/vagrant-aws/action/create_test.rb +65 -0
- data/test/vagrant-aws/action/terminate_test.rb +20 -0
- data/test/vagrant-aws/command_test.rb +21 -0
- data/test/vagrant-aws/config_test.rb +14 -0
- data/vagrant-aws.gemspec +29 -0
- metadata +165 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module VagrantAWS
|
2
|
+
class Action
|
3
|
+
class Resume
|
4
|
+
def initialize(app, env)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
if env["vm"].vm.state == "stopped"
|
10
|
+
raise VagrantAWS::Errors::EBSDeviceRequired, :command => "resume" if env["vm"].vm.root_device_type != "ebs"
|
11
|
+
env.ui.info I18n.t("vagrant.actions.vm.resume.resuming")
|
12
|
+
env["vm"].vm.start
|
13
|
+
end
|
14
|
+
|
15
|
+
@app.call(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module VagrantAWS
|
2
|
+
class Action
|
3
|
+
class Suspend
|
4
|
+
def initialize(app, env)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
if env["vm"].vm.running?
|
10
|
+
raise VagrantAWS::Errors::EBSDeviceRequired, :command => "suspend" if env["vm"].vm.root_device_type != "ebs"
|
11
|
+
env.ui.info I18n.t("vagrant.actions.vm.suspend.suspending")
|
12
|
+
env["vm"].vm.stop
|
13
|
+
end
|
14
|
+
|
15
|
+
@app.call(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module VagrantAWS
|
2
|
+
class Action
|
3
|
+
class Terminate
|
4
|
+
def initialize(app, env)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
env.ui.info I18n.t("vagrant.actions.vm.destroy.destroying")
|
10
|
+
|
11
|
+
env["vm"].vm.destroy
|
12
|
+
env["vm"].vm = nil
|
13
|
+
|
14
|
+
@app.call(env)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module VagrantAWS
|
2
|
+
|
3
|
+
class Box < Vagrant::Box
|
4
|
+
|
5
|
+
%w{ ovf_file repackage destroy }.each do |method|
|
6
|
+
undef_method(method)
|
7
|
+
end
|
8
|
+
|
9
|
+
def add
|
10
|
+
raise Vagrant::Errors::BoxAlreadyExists, :name => name if File.directory?(directory)
|
11
|
+
env.actions.run(:aws_add_image, { "box" => self, "validate" => false })
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove(options=nil)
|
15
|
+
env.actions.run(:aws_remove_image, { "box" => self, "validate" => false }.merge(options || {}))
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module VagrantAWS
|
2
|
+
class BoxCollection < Vagrant::BoxCollection
|
3
|
+
|
4
|
+
def reload!
|
5
|
+
@boxes.clear
|
6
|
+
Dir.open(env.boxes_path) do |dir|
|
7
|
+
dir.each do |d|
|
8
|
+
next if d == "." || d == ".." || !File.directory?(env.boxes_path.join(d))
|
9
|
+
@boxes << VagrantAWS::Box.new(env, d)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'fog'
|
2
|
+
|
3
|
+
module VagrantAWS
|
4
|
+
|
5
|
+
class AWSCommands < Vagrant::Command::GroupBase
|
6
|
+
register "aws", "Commands to interact with Amazon AWS (EC2)"
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
initialize_aws_environment(*args)
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
desc "up [NAME]", "Creates the Vagrant environment on Amazon AWS."
|
15
|
+
method_options :provision => true
|
16
|
+
def up(name=nil)
|
17
|
+
target_vms(name).each do |vm|
|
18
|
+
if vm.created?
|
19
|
+
vm.env.ui.info I18n.t("vagrant.commands.up.vm_created")
|
20
|
+
else
|
21
|
+
vm.env.actions.run(:aws_up, "provision.enabled" => options[:provision])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
desc "destroy [NAME]", "Destroy the Vagrant AWS environment, terminating the created virtual machines."
|
28
|
+
def destroy(name=nil)
|
29
|
+
target_vms(name).each do |vm|
|
30
|
+
if vm.created?
|
31
|
+
vm.env.actions.run(:aws_destroy)
|
32
|
+
else
|
33
|
+
vm.env.ui.info I18n.t("vagrant.commands.common.vm_not_created")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
desc "status", "Show the status of the current Vagrant AWS environment."
|
40
|
+
def status
|
41
|
+
state = nil
|
42
|
+
results = target_vms.collect do |vm|
|
43
|
+
state = vm.created? ? vm.vm.state.to_s : 'not_created'
|
44
|
+
"#{vm.name.to_s.ljust(25)}#{state.gsub("_", " ")}"
|
45
|
+
end
|
46
|
+
state = target_vms.length == 1 ? state : "listing"
|
47
|
+
@env.ui.info(I18n.t("vagrant.commands.status.output",
|
48
|
+
:states => results.join("\n"),
|
49
|
+
:message => I18n.t("vagrant.plugins.aws.commands.status.#{state}")),
|
50
|
+
:prefix => false)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
desc "ssh [NAME]", "SSH into the currently running Vagrant AWS environment."
|
55
|
+
method_options %w( execute -e ) => :string
|
56
|
+
def ssh(name=nil)
|
57
|
+
raise Vagrant::Errors::MultiVMTargetRequired, :command => "ssh" if target_vms.length > 1
|
58
|
+
|
59
|
+
ssh_vm = target_vms.first
|
60
|
+
ssh_vm.env.actions.run(VagrantAWS::Action::PopulateSSH)
|
61
|
+
|
62
|
+
if options[:execute]
|
63
|
+
ssh_vm.ssh.execute do |ssh|
|
64
|
+
ssh_vm.env.ui.info I18n.t("vagrant.commands.ssh.execute", :command => options[:execute])
|
65
|
+
ssh.exec!(options[:execute]) do |channel, type, data|
|
66
|
+
ssh_vm.env.ui.info "#{data}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
raise Vagrant::Errors::VMNotCreatedError if !ssh_vm.created?
|
71
|
+
raise Vagrant::Errors::VMNotRunningError if !ssh_vm.vm.running?
|
72
|
+
ssh_vm.ssh.connect
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
desc "provision [NAME]", "Rerun the provisioning scripts on a running VM."
|
78
|
+
def provision(name=nil)
|
79
|
+
target_vms(name).each do |vm|
|
80
|
+
if vm.created? && vm.vm.state == 'running'
|
81
|
+
vm.env.actions.run(:aws_provision)
|
82
|
+
else
|
83
|
+
vm.env.ui.info I18n.t("vagrant.commands.common.vm_not_created")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
desc "suspend [NAME]", "Suspend a running Vagrant AWS environment"
|
90
|
+
def suspend(name=nil)
|
91
|
+
target_vms(name).each do |vm|
|
92
|
+
if vm.created?
|
93
|
+
vm.env.actions.run(:aws_suspend)
|
94
|
+
else
|
95
|
+
vm.env.ui.info I18n.t("vagrant.commands.common.vm_not_created")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
desc "resume [NAME]", "Resume a suspended Vagrant AWS environment"
|
102
|
+
def resume(name=nil)
|
103
|
+
target_vms(name).each do |vm|
|
104
|
+
if vm.created?
|
105
|
+
vm.env.actions.run(:aws_resume)
|
106
|
+
else
|
107
|
+
vm.env.ui.info I18n.t("vagrant.commands.common.vm_not_created")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
desc "ssh_config [NAME]", "outputs .ssh/config valid syntax for connecting to this environment via ssh"
|
114
|
+
method_options %w{ host_name -h } => :string
|
115
|
+
def ssh_config(name=nil)
|
116
|
+
raise Vagrant::Errors::MultiVMTargetRequired, :command => "ssh_config" if target_vms.length > 1
|
117
|
+
|
118
|
+
ssh_vm = target_vms.first
|
119
|
+
raise Vagrant::Errors::VMNotCreatedError if !ssh_vm.created?
|
120
|
+
ssh_vm.env.actions.run(VagrantAWS::Action::PopulateSSH)
|
121
|
+
|
122
|
+
$stdout.puts(Vagrant::Util::TemplateRenderer.render("ssh_config", {
|
123
|
+
:host_key => options[:host] || "vagrant",
|
124
|
+
:ssh_host => ssh_vm.env.config.ssh.host,
|
125
|
+
:ssh_user => ssh_vm.env.config.ssh.username,
|
126
|
+
:ssh_port => ssh_vm.ssh.port,
|
127
|
+
:private_key_path => ssh_vm.env.config.ssh.private_key_path
|
128
|
+
}))
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
desc "box_create [NAME]", "create image box from running Vagrant AWS VM"
|
133
|
+
method_option :register, :aliases => '-r', :type => :boolean, :default => true, :banner => "register with AWS"
|
134
|
+
method_option :image_name, :aliases => '-f', :type => :string, :banner => "name of created image"
|
135
|
+
method_option :image_desc, :aliases => '-d', :type => :string, :banner => "description of created image"
|
136
|
+
def box_create(name=nil)
|
137
|
+
raise Vagrant::Errors::MultiVMTargetRequired, :command => "box_create" if target_vms.length > 1
|
138
|
+
|
139
|
+
ami_vm = target_vms.first
|
140
|
+
ami_vm.env.actions.run(:aws_create_image, {
|
141
|
+
'package.output' => options[:image_name] || env.config.package.name,
|
142
|
+
'image.register' => options[:register],
|
143
|
+
'image.name' => options[:image_name] || "vagrantaws_#{rand(36**8).to_s(36)}",
|
144
|
+
'image.desc' => options[:image_desc] || "Image created by vagrant-aws"
|
145
|
+
})
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
desc "box_add NAME URI", "Add an AWS image box to the system"
|
151
|
+
def box_add(name, uri)
|
152
|
+
Box.add(env, name, uri)
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
desc "box_list", "list available AWS image boxes"
|
157
|
+
def box_list
|
158
|
+
boxes = env.boxes.sort
|
159
|
+
return env.ui.warn(I18n.t("vagrant.commands.box.no_installed_boxes"), :prefix => false) if boxes.empty?
|
160
|
+
boxes.each { |b| env.ui.info(b.name, :prefix => false) }
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
desc "box_remove NAME", "Remove named image box from system and optionally deregister image with AWS"
|
165
|
+
method_option :deregister, :aliases => '-d', :type => :boolean, :default => false, :banner => "deregister with AWS"
|
166
|
+
def box_remove(name)
|
167
|
+
b = env.boxes.find(name)
|
168
|
+
raise Vagrant::Errors::BoxNotFound, :name => name if !b
|
169
|
+
b.remove({ 'image.deregister' => options[:deregister] })
|
170
|
+
end
|
171
|
+
|
172
|
+
protected
|
173
|
+
|
174
|
+
# Reinitialize "AWS" environment
|
175
|
+
def initialize_aws_environment(args, options, config)
|
176
|
+
raise Errors::CLIMissingEnvironment if !config[:env]
|
177
|
+
if config[:env].is_a?(VagrantAWS::Environment)
|
178
|
+
@env = config[:env]
|
179
|
+
else
|
180
|
+
@env = VagrantAWS::Environment.new
|
181
|
+
@env.ui = config[:env].ui # Touch up UI
|
182
|
+
@env.load!
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module VagrantAWS
|
2
|
+
# A configuration class to configure defaults which are used for
|
3
|
+
# the `vagrant-aws` plugin.
|
4
|
+
class Config < Vagrant::Config::Base
|
5
|
+
configures :aws
|
6
|
+
|
7
|
+
attr_accessor :key_name
|
8
|
+
attr_writer :private_key_path
|
9
|
+
attr_accessor :username
|
10
|
+
attr_accessor :security_groups
|
11
|
+
|
12
|
+
attr_accessor :image
|
13
|
+
attr_accessor :flavor
|
14
|
+
|
15
|
+
attr_accessor :region
|
16
|
+
attr_accessor :availability_zone
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@security_groups = ["default"]
|
20
|
+
@region = "us-east-1"
|
21
|
+
@username = "ubuntu"
|
22
|
+
@image = "ami-2ec83147"
|
23
|
+
@flavor = "t1.micro"
|
24
|
+
end
|
25
|
+
|
26
|
+
def using?
|
27
|
+
return top.env.is_a?(VagrantAWS::Environment)
|
28
|
+
end
|
29
|
+
|
30
|
+
def private_key_path
|
31
|
+
@private_key_path.nil? ? nil : File.expand_path(@private_key_path)
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate(errors)
|
35
|
+
errors.add(I18n.t("vagrant.config.ssh.private_key_missing", :path => private_key_path)) if private_key_path && !File.exists?(private_key_path)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'fog'
|
2
|
+
|
3
|
+
module VagrantAWS
|
4
|
+
# Represents a single Vagrant environment, overridden to not alias with
|
5
|
+
# existing Vagrant data storage, VM implementation, etc.
|
6
|
+
class Environment < Vagrant::Environment
|
7
|
+
DEFAULT_DOTFILE = ".vagrantaws"
|
8
|
+
FOGFILE = ".fog"
|
9
|
+
|
10
|
+
def dotfile_path
|
11
|
+
root_path.join(DEFAULT_DOTFILE) rescue nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def aws_home_path
|
15
|
+
home_path.join("aws")
|
16
|
+
end
|
17
|
+
|
18
|
+
def boxes_path
|
19
|
+
aws_home_path.join("images")
|
20
|
+
end
|
21
|
+
|
22
|
+
def boxes
|
23
|
+
return parent.boxes if parent
|
24
|
+
@_boxes ||= VagrantAWS::BoxCollection.new(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def ssh_keys_path
|
28
|
+
aws_home_path.join("keys")
|
29
|
+
end
|
30
|
+
|
31
|
+
def ssh_keys
|
32
|
+
return parent.ssh_keys if parent
|
33
|
+
Dir.chdir(ssh_keys_path) { |unused| Dir.entries('.').select { |f| File.file?(f) } }
|
34
|
+
end
|
35
|
+
|
36
|
+
def load!
|
37
|
+
super
|
38
|
+
|
39
|
+
# Setup fog credential path
|
40
|
+
project_fog_path = root_path.join(FOGFILE) rescue nil
|
41
|
+
Fog.credentials_path = File.expand_path(fogfile_path) if project_fog_path && File.exist?(project_fog_path)
|
42
|
+
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
# Override to create "AWS" specific directories in 'home_dir'
|
47
|
+
def load_home_directory!
|
48
|
+
super
|
49
|
+
|
50
|
+
dirs = %w{ images keys }.map { |d| aws_home_path.join(d) }
|
51
|
+
dirs.each do |dir|
|
52
|
+
next if File.directory?(dir)
|
53
|
+
ui.info I18n.t("vagrant.general.creating_home_dir", :directory => dir)
|
54
|
+
FileUtils.mkdir_p(dir)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Override to create "AWS" VM
|
59
|
+
def load_vms!
|
60
|
+
result = {}
|
61
|
+
|
62
|
+
# Load the VM UUIDs from the local data store
|
63
|
+
(local_data[:active] || {}).each do |name, desc|
|
64
|
+
result[name.to_sym] = VagrantAWS::VM.find(desc, self, name.to_sym)
|
65
|
+
end
|
66
|
+
|
67
|
+
# For any VMs which aren't created, create a blank VM instance for
|
68
|
+
# them
|
69
|
+
all_keys = config.vm.defined_vm_keys
|
70
|
+
all_keys = [DEFAULT_VM] if all_keys.empty?
|
71
|
+
all_keys.each do |name|
|
72
|
+
result[name] = VagrantAWS::VM.new(:name => name, :env => self) if !result.has_key?(name)
|
73
|
+
end
|
74
|
+
|
75
|
+
result
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module VagrantAWS
|
2
|
+
module Errors
|
3
|
+
|
4
|
+
class VagrantError < Vagrant::Errors::VagrantError
|
5
|
+
def error_namespace; "vagrant.plugins.aws.errors"; end
|
6
|
+
end
|
7
|
+
|
8
|
+
class NotYetSupported < VagrantError
|
9
|
+
error_key(:not_yet_supported)
|
10
|
+
end
|
11
|
+
|
12
|
+
class KeyNameNotSpecified < VagrantError
|
13
|
+
error_key(:key_name_not_specified)
|
14
|
+
end
|
15
|
+
|
16
|
+
class PrivateKeyFileNotSpecified < VagrantError
|
17
|
+
error_key(:private_key_file_not_specified)
|
18
|
+
end
|
19
|
+
|
20
|
+
class EBSDeviceRequired < VagrantError
|
21
|
+
error_key(:ebs_device_required)
|
22
|
+
end
|
23
|
+
|
24
|
+
class VMCreateFailure < VagrantError
|
25
|
+
error_key(:vm_create_failure)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'vagrant-aws/action/create'
|
2
|
+
require 'vagrant-aws/action/terminate'
|
3
|
+
require 'vagrant-aws/action/create_sshkey'
|
4
|
+
require 'vagrant-aws/action/populate_ssh'
|
5
|
+
require 'vagrant-aws/action/prepare_provisioners'
|
6
|
+
require 'vagrant-aws/action/suspend'
|
7
|
+
require 'vagrant-aws/action/resume'
|
8
|
+
require 'vagrant-aws/action/create_image'
|
9
|
+
require 'vagrant-aws/action/deregister_image'
|
10
|
+
|
11
|
+
module VagrantAWS
|
12
|
+
|
13
|
+
Vagrant::Action.register(:aws_provision, Vagrant::Action::Builder.new do
|
14
|
+
use Action::PopulateSSH
|
15
|
+
use Action::PrepareProvisioners
|
16
|
+
use Vagrant::Action[:provision]
|
17
|
+
end)
|
18
|
+
|
19
|
+
Vagrant::Action.register(:aws_up, Vagrant::Action::Builder.new do
|
20
|
+
use Action::CreateSSHKey
|
21
|
+
use Action::Create
|
22
|
+
use Vagrant::Action[:aws_provision]
|
23
|
+
end)
|
24
|
+
|
25
|
+
Vagrant::Action.register(:aws_destroy, Vagrant::Action::Builder.new do
|
26
|
+
use Action::Terminate
|
27
|
+
end)
|
28
|
+
|
29
|
+
Vagrant::Action.register(:aws_suspend, Vagrant::Action::Builder.new do
|
30
|
+
use Action::Suspend
|
31
|
+
end)
|
32
|
+
|
33
|
+
Vagrant::Action.register(:aws_resume, Vagrant::Action::Builder.new do
|
34
|
+
use Action::Resume
|
35
|
+
end)
|
36
|
+
|
37
|
+
Vagrant::Action.register(:aws_create_image, Vagrant::Action::Builder.new do
|
38
|
+
use Action::CreateImage
|
39
|
+
use Vagrant::Action::VM::Package
|
40
|
+
end)
|
41
|
+
|
42
|
+
Vagrant::Action.register(:aws_add_image, Vagrant::Action::Builder.new do
|
43
|
+
use Vagrant::Action::Box::Download
|
44
|
+
use Vagrant::Action::Box::Unpackage
|
45
|
+
end)
|
46
|
+
|
47
|
+
Vagrant::Action.register(:aws_remove_image, Vagrant::Action::Builder.new do
|
48
|
+
use Action::DeregisterImage
|
49
|
+
use Vagrant::Action::Box::Destroy
|
50
|
+
end)
|
51
|
+
|
52
|
+
|
53
|
+
end
|