Merge ~hloeung/content-cache-charm:sites-persistent-ports-kv into content-cache-charm:master

Proposed by Haw Loeung
Status: Merged
Approved by: Haw Loeung
Approved revision: ef84acfe55e6ff83dc1ef2fa0d6915b60fc837c6
Merged at revision: c3721b36276e5b48cc656cd547961bc38bc00cf3
Proposed branch: ~hloeung/content-cache-charm:sites-persistent-ports-kv
Merge into: content-cache-charm:master
Diff against target: 387 lines (+311/-9)
3 files modified
reactive/content_cache.py (+78/-9)
tests/unit/files/config_test_sites_map.txt (+127/-0)
tests/unit/test_content_cache.py (+106/-0)
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Content Cache Charmers Pending
Review via email: mp+380871@code.launchpad.net

Commit message

Don't shuffling sites and port-pair mappings - LP:1865945

When sites are added or removed, site to port-pair mappings are reshuffled and re-allocated sequentially. This can cause issues if either HAProxy or Nginx fails to reload where sites end up talking to incorrect backends. Instead, we want to keep sites to port-pair mappings persistent.

To post a comment you must log in.
Revision history for this message
Joel Sing (jsing) wrote :

Quick pass, few comments, but general approach seems fine.

Revision history for this message
Haw Loeung (hloeung) :
Revision history for this message
Haw Loeung (hloeung) :
Revision history for this message
Haw Loeung (hloeung) :
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Unable to determine commit message from repository - please click "Set commit message" and enter the commit message manually.

Revision history for this message
Stuart Bishop (stub) wrote :

Code looks good. A few inline comments, but likely nothing requiring a rereview.

review: Approve
Revision history for this message
Haw Loeung (hloeung) :
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision c3721b36276e5b48cc656cd547961bc38bc00cf3

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/reactive/content_cache.py b/reactive/content_cache.py
2index 450c63f..08f9bbc 100644
3--- a/reactive/content_cache.py
4+++ b/reactive/content_cache.py
5@@ -12,7 +12,7 @@ import yaml
6 from charms import reactive
7 from charms.layer import status
8 from charmhelpers import context
9-from charmhelpers.core import hookenv, host
10+from charmhelpers.core import hookenv, host, unitdata
11 from charmhelpers.contrib.charmsupport import nrpe
12
13 from lib import utils
14@@ -500,26 +500,95 @@ def check_haproxy_alerts():
15 reactive.set_flag('nagios-nrpe-telegraf.configured')
16
17
18+def cleanout_sites(site_ports_map, sites):
19+ new_site_ports_map = {}
20+ for site, site_conf in site_ports_map.items():
21+ if site not in sites:
22+ continue
23+
24+ site_map = {'locations': {}}
25+ site_map['cache_port'] = site_conf['cache_port']
26+ for location, loc_conf in site_conf.get('locations', {}).items():
27+ site_map['locations'][location] = loc_conf
28+
29+ new_site_ports_map[site] = site_map
30+
31+ return new_site_ports_map
32+
33+
34+def allocated_ports(site_ports_map):
35+ allocated_ports = []
36+ for site, site_conf in site_ports_map.items():
37+ allocated_ports.append(site_conf['cache_port'])
38+ for location, loc_conf in site_conf.get('locations', {}).items():
39+ if 'backend_port' not in loc_conf:
40+ continue
41+ allocated_ports.append(loc_conf['backend_port'])
42+ return sorted(allocated_ports)
43+
44+
45+def ports_map_lookup(ports_map, site, base_port, blacklist_ports=None, key=None):
46+ if key:
47+ (unused_port, port) = utils.next_port_pair(0, base_port, blacklist_ports=blacklist_ports)
48+ else:
49+ (port, unused_port) = utils.next_port_pair(base_port, 0, blacklist_ports=blacklist_ports)
50+
51+ if site not in ports_map:
52+ return port
53+
54+ if key:
55+ if 'locations' not in ports_map[site] or key not in ports_map[site]['locations']:
56+ return port
57+ return ports_map[site]['locations'][key].get('backend_port', port)
58+ else:
59+ return ports_map[site].get('cache_port', port)
60+
61+
62 def sites_from_config(sites_yaml, sites_secrets=None, blacklist_ports=None):
63 conf = yaml.safe_load(sites_yaml)
64 sites = interpolate_secrets(conf, sites_secrets)
65 cache_port = 0
66 backend_port = 0
67 new_sites = {}
68+ existing_site_ports_map = unitdata.kv().get('existing_site_ports_map', {})
69+ new_site_ports_map = {}
70+ if not blacklist_ports:
71+ blacklist_ports = []
72+
73+ blacklist_ports += allocated_ports(existing_site_ports_map)
74+ # We need to clean out sites and backends that no longer
75+ # exists. This should happen after we've built a list of ports to
76+ # blacklist to ensure that we don't reuse one for a site that's
77+ # being or been removed.
78+ existing_site_ports_map = cleanout_sites(existing_site_ports_map, sites)
79 for site, site_conf in sites.items():
80 if not site_conf:
81 continue
82- (cache_port, unused_backend_port) = utils.next_port_pair(
83- cache_port, backend_port, blacklist_ports=blacklist_ports
84- )
85+ site_ports_map = {'locations': {}}
86+ cache_port = ports_map_lookup(existing_site_ports_map, site, cache_port, blacklist_ports)
87 site_conf['cache_port'] = cache_port
88+ site_ports_map['cache_port'] = cache_port
89+ # With the new port allocated, make sure it's blacklisted so it doesn't
90+ # get reused later.
91+ blacklist_ports.append(cache_port)
92+
93 for location, loc_conf in site_conf.get('locations', {}).items():
94- if loc_conf and loc_conf.get('backends'):
95- (unused_cache_port, backend_port) = utils.next_port_pair(
96- cache_port, backend_port, blacklist_ports=blacklist_ports
97- )
98- loc_conf['backend_port'] = backend_port
99+ if not loc_conf or not loc_conf.get('backends'):
100+ continue
101+ location_map = {}
102+ backend_port = ports_map_lookup(existing_site_ports_map, site, backend_port, blacklist_ports, key=location)
103+ loc_conf['backend_port'] = backend_port
104+ location_map['backend_port'] = backend_port
105+
106+ # With the new port allocated, make sure it's blacklisted so it doesn't
107+ # get reused later.
108+ blacklist_ports.append(backend_port)
109+ site_ports_map['locations'][location] = location_map
110+
111 new_sites[site] = site_conf
112+ new_site_ports_map[site] = site_ports_map
113+
114+ unitdata.kv().set('existing_site_ports_map', new_site_ports_map)
115 return new_sites
116
117
118diff --git a/tests/unit/files/config_test_sites_map.txt b/tests/unit/files/config_test_sites_map.txt
119new file mode 100644
120index 0000000..7158e24
121--- /dev/null
122+++ b/tests/unit/files/config_test_sites_map.txt
123@@ -0,0 +1,127 @@
124+site1.local:
125+ cache_port: 6080
126+ locations:
127+ /:
128+ backend_port: 8080
129+ backends:
130+ - 127.0.1.10:80
131+ - 127.0.1.11:80
132+ - 127.0.1.12:80
133+ origin-headers:
134+ - X-Origin-Key: Sae6oob2aethuosh
135+ - X-Some-Header-1: something one two three
136+ - X-Some-Header-2: something:one:two:three
137+ signed-url-hmac-key: SrRorTsImr92B6FfSKBFrSIiR5NYGS+gdjd8oGoVH44=
138+ port: 80
139+site2.local:
140+ cache_port: 6081
141+ locations:
142+ /:
143+ backend-check-method: GET
144+ backend-check-path: /check/
145+ backend-maxconn: 1024
146+ backend-tls: true
147+ backend_port: 8081
148+ backends:
149+ - 127.0.1.10:443
150+ - 127.0.1.11:443
151+ - 127.0.1.12:443
152+ /my-local-content/:
153+ extra-config:
154+ - root /var/www/html
155+ /my-local-content2/:
156+ extra-config:
157+ - root /var/www/html
158+ redirect-http-to-https: true
159+ tls-cert-bundle-path: /etc/haproxy/site2-bundle.crt
160+site3.local:
161+ cache_port: 6082
162+ locations:
163+ /:
164+ backend-options:
165+ - forwardfor except 127.0.0.1
166+ - forceclose
167+ backend_port: 8082
168+ backends:
169+ - 127.0.1.10:80
170+ - 127.0.1.11:80
171+ - 127.0.1.12:80
172+ cache-maxconn: 4096
173+site4.local:
174+ cache_port: 6083
175+ locations:
176+ /:
177+ extra-config:
178+ - autoindex on
179+ /ubuntu/pool/:
180+ extra-config:
181+ - autoindex on
182+ - auth_request /auth
183+site5:
184+ cache_port: 6084
185+ locations:
186+ /:
187+ backend_port: 8083
188+ backends:
189+ - 127.0.1.10:80
190+ /auth:
191+ backend-path: /auth-check/
192+ backend_port: 8084
193+ backends:
194+ - 127.0.1.11:80
195+ cache-background-update: None
196+ cache-min-uses: None
197+ cache-validity: 200 401 1h
198+ modifier: '='
199+ site-name: site5.local
200+site6.local:
201+ cache_port: 6085
202+ locations:
203+ /:
204+ backend-tls: true
205+ backend_port: 8085
206+ backends:
207+ - 127.0.1.10:443
208+site7.local:
209+ cache_port: 6086
210+ locations:
211+ /:
212+ backend-tls: false
213+ backend_port: 8086
214+ backends:
215+ - 127.0.1.10:80
216+ port: 444
217+ tls-cert-bundle-path: /etc/haproxy/site7-bundle.crt
218+site8.local:
219+ cache_port: 6087
220+ locations:
221+ /:
222+ backend-tls: false
223+ backend_port: 8087
224+ backends:
225+ - 127.0.1.10:80
226+ /auth:
227+ backend-tls: true
228+ backend_port: 8088
229+ backends:
230+ - 127.0.1.10:443
231+ site-name: auth.site8.local
232+ port: 444
233+ tls-cert-bundle-path: /etc/haproxy/site8-bundle.crt
234+site9.local:
235+ cache_port: 6088
236+ locations:
237+ /:
238+ backend-inter-time: 1m
239+ backend-tls: false
240+ backend_port: 8089
241+ backends:
242+ - 127.0.1.15:80
243+ extra-config:
244+ - proxy_force_ranges off
245+ /private/content:
246+ extra-config:
247+ - root /srv/example1.com/content/
248+ - autoindex on
249+ - auth_request /auth
250+ nagios-expect: 401 Unauthorized
251diff --git a/tests/unit/test_content_cache.py b/tests/unit/test_content_cache.py
252index 4cbbf05..1abaff0 100644
253--- a/tests/unit/test_content_cache.py
254+++ b/tests/unit/test_content_cache.py
255@@ -7,12 +7,14 @@ from unittest import mock
256
257 import freezegun
258 import jinja2
259+import yaml
260
261 # We also need to mock up charms.layer so we can run unit tests without having
262 # to build the charm and pull in layers such as layer-status.
263 sys.modules['charms.layer'] = mock.MagicMock()
264
265 from charms.layer import status # NOQA: E402
266+from charmhelpers.core import unitdata # NOQA: E402
267
268 # Add path to where our reactive layer lives and import.
269 sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
270@@ -25,6 +27,8 @@ class TestCharm(unittest.TestCase):
271 self.maxDiff = None
272 self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
273 self.addCleanup(shutil.rmtree, self.tmpdir)
274+ os.environ['UNIT_STATE_DB'] = os.path.join(self.tmpdir, '.unit-state.db')
275+ unitdata.kv().set('existing_site_ports_map', {})
276
277 self.charm_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
278
279@@ -774,6 +778,108 @@ site5.local:
280 }
281 self.assertEqual(want, content_cache.sites_from_config(config_yaml))
282
283+ def test_allocated_ports(self):
284+ with open('tests/unit/files/config_test_sites_map.txt', 'r', encoding='utf-8') as f:
285+ config_yaml = f.read()
286+
287+ sites = yaml.safe_load(config_yaml)
288+ sites_list = list(sites.keys())
289+
290+ want = [
291+ 6080,
292+ 8080,
293+ 6081,
294+ 8081,
295+ 6082,
296+ 8082,
297+ 6083,
298+ 6084,
299+ 8083,
300+ 8084,
301+ 6085,
302+ 8085,
303+ 6086,
304+ 8086,
305+ 6087,
306+ 8087,
307+ 8088,
308+ 6088,
309+ 8089,
310+ ]
311+ self.assertEqual(sorted(want), content_cache.allocated_ports(sites))
312+
313+ want = []
314+ self.assertEqual(want, content_cache.allocated_ports({}))
315+
316+ new = {}
317+ new[sites_list[1]] = sites[sites_list[1]]
318+ new[sites_list[len(sites_list) - 1]] = sites[sites_list[len(sites_list) - 1]]
319+ want = [6081, 6088, 8081, 8089]
320+ self.assertEqual(want, content_cache.allocated_ports(new))
321+
322+ def test_sites_from_config_no_reshuffling(self):
323+ with open('tests/unit/files/config_test_sites_map.txt', 'r', encoding='utf-8') as f:
324+ config_yaml = f.read()
325+
326+ sites = yaml.safe_load(config_yaml)
327+ sites_list = list(sites.keys())
328+
329+ want = sites
330+ self.assertEqual(want, content_cache.sites_from_config(config_yaml))
331+
332+ # Remove all except second and last site for testing. Check to
333+ # make sure it's correct and ports aren't reshuffled.
334+ new = {}
335+ new[sites_list[1]] = sites[sites_list[1]]
336+ new[sites_list[len(sites_list) - 1]] = sites[sites_list[len(sites_list) - 1]]
337+ config_yaml = yaml.safe_dump(new, indent=4, default_flow_style=False)
338+ want = new
339+ self.assertEqual(want, content_cache.sites_from_config(config_yaml))
340+
341+ # Add two sites back and make sure the existing two aren't reshuffled.
342+ new = {}
343+ new[sites_list[1]] = sites[sites_list[1]]
344+ new[sites_list[len(sites_list) - 1]] = sites[sites_list[len(sites_list) - 1]]
345+ new[sites_list[0]] = sites[sites_list[0]]
346+ new[sites_list[2]] = sites[sites_list[2]]
347+ config_yaml = yaml.safe_dump(new, indent=4, default_flow_style=False)
348+ want = new
349+ self.assertEqual(want, content_cache.sites_from_config(config_yaml))
350+
351+ # Add new site somewhere in the middle.
352+ new = {}
353+ new[sites_list[1]] = sites[sites_list[1]]
354+ new[sites_list[len(sites_list) - 1]] = sites[sites_list[len(sites_list) - 1]]
355+ new[sites_list[0]] = sites[sites_list[0]]
356+ new[sites_list[2]] = sites[sites_list[2]]
357+ new['site11'] = {'locations': {'/': {'backend-tls': True, 'backends': ['127.0.1.10:443']}}}
358+ config_yaml = yaml.safe_dump(new, indent=4, default_flow_style=False)
359+ want = new
360+ want['site11']['cache_port'] = 6083
361+ want['site11']['locations']['/']['backend_port'] = 8083
362+ self.assertEqual(want, content_cache.sites_from_config(config_yaml))
363+
364+ # Add a new site at the start, in the middle, and at the
365+ # end. We also want to make sure we don't recycle and reuse a
366+ # port for a site that's just been or is being removed.
367+ new = {}
368+ new[sites_list[1]] = sites[sites_list[1]]
369+ new[sites_list[len(sites_list) - 1]] = sites[sites_list[len(sites_list) - 1]]
370+ new[sites_list[0]] = sites[sites_list[0]]
371+ new[sites_list[2]] = sites[sites_list[2]]
372+ new['site0'] = {'locations': {'/': {'backend-tls': True, 'backends': ['127.0.1.10:443']}}}
373+ new['site666'] = {'locations': {'/': {'backend-tls': True, 'backends': ['127.0.1.10:443']}}}
374+ new['zzz'] = {'locations': {'/': {'backend-tls': True, 'backends': ['127.0.1.10:443']}}}
375+ config_yaml = yaml.safe_dump(new, indent=4, default_flow_style=False)
376+ want = new
377+ want['site0']['cache_port'] = 6084
378+ want['site0']['locations']['/']['backend_port'] = 8084
379+ want['site666']['cache_port'] = 6085
380+ want['site666']['locations']['/']['backend_port'] = 8085
381+ want['zzz']['cache_port'] = 6089
382+ want['zzz']['locations']['/']['backend_port'] = 8090
383+ self.assertEqual(want, content_cache.sites_from_config(config_yaml))
384+
385 def test_sites_from_config_blacklist_ports(self):
386 blacklist_ports = [6080, 8080]
387 config_yaml = '''

Subscribers

People subscribed via source and target branches