Merge ~aluria/charm-prometheus-blackbox-exporter-peer:rewrite-move-complexity into charm-prometheus-blackbox-exporter-peer:master
- Git
- lp:~aluria/charm-prometheus-blackbox-exporter-peer
- rewrite-move-complexity
- Merge into master
Status: | Superseded |
---|---|
Proposed branch: | ~aluria/charm-prometheus-blackbox-exporter-peer:rewrite-move-complexity |
Merge into: | charm-prometheus-blackbox-exporter-peer:master |
Diff against target: |
1183 lines (+881/-117) 25 files modified
.gitignore (+22/-0) Makefile (+56/-0) config.yaml (+14/-13) dev/null (+0/-94) icon.svg (+12/-0) interfaces/.empty (+1/-0) layer.yaml (+12/-3) layers/.empty (+1/-0) lib/lib_bb_peer_exporter.py (+148/-0) lib/lib_network.py (+55/-0) metadata.yaml (+17/-7) reactive/prometheus_blackbox_exporter_peer.py (+85/-0) tests/functional/bundle.yaml.j2 (+13/-0) tests/functional/conftest.py (+68/-0) tests/functional/juju_tools.py (+71/-0) tests/functional/requirements.txt (+7/-0) tests/functional/test_deploy.py (+131/-0) tests/unit/conftest.py (+60/-0) tests/unit/requirements.txt (+5/-0) tests/unit/samples/expected.multi (+1/-0) tests/unit/samples/expected.single (+1/-0) tests/unit/test_lib_bb_peer_exporter.py (+12/-0) tests/unit/test_lib_network.py (+40/-0) tox.ini (+48/-0) wheelhouse.txt (+1/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Peter Sabaini (community) | Approve | ||
Canonical IS Reviewers | Pending | ||
Canonical IS Reviewers | Pending | ||
Review via email: mp+374648@code.launchpad.net |
This proposal has been superseded by a proposal from 2020-04-16.
Commit message
Description of the change
- bf9bd8b... by Alvaro Uria
-
Improve lib_network unit test
Alvaro Uria (aluria) wrote : | # |
Peter Sabaini (peter-sabaini) wrote : | # |
Thanks, good stuff! I've added an inline style nit comment but otherwise +1
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.
🤖 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.
Unmerged commits
- bf9bd8b... by Alvaro Uria
-
Improve lib_network unit test
- cce865f... by Alvaro Uria
-
Fixes per initial review
- 8633d54... by Alvaro Uria
-
Update unit tests
- ca94a30... by Alvaro Uria
-
Rewrite the blackbox-exporter relation
* Each unit exposes its available networks with the blackbox-exporter
remote units (juju config updates the relation info)
* lib_network uses psutil everywhere instead of a mix of psutil and
netifaces (ipaddress module is still used for validation)
* module for "icmp_jump" added to the default config - 1901aed... by Alvaro Uria
-
Rename p-bb-peer-exporter to p-bb-exporter-peer
- 371fd4b... by Alvaro Uria
-
Rewrite charm using template-
python- pytest * Cloned template-
python- pytest
* Moved the reactive script helpers to lib_bb_peer_exporter (generic)
and lib_network (network related helpers)
* Created minimum unit (for the libs) and functional tests
* wheelhouse.txt installs psutil as well as netifaces and pyroute2.
* Linting now runs flake8-docstrings, flake8-import-order and other
extra checks. All scripts have been updated following parser
recommendations.
* Better use of the peer-discovery interface. Available (and transient)
states when units join or leave a peer relation are used to trigger
config changes. - a458c6a... by Andrea Ieri
-
Noop readability refactor
- 881b419... by Andrea Ieri
-
Lookup source ips for all probes and provide the address over the relation
- 64d1422... by Andrea Ieri
-
Filter out uninteresting IPs and interfaces
- 413057f... by Andrea Ieri
-
Publish the external IP in case we're running cross-model relations
Preview Diff
1 | diff --git a/.gitignore b/.gitignore |
2 | new file mode 100644 |
3 | index 0000000..32e2995 |
4 | --- /dev/null |
5 | +++ b/.gitignore |
6 | @@ -0,0 +1,22 @@ |
7 | +# Byte-compiled / optimized / DLL files |
8 | +__pycache__/ |
9 | +*.py[cod] |
10 | +*$py.class |
11 | + |
12 | +# Log files |
13 | +*.log |
14 | + |
15 | +.tox/ |
16 | +.coverage |
17 | + |
18 | +# vi |
19 | +.*.swp |
20 | + |
21 | +# pycharm |
22 | +.idea/ |
23 | + |
24 | +# version data |
25 | +repo-info |
26 | + |
27 | +# reports |
28 | +report/* |
29 | diff --git a/Makefile b/Makefile |
30 | new file mode 100644 |
31 | index 0000000..eb95ca8 |
32 | --- /dev/null |
33 | +++ b/Makefile |
34 | @@ -0,0 +1,56 @@ |
35 | +PROJECTPATH = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) |
36 | +DIRNAME = $(notdir $(PROJECTPATH:%/=%)) |
37 | + |
38 | +ifndef CHARM_BUILD_DIR |
39 | + CHARM_BUILD_DIR := /tmp/$(DIRNAME)-builds |
40 | + $(warning Warning CHARM_BUILD_DIR was not set, defaulting to $(CHARM_BUILD_DIR)) |
41 | +endif |
42 | + |
43 | +help: |
44 | + @echo "This project supports the following targets" |
45 | + @echo "" |
46 | + @echo " make help - show this text" |
47 | + @echo " make lint - run flake8" |
48 | + @echo " make test - run the unittests and lint" |
49 | + @echo " make unittest - run the tests defined in the unittest subdirectory" |
50 | + @echo " make functional - run the tests defined in the functional subdirectory" |
51 | + @echo " make release - build the charm" |
52 | + @echo " make clean - remove unneeded files" |
53 | + @echo "" |
54 | + |
55 | +lint: |
56 | + @echo "Running flake8" |
57 | + @tox -e lint |
58 | + |
59 | +test: lint unittest functional |
60 | + |
61 | +unittest: |
62 | + @tox -e unit |
63 | + |
64 | +functional: build |
65 | + @PYTEST_KEEP_MODEL=$(PYTEST_KEEP_MODEL) \ |
66 | + PYTEST_CLOUD_NAME=$(PYTEST_CLOUD_NAME) \ |
67 | + PYTEST_CLOUD_REGION=$(PYTEST_CLOUD_REGION) \ |
68 | + CHARM_BUILD_DIR=$(CHARM_BUILD_DIR) \ |
69 | + tox -e functional |
70 | + |
71 | +build: |
72 | + @echo "Building charm to base directory $(CHARM_BUILD_DIR)" |
73 | + @CHARM_LAYERS_DIR=./layers \ |
74 | + CHARM_INTERFACES_DIR=./interfaces \ |
75 | + TERM=linux \ |
76 | + CHARM_BUILD_DIR=$(CHARM_BUILD_DIR) \ |
77 | + charm build . --force |
78 | + |
79 | +release: clean build |
80 | + @echo "Charm is built at $(CHARM_BUILD_DIR)" |
81 | + |
82 | +clean: |
83 | + @echo "Cleaning files" |
84 | + @if [ -d $(CHARM_BUILD_DIR) ] ; then rm -r $(CHARM_BUILD_DIR) ; fi |
85 | + @if [ -d .tox ] ; then rm -r .tox ; fi |
86 | + @if [ -d .pytest_cache ] ; then rm -r .pytest_cache ; fi |
87 | + @find . -iname __pycache__ -exec rm -r {} + |
88 | + |
89 | +# The targets below don't depend on a file |
90 | +.PHONY: lint test unittest functional build release clean help |
91 | diff --git a/config.yaml b/config.yaml |
92 | index d1dc24d..3a9977e 100644 |
93 | --- a/config.yaml |
94 | +++ b/config.yaml |
95 | @@ -1,21 +1,22 @@ |
96 | options: |
97 | - snap_channel: |
98 | - default: "stable" |
99 | - type: string |
100 | - description: | |
101 | - If install_method is set to "snap" this option controlls channel name. |
102 | - Supported values are: "stable", "candidate", "beta" and "edge" |
103 | modules: |
104 | default: | |
105 | - http_2xx: |
106 | - prober: http |
107 | - timeout: 10s |
108 | - tcp_connect: |
109 | - prober: tcp |
110 | - timeout: 10s |
111 | icmp: |
112 | prober: icmp |
113 | timeout: 10s |
114 | + icmp: |
115 | + preferred_ip_protocol: "ip4" |
116 | + payload_size: 1472 |
117 | + icmp_jumbo: |
118 | + prober: icmp |
119 | + timeout: 10s |
120 | + icmp: |
121 | + preferred_ip_protocol: "ip4" |
122 | + payload_size: 8972 |
123 | type: string |
124 | description: | |
125 | - Blackbox exporter configuratin in raw YAML format |
126 | + Blackbox exporter configuration in raw YAML format |
127 | + scrape-interval: |
128 | + type: string |
129 | + default: "60s" |
130 | + description: Set the blackbox exporter scrape jobs custom interval. |
131 | diff --git a/icon.svg b/icon.svg |
132 | new file mode 100644 |
133 | index 0000000..ffa6296 |
134 | --- /dev/null |
135 | +++ b/icon.svg |
136 | @@ -0,0 +1,12 @@ |
137 | +<?xml version="1.0" encoding="UTF-8"?> |
138 | +<svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> |
139 | + <!-- Generator: Sketch 45.2 (43514) - http://www.bohemiancoding.com/sketch --> |
140 | + <title>prometheus</title> |
141 | + <desc>Created with Sketch.</desc> |
142 | + <defs></defs> |
143 | + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
144 | + <g id="prometheus" fill-rule="nonzero" fill="#B8B8B8"> |
145 | + <path d="M50.0004412,3.12178676e-15 C22.3871247,3.12178676e-15 0,22.3848726 0,49.9995588 C0,77.6133626 22.3871247,100 50.0004412,100 C77.6137577,100 100,77.6133626 100,49.9995588 C100,22.3848726 77.6128753,-1.77635684e-15 50.0004412,3.12178676e-15 Z M49.8864141,88.223871 C42.8334797,88.223871 37.1152482,83.6412396 37.1152482,77.9899953 L62.6575799,77.9899953 C62.6575799,83.6404692 56.9393484,88.223871 49.8864141,88.223871 Z M70.9802642,74.6007896 L28.7901876,74.6007896 L28.7901876,67.159792 L70.9810563,67.159792 L70.9810563,74.6007896 L70.9802642,74.6007896 Z M70.8289715,63.3298895 L28.9105881,63.3298895 C28.771177,63.1734883 28.6285974,63.0193985 28.493939,62.860686 C24.1753633,57.7603128 23.1582959,55.0976406 22.1705366,52.3841188 C22.1539023,52.2947467 27.4071661,53.4280774 31.1324525,54.2432125 C31.1324525,54.2432125 33.0493552,54.6746641 35.8518351,55.1716037 C33.1610425,52.1036753 31.5633596,48.2036619 31.5633596,44.2173581 C31.5633596,35.4658266 38.4642091,27.8183486 35.974612,21.6370353 C38.397672,21.8288772 40.9894511,26.6110549 41.164507,34.0882636 C43.740444,30.6258652 44.8185037,24.3027893 44.8185037,20.4258893 C44.8185037,16.4118494 47.5378123,11.7490913 50.257913,11.5896084 C47.8332688,15.4765242 50.886055,18.8087166 53.5998189,27.0748652 C54.6176783,30.1797752 54.4877725,35.4049611 55.2735442,38.7186628 C55.5341479,31.8362408 56.7508266,21.794207 61.2397056,18.3271859 C59.2594343,22.6933211 61.5327858,28.1565758 63.0876948,30.7830368 C65.5963025,35.020507 67.1171509,38.2309685 67.1171509,44.302878 C67.1171509,48.3739312 65.5717472,52.2069155 62.964918,55.2031922 C65.9289881,54.6623369 67.9757966,54.1746426 67.9757966,54.1746426 L77.6014994,52.3479077 C77.6022915,52.3471373 76.2034279,57.9421388 70.8289715,63.3298895 Z" id="path3023"></path> |
146 | + </g> |
147 | + </g> |
148 | +</svg> |
149 | \ No newline at end of file |
150 | diff --git a/interfaces/.empty b/interfaces/.empty |
151 | new file mode 100644 |
152 | index 0000000..792d600 |
153 | --- /dev/null |
154 | +++ b/interfaces/.empty |
155 | @@ -0,0 +1 @@ |
156 | +# |
157 | diff --git a/layer.yaml b/layer.yaml |
158 | index ddd95fc..a161477 100644 |
159 | --- a/layer.yaml |
160 | +++ b/layer.yaml |
161 | @@ -1,6 +1,15 @@ |
162 | -includes: ['layer:basic', 'interface:http', 'layer:snap'] |
163 | -repo: 'https://git.launchpad.net/prometheus-blackbox-exporter-charm' |
164 | -ignore: ['.*.swp' ] |
165 | +# exclude the interfaces and layers folders we use for submodules |
166 | +exclude: |
167 | + - interfaces |
168 | + - layers |
169 | +# include required layers here |
170 | +includes: |
171 | + - 'layer:basic' |
172 | + - 'layer:status' |
173 | + - 'interface:http' |
174 | + - 'interface:juju-info' |
175 | + - 'interface:peer-discovery' |
176 | +repo: 'https://git.launchpad.net/charm-prometheus-blackbox-exporter-peer' |
177 | options: |
178 | basic: |
179 | use_venv: true |
180 | diff --git a/layers/.empty b/layers/.empty |
181 | new file mode 100644 |
182 | index 0000000..792d600 |
183 | --- /dev/null |
184 | +++ b/layers/.empty |
185 | @@ -0,0 +1 @@ |
186 | +# |
187 | diff --git a/lib/lib_bb_peer_exporter.py b/lib/lib_bb_peer_exporter.py |
188 | new file mode 100644 |
189 | index 0000000..48203d8 |
190 | --- /dev/null |
191 | +++ b/lib/lib_bb_peer_exporter.py |
192 | @@ -0,0 +1,148 @@ |
193 | +"""General helpers.""" |
194 | +import os |
195 | +import subprocess |
196 | + |
197 | +from charmhelpers import fetch |
198 | +from charmhelpers.core import hookenv, host, unitdata |
199 | +from charmhelpers.core.templating import render |
200 | + |
201 | +from charms.reactive.helpers import any_file_changed, data_changed |
202 | + |
203 | +import lib_network |
204 | + |
205 | +import yaml |
206 | + |
207 | + |
208 | +APT_PKG_NAME = 'prometheus-blackbox-exporter' |
209 | +SVC_NAME = 'prometheus-blackbox-exporter' |
210 | +SVC_PATH = os.path.join('/usr/bin', SVC_NAME) |
211 | +PORT_DEF = 9115 # blackbox_exporter default port |
212 | +BLACKBOX_EXPORTER_YML_TMPL = 'blackbox.yaml.j2' |
213 | +CONF_FILE_PATH = '/etc/prometheus/blackbox.yml' |
214 | + |
215 | + |
216 | +class BBPeerExporterError(Exception): |
217 | + """Handle exceptions encountered in BBPeerExporterHelper.""" |
218 | + |
219 | + pass |
220 | + |
221 | + |
222 | +class BBPeerExporterHelper(): |
223 | + """General helpers.""" |
224 | + |
225 | + def __init__(self): |
226 | + """Load config.yaml.""" |
227 | + self.charm_config = hookenv.config() |
228 | + |
229 | + @property |
230 | + def is_config_changed(self): |
231 | + """Verify and update checksum if config has changed.""" |
232 | + return data_changed('blackbox-peer-exporter.config', self.charm_config) |
233 | + |
234 | + def bbexporter_relation_data_changed(self, keymap): |
235 | + """Verify and update checksum if provides relation data has changed.""" |
236 | + return data_changed('blackbox-exporter.relation_data', keymap) |
237 | + |
238 | + @property |
239 | + def is_blackbox_exporter_relation_enabled(self): |
240 | + """Verify if the blackbox-export relation exists.""" |
241 | + kv = unitdata.kv() |
242 | + return kv.get('blackbox_exporter', False) |
243 | + |
244 | + @property |
245 | + def enable_blackbox_exporter_relation(self): |
246 | + """Enable the blackbox-export flag for one time functions.""" |
247 | + kv = unitdata.kv() |
248 | + kv.set('blackbox_exporter', True) |
249 | + |
250 | + @property |
251 | + def disable_blackbox_exporter_relation(self): |
252 | + """Disable the blackbox-export flag for one time functions.""" |
253 | + kv = unitdata.kv() |
254 | + kv.set('blackbox_exporter', False) |
255 | + |
256 | + @property |
257 | + def modules(self): |
258 | + """Return the modules config parameter.""" |
259 | + return self.charm_config['modules'] |
260 | + |
261 | + @property |
262 | + def scrape_interval(self): |
263 | + """Return the scrape-interval config parameter.""" |
264 | + return self.charm_config['scrape-interval'] |
265 | + |
266 | + @property |
267 | + def port_def(self): |
268 | + """Return the port exposed by blackbox-exporter.""" |
269 | + return PORT_DEF |
270 | + |
271 | + @property |
272 | + def templates_changed(self): |
273 | + """Verify if any stored template has changed.""" |
274 | + return any_file_changed(['templates/{}'.format(tmpl) |
275 | + for tmpl in [BLACKBOX_EXPORTER_YML_TMPL]]) |
276 | + |
277 | + def install_packages(self): |
278 | + """Install the APT package and sets Linux capabilities.""" |
279 | + fetch.install(APT_PKG_NAME, fatal=True) |
280 | + cmd = ["setcap", "cap_net_raw+ep", SVC_PATH] |
281 | + try: |
282 | + subprocess.check_output(cmd) |
283 | + except subprocess.CalledProcessError as error: |
284 | + hookenv.log('unable to set linux capabilities: {}'.format(str(error)), |
285 | + hookenv.ERROR) |
286 | + raise BBPeerExporterError('Unable to set linux capabilities') |
287 | + |
288 | + def render_modules(self): |
289 | + """Generate /etc/prometheus/blackbox.yml from the template.""" |
290 | + try: |
291 | + modules = yaml.safe_load(self.modules) |
292 | + if 'modules' in modules: |
293 | + modules = modules['modules'] |
294 | + except (yaml.parser.ParserError, yaml.scanner.ScannerError, TypeError) as error: |
295 | + hookenv.log('error retrieving modules yaml config: {}'.format(str(error)), |
296 | + hookenv.ERROR) |
297 | + # Could not render modules |
298 | + return False |
299 | + |
300 | + context = {'modules': yaml.safe_dump(modules, default_flow_style=False)} |
301 | + render(source=BLACKBOX_EXPORTER_YML_TMPL, target=CONF_FILE_PATH, context=context) |
302 | + hookenv.open_port(PORT_DEF) |
303 | + return True |
304 | + |
305 | + def restart_bbexporter(self): |
306 | + """Restart the exporter daemon.""" |
307 | + if not host.service_running(SVC_NAME): |
308 | + hookenv.log('Starting {}...'.format(SVC_NAME)) |
309 | + host.service_start(SVC_NAME) |
310 | + else: |
311 | + hookenv.log('Restarting {}, config file changed...'.format(SVC_NAME)) |
312 | + host.service_restart(SVC_NAME) |
313 | + |
314 | + def cleanup_blackbox_exporter_relation(self, rid): |
315 | + """Remove network data previously shared with Prometheus.""" |
316 | + relation_keys = ["ip_address", "port", "scrape_interval", "unit-principal", |
317 | + "unit-networks", |
318 | + ] |
319 | + relation_settings = {relkey: None for relkey in relation_keys} |
320 | + # Resets information previously shared with the "prometheus" application(s) |
321 | + hookenv.relation_set(relation_id=rid, relation_settings=relation_settings) |
322 | + |
323 | + def configure_blackbox_exporter_relation(self): |
324 | + """Retrieve network data and share it with Prometheus.""" |
325 | + relation_settings = { |
326 | + 'private-address': hookenv.unit_get('private-address'), |
327 | + 'port': self.port_def, |
328 | + 'scrape_interval': self.scrape_interval, |
329 | + 'unit-principal': hookenv.principal_unit(), |
330 | + 'unit-networks': lib_network.get_unit_ipv4_networks(), |
331 | + } |
332 | + |
333 | + if not self.bbexporter_relation_data_changed(relation_settings): |
334 | + return |
335 | + |
336 | + # Shares information with the "prometheus" application |
337 | + for rel_id in hookenv.relation_ids('blackbox-exporter'): |
338 | + relation_settings['ip_address'] = \ |
339 | + hookenv.ingress_address(rid=rel_id, unit=hookenv.local_unit()) |
340 | + hookenv.relation_set(relation_id=rel_id, relation_settings=relation_settings) |
341 | diff --git a/lib/lib_network.py b/lib/lib_network.py |
342 | new file mode 100644 |
343 | index 0000000..974bf9d |
344 | --- /dev/null |
345 | +++ b/lib/lib_network.py |
346 | @@ -0,0 +1,55 @@ |
347 | +"""Network helpers.""" |
348 | +import ipaddress |
349 | +import json |
350 | +import re |
351 | +import socket |
352 | + |
353 | +import psutil |
354 | + |
355 | + |
356 | +IFACE_WHITELIST_PATTERN = "^(en|eth|bond|br)" |
357 | +IFACE_WHITELIST_RE = re.compile(IFACE_WHITELIST_PATTERN) |
358 | + |
359 | + |
360 | +def get_unit_ipv4_networks(): |
361 | + """Return a list of IPv4 network blocks available on the host (json format). |
362 | + |
363 | + :returns: dict<dict> |
364 | + """ |
365 | + networks = {} |
366 | + # whitelist of available interfaces |
367 | + unit_ifaces = {iface_name: iface_info |
368 | + for iface_name, iface_info in psutil.net_if_addrs().items() |
369 | + if IFACE_WHITELIST_RE.search(iface_name)} |
370 | + iface_names = unit_ifaces.keys() |
371 | + unit_ifaces_stats = {iface_name: iface_stats |
372 | + for iface_name, iface_stats in psutil.net_if_stats().items() |
373 | + if iface_name in iface_names} |
374 | + for iface_name, iface_info in unit_ifaces.items(): |
375 | + # Retrieve the primary IPv4 address (no VIPs or secondary IPs) |
376 | + # we don't want floating IPs to be configured for probes |
377 | + ipv4_addrs = [real_ip for real_ip in iface_info |
378 | + if real_ip.family == socket.AF_INET] |
379 | + # No IPv4 addresses configured |
380 | + if not ipv4_addrs: |
381 | + continue |
382 | + |
383 | + ip_addr = ipv4_addrs[0] |
384 | + network = "{}/{}".format(ip_addr.address, ip_addr.netmask) |
385 | + ip_ipv4 = ipaddress.IPv4Interface(network) |
386 | + invalid = any((ip_ipv4.is_multicast, ip_ipv4.is_reserved, |
387 | + ip_ipv4.is_link_local, ip_ipv4.is_loopback)) |
388 | + if invalid: |
389 | + continue |
390 | + |
391 | + mtu = 1500 |
392 | + if iface_name in unit_ifaces_stats: |
393 | + mtu = unit_ifaces_stats[iface_name].mtu |
394 | + |
395 | + networks[str(ip_ipv4.network)] = { |
396 | + "iface": iface_name, |
397 | + "ip": str(ip_ipv4.ip), |
398 | + "net": str(ip_ipv4.network), |
399 | + "mtu": str(mtu), |
400 | + } |
401 | + return json.dumps(networks) |
402 | diff --git a/metadata.yaml b/metadata.yaml |
403 | index 4f9228b..fccea60 100644 |
404 | --- a/metadata.yaml |
405 | +++ b/metadata.yaml |
406 | @@ -1,15 +1,25 @@ |
407 | -name: prometheus-blackbox-exporter |
408 | -summary: Blackbox exporter for Prometheus |
409 | -maintainer: Jacek Nykis <jacek.nykis@canonical.com> |
410 | +name: prometheus-blackbox-exporter-peer |
411 | +display-name: Prometheus Blackbox Exporter Peer |
412 | +summary: Blackbox peer exporter for Prometheus |
413 | +maintainer: Llama (LMA) Charmers <llama-charmers@lists.launchpad.net> |
414 | description: | |
415 | - The blackbox exporter allows blackbox probing of |
416 | - endpoints over HTTP, HTTPS, DNS, TCP and ICMP. |
417 | + The blackbox peer exporter allows blackbox probing of |
418 | + endpoints over HTTP, HTTPS, DNS, TCP and ICMP. This |
419 | + charm allows all units to probe their peers, whereas |
420 | + the prometheus-blackbox-exporter charm deploys a single |
421 | + unit that can probe external endpoints. |
422 | tags: |
423 | - monitoring |
424 | series: |
425 | - - xenial |
426 | - bionic |
427 | -subordinate: false |
428 | +subordinate: true |
429 | provides: |
430 | blackbox-exporter: |
431 | interface: http |
432 | +requires: |
433 | + general-info: |
434 | + interface: juju-info |
435 | + scope: container |
436 | +peers: |
437 | + blackbox-peer: |
438 | + interface: peer-discovery |
439 | diff --git a/reactive/prometheus-blackbox-exporter.py b/reactive/prometheus-blackbox-exporter.py |
440 | deleted file mode 100644 |
441 | index a5d792a..0000000 |
442 | --- a/reactive/prometheus-blackbox-exporter.py |
443 | +++ /dev/null |
444 | @@ -1,94 +0,0 @@ |
445 | -import yaml |
446 | - |
447 | -from charmhelpers.core import host, hookenv |
448 | -from charmhelpers.core.templating import render |
449 | -from charms.reactive import ( |
450 | - when, when_not, set_state, remove_state |
451 | -) |
452 | -from charms.reactive.helpers import any_file_changed, data_changed |
453 | -from charms.layer import snap |
454 | - |
455 | - |
456 | -SNAP_NAME = 'prometheus-blackbox-exporter' |
457 | -SVC_NAME = 'snap.prometheus-blackbox-exporter.daemon' |
458 | -PORT_DEF = 9115 |
459 | -BLACKBOX_EXPORTER_YML_TMPL = 'blackbox.yaml.j2' |
460 | -CONF_FILE_PATH = '/var/snap/prometheus-blackbox-exporter/current/blackbox.yml' |
461 | - |
462 | - |
463 | -def templates_changed(tmpl_list): |
464 | - return any_file_changed(['templates/{}'.format(x) for x in tmpl_list]) |
465 | - |
466 | - |
467 | -@when_not('blackbox-exporter.installed') |
468 | -def install_packages(): |
469 | - hookenv.status_set('maintenance', 'Installing software') |
470 | - config = hookenv.config() |
471 | - channel = config.get('snap_channel', 'stable') |
472 | - snap.install(SNAP_NAME, channel=channel, force_dangerous=False) |
473 | - set_state('blackbox-exporter.installed') |
474 | - set_state('blackbox-exporter.do-check-reconfig') |
475 | - |
476 | - |
477 | -def get_modules(): |
478 | - config = hookenv.config() |
479 | - try: |
480 | - modules = yaml.safe_load(config.get('modules')) |
481 | - except: |
482 | - return None |
483 | - |
484 | - if 'modules' in modules: |
485 | - return yaml.safe_dump(modules['modules'], default_flow_style=False) |
486 | - else: |
487 | - return yaml.safe_dump(modules, default_flow_style=False) |
488 | - |
489 | - |
490 | -@when('blackbox-exporter.installed') |
491 | -@when('blackbox-exporter.do-reconfig-yaml') |
492 | -def write_blackbox_exporter_config_yaml(): |
493 | - modules = get_modules() |
494 | - render(source=BLACKBOX_EXPORTER_YML_TMPL, |
495 | - target=CONF_FILE_PATH, |
496 | - context={'modules': modules} |
497 | - ) |
498 | - hookenv.open_port(PORT_DEF) |
499 | - set_state('blackbox-exporter.do-restart') |
500 | - remove_state('blackbox-exporter.do-reconfig-yaml') |
501 | - |
502 | - |
503 | -@when('blackbox-exporter.started') |
504 | -def check_config(): |
505 | - set_state('blackbox-exporter.do-check-reconfig') |
506 | - |
507 | - |
508 | -@when('blackbox-exporter.do-check-reconfig') |
509 | -def check_reconfig_blackbox_exporter(): |
510 | - config = hookenv.config() |
511 | - |
512 | - if data_changed('blackbox-exporter.config', config): |
513 | - set_state('blackbox-exporter.do-reconfig-yaml') |
514 | - |
515 | - if templates_changed([BLACKBOX_EXPORTER_YML_TMPL]): |
516 | - set_state('blackbox-exporter.do-reconfig-yaml') |
517 | - |
518 | - remove_state('blackbox-exporter.do-check-reconfig') |
519 | - |
520 | - |
521 | -@when('blackbox-exporter.do-restart') |
522 | -def restart_blackbox_exporter(): |
523 | - if not host.service_running(SVC_NAME): |
524 | - hookenv.log('Starting {}...'.format(SVC_NAME)) |
525 | - host.service_start(SVC_NAME) |
526 | - else: |
527 | - hookenv.log('Restarting {}, config file changed...'.format(SVC_NAME)) |
528 | - host.service_restart(SVC_NAME) |
529 | - hookenv.status_set('active', 'Ready') |
530 | - set_state('blackbox-exporter.started') |
531 | - remove_state('blackbox-exporter.do-restart') |
532 | - |
533 | - |
534 | -# Relations |
535 | -@when('blackbox-exporter.started') |
536 | -@when('blackbox-exporter.available') # Relation name is "blackbox-exporter" |
537 | -def configure_blackbox_exporter_relation(target): |
538 | - target.configure(PORT_DEF) |
539 | diff --git a/reactive/prometheus_blackbox_exporter_peer.py b/reactive/prometheus_blackbox_exporter_peer.py |
540 | new file mode 100644 |
541 | index 0000000..0fa608a |
542 | --- /dev/null |
543 | +++ b/reactive/prometheus_blackbox_exporter_peer.py |
544 | @@ -0,0 +1,85 @@ |
545 | +"""Reactive script describing the prometheus-blackbox-exporter-peer behavior.""" |
546 | + |
547 | +from charmhelpers.core import hookenv |
548 | + |
549 | +from charms.layer import status |
550 | +from charms.reactive import ( |
551 | + clear_flag, set_flag, when, when_not |
552 | +) |
553 | + |
554 | +from lib_bb_peer_exporter import BBPeerExporterError, BBPeerExporterHelper |
555 | + |
556 | +helper = BBPeerExporterHelper() |
557 | + |
558 | + |
559 | +@when_not('prometheus-blackbox-exporter-peer.installed') |
560 | +def install_packages(): |
561 | + """Install APT package or get blocked until user action.""" |
562 | + status.maintenance('Installing software') |
563 | + try: |
564 | + helper.install_packages() |
565 | + set_flag('prometheus-blackbox-exporter-peer.installed') |
566 | + set_flag('prometheus-blackbox-exporter-peer.do-check-reconfig') |
567 | + except BBPeerExporterError as error: |
568 | + status.blocked(error) |
569 | + |
570 | + |
571 | +@when('prometheus-blackbox-exporter-peer.installed') |
572 | +@when('prometheus-blackbox-exporter-peer.do-reconfig-yaml') |
573 | +def write_blackbox_exporter_config_yaml(): |
574 | + """Generate /etc/prometheus/blackbox.yml.""" |
575 | + if not helper.render_modules(): |
576 | + status.blocked('Juju config: modules could not be loaded') |
577 | + return |
578 | + set_flag('prometheus-blackbox-exporter-peer.do-restart') |
579 | + clear_flag('prometheus-blackbox-exporter-peer.do-reconfig-yaml') |
580 | + |
581 | + |
582 | +@when('prometheus-blackbox-exporter-peer.started') |
583 | +def check_config(): |
584 | + """Trigger a config check.""" |
585 | + set_flag('prometheus-blackbox-exporter-peer.do-check-reconfig') |
586 | + |
587 | + |
588 | +@when('prometheus-blackbox-exporter-peer.do-check-reconfig') |
589 | +def check_reconfig_blackbox_exporter(): |
590 | + """Trigger a blackbox.yml config regeneration on changes.""" |
591 | + if helper.is_config_changed or helper.templates_changed: |
592 | + set_flag('prometheus-blackbox-exporter-peer.do-reconfig-yaml') |
593 | + |
594 | + clear_flag('prometheus-blackbox-exporter-peer.do-check-reconfig') |
595 | + |
596 | + |
597 | +@when('prometheus-blackbox-exporter-peer.do-restart') |
598 | +def restart_blackbox_exporter(): |
599 | + """Restart the blackbox-exporter daemon.""" |
600 | + helper.restart_bbexporter() |
601 | + status.active('Ready') |
602 | + set_flag('prometheus-blackbox-exporter-peer.started') |
603 | + clear_flag('prometheus-blackbox-exporter-peer.do-restart') |
604 | + |
605 | + |
606 | +@when('blackbox-exporter.available') |
607 | +def blackbox_peer_and_exporter_any_hook(): |
608 | + """First time the prometheus2:blackbox-exporter relation is seen.""" |
609 | + if not helper.is_blackbox_exporter_relation_enabled or helper.is_config_changed: |
610 | + helper.enable_blackbox_exporter_relation |
611 | + hookenv.log('Running blackbox exporter relation.') |
612 | + status.maintenance('Running blackbox exporter relation.') |
613 | + helper.configure_blackbox_exporter_relation() |
614 | + status.active('Ready') |
615 | + |
616 | + |
617 | +@when_not('blackbox-exporter.available') |
618 | +def blackbox_peer_and_no_exporter_any_hook(): |
619 | + """When the prometheus2:blackbox-exporter relation is gone.""" |
620 | + if helper.is_blackbox_exporter_relation_enabled: |
621 | + helper.disable_blackbox_exporter_relation |
622 | + rid, hook = hookenv.relation_id(), hookenv.hook_name() |
623 | + if hook != 'blackbox-exporter-relation-departed' or rid is None: |
624 | + return |
625 | + |
626 | + hookenv.log('Cleaning blackbox exporter relation data.') |
627 | + status.maintenance('Cleaning blackbox exporter relation data.') |
628 | + helper.cleanup_blackbox_exporter_relation(rid) |
629 | + status.active('Ready') |
630 | diff --git a/tests/functional/bundle.yaml.j2 b/tests/functional/bundle.yaml.j2 |
631 | new file mode 100644 |
632 | index 0000000..e52b841 |
633 | --- /dev/null |
634 | +++ b/tests/functional/bundle.yaml.j2 |
635 | @@ -0,0 +1,13 @@ |
636 | +applications: |
637 | + {{ ubuntu_appname }}: |
638 | + series: {{ series }} |
639 | + charm: cs:ubuntu |
640 | + num_units: 2 |
641 | + |
642 | + {{ bb_appname }}: |
643 | + series: {{ series }} |
644 | + charm: {{ charm_path }} |
645 | + |
646 | +relations: |
647 | + - - {{ ubuntu_appname }} |
648 | + - {{ bb_appname }} |
649 | diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py |
650 | new file mode 100644 |
651 | index 0000000..9679d97 |
652 | --- /dev/null |
653 | +++ b/tests/functional/conftest.py |
654 | @@ -0,0 +1,68 @@ |
655 | +""" |
656 | +Reusable pytest fixtures for functional testing. |
657 | + |
658 | +Environment variables |
659 | +--------------------- |
660 | + |
661 | +PYTEST_CLOUD_REGION, PYTEST_CLOUD_NAME: cloud name and region to use for juju model creation |
662 | + |
663 | +PYTEST_KEEP_MODEL: if set, the testing model won't be torn down at the end of the testing session |
664 | +""" |
665 | + |
666 | +import asyncio |
667 | +import os |
668 | +import subprocess |
669 | +import uuid |
670 | + |
671 | +from juju.controller import Controller |
672 | + |
673 | +from juju_tools import JujuTools |
674 | + |
675 | +import pytest |
676 | + |
677 | + |
678 | +@pytest.fixture(scope='module') |
679 | +def event_loop(): |
680 | + """Override the default pytest event loop to allow for fixtures using a broader scope.""" |
681 | + loop = asyncio.get_event_loop_policy().new_event_loop() |
682 | + asyncio.set_event_loop(loop) |
683 | + loop.set_debug(True) |
684 | + yield loop |
685 | + loop.close() |
686 | + asyncio.set_event_loop(None) |
687 | + |
688 | + |
689 | +@pytest.fixture(scope='module') |
690 | +async def controller(): |
691 | + """Connect to the current controller.""" |
692 | + _controller = Controller() |
693 | + await _controller.connect_current() |
694 | + yield _controller |
695 | + await _controller.disconnect() |
696 | + |
697 | + |
698 | +@pytest.fixture(scope='module') |
699 | +async def model(controller): |
700 | + """Create a temporary model to run the tests.""" |
701 | + model_name = "functest-{}".format(str(uuid.uuid4())[-12:]) |
702 | + _model = await controller.add_model(model_name, |
703 | + cloud_name=os.getenv('PYTEST_CLOUD_NAME'), |
704 | + region=os.getenv('PYTEST_CLOUD_REGION'), |
705 | + ) |
706 | + # https://github.com/juju/python-libjuju/issues/267 |
707 | + subprocess.check_call(['juju', 'models']) |
708 | + while model_name not in await controller.list_models(): |
709 | + await asyncio.sleep(1) |
710 | + yield _model |
711 | + await _model.disconnect() |
712 | + if not os.getenv('PYTEST_KEEP_MODEL'): |
713 | + await controller.destroy_model(model_name) |
714 | + while model_name in await controller.list_models(): |
715 | + await asyncio.sleep(1) |
716 | + |
717 | + |
718 | +@pytest.fixture(scope='module') |
719 | +async def jujutools(controller, model): |
720 | + """Load helpers to run commands on the units.""" |
721 | + tools = JujuTools(controller, model) |
722 | + return tools |
723 | diff --git a/tests/functional/juju_tools.py b/tests/functional/juju_tools.py |
724 | new file mode 100644 |
725 | index 0000000..850c296 |
726 | --- /dev/null |
727 | +++ b/tests/functional/juju_tools.py |
728 | @@ -0,0 +1,71 @@ |
729 | +"""Juju helpers to run commands on the units.""" |
730 | +import base64 |
731 | +import pickle |
732 | + |
733 | +import juju |
734 | + |
735 | + |
736 | +class JujuTools: |
737 | + """Load helpers to run commands on units.""" |
738 | + |
739 | + def __init__(self, controller, model): |
740 | + """Load initialized controller and model.""" |
741 | + self.controller = controller |
742 | + self.model = model |
743 | + |
744 | + async def run_command(self, cmd, target): |
745 | + """ |
746 | + Run a command on a unit. |
747 | + |
748 | + :param cmd: Command to be run |
749 | + :param unit: Unit object or unit name string |
750 | + """ |
751 | + unit = ( |
752 | + target |
753 | + if isinstance(target, juju.unit.Unit) |
754 | + else await self.get_unit(target) |
755 | + ) |
756 | + action = await unit.run(cmd) |
757 | + return action.results |
758 | + |
759 | + async def remote_object(self, imports, remote_cmd, target): |
760 | + """ |
761 | + Run command on target machine and returns a python object of the result. |
762 | + |
763 | + :param imports: Imports needed for the command to run |
764 | + :param remote_cmd: The python command to execute |
765 | + :param target: Unit object or unit name string |
766 | + """ |
767 | + python3 = "python3 -c '{}'" |
768 | + python_cmd = ('import pickle;' |
769 | + 'import base64;' |
770 | + '{}' |
771 | + 'print(base64.b64encode(pickle.dumps({})), end="")' |
772 | + .format(imports, remote_cmd)) |
773 | + cmd = python3.format(python_cmd) |
774 | + results = await self.run_command(cmd, target) |
775 | + return pickle.loads(base64.b64decode(bytes(results['Stdout'][2:-1], 'utf8'))) |
776 | + |
777 | + async def file_stat(self, path, target): |
778 | + """ |
779 | + Run stat on a file. |
780 | + |
781 | + :param path: File path |
782 | + :param target: Unit object or unit name string |
783 | + """ |
784 | + imports = 'import os;' |
785 | + python_cmd = ('os.stat("{}")' |
786 | + .format(path)) |
787 | + print("Calling remote cmd: " + python_cmd) |
788 | + return await self.remote_object(imports, python_cmd, target) |
789 | + |
790 | + async def file_contents(self, path, target): |
791 | + """ |
792 | + Return the contents of a file. |
793 | + |
794 | + :param path: File path |
795 | + :param target: Unit object or unit name string |
796 | + """ |
797 | + cmd = 'cat {}'.format(path) |
798 | + result = await self.run_command(cmd, target) |
799 | + return result['Stdout'] |
800 | diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt |
801 | new file mode 100644 |
802 | index 0000000..3d8a11b |
803 | --- /dev/null |
804 | +++ b/tests/functional/requirements.txt |
805 | @@ -0,0 +1,7 @@ |
806 | +flake8 |
807 | +jinja2 |
808 | +juju |
809 | +mock |
810 | +pytest |
811 | +pytest-asyncio |
812 | +requests |
813 | diff --git a/tests/functional/test_deploy.py b/tests/functional/test_deploy.py |
814 | new file mode 100644 |
815 | index 0000000..863100c |
816 | --- /dev/null |
817 | +++ b/tests/functional/test_deploy.py |
818 | @@ -0,0 +1,131 @@ |
819 | +"""Tests around Juju deployed charms.""" |
820 | +import asyncio |
821 | +import os |
822 | +import stat |
823 | +import subprocess |
824 | + |
825 | +import jinja2 |
826 | + |
827 | +import pytest |
828 | + |
829 | +# Treat all tests as coroutines |
830 | +pytestmark = pytest.mark.asyncio |
831 | + |
832 | +CHARM_BUILD_DIR = os.getenv('CHARM_BUILD_DIR', '.').rstrip('/') |
833 | + |
834 | +series = ['bionic'] |
835 | +sources = [('local', os.path.join(CHARM_BUILD_DIR, 'prometheus-blackbox-exporter-peer')), |
836 | + # ('jujucharms', 'cs:...'), |
837 | + ] |
838 | + |
839 | + |
840 | +def render(templates_dir, template_name, context): |
841 | + """Render a configuration file from a template.""" |
842 | + templates = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_dir)) |
843 | + template = templates.get_template(template_name) |
844 | + return template.render(context) |
845 | + |
846 | +# Uncomment for re-using the current model, useful for debugging functional tests |
847 | +# @pytest.fixture(scope='module') |
848 | +# async def model(): |
849 | +# from juju.model import Model |
850 | +# model = Model() |
851 | +# await model.connect_current() |
852 | +# yield model |
853 | +# await model.disconnect() |
854 | + |
855 | + |
856 | +# Custom fixtures |
857 | +@pytest.fixture(params=series) |
858 | +def series(request): |
859 | + """Return series scope.""" |
860 | + return request.param |
861 | + |
862 | + |
863 | +@pytest.fixture(params=sources, ids=[s[0] for s in sources]) |
864 | +def source(request): |
865 | + """Return location of deployed charm (local disk or charm store).""" |
866 | + return request.param |
867 | + |
868 | + |
869 | +@pytest.fixture |
870 | +async def app(model, series, source): |
871 | + """Return Juju application object for the deployed series.""" |
872 | + app_name = 'prometheus-blackbox-exporter-peer-{}-{}'.format(series, source[0]) |
873 | + return await model._wait_for_new('application', app_name) |
874 | + |
875 | + |
876 | +async def test_bbpeerexporter_deploy(model, series, source, request): |
877 | + """Start a deploy for each series. |
878 | + |
879 | + subprocess is used because libjuju fails with JAAS |
880 | + https://github.com/juju/python-libjuju/issues/221 |
881 | + """ |
882 | + dir_path = os.path.dirname(os.path.realpath(__file__)) |
883 | + bundle_path = os.path.join(CHARM_BUILD_DIR, 'bundle.yaml') |
884 | + |
885 | + ubuntu_appname = "ubuntu-{}-{}".format(series, source[0]) |
886 | + bb_appname = 'prometheus-blackbox-exporter-peer-{}-{}'.format(series, source[0]) |
887 | + |
888 | + context = { |
889 | + "ubuntu_appname": ubuntu_appname, |
890 | + "bb_appname": bb_appname, |
891 | + "series": series, |
892 | + "charm_path": os.path.join(CHARM_BUILD_DIR, "prometheus-blackbox-exporter-peer") |
893 | + } |
894 | + rendered = render(dir_path, "bundle.yaml.j2", context) |
895 | + with open(bundle_path, "w") as fd: |
896 | + fd.write(rendered) |
897 | + |
898 | + if not os.path.exists(bundle_path): |
899 | + assert False |
900 | + |
901 | + cmd = ["juju", "deploy", "-m", model.info.name, bundle_path] |
902 | + if request.node.get_closest_marker('xfail'): |
903 | + # If series is 'xfail' force install to allow testing against versions not in |
904 | + # metadata.yaml |
905 | + cmd.append('--force') |
906 | + subprocess.check_call(cmd) |
907 | + while True: |
908 | + try: |
909 | + model.applications[bb_appname] |
910 | + break |
911 | + except KeyError: |
912 | + await asyncio.sleep(5) |
913 | + assert True |
914 | + |
915 | + |
916 | +# Tests |
917 | +async def test_bbpeerexporter_status(model, app): |
918 | + """Verify status for all deployed series of the charm.""" |
919 | + await model.block_until(lambda: app.status == 'active', |
920 | + timeout=900) |
921 | + unit = app.units[0] |
922 | + await model.block_until(lambda: unit.agent_status == 'idle', |
923 | + timeout=900) |
924 | + |
925 | + |
926 | +# async def test_example_action(app): |
927 | +# unit = app.units[0] |
928 | +# action = await unit.run_action('example-action') |
929 | +# action = await action.wait() |
930 | +# assert action.status == 'completed' |
931 | + |
932 | + |
933 | +async def test_run_command(app, jujutools): |
934 | + """Test simple juju-run command.""" |
935 | + unit = app.units[0] |
936 | + cmd = 'hostname -i' |
937 | + results = await jujutools.run_command(cmd, unit) |
938 | + assert results['Code'] == '0' |
939 | + assert unit.public_address in results['Stdout'] |
940 | + |
941 | + |
942 | +async def test_file_stat(app, jujutools): |
943 | + """Verify a file exists in the deployed unit.""" |
944 | + unit = app.units[0] |
945 | + path = '/var/lib/juju/agents/unit-{}/charm/metadata.yaml'.format(unit.entity_id.replace('/', '-')) |
946 | + fstat = await jujutools.file_stat(path, unit) |
947 | + assert stat.filemode(fstat.st_mode) == '-rw-r--r--' |
948 | + assert fstat.st_uid == 0 |
949 | + assert fstat.st_gid == 0 |
950 | diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py |
951 | new file mode 100644 |
952 | index 0000000..eb2631b |
953 | --- /dev/null |
954 | +++ b/tests/unit/conftest.py |
955 | @@ -0,0 +1,60 @@ |
956 | +"""Reusable pytest fixtures for functional testing.""" |
957 | +import unittest.mock as mock |
958 | + |
959 | +import pytest |
960 | + |
961 | + |
962 | +# If layer options are used, add this to prometheusblackboxpeerexporter |
963 | +# and import layer in lib_bb_peer_exporter |
964 | +@pytest.fixture |
965 | +def mock_layers(monkeypatch): |
966 | + """Mock imported modules and calls.""" |
967 | + import sys |
968 | + sys.modules['charms.layer'] = mock.Mock() |
969 | + sys.modules['charms.reactive.helpers'] = mock.Mock() |
970 | + sys.modules['reactive'] = mock.Mock() |
971 | + # Mock any functions in layers that need to be mocked here |
972 | + |
973 | + |
974 | +@pytest.fixture |
975 | +def mock_hookenv_config(monkeypatch): |
976 | + """Mock hookenv.config().""" |
977 | + import yaml |
978 | + |
979 | + def mock_config(): |
980 | + cfg = {} |
981 | + yml = yaml.load(open('./config.yaml')) |
982 | + |
983 | + # Load all defaults |
984 | + for key, value in yml['options'].items(): |
985 | + cfg[key] = value['default'] |
986 | + |
987 | + # Manually add cfg from other layers |
988 | + return cfg |
989 | + # cfg['my-other-layer'] = 'mock' |
990 | + |
991 | + monkeypatch.setattr('lib_bb_peer_exporter.hookenv.config', mock_config) |
992 | + |
993 | + |
994 | +@pytest.fixture |
995 | +def mock_remote_unit(monkeypatch): |
996 | + """Mock a remote unit name.""" |
997 | + monkeypatch.setattr('lib_bb_peer_exporter.hookenv.remote_unit', lambda: 'unit-mock/0') |
998 | + |
999 | + |
1000 | +@pytest.fixture |
1001 | +def mock_charm_dir(monkeypatch): |
1002 | + """Mock CHARM_DIR Juju environment variable.""" |
1003 | + monkeypatch.setattr('lib_bb_peer_exporter.hookenv.charm_dir', lambda: '/mock/charm/dir') |
1004 | + |
1005 | + |
1006 | +@pytest.fixture |
1007 | +def bbpeerexporter(tmpdir, mock_layers, mock_hookenv_config, mock_charm_dir, monkeypatch): |
1008 | + """Test the basic structure of the helper class.""" |
1009 | + from lib_bb_peer_exporter import BBPeerExporterHelper |
1010 | + helper = BBPeerExporterHelper() |
1011 | + |
1012 | + # Any other functions that load helper will get this version |
1013 | + monkeypatch.setattr('lib_bb_peer_exporter.BBPeerExporterHelper', lambda: helper) |
1014 | + |
1015 | + return helper |
1016 | diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt |
1017 | new file mode 100644 |
1018 | index 0000000..ec17338 |
1019 | --- /dev/null |
1020 | +++ b/tests/unit/requirements.txt |
1021 | @@ -0,0 +1,5 @@ |
1022 | +charmhelpers |
1023 | +charms.reactive |
1024 | +psutil |
1025 | +pytest |
1026 | +pytest-cov |
1027 | diff --git a/tests/unit/samples/expected.multi b/tests/unit/samples/expected.multi |
1028 | new file mode 100644 |
1029 | index 0000000..d1cb749 |
1030 | --- /dev/null |
1031 | +++ b/tests/unit/samples/expected.multi |
1032 | @@ -0,0 +1 @@ |
1033 | +{"172.16.138.0/25": {"iface": "bond3", "ip": "172.16.138.34", "net": "172.16.138.0/25", "mtu": "9000"}, "172.16.137.0/24": {"iface": "bond2", "ip": "172.16.137.79", "net": "172.16.137.0/24", "mtu": "9000"}, "172.16.148.0/23": {"iface": "bond1", "ip": "172.16.149.44", "net": "172.16.148.0/23", "mtu": "1500"}, "172.16.136.0/25": {"iface": "bond1.50", "ip": "172.16.136.43", "net": "172.16.136.0/25", "mtu": "1500"}} |
1034 | \ No newline at end of file |
1035 | diff --git a/tests/unit/samples/expected.single b/tests/unit/samples/expected.single |
1036 | new file mode 100644 |
1037 | index 0000000..80023f6 |
1038 | --- /dev/null |
1039 | +++ b/tests/unit/samples/expected.single |
1040 | @@ -0,0 +1 @@ |
1041 | +{"10.205.6.0/24": {"iface": "eth0", "ip": "10.205.6.172", "net": "10.205.6.0/24", "mtu": "1500"}} |
1042 | \ No newline at end of file |
1043 | diff --git a/tests/unit/samples/if_addrs.multi b/tests/unit/samples/if_addrs.multi |
1044 | new file mode 100644 |
1045 | index 0000000..e702820 |
1046 | Binary files /dev/null and b/tests/unit/samples/if_addrs.multi differ |
1047 | diff --git a/tests/unit/samples/if_addrs.single b/tests/unit/samples/if_addrs.single |
1048 | new file mode 100644 |
1049 | index 0000000..148b2d5 |
1050 | Binary files /dev/null and b/tests/unit/samples/if_addrs.single differ |
1051 | diff --git a/tests/unit/samples/if_stats.multi b/tests/unit/samples/if_stats.multi |
1052 | new file mode 100644 |
1053 | index 0000000..1b618b9 |
1054 | Binary files /dev/null and b/tests/unit/samples/if_stats.multi differ |
1055 | diff --git a/tests/unit/samples/if_stats.single b/tests/unit/samples/if_stats.single |
1056 | new file mode 100644 |
1057 | index 0000000..ca0eba5 |
1058 | Binary files /dev/null and b/tests/unit/samples/if_stats.single differ |
1059 | diff --git a/tests/unit/test_lib_bb_peer_exporter.py b/tests/unit/test_lib_bb_peer_exporter.py |
1060 | new file mode 100644 |
1061 | index 0000000..3d318e4 |
1062 | --- /dev/null |
1063 | +++ b/tests/unit/test_lib_bb_peer_exporter.py |
1064 | @@ -0,0 +1,12 @@ |
1065 | +"""Tests around lib_bb_peer_exporter module.""" |
1066 | + |
1067 | + |
1068 | +class TestLibBBExporter(): |
1069 | + """Test suite for lib_bb_peer_exporter module.""" |
1070 | + |
1071 | + def test_bbpeerexporter(self, bbpeerexporter): |
1072 | + """See if the helper fixture works to load charm configs.""" |
1073 | + assert isinstance(bbpeerexporter.charm_config, dict) |
1074 | + assert bbpeerexporter.modules |
1075 | + assert bbpeerexporter.scrape_interval == "60s" |
1076 | + assert bbpeerexporter.port_def == 9115 |
1077 | diff --git a/tests/unit/test_lib_network.py b/tests/unit/test_lib_network.py |
1078 | new file mode 100644 |
1079 | index 0000000..40052cc |
1080 | --- /dev/null |
1081 | +++ b/tests/unit/test_lib_network.py |
1082 | @@ -0,0 +1,40 @@ |
1083 | +"""Tests around lib_network module.""" |
1084 | +import os |
1085 | +import pickle |
1086 | + |
1087 | +import lib_network |
1088 | + |
1089 | +import pytest |
1090 | + |
1091 | +DIRNAME = os.path.dirname(__file__) |
1092 | + |
1093 | + |
1094 | +class TestLibNetwork(): |
1095 | + """Test suite for lib_network module.""" |
1096 | + |
1097 | + @pytest.mark.parametrize('n_ifaces', ['single', 'multi']) |
1098 | + def test_get_unit_ipv4_networks(self, n_ifaces, monkeypatch): |
1099 | + """Retrieved psutil results return IPv4 interfaces details. |
1100 | + |
1101 | + psutil.if_addrs() and psutil.if_stats() are mocked to simulate |
1102 | + - unit with a single IPv4 interface |
1103 | + - unit with multiple IPv4 interfaces and different MTU sizes""" |
1104 | + def load_obj(funcname, n_ifaces): |
1105 | + filename = os.path.join( |
1106 | + DIRNAME, 'samples', '{}.{}'.format(funcname, n_ifaces)) |
1107 | + with open(filename, 'rb') as fd: |
1108 | + obj = pickle.load(fd) |
1109 | + return obj |
1110 | + |
1111 | + def expected_result(n_ifaces): |
1112 | + filename = os.path.join( |
1113 | + DIRNAME, 'samples', 'expected.{}'.format(n_ifaces)) |
1114 | + with open(filename, 'r') as fd: |
1115 | + expected = fd.read().strip() |
1116 | + return expected |
1117 | + |
1118 | + if_addrs = load_obj('if_addrs', n_ifaces) |
1119 | + if_stats = load_obj('if_stats', n_ifaces) |
1120 | + monkeypatch.setattr('psutil.net_if_addrs', lambda: if_addrs) |
1121 | + monkeypatch.setattr('psutil.net_if_stats', lambda: if_stats) |
1122 | + assert lib_network.get_unit_ipv4_networks() == expected_result(n_ifaces) |
1123 | diff --git a/tox.ini b/tox.ini |
1124 | new file mode 100644 |
1125 | index 0000000..3715246 |
1126 | --- /dev/null |
1127 | +++ b/tox.ini |
1128 | @@ -0,0 +1,48 @@ |
1129 | +[tox] |
1130 | +skipsdist=True |
1131 | +envlist = unit, functional |
1132 | +skip_missing_interpreters = True |
1133 | + |
1134 | +[testenv] |
1135 | +basepython = python3 |
1136 | +setenv = |
1137 | + PYTHONPATH = . |
1138 | + |
1139 | +[testenv:unit] |
1140 | +commands = pytest -v --ignore {toxinidir}/tests/functional \ |
1141 | + --cov=lib \ |
1142 | + --cov=reactive \ |
1143 | + --cov=actions \ |
1144 | + --cov-report=term \ |
1145 | + --cov-report=annotate:report/annotated \ |
1146 | + --cov-report=html:report/html |
1147 | +deps = -r{toxinidir}/tests/unit/requirements.txt |
1148 | +setenv = PYTHONPATH={toxinidir}/lib |
1149 | + |
1150 | +[testenv:functional] |
1151 | +passenv = |
1152 | + HOME |
1153 | + CHARM_BUILD_DIR |
1154 | + PATH |
1155 | + PYTEST_KEEP_MODEL |
1156 | + PYTEST_CLOUD_NAME |
1157 | + PYTEST_CLOUD_REGION |
1158 | +commands = pytest -v --ignore {toxinidir}/tests/unit |
1159 | +deps = -r{toxinidir}/tests/functional/requirements.txt |
1160 | + |
1161 | +[testenv:lint] |
1162 | +commands = flake8 |
1163 | +deps = |
1164 | + flake8 |
1165 | + flake8-docstrings |
1166 | + flake8-import-order |
1167 | + pep8-naming |
1168 | + flake8-colors |
1169 | + |
1170 | +[flake8] |
1171 | +exclude = |
1172 | + .git, |
1173 | + __pycache__, |
1174 | + .tox, |
1175 | +max-line-length = 120 |
1176 | +max-complexity = 10 |
1177 | diff --git a/wheelhouse.txt b/wheelhouse.txt |
1178 | new file mode 100644 |
1179 | index 0000000..a4d92cc |
1180 | --- /dev/null |
1181 | +++ b/wheelhouse.txt |
1182 | @@ -0,0 +1 @@ |
1183 | +psutil |
Hi! I've addressed the previously suggested fixes at [1].
OTOH, the reactive script has been rewritten so it only interacts with the "blackbox-exporter" relation. It's up to prometheus2-charm to create the icmp probes.
Although this charm exposes a "icmp_jumbo" module by default, prometheus2-charm won't configure probes using this module for now.
1. https:/ /code.launchpad .net/~aluria/ prometheus- blackbox- exporter- charm/+ git/prometheus- blackbox- exporter- charm/+ merge/374432