rvc 1.6.0 → 1.7.0
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.
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/bin/rvc +46 -70
- data/devel/test-dependencies.sh +4 -0
- data/lib/rvc.rb +5 -6
- data/lib/rvc/command.rb +65 -0
- data/lib/rvc/command_slate.rb +112 -0
- data/lib/rvc/completion.rb +89 -58
- data/lib/rvc/connection.rb +48 -0
- data/lib/rvc/extensions/DistributedVirtualPortgroup.rb +1 -1
- data/lib/rvc/extensions/DistributedVirtualSwitch.rb +3 -3
- data/lib/rvc/extensions/HostSystem.rb +90 -0
- data/lib/rvc/extensions/VirtualMachine.rb +37 -7
- data/lib/rvc/field.rb +59 -12
- data/lib/rvc/fs.rb +34 -4
- data/lib/rvc/inventory.rb +5 -1
- data/lib/rvc/modules/alarm.rb +2 -0
- data/lib/rvc/modules/basic.rb +66 -61
- data/lib/rvc/modules/cluster.rb +117 -22
- data/lib/rvc/modules/connection.rb +40 -0
- data/lib/rvc/modules/core.rb +4 -16
- data/lib/rvc/modules/datacenter.rb +2 -0
- data/lib/rvc/modules/datastore.rb +11 -78
- data/lib/rvc/modules/device.rb +40 -5
- data/lib/rvc/modules/diagnostics.rb +169 -0
- data/lib/rvc/modules/esxcli.rb +9 -5
- data/lib/rvc/modules/find.rb +5 -3
- data/lib/rvc/modules/host.rb +46 -3
- data/lib/rvc/modules/issue.rb +2 -0
- data/lib/rvc/modules/mark.rb +5 -3
- data/lib/rvc/modules/perf.rb +99 -33
- data/lib/rvc/modules/permissions.rb +2 -0
- data/lib/rvc/modules/resource_pool.rb +2 -0
- data/lib/rvc/modules/role.rb +3 -1
- data/lib/rvc/modules/snapshot.rb +12 -4
- data/lib/rvc/modules/statsinterval.rb +13 -11
- data/lib/rvc/modules/vds.rb +67 -10
- data/lib/rvc/modules/vim.rb +19 -53
- data/lib/rvc/modules/vm.rb +27 -6
- data/lib/rvc/modules/vm_guest.rb +490 -0
- data/lib/rvc/modules/vmrc.rb +60 -32
- data/lib/rvc/modules/vnc.rb +2 -0
- data/lib/rvc/namespace.rb +114 -0
- data/lib/rvc/option_parser.rb +12 -15
- data/lib/rvc/readline-ffi.rb +4 -1
- data/lib/rvc/ruby_evaluator.rb +84 -0
- data/lib/rvc/shell.rb +68 -83
- data/lib/rvc/uri_parser.rb +59 -0
- data/lib/rvc/util.rb +134 -29
- data/lib/rvc/{extensions/PerfCounterInfo.rb → version.rb} +2 -4
- data/lib/rvc/{memory_session.rb → vim.rb} +10 -32
- data/test/modules/foo.rb +9 -0
- data/test/modules/foo/bar.rb +9 -0
- data/test/test_completion.rb +17 -0
- data/test/test_fs.rb +9 -11
- data/test/test_help.rb +46 -0
- data/test/test_helper.rb +12 -0
- data/test/test_metric.rb +1 -2
- data/test/test_modules.rb +38 -0
- data/test/test_parse_path.rb +1 -2
- data/test/test_shell.rb +138 -0
- data/test/test_uri.rb +34 -0
- metadata +115 -81
- data/lib/rvc/extensions/PerformanceManager.rb +0 -83
- data/lib/rvc/filesystem_session.rb +0 -101
- data/lib/rvc/modules.rb +0 -138
data/lib/rvc/modules/vmrc.rb
CHANGED
@@ -18,40 +18,59 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
+
require 'rvc/vim'
|
22
|
+
|
21
23
|
require 'tmpdir'
|
22
24
|
require 'digest/sha2'
|
23
25
|
require 'zip'
|
24
26
|
require 'rbconfig'
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
VMRC_BIN = nil
|
39
|
-
$stderr.puts "No VMRC available for OS #{RbConfig::CONFIG['host_os']}"
|
28
|
+
VMRC_CHECKSUMS = {
|
29
|
+
"i686-linux" => "b8f11c92853502c3dd208da79514a66d2dd4734b8564aceb9952333037859d04",
|
30
|
+
"x86_64-linux" => "86ec4bc6f23da0c33045d9bf48d9fe66ab2f426b523d8b37531646819891bf54",
|
31
|
+
"i686-mswin" => "f8455f0df038fbc8e817e4381af44fa2141496cb4e2b61f505f75bc447841949",
|
32
|
+
}
|
33
|
+
|
34
|
+
PACKAGE_VERSION = 'A'
|
35
|
+
ARCH = RbConfig::CONFIG['arch']
|
36
|
+
ON_WINDOWS = (RbConfig::CONFIG['host_os'] =~ /(mswin|mingw)/) != nil
|
37
|
+
|
38
|
+
def vmrc_url arch
|
39
|
+
"http://cloud.github.com/downloads/vmware/rvc/vmware-vmrc-public-#{arch}-#{PACKAGE_VERSION}.zip"
|
40
40
|
end
|
41
41
|
|
42
|
-
|
43
|
-
|
42
|
+
def local_vmrc_dir arch
|
43
|
+
File.join(Dir.tmpdir, "vmware-vmrc-#{arch}-#{Process.uid}-#{PACKAGE_VERSION}")
|
44
|
+
end
|
45
|
+
|
46
|
+
def check_installed
|
47
|
+
File.exists? local_vmrc_dir(ARCH)
|
48
|
+
end
|
44
49
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
50
|
+
def find_vmrc arch, version
|
51
|
+
path = if version == '3.0.0'
|
52
|
+
basename = ON_WINDOWS ? 'vmware-vmrc.exe' : 'vmware-vmrc'
|
53
|
+
File.join(local_vmrc_dir(arch), version, 'plugins', basename)
|
54
|
+
else
|
55
|
+
fail "VMRC5 not yet supported on win32" if ON_WINDOWS
|
56
|
+
File.join(local_vmrc_dir(arch), version, 'vmware-vmrc-5.0', 'run.sh')
|
57
|
+
end
|
48
58
|
File.exists?(path) && path
|
49
59
|
end
|
50
60
|
|
51
|
-
def
|
52
|
-
|
61
|
+
def choose_vmrc_version vim_version
|
62
|
+
if vim_version >= '5.1.0'
|
63
|
+
'5.0.0'
|
64
|
+
else
|
65
|
+
'3.0.0'
|
66
|
+
end
|
53
67
|
end
|
54
68
|
|
69
|
+
fail unless choose_vmrc_version('4.1.0') == '3.0.0'
|
70
|
+
fail unless choose_vmrc_version('5.0.1') == '3.0.0'
|
71
|
+
fail unless choose_vmrc_version('5.1.0') == '5.0.0'
|
72
|
+
fail unless choose_vmrc_version('6.0.0') == '5.0.0'
|
73
|
+
|
55
74
|
|
56
75
|
opts :view do
|
57
76
|
summary "Spawn a VMRC"
|
@@ -65,10 +84,13 @@ rvc_alias :view, :vmrc
|
|
65
84
|
rvc_alias :view, :v
|
66
85
|
|
67
86
|
def view vms, opts
|
68
|
-
|
87
|
+
conn = single_connection vms
|
88
|
+
vim_version = conn.serviceContent.about.version
|
89
|
+
vmrc_version = choose_vmrc_version vim_version
|
90
|
+
unless vmrc = find_vmrc(ARCH, vmrc_version)
|
69
91
|
if opts[:install]
|
70
92
|
install
|
71
|
-
vmrc = find_vmrc
|
93
|
+
vmrc = find_vmrc(ARCH, vmrc_version)
|
72
94
|
else
|
73
95
|
err "VMRC not found. You may need to run vmrc.install."
|
74
96
|
end
|
@@ -82,8 +104,7 @@ def view vms, opts
|
|
82
104
|
end
|
83
105
|
end
|
84
106
|
|
85
|
-
|
86
|
-
when /mswin/, /mingw/
|
107
|
+
if ON_WINDOWS
|
87
108
|
def spawn_vmrc vmrc, moref, host, ticket
|
88
109
|
err "Ruby 1.9 required" unless Process.respond_to? :spawn
|
89
110
|
Process.spawn vmrc, '-h', host, '-p', ticket, '-M', moref,
|
@@ -109,10 +130,12 @@ opts :install do
|
|
109
130
|
end
|
110
131
|
|
111
132
|
def install
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
133
|
+
err "No VMRC available for architecture #{ARCH}" unless VMRC_CHECKSUMS.member? ARCH
|
134
|
+
zip_filename = "#{local_vmrc_dir(ARCH)}.zip"
|
135
|
+
url = vmrc_url ARCH
|
136
|
+
download url, zip_filename
|
137
|
+
verify zip_filename, VMRC_CHECKSUMS[ARCH]
|
138
|
+
extract zip_filename, local_vmrc_dir(ARCH)
|
116
139
|
puts "VMRC was installed successfully."
|
117
140
|
end
|
118
141
|
|
@@ -136,6 +159,7 @@ def download url_str, dest
|
|
136
159
|
io.write segment
|
137
160
|
end
|
138
161
|
end
|
162
|
+
res.value
|
139
163
|
end
|
140
164
|
rescue Exception
|
141
165
|
err "Error downloading VMRC: #{$!.class}: #{$!.message}"
|
@@ -143,9 +167,13 @@ def download url_str, dest
|
|
143
167
|
end
|
144
168
|
|
145
169
|
def verify filename, expected_hash
|
146
|
-
|
147
|
-
|
148
|
-
|
170
|
+
if expected_hash == :nocheck
|
171
|
+
puts "WARNING: skipping hash check"
|
172
|
+
else
|
173
|
+
puts "Checking integrity..."
|
174
|
+
hexdigest = Digest::SHA256.file(filename).hexdigest
|
175
|
+
err "Hash mismatch: expected #{expected_hash}, found #{hexdigest}" if hexdigest != expected_hash
|
176
|
+
end
|
149
177
|
end
|
150
178
|
|
151
179
|
def extract src, dst
|
data/lib/rvc/modules/vnc.rb
CHANGED
@@ -0,0 +1,114 @@
|
|
1
|
+
# Copyright (c) 2011 VMware, Inc. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'rvc/command_slate'
|
22
|
+
|
23
|
+
module RVC
|
24
|
+
|
25
|
+
class Namespace
|
26
|
+
attr_reader :name, :shell, :parent, :slate, :namespaces, :commands, :aliases
|
27
|
+
|
28
|
+
def inspect
|
29
|
+
"#<RVC::Namespace:#{name}>"
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize name, shell, parent
|
33
|
+
@name = name
|
34
|
+
@shell = shell
|
35
|
+
@parent = parent
|
36
|
+
@slate = CommandSlate.new self
|
37
|
+
@namespaces = {}
|
38
|
+
@commands = {}
|
39
|
+
@aliases = {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def load_code code, filename
|
43
|
+
@slate.instance_eval code, filename
|
44
|
+
end
|
45
|
+
|
46
|
+
def child_namespace name
|
47
|
+
if ns = namespaces[name]
|
48
|
+
return ns
|
49
|
+
else
|
50
|
+
namespaces[name] = Namespace.new(name, shell, self)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def lookup cmdpath, accept=Command
|
55
|
+
if cmdpath.empty?
|
56
|
+
if accept == Command
|
57
|
+
return nil
|
58
|
+
elsif accept == Namespace
|
59
|
+
return self
|
60
|
+
end
|
61
|
+
elsif cmdpath.length == 1 and accept == Command
|
62
|
+
sym = cmdpath[0]
|
63
|
+
if @aliases.member? sym
|
64
|
+
@shell.cmds.lookup @aliases[sym], accept
|
65
|
+
else
|
66
|
+
@commands[sym]
|
67
|
+
end
|
68
|
+
else
|
69
|
+
sym = cmdpath[0]
|
70
|
+
child = @namespaces[sym]
|
71
|
+
return nil if child == nil
|
72
|
+
child.lookup cmdpath[1..-1], accept
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def [] sym
|
77
|
+
@namespaces[sym]
|
78
|
+
end
|
79
|
+
|
80
|
+
def method_missing sym, *args
|
81
|
+
if cmd = @commands[sym]
|
82
|
+
cmd.invoke *args
|
83
|
+
elsif args.empty? and x = self[sym]
|
84
|
+
x
|
85
|
+
else
|
86
|
+
super
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def load_module_dir dir, verbose
|
91
|
+
puts "loading module directory #{dir}" if verbose
|
92
|
+
Dir.foreach(dir) do |f|
|
93
|
+
path = File.join(dir, f)
|
94
|
+
if f[0..0] == '.'
|
95
|
+
next
|
96
|
+
elsif File.directory? path
|
97
|
+
ns_name = f.to_sym
|
98
|
+
child_namespace(ns_name).load_module_dir(path, verbose)
|
99
|
+
elsif f =~ /\.rb$/
|
100
|
+
ns_name = f[0...-3].to_sym
|
101
|
+
puts "loading #{ns_name} from #{path}" if verbose
|
102
|
+
begin
|
103
|
+
code = File.read path
|
104
|
+
child_namespace(ns_name).load_code(code, path)
|
105
|
+
rescue
|
106
|
+
puts "#{$!.class} while loading #{f}: #{$!.message}"
|
107
|
+
$!.backtrace.each { |x| puts x }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
data/lib/rvc/option_parser.rb
CHANGED
@@ -19,21 +19,23 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require 'trollop'
|
22
|
+
require 'set'
|
23
|
+
|
24
|
+
module RVC
|
22
25
|
|
23
26
|
begin
|
24
27
|
require 'chronic'
|
25
|
-
|
28
|
+
HAVE_CHRONIC = true
|
26
29
|
rescue LoadError
|
27
|
-
|
30
|
+
HAVE_CHRONIC = false
|
28
31
|
end
|
29
32
|
|
30
|
-
module RVC
|
31
|
-
|
32
33
|
class OptionParser < Trollop::Parser
|
33
34
|
attr_reader :applicable
|
34
35
|
|
35
|
-
def initialize cmd, &b
|
36
|
+
def initialize cmd, fs, &b
|
36
37
|
@cmd = cmd
|
38
|
+
@fs = fs
|
37
39
|
@summary = nil
|
38
40
|
@args = []
|
39
41
|
@has_options = false
|
@@ -41,8 +43,8 @@ class OptionParser < Trollop::Parser
|
|
41
43
|
@seen_multi = false
|
42
44
|
@applicable = Set.new
|
43
45
|
super() do
|
44
|
-
opt :help, "Show this message", :short => 'h'
|
45
46
|
instance_eval &b
|
47
|
+
opt :help, "Show this message", :short => 'h'
|
46
48
|
end
|
47
49
|
end
|
48
50
|
|
@@ -97,7 +99,7 @@ class OptionParser < Trollop::Parser
|
|
97
99
|
|
98
100
|
@specs.each do |name,spec|
|
99
101
|
next unless klass = spec[:lookup] and path = opts[name]
|
100
|
-
opts[name] =
|
102
|
+
opts[name] = @fs.lookup_single! path, klass
|
101
103
|
end
|
102
104
|
|
103
105
|
argv = leftovers
|
@@ -136,10 +138,10 @@ class OptionParser < Trollop::Parser
|
|
136
138
|
|
137
139
|
def postprocess_arg x, spec
|
138
140
|
if spec[:lookup]
|
139
|
-
|
141
|
+
@fs.lookup!(x, spec[:lookup]).
|
140
142
|
tap { |a| RVC::Util.err "no matches for #{x.inspect}" if a.empty? }
|
141
143
|
elsif spec[:lookup_parent]
|
142
|
-
|
144
|
+
@fs.lookup!(File.dirname(x), spec[:lookup_parent]).
|
143
145
|
map { |y| [y, File.basename(x)] }.
|
144
146
|
tap { |a| RVC::Util.err "no matches for #{File.dirname(x).inspect}" if a.empty? }
|
145
147
|
else
|
@@ -163,16 +165,11 @@ end
|
|
163
165
|
class RawOptionParser
|
164
166
|
attr_reader :applicable
|
165
167
|
|
166
|
-
def initialize cmd
|
168
|
+
def initialize cmd
|
167
169
|
@cmd = cmd
|
168
|
-
@summary = summary
|
169
170
|
@applicable = []
|
170
171
|
end
|
171
172
|
|
172
|
-
def summary?
|
173
|
-
@summary
|
174
|
-
end
|
175
|
-
|
176
173
|
def parse args
|
177
174
|
[args, {}]
|
178
175
|
end
|
data/lib/rvc/readline-ffi.rb
CHANGED
@@ -19,8 +19,10 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require 'ffi'
|
22
|
+
require 'readline'
|
22
23
|
|
23
|
-
module RVC
|
24
|
+
module RVC
|
25
|
+
module ReadlineFFI
|
24
26
|
extend FFI::Library
|
25
27
|
libreadline = ENV['RVC_READLINE'] == nil ? 'readline.so' : ENV['RVC_READLINE']
|
26
28
|
ffi_lib libreadline
|
@@ -28,6 +30,7 @@ module RVC::ReadlineFFI
|
|
28
30
|
attach_variable :rl_char_is_quoted_p, :rl_char_is_quoted_p, :rl_linebuf_func_t
|
29
31
|
attach_variable :rl_line_buffer, :rl_line_buffer, :string
|
30
32
|
end
|
33
|
+
end
|
31
34
|
|
32
35
|
unless Readline.respond_to? :line_buffer
|
33
36
|
def Readline.line_buffer
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# Copyright (c) 2011 VMware, Inc. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'rvc/util'
|
22
|
+
|
23
|
+
module RVC
|
24
|
+
|
25
|
+
class RubyEvaluator
|
26
|
+
include RVC::Util
|
27
|
+
|
28
|
+
def initialize shell
|
29
|
+
@binding = toplevel
|
30
|
+
@shell = shell
|
31
|
+
end
|
32
|
+
|
33
|
+
def toplevel
|
34
|
+
binding
|
35
|
+
end
|
36
|
+
|
37
|
+
def do_eval input, file
|
38
|
+
begin
|
39
|
+
eval input, @binding, file
|
40
|
+
rescue Exception => e
|
41
|
+
bt = e.backtrace
|
42
|
+
bt = bt.reverse.drop_while { |x| !(x =~ /toplevel/) }.reverse
|
43
|
+
bt[-1].gsub! ':in `toplevel\'', '' if bt[-1]
|
44
|
+
e.set_backtrace bt
|
45
|
+
raise
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def this
|
50
|
+
@shell.fs.cur
|
51
|
+
end
|
52
|
+
|
53
|
+
def dc
|
54
|
+
@shell.fs.lookup("~").first
|
55
|
+
end
|
56
|
+
|
57
|
+
def conn
|
58
|
+
@shell.fs.lookup("~@").first
|
59
|
+
end
|
60
|
+
|
61
|
+
def rvc_exec command
|
62
|
+
@shell.eval_command command
|
63
|
+
end
|
64
|
+
|
65
|
+
def method_missing sym, *a
|
66
|
+
if a.empty?
|
67
|
+
if @shell.cmds.namespaces.member? sym
|
68
|
+
@shell.cmds.namespaces[sym]
|
69
|
+
elsif sym.to_s =~ /_?([\w\d]+)(!?)/ && objs = @shell.fs.marks[$1]
|
70
|
+
if $2 == '!'
|
71
|
+
objs
|
72
|
+
else
|
73
|
+
objs.first
|
74
|
+
end
|
75
|
+
else
|
76
|
+
super
|
77
|
+
end
|
78
|
+
else
|
79
|
+
super
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|