#!/usr/bin/env python3
"""Report state based on FAUCET/Gauge/Prometheus variables."""
# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2019 The Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# pytype: disable=pyi-error
# pytype: disable=import-error
import argparse
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
import requests
from prometheus_client import parser
# TODO: byte/packet counters could be per second (given multiple samples)
[docs]
def decode_value(metric_name, value):
"""Convert values to human readible format based on metric name"""
result = value
if metric_name == "learned_macs":
result = ":".join(
format(octet, "02x")
for octet in int(value).to_bytes( # pytype: disable=attribute-error
6, byteorder="big"
)
)
return result
[docs]
def scrape_prometheus(endpoints, retries=3, err_output_file=sys.stdout):
"""Scrape a list of Prometheus/FAUCET/Gauge endpoints and aggregate results."""
metrics = []
for endpoint in endpoints:
content = None
err = None
for _ in range(retries):
try:
if endpoint.startswith("http"):
response = requests.get(endpoint, timeout=30)
if (
response.status_code
== requests.status_codes.codes.ok # pylint: disable=no-member
):
content = response.content.decode("utf-8", "strict")
break
else:
with urllib.request.urlopen( # pytype: disable=module-attr
endpoint
) as response:
content = response.read().decode("utf-8", "strict")
break
except (requests.exceptions.ConnectionError, ValueError) as exception:
err = exception
time.sleep(1)
if err is not None:
err_output_file.write(str(err))
return None
try:
endpoint_metrics = parser.text_string_to_metric_families(content)
metrics.extend(endpoint_metrics)
except ValueError as err:
err_output_file.write(str(err))
return None
return metrics
def _get_samples_from_metrics(metrics, metric_name, label_matches, nonzero_only=False):
result = []
for metric in metrics:
if metric_name is None or metric.name == metric_name:
for sample in metric.samples:
labels = sample.labels
value = sample.value
if label_matches is None or set(label_matches.items()).issubset(
set(labels.items())
):
if nonzero_only and int(value) == 0:
continue
result.append(sample)
return result
[docs]
def get_samples(endpoints, metric_name, label_matches, nonzero_only=False, retries=3):
"""return a list of Prometheus samples for a given metric
Prometheus Sample objects are named tuples with the fields: name, labels,
value, timestamp, exemplar.
Arguments:
endpoints (list of strings): the prometheus endpoints to query
metric_name (string): the metric to retrieve
label_matches (dict): filters results by label
nonzero_only (bool): only return samples with non-zero values
retries (int): number of retries when querying
Returns:
list of Prometheus Sample objects"""
metrics = scrape_prometheus(endpoints, retries)
if metrics is None:
return None
return _get_samples_from_metrics(metrics, metric_name, label_matches, nonzero_only)
[docs]
def report_label_match_metrics(
report_metrics,
metrics,
display_labels=None,
nonzero_only=False,
delim="\t",
label_matches=None,
):
"""Text report on a list of Prometheus metrics."""
report_output = []
if report_metrics is None:
report_metrics = [None]
for metric_name in report_metrics:
for sample in _get_samples_from_metrics(
metrics, metric_name, label_matches, nonzero_only
):
labels = sample.labels
value = sample.value
sorted_labels = [
(key, val)
for key, val in sorted(labels.items())
if not display_labels or key in display_labels
]
value = decode_value(metric_name, value)
report_output.append(
delim.join((metric_name, str(sorted_labels), str(value)))
)
report_output = "\n".join(report_output)
return report_output
[docs]
def parse_args(sys_args):
"""Parse and return CLI args."""
arg_parser = argparse.ArgumentParser(
prog="fctl",
description="Retrieve FAUCET/Gauge state using Prometheus variables.",
usage="""
MACs learned on a DP.
{argv0} -n --endpoints=http://172.17.0.1:9302 --metrics=learned_macs --labels=dp_id:0xb827eb608918
Status of all DPs
{argv0} -n --endpoints=http://172.17.0.1:9302 --metrics=dp_status
""".format(
**{"argv0": sys.argv[0]}
),
)
arg_parser.add_argument(
"-n", "--nonzero", action="store_true", help="nonzero results only"
)
arg_parser.add_argument("-e", "--endpoints", help="list of endpoint URLs to query")
arg_parser.add_argument("-m", "--metrics", help="list of metrics to query")
arg_parser.add_argument(
"-l", "--labels", help="list of labels that must be present"
)
arg_parser.add_argument(
"--display-labels", help="list of labels to filter display by (default all)"
)
endpoints = []
report_metrics = []
label_matches = None
display_labels = None
nonzero_only = False
try:
args = arg_parser.parse_args(sys_args)
if args.nonzero:
nonzero_only = True
if args.endpoints:
endpoints = args.endpoints.split(",")
if args.metrics:
report_metrics = args.metrics.split(",")
if args.labels:
label_matches = {}
for label_value in args.labels.split(","):
label, value = label_value.split(":")
label_matches[label] = value
if args.display_labels:
display_labels = args.display_labels.split(",")
except (KeyError, IndexError):
arg_parser.print_usage()
sys.exit(-1)
return (endpoints, report_metrics, label_matches, nonzero_only, display_labels)
[docs]
def main():
(
endpoints,
report_metrics,
label_matches,
nonzero_only,
display_labels,
) = parse_args(sys.argv[1:])
metrics = scrape_prometheus(endpoints)
if metrics is None:
sys.exit(1)
report = report_label_match_metrics(
report_metrics,
metrics,
nonzero_only=nonzero_only,
label_matches=label_matches,
display_labels=display_labels,
)
print(report)
if __name__ == "__main__":
main()