|
18 | 18 | NAV that wants to make use of DHCP stats. |
19 | 19 | """ |
20 | 20 |
|
| 21 | +from collections import defaultdict |
21 | 22 | from dataclasses import dataclass |
22 | | -from typing import Iterable, Literal, Optional, Union |
| 23 | +from typing import Any, Iterable, Literal, Optional, Union |
23 | 24 |
|
24 | 25 | import IPy |
25 | 26 |
|
| 27 | +from nav.metrics.graphs import ( |
| 28 | + aliased_series, |
| 29 | + diffed_series, |
| 30 | + json_graph_url, |
| 31 | + nonempty_series, |
| 32 | + summed_series, |
| 33 | +) |
| 34 | +from nav.metrics.names import get_expanded_nodes, safe_name |
26 | 35 | from nav.metrics.templates import metric_path_for_dhcp |
27 | 36 |
|
28 | 37 |
|
|
31 | 40 | GraphiteMetric = tuple[str, tuple[float, int]] |
32 | 41 |
|
33 | 42 |
|
| 43 | +def fetch_graph_urls_for_prefixes(prefixes: list[IPy.IP]) -> list[str]: |
| 44 | + """ |
| 45 | + Takes a list of IP prefixes, queries Graphite for DHCP stat paths, and |
| 46 | + returns a list of Graphite graph URLs; one URL for each group of DHCP stat |
| 47 | + paths in Graphite where at least one path in the group represents a range of |
| 48 | + IP addresses that intersect with one or more of the given prefixes. |
| 49 | +
|
| 50 | + Each returned Graphite URL points to JSON graph data and can be supplied |
| 51 | + directly to NAV's JavaScript RickshawGraph function. |
| 52 | + """ |
| 53 | + if not prefixes: |
| 54 | + return [] |
| 55 | + |
| 56 | + all_paths = fetch_paths_from_graphite() |
| 57 | + grouped_paths = group_paths(all_paths) |
| 58 | + filtered_grouped_paths = drop_groups_not_in_prefixes(grouped_paths, prefixes) |
| 59 | + |
| 60 | + graph_urls: list[str] = [] |
| 61 | + for paths_of_same_group in sorted(filtered_grouped_paths): |
| 62 | + paths_of_same_group = sorted(paths_of_same_group) |
| 63 | + graph_lines = [] |
| 64 | + for path in paths_of_same_group: |
| 65 | + assigned_addresses = aliased_series( |
| 66 | + nonempty_series( |
| 67 | + path.to_graphite_path("assigned"), |
| 68 | + ), |
| 69 | + name=f"Assigned addresses in {path.first_ip} - {path.last_ip}", |
| 70 | + renderer="area", |
| 71 | + ) |
| 72 | + graph_lines.append(assigned_addresses) |
| 73 | + |
| 74 | + assert len(paths_of_same_group) > 0 |
| 75 | + path = paths_of_same_group[0] # Just select an arbitrary path instance in the group |
| 76 | + unassigned_addresses = aliased_series( |
| 77 | + diffed_series( |
| 78 | + summed_series( |
| 79 | + nonempty_series( |
| 80 | + path.to_graphite_path("total", wildcard_for_group=True) |
| 81 | + ), |
| 82 | + ), |
| 83 | + summed_series( |
| 84 | + nonempty_series( |
| 85 | + path.to_graphite_path("assigned", wildcard_for_group=True) |
| 86 | + ), |
| 87 | + ), |
| 88 | + ), |
| 89 | + name="Unassigned addresses", |
| 90 | + renderer="area", |
| 91 | + color="#d9d9d9", |
| 92 | + ) |
| 93 | + graph_lines.append(unassigned_addresses) |
| 94 | + |
| 95 | + total_addresses = aliased_series( |
| 96 | + summed_series( |
| 97 | + nonempty_series( |
| 98 | + path.to_graphite_path("total", wildcard_for_group=True) |
| 99 | + ), |
| 100 | + ), |
| 101 | + name="Total addresses", |
| 102 | + color="#707070", |
| 103 | + ) |
| 104 | + graph_lines.append(total_addresses) |
| 105 | + |
| 106 | + type_human = path.allocation_type + "s" |
| 107 | + title = f"DHCP {type_human} in '{path.group_name}' on server '{path.server_name}'" |
| 108 | + graph_urls.append(json_graph_url(*graph_lines, title=title)) |
| 109 | + return graph_urls |
| 110 | + |
| 111 | + |
| 112 | +def fetch_paths_from_graphite(): |
| 113 | + """ |
| 114 | + Fetches and returns all unique DHCP stat paths in Graphite when their |
| 115 | + trailing metric_name path segment has been removed. |
| 116 | + """ |
| 117 | + wildcard = metric_path_for_dhcp( |
| 118 | + ip_version=safe_name("{4,6}"), |
| 119 | + server_name=safe_name("*"), |
| 120 | + allocation_type=safe_name("{range,pool,subnet}"), |
| 121 | + group_name_source=safe_name("{custom_groups,special_groups}"), |
| 122 | + group_name=safe_name("*"), |
| 123 | + first_ip=safe_name("*"), |
| 124 | + last_ip=safe_name("*"), |
| 125 | + metric_name="total", |
| 126 | + ) |
| 127 | + graphite_paths = get_expanded_nodes(wildcard) |
| 128 | + |
| 129 | + native_paths: list[DhcpPath] = [] |
| 130 | + for graphite_path in graphite_paths: |
| 131 | + try: |
| 132 | + native_path = DhcpPath.from_graphite_path(graphite_path) |
| 133 | + except ValueError: |
| 134 | + pass |
| 135 | + else: |
| 136 | + native_paths.append(native_path) |
| 137 | + return native_paths |
| 138 | + |
| 139 | + |
| 140 | +def group_paths(paths: list["DhcpPath"]): |
| 141 | + """ |
| 142 | + Takes a list of DhcpPath instances and partitions it into multiple lists, |
| 143 | + such that two DhcpPath instances belong to the same list if and only if they |
| 144 | + have the same group data. |
| 145 | + """ |
| 146 | + grouped_paths: dict[Any, list[DhcpPath]] = defaultdict(list) |
| 147 | + for path in paths: |
| 148 | + group_total = path.to_graphite_path("total", wildcard_for_group=True) |
| 149 | + group_assigned = path.to_graphite_path("assigned", wildcard_for_group=True) |
| 150 | + grouped_paths[(group_total, group_assigned)].append(path) |
| 151 | + return list(grouped_paths.values()) |
| 152 | + |
| 153 | + |
| 154 | +def drop_groups_not_in_prefixes(grouped_paths: list[list["DhcpPath"]], prefixes: list[IPy.IP]): |
| 155 | + """ |
| 156 | + Takes a list of grouped DhcpPath instances, and return only the groups that |
| 157 | + has at least one DhcpPath instance that intersect with one or more of the |
| 158 | + given prefixes. |
| 159 | + """ |
| 160 | + grouped_paths_to_keep: list[list[DhcpPath]] = [] |
| 161 | + for paths_of_same_group in grouped_paths: |
| 162 | + if any( |
| 163 | + path.intersects(prefixes) |
| 164 | + for path in paths_of_same_group |
| 165 | + ): |
| 166 | + grouped_paths_to_keep.append(paths_of_same_group) |
| 167 | + return grouped_paths_to_keep |
| 168 | + |
| 169 | + |
34 | 170 | @dataclass(frozen=True, order=True) |
35 | 171 | class DhcpPath: |
36 | 172 | """ |
|
0 commit comments