miasma-aws 0.3.10 → 0.3.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.md +4 -0
- data/lib/miasma-aws.rb +2 -2
- data/lib/miasma-aws/api.rb +3 -3
- data/lib/miasma-aws/api/iam.rb +16 -17
- data/lib/miasma-aws/api/sts.rb +29 -30
- data/lib/miasma-aws/version.rb +1 -1
- data/lib/miasma/contrib/aws.rb +170 -185
- data/lib/miasma/contrib/aws/auto_scale.rb +23 -25
- data/lib/miasma/contrib/aws/compute.rb +51 -56
- data/lib/miasma/contrib/aws/load_balancer.rb +64 -70
- data/lib/miasma/contrib/aws/orchestration.rb +153 -159
- data/lib/miasma/contrib/aws/storage.rb +109 -113
- data/miasma-aws.gemspec +3 -2
- metadata +33 -19
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "miasma"
|
2
2
|
|
3
3
|
module Miasma
|
4
4
|
module Models
|
@@ -7,9 +7,9 @@ module Miasma
|
|
7
7
|
class Aws < AutoScale
|
8
8
|
|
9
9
|
# Service name of the API
|
10
|
-
API_SERVICE =
|
10
|
+
API_SERVICE = "autoscaling".freeze
|
11
11
|
# Supported version of the AutoScaling API
|
12
|
-
API_VERSION =
|
12
|
+
API_VERSION = "2011-01-01".freeze
|
13
13
|
|
14
14
|
include Contrib::AwsApiCore::ApiCommon
|
15
15
|
include Contrib::AwsApiCore::RequestUtils
|
@@ -27,7 +27,7 @@ module Miasma
|
|
27
27
|
# @param group [Models::AutoScale::Group]
|
28
28
|
# @return [Models::AutoScale::Group]
|
29
29
|
def group_reload(group)
|
30
|
-
if
|
30
|
+
if group.id || group.name
|
31
31
|
load_group_data(group)
|
32
32
|
end
|
33
33
|
group
|
@@ -45,35 +45,34 @@ module Miasma
|
|
45
45
|
#
|
46
46
|
# @param group [Models::AutoScale::Group]
|
47
47
|
# @return [Array<Models::AutoScale::Group>]
|
48
|
-
def load_group_data(group=nil)
|
49
|
-
params = Smash.new(
|
50
|
-
if
|
51
|
-
params.merge(
|
48
|
+
def load_group_data(group = nil)
|
49
|
+
params = Smash.new("Action" => "DescribeAutoScalingGroups")
|
50
|
+
if group
|
51
|
+
params.merge("AutoScalingGroupNames.member.1" => group.id || group.name)
|
52
52
|
end
|
53
53
|
result = all_result_pages(nil, :body,
|
54
|
-
|
55
|
-
|
56
|
-
) do |options|
|
54
|
+
"DescribeAutoScalingGroupsResponse", "DescribeAutoScalingGroupsResult",
|
55
|
+
"AutoScalingGroups", "member") do |options|
|
57
56
|
request(
|
58
57
|
:method => :post,
|
59
|
-
:path =>
|
60
|
-
:form => options.merge(params)
|
58
|
+
:path => "/",
|
59
|
+
:form => options.merge(params),
|
61
60
|
)
|
62
61
|
end.map do |grp|
|
63
62
|
(group || Group.new(self)).load_data(
|
64
|
-
:id => grp[
|
65
|
-
:name => grp[
|
66
|
-
:servers => [grp.get(
|
67
|
-
Group::Server.new(self, :id => i[
|
63
|
+
:id => grp["AutoScalingGroupName"],
|
64
|
+
:name => grp["AutoScalingGroupName"],
|
65
|
+
:servers => [grp.get("Instances", "member")].flatten(1).compact.map { |i|
|
66
|
+
Group::Server.new(self, :id => i["InstanceId"])
|
68
67
|
},
|
69
|
-
:minimum_size => grp[
|
70
|
-
:maximum_size => grp[
|
71
|
-
:status => grp[
|
68
|
+
:minimum_size => grp["MinSize"],
|
69
|
+
:maximum_size => grp["MaxSize"],
|
70
|
+
:status => grp["Status"],
|
72
71
|
:load_balancers => [
|
73
|
-
grp.get(
|
74
|
-
].flatten(1).compact.map{|i|
|
72
|
+
grp.get("LoadBalancerNames", "member"),
|
73
|
+
].flatten(1).compact.map { |i|
|
75
74
|
Group::Balancer.new(self, :id => i, :name => i)
|
76
|
-
}
|
75
|
+
},
|
77
76
|
).valid_state
|
78
77
|
end
|
79
78
|
end
|
@@ -82,10 +81,9 @@ module Miasma
|
|
82
81
|
#
|
83
82
|
# @param options [Hash] filter
|
84
83
|
# @return [Array<Models::AutoScale::Group>]
|
85
|
-
def group_all(options={})
|
84
|
+
def group_all(options = {})
|
86
85
|
load_group_data
|
87
86
|
end
|
88
|
-
|
89
87
|
end
|
90
88
|
end
|
91
89
|
end
|
@@ -1,73 +1,71 @@
|
|
1
|
-
require
|
1
|
+
require "miasma"
|
2
2
|
|
3
3
|
module Miasma
|
4
|
-
|
5
4
|
module Models
|
6
5
|
class Compute
|
7
6
|
# Compute interface for AWS
|
8
7
|
class Aws < Compute
|
9
8
|
|
10
9
|
# Service name of the API
|
11
|
-
API_SERVICE =
|
10
|
+
API_SERVICE = "ec2".freeze
|
12
11
|
# Supported version of the EC2 API
|
13
|
-
API_VERSION =
|
12
|
+
API_VERSION = "2014-06-15".freeze
|
14
13
|
|
15
14
|
include Contrib::AwsApiCore::ApiCommon
|
16
15
|
include Contrib::AwsApiCore::RequestUtils
|
17
16
|
|
18
17
|
# @return [Smash] map state to valid internal values
|
19
18
|
SERVER_STATE_MAP = Smash.new(
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
19
|
+
"running" => :running,
|
20
|
+
"pending" => :pending,
|
21
|
+
"shutting-down" => :pending,
|
22
|
+
"terminated" => :terminated,
|
23
|
+
"stopping" => :pending,
|
24
|
+
"stopped" => :stopped,
|
26
25
|
).to_smash(:freeze)
|
27
26
|
|
28
27
|
# @todo catch bad lookup and clear model
|
29
28
|
def server_reload(server)
|
30
29
|
result = request(
|
31
30
|
:method => :post,
|
32
|
-
:path =>
|
31
|
+
:path => "/",
|
33
32
|
:form => {
|
34
|
-
|
35
|
-
|
36
|
-
}
|
33
|
+
"Action" => "DescribeInstances",
|
34
|
+
"InstanceId.1" => server.id,
|
35
|
+
},
|
37
36
|
)
|
38
37
|
srv = result.get(:body,
|
39
|
-
|
40
|
-
|
41
|
-
)
|
38
|
+
"DescribeInstancesResponse", "reservationSet",
|
39
|
+
"item", "instancesSet", "item")
|
42
40
|
server.load_data(
|
43
41
|
:id => srv[:instanceId],
|
44
|
-
:name => [srv.fetch(:tagSet, :item, [])].flatten.map{|tag|
|
45
|
-
tag[:value] if tag.is_a?(Hash) && tag[:key] ==
|
42
|
+
:name => [srv.fetch(:tagSet, :item, [])].flatten.map { |tag|
|
43
|
+
tag[:value] if tag.is_a?(Hash) && tag[:key] == "Name"
|
46
44
|
}.compact.first,
|
47
45
|
:image_id => srv[:imageId],
|
48
46
|
:flavor_id => srv[:instanceType],
|
49
47
|
:state => SERVER_STATE_MAP.fetch(srv.get(:instanceState, :name), :pending),
|
50
48
|
:addresses_private => [
|
51
|
-
Server::Address.new(:version => 4, :address => srv[:privateIpAddress])
|
49
|
+
Server::Address.new(:version => 4, :address => srv[:privateIpAddress]),
|
52
50
|
],
|
53
51
|
:addresses_public => [
|
54
|
-
Server::Address.new(:version => 4, :address => srv[:ipAddress])
|
52
|
+
Server::Address.new(:version => 4, :address => srv[:ipAddress]),
|
55
53
|
],
|
56
54
|
:status => srv.get(:instanceState, :name),
|
57
|
-
:key_name => srv[:keyName]
|
55
|
+
:key_name => srv[:keyName],
|
58
56
|
)
|
59
57
|
server.valid_state
|
60
58
|
end
|
61
59
|
|
62
60
|
def server_destroy(server)
|
63
|
-
if
|
61
|
+
if server.persisted?
|
64
62
|
result = request(
|
65
63
|
:method => :post,
|
66
|
-
:path =>
|
64
|
+
:path => "/",
|
67
65
|
:form => {
|
68
|
-
|
69
|
-
|
70
|
-
}
|
66
|
+
"Action" => "TerminateInstances",
|
67
|
+
"InstanceId.1" => server.id,
|
68
|
+
},
|
71
69
|
)
|
72
70
|
else
|
73
71
|
raise "this doesn't even exist"
|
@@ -75,50 +73,48 @@ module Miasma
|
|
75
73
|
end
|
76
74
|
|
77
75
|
def server_save(server)
|
78
|
-
unless
|
76
|
+
unless server.persisted?
|
79
77
|
server.load_data(server.attributes)
|
80
78
|
result = request(
|
81
79
|
:method => :post,
|
82
|
-
:path =>
|
80
|
+
:path => "/",
|
83
81
|
:form => {
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
}
|
82
|
+
"Action" => "RunInstances",
|
83
|
+
"ImageId" => server.image_id,
|
84
|
+
"InstanceType" => server.flavor_id,
|
85
|
+
"KeyName" => server.key_name,
|
86
|
+
"MinCount" => 1,
|
87
|
+
"MaxCount" => 1,
|
88
|
+
},
|
91
89
|
)
|
92
90
|
server.id = result.get(:body,
|
93
|
-
|
94
|
-
)
|
91
|
+
"RunInstancesResponse", "instancesSet", "item", "instanceId")
|
95
92
|
server.valid_state
|
96
93
|
request(
|
97
94
|
:method => :post,
|
98
|
-
:path =>
|
95
|
+
:path => "/",
|
99
96
|
:form => {
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
}
|
97
|
+
"Action" => "CreateTags",
|
98
|
+
"ResourceId.1" => server.id,
|
99
|
+
"Tag.1.Key" => "Name",
|
100
|
+
"Tag.1.Value" => server.name,
|
101
|
+
},
|
105
102
|
)
|
106
103
|
else
|
107
|
-
raise
|
104
|
+
raise "WAT DO I DO!?"
|
108
105
|
end
|
109
106
|
end
|
110
107
|
|
111
108
|
# @todo need to add auto pagination helper (as common util)
|
112
109
|
def server_all
|
113
110
|
results = all_result_pages(nil, :body,
|
114
|
-
|
115
|
-
) do |options|
|
111
|
+
"DescribeInstancesResponse", "reservationSet", "item") do |options|
|
116
112
|
request(
|
117
113
|
:method => :post,
|
118
|
-
:path =>
|
114
|
+
:path => "/",
|
119
115
|
:form => options.merge(
|
120
|
-
|
121
|
-
)
|
116
|
+
"Action" => "DescribeInstances",
|
117
|
+
),
|
122
118
|
)
|
123
119
|
end
|
124
120
|
results.map do |server|
|
@@ -126,25 +122,24 @@ module Miasma
|
|
126
122
|
Server.new(
|
127
123
|
self,
|
128
124
|
:id => srv[:instanceId],
|
129
|
-
:name => srv.fetch(:tagSet, :item, []).map{|tag|
|
130
|
-
tag[:value] if tag.is_a?(Hash) && tag[:key] ==
|
125
|
+
:name => srv.fetch(:tagSet, :item, []).map { |tag|
|
126
|
+
tag[:value] if tag.is_a?(Hash) && tag[:key] == "Name"
|
131
127
|
}.compact.first,
|
132
128
|
:image_id => srv[:imageId],
|
133
129
|
:flavor_id => srv[:instanceType],
|
134
130
|
:state => SERVER_STATE_MAP.fetch(srv.get(:instanceState, :name), :pending),
|
135
131
|
:addresses_private => [
|
136
|
-
Server::Address.new(:version => 4, :address => srv[:privateIpAddress])
|
132
|
+
Server::Address.new(:version => 4, :address => srv[:privateIpAddress]),
|
137
133
|
],
|
138
134
|
:addresses_public => [
|
139
|
-
Server::Address.new(:version => 4, :address => srv[:ipAddress])
|
135
|
+
Server::Address.new(:version => 4, :address => srv[:ipAddress]),
|
140
136
|
],
|
141
137
|
:status => srv.get(:instanceState, :name),
|
142
|
-
:key_name => srv[:keyName]
|
138
|
+
:key_name => srv[:keyName],
|
143
139
|
).valid_state
|
144
140
|
end
|
145
141
|
end.flatten
|
146
142
|
end
|
147
|
-
|
148
143
|
end
|
149
144
|
end
|
150
145
|
end
|
@@ -1,70 +1,68 @@
|
|
1
|
-
require
|
1
|
+
require "miasma"
|
2
2
|
|
3
3
|
module Miasma
|
4
4
|
module Models
|
5
5
|
class LoadBalancer
|
6
6
|
# AWS load balancer API
|
7
7
|
class Aws < LoadBalancer
|
8
|
-
|
9
8
|
include Contrib::AwsApiCore::ApiCommon
|
10
9
|
include Contrib::AwsApiCore::RequestUtils
|
11
10
|
|
12
11
|
# Service name of API
|
13
|
-
API_SERVICE =
|
12
|
+
API_SERVICE = "elasticloadbalancing".freeze
|
14
13
|
# Supported version of the ELB API
|
15
|
-
API_VERSION =
|
14
|
+
API_VERSION = "2012-06-01".freeze
|
16
15
|
|
17
16
|
# Save load balancer
|
18
17
|
#
|
19
18
|
# @param balancer [Models::LoadBalancer::Balancer]
|
20
19
|
# @return [Models::LoadBalancer::Balancer]
|
21
20
|
def balancer_save(balancer)
|
22
|
-
unless
|
21
|
+
unless balancer.persisted?
|
23
22
|
params = Smash.new(
|
24
|
-
|
23
|
+
"LoadBalancerName" => balancer.name,
|
25
24
|
)
|
26
25
|
availability_zones.each_with_index do |az, i|
|
27
26
|
params["AvailabilityZones.member.#{i + 1}"] = az
|
28
27
|
end
|
29
|
-
if
|
28
|
+
if balancer.listeners
|
30
29
|
balancer.listeners.each_with_index do |listener, i|
|
31
30
|
key = "Listeners.member.#{i + 1}"
|
32
31
|
params["#{key}.Protocol"] = listener.protocol
|
33
32
|
params["#{key}.InstanceProtocol"] = listener.instance_protocol
|
34
33
|
params["#{key}.LoadBalancerPort"] = listener.load_balancer_port
|
35
34
|
params["#{key}.InstancePort"] = listener.instance_port
|
36
|
-
if
|
35
|
+
if listener.ssl_certificate_id
|
37
36
|
params["#{key}.SSLCertificateId"] = listener.ssl_certificate_id
|
38
37
|
end
|
39
38
|
end
|
40
39
|
end
|
41
40
|
result = request(
|
42
41
|
:method => :post,
|
43
|
-
:path =>
|
42
|
+
:path => "/",
|
44
43
|
:form => params.merge(
|
45
44
|
Smash.new(
|
46
|
-
|
45
|
+
"Action" => "CreateLoadBalancer",
|
47
46
|
)
|
48
|
-
)
|
47
|
+
),
|
49
48
|
)
|
50
49
|
balancer.public_addresses = [
|
51
50
|
:address => result.get(:body,
|
52
|
-
|
53
|
-
)
|
51
|
+
"CreateLoadBalancerResponse", "CreateLoadBalancerResult", "DNSName"),
|
54
52
|
]
|
55
53
|
balancer.load_data(:id => balancer.name).valid_state
|
56
|
-
if
|
54
|
+
if balancer.health_check
|
57
55
|
balancer_health_check(balancer)
|
58
56
|
end
|
59
|
-
if
|
57
|
+
if balancer.servers && !balancer.servers.empty?
|
60
58
|
balancer_set_instances(balancer)
|
61
59
|
end
|
62
60
|
else
|
63
|
-
if
|
64
|
-
if
|
61
|
+
if balancer.dirty?
|
62
|
+
if balancer.dirty?(:health_check)
|
65
63
|
balancer_health_check(balancer)
|
66
64
|
end
|
67
|
-
if
|
65
|
+
if balancer.dirty?(:servers)
|
68
66
|
balancer_set_instances(balancer)
|
69
67
|
end
|
70
68
|
balancer.reload
|
@@ -94,13 +92,13 @@ module Miasma
|
|
94
92
|
# @param balancer [Models::LoadBalancer::Balancer]
|
95
93
|
# @return [Models::LoadBalancer::Balancer]
|
96
94
|
def balancer_reload(balancer)
|
97
|
-
if
|
95
|
+
if balancer.persisted?
|
98
96
|
begin
|
99
97
|
load_balancer_data(balancer)
|
100
98
|
rescue Miasma::Error::ApiError::RequestError => e
|
101
|
-
if
|
99
|
+
if e.response_error_msg.include?("LoadBalancerNotFound")
|
102
100
|
balancer.state = :terminated
|
103
|
-
balancer.status =
|
101
|
+
balancer.status = "terminated"
|
104
102
|
balancer.valid_state
|
105
103
|
else
|
106
104
|
raise
|
@@ -114,65 +112,63 @@ module Miasma
|
|
114
112
|
#
|
115
113
|
# @param balancer [Models::LoadBalancer::Balancer]
|
116
114
|
# @return [Array<Models::LoadBalancer::Balancer>]
|
117
|
-
def load_balancer_data(balancer=nil)
|
118
|
-
params = Smash.new(
|
119
|
-
if
|
120
|
-
params[
|
115
|
+
def load_balancer_data(balancer = nil)
|
116
|
+
params = Smash.new("Action" => "DescribeLoadBalancers")
|
117
|
+
if balancer
|
118
|
+
params["LoadBalancerNames.member.1"] = balancer.id || balancer.name
|
121
119
|
end
|
122
120
|
result = all_result_pages(nil, :body,
|
123
|
-
|
124
|
-
|
125
|
-
) do |options|
|
121
|
+
"DescribeLoadBalancersResponse", "DescribeLoadBalancersResult",
|
122
|
+
"LoadBalancerDescriptions", "member") do |options|
|
126
123
|
request(
|
127
124
|
:method => :post,
|
128
|
-
:path =>
|
129
|
-
:form => options.merge(params)
|
125
|
+
:path => "/",
|
126
|
+
:form => options.merge(params),
|
130
127
|
)
|
131
128
|
end
|
132
|
-
if
|
129
|
+
if balancer
|
133
130
|
health_result = all_result_pages(nil, :body,
|
134
|
-
|
135
|
-
|
136
|
-
) do |options|
|
131
|
+
"DescribeInstanceHealthResponse", "DescribeInstanceHealthResult",
|
132
|
+
"InstanceStates", "member") do |options|
|
137
133
|
request(
|
138
134
|
:method => :post,
|
139
|
-
:path =>
|
135
|
+
:path => "/",
|
140
136
|
:form => options.merge(
|
141
|
-
|
142
|
-
|
143
|
-
)
|
137
|
+
"LoadBalancerName" => balancer.id || balancer.name,
|
138
|
+
"Action" => "DescribeInstanceHealth",
|
139
|
+
),
|
144
140
|
)
|
145
141
|
end
|
146
142
|
end
|
147
143
|
result.map do |blr|
|
148
144
|
(balancer || Balancer.new(self)).load_data(
|
149
145
|
Smash.new(
|
150
|
-
:id => blr[
|
151
|
-
:name => blr[
|
146
|
+
:id => blr["LoadBalancerName"],
|
147
|
+
:name => blr["LoadBalancerName"],
|
152
148
|
:state => :active,
|
153
|
-
:status =>
|
154
|
-
:created => blr[
|
155
|
-
:updated => blr[
|
149
|
+
:status => "ACTIVE",
|
150
|
+
:created => blr["CreatedTime"],
|
151
|
+
:updated => blr["CreatedTime"],
|
156
152
|
:public_addresses => [
|
157
153
|
Balancer::Address.new(
|
158
|
-
:address => blr[
|
159
|
-
:version => 4
|
160
|
-
)
|
154
|
+
:address => blr["DNSName"],
|
155
|
+
:version => 4,
|
156
|
+
),
|
161
157
|
],
|
162
|
-
:servers => [blr.get(
|
163
|
-
Balancer::Server.new(self.api_for(:compute), :id => i[
|
164
|
-
}
|
158
|
+
:servers => [blr.get("Instances", "member")].flatten(1).compact.map { |i|
|
159
|
+
Balancer::Server.new(self.api_for(:compute), :id => i["InstanceId"])
|
160
|
+
},
|
165
161
|
).merge(
|
166
162
|
health_result.nil? ? {} : Smash.new(
|
167
|
-
:server_states => health_result.nil? ? nil : health_result.map{|i|
|
163
|
+
:server_states => health_result.nil? ? nil : health_result.map { |i|
|
168
164
|
Balancer::ServerState.new(
|
169
165
|
self.api_for(:compute),
|
170
|
-
:id => i[
|
171
|
-
:status => i[
|
172
|
-
:reason => i[
|
173
|
-
:state => i[
|
166
|
+
:id => i["InstanceId"],
|
167
|
+
:status => i["State"],
|
168
|
+
:reason => i["ReasonCode"],
|
169
|
+
:state => i["State"] == "InService" ? :up : :down,
|
174
170
|
)
|
175
|
-
}
|
171
|
+
},
|
176
172
|
)
|
177
173
|
)
|
178
174
|
).valid_state
|
@@ -184,17 +180,17 @@ module Miasma
|
|
184
180
|
# @param balancer [Models::LoadBalancer::Balancer]
|
185
181
|
# @return [TrueClass, FalseClass]
|
186
182
|
def balancer_destroy(balancer)
|
187
|
-
if
|
183
|
+
if balancer.persisted?
|
188
184
|
request(
|
189
185
|
:method => :post,
|
190
|
-
:path =>
|
186
|
+
:path => "/",
|
191
187
|
:form => Smash.new(
|
192
|
-
|
193
|
-
|
194
|
-
)
|
188
|
+
"Action" => "DeleteLoadBalancer",
|
189
|
+
"LoadBalancerName" => balancer.name,
|
190
|
+
),
|
195
191
|
)
|
196
192
|
balancer.state = :pending
|
197
|
-
balancer.status =
|
193
|
+
balancer.status = "DELETE_IN_PROGRESS"
|
198
194
|
balancer.valid_state
|
199
195
|
true
|
200
196
|
else
|
@@ -206,7 +202,7 @@ module Miasma
|
|
206
202
|
#
|
207
203
|
# @param options [Hash] filter
|
208
204
|
# @return [Array<Models::LoadBalancer::Balancer>]
|
209
|
-
def balancer_all(options={})
|
205
|
+
def balancer_all(options = {})
|
210
206
|
load_balancer_data
|
211
207
|
end
|
212
208
|
|
@@ -217,21 +213,19 @@ module Miasma
|
|
217
213
|
memoize(:availability_zones) do
|
218
214
|
res = api_for(:compute).request(
|
219
215
|
:method => :post,
|
220
|
-
:path =>
|
216
|
+
:path => "/",
|
221
217
|
:form => Smash.new(
|
222
|
-
|
223
|
-
)
|
218
|
+
"Action" => "DescribeAvailabilityZones",
|
219
|
+
),
|
224
220
|
).fetch(:body,
|
225
|
-
|
226
|
-
)
|
221
|
+
"DescribeAvailabilityZonesResponse", "availabilityZoneInfo", "item", [])
|
227
222
|
[res].flatten.compact.map do |item|
|
228
|
-
if
|
229
|
-
item[
|
223
|
+
if item["zoneState"] == "available"
|
224
|
+
item["zoneName"]
|
230
225
|
end
|
231
226
|
end.compact
|
232
227
|
end
|
233
228
|
end
|
234
|
-
|
235
229
|
end
|
236
230
|
end
|
237
231
|
end
|