vmfloaty 0.8.1 → 0.10.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.
- checksums.yaml +5 -5
- data/README.md +96 -55
- data/bin/floaty +2 -1
- data/lib/vmfloaty.rb +60 -53
- data/lib/vmfloaty/abs.rb +318 -0
- data/lib/vmfloaty/auth.rb +14 -22
- data/lib/vmfloaty/conf.rb +3 -2
- data/lib/vmfloaty/errors.rb +6 -4
- data/lib/vmfloaty/http.rb +14 -25
- data/lib/vmfloaty/nonstandard_pooler.rb +15 -31
- data/lib/vmfloaty/pooler.rb +64 -55
- data/lib/vmfloaty/service.rb +25 -17
- data/lib/vmfloaty/ssh.rb +25 -25
- data/lib/vmfloaty/utils.rb +103 -97
- data/lib/vmfloaty/version.rb +3 -1
- data/spec/spec_helper.rb +13 -0
- data/spec/vmfloaty/abs/auth_spec.rb +84 -0
- data/spec/vmfloaty/abs_spec.rb +126 -0
- data/spec/vmfloaty/auth_spec.rb +39 -43
- data/spec/vmfloaty/nonstandard_pooler_spec.rb +132 -146
- data/spec/vmfloaty/pooler_spec.rb +121 -101
- data/spec/vmfloaty/service_spec.rb +17 -17
- data/spec/vmfloaty/ssh_spec.rb +49 -0
- data/spec/vmfloaty/utils_spec.rb +123 -98
- data/spec/vmfloaty/vmfloaty_services_spec.rb +39 -0
- metadata +38 -22
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'vmfloaty/errors'
|
2
4
|
require 'vmfloaty/http'
|
3
5
|
require 'faraday'
|
@@ -15,27 +17,17 @@ class NonstandardPooler
|
|
15
17
|
os_filter ? os_list.select { |i| i[/#{os_filter}/] } : os_list
|
16
18
|
end
|
17
19
|
|
18
|
-
def self.list_active(verbose, url, token)
|
20
|
+
def self.list_active(verbose, url, token, _user)
|
19
21
|
status = Auth.token_status(verbose, url, token)
|
20
22
|
status['reserved_hosts'] || []
|
21
23
|
end
|
22
24
|
|
23
|
-
def self.retrieve(verbose, os_type, token, url)
|
25
|
+
def self.retrieve(verbose, os_type, token, url, _user, _options)
|
24
26
|
conn = Http.get_conn(verbose, url)
|
25
27
|
conn.headers['X-AUTH-TOKEN'] = token if token
|
26
28
|
|
27
|
-
os_string = ''
|
28
|
-
|
29
|
-
num.times do |_i|
|
30
|
-
os_string << os + '+'
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
os_string = os_string.chomp('+')
|
35
|
-
|
36
|
-
if os_string.empty?
|
37
|
-
raise MissingParamError, 'No operating systems provided to obtain.'
|
38
|
-
end
|
29
|
+
os_string = os_type.map { |os, num| Array(os) * num }.flatten.join('+')
|
30
|
+
raise MissingParamError, 'No operating systems provided to obtain.' if os_string.empty?
|
39
31
|
|
40
32
|
response = conn.post "host/#{os_string}"
|
41
33
|
|
@@ -51,14 +43,10 @@ class NonstandardPooler
|
|
51
43
|
end
|
52
44
|
|
53
45
|
def self.modify(verbose, url, hostname, token, modify_hash)
|
54
|
-
if token.nil?
|
55
|
-
raise TokenError, 'Token provided was nil; Request cannot be made to modify VM'
|
56
|
-
end
|
46
|
+
raise TokenError, 'Token provided was nil; Request cannot be made to modify VM' if token.nil?
|
57
47
|
|
58
|
-
modify_hash.each do |key,
|
59
|
-
unless [
|
60
|
-
raise ModifyError, "Configured service type does not support modification of #{key}"
|
61
|
-
end
|
48
|
+
modify_hash.each do |key, _value|
|
49
|
+
raise ModifyError, "Configured service type does not support modification of #{key}" unless %i[reason reserved_for_reason].include? key
|
62
50
|
end
|
63
51
|
|
64
52
|
if modify_hash[:reason]
|
@@ -77,22 +65,20 @@ class NonstandardPooler
|
|
77
65
|
response.body.empty? ? {} : JSON.parse(response.body)
|
78
66
|
end
|
79
67
|
|
80
|
-
def self.disk(
|
68
|
+
def self.disk(_verbose, _url, _hostname, _token, _disk)
|
81
69
|
raise ModifyError, 'Configured service type does not support modification of disk space'
|
82
70
|
end
|
83
71
|
|
84
|
-
def self.snapshot(
|
72
|
+
def self.snapshot(_verbose, _url, _hostname, _token)
|
85
73
|
raise ModifyError, 'Configured service type does not support snapshots'
|
86
74
|
end
|
87
75
|
|
88
|
-
def self.revert(
|
76
|
+
def self.revert(_verbose, _url, _hostname, _token, _snapshot_sha)
|
89
77
|
raise ModifyError, 'Configured service type does not support snapshots'
|
90
78
|
end
|
91
79
|
|
92
|
-
def self.delete(verbose, url, hosts, token)
|
93
|
-
if token.nil?
|
94
|
-
raise TokenError, 'Token provided was nil; Request cannot be made to delete VM'
|
95
|
-
end
|
80
|
+
def self.delete(verbose, url, hosts, token, _user)
|
81
|
+
raise TokenError, 'Token provided was nil; Request cannot be made to delete VM' if token.nil?
|
96
82
|
|
97
83
|
conn = Http.get_conn(verbose, url)
|
98
84
|
|
@@ -100,9 +86,7 @@ class NonstandardPooler
|
|
100
86
|
|
101
87
|
response_body = {}
|
102
88
|
|
103
|
-
unless hosts.is_a? Array
|
104
|
-
hosts = hosts.split(',')
|
105
|
-
end
|
89
|
+
hosts = hosts.split(',') unless hosts.is_a? Array
|
106
90
|
hosts.each do |host|
|
107
91
|
response = conn.delete "host/#{host}"
|
108
92
|
res_body = JSON.parse(response.body)
|
data/lib/vmfloaty/pooler.rb
CHANGED
@@ -1,77 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'faraday'
|
2
4
|
require 'vmfloaty/http'
|
3
5
|
require 'json'
|
4
6
|
require 'vmfloaty/errors'
|
5
7
|
|
6
8
|
class Pooler
|
7
|
-
def self.list(verbose, url, os_filter=nil)
|
9
|
+
def self.list(verbose, url, os_filter = nil)
|
8
10
|
conn = Http.get_conn(verbose, url)
|
9
11
|
|
10
12
|
response = conn.get 'vm'
|
11
13
|
response_body = JSON.parse(response.body)
|
12
14
|
|
13
|
-
if os_filter
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
hosts = if os_filter
|
16
|
+
response_body.select { |i| i[/#{os_filter}/] }
|
17
|
+
else
|
18
|
+
response_body
|
19
|
+
end
|
18
20
|
|
19
21
|
hosts
|
20
22
|
end
|
21
23
|
|
22
|
-
def self.list_active(verbose, url, token)
|
24
|
+
def self.list_active(verbose, url, token, _user)
|
23
25
|
status = Auth.token_status(verbose, url, token)
|
24
26
|
vms = []
|
25
|
-
if status[token] && status[token]['vms']
|
26
|
-
vms = status[token]['vms']['running']
|
27
|
-
end
|
27
|
+
vms = status[token]['vms']['running'] if status[token] && status[token]['vms']
|
28
28
|
vms
|
29
29
|
end
|
30
30
|
|
31
|
-
def self.retrieve(verbose, os_type, token, url)
|
31
|
+
def self.retrieve(verbose, os_type, token, url, _user, _options, ondemand = nil)
|
32
32
|
# NOTE:
|
33
33
|
# Developers can use `Utils.generate_os_hash` to
|
34
34
|
# generate the os_type param.
|
35
35
|
conn = Http.get_conn(verbose, url)
|
36
|
-
if token
|
37
|
-
conn.headers['X-AUTH-TOKEN'] = token
|
38
|
-
end
|
36
|
+
conn.headers['X-AUTH-TOKEN'] = token if token
|
39
37
|
|
40
|
-
os_string =
|
41
|
-
|
42
|
-
num.times do |i|
|
43
|
-
os_string << os+"+"
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
os_string = os_string.chomp("+")
|
48
|
-
|
49
|
-
if os_string.size == 0
|
50
|
-
raise MissingParamError, "No operating systems provided to obtain."
|
51
|
-
end
|
38
|
+
os_string = os_type.map { |os, num| Array(os) * num }.flatten.join('+')
|
39
|
+
raise MissingParamError, 'No operating systems provided to obtain.' if os_string.empty?
|
52
40
|
|
53
|
-
response = conn.post "vm/#{os_string}"
|
41
|
+
response = conn.post "vm/#{os_string}" unless ondemand
|
42
|
+
response ||= conn.post "ondemandvm/#{os_string}"
|
54
43
|
|
55
44
|
res_body = JSON.parse(response.body)
|
56
45
|
|
57
|
-
if res_body[
|
46
|
+
if res_body['ok']
|
58
47
|
res_body
|
59
48
|
elsif response.status == 401
|
60
49
|
raise AuthError, "HTTP #{response.status}: The token provided could not authenticate to the pooler.\n#{res_body}"
|
50
|
+
elsif response.status == 403
|
51
|
+
raise "HTTP #{response.status}: Failed to obtain VMs from the pooler at #{url}/vm/#{os_string}. Request exceeds the configured per pool maximum. #{res_body}"
|
61
52
|
else
|
62
|
-
raise "HTTP #{response.status}: Failed to obtain VMs from the pooler at #{url}/vm/#{os_string}. #{res_body}"
|
53
|
+
raise "HTTP #{response.status}: Failed to obtain VMs from the pooler at #{url}/vm/#{os_string}. #{res_body}" unless ondemand
|
54
|
+
raise "HTTP #{response.status}: Failed to obtain VMs from the pooler at #{url}/ondemandvm/#{os_string}. #{res_body}"
|
63
55
|
end
|
64
56
|
end
|
65
57
|
|
66
|
-
def self.
|
67
|
-
|
68
|
-
|
58
|
+
def self.wait_for_request(verbose, request_id, url, timeout = 300)
|
59
|
+
start_time = Time.now
|
60
|
+
while check_ondemandvm(verbose, request_id, url) == false
|
61
|
+
return false if (Time.now - start_time).to_i > timeout
|
62
|
+
|
63
|
+
STDOUT.puts "waiting for request #{request_id} to be fulfilled"
|
64
|
+
sleep 5
|
69
65
|
end
|
66
|
+
STDOUT.puts "The request has been fulfilled"
|
67
|
+
check_ondemandvm(verbose, request_id, url)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.check_ondemandvm(verbose, request_id, url)
|
71
|
+
conn = Http.get_conn(verbose, url)
|
72
|
+
|
73
|
+
response = conn.get "ondemandvm/#{request_id}"
|
74
|
+
res_body = JSON.parse(response.body)
|
75
|
+
return res_body if response.status == 200
|
76
|
+
|
77
|
+
return false if response.status == 202
|
78
|
+
|
79
|
+
raise "HTTP #{response.status}: The request cannot be found, or an unknown error occurred" if response.status == 404
|
80
|
+
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.modify(verbose, url, hostname, token, modify_hash)
|
85
|
+
raise TokenError, 'Token provided was nil. Request cannot be made to modify vm' if token.nil?
|
70
86
|
|
71
87
|
modify_hash.keys.each do |key|
|
72
|
-
unless [
|
73
|
-
raise ModifyError, "Configured service type does not support modification of #{key}."
|
74
|
-
end
|
88
|
+
raise ModifyError, "Configured service type does not support modification of #{key}." unless %i[tags lifetime disk].include? key
|
75
89
|
end
|
76
90
|
|
77
91
|
conn = Http.get_conn(verbose, url)
|
@@ -88,13 +102,18 @@ class Pooler
|
|
88
102
|
end
|
89
103
|
|
90
104
|
res_body = JSON.parse(response.body)
|
91
|
-
|
105
|
+
|
106
|
+
if res_body['ok']
|
107
|
+
res_body
|
108
|
+
elsif response.status == 401
|
109
|
+
raise AuthError, "HTTP #{response.status}: The token provided could not authenticate to the pooler.\n#{res_body}"
|
110
|
+
else
|
111
|
+
raise ModifyError, "HTTP #{response.status}: Failed to modify VMs from the pooler vm/#{hostname}. #{res_body}"
|
112
|
+
end
|
92
113
|
end
|
93
114
|
|
94
115
|
def self.disk(verbose, url, hostname, token, disk)
|
95
|
-
if token.nil?
|
96
|
-
raise TokenError, "Token provided was nil. Request cannot be made to modify vm"
|
97
|
-
end
|
116
|
+
raise TokenError, 'Token provided was nil. Request cannot be made to modify vm' if token.nil?
|
98
117
|
|
99
118
|
conn = Http.get_conn(verbose, url)
|
100
119
|
conn.headers['X-AUTH-TOKEN'] = token
|
@@ -105,16 +124,12 @@ class Pooler
|
|
105
124
|
res_body
|
106
125
|
end
|
107
126
|
|
108
|
-
def self.delete(verbose, url, hosts, token)
|
109
|
-
if token.nil?
|
110
|
-
raise TokenError, "Token provided was nil. Request cannot be made to delete vm"
|
111
|
-
end
|
127
|
+
def self.delete(verbose, url, hosts, token, _user)
|
128
|
+
raise TokenError, 'Token provided was nil. Request cannot be made to delete vm' if token.nil?
|
112
129
|
|
113
130
|
conn = Http.get_conn(verbose, url)
|
114
131
|
|
115
|
-
if token
|
116
|
-
conn.headers['X-AUTH-TOKEN'] = token
|
117
|
-
end
|
132
|
+
conn.headers['X-AUTH-TOKEN'] = token if token
|
118
133
|
|
119
134
|
response_body = {}
|
120
135
|
|
@@ -153,9 +168,7 @@ class Pooler
|
|
153
168
|
end
|
154
169
|
|
155
170
|
def self.snapshot(verbose, url, hostname, token)
|
156
|
-
if token.nil?
|
157
|
-
raise TokenError, "Token provided was nil. Request cannot be made to snapshot vm"
|
158
|
-
end
|
171
|
+
raise TokenError, 'Token provided was nil. Request cannot be made to snapshot vm' if token.nil?
|
159
172
|
|
160
173
|
conn = Http.get_conn(verbose, url)
|
161
174
|
conn.headers['X-AUTH-TOKEN'] = token
|
@@ -166,16 +179,12 @@ class Pooler
|
|
166
179
|
end
|
167
180
|
|
168
181
|
def self.revert(verbose, url, hostname, token, snapshot_sha)
|
169
|
-
if token.nil?
|
170
|
-
raise TokenError, "Token provided was nil. Request cannot be made to revert vm"
|
171
|
-
end
|
182
|
+
raise TokenError, 'Token provided was nil. Request cannot be made to revert vm' if token.nil?
|
172
183
|
|
173
184
|
conn = Http.get_conn(verbose, url)
|
174
185
|
conn.headers['X-AUTH-TOKEN'] = token
|
175
186
|
|
176
|
-
if snapshot_sha.nil?
|
177
|
-
raise "Snapshot SHA provided was nil, could not revert #{hostname}"
|
178
|
-
end
|
187
|
+
raise "Snapshot SHA provided was nil, could not revert #{hostname}" if snapshot_sha.nil?
|
179
188
|
|
180
189
|
response = conn.post "vm/#{hostname}/snapshot/#{snapshot_sha}"
|
181
190
|
res_body = JSON.parse(response.body)
|
data/lib/vmfloaty/service.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'commander/user_interaction'
|
2
4
|
require 'commander/command'
|
3
5
|
require 'vmfloaty/utils'
|
4
6
|
require 'vmfloaty/ssh'
|
5
7
|
|
6
8
|
class Service
|
7
|
-
|
8
9
|
attr_reader :config
|
9
10
|
|
10
11
|
def initialize(options, config_hash = {})
|
@@ -13,14 +14,18 @@ class Service
|
|
13
14
|
@service_object = Utils.get_service_object @config['type']
|
14
15
|
end
|
15
16
|
|
16
|
-
def method_missing(
|
17
|
-
if @service_object.respond_to?
|
18
|
-
@service_object.send(
|
17
|
+
def method_missing(method_name, *args, &block)
|
18
|
+
if @service_object.respond_to?(method_name)
|
19
|
+
@service_object.send(method_name, *args, &block)
|
19
20
|
else
|
20
21
|
super
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
25
|
+
def respond_to_missing?(method_name, *)
|
26
|
+
@service_object.respond_to?(method_name) || super
|
27
|
+
end
|
28
|
+
|
24
29
|
def url
|
25
30
|
@config['url']
|
26
31
|
end
|
@@ -31,7 +36,7 @@ class Service
|
|
31
36
|
|
32
37
|
def user
|
33
38
|
unless @config['user']
|
34
|
-
puts "Enter your
|
39
|
+
puts "Enter your #{@config['url']} service username:"
|
35
40
|
@config['user'] = STDIN.gets.chomp
|
36
41
|
end
|
37
42
|
@config['user']
|
@@ -39,7 +44,7 @@ class Service
|
|
39
44
|
|
40
45
|
def token
|
41
46
|
unless @config['token']
|
42
|
-
puts
|
47
|
+
puts 'No token found. Retrieving a token...'
|
43
48
|
@config['token'] = get_new_token(nil)
|
44
49
|
end
|
45
50
|
@config['token']
|
@@ -47,13 +52,13 @@ class Service
|
|
47
52
|
|
48
53
|
def get_new_token(verbose)
|
49
54
|
username = user
|
50
|
-
pass = Commander::UI
|
55
|
+
pass = Commander::UI.password "Enter your #{@config['url']} service password:", '*'
|
51
56
|
Auth.get_token(verbose, url, username, pass)
|
52
57
|
end
|
53
58
|
|
54
59
|
def delete_token(verbose, token_value = @config['token'])
|
55
60
|
username = user
|
56
|
-
pass = Commander::UI
|
61
|
+
pass = Commander::UI.password "Enter your #{@config['url']} service password:", '*'
|
57
62
|
Auth.delete_token(verbose, url, username, pass, token_value)
|
58
63
|
end
|
59
64
|
|
@@ -67,13 +72,17 @@ class Service
|
|
67
72
|
end
|
68
73
|
|
69
74
|
def list_active(verbose)
|
70
|
-
@service_object.list_active verbose, url, token
|
75
|
+
@service_object.list_active verbose, url, token, user
|
71
76
|
end
|
72
77
|
|
73
|
-
def retrieve(verbose, os_types, use_token = true)
|
78
|
+
def retrieve(verbose, os_types, use_token = true, ondemand = nil)
|
74
79
|
puts 'Requesting a vm without a token...' unless use_token
|
75
80
|
token_value = use_token ? token : nil
|
76
|
-
@service_object.retrieve verbose, os_types, token_value, url
|
81
|
+
@service_object.retrieve verbose, os_types, token_value, url, user, @config, ondemand
|
82
|
+
end
|
83
|
+
|
84
|
+
def wait_for_request(verbose, requestid)
|
85
|
+
@service_object.wait_for_request verbose, requestid, url
|
77
86
|
end
|
78
87
|
|
79
88
|
def ssh(verbose, host_os, use_token = true)
|
@@ -86,14 +95,14 @@ class Service
|
|
86
95
|
STDERR.puts 'Could not get token... requesting vm without a token anyway...'
|
87
96
|
end
|
88
97
|
end
|
89
|
-
Ssh.ssh(verbose, host_os, token_value
|
98
|
+
Ssh.ssh(verbose, self, host_os, token_value)
|
90
99
|
end
|
91
100
|
|
92
101
|
def pretty_print_running(verbose, hostnames = [])
|
93
102
|
if hostnames.empty?
|
94
|
-
puts
|
103
|
+
puts 'You have no running VMs.'
|
95
104
|
else
|
96
|
-
puts
|
105
|
+
puts 'Running VMs:'
|
97
106
|
@service_object.pretty_print_hosts(verbose, hostnames, url)
|
98
107
|
end
|
99
108
|
end
|
@@ -107,7 +116,7 @@ class Service
|
|
107
116
|
end
|
108
117
|
|
109
118
|
def delete(verbose, hosts)
|
110
|
-
@service_object.delete verbose, url, hosts, token
|
119
|
+
@service_object.delete verbose, url, hosts, token, user
|
111
120
|
end
|
112
121
|
|
113
122
|
def status(verbose)
|
@@ -129,5 +138,4 @@ class Service
|
|
129
138
|
def disk(verbose, hostname, disk)
|
130
139
|
@service_object.disk(verbose, url, hostname, token, disk)
|
131
140
|
end
|
132
|
-
|
133
|
-
end
|
141
|
+
end
|
data/lib/vmfloaty/ssh.rb
CHANGED
@@ -1,43 +1,43 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
class Ssh
|
3
4
|
def self.which(cmd)
|
4
5
|
# Gets path of executable for given command
|
5
6
|
|
6
7
|
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
7
8
|
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
8
|
-
exts.each
|
9
|
+
exts.each do |ext|
|
9
10
|
exe = File.join(path, "#{cmd}#{ext}")
|
10
11
|
return exe if File.executable?(exe) && !File.directory?(exe)
|
11
|
-
|
12
|
+
end
|
12
13
|
end
|
13
|
-
|
14
|
+
nil
|
14
15
|
end
|
15
16
|
|
16
|
-
def self.
|
17
|
-
ssh_path = which(
|
18
|
-
|
19
|
-
|
20
|
-
end
|
17
|
+
def self.command_string(verbose, service, host_os, use_token)
|
18
|
+
ssh_path = which('ssh')
|
19
|
+
raise 'Could not determine path to ssh' unless ssh_path
|
20
|
+
|
21
21
|
os_types = {}
|
22
22
|
os_types[host_os] = 1
|
23
23
|
|
24
|
-
response =
|
25
|
-
|
26
|
-
if host_os =~ /win/
|
27
|
-
user = "Administrator"
|
28
|
-
else
|
29
|
-
user = "root"
|
30
|
-
end
|
24
|
+
response = service.retrieve(verbose, os_types, use_token)
|
25
|
+
raise "Could not get vm from #{service.type}:\n #{response}" unless response['ok']
|
31
26
|
|
32
|
-
|
33
|
-
cmd = "#{ssh_path} #{user}@#{hostname}"
|
27
|
+
user = /win/.match?(host_os) ? 'Administrator' : 'root'
|
34
28
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
29
|
+
hostname = response[host_os]['hostname']
|
30
|
+
hostname = response[host_os]['hostname'][0] if response[host_os]['hostname'].is_a?(Array)
|
31
|
+
hostname = "#{hostname}.#{response['domain']}" unless hostname.end_with?('puppetlabs.net')
|
32
|
+
|
33
|
+
"#{ssh_path} #{user}@#{hostname}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.ssh(verbose, service, host_os, use_token)
|
37
|
+
cmd = command_string(verbose, service, host_os, use_token)
|
38
|
+
# TODO: Should this respect more ssh settings? Can it be configured
|
39
|
+
# by users ssh config and does this respect those settings?
|
40
|
+
Kernel.exec(cmd)
|
41
|
+
nil
|
42
42
|
end
|
43
43
|
end
|