vagrant-windows-hyperv 1.0.1
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 -0
- data/.gitignore +19 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +7 -0
- data/README.md +89 -0
- data/Rakefile +29 -0
- data/example_box/metadata.json +1 -0
- data/lib/vagrant-windows-hyperv.rb +38 -0
- data/lib/vagrant-windows-hyperv/action.rb +81 -0
- data/lib/vagrant-windows-hyperv/action/export.rb +54 -0
- data/lib/vagrant-windows-hyperv/action/package.rb +21 -0
- data/lib/vagrant-windows-hyperv/action/rdp.rb +49 -0
- data/lib/vagrant-windows-hyperv/action/setup_package_files.rb +55 -0
- data/lib/vagrant-windows-hyperv/command/rdp/command.rb +22 -0
- data/lib/vagrant-windows-hyperv/communication/powershell.rb +37 -0
- data/lib/vagrant-windows-hyperv/driver.rb +89 -0
- data/lib/vagrant-windows-hyperv/errors.rb +31 -0
- data/lib/vagrant-windows-hyperv/guest/cap/halt.rb +19 -0
- data/lib/vagrant-windows-hyperv/guest/windows.rb +25 -0
- data/lib/vagrant-windows-hyperv/monkey_patch/action/provision.rb +32 -0
- data/lib/vagrant-windows-hyperv/monkey_patch/machine.rb +22 -0
- data/lib/vagrant-windows-hyperv/monkey_patch/plugins/synced_folders/smb/synced_folders.rb +55 -0
- data/lib/vagrant-windows-hyperv/monkey_patch/util/powershell.rb +37 -0
- data/lib/vagrant-windows-hyperv/plugin.rb +85 -0
- data/lib/vagrant-windows-hyperv/provider.rb +30 -0
- data/lib/vagrant-windows-hyperv/provisioner/chef_solo.rb +181 -0
- data/lib/vagrant-windows-hyperv/provisioner/puppet.rb +99 -0
- data/lib/vagrant-windows-hyperv/provisioner/shell.rb +81 -0
- data/lib/vagrant-windows-hyperv/scripts/check_winrm.ps1 +41 -0
- data/lib/vagrant-windows-hyperv/scripts/export_vm.ps1 +31 -0
- data/lib/vagrant-windows-hyperv/scripts/file_sync.ps1 +145 -0
- data/lib/vagrant-windows-hyperv/scripts/host_info.ps1 +25 -0
- data/lib/vagrant-windows-hyperv/scripts/hyperv_manager.ps1 +36 -0
- data/lib/vagrant-windows-hyperv/scripts/run_in_remote.ps1 +31 -0
- data/lib/vagrant-windows-hyperv/scripts/upload_file.ps1 +95 -0
- data/lib/vagrant-windows-hyperv/scripts/utils/create_session.ps1 +34 -0
- data/lib/vagrant-windows-hyperv/scripts/utils/write_messages.ps1 +18 -0
- data/lib/vagrant-windows-hyperv/version.rb +10 -0
- data/locales/en.yml +15 -0
- data/spec/hyper-v/config_spec.rb +36 -0
- data/spec/hyper-v/spec_helper.rb +9 -0
- data/templates/provisioners/chef-solo/solo.erb +51 -0
- data/vagrant-windows-hyperv.gemspec +63 -0
- data/vagrantfile_examples/Vagrantfile_linux +23 -0
- data/vagrantfile_examples/Vagrantfile_windows +24 -0
- metadata +171 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
module VagrantPlugins
|
7
|
+
module VagrantHyperV
|
8
|
+
class Command < Vagrant.plugin("2", :command)
|
9
|
+
def self.synopsis
|
10
|
+
"opens a RDP session for a vagrant machine"
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute
|
14
|
+
with_target_vms do |vm|
|
15
|
+
vm.action(:rdp)
|
16
|
+
end
|
17
|
+
# Success, exit status 0
|
18
|
+
0
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
module VagrantPlugins
|
7
|
+
module VagrantHyperV
|
8
|
+
module Communicator
|
9
|
+
class PowerShell < Vagrant.plugin("2", :communicator)
|
10
|
+
def initialize(machine)
|
11
|
+
@machine = machine
|
12
|
+
end
|
13
|
+
|
14
|
+
def wait_for_ready(timeout)
|
15
|
+
ready?
|
16
|
+
end
|
17
|
+
|
18
|
+
def ready?
|
19
|
+
# Return True when the guest has enabled WinRM
|
20
|
+
# In this case we can try any remote PowerShell commands to see if
|
21
|
+
# further vagrant can be carried out using this communication
|
22
|
+
if !@winrm_status
|
23
|
+
status = false
|
24
|
+
response = @machine.provider.driver.check_winrm
|
25
|
+
@winrm_status = response["message"] == "Running"
|
26
|
+
raise Errors::WinRMNotReady if !@winrm_status
|
27
|
+
end
|
28
|
+
@winrm_status
|
29
|
+
end
|
30
|
+
|
31
|
+
def test(command, opts=nil)
|
32
|
+
true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
require "json"
|
7
|
+
require "#{Vagrant::source_root}/plugins/providers/hyperv/driver"
|
8
|
+
|
9
|
+
module VagrantPlugins
|
10
|
+
module VagrantHyperV
|
11
|
+
class Driver < VagrantPlugins::HyperV::Driver
|
12
|
+
|
13
|
+
def initialize(machine)
|
14
|
+
@vm_id = machine.id
|
15
|
+
@machine = machine
|
16
|
+
end
|
17
|
+
|
18
|
+
def ssh_info
|
19
|
+
@ssh_info ||= @machine.ssh_info
|
20
|
+
end
|
21
|
+
|
22
|
+
def remote_credentials
|
23
|
+
@remote_credentials ||= { guest_ip: ssh_info[:host],
|
24
|
+
username: ssh_info[:username],
|
25
|
+
password: "vagrant" }
|
26
|
+
end
|
27
|
+
|
28
|
+
def run_remote_ps(command, &block)
|
29
|
+
options = remote_credentials.merge(command: command)
|
30
|
+
script_path = local_script_path('run_in_remote.ps1')
|
31
|
+
# FIXME: Vagrant's core driver method which executes PowerShell,
|
32
|
+
# need to take an &block so that this piece of code can be avoided.
|
33
|
+
ps_options = []
|
34
|
+
options.each do |key, value|
|
35
|
+
ps_options << "-#{key}"
|
36
|
+
ps_options << "'#{value}'"
|
37
|
+
end
|
38
|
+
ps_options << "-ErrorAction" << "Stop"
|
39
|
+
opts = { notify: [:stdout, :stderr, :stdin] }
|
40
|
+
Vagrant::Util::PowerShell.execute(script_path, *ps_options, **opts, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def export_vm_to(path)
|
44
|
+
options = {
|
45
|
+
vm_id: vm_id,
|
46
|
+
path: windows_path(path)
|
47
|
+
}
|
48
|
+
script_path = local_script_path('export_vm.ps1')
|
49
|
+
execute(script_path, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def upload(from, to)
|
53
|
+
options = {
|
54
|
+
vm_id: vm_id,
|
55
|
+
host_path: windows_path(from),
|
56
|
+
guest_path: windows_path(to)
|
57
|
+
}.merge(remote_credentials)
|
58
|
+
script_path = local_script_path('upload_file.ps1')
|
59
|
+
execute(script_path, options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def check_winrm
|
63
|
+
script_path = local_script_path('check_winrm.ps1')
|
64
|
+
execute(script_path, remote_credentials)
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_host_ip
|
68
|
+
script_path = local_script_path('host_info.ps1')
|
69
|
+
execute(script_path, {})
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
def local_script_path(path)
|
75
|
+
lib_path = Pathname.new(File.expand_path("../scripts", __FILE__))
|
76
|
+
lib_path.join(path).to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
def windows_path(path)
|
80
|
+
if path
|
81
|
+
path = path.gsub("/","\\")
|
82
|
+
path = "c:#{path}" if path =~ /^\\/
|
83
|
+
end
|
84
|
+
path
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
module VagrantPlugins
|
7
|
+
module VagrantHyperV
|
8
|
+
module Errors
|
9
|
+
class VagrantHyperVError < Vagrant::Errors::VagrantError
|
10
|
+
error_namespace("vagrant_win_hyperv.errors")
|
11
|
+
end
|
12
|
+
|
13
|
+
class SSHNotAvailable < VagrantHyperVError
|
14
|
+
error_key(:ssh_not_available)
|
15
|
+
end
|
16
|
+
|
17
|
+
class RDPNotAvailable < VagrantHyperVError
|
18
|
+
error_key(:rdp_not_available)
|
19
|
+
end
|
20
|
+
|
21
|
+
class InvalidSMBCredentials < VagrantHyperVError
|
22
|
+
error_key(:no_smb_credentials)
|
23
|
+
end
|
24
|
+
|
25
|
+
class WinRMNotReady < VagrantHyperVError
|
26
|
+
error_key(:win_rm_not_available)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
module VagrantPlugins
|
7
|
+
module VagrantHyperV
|
8
|
+
module Guest
|
9
|
+
module Cap
|
10
|
+
class Halt
|
11
|
+
def self.halt(machine)
|
12
|
+
machine.provider.driver.run_remote_ps("shutdown -a")
|
13
|
+
machine.provider.driver.run_remote_ps("shutdown /s /t 1")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
require "vagrant"
|
6
|
+
|
7
|
+
module VagrantPlugins
|
8
|
+
module VagrantHyperV
|
9
|
+
module Guest
|
10
|
+
class Windows < Vagrant.plugin("2", :guest)
|
11
|
+
attr_reader :machin
|
12
|
+
|
13
|
+
def initialize(machine = nil)
|
14
|
+
super(machine) unless machine == nil
|
15
|
+
@machine = machine
|
16
|
+
end
|
17
|
+
|
18
|
+
def halt
|
19
|
+
VagrantHyperV::VagrantPlugins::Guest::Cap::Halt.halt(@machine)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
require "#{Vagrant::source_root}/lib/vagrant/action/builtin/provision"
|
7
|
+
|
8
|
+
module Vagrant
|
9
|
+
module Action
|
10
|
+
module Builtin
|
11
|
+
class Provision
|
12
|
+
|
13
|
+
alias_method :original_run_provisioner, :run_provisioner
|
14
|
+
# Override this method from core vagrant, here we branch out the provision for windows
|
15
|
+
def run_provisioner(env)
|
16
|
+
if env[:machine].config.vm.guest == :windows
|
17
|
+
case env[:provisioner].class.to_s
|
18
|
+
when "VagrantPlugins::Shell::Provisioner"
|
19
|
+
VagrantPlugins::VagrantHyperV::Provisioner::Shell.new(env).provision_for_windows
|
20
|
+
when "VagrantPlugins::Puppet::Provisioner::Puppet"
|
21
|
+
VagrantPlugins::VagrantHyperV::Provisioner::Puppet.new(env).provision_for_windows
|
22
|
+
when "VagrantPlugins::Chef::Provisioner::ChefSolo"
|
23
|
+
VagrantPlugins::VagrantHyperV::Provisioner::ChefSolo.new(env).provision_for_windows
|
24
|
+
end
|
25
|
+
else
|
26
|
+
original_run_provisioner(env)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
module Vagrant
|
7
|
+
class Machine
|
8
|
+
|
9
|
+
alias_method :original_communicate, :communicate
|
10
|
+
|
11
|
+
def communicate
|
12
|
+
unless @communicator
|
13
|
+
if @config.vm.guest == :windows
|
14
|
+
@communicator = VagrantPlugins::VagrantHyperV::Communicator::PowerShell.new(self)
|
15
|
+
else
|
16
|
+
@communicator = original_communicate
|
17
|
+
end
|
18
|
+
end
|
19
|
+
@communicator
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
require "#{Vagrant::source_root}/plugins/synced_folders/smb/synced_folder"
|
7
|
+
|
8
|
+
module VagrantPlugins
|
9
|
+
module SyncedFolderSMB
|
10
|
+
class SyncedFolder < Vagrant.plugin("2", :synced_folder)
|
11
|
+
|
12
|
+
alias_method :original_enable, :enable
|
13
|
+
|
14
|
+
def enable(machine, folders, nfsopts)
|
15
|
+
response = machine.provider.driver.get_host_ip
|
16
|
+
host_ip = response["host_ip"]
|
17
|
+
if machine.config.vm.guest == :windows
|
18
|
+
folders.each do |id, data|
|
19
|
+
machine.ui.output "#{data[:hostpath]} ==>"
|
20
|
+
machine.ui.output "\\\\#{host_ip}\\#{data[:smb_id]}"
|
21
|
+
end
|
22
|
+
guest_startup_script(machine, folders, host_ip)
|
23
|
+
else
|
24
|
+
original_enable(machine, folders, nfsopts)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def guest_startup_script(machine, folders, host_ip)
|
29
|
+
# Upload a startup script to VM,
|
30
|
+
# This script will authenticate the Network share with the host, and
|
31
|
+
# the guest can access the share path from a RDP session
|
32
|
+
file = Tempfile.new(['vagrant-smb-auth', '.ps1'])
|
33
|
+
begin
|
34
|
+
folders.each do |id, data|
|
35
|
+
smb_map_command = "New-SmbMapping"
|
36
|
+
smb_map_command += " -RemotePath \\\\#{host_ip}\\#{data[:smb_id]}"
|
37
|
+
smb_map_command += " -UserName #{@creds[:username]}"
|
38
|
+
smb_map_command += " -Password #{@creds[:password]}"
|
39
|
+
file.puts(smb_map_command)
|
40
|
+
end
|
41
|
+
file.fsync
|
42
|
+
file.close
|
43
|
+
ensure
|
44
|
+
file.close
|
45
|
+
end
|
46
|
+
machine.provider.driver.upload(file.path.to_s, "c:\\tmp\\vagrant-smb-auth.ps1")
|
47
|
+
# Invoke Remote Schedule task command in VM
|
48
|
+
command = 'schtasks /create /sc ONLOGON /tn vagrant-smb-auth /tr \"powershell c:\tmp\vagrant-smb-auth.ps1\"'
|
49
|
+
machine.provider.driver.run_remote_ps(command)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
require "#{Vagrant::source_root}/lib/vagrant/util/which"
|
7
|
+
require "#{Vagrant::source_root}/lib/vagrant/util/subprocess"
|
8
|
+
|
9
|
+
module Vagrant
|
10
|
+
module Util
|
11
|
+
# Executes PowerShell scripts.
|
12
|
+
#
|
13
|
+
# This is primarily a convenience wrapper around Subprocess that
|
14
|
+
# properly sets powershell flags for you.
|
15
|
+
class PowerShell
|
16
|
+
# Monkey patch to fix a bug with Vagrant 1.5.1.
|
17
|
+
# https://github.com/mitchellh/vagrant/issues/3192.
|
18
|
+
# This has been fixed in 1.5.2. by
|
19
|
+
# https://github.com/jyggen/vagrant/commit/d7320399e2497aae9b9c3fa83d94b7210d21bfb5
|
20
|
+
def self.execute(path, *args, **opts, &block)
|
21
|
+
command = [
|
22
|
+
"powershell",
|
23
|
+
"-NoProfile",
|
24
|
+
"-ExecutionPolicy", "Bypass",
|
25
|
+
"&('#{path}')",
|
26
|
+
args
|
27
|
+
].flatten
|
28
|
+
|
29
|
+
# Append on the options hash since Subprocess doesn't use
|
30
|
+
# Ruby 2.0 style options yet.
|
31
|
+
command << opts
|
32
|
+
|
33
|
+
Subprocess.execute(*command, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
#-------------------------------------------------------------------------
|
2
|
+
# Copyright (c) Microsoft Open Technologies, Inc.
|
3
|
+
# All Rights Reserved. Licensed under the Apache 2.0 License.
|
4
|
+
#--------------------------------------------------------------------------
|
5
|
+
|
6
|
+
# This is a sanity check to make sure no one is attempting to install
|
7
|
+
# this into an early Vagrant version.
|
8
|
+
if Vagrant::VERSION < "1.5.0"
|
9
|
+
raise "The Vagrant Hyper-V plugin is only compatible with Vagrant 1.5+"
|
10
|
+
end
|
11
|
+
|
12
|
+
module VagrantPlugins
|
13
|
+
module VagrantHyperV
|
14
|
+
class Plugin < Vagrant.plugin("2")
|
15
|
+
name "VagrantHyperV"
|
16
|
+
description <<-DESC
|
17
|
+
This plugin installs a provider that allows Vagrant to manage
|
18
|
+
windows guest machines in Hyper-V.
|
19
|
+
DESC
|
20
|
+
|
21
|
+
command "rdp" do
|
22
|
+
require_relative "command/rdp/command"
|
23
|
+
Command
|
24
|
+
end
|
25
|
+
|
26
|
+
provider(:hyperv) do
|
27
|
+
# Setup logging and i18n
|
28
|
+
# setup_logging
|
29
|
+
setup_i18n_hyperv
|
30
|
+
|
31
|
+
# Return the provider
|
32
|
+
require_relative "provider"
|
33
|
+
Provider
|
34
|
+
end
|
35
|
+
|
36
|
+
guest(:windows) do
|
37
|
+
require_relative "guest/windows"
|
38
|
+
Guest::Windows
|
39
|
+
end
|
40
|
+
|
41
|
+
guest_capability(:windows, :halt) do
|
42
|
+
require_relative "guest/cap/halt"
|
43
|
+
Guest::Cap::Halt
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
# This initializes the internationalization strings.
|
49
|
+
def self.setup_i18n_hyperv
|
50
|
+
I18n.load_path << File.expand_path("locales/en.yml", VagrantHyperV.source_root)
|
51
|
+
I18n.load_path << File.expand_path(
|
52
|
+
"templates/locales/providers_hyperv.yml", Vagrant.source_root)
|
53
|
+
I18n.reload!
|
54
|
+
end
|
55
|
+
|
56
|
+
# This sets up our log level to be whatever VAGRANT_LOG is.
|
57
|
+
def self.setup_logging
|
58
|
+
require "log4r"
|
59
|
+
level = nil
|
60
|
+
begin
|
61
|
+
level = Log4r.const_get(ENV["VAGRANT_LOG"].upcase)
|
62
|
+
rescue NameError
|
63
|
+
# This means that the logging constant wasn't found,
|
64
|
+
# which is fine. We just keep `level` as `nil`. But
|
65
|
+
# we tell the user.
|
66
|
+
level = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# Some constants, such as "true" resolve to booleans, so the
|
70
|
+
# above error checking doesn't catch it. This will check to make
|
71
|
+
# sure that the log level is an integer, as Log4r requires.
|
72
|
+
level = nil if !level.is_a?(Integer)
|
73
|
+
|
74
|
+
# Set the logging level on all "vagrant" namespaced
|
75
|
+
# logs as long as we have a valid level.
|
76
|
+
if level
|
77
|
+
logger = Log4r::Logger.new("vagrant_hyperv")
|
78
|
+
logger.outputters = Log4r::Outputter.stderr
|
79
|
+
logger.level = level
|
80
|
+
logger = nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|