Merge ~xavpaice/charm-telegraf:lint-20.08 into charm-telegraf:master

Proposed by Xav Paice
Status: Merged
Approved by: Alvaro Uria
Approved revision: b9e75f78db2428389d614f412f6dc0baca255fce
Merged at revision: 5b7391d840a618ab91dc1597864bb29138a04d19
Proposed branch: ~xavpaice/charm-telegraf:lint-20.08
Merge into: charm-telegraf:master
Prerequisite: ~xavpaice/charm-telegraf:blacken-20.08
Diff against target: 1281 lines (+226/-112)
12 files modified
src/files/telegraf_exec_metrics.py (+69/-34)
src/hooks/relations/apache-website/requires.py (+1/-3)
src/hooks/relations/statistics/requires.py (+2/-3)
src/hooks/relations/telegraf-exec/requires.py (+7/-3)
src/reactive/__init__.py (+1/-0)
src/reactive/telegraf.py (+82/-31)
src/tests/functional/tests/test_telegraf.py (+19/-6)
src/tests/unit/__init__.py (+2/-0)
src/tests/unit/test_mysql.py (+2/-1)
src/tests/unit/test_postgresql.py (+4/-4)
src/tests/unit/test_telegraf.py (+35/-26)
src/tox.ini (+2/-1)
Reviewer Review Type Date Requested Status
Alvaro Uria (community) Approve
James Troup (community) Approve
Canonical IS Reviewers Pending
Review via email: mp+388546@code.launchpad.net

Commit message

Extra Linting completed for 20.08 charm release

To post a comment you must log in.
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
James Troup (elmo) wrote :

LGTM

review: Approve
Revision history for this message
Alvaro Uria (aluria) wrote :

+1 to this and the other 2 related MPs (makefile and blacken)

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

Change successfully merged at revision 5b7391d840a618ab91dc1597864bb29138a04d19

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/src/files/telegraf_exec_metrics.py b/src/files/telegraf_exec_metrics.py
index f03e23a..d9b3402 100755
--- a/src/files/telegraf_exec_metrics.py
+++ b/src/files/telegraf_exec_metrics.py
@@ -1,29 +1,32 @@
1#!/usr/bin/env python31#!/usr/bin/env python3
2import os2import argparse
3import re
4import sys
5import json3import json
6import logging4import logging
7import argparse5import os
8import subprocess6import re
9import shutil7import shutil
8import subprocess
9import sys
10from collections import OrderedDict10from collections import OrderedDict
11from textwrap import dedent11from textwrap import dedent
1212
13from insights.tests import context_wrap
14from insights.parsers.softnet_stat import SoftNetStats13from insights.parsers.softnet_stat import SoftNetStats
1514from insights.tests import context_wrap
1615
17LOG = logging.getLogger()16LOG = logging.getLogger()
1817
19METRICS = OrderedDict()18METRICS = OrderedDict()
2019
2120
22def register_metric(MetricClass):21def register_metric(metricclass):
23 """A decorator to create a metric class instance and register it"""22 """Create metric class instance.
24 metric = MetricClass()23
24 Decorator that creates a metric class and registers it.
25 """
26 metric = metricclass()
25 METRICS[metric.name] = metric27 METRICS[metric.name] = metric
26 return MetricClass28
29 return metricclass
2730
2831
29class FileMetric:32class FileMetric:
@@ -46,18 +49,22 @@ class FileMetric:
46 LOG.info("rendering config to %s", config_path)49 LOG.info("rendering config to %s", config_path)
47 script = os.path.abspath(__file__)50 script = os.path.abspath(__file__)
48 output = self.config_template.format(51 output = self.config_template.format(
49 python=python or sys.executable, script=script, name=self.name,52 python=python or sys.executable, script=script, name=self.name
50 )53 )
51 LOG.debug(output)54 LOG.debug(output)
55
52 if not dry_run:56 if not dry_run:
53 with open(config_path, mode="w", encoding="utf8") as f:57 with open(config_path, mode="w", encoding="utf8") as f:
54 f.write(output)58 f.write(output)
59
55 return output60 return output
5661
57 def remove_config(self, configs_dir, dry_run=False):62 def remove_config(self, configs_dir, dry_run=False):
58 config_path = self.config_path(configs_dir)63 config_path = self.config_path(configs_dir)
64
59 if os.path.isfile(config_path):65 if os.path.isfile(config_path):
60 LOG.info("removing %s", config_path)66 LOG.info("removing %s", config_path)
67
61 if not dry_run:68 if not dry_run:
62 os.remove(config_path)69 os.remove(config_path)
63 else:70 else:
@@ -78,6 +85,7 @@ class SockStatFileMetric(FileMetric):
7885
79 def parse(self, content):86 def parse(self, content):
80 results = {}87 results = {}
88
81 for line in content.splitlines():89 for line in content.splitlines():
82 LOG.debug("parsing line: %s", line)90 LOG.debug("parsing line: %s", line)
83 # line example: TCP: inuse 62 orphan 0 tw 5 alloc 222 mem 2091 # line example: TCP: inuse 62 orphan 0 tw 5 alloc 222 mem 20
@@ -86,9 +94,11 @@ class SockStatFileMetric(FileMetric):
86 count = len(items)94 count = len(items)
87 assert count % 2 == 095 assert count % 2 == 0
88 group = {}96 group = {}
97
89 for i in range(0, count, 2):98 for i in range(0, count, 2):
90 group[items[i]] = int(items[i + 1])99 group[items[i]] = int(items[i + 1])
91 results[key.strip()] = group100 results[key.strip()] = group
101
92 return results102 return results
93103
94104
@@ -175,18 +185,22 @@ class BuddyinfoFileMetric(FileMetric):
175 re_line = re.compile(185 re_line = re.compile(
176 r"Node\s+(?P<node>\d+).*zone\s+(?P<zone>\w+)\s+(?P<pages>.*)"186 r"Node\s+(?P<node>\d+).*zone\s+(?P<zone>\w+)\s+(?P<pages>.*)"
177 )187 )
188
178 for line in content.splitlines():189 for line in content.splitlines():
179 LOG.debug(line)190 LOG.debug(line)
180 match = re_line.match(line)191 match = re_line.match(line)
192
181 if match:193 if match:
182 data = match.groupdict()194 data = match.groupdict()
183 LOG.debug(data)195 LOG.debug(data)
184 pages = data["pages"].split()196 pages = data["pages"].split()
197
185 if not human_sizes:198 if not human_sizes:
186 human_sizes = [199 human_sizes = [
187 self._get_human_size(page_size * 2 ** i)200 self._get_human_size(page_size * 2 ** i)
188 for i, _ in enumerate(pages)201 for i, _ in enumerate(pages)
189 ]202 ]
203
190 for k, v in zip(human_sizes, pages):204 for k, v in zip(human_sizes, pages):
191 items.append(205 items.append(
192 {206 {
@@ -196,6 +210,7 @@ class BuddyinfoFileMetric(FileMetric):
196 "count": int(v),210 "count": int(v),
197 }211 }
198 )212 )
213
199 return items214 return items
200215
201216
@@ -228,14 +243,17 @@ class ZoneinfoFileMetric(FileMetric):
228 re.MULTILINE + re.DOTALL,243 re.MULTILINE + re.DOTALL,
229 )244 )
230 items = []245 items = []
246
231 for match in pattern.finditer(content):247 for match in pattern.finditer(content):
232 data = match.groupdict()248 data = match.groupdict()
233 LOG.debug(data)249 LOG.debug(data)
234 # convert str to int250 # convert str to int
251
235 for k, v in data.items():252 for k, v in data.items():
236 if v.isdigit():253 if v.isdigit():
237 data[k] = int(v)254 data[k] = int(v)
238 items.append(data)255 items.append(data)
256
239 return items257 return items
240258
241259
@@ -246,11 +264,14 @@ class CmdMetric(FileMetric):
246 def get_cmd_output(self, cmd, is_json=False):264 def get_cmd_output(self, cmd, is_json=False):
247 LOG.debug("running cmd: %s", " ".join(cmd))265 LOG.debug("running cmd: %s", " ".join(cmd))
248 output = subprocess.check_output(cmd).decode("utf8").strip()266 output = subprocess.check_output(cmd).decode("utf8").strip()
267
249 return json.loads(output) if is_json else output268 return json.loads(output) if is_json else output
250269
251 def get_input_content(self):270 def get_input_content(self):
252 if self.input_file:271 if self.input_file:
253 # if provided, still read cmd output from a file, helpful for debuging/testing.272 # if provided, still read cmd output from a file, helpful
273 # for debuging/testing.
274
254 return super().get_input_content()275 return super().get_input_content()
255 else:276 else:
256 return self.get_cmd_output(self.cmd)277 return self.get_cmd_output(self.cmd)
@@ -277,17 +298,18 @@ class NetNSCmdMetric(CmdMetric):
277 items = []298 items = []
278 # find openstack/neutron netns list pattern299 # find openstack/neutron netns list pattern
279 pattern = re.compile(r"^([\w-]+)\s+\(id:\s+(\d+)\)", flags=re.M)300 pattern = re.compile(r"^([\w-]+)\s+\(id:\s+(\d+)\)", flags=re.M)
301
280 for match in pattern.finditer(content):302 for match in pattern.finditer(content):
281 items.append(303 items.append(
282 {"namespace": match.group(1), "id": match.group(2), "state": 1,}304 {"namespace": match.group(1), "id": match.group(2), "state": 1}
283 )305 )
284306
285 # no re matches, should be in simple list format307 # no re matches, should be in simple list format
308
286 if not items:309 if not items:
287 for line in content.splitlines():310 for line in content.splitlines():
288 items.append(311 items.append({"namespace": line.strip(), "state": 1})
289 {"namespace": line.strip(), "state": 1,}312
290 )
291 return items313 return items
292314
293315
@@ -321,8 +343,10 @@ class OvsDumpFlowsCmdMetric(CmdMetric):
321 flows = {}343 flows = {}
322344
323 ovs_cmd = "ovs-ofctl"345 ovs_cmd = "ovs-ofctl"
346
324 if not shutil.which(ovs_cmd):347 if not shutil.which(ovs_cmd):
325 LOG.error('openvswitch command "%s" is not available, exit', ovs_cmd)348 LOG.error('openvswitch command "%s" is not available, exit', ovs_cmd)
349
326 return flows350 return flows
327351
328 for bridge in ["br-int", "br-tun", "br-data"]:352 for bridge in ["br-int", "br-tun", "br-data"]:
@@ -335,7 +359,6 @@ class OvsDumpFlowsCmdMetric(CmdMetric):
335 def parse(self, input_content):359 def parse(self, input_content):
336 """Parse input content for each bridge and convert to a json list.360 """Parse input content for each bridge and convert to a json list.
337361
338
339 Example final json result:362 Example final json result:
340363
341 [364 [
@@ -356,12 +379,14 @@ class OvsDumpFlowsCmdMetric(CmdMetric):
356 # input_content here is a dict with cmd output for each bridge for this metric379 # input_content here is a dict with cmd output for each bridge for this metric
357 flows = input_content380 flows = input_content
358 items = []381 items = []
382
359 for bridge, output in flows.items():383 for bridge, output in flows.items():
360 # when invoked via python (or redirected) it includes an extra line384 # when invoked via python (or redirected) it includes an extra line
361 # like 'NXST_FLOW reply (xid=0x4):'385 # like 'NXST_FLOW reply (xid=0x4):'
362 # so need to substract one386 # so need to substract one
363 lines = output.splitlines()387 lines = output.splitlines()
364 items.append({"bridge": bridge, "count": len(lines) - 1})388 items.append({"bridge": bridge, "count": len(lines) - 1})
389
365 return items390 return items
366391
367392
@@ -381,8 +406,9 @@ class OvsDpCtlCmdMetric(CmdMetric):
381 )406 )
382407
383 def parse_fields(self, text):408 def parse_fields(self, text):
384 """409 """Parse lines following pattern to a dictionary.
385 parse lines following pattern to a dictionary,410
411 Parse lines following pattern to a dictionary,
386 converting int/floats as relevant412 converting int/floats as relevant
387 'hit:706 missed:456 lost:0'413 'hit:706 missed:456 lost:0'
388 """414 """
@@ -396,23 +422,26 @@ class OvsDpCtlCmdMetric(CmdMetric):
396 # parse lines of key/values into a dict422 # parse lines of key/values into a dict
397 # hit:706 missed:489 lost:1423 # hit:706 missed:489 lost:1
398 data = {}424 data = {}
425
399 for datapair in text.split():426 for datapair in text.split():
400 k, v = datapair.split(":")427 k, v = datapair.split(":")
401 # do nothing if the line has question marks (no data), e.g.:428 # do nothing if the line has question marks (no data), e.g.:
402 # RX packets:0 errors:? dropped:? overruns:? frame:?429 # RX packets:0 errors:? dropped:? overruns:? frame:?
430
403 if "?" in v:431 if "?" in v:
404 continue432 continue
405 # reuse field parsing logic by recursion, no nested dicts433 # reuse field parsing logic by recursion, no nested dicts
406 data[k] = self.parse_fields(v)434 data[k] = self.parse_fields(v)
435
407 return data436 return data
437
408 return text438 return text
409439
410 def parse(self, input_content):440 def parse(self, input_content):
411 """441 """Parse the output of `ovs-appctl dpctl/show -s`."""
412 parse the output of `ovs-appctl dpctl/show -s`
413 """
414 result = []442 result = []
415 datapath_entry = None443 datapath_entry = None
444
416 for line in input_content.splitlines():445 for line in input_content.splitlines():
417 if line[0].isalnum(): # datapath header, other lines start w/space or tab446 if line[0].isalnum(): # datapath header, other lines start w/space or tab
418 if datapath_entry: # store the current one before creating a new one447 if datapath_entry: # store the current one before creating a new one
@@ -422,9 +451,10 @@ class OvsDpCtlCmdMetric(CmdMetric):
422 "port": "",451 "port": "",
423 "portname": "",452 "portname": "",
424 }453 }
454
425 continue455 continue
426456
427 label, rest = (l.strip() for l in line.split(":", maxsplit=1))457 label, rest = (item.strip() for item in line.split(":", maxsplit=1))
428458
429 if label.startswith("port"):459 if label.startswith("port"):
430 # complete previous entry, and initialise a new one for the new port460 # complete previous entry, and initialise a new one for the new port
@@ -434,12 +464,14 @@ class OvsDpCtlCmdMetric(CmdMetric):
434 }464 }
435 datapath_entry["port"] = label.split()[-1]465 datapath_entry["port"] = label.split()[-1]
436 datapath_entry["portname"] = rest466 datapath_entry["portname"] = rest
467
437 continue468 continue
438469
439 if datapath_entry["port"]: # parsing lines corresponding to a port block470 if datapath_entry["port"]: # parsing lines corresponding to a port block
440 if "X packets" in label:471 if "X packets" in label:
441 # parse the RX|TX packets line using a prefix472 # parse the RX|TX packets line using a prefix
442 prefix, data = line.strip().split(maxsplit=1)473 prefix, data = line.strip().split(maxsplit=1)
474
443 for k, v in self.parse_fields(data).items():475 for k, v in self.parse_fields(data).items():
444 datapath_entry["{}_{}".format(prefix, k)] = v476 datapath_entry["{}_{}".format(prefix, k)] = v
445 else: # parse the non-packet lines (collisions/bytes)477 else: # parse the non-packet lines (collisions/bytes)
@@ -449,12 +481,14 @@ class OvsDpCtlCmdMetric(CmdMetric):
449 # use RX/TX as a prefix by removing the space481 # use RX/TX as a prefix by removing the space
450 data = line.replace(" bytes", "_bytes")482 data = line.replace(" bytes", "_bytes")
451 datapath_entry.update(self.parse_fields(data))483 datapath_entry.update(self.parse_fields(data))
484
452 continue485 continue
453486
454 # all other cases, are datapairs or metrics487 # all other cases, are datapairs or metrics
455 datapath_entry[label] = self.parse_fields(rest.rstrip())488 datapath_entry[label] = self.parse_fields(rest.rstrip())
456489
457 # handle the end of the output490 # handle the end of the output
491
458 if datapath_entry:492 if datapath_entry:
459 result.append(datapath_entry)493 result.append(datapath_entry)
460494
@@ -462,17 +496,18 @@ class OvsDpCtlCmdMetric(CmdMetric):
462496
463497
464def render_config_files(configs_dir, python, disabled_metrics="", dry_run=False):498def render_config_files(configs_dir, python, disabled_metrics="", dry_run=False):
465 """Render config files for any metrics not disabled, and remove any disabled"""499 """Render config files for any metrics not disabled, and remove any disabled."""
466
467 disabled_metrics_set = (500 disabled_metrics_set = (
468 set(disabled_metrics.split(":")) if disabled_metrics else set([])501 set(disabled_metrics.split(":")) if disabled_metrics else set([])
469 )502 )
503
470 for metric in disabled_metrics_set:504 for metric in disabled_metrics_set:
471 try:505 try:
472 METRICS[metric].remove_config(configs_dir, dry_run=dry_run)506 METRICS[metric].remove_config(configs_dir, dry_run=dry_run)
473 except KeyError:507 except KeyError:
474 LOG.warning("metric name %s not in %s", metric, ",".join(METRICS))508 LOG.warning("metric name %s not in %s", metric, ",".join(METRICS))
475 enabled_metrics_set = set(METRICS.keys()) - disabled_metrics_set509 enabled_metrics_set = set(METRICS.keys()) - disabled_metrics_set
510
476 for metric in enabled_metrics_set:511 for metric in enabled_metrics_set:
477 METRICS[metric].render_config(configs_dir, python, dry_run=dry_run)512 METRICS[metric].render_config(configs_dir, python, dry_run=dry_run)
478513
@@ -484,10 +519,7 @@ def main():
484 )519 )
485 choices = list(METRICS) # sorted dict520 choices = list(METRICS) # sorted dict
486 argparser.add_argument(521 argparser.add_argument(
487 "--metric",522 "--metric", choices=choices, default=choices[0], help="which metric to execuate"
488 choices=choices,
489 default=choices[0],
490 help="which metric to execuate",
491 )523 )
492 argparser.add_argument(524 argparser.add_argument(
493 "-f",525 "-f",
@@ -499,7 +531,8 @@ def main():
499 "--render-config-files",531 "--render-config-files",
500 dest="render_config_files",532 dest="render_config_files",
501 action="store_true",533 action="store_true",
502 help="render config files for all metrics, use --disabled-metrics to exclude specific ones",534 help="render config files for all metrics, use --disabled-metrics to "
535 "exclude specific ones",
503 )536 )
504 argparser.add_argument(537 argparser.add_argument(
505 "--disabled-metrics",538 "--disabled-metrics",
@@ -511,7 +544,8 @@ def main():
511 "--python",544 "--python",
512 dest="python",545 dest="python",
513 default=sys.executable,546 default=sys.executable,
514 help="python interperter path in config file to execute this script, required for venv",547 help="python interperter path in config file to execute this script, "
548 "required for venv",
515 )549 )
516 # useful for testing550 # useful for testing
517 argparser.add_argument(551 argparser.add_argument(
@@ -521,10 +555,10 @@ def main():
521 help="telegraf configs dir for exec metrics",555 help="telegraf configs dir for exec metrics",
522 )556 )
523 argparser.add_argument(557 argparser.add_argument(
524 "--dry-run", action="store_true", help="dry run without real actions",558 "--dry-run", action="store_true", help="dry run without real actions"
525 )559 )
526 argparser.add_argument(560 argparser.add_argument(
527 "-v", "--verbose", action="store_true", help="be verbose in log",561 "-v", "--verbose", action="store_true", help="be verbose in log"
528 )562 )
529563
530 cli = argparser.parse_args()564 cli = argparser.parse_args()
@@ -542,6 +576,7 @@ def main():
542 )576 )
543 else:577 else:
544 metric = METRICS[cli.metric]578 metric = METRICS[cli.metric]
579
545 if cli.input_file:580 if cli.input_file:
546 metric.input_file = cli.input_file581 metric.input_file = cli.input_file
547 output = metric.parse(metric.get_input_content())582 output = metric.parse(metric.get_input_content())
diff --git a/src/hooks/relations/apache-website/requires.py b/src/hooks/relations/apache-website/requires.py
index ecb663a..861fb8f 100644
--- a/src/hooks/relations/apache-website/requires.py
+++ b/src/hooks/relations/apache-website/requires.py
@@ -14,9 +14,7 @@
14# You should have received a copy of the GNU General Public License14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17from charms.reactive import hook17from charms.reactive import RelationBase, hook, scopes
18from charms.reactive import RelationBase
19from charms.reactive import scopes
2018
2119
22class WSGIRequires(RelationBase):20class WSGIRequires(RelationBase):
diff --git a/src/hooks/relations/statistics/requires.py b/src/hooks/relations/statistics/requires.py
index 4aafb2a..702db6f 100644
--- a/src/hooks/relations/statistics/requires.py
+++ b/src/hooks/relations/statistics/requires.py
@@ -14,9 +14,7 @@
14# You should have received a copy of the GNU General Public License14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17from charms.reactive import hook17from charms.reactive import RelationBase, hook, scopes
18from charms.reactive import RelationBase
19from charms.reactive import scopes
2018
2119
22class WSGIRequires(RelationBase):20class WSGIRequires(RelationBase):
@@ -26,6 +24,7 @@ class WSGIRequires(RelationBase):
26 def changed(self):24 def changed(self):
27 conv = self.conversation()25 conv = self.conversation()
28 conv.set_state("{relation_name}.connected")26 conv.set_state("{relation_name}.connected")
27
29 if conv.get_remote("enabled"):28 if conv.get_remote("enabled"):
30 # this unit's conversation has a port, so29 # this unit's conversation has a port, so
31 # it is part of the set of available units30 # it is part of the set of available units
diff --git a/src/hooks/relations/telegraf-exec/requires.py b/src/hooks/relations/telegraf-exec/requires.py
index 42bb783..453203b 100644
--- a/src/hooks/relations/telegraf-exec/requires.py
+++ b/src/hooks/relations/telegraf-exec/requires.py
@@ -16,9 +16,7 @@
1616
17import json17import json
1818
19from charms.reactive import hook19from charms.reactive import RelationBase, hook, scopes
20from charms.reactive import RelationBase
21from charms.reactive import scopes
2220
2321
24class ExecRequires(RelationBase):22class ExecRequires(RelationBase):
@@ -29,6 +27,7 @@ class ExecRequires(RelationBase):
29 conv = self.conversation()27 conv = self.conversation()
30 conv.set_state("{relation_name}.connected")28 conv.set_state("{relation_name}.connected")
31 commands_json_dict = conv.get_remote("commands") # list of commands to run29 commands_json_dict = conv.get_remote("commands") # list of commands to run
30
32 if commands_json_dict is not None and json.loads(commands_json_dict):31 if commands_json_dict is not None and json.loads(commands_json_dict):
33 conv.set_state("{relation_name}.available")32 conv.set_state("{relation_name}.available")
3433
@@ -40,12 +39,16 @@ class ExecRequires(RelationBase):
4039
41 def commands(self):40 def commands(self):
42 cmds = []41 cmds = []
42
43 for conv in self.conversations():43 for conv in self.conversations():
44 commands_json_dict = conv.get_remote("commands") # list of commands dicts44 commands_json_dict = conv.get_remote("commands") # list of commands dicts
45
45 for cmd_info in json.loads(commands_json_dict):46 for cmd_info in json.loads(commands_json_dict):
46 commands = cmd_info.pop("commands", []) # list of commands47 commands = cmd_info.pop("commands", []) # list of commands
48
47 if commands is None and "command" in cmd_info:49 if commands is None and "command" in cmd_info:
48 commands = [cmd_info.pop("command")]50 commands = [cmd_info.pop("command")]
51
49 if not commands:52 if not commands:
50 continue53 continue
51 data_format = cmd_info.pop("data_format") # json, graphite, influx54 data_format = cmd_info.pop("data_format") # json, graphite, influx
@@ -57,4 +60,5 @@ class ExecRequires(RelationBase):
57 cmd["run_on_this_unit"] = cmd_info.pop("run_on_this_unit", True)60 cmd["run_on_this_unit"] = cmd_info.pop("run_on_this_unit", True)
58 cmd.update(cmd_info)61 cmd.update(cmd_info)
59 cmds.append(cmd)62 cmds.append(cmd)
63
60 return cmds64 return cmds
diff --git a/src/reactive/__init__.py b/src/reactive/__init__.py
index e69de29..82e3dfc 100644
--- a/src/reactive/__init__.py
+++ b/src/reactive/__init__.py
@@ -0,0 +1 @@
1"""Reactive package."""
diff --git a/src/reactive/telegraf.py b/src/reactive/telegraf.py
index ba47505..4b72eab 100644
--- a/src/reactive/telegraf.py
+++ b/src/reactive/telegraf.py
@@ -16,40 +16,43 @@
1616
17import base6417import base64
18import binascii18import binascii
19from distutils.version import LooseVersion
20import io19import io
21import json20import json
22import os21import os
22import re
23import shutil23import shutil
24import socket24import socket
25import subprocess
25import sys26import sys
26import time27import time
27import yaml28from distutils.version import LooseVersion
28import re29
29import netifaces30from charmhelpers import context
30import subprocess31from charmhelpers.contrib.charmsupport import nrpe
32from charmhelpers.core import hookenv, host, unitdata
33from charmhelpers.core.host import is_container
34from charmhelpers.core.templating import render
3135
36import charms.promreg
32from charms import apt37from charms import apt
33from charms.layer import snap38from charms.layer import snap
34from charms.reactive import (39from charms.reactive import (
40 clear_flag,
35 endpoint_from_flag,41 endpoint_from_flag,
36 helpers,42 helpers,
37 hook,43 hook,
38 when,
39 when_not,
40 set_flag,44 set_flag,
41 clear_flag,
42 toggle_flag,45 toggle_flag,
46 when,
47 when_not,
43)48)
44from charms.reactive.bus import get_states49from charms.reactive.bus import get_states
4550
46from charmhelpers import context51from jinja2 import Environment, FileSystemLoader, Template, exceptions
47from charmhelpers.core import hookenv, host, unitdata52
48from charmhelpers.core.templating import render53import netifaces
49from charmhelpers.core.host import is_container54
50from charmhelpers.contrib.charmsupport import nrpe55import yaml
51import charms.promreg
52from jinja2 import Template, Environment, FileSystemLoader, exceptions
5356
54DEB_BASE_DIR = "/etc/telegraf"57DEB_BASE_DIR = "/etc/telegraf"
55SNAP_BASE_DIR = "/var/snap/telegraf/current"58SNAP_BASE_DIR = "/var/snap/telegraf/current"
@@ -75,6 +78,7 @@ class InvalidInstallMethod(Exception):
7578
76def get_install_method():79def get_install_method():
77 config = hookenv.config()80 config = hookenv.config()
81
78 if config["install_method"] in ["deb", "snap"]:82 if config["install_method"] in ["deb", "snap"]:
79 return config["install_method"]83 return config["install_method"]
80 else:84 else:
@@ -87,6 +91,7 @@ def get_install_method():
8791
88def get_base_dir():92def get_base_dir():
89 config = hookenv.config()93 config = hookenv.config()
94
90 if config["install_method"] == "deb":95 if config["install_method"] == "deb":
91 return DEB_BASE_DIR96 return DEB_BASE_DIR
92 elif config["install_method"] == "snap":97 elif config["install_method"] == "snap":
@@ -101,6 +106,7 @@ def get_base_dir():
101106
102def get_service():107def get_service():
103 config = hookenv.config()108 config = hookenv.config()
109
104 if config["install_method"] == "deb":110 if config["install_method"] == "deb":
105 return DEB_SERVICE111 return DEB_SERVICE
106 elif config["install_method"] == "snap":112 elif config["install_method"] == "snap":
@@ -139,13 +145,16 @@ def list_config_files():
139 config_files = [get_main_config_path()]145 config_files = [get_main_config_path()]
140 # only include config files for configured plugins146 # only include config files for configured plugins
141 current_states = get_states()147 current_states = get_states()
148
142 for plugin in list_supported_plugins():149 for plugin in list_supported_plugins():
143 if "plugins.{}.configured".format(plugin) in current_states.keys():150 if "plugins.{}.configured".format(plugin) in current_states.keys():
144 config_path = "{}/{}.conf".format(get_configs_dir(), plugin)151 config_path = "{}/{}.conf".format(get_configs_dir(), plugin)
145 config_files.append(config_path)152 config_files.append(config_path)
153
146 if "extra_plugins.configured" in current_states.keys():154 if "extra_plugins.configured" in current_states.keys():
147 config_files.append("{}/extra_plugins.conf".format(get_configs_dir()))155 config_files.append("{}/extra_plugins.conf".format(get_configs_dir()))
148 config_files.append("{}/socket_listener.conf".format(get_configs_dir()))156 config_files.append("{}/socket_listener.conf".format(get_configs_dir()))
157
149 return config_files158 return config_files
150159
151160
@@ -153,33 +162,40 @@ def get_hostname_label():
153 config = hookenv.config()162 config = hookenv.config()
154 hostname_fmt = config["hostname"]163 hostname_fmt = config["hostname"]
155 unit = get_remote_unit_name().replace("/", "-") # / is invalid in labels.164 unit = get_remote_unit_name().replace("/", "-") # / is invalid in labels.
165
156 if hostname_fmt == "UNIT_NAME": # Deprecated166 if hostname_fmt == "UNIT_NAME": # Deprecated
157 return unit167 return unit
158 env = os.environ168 env = os.environ
159 model = env.get("JUJU_ENV_NAME") or env.get("JUJU_MODEL_NAME", "")169 model = env.get("JUJU_ENV_NAME") or env.get("JUJU_MODEL_NAME", "")
160 uuid = env.get("JUJU_ENV_UUID") or env.get("JUJU_MODEL_UUID", "")170 uuid = env.get("JUJU_ENV_UUID") or env.get("JUJU_MODEL_UUID", "")
161 syshost = socket.gethostname()171 syshost = socket.gethostname()
172
162 return hostname_fmt.format(unit=unit, model=model, uuid=uuid, host=syshost)173 return hostname_fmt.format(unit=unit, model=model, uuid=uuid, host=syshost)
163174
164175
165def get_remote_unit_name():176def get_remote_unit_name():
166 unit = hookenv.principal_unit()177 unit = hookenv.principal_unit()
178
167 if unit:179 if unit:
168 # Note(aluria): use Juju env var available since 2017180 # Note(aluria): use Juju env var available since 2017
181
169 return unit182 return unit
170 else:183 else:
171 # Note(aluria): lookup all available IPv4/IPv6 addresses (except lo)184 # Note(aluria): lookup all available IPv4/IPv6 addresses (except lo)
172 ip_addresses = set()185 ip_addresses = set()
186
173 for iface in netifaces.interfaces():187 for iface in netifaces.interfaces():
174 if iface == "lo":188 if iface == "lo":
175 continue189 continue
176 ip_addrs = netifaces.ifaddresses(iface)190 ip_addrs = netifaces.ifaddresses(iface)
191
177 for iface_type in ip_addrs:192 for iface_type in ip_addrs:
178 if iface_type in (netifaces.AF_INET, netifaces.AF_INET6):193 if iface_type in (netifaces.AF_INET, netifaces.AF_INET6):
179 for addrs in ip_addrs[iface_type]:194 for addrs in ip_addrs[iface_type]:
180 ip_addresses.add(addrs["addr"])195 ip_addresses.add(addrs["addr"])
181196
182 # Note(aluria): and try to match them against rel['private-address']197 # Note(aluria): and try to match them against rel['private-address']
198
183 for rel_type in hookenv.metadata()["requires"].keys():199 for rel_type in hookenv.metadata()["requires"].keys():
184 for rel in hookenv.relations_of_type(rel_type):200 for rel in hookenv.relations_of_type(rel_type):
185 if rel["private-address"] in ip_addresses:201 if rel["private-address"] in ip_addresses:
@@ -188,6 +204,7 @@ def get_remote_unit_name():
188204
189def get_base_inputs():205def get_base_inputs():
190 """Make a structure for rendering the base_inputs template.206 """Make a structure for rendering the base_inputs template.
207
191 Returns a dict of items for the template.208 Returns a dict of items for the template.
192 """209 """
193 extra_options = get_extra_options()210 extra_options = get_extra_options()
@@ -195,6 +212,7 @@ def get_base_inputs():
195 config = hookenv.config()212 config = hookenv.config()
196 str_disabled_plugins = config["disabled_plugins"]213 str_disabled_plugins = config["disabled_plugins"]
197 disabled_plugins = str_disabled_plugins.split(":") if str_disabled_plugins else []214 disabled_plugins = str_disabled_plugins.split(":") if str_disabled_plugins else []
215
198 return {216 return {
199 "extra_options": extra_options["inputs"],217 "extra_options": extra_options["inputs"],
200 "bcache": is_bcache(),218 "bcache": is_bcache(),
@@ -221,20 +239,25 @@ def get_extra_options():
221 # to raw json239 # to raw json
222 json_vals = {}240 json_vals = {}
223 # kind level241 # kind level
242
224 for k, v in extra_options.items():243 for k, v in extra_options.items():
225 json_vals[k] = {}244 json_vals[k] = {}
226 # plugins level245 # plugins level
246
227 for plugin, values in v.items():247 for plugin, values in v.items():
228 json_vals[k][plugin] = {}248 json_vals[k][plugin] = {}
229 # inner plugin (aka key:value)249 # inner plugin (aka key:value)
250
230 for key, val in values.items():251 for key, val in values.items():
231 if key in ("tagpass", "tagdrop"):252 if key in ("tagpass", "tagdrop"):
232 # this is a tagpass/drop, we need to go deeper253 # this is a tagpass/drop, we need to go deeper
233 json_vals[k][plugin][key] = {}254 json_vals[k][plugin][key] = {}
255
234 for tag, tagvalue in val.items():256 for tag, tagvalue in val.items():
235 json_vals[k][plugin][key][tag] = json.dumps(tagvalue)257 json_vals[k][plugin][key][tag] = json.dumps(tagvalue)
236 else:258 else:
237 json_vals[k][plugin][key] = json.dumps(val)259 json_vals[k][plugin][key] = json.dumps(val)
260
238 return json_vals261 return json_vals
239262
240263
@@ -323,13 +346,17 @@ def render_socket_listener_config(context):
323def get_sysstat_config_with_sadc_xall(content):346def get_sysstat_config_with_sadc_xall(content):
324 """Get updated sysstat config content with `-S XALL` in `SADC_OPTIONS`.347 """Get updated sysstat config content with `-S XALL` in `SADC_OPTIONS`.
325348
326 `/etc/sysstat/systat` consists of a sequence of shell variable assignments used to configure sysstat logging.349 `/etc/sysstat/systat` consists of a sequence of shell variable assignments
350 used to configure sysstat logging.
327351
328 Check the original config content.352 Check the original config content.
353
329 If change needed, make the change and return new config content.354 If change needed, make the change and return new config content.
355
330 If no change, return None.356 If no change, return None.
331 """357 """
332 # if SADC_OPTIONS already exists with `-S XALL` in value, no need to change, return None358 # if SADC_OPTIONS already exists with `-S XALL` in value, no need to change,
359 # return None
333 if re.search(r'^SADC_OPTIONS=".*-S\s+XALL.*"', content, flags=re.M):360 if re.search(r'^SADC_OPTIONS=".*-S\s+XALL.*"', content, flags=re.M):
334 return None361 return None
335362
@@ -365,6 +392,7 @@ def update_sysstat_config_with_sdac_xall(path="/etc/sysstat/sysstat"):
365 if os.path.isfile(path):392 if os.path.isfile(path):
366 with open(path, mode="r", encoding="utf8") as f:393 with open(path, mode="r", encoding="utf8") as f:
367 new_text = get_sysstat_config_with_sadc_xall(f.read())394 new_text = get_sysstat_config_with_sadc_xall(f.read())
395
368 if new_text:396 if new_text:
369 hookenv.log("updating {} to ensure `-S XALL` in SADC_OPTIONS".format(path))397 hookenv.log("updating {} to ensure `-S XALL` in SADC_OPTIONS".format(path))
370 with open(path, mode="w", encoding="utf8") as f:398 with open(path, mode="w", encoding="utf8") as f:
@@ -386,18 +414,23 @@ def configure_telegraf(): # noqa: C901
386 "blocked",414 "blocked",
387 "Wrong install_method provided: {!r}".format(config["install_method"]),415 "Wrong install_method provided: {!r}".format(config["install_method"]),
388 )416 )
417
389 return418 return
419
390 if get_remote_unit_name() is None:420 if get_remote_unit_name() is None:
391 hookenv.status_set("waiting", "Waiting for juju-info relation")421 hookenv.status_set("waiting", "Waiting for juju-info relation")
392 # if UNIT_NAME in hostname config and relation not yet available,422 # if UNIT_NAME in hostname config and relation not yet available,
393 # make telegraf unable to start to not get weird metrics names423 # make telegraf unable to start to not get weird metrics names
424
394 if os.path.exists(config_path):425 if os.path.exists(config_path):
395 os.unlink(config_path)426 os.unlink(config_path)
427
396 return428 return
397429
398 inputs = config.get("inputs_config", "")430 inputs = config.get("inputs_config", "")
399 outputs = config.get("outputs_config", "")431 outputs = config.get("outputs_config", "")
400 # just for the migration out of base64432 # just for the migration out of base64
433
401 if inputs:434 if inputs:
402 try:435 try:
403 inputs = base64.b64decode(inputs.encode("utf-8"), validate=True).decode(436 inputs = base64.b64decode(inputs.encode("utf-8"), validate=True).decode(
@@ -406,6 +439,7 @@ def configure_telegraf(): # noqa: C901
406 except binascii.Error:439 except binascii.Error:
407 # not bas64, probably already up to date configs440 # not bas64, probably already up to date configs
408 pass441 pass
442
409 if outputs:443 if outputs:
410 try:444 try:
411 outputs = base64.b64decode(outputs.encode("utf-8"), validate=True).decode(445 outputs = base64.b64decode(outputs.encode("utf-8"), validate=True).decode(
@@ -415,6 +449,7 @@ def configure_telegraf(): # noqa: C901
415 # not bas64, probably already up to date configs449 # not bas64, probably already up to date configs
416 pass450 pass
417 tags = []451 tags = []
452
418 if config["tags"]:453 if config["tags"]:
419 for tag in config["tags"].split(","):454 for tag in config["tags"].split(","):
420 key, value = tag.split("=")455 key, value = tag.split("=")
@@ -424,11 +459,13 @@ def configure_telegraf(): # noqa: C901
424 tags.append('juju_unit = "{}"'.format(get_remote_unit_name().replace("/", "-")))459 tags.append('juju_unit = "{}"'.format(get_remote_unit_name().replace("/", "-")))
425 tags.append('juju_model = "{}"'.format(hookenv.model_name()))460 tags.append('juju_model = "{}"'.format(hookenv.model_name()))
426 context["tags"] = tags461 context["tags"] = tags
462
427 if inputs:463 if inputs:
428 context["inputs"] = inputs464 context["inputs"] = inputs
429 else:465 else:
430 # use base inputs from charm templates466 # use base inputs from charm templates
431 context["inputs"] = render_base_inputs()467 context["inputs"] = render_base_inputs()
468
432 if outputs:469 if outputs:
433 context["outputs"] = outputs470 context["outputs"] = outputs
434 else:471 else:
@@ -438,12 +475,13 @@ def configure_telegraf(): # noqa: C901
438 context["hostname"] = get_hostname_label()475 context["hostname"] = get_hostname_label()
439476
440 logfile_path = os.path.normpath(context["logfile"])477 logfile_path = os.path.normpath(context["logfile"])
478
441 if (479 if (
442 context["logfile"]480 context["logfile"]
443 and not logfile_path.startswith("/var/log/")481 and not logfile_path.startswith("/var/log/") # noqa W503
444 and not (482 and not ( # noqa W503
445 config["install_method"] == "snap"483 config["install_method"] == "snap"
446 and logfile_path.startswith("/var/snap/telegraf/common/")484 and logfile_path.startswith("/var/snap/telegraf/common/") # noqa W503
447 )485 )
448 ):486 ):
449 # only allow logging in /var/log, syslog, or /var/snap/telegraf/common487 # only allow logging in /var/log, syslog, or /var/snap/telegraf/common
@@ -487,6 +525,7 @@ def configure_telegraf(): # noqa: C901
487 )525 )
488526
489 # add sudoers file for telegraf if openvswitch is running527 # add sudoers file for telegraf if openvswitch is running
528
490 if host.service_running("openvswitch-switch"):529 if host.service_running("openvswitch-switch"):
491 sudoers_filename = "telegraf_sudoers"530 sudoers_filename = "telegraf_sudoers"
492 src = os.path.join(get_files_dir(), sudoers_filename)531 src = os.path.join(get_files_dir(), sudoers_filename)
@@ -508,6 +547,7 @@ def configure_telegraf(): # noqa: C901
508547
509 set_flag("telegraf.configured")548 set_flag("telegraf.configured")
510 set_flag("telegraf.needs_reload")549 set_flag("telegraf.needs_reload")
550
511 if config["install_method"] == "deb":551 if config["install_method"] == "deb":
512 set_flag("telegraf.apt.configured")552 set_flag("telegraf.apt.configured")
513 else:553 else:
@@ -525,12 +565,15 @@ def install_telegraf():
525 hookenv.status_set(565 hookenv.status_set(
526 "blocked", "Wrong install_method provided. Expected either 'deb' or 'snap'."566 "blocked", "Wrong install_method provided. Expected either 'deb' or 'snap'."
527 )567 )
568
528 return569 return
570
529 if install_method == "deb":571 if install_method == "deb":
530 try:572 try:
531 snap.remove("telegraf")573 snap.remove("telegraf")
532 except Exception:574 except Exception:
533 # the snap may already be absent, or snaps may not even be supported in this environment575 # the snap may already be absent, or snaps may not even be supported
576 # in this environment
534 pass577 pass
535 apt.queue_install(["telegraf"])578 apt.queue_install(["telegraf"])
536 elif install_method == "snap":579 elif install_method == "snap":
@@ -538,6 +581,7 @@ def install_telegraf():
538 config = hookenv.config()581 config = hookenv.config()
539 snap_channel = config.get("snap_channel")582 snap_channel = config.get("snap_channel")
540 snap.install("telegraf", channel=snap_channel, classic=True)583 snap.install("telegraf", channel=snap_channel, classic=True)
584
541 if install_method:585 if install_method:
542 set_flag("telegraf.installed")586 set_flag("telegraf.installed")
543587
@@ -572,16 +616,19 @@ def upgrade_charm():
572@when("config.changed")616@when("config.changed")
573def handle_config_changes():617def handle_config_changes():
574 config = hookenv.config()618 config = hookenv.config()
619
575 if config.changed("extra_options"):620 if config.changed("extra_options"):
576 for plugin in list_supported_plugins():621 for plugin in list_supported_plugins():
577 clear_flag("plugins.{}.configured".format(plugin))622 clear_flag("plugins.{}.configured".format(plugin))
578 # if something else changed, let's reconfigure telegraf itself just in case623 # if something else changed, let's reconfigure telegraf itself just in case
624
579 if config.changed("extra_plugins"):625 if config.changed("extra_plugins"):
580 clear_flag("extra_plugins.configured")626 clear_flag("extra_plugins.configured")
627
581 if (628 if (
582 config.changed("install_method")629 config.changed("install_method")
583 or config.changed("snap_channel")630 or config.changed("snap_channel") # noqa W503
584 or config.changed("install_sources")631 or config.changed("install_sources") # noqa W503
585 ):632 ):
586 clear_flag("telegraf.installed")633 clear_flag("telegraf.installed")
587 clear_flag("telegraf.configured")634 clear_flag("telegraf.configured")
@@ -595,6 +642,7 @@ def handle_config_changes():
595def configure_extra_plugins():642def configure_extra_plugins():
596 config = hookenv.config()643 config = hookenv.config()
597 plugins = config["extra_plugins"]644 plugins = config["extra_plugins"]
645
598 if plugins:646 if plugins:
599 config_path = "{}/extra_plugins.conf".format(get_configs_dir())647 config_path = "{}/extra_plugins.conf".format(get_configs_dir())
600 host.write_file(config_path, plugins.encode("utf-8"))648 host.write_file(config_path, plugins.encode("utf-8"))
@@ -894,7 +942,7 @@ def redis_input(redis):
894 servers = ["tcp://{{ host }}:{{ port }}"]942 servers = ["tcp://{{ host }}:{{ port }}"]
895 # Until https://github.com/influxdata/telegraf/issues/5036 is fixed943 # Until https://github.com/influxdata/telegraf/issues/5036 is fixed
896 fielddrop = ["aof_last_bgrewrite_status","aof_last_write_status","maxmemory_policy","rdb_last_bgsave_status","used_memory_dataset_perc","used_memory_peak_perc"]944 fielddrop = ["aof_last_bgrewrite_status","aof_last_write_status","maxmemory_policy","rdb_last_bgsave_status","used_memory_dataset_perc","used_memory_peak_perc"]
897"""945""" # noqa E501 (inline template)
898 config_path = "{}/{}.conf".format(get_configs_dir(), "redis")946 config_path = "{}/{}.conf".format(get_configs_dir(), "redis")
899947
900 rels = hookenv.relations_of_type("redis")948 rels = hookenv.relations_of_type("redis")
@@ -1146,7 +1194,8 @@ def prometheus_client(prometheus):
11461194
11471195
1148def convert_days(time_string):1196def convert_days(time_string):
1149 """1197 """Convert string time descript to days.
1198
1150 Function to convert strings like 2w or 14d to a sting containing the number1199 Function to convert strings like 2w or 14d to a sting containing the number
1151 of days.1200 of days.
11521201
@@ -1302,8 +1351,8 @@ def start_or_restart():
1302 active_plugins_changed = helpers.data_changed("active_plugins", states or "")1351 active_plugins_changed = helpers.data_changed("active_plugins", states or "")
1303 if (1352 if (
1304 not host.service_running(service)1353 not host.service_running(service)
1305 or config_files_changed1354 or config_files_changed # noqa W503
1306 or active_plugins_changed1355 or active_plugins_changed # noqa W503
1307 ):1356 ):
1308 hookenv.log("Restarting telegraf")1357 hookenv.log("Restarting telegraf")
1309 host.service_restart(service)1358 host.service_restart(service)
@@ -1328,8 +1377,9 @@ def start_or_restart():
13281377
13291378
1330def is_bcache():1379def is_bcache():
1331 """1380 """Determine if this is a container.
1332 return true if bcache is present, and this is not a container1381
1382 return true if bcache is present, and this is not a container.
1333 """1383 """
1334 container = is_container()1384 container = is_container()
1335 return os.path.exists("/sys/fs/bcache") and not container1385 return os.path.exists("/sys/fs/bcache") and not container
@@ -1352,9 +1402,10 @@ def update_status():
1352@when_not("telegraf.nagios-setup.complete")1402@when_not("telegraf.nagios-setup.complete")
1353def configure_nagios(nagios):1403def configure_nagios(nagios):
1354 """Configure nagios process check.1404 """Configure nagios process check.
1355 the flag 'telegraf.nagios-setup.complete' is reset at the moment config is
1356 changed, so this should make sure that updates are handled."""
13571405
1406 The flag 'telegraf.nagios-setup.complete' is reset at the moment config is
1407 changed, so this should make sure that updates are handled.
1408 """
1358 # Use charmhelpers to handle the configuration of nrpe1409 # Use charmhelpers to handle the configuration of nrpe
1359 hostname = nrpe.get_nagios_hostname()1410 hostname = nrpe.get_nagios_hostname()
1360 nrpe_setup = nrpe.NRPE(hostname=hostname, primary=False)1411 nrpe_setup = nrpe.NRPE(hostname=hostname, primary=False)
diff --git a/src/tests/functional/tests/test_telegraf.py b/src/tests/functional/tests/test_telegraf.py
index 1b80661..3c4354d 100644
--- a/src/tests/functional/tests/test_telegraf.py
+++ b/src/tests/functional/tests/test_telegraf.py
@@ -21,9 +21,8 @@ import unittest
2121
22import requests22import requests
2323
24from zaza.utilities import juju
25from zaza import model24from zaza import model
2625from zaza.utilities import juju
2726
28DEFAULT_HTTPGET_TIMEOUT = 1027DEFAULT_HTTPGET_TIMEOUT = 10
29DEFAULT_RETRIES = 1228DEFAULT_RETRIES = 12
@@ -58,21 +57,24 @@ class TestTelegraf(BaseTelegrafTest):
58 model.get_units(app)57 model.get_units(app)
59 for app in juju.get_principle_applications(self.application_name)58 for app in juju.get_principle_applications(self.application_name)
60 )59 )
60
61 for unit in it.chain.from_iterable(principal_units):61 for unit in it.chain.from_iterable(principal_units):
62 if not unit.public_address:62 if not unit.public_address:
63 continue63 continue
64 url = "http://{}:{}/metrics".format(64 url = "http://{}:{}/metrics".format(
65 unit.public_address, DEFAULT_TELEGRAF_EXPORTER_PORT65 unit.public_address, DEFAULT_TELEGRAF_EXPORTER_PORT
66 )66 )
67
67 for retry in range(DEFAULT_RETRIES):68 for retry in range(DEFAULT_RETRIES):
68 resp = requests.get(url, timeout=DEFAULT_HTTPGET_TIMEOUT)69 resp = requests.get(url, timeout=DEFAULT_HTTPGET_TIMEOUT)
69 self.assertEqual(resp.status_code, 200)70 self.assertEqual(resp.status_code, 200)
7071
71 if (72 if (
72 unit.name.startswith("postgresql/")73 unit.name.startswith("postgresql/")
73 and "postgresql_blks_hit" not in resp.text74 and "postgresql_blks_hit" not in resp.text # noqa W503
74 ) or "cpu_usage_idle" not in resp.text:75 ) or "cpu_usage_idle" not in resp.text:
75 time.sleep(DEFAULT_RETRIES_TIMEOUT)76 time.sleep(DEFAULT_RETRIES_TIMEOUT)
77
76 continue78 continue
7779
78 logging.info("test_service: Unit {} is reachable".format(unit.name))80 logging.info("test_service: Unit {} is reachable".format(unit.name))
@@ -90,6 +92,7 @@ class TestTelegraf(BaseTelegrafTest):
90 r"^zoneinfo_",92 r"^zoneinfo_",
91 r"^processes_",93 r"^processes_",
92 ]94 ]
95
93 for re_pattern in re_patterns:96 for re_pattern in re_patterns:
94 self.check_re_pattern(re_pattern, text)97 self.check_re_pattern(re_pattern, text)
9598
@@ -104,12 +107,14 @@ class TestTelegraf(BaseTelegrafTest):
104 install_method = model.get_application_config("telegraf")["install_method"][107 install_method = model.get_application_config("telegraf")["install_method"][
105 "value"108 "value"
106 ]109 ]
110
107 if install_method == "deb":111 if install_method == "deb":
108 telegraf_conf = DEB_TELEGRAF_CONF112 telegraf_conf = DEB_TELEGRAF_CONF
109 else:113 else:
110 telegraf_conf = SNAP_TELEGRAF_CONF114 telegraf_conf = SNAP_TELEGRAF_CONF
111 cmd = "cat {}".format(telegraf_conf)115 cmd = "cat {}".format(telegraf_conf)
112 response = model.run_on_unit(self.lead_unit_name, cmd)116 response = model.run_on_unit(self.lead_unit_name, cmd)
117
113 if response["Code"] != "0":118 if response["Code"] != "0":
114 self.fail(119 self.fail(
115 "test_02_telegraf_logfile: could not read file {}".format(telegraf_conf)120 "test_02_telegraf_logfile: could not read file {}".format(telegraf_conf)
@@ -118,6 +123,7 @@ class TestTelegraf(BaseTelegrafTest):
118 for line in response["Stdout"].splitlines():123 for line in response["Stdout"].splitlines():
119 if line.strip() == 'logfile = "/var/log/telegraf/telegraf.log"':124 if line.strip() == 'logfile = "/var/log/telegraf/telegraf.log"':
120 logging.info("test_02_telegraf_logfile: logfile config parameter found")125 logging.info("test_02_telegraf_logfile: logfile config parameter found")
126
121 return127 return
122128
123 self.fail(129 self.fail(
@@ -127,25 +133,32 @@ class TestTelegraf(BaseTelegrafTest):
127 )133 )
128134
129 def test_03_system_service(self):135 def test_03_system_service(self):
130 """Check that the right service is running, e.g. either the deb's or the snap's."""136 """Check that the right service is running.
137
138 e.g. either the deb's or the snap's.
139 """
131 install_method = model.get_application_config("telegraf")["install_method"][140 install_method = model.get_application_config("telegraf")["install_method"][
132 "value"141 "value"
133 ]142 ]
134 services = {"deb": "telegraf", "snap": "snap.telegraf.telegraf"}143 services = {"deb": "telegraf", "snap": "snap.telegraf.telegraf"}
144
135 for method in services.keys():145 for method in services.keys():
136 service = services[method]146 service = services[method]
137 cmd = "service {} status".format(service)147 cmd = "service {} status".format(service)
138 response = model.run_on_unit(self.lead_unit_name, cmd)148 response = model.run_on_unit(self.lead_unit_name, cmd)
149
139 if install_method == method and response["Code"] != "0":150 if install_method == method and response["Code"] != "0":
140 self.fail(151 self.fail(
141 "test_03_system_service: service {} should be running on {} but is not. "152 "test_03_system_service: service {} should be running "
153 "on {} but is not. "
142 "install_method is {}.".format(154 "install_method is {}.".format(
143 service, self.lead_unit_name, install_method155 service, self.lead_unit_name, install_method
144 )156 )
145 )157 )
146 elif install_method != method and response["Code"] == "0":158 elif install_method != method and response["Code"] == "0":
147 self.fail(159 self.fail(
148 "test_03_system_service: service {} is running on {} but shouldn't. "160 "test_03_system_service: service {} is running "
161 "on {} but shouldn't. "
149 "install_method is {}.".format(162 "install_method is {}.".format(
150 service, self.lead_unit_name, install_method163 service, self.lead_unit_name, install_method
151 )164 )
diff --git a/src/tests/unit/__init__.py b/src/tests/unit/__init__.py
index 28e9795..5f22a2b 100644
--- a/src/tests/unit/__init__.py
+++ b/src/tests/unit/__init__.py
@@ -1,3 +1,5 @@
1"""Unit test module."""
2
1import sys3import sys
24
3sys.path.append(".")5sys.path.append(".")
diff --git a/src/tests/unit/test_mysql.py b/src/tests/unit/test_mysql.py
index 27f206d..9015600 100644
--- a/src/tests/unit/test_mysql.py
+++ b/src/tests/unit/test_mysql.py
@@ -22,10 +22,11 @@ from unittest.mock import ANY, MagicMock, patch, sentinel
22# Mock layer modules22# Mock layer modules
23import charms23import charms
2424
25import reactive
26
25promreg = MagicMock()27promreg = MagicMock()
26charms.promreg = promreg28charms.promreg = promreg
27sys.modules["charms.promreg"] = promreg29sys.modules["charms.promreg"] = promreg
28import reactive
2930
3031
31class TestMySQL(unittest.TestCase):32class TestMySQL(unittest.TestCase):
diff --git a/src/tests/unit/test_postgresql.py b/src/tests/unit/test_postgresql.py
index 9192ea4..c011b32 100644
--- a/src/tests/unit/test_postgresql.py
+++ b/src/tests/unit/test_postgresql.py
@@ -17,7 +17,7 @@
17import os17import os
18import sys18import sys
19import unittest19import unittest
20from unittest.mock import ANY, call, MagicMock, patch, sentinel20from unittest.mock import ANY, MagicMock, call, patch, sentinel
2121
22# Mock layer modules22# Mock layer modules
23import charms23import charms
@@ -32,8 +32,8 @@ sys.modules["charms.apt"] = apt
32sys.modules["charms.layer"] = layer32sys.modules["charms.layer"] = layer
33sys.modules["charms.promreg"] = promreg33sys.modules["charms.promreg"] = promreg
3434
35import reactive35import reactive # noqa E402
36from reactive import telegraf36from reactive import telegraf # noqa E402
3737
3838
39class TestPostgreSQL(unittest.TestCase):39class TestPostgreSQL(unittest.TestCase):
@@ -123,7 +123,7 @@ class TestPostgreSQL(unittest.TestCase):
123 get_base_dir.return_value = "/etc/telegraf"123 get_base_dir.return_value = "/etc/telegraf"
124124
125 reactive.telegraf.render_postgresql_tmpl(125 reactive.telegraf.render_postgresql_tmpl(
126 [{"replica": "master", "conn_str": "my_conn_str", "server": "my_server"},]126 [{"replica": "master", "conn_str": "my_conn_str", "server": "my_server"}]
127 )127 )
128128
129 write_file.assert_called_once_with(129 write_file.assert_called_once_with(
diff --git a/src/tests/unit/test_telegraf.py b/src/tests/unit/test_telegraf.py
index 1825fa6..57e7e3c 100644
--- a/src/tests/unit/test_telegraf.py
+++ b/src/tests/unit/test_telegraf.py
@@ -14,36 +14,38 @@
14# You should have received a copy of the GNU General Public License14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17"""actions.py tests"""17"""Tests for actions.py."""
18import base6418import base64
19import os
20import getpass19import getpass
21import grp20import grp
22import json21import json
23import sys22import os
24import subprocess23import subprocess
24import sys
25import unittest25import unittest
26from textwrap import dedent26from textwrap import dedent
27from unittest.mock import patch, MagicMock, call
28from unittest import mock27from unittest import mock
28from unittest.mock import MagicMock, call, patch
2929
30import yaml
31import pytest
32import py
33
34from charms.reactive import bus, helpers, RelationBase, set_flag
35from charmhelpers.core.hookenv import Config30from charmhelpers.core.hookenv import Config
36from charmhelpers.core.templating import render31from charmhelpers.core.templating import render
3732
38# Mock layer modules
39import charms33import charms
34from charms.reactive import RelationBase, bus, helpers, set_flag
35
36import py
4037
38import pytest
39
40import yaml
41
42# Mock layer modules
41promreg = MagicMock()43promreg = MagicMock()
42charms.promreg = promreg44charms.promreg = promreg
43sys.modules["charms.promreg"] = promreg45sys.modules["charms.promreg"] = promreg
4446
45import reactive47import reactive # noqa E402
46from reactive import telegraf48from reactive import telegraf # noqa E402
4749
48UNIT_TESTS_DIR = os.path.dirname(os.path.abspath(__file__))50UNIT_TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
49UNIT_TESTS_DATA_DIR = os.path.join(UNIT_TESTS_DIR, "data")51UNIT_TESTS_DATA_DIR = os.path.join(UNIT_TESTS_DIR, "data")
@@ -71,6 +73,7 @@ def setup(monkeypatch, tmpdir):
71 # fix the args to use non-root, this is for callers that pass73 # fix the args to use non-root, this is for callers that pass
72 # owner/group as positional arguments like74 # owner/group as positional arguments like
73 # charmhelpers.core.templating.render75 # charmhelpers.core.templating.render
76
74 if len(a) > 2:77 if len(a) > 2:
75 if a[2] == "root" and a[3] == "root":78 if a[2] == "root" and a[3] == "root":
76 # make all files writable by owner, as we need don't run as root79 # make all files writable by owner, as we need don't run as root
@@ -80,6 +83,7 @@ def setup(monkeypatch, tmpdir):
80 kw["group"] = group83 kw["group"] = group
81 # make all files writable by owner, as we need don't run as root84 # make all files writable by owner, as we need don't run as root
82 kw["perms"] = 0o74485 kw["perms"] = 0o744
86
83 return orig_write_file(*a, **kw)87 return orig_write_file(*a, **kw)
8488
85 monkeypatch.setattr(telegraf.host, "write_file", intercept_write_file)89 monkeypatch.setattr(telegraf.host, "write_file", intercept_write_file)
@@ -94,6 +98,7 @@ def cleanup(request):
94 unitdata._KV = None98 unitdata._KV = None
95 # rm unit-state.db file99 # rm unit-state.db file
96 unit_state_db = os.path.join(telegraf.hookenv.charm_dir(), ".unit-state.db")100 unit_state_db = os.path.join(telegraf.hookenv.charm_dir(), ".unit-state.db")
101
97 if os.path.exists(unit_state_db):102 if os.path.exists(unit_state_db):
98 os.unlink(unit_state_db)103 os.unlink(unit_state_db)
99104
@@ -134,6 +139,7 @@ def config(monkeypatch, temp_charm_dir):
134 data = dict((k, v["default"]) for k, v in raw_config["options"].items())139 data = dict((k, v["default"]) for k, v in raw_config["options"].items())
135 config = Config(data)140 config = Config(data)
136 monkeypatch.setattr(telegraf.hookenv, "config", lambda: config)141 monkeypatch.setattr(telegraf.hookenv, "config", lambda: config)
142
137 return config143 return config
138144
139145
@@ -147,13 +153,14 @@ def configs_dir():
147153
148154
149def persist_state():155def persist_state():
150 """Fake persistent state by calling helpers that modify unitdata.kv"""156 """Fake persistent state by calling helpers that modify unitdata.kv."""
151 states = [157 states = [
152 k158 k
153 for k in bus.get_states().keys()159 for k in bus.get_states().keys()
154 if k.startswith("plugins") or k.startswith("extra_plugins")160 if k.startswith("plugins") or k.startswith("extra_plugins")
155 ]161 ]
156 helpers.any_file_changed(telegraf.list_config_files())162 helpers.any_file_changed(telegraf.list_config_files())
163
157 if states:164 if states:
158 helpers.data_changed("active_plugins", states)165 helpers.data_changed("active_plugins", states)
159166
@@ -169,7 +176,7 @@ def check_sysstat_config(original, expected):
169176
170177
171def test_sadc_options_correct(monkeypatch):178def test_sadc_options_correct(monkeypatch):
172 """If SADC_OPTIONS is already correct, should return None"""179 """If SADC_OPTIONS is already correct, should return None."""
173 original = dedent(180 original = dedent(
174 """181 """
175 # some comment182 # some comment
@@ -183,7 +190,7 @@ def test_sadc_options_correct(monkeypatch):
183190
184191
185def test_sadc_options_correct_included(monkeypatch):192def test_sadc_options_correct_included(monkeypatch):
186 """If SADC_OPTIONS already includes `-S XALL`, should return None"""193 """If SADC_OPTIONS already includes `-S XALL`, should return None."""
187 original = dedent(194 original = dedent(
188 """195 """
189 # some comment196 # some comment
@@ -197,7 +204,7 @@ def test_sadc_options_correct_included(monkeypatch):
197204
198205
199def test_sadc_options_non_exist(monkeypatch):206def test_sadc_options_non_exist(monkeypatch):
200 """If SADC_OPTIONS doesn't exist, should just append"""207 """If SADC_OPTIONS doesn't exist, should just append."""
201 original = dedent(208 original = dedent(
202 """209 """
203 # some comment210 # some comment
@@ -217,7 +224,7 @@ def test_sadc_options_non_exist(monkeypatch):
217224
218225
219def test_sadc_options_commented(monkeypatch):226def test_sadc_options_commented(monkeypatch):
220 """If SADC_OPTIONS exists but commented, should ignore and append"""227 """If SADC_OPTIONS exists but commented, should ignore and append."""
221 original = dedent(228 original = dedent(
222 """229 """
223 # some comment230 # some comment
@@ -243,7 +250,7 @@ def test_sadc_options_commented(monkeypatch):
243250
244251
245def test_sadc_options_incorrect(monkeypatch):252def test_sadc_options_incorrect(monkeypatch):
246 """If SADC_OPTIONS exists but not XALL, should replace"""253 """If SADC_OPTIONS exists but not XALL, should replace."""
247 original = dedent(254 original = dedent(
248 """255 """
249 # some comment256 # some comment
@@ -267,7 +274,7 @@ def test_sadc_options_incorrect(monkeypatch):
267274
268275
269def test_sadc_options_incorrect_included(monkeypatch):276def test_sadc_options_incorrect_included(monkeypatch):
270 """If SADC_OPTIONS exists but not XALL, should replace"""277 """If SADC_OPTIONS exists but not XALL, should replace."""
271 original = dedent(278 original = dedent(
272 """279 """
273 # some comment280 # some comment
@@ -291,7 +298,7 @@ def test_sadc_options_incorrect_included(monkeypatch):
291298
292299
293def test_sadc_options_commented_line_not_touched(monkeypatch):300def test_sadc_options_commented_line_not_touched(monkeypatch):
294 """If SADC_OPTIONS exists but has no -S, should append"""301 """If SADC_OPTIONS exists but has no -S, should append."""
295 original = dedent(302 original = dedent(
296 """303 """
297 # some comment304 # some comment
@@ -318,7 +325,7 @@ def test_sadc_options_commented_line_not_touched(monkeypatch):
318325
319326
320def test_sadc_options_s_missing(monkeypatch):327def test_sadc_options_s_missing(monkeypatch):
321 """If SADC_OPTIONS exists but has no -S, should append"""328 """If SADC_OPTIONS exists but has no -S, should append."""
322 original = dedent(329 original = dedent(
323 """330 """
324 # some comment331 # some comment
@@ -351,6 +358,7 @@ def test_telegraf_exec_metrics(monkeypatch, temp_config_dir):
351 metrics = set(358 metrics = set(
352 ["sockstat", "sockstat6", "softnet_stat", "buddyinfo", "zoneinfo", "netns"]359 ["sockstat", "sockstat6", "softnet_stat", "buddyinfo", "zoneinfo", "netns"]
353 )360 )
361
354 for metric in metrics:362 for metric in metrics:
355 run_telegraf_exec_metrics("--metric", metric)363 run_telegraf_exec_metrics("--metric", metric)
356364
@@ -370,9 +378,8 @@ def test_telegraf_exec_metrics(monkeypatch, temp_config_dir):
370 )378 )
371379
372 # ensure config files exists after render380 # ensure config files exists after render
373 run_telegraf_exec_metrics(381 run_telegraf_exec_metrics("--render-config-files", "--configs-dir", configs_dir())
374 "--render-config-files", "--configs-dir", configs_dir(),382
375 )
376 for metric in metrics:383 for metric in metrics:
377 assert os.path.isfile(os.path.join(configs_dir(), "{}.conf".format(metric)))384 assert os.path.isfile(os.path.join(configs_dir(), "{}.conf".format(metric)))
378385
@@ -386,9 +393,11 @@ def test_telegraf_exec_metrics(monkeypatch, temp_config_dir):
386 ":".join(disabled_metrics),393 ":".join(disabled_metrics),
387 )394 )
388 # config files for disabled metrics should be removed.395 # config files for disabled metrics should be removed.
396
389 for metric in disabled_metrics:397 for metric in disabled_metrics:
390 assert not os.path.isfile(os.path.join(configs_dir(), "{}.conf".format(metric)))398 assert not os.path.isfile(os.path.join(configs_dir(), "{}.conf".format(metric)))
391 # config files for other metrics should still be there399 # config files for other metrics should still be there
400
392 for metric in metrics - disabled_metrics:401 for metric in metrics - disabled_metrics:
393 assert os.path.isfile(os.path.join(configs_dir(), "{}.conf".format(metric)))402 assert os.path.isfile(os.path.join(configs_dir(), "{}.conf".format(metric)))
394403
@@ -619,7 +628,7 @@ inputs:
619628
620629
621def test_base_inputs_disabled_plugins(config):630def test_base_inputs_disabled_plugins(config):
622 """Check disabled_plugins option is working for builtin inputs"""631 """Check disabled_plugins option is working for builtin inputs."""
623 config["disabled_plugins"] = "cpu:disk"632 config["disabled_plugins"] = "cpu:disk"
624 content = telegraf.render_base_inputs()633 content = telegraf.render_base_inputs()
625634
@@ -1246,7 +1255,7 @@ class TestGrafanaDashboard(unittest.TestCase):
1246 variable_end_string=">>",1255 variable_end_string=">>",
1247 )1256 )
1248 mock_grafana.register_dashboard.assert_called_once_with(1257 mock_grafana.register_dashboard.assert_called_once_with(
1249 name=telegraf.GRAFANA_DASHBOARD_NAME, dashboard=mock_dashboard_dict,1258 name=telegraf.GRAFANA_DASHBOARD_NAME, dashboard=mock_dashboard_dict
1250 )1259 )
1251 mock_set_flag.assert_called_once_with("grafana.configured")1260 mock_set_flag.assert_called_once_with("grafana.configured")
12521261
diff --git a/src/tox.ini b/src/tox.ini
index 40c62d6..4977737 100644
--- a/src/tox.ini
+++ b/src/tox.ini
@@ -42,7 +42,8 @@ exclude =
42 charmhelpers,42 charmhelpers,
43 mod,43 mod,
44 .build44 .build
45ignore = I100, D101, D102, D103, I201, D205, W504, D400, D401, D403, D209, D100, E403, I101, E501, N803, E226, E128, D200, E741, D202, E261, E402, D104, W503, E231 # TODO45#ignore = I100, D101, D102, D103, I201, D205, W504, D400, D401, D403, D209, D100, E403, I101, E501, N803, E226, E128, D200, E741, D202, E261, E402, D104, W503, E231 # TODO
46ignore = D100, D101, D102, D103
4647
47max-line-length = 8848max-line-length = 88
48max-complexity = 1049max-complexity = 10

Subscribers

People subscribed via source and target branches

to all changes: