-
Notifications
You must be signed in to change notification settings - Fork 59
/
loadbalancer_configurator.rb
224 lines (194 loc) · 9.54 KB
/
loadbalancer_configurator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
module Bosh::OpenStackCloud
class LoadbalancerConfigurator
def initialize(openstack, logger)
@openstack = openstack
@logger = logger
end
def create_pool_memberships(server, network_spec, pools)
pools
.map { |pool| add_vm_to_pool(server, network_spec, pool) }
.each_with_index
.map do |membership, index|
["lbaas_pool_#{index + 1}", "#{membership.pool_id}/#{membership.membership_id}"]
end
.to_h
end
def add_vm_to_pool(server, network_spec, pool_spec)
validate_configuration(pool_spec)
openstack_pool_id = openstack_pool_id(pool_spec['name'])
ip = NetworkConfigurator.gateway_ip(network_spec, @openstack, server)
subnet_id = matching_subnet_id(network_spec, ip)
membership_id = create_membership(openstack_pool_id, ip, pool_spec['port'], subnet_id)
LoadbalancerPoolMembership.new(pool_spec['name'], pool_spec['port'], openstack_pool_id, membership_id)
rescue Bosh::Clouds::VMCreationFailed => e
message = "VM with id '#{server.id}' cannot be attached to load balancer pool '#{pool_spec['name']}'. Reason: #{e.message}"
raise Bosh::Clouds::VMCreationFailed.new(false), message
end
def create_membership(pool_id, ip, port, subnet_id)
@openstack.with_openstack do
membership_id = nil
begin
@logger.debug("Creating load balancer pool membership with pool id '#{pool_id}', ip '#{ip}', and port '#{port}'.")
membership_id = retry_on_conflict_pending_update(pool_id) {
@openstack.network.create_lbaas_pool_member(pool_id, ip, port, subnet_id:).body['member']['id']
}
rescue Excon::Error::Conflict
lbaas_pool_members = @openstack.with_openstack(retryable: true) { @openstack.network.list_lbaas_pool_members(pool_id) }
membership_id =
lbaas_pool_members
.body.fetch('members', [])
.detect(
raise_if_not_found(pool_id, ip, port),
&matching_member(ip, port, subnet_id)
)
.fetch('id')
@logger.info("Load balancer pool membership with pool id '#{pool_id}', ip '#{ip}', and port '#{port}' already exists. The membership has the id '#{membership_id}'.")
rescue LoadBalancerResource::NotFound, LoadBalancerResource::NotSupportedConfiguration => e
raise Bosh::Clouds::VMCreationFailed.new(false), e.message
rescue StandardError => e
raise Bosh::Clouds::CloudError, "Creating load balancer pool membership with pool_id '#{pool_id}' and membership_id '#{membership_id}' failed. Reason: #{e.class} #{e.message}"
end
membership_id
end
end
def raise_if_not_found(pool_id, ip, port)
-> { raise Bosh::Clouds::CloudError, "Load balancer pool membership with pool id '#{pool_id}', ip '#{ip}', and port '#{port}' supposedly exists, but cannot be found." }
end
def matching_member(ip, port, subnet_id)
->(member) { member['address'] == ip && member['protocol_port'] == port && member['subnet_id'] == subnet_id }
end
def cleanup_memberships(server_metadata)
server_metadata
.select { |key, _| key.start_with?('lbaas_pool_') }
.map { |_, value| value.split('/') }
.each { |pool_id, membership_id| remove_vm_from_pool(pool_id, membership_id) }
end
def remove_vm_from_pool(pool_id, membership_id)
@openstack.with_openstack(retryable: true) do
begin
@logger.debug("Deleting load balancer pool membership with pool id '#{pool_id}' and membership id '#{membership_id}'.")
retry_on_conflict_pending_update(pool_id) {
@openstack.network.delete_lbaas_pool_member(pool_id, membership_id)
}
rescue Fog::OpenStack::Network::NotFound
@logger.debug("Skipping deletion of load balancer pool membership. Member with pool_id '#{pool_id}' and membership_id '#{membership_id}' does not exist.")
rescue LoadBalancerResource::NotFound => e
@logger.debug("Skipping deletion of load balancer pool membership because load balancer resource cannot be found. #{e.message}")
rescue StandardError => e
raise Bosh::Clouds::CloudError, "Deleting load balancer pool membership with pool_id '#{pool_id}' and membership_id '#{membership_id}' failed. Reason: #{e.class} #{e.message}"
end
end
end
def retry_on_conflict_pending_update(pool_id)
action_result = nil
start_time = Time.now
attempts = 0
loadbalancer_id = loadbalancer_id(pool_id)
resource = LoadBalancerResource.new(loadbalancer_id, @openstack)
loop do
@openstack.wait_resource(resource, :active, :provisioning_status)
attempts += 1
action_result = yield
break
rescue Excon::Error::Conflict => e
neutron_error = @openstack.parse_openstack_response(e.response, 'NeutronError')
if neutron_error&.fetch('message', '')&.include? 'PENDING_UPDATE'
@logger.debug("Changing load balancer resource failed with '#{e.message}', unsuccessful attempts: '#{attempts}'")
if Time.now - start_time >= @openstack.state_timeout
@openstack.cloud_error("Failed after #{Time.now - start_time}s with #{attempts} attempts with '#{e.message}'")
end
else
raise e
end
end
@openstack.wait_resource(resource, :active, :provisioning_status)
action_result
end
def loadbalancer_id(pool_id)
pool_response = @openstack.with_openstack(retryable: true) do
begin
@openstack.network.get_lbaas_pool(pool_id)
rescue Fog::OpenStack::Network::NotFound => e
raise LoadBalancerResource::NotFound, "Load balancer ID could not be determined because pool with ID '#{pool_id}' was not found. Reason: #{e.message}"
end
end
loadbalancers = pool_response.body['pool']['loadbalancers'] ||
retrieve_loadbalancers_via_listener(
pool_response.body['pool']['listeners'],
pool_id,
)
extract_loadbalancer_id(loadbalancers, pool_id)
end
private
def retrieve_loadbalancers_via_listener(listeners, pool_id)
if listeners.empty?
raise LoadBalancerResource::NotFound, "No listeners associated with load balancer pool '#{pool_id}'"
elsif listeners.size > 1
raise LoadBalancerResource::NotSupportedConfiguration, "More than one listener is associated with load balancer pool '#{pool_id}'. It is not possible to verify the status of the load balancer responsible for the pool membership."
end
listener_response = @openstack.with_openstack(retryable: true) { @openstack.network.get_lbaas_listener(listeners[0]['id']) }
listener_response.body['listener']['loadbalancers']
end
def extract_loadbalancer_id(loadbalancers, pool_id)
if loadbalancers.empty?
raise LoadBalancerResource::NotFound, "No load balancers associated with load balancer pool '#{pool_id}'"
elsif loadbalancers.size > 1
raise LoadBalancerResource::NotSupportedConfiguration, "More than one load balancer is associated with load balancer pool '#{pool_id}'. It is not possible to verify the status of the load balancer responsible for the pool membership."
end
loadbalancers[0]['id']
end
def matching_subnet_id(network_spec, ip)
subnet_ids = NetworkConfigurator.matching_gateway_subnet_ids_for_ip(network_spec, @openstack, ip)
if subnet_ids.size > 1
raise Bosh::Clouds::VMCreationFailed.new(false), "In network '#{NetworkConfigurator.get_gateway_network_id(network_spec)}' more than one subnet CIDRs match the IP '#{ip}'"
end
if subnet_ids.empty?
raise Bosh::Clouds::VMCreationFailed.new(false), "Network '#{NetworkConfigurator.get_gateway_network_id(network_spec)}' does not contain any subnet to match the IP '#{ip}'"
end
subnet_ids.first
end
def validate_configuration(pool_spec)
raise Bosh::Clouds::VMCreationFailed.new(false), 'Load balancer pool defined without a name' unless pool_spec['name']
unless pool_spec['port']
raise Bosh::Clouds::VMCreationFailed.new(false), "Load balancer pool '#{pool_spec['name']}' has no port definition"
end
end
class LoadbalancerPoolMembership
attr_reader :name, :port, :membership_id, :pool_id
def initialize(name, port, pool_id, membership_id)
@name = name
@port = port
@pool_id = pool_id
@membership_id = membership_id
end
end
def openstack_pool_id(pool_name)
pools = @openstack.with_openstack(retryable: true) {
@openstack.network.list_lbaas_pools('name' => pool_name).body['pools']
}
if pools.empty?
raise Bosh::Clouds::VMCreationFailed.new(false), "Load balancer pool '#{pool_name}' does not exist"
elsif pools.size > 1
raise Bosh::Clouds::VMCreationFailed.new(false), "Load balancer pool '#{pool_name}' exists multiple times. Make sure to use unique naming."
end
pools.first['id']
end
class LoadBalancerResource
attr_reader :id
def initialize(loadbalancer_id, openstack)
@id = loadbalancer_id
@openstack = openstack
end
def reload
true
end
def provisioning_status
@openstack.with_openstack(retryable: true) do
@openstack.network.get_lbaas_loadbalancer(@id).body['loadbalancer']['provisioning_status']
end
end
class NotFound < StandardError; end
class NotSupportedConfiguration < StandardError; end
end
end
end