netdata-cli 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netdata_cli/__init__.py +1 -1
- netdata_cli/cli.py +466 -8
- netdata_cli/mcp_client.py +263 -0
- {netdata_cli-0.1.0.dist-info → netdata_cli-0.2.0.dist-info}/METADATA +6 -2
- netdata_cli-0.2.0.dist-info/RECORD +11 -0
- netdata_cli-0.1.0.dist-info/RECORD +0 -10
- {netdata_cli-0.1.0.dist-info → netdata_cli-0.2.0.dist-info}/WHEEL +0 -0
- {netdata_cli-0.1.0.dist-info → netdata_cli-0.2.0.dist-info}/entry_points.txt +0 -0
- {netdata_cli-0.1.0.dist-info → netdata_cli-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {netdata_cli-0.1.0.dist-info → netdata_cli-0.2.0.dist-info}/top_level.txt +0 -0
netdata_cli/__init__.py
CHANGED
netdata_cli/cli.py
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
"""netdata-cli — CLI entry point.
|
|
1
|
+
"""netdata-cli — CLI entry point.
|
|
2
|
+
|
|
3
|
+
Combines Netdata REST API commands with MCP (Model Context Protocol) tools.
|
|
4
|
+
"""
|
|
2
5
|
|
|
3
6
|
from __future__ import annotations
|
|
4
7
|
|
|
8
|
+
import json
|
|
5
9
|
import sys
|
|
6
10
|
|
|
7
11
|
import click
|
|
8
12
|
|
|
9
13
|
from . import __version__
|
|
10
14
|
from .client import NetdataClient, NetdataError
|
|
15
|
+
from .mcp_client import MCPClient, MCPError
|
|
11
16
|
from .formatters import (
|
|
12
17
|
HAS_RICH,
|
|
13
18
|
console,
|
|
@@ -26,7 +31,11 @@ def _client(url: str, timeout: int) -> NetdataClient:
|
|
|
26
31
|
return NetdataClient(base_url=url, timeout=timeout)
|
|
27
32
|
|
|
28
33
|
|
|
29
|
-
def
|
|
34
|
+
def _mcp_client(url: str, timeout: int) -> MCPClient:
|
|
35
|
+
return MCPClient(base_url=url, timeout=timeout)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _handle_error(exc: Exception) -> None:
|
|
30
39
|
msg = f"Error: {exc}"
|
|
31
40
|
if console:
|
|
32
41
|
console.print(f"[bold red]{msg}[/]")
|
|
@@ -35,6 +44,16 @@ def _handle_error(exc: NetdataError) -> None:
|
|
|
35
44
|
sys.exit(1)
|
|
36
45
|
|
|
37
46
|
|
|
47
|
+
def _output(data, as_json: bool, title: str = "") -> None:
|
|
48
|
+
"""Output data as JSON or pretty-printed."""
|
|
49
|
+
if as_json:
|
|
50
|
+
print_json(data)
|
|
51
|
+
elif HAS_RICH and isinstance(data, (dict, list)):
|
|
52
|
+
console.print_json(json.dumps(data, default=str, ensure_ascii=False))
|
|
53
|
+
else:
|
|
54
|
+
print(json.dumps(data, indent=2, default=str, ensure_ascii=False))
|
|
55
|
+
|
|
56
|
+
|
|
38
57
|
# ------------------------------------------------------------------
|
|
39
58
|
# Root group
|
|
40
59
|
# ------------------------------------------------------------------
|
|
@@ -54,18 +73,30 @@ def _handle_error(exc: NetdataError) -> None:
|
|
|
54
73
|
@click.option("--json", "-j", "as_json", is_flag=True, help="Output raw JSON.")
|
|
55
74
|
@click.pass_context
|
|
56
75
|
def cli(ctx: click.Context, url: str, timeout: int, as_json: bool) -> None:
|
|
57
|
-
"""netdata-cli — A powerful CLI for the Netdata Agent REST API.
|
|
76
|
+
"""netdata-cli — A powerful CLI for the Netdata Agent REST API + MCP.
|
|
58
77
|
|
|
59
78
|
Query metrics, charts, alarms, functions, and agent info from the
|
|
60
79
|
command line. Supports rich tables, JSON output, and time filtering.
|
|
61
80
|
|
|
81
|
+
REST API commands: info, ping, charts, chart, data, alarms, alarm-log,
|
|
82
|
+
allmetrics, function, functions, collectors
|
|
83
|
+
|
|
84
|
+
MCP commands: mcp list-tools, mcp nodes, mcp metrics, mcp query,
|
|
85
|
+
mcp anomalies, mcp correlated, mcp alerts, etc.
|
|
86
|
+
|
|
62
87
|
Set NETDATA_URL env var to change the default agent address.
|
|
63
88
|
"""
|
|
64
89
|
ctx.ensure_object(dict)
|
|
65
90
|
ctx.obj["client"] = _client(url, timeout)
|
|
91
|
+
ctx.obj["mcp_client"] = _mcp_client(url, timeout)
|
|
66
92
|
ctx.obj["as_json"] = as_json
|
|
67
93
|
|
|
68
94
|
|
|
95
|
+
# ==================================================================
|
|
96
|
+
# REST API COMMANDS (existing)
|
|
97
|
+
# ==================================================================
|
|
98
|
+
|
|
99
|
+
|
|
69
100
|
# ------------------------------------------------------------------
|
|
70
101
|
# info
|
|
71
102
|
# ------------------------------------------------------------------
|
|
@@ -134,11 +165,9 @@ def charts(ctx: click.Context, query: str | None, chart_type: str | None, limit:
|
|
|
134
165
|
for chart_id, info in items.items():
|
|
135
166
|
entry = {"id": chart_id, **info}
|
|
136
167
|
|
|
137
|
-
# type filter
|
|
138
168
|
if chart_type and info.get("type", "") != chart_type:
|
|
139
169
|
continue
|
|
140
170
|
|
|
141
|
-
# text search
|
|
142
171
|
if query:
|
|
143
172
|
q = query.lower()
|
|
144
173
|
searchable = " ".join(
|
|
@@ -258,7 +287,6 @@ def alarms(ctx: click.Context, show_all: bool, active_only: bool) -> None:
|
|
|
258
287
|
if a.get("status", "").upper() in ("WARNING", "CRITICAL")
|
|
259
288
|
]
|
|
260
289
|
|
|
261
|
-
# sort by status severity
|
|
262
290
|
severity_order = {"CRITICAL": 0, "WARNING": 1, "UNDEFINED": 2, "CLEAR": 3, "REMOVED": 4}
|
|
263
291
|
alarm_list.sort(key=lambda a: severity_order.get(a.get("status", "").upper(), 5))
|
|
264
292
|
|
|
@@ -292,7 +320,6 @@ def alarm_log(ctx: click.Context, after: int, unique: bool) -> None:
|
|
|
292
320
|
else:
|
|
293
321
|
entries = c.alarm_log(after=after if after else 0)
|
|
294
322
|
|
|
295
|
-
# Normalize — API returns dict with 'alarms' key or a list
|
|
296
323
|
if isinstance(entries, dict):
|
|
297
324
|
entries = entries.get("alarms", [])
|
|
298
325
|
if isinstance(entries, list):
|
|
@@ -325,7 +352,6 @@ def allmetrics(ctx: click.Context, fmt: str) -> None:
|
|
|
325
352
|
if ctx.obj["as_json"]:
|
|
326
353
|
print_json(data)
|
|
327
354
|
else:
|
|
328
|
-
# Show summary
|
|
329
355
|
for name in sorted(data.keys())[:30]:
|
|
330
356
|
entry = data[name]
|
|
331
357
|
dims = entry.get("dimensions", {})
|
|
@@ -448,6 +474,438 @@ def collectors(ctx: click.Context) -> None:
|
|
|
448
474
|
_handle_error(exc)
|
|
449
475
|
|
|
450
476
|
|
|
477
|
+
# ==================================================================
|
|
478
|
+
# MCP COMMANDS (new)
|
|
479
|
+
# ==================================================================
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@cli.group()
|
|
483
|
+
@click.pass_context
|
|
484
|
+
def mcp(ctx: click.Context) -> None:
|
|
485
|
+
"""MCP (Model Context Protocol) commands — ML-powered analysis.
|
|
486
|
+
|
|
487
|
+
These commands use Netdata's MCP server for anomaly detection,
|
|
488
|
+
metric correlations, and advanced querying.
|
|
489
|
+
"""
|
|
490
|
+
pass
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# ------------------------------------------------------------------
|
|
494
|
+
# mcp ping
|
|
495
|
+
# ------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@mcp.command()
|
|
499
|
+
@click.pass_context
|
|
500
|
+
def ping(ctx: click.Context) -> None:
|
|
501
|
+
"""Check if MCP server is reachable."""
|
|
502
|
+
try:
|
|
503
|
+
ok = ctx.obj["mcp_client"].ping()
|
|
504
|
+
if ok:
|
|
505
|
+
msg = "MCP pong ✓"
|
|
506
|
+
if console:
|
|
507
|
+
console.print(f"[bold green]{msg}[/]")
|
|
508
|
+
else:
|
|
509
|
+
print(msg)
|
|
510
|
+
else:
|
|
511
|
+
msg = "MCP server not responding"
|
|
512
|
+
if console:
|
|
513
|
+
console.print(f"[bold red]{msg}[/]")
|
|
514
|
+
else:
|
|
515
|
+
print(msg)
|
|
516
|
+
sys.exit(1)
|
|
517
|
+
except MCPError as exc:
|
|
518
|
+
_handle_error(exc)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
# ------------------------------------------------------------------
|
|
522
|
+
# mcp list-tools
|
|
523
|
+
# ------------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@mcp.command("list-tools")
|
|
527
|
+
@click.pass_context
|
|
528
|
+
def mcp_list_tools(ctx: click.Context) -> None:
|
|
529
|
+
"""List available MCP tools."""
|
|
530
|
+
try:
|
|
531
|
+
tools = ctx.obj["mcp_client"].list_tools()
|
|
532
|
+
if ctx.obj["as_json"]:
|
|
533
|
+
print_json(tools)
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
if HAS_RICH:
|
|
537
|
+
from rich.table import Table
|
|
538
|
+
|
|
539
|
+
t = Table(title=f"MCP Tools ({len(tools)})")
|
|
540
|
+
t.add_column("Name", style="cyan")
|
|
541
|
+
t.add_column("Description", max_width=70)
|
|
542
|
+
for tool in tools:
|
|
543
|
+
t.add_row(tool.get("name", "?"), tool.get("description", "")[:70])
|
|
544
|
+
console.print(t)
|
|
545
|
+
else:
|
|
546
|
+
for tool in tools:
|
|
547
|
+
print(f" {tool.get('name', '?')}: {tool.get('description', '')[:80]}")
|
|
548
|
+
except MCPError as exc:
|
|
549
|
+
_handle_error(exc)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# ------------------------------------------------------------------
|
|
553
|
+
# mcp nodes
|
|
554
|
+
# ------------------------------------------------------------------
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@mcp.command()
|
|
558
|
+
@click.option("--metrics", "-m", multiple=True, help="Filter nodes by metric name.")
|
|
559
|
+
@click.pass_context
|
|
560
|
+
def nodes(ctx: click.Context, metrics: tuple) -> None:
|
|
561
|
+
"""List monitored nodes."""
|
|
562
|
+
try:
|
|
563
|
+
data = ctx.obj["mcp_client"].list_nodes(list(metrics) if metrics else None)
|
|
564
|
+
_output(data, ctx.obj["as_json"], "Nodes")
|
|
565
|
+
except MCPError as exc:
|
|
566
|
+
_handle_error(exc)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ------------------------------------------------------------------
|
|
570
|
+
# mcp nodes-details
|
|
571
|
+
# ------------------------------------------------------------------
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
@mcp.command("nodes-details")
|
|
575
|
+
@click.option("--node", "-n", multiple=True, help="Node ID or hostname.")
|
|
576
|
+
@click.pass_context
|
|
577
|
+
def nodes_details(ctx: click.Context, node: tuple) -> None:
|
|
578
|
+
"""Get detailed node information (hardware, OS, capabilities)."""
|
|
579
|
+
try:
|
|
580
|
+
data = ctx.obj["mcp_client"].get_nodes_details(list(node) if node else None)
|
|
581
|
+
_output(data, ctx.obj["as_json"], "Node Details")
|
|
582
|
+
except MCPError as exc:
|
|
583
|
+
_handle_error(exc)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
# ------------------------------------------------------------------
|
|
587
|
+
# mcp metrics
|
|
588
|
+
# ------------------------------------------------------------------
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@mcp.command()
|
|
592
|
+
@click.argument("query", required=False)
|
|
593
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
594
|
+
@click.pass_context
|
|
595
|
+
def metrics(ctx: click.Context, query: str | None, node: tuple) -> None:
|
|
596
|
+
"""Search available metrics (contexts).
|
|
597
|
+
|
|
598
|
+
\b
|
|
599
|
+
Examples:
|
|
600
|
+
nd mcp metrics # list all
|
|
601
|
+
nd mcp metrics "*nginx*" # pattern search
|
|
602
|
+
nd mcp metrics "q:mysql|redis" # full-text search
|
|
603
|
+
"""
|
|
604
|
+
try:
|
|
605
|
+
q = query or ""
|
|
606
|
+
if q.startswith("q:"):
|
|
607
|
+
q = q[2:]
|
|
608
|
+
data = ctx.obj["mcp_client"].list_metrics(
|
|
609
|
+
q=q,
|
|
610
|
+
nodes=list(node) if node else None,
|
|
611
|
+
)
|
|
612
|
+
_output(data, ctx.obj["as_json"], "Metrics")
|
|
613
|
+
except MCPError as exc:
|
|
614
|
+
_handle_error(exc)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# ------------------------------------------------------------------
|
|
618
|
+
# mcp metrics-details
|
|
619
|
+
# ------------------------------------------------------------------
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@mcp.command("metrics-details")
|
|
623
|
+
@click.argument("metrics_names", nargs=-1, required=True)
|
|
624
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
625
|
+
@click.pass_context
|
|
626
|
+
def metrics_details(ctx: click.Context, metrics_names: tuple, node: tuple) -> None:
|
|
627
|
+
"""Get comprehensive metadata for specific metrics.
|
|
628
|
+
|
|
629
|
+
\b
|
|
630
|
+
Examples:
|
|
631
|
+
nd mcp metrics-details system.cpu system.ram
|
|
632
|
+
nd mcp metrics-details disk.util.* --node ubuntuserver
|
|
633
|
+
"""
|
|
634
|
+
try:
|
|
635
|
+
data = ctx.obj["mcp_client"].get_metrics_details(
|
|
636
|
+
list(metrics_names),
|
|
637
|
+
nodes=list(node) if node else None,
|
|
638
|
+
)
|
|
639
|
+
_output(data, ctx.obj["as_json"], "Metrics Details")
|
|
640
|
+
except MCPError as exc:
|
|
641
|
+
_handle_error(exc)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
# ------------------------------------------------------------------
|
|
645
|
+
# mcp query
|
|
646
|
+
# ------------------------------------------------------------------
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@mcp.command()
|
|
650
|
+
@click.argument("metric")
|
|
651
|
+
@click.option("--dimensions", "-d", multiple=True, help="Specific dimensions to query.")
|
|
652
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
653
|
+
@click.option("--after", "-a", default=-300, show_default=True, help="Start time (seconds from now).")
|
|
654
|
+
@click.option("--before", "-b", default=0, show_default=True, help="End time (0=now).")
|
|
655
|
+
@click.option("--points", "-p", default=20, show_default=True, help="Number of data points.")
|
|
656
|
+
@click.option("--time-group", "-g", default="average", show_default=True, help="Time grouping: average, min, max, sum.")
|
|
657
|
+
@click.option("--group-by", multiple=True, default=["dimension"], help="Group by: dimension, instance, node.")
|
|
658
|
+
@click.option("--aggregation", default="average", show_default=True, help="Aggregation: average, sum, min, max.")
|
|
659
|
+
@click.option("--cardinality-limit", default=10, show_default=True, help="Max items returned.")
|
|
660
|
+
@click.pass_context
|
|
661
|
+
def query(
|
|
662
|
+
ctx: click.Context,
|
|
663
|
+
metric: str,
|
|
664
|
+
dimensions: tuple,
|
|
665
|
+
node: tuple,
|
|
666
|
+
after: int,
|
|
667
|
+
before: int,
|
|
668
|
+
points: int,
|
|
669
|
+
time_group: str,
|
|
670
|
+
group_by: tuple,
|
|
671
|
+
aggregation: str,
|
|
672
|
+
cardinality_limit: int,
|
|
673
|
+
) -> None:
|
|
674
|
+
"""Query time-series metrics with ML anomaly detection.
|
|
675
|
+
|
|
676
|
+
\b
|
|
677
|
+
Examples:
|
|
678
|
+
nd mcp query system.cpu --after -60 --points 10
|
|
679
|
+
nd mcp query system.ram -d used -d cached -d free
|
|
680
|
+
nd mcp query disk.util.* --group-by dimension,node
|
|
681
|
+
nd mcp query net.drops --after -3600 --aggregation sum
|
|
682
|
+
"""
|
|
683
|
+
try:
|
|
684
|
+
data = ctx.obj["mcp_client"].query_metrics(
|
|
685
|
+
metric=metric,
|
|
686
|
+
dimensions=list(dimensions) if dimensions else None,
|
|
687
|
+
nodes=list(node) if node else None,
|
|
688
|
+
after=after,
|
|
689
|
+
before=before,
|
|
690
|
+
points=points,
|
|
691
|
+
time_group=time_group,
|
|
692
|
+
group_by=list(group_by) if group_by else ["dimension"],
|
|
693
|
+
aggregation=aggregation,
|
|
694
|
+
cardinality_limit=cardinality_limit,
|
|
695
|
+
)
|
|
696
|
+
_output(data, ctx.obj["as_json"], f"Query: {metric}")
|
|
697
|
+
except MCPError as exc:
|
|
698
|
+
_handle_error(exc)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
# ------------------------------------------------------------------
|
|
702
|
+
# mcp anomalies
|
|
703
|
+
# ------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
@mcp.command()
|
|
707
|
+
@click.option("--after", "-a", default=-3600, show_default=True, help="Start time (seconds from now).")
|
|
708
|
+
@click.option("--before", "-b", default=0, show_default=True, help="End time (0=now).")
|
|
709
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
710
|
+
@click.pass_context
|
|
711
|
+
def anomalies(ctx: click.Context, after: int, before: int, node: tuple) -> None:
|
|
712
|
+
"""Find metrics detected as anomalous by ML models.
|
|
713
|
+
|
|
714
|
+
\b
|
|
715
|
+
Examples:
|
|
716
|
+
nd mcp anomalies # last hour
|
|
717
|
+
nd mcp anomalies --after -86400 # last 24h
|
|
718
|
+
nd mcp anomalies --after -300 # last 5 min
|
|
719
|
+
"""
|
|
720
|
+
try:
|
|
721
|
+
data = ctx.obj["mcp_client"].find_anomalous_metrics(
|
|
722
|
+
after=after,
|
|
723
|
+
before=before,
|
|
724
|
+
nodes=list(node) if node else None,
|
|
725
|
+
)
|
|
726
|
+
_output(data, ctx.obj["as_json"], "Anomalous Metrics")
|
|
727
|
+
except MCPError as exc:
|
|
728
|
+
_handle_error(exc)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
# ------------------------------------------------------------------
|
|
732
|
+
# mcp correlated
|
|
733
|
+
# ------------------------------------------------------------------
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@mcp.command()
|
|
737
|
+
@click.option("--after", "-a", default=-3600, show_default=True, help="Start time (seconds from now).")
|
|
738
|
+
@click.option("--before", "-b", default=0, show_default=True, help="End time (0=now).")
|
|
739
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
740
|
+
@click.pass_context
|
|
741
|
+
def correlated(ctx: click.Context, after: int, before: int, node: tuple) -> None:
|
|
742
|
+
"""Find metrics that changed significantly during a time period.
|
|
743
|
+
|
|
744
|
+
Compares the specified window against a 4x earlier baseline.
|
|
745
|
+
|
|
746
|
+
\b
|
|
747
|
+
Examples:
|
|
748
|
+
nd mcp correlated # last hour vs previous 4h
|
|
749
|
+
nd mcp correlated --after -300 # last 5min vs previous 20min
|
|
750
|
+
"""
|
|
751
|
+
try:
|
|
752
|
+
data = ctx.obj["mcp_client"].find_correlated_metrics(
|
|
753
|
+
after=after,
|
|
754
|
+
before=before,
|
|
755
|
+
nodes=list(node) if node else None,
|
|
756
|
+
)
|
|
757
|
+
_output(data, ctx.obj["as_json"], "Correlated Metrics")
|
|
758
|
+
except MCPError as exc:
|
|
759
|
+
_handle_error(exc)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
# ------------------------------------------------------------------
|
|
763
|
+
# mcp unstable
|
|
764
|
+
# ------------------------------------------------------------------
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
@mcp.command()
|
|
768
|
+
@click.option("--after", "-a", default=-3600, show_default=True, help="Start time (seconds from now).")
|
|
769
|
+
@click.option("--before", "-b", default=0, show_default=True, help="End time (0=now).")
|
|
770
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
771
|
+
@click.pass_context
|
|
772
|
+
def unstable(ctx: click.Context, after: int, before: int, node: tuple) -> None:
|
|
773
|
+
"""Find metrics with highest variability (coefficient of variation).
|
|
774
|
+
|
|
775
|
+
\b
|
|
776
|
+
Examples:
|
|
777
|
+
nd mcp unstable # last hour
|
|
778
|
+
nd mcp unstable --after -86400 # last 24h
|
|
779
|
+
"""
|
|
780
|
+
try:
|
|
781
|
+
data = ctx.obj["mcp_client"].find_unstable_metrics(
|
|
782
|
+
after=after,
|
|
783
|
+
before=before,
|
|
784
|
+
nodes=list(node) if node else None,
|
|
785
|
+
)
|
|
786
|
+
_output(data, ctx.obj["as_json"], "Unstable Metrics")
|
|
787
|
+
except MCPError as exc:
|
|
788
|
+
_handle_error(exc)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
# ------------------------------------------------------------------
|
|
792
|
+
# mcp mcp-alerts
|
|
793
|
+
# ------------------------------------------------------------------
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@mcp.command("mcp-alerts")
|
|
797
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
798
|
+
@click.pass_context
|
|
799
|
+
def mcp_alerts(ctx: click.Context, node: tuple) -> None:
|
|
800
|
+
"""List active alerts (WARNING/CRITICAL) via MCP."""
|
|
801
|
+
try:
|
|
802
|
+
data = ctx.obj["mcp_client"].list_raised_alerts(
|
|
803
|
+
nodes=list(node) if node else None,
|
|
804
|
+
)
|
|
805
|
+
_output(data, ctx.obj["as_json"], "Active Alerts (MCP)")
|
|
806
|
+
except MCPError as exc:
|
|
807
|
+
_handle_error(exc)
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
# ------------------------------------------------------------------
|
|
811
|
+
# mcp all-alerts
|
|
812
|
+
# ------------------------------------------------------------------
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@mcp.command("all-alerts")
|
|
816
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
817
|
+
@click.pass_context
|
|
818
|
+
def all_alerts(ctx: click.Context, node: tuple) -> None:
|
|
819
|
+
"""List all alerts including cleared and uninitialized."""
|
|
820
|
+
try:
|
|
821
|
+
data = ctx.obj["mcp_client"].list_running_alerts(
|
|
822
|
+
nodes=list(node) if node else None,
|
|
823
|
+
)
|
|
824
|
+
_output(data, ctx.obj["as_json"], "All Alerts (MCP)")
|
|
825
|
+
except MCPError as exc:
|
|
826
|
+
_handle_error(exc)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
# ------------------------------------------------------------------
|
|
830
|
+
# mcp alert-history
|
|
831
|
+
# ------------------------------------------------------------------
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
@mcp.command("alert-history")
|
|
835
|
+
@click.option("--after", "-a", default=-3600, show_default=True, help="Start time (seconds from now).")
|
|
836
|
+
@click.option("--before", "-b", default=0, show_default=True, help="End time (0=now).")
|
|
837
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
838
|
+
@click.pass_context
|
|
839
|
+
def alert_history(ctx: click.Context, after: int, before: int, node: tuple) -> None:
|
|
840
|
+
"""Show alert state transitions (history).
|
|
841
|
+
|
|
842
|
+
\b
|
|
843
|
+
Examples:
|
|
844
|
+
nd mcp alert-history # last hour
|
|
845
|
+
nd mcp alert-history --after -86400 # last 24h
|
|
846
|
+
"""
|
|
847
|
+
try:
|
|
848
|
+
data = ctx.obj["mcp_client"].list_alert_transitions(
|
|
849
|
+
after=after,
|
|
850
|
+
before=before,
|
|
851
|
+
nodes=list(node) if node else None,
|
|
852
|
+
)
|
|
853
|
+
_output(data, ctx.obj["as_json"], "Alert History (MCP)")
|
|
854
|
+
except MCPError as exc:
|
|
855
|
+
_handle_error(exc)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
# ------------------------------------------------------------------
|
|
859
|
+
# mcp functions
|
|
860
|
+
# ------------------------------------------------------------------
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
@mcp.command("mcp-functions")
|
|
864
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
865
|
+
@click.pass_context
|
|
866
|
+
def mcp_functions(ctx: click.Context, node: tuple) -> None:
|
|
867
|
+
"""List available Netdata functions via MCP."""
|
|
868
|
+
try:
|
|
869
|
+
data = ctx.obj["mcp_client"].list_functions(
|
|
870
|
+
nodes=list(node) if node else None,
|
|
871
|
+
)
|
|
872
|
+
_output(data, ctx.obj["as_json"], "Functions (MCP)")
|
|
873
|
+
except MCPError as exc:
|
|
874
|
+
_handle_error(exc)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
# ------------------------------------------------------------------
|
|
878
|
+
# mcp exec
|
|
879
|
+
# ------------------------------------------------------------------
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
@mcp.command("exec")
|
|
883
|
+
@click.argument("function_name")
|
|
884
|
+
@click.option("--node", "-n", multiple=True, help="Filter by node.")
|
|
885
|
+
@click.option("--after", "-a", default=0, help="Start time (seconds from now).")
|
|
886
|
+
@click.option("--before", "-b", default=0, help="End time (0=now).")
|
|
887
|
+
@click.pass_context
|
|
888
|
+
def mcp_exec(ctx: click.Context, function_name: str, node: tuple, after: int, before: int) -> None:
|
|
889
|
+
"""Execute a Netdata function via MCP.
|
|
890
|
+
|
|
891
|
+
\b
|
|
892
|
+
Examples:
|
|
893
|
+
nd mcp exec processes
|
|
894
|
+
nd mcp exec network-connections --node ubuntuserver
|
|
895
|
+
nd mcp exec systemd-journal --after -3600
|
|
896
|
+
"""
|
|
897
|
+
try:
|
|
898
|
+
data = ctx.obj["mcp_client"].execute_function(
|
|
899
|
+
function_name=function_name,
|
|
900
|
+
nodes=list(node) if node else None,
|
|
901
|
+
after=after,
|
|
902
|
+
before=before,
|
|
903
|
+
)
|
|
904
|
+
_output(data, ctx.obj["as_json"], f"Function: {function_name}")
|
|
905
|
+
except MCPError as exc:
|
|
906
|
+
_handle_error(exc)
|
|
907
|
+
|
|
908
|
+
|
|
451
909
|
# ------------------------------------------------------------------
|
|
452
910
|
# Top-level entry
|
|
453
911
|
# ------------------------------------------------------------------
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Netdata MCP client — JSON-RPC 2.0 over HTTP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MCPError(Exception):
|
|
12
|
+
"""Error communicating with Netdata MCP server."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MCPClient:
|
|
16
|
+
"""Thin wrapper around the Netdata MCP server (JSON-RPC 2.0)."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, base_url: str = "http://localhost:19999", timeout: int = 30):
|
|
19
|
+
self.base_url = base_url.rstrip("/")
|
|
20
|
+
self.mcp_url = f"{self.base_url}/mcp"
|
|
21
|
+
self.timeout = timeout
|
|
22
|
+
self._session = requests.Session()
|
|
23
|
+
self._session.headers.update({"Content-Type": "application/json"})
|
|
24
|
+
self._id = 0
|
|
25
|
+
|
|
26
|
+
def _call(self, method: str, params: dict | None = None) -> Any:
|
|
27
|
+
"""Make a JSON-RPC 2.0 call to the MCP server."""
|
|
28
|
+
self._id += 1
|
|
29
|
+
payload: dict[str, Any] = {
|
|
30
|
+
"jsonrpc": "2.0",
|
|
31
|
+
"id": self._id,
|
|
32
|
+
"method": method,
|
|
33
|
+
}
|
|
34
|
+
if params is not None:
|
|
35
|
+
payload["params"] = params
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
resp = self._session.post(
|
|
39
|
+
self.mcp_url,
|
|
40
|
+
json=payload,
|
|
41
|
+
timeout=self.timeout,
|
|
42
|
+
)
|
|
43
|
+
resp.raise_for_status()
|
|
44
|
+
data = resp.json()
|
|
45
|
+
except requests.ConnectionError:
|
|
46
|
+
raise MCPError(
|
|
47
|
+
f"Cannot connect to MCP at {self.mcp_url}. "
|
|
48
|
+
"Is Netdata running and MCP enabled?"
|
|
49
|
+
)
|
|
50
|
+
except requests.Timeout:
|
|
51
|
+
raise MCPError(f"MCP request timed out after {self.timeout}s")
|
|
52
|
+
except requests.HTTPError as exc:
|
|
53
|
+
raise MCPError(f"MCP HTTP error: {exc}")
|
|
54
|
+
except (ValueError, json.JSONDecodeError):
|
|
55
|
+
raise MCPError(f"Invalid JSON from MCP at {self.mcp_url}")
|
|
56
|
+
|
|
57
|
+
if "error" in data:
|
|
58
|
+
err = data["error"]
|
|
59
|
+
msg = err.get("message", "Unknown MCP error")
|
|
60
|
+
raise MCPError(f"MCP error: {msg}")
|
|
61
|
+
|
|
62
|
+
return data.get("result", {})
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# Server info
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def initialize(self) -> dict:
|
|
69
|
+
"""Initialize MCP session and get server capabilities."""
|
|
70
|
+
return self._call("initialize", {
|
|
71
|
+
"protocolVersion": "2024-11-05",
|
|
72
|
+
"capabilities": {},
|
|
73
|
+
"clientInfo": {"name": "netdata-cli", "version": "1.0"},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
def ping(self) -> bool:
|
|
77
|
+
"""Check if MCP server is reachable."""
|
|
78
|
+
try:
|
|
79
|
+
result = self._call("initialize", {
|
|
80
|
+
"protocolVersion": "2024-11-05",
|
|
81
|
+
"capabilities": {},
|
|
82
|
+
"clientInfo": {"name": "netdata-cli", "version": "1.0"},
|
|
83
|
+
})
|
|
84
|
+
return bool(result.get("serverInfo"))
|
|
85
|
+
except MCPError:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
# Tools
|
|
90
|
+
# ------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def list_tools(self) -> list[dict]:
|
|
93
|
+
"""List available MCP tools."""
|
|
94
|
+
result = self._call("tools/list", {})
|
|
95
|
+
return result.get("tools", [])
|
|
96
|
+
|
|
97
|
+
def call_tool(self, name: str, arguments: dict | None = None) -> Any:
|
|
98
|
+
"""Call an MCP tool by name with arguments."""
|
|
99
|
+
result = self._call("tools/call", {
|
|
100
|
+
"name": name,
|
|
101
|
+
"arguments": arguments or {},
|
|
102
|
+
})
|
|
103
|
+
# Extract text content from MCP response
|
|
104
|
+
content = result.get("content", [])
|
|
105
|
+
for item in content:
|
|
106
|
+
if item.get("type") == "text":
|
|
107
|
+
text = item.get("text", "")
|
|
108
|
+
try:
|
|
109
|
+
return json.loads(text)
|
|
110
|
+
except (ValueError, json.JSONDecodeError):
|
|
111
|
+
return text
|
|
112
|
+
return content
|
|
113
|
+
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
# High-level wrappers for each MCP tool
|
|
116
|
+
# ------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
def list_metrics(self, q: str = "", nodes: list[str] | None = None) -> Any:
|
|
119
|
+
"""Search available metrics by pattern or full-text query."""
|
|
120
|
+
args: dict[str, Any] = {}
|
|
121
|
+
if q:
|
|
122
|
+
args["q"] = q
|
|
123
|
+
if nodes:
|
|
124
|
+
args["nodes"] = nodes
|
|
125
|
+
return self.call_tool("list_metrics", args)
|
|
126
|
+
|
|
127
|
+
def get_metrics_details(self, metrics: list[str], nodes: list[str] | None = None) -> Any:
|
|
128
|
+
"""Get comprehensive metadata for specific metrics."""
|
|
129
|
+
args: dict[str, Any] = {"metrics": metrics}
|
|
130
|
+
if nodes:
|
|
131
|
+
args["nodes"] = nodes
|
|
132
|
+
return self.call_tool("get_metrics_details", args)
|
|
133
|
+
|
|
134
|
+
def list_nodes(self, metrics: list[str] | None = None) -> Any:
|
|
135
|
+
"""List all monitored nodes."""
|
|
136
|
+
args: dict[str, Any] = {}
|
|
137
|
+
if metrics:
|
|
138
|
+
args["metrics"] = metrics
|
|
139
|
+
return self.call_tool("list_nodes", args)
|
|
140
|
+
|
|
141
|
+
def get_nodes_details(self, nodes: list[str] | None = None) -> Any:
|
|
142
|
+
"""Get comprehensive node information."""
|
|
143
|
+
args: dict[str, Any] = {}
|
|
144
|
+
if nodes:
|
|
145
|
+
args["nodes"] = nodes
|
|
146
|
+
return self.call_tool("get_nodes_details", args)
|
|
147
|
+
|
|
148
|
+
def list_functions(self, nodes: list[str] | None = None) -> Any:
|
|
149
|
+
"""List available Netdata functions."""
|
|
150
|
+
args: dict[str, Any] = {}
|
|
151
|
+
if nodes:
|
|
152
|
+
args["nodes"] = nodes
|
|
153
|
+
return self.call_tool("list_functions", args)
|
|
154
|
+
|
|
155
|
+
def execute_function(
|
|
156
|
+
self,
|
|
157
|
+
function_name: str,
|
|
158
|
+
nodes: list[str] | None = None,
|
|
159
|
+
after: int = 0,
|
|
160
|
+
before: int = 0,
|
|
161
|
+
) -> Any:
|
|
162
|
+
"""Execute a Netdata function (processes, connections, etc)."""
|
|
163
|
+
args: dict[str, Any] = {"function": function_name}
|
|
164
|
+
if nodes:
|
|
165
|
+
args["nodes"] = nodes
|
|
166
|
+
if after:
|
|
167
|
+
args["after"] = after
|
|
168
|
+
if before:
|
|
169
|
+
args["before"] = before
|
|
170
|
+
return self.call_tool("execute_function", args)
|
|
171
|
+
|
|
172
|
+
def query_metrics(
|
|
173
|
+
self,
|
|
174
|
+
metric: str,
|
|
175
|
+
dimensions: list[str] | None = None,
|
|
176
|
+
nodes: list[str] | None = None,
|
|
177
|
+
after: int = -300,
|
|
178
|
+
before: int = 0,
|
|
179
|
+
points: int = 20,
|
|
180
|
+
time_group: str = "average",
|
|
181
|
+
group_by: list[str] | None = None,
|
|
182
|
+
aggregation: str = "average",
|
|
183
|
+
cardinality_limit: int = 10,
|
|
184
|
+
) -> Any:
|
|
185
|
+
"""Query time-series metrics data with aggregation."""
|
|
186
|
+
args: dict[str, Any] = {
|
|
187
|
+
"metric": metric,
|
|
188
|
+
"after": after,
|
|
189
|
+
"before": before,
|
|
190
|
+
"points": points,
|
|
191
|
+
"time_group": time_group,
|
|
192
|
+
"aggregation": aggregation,
|
|
193
|
+
"cardinality_limit": cardinality_limit,
|
|
194
|
+
}
|
|
195
|
+
if dimensions:
|
|
196
|
+
args["dimensions"] = dimensions
|
|
197
|
+
if nodes:
|
|
198
|
+
args["nodes"] = nodes
|
|
199
|
+
if group_by:
|
|
200
|
+
args["group_by"] = group_by
|
|
201
|
+
return self.call_tool("query_metrics", args)
|
|
202
|
+
|
|
203
|
+
def find_correlated_metrics(
|
|
204
|
+
self,
|
|
205
|
+
after: int = -3600,
|
|
206
|
+
before: int = 0,
|
|
207
|
+
nodes: list[str] | None = None,
|
|
208
|
+
) -> Any:
|
|
209
|
+
"""Find metrics that changed significantly during a time period."""
|
|
210
|
+
args: dict[str, Any] = {"after": after, "before": before}
|
|
211
|
+
if nodes:
|
|
212
|
+
args["nodes"] = nodes
|
|
213
|
+
return self.call_tool("find_correlated_metrics", args)
|
|
214
|
+
|
|
215
|
+
def find_anomalous_metrics(
|
|
216
|
+
self,
|
|
217
|
+
after: int = -3600,
|
|
218
|
+
before: int = 0,
|
|
219
|
+
nodes: list[str] | None = None,
|
|
220
|
+
) -> Any:
|
|
221
|
+
"""Find metrics detected as anomalous by ML models."""
|
|
222
|
+
args: dict[str, Any] = {"after": after, "before": before}
|
|
223
|
+
if nodes:
|
|
224
|
+
args["nodes"] = nodes
|
|
225
|
+
return self.call_tool("find_anomalous_metrics", args)
|
|
226
|
+
|
|
227
|
+
def find_unstable_metrics(
|
|
228
|
+
self,
|
|
229
|
+
after: int = -3600,
|
|
230
|
+
before: int = 0,
|
|
231
|
+
nodes: list[str] | None = None,
|
|
232
|
+
) -> Any:
|
|
233
|
+
"""Find metrics with highest variability."""
|
|
234
|
+
args: dict[str, Any] = {"after": after, "before": before}
|
|
235
|
+
if nodes:
|
|
236
|
+
args["nodes"] = nodes
|
|
237
|
+
return self.call_tool("find_unstable_metrics", args)
|
|
238
|
+
|
|
239
|
+
def list_raised_alerts(self, nodes: list[str] | None = None) -> Any:
|
|
240
|
+
"""List currently active alerts (WARNING/CRITICAL)."""
|
|
241
|
+
args: dict[str, Any] = {}
|
|
242
|
+
if nodes:
|
|
243
|
+
args["nodes"] = nodes
|
|
244
|
+
return self.call_tool("list_raised_alerts", args)
|
|
245
|
+
|
|
246
|
+
def list_running_alerts(self, nodes: list[str] | None = None) -> Any:
|
|
247
|
+
"""List all alerts including cleared and uninitialized."""
|
|
248
|
+
args: dict[str, Any] = {}
|
|
249
|
+
if nodes:
|
|
250
|
+
args["nodes"] = nodes
|
|
251
|
+
return self.call_tool("list_running_alerts", args)
|
|
252
|
+
|
|
253
|
+
def list_alert_transitions(
|
|
254
|
+
self,
|
|
255
|
+
after: int = -3600,
|
|
256
|
+
before: int = 0,
|
|
257
|
+
nodes: list[str] | None = None,
|
|
258
|
+
) -> Any:
|
|
259
|
+
"""List recent alert state transitions."""
|
|
260
|
+
args: dict[str, Any] = {"after": after, "before": before}
|
|
261
|
+
if nodes:
|
|
262
|
+
args["nodes"] = nodes
|
|
263
|
+
return self.call_tool("list_alert_transitions", args)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netdata-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A powerful CLI for the Netdata Agent REST API — query metrics, charts, alarms, and functions from the terminal.
|
|
5
|
-
Author-email: Sosi <
|
|
5
|
+
Author-email: Sosi <albertotplaza@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/sosiman/netdata-cli
|
|
8
8
|
Project-URL: Repository, https://github.com/sosiman/netdata-cli
|
|
@@ -35,6 +35,10 @@ Dynamic: license-file
|
|
|
35
35
|
|
|
36
36
|
# netdata-cli
|
|
37
37
|
|
|
38
|
+
[](https://pypi.org/project/netdata-cli/)
|
|
39
|
+
[](https://pypi.org/project/netdata-cli/)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
|
|
38
42
|
A powerful CLI for the [Netdata](https://www.netdata.cloud/) Agent REST API.
|
|
39
43
|
|
|
40
44
|
Query metrics, charts, alarms, functions, and agent info — all from the terminal.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
netdata_cli/__init__.py,sha256=98BjFSTP9kfsojaOeT-kQNw65tfi-uiWolU549sPMpM,92
|
|
2
|
+
netdata_cli/cli.py,sha256=PhvxMkZo04UzvBCxHm4Pq1REyaQ-w4YAV_OJU2xhyzo,30264
|
|
3
|
+
netdata_cli/client.py,sha256=T4DuVOP_CKvzpF1FiXbG1OvYvqhgO29tOqHykFCp48A,7862
|
|
4
|
+
netdata_cli/formatters.py,sha256=rokGtvuwK1MsR2cMOEjza2ggL4YvqSOLnvCgCwiPm_w,11644
|
|
5
|
+
netdata_cli/mcp_client.py,sha256=XNk2aKHAfnBHRjjnewuy4dxve7WfGco2YElpUoXxxXA,9076
|
|
6
|
+
netdata_cli-0.2.0.dist-info/licenses/LICENSE,sha256=Tq3uYZqMEv-MzVnsiXx-L99tt5Css4jGrocf6KpFdfM,1061
|
|
7
|
+
netdata_cli-0.2.0.dist-info/METADATA,sha256=UhjQzTgLYHywoA7T3ipGMb6P4C9_iuHDRLFghoikzh8,5977
|
|
8
|
+
netdata_cli-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
9
|
+
netdata_cli-0.2.0.dist-info/entry_points.txt,sha256=15fw-SmjPRO9-1z1h0g8I2pWuyNXZDm_MmHkiDklELw,44
|
|
10
|
+
netdata_cli-0.2.0.dist-info/top_level.txt,sha256=TC9SwX7mrl2Q1QDRKMs045wkb5Bg3MH1y_iAiL1jhDc,12
|
|
11
|
+
netdata_cli-0.2.0.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
netdata_cli/__init__.py,sha256=rIbZbYbNCaZoSgcA-IOtDHW52a1eZSUD-2ONKIaMVsM,92
|
|
2
|
-
netdata_cli/cli.py,sha256=ZCfaEzf119wYfzv5ZM2kM4hjrZMC6BvpMInU8PBpszQ,14570
|
|
3
|
-
netdata_cli/client.py,sha256=T4DuVOP_CKvzpF1FiXbG1OvYvqhgO29tOqHykFCp48A,7862
|
|
4
|
-
netdata_cli/formatters.py,sha256=rokGtvuwK1MsR2cMOEjza2ggL4YvqSOLnvCgCwiPm_w,11644
|
|
5
|
-
netdata_cli-0.1.0.dist-info/licenses/LICENSE,sha256=Tq3uYZqMEv-MzVnsiXx-L99tt5Css4jGrocf6KpFdfM,1061
|
|
6
|
-
netdata_cli-0.1.0.dist-info/METADATA,sha256=QoL8zXtGoXNJFhlbCjhuhNmazh3m_u_fQ8dD9S4Fs7A,5686
|
|
7
|
-
netdata_cli-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
-
netdata_cli-0.1.0.dist-info/entry_points.txt,sha256=15fw-SmjPRO9-1z1h0g8I2pWuyNXZDm_MmHkiDklELw,44
|
|
9
|
-
netdata_cli-0.1.0.dist-info/top_level.txt,sha256=TC9SwX7mrl2Q1QDRKMs045wkb5Bg3MH1y_iAiL1jhDc,12
|
|
10
|
-
netdata_cli-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|