linode-cli 5.66.0__tar.gz → 5.68.0__tar.gz
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.
- {linode_cli-5.66.0/linode_cli.egg-info → linode_cli-5.68.0}/PKG-INFO +1 -1
- {linode_cli-5.66.0 → linode_cli-5.68.0/linode_cli.egg-info}/PKG-INFO +1 -1
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/SOURCES.txt +1 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/cli.py +48 -0
- linode_cli-5.68.0/linodecli/data-3 +0 -0
- linode_cli-5.68.0/linodecli/plugins/monitor-api.py +646 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/version.py +1 -1
- linode_cli-5.66.0/linodecli/data-3 +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/LICENSE +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/MANIFEST.in +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/README.md +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/dependency_links.txt +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/entry_points.txt +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/requires.txt +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/top_level.txt +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/__init__.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/__main__.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/api_request.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/arg_helpers.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/__init__.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/operation.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/parsing.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/request.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/response.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/util.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/completion.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/configuration/__init__.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/configuration/auth.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/configuration/config.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/configuration/helpers.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/exit_codes.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/help_formatter.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/help_pages.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/helpers.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/oauth-landing-page.html +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/output/__init__.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/output/helpers.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/output/output_handler.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/overrides.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/__init__.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/firewall-editor.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/get-kubeconfig.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/image-upload.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/metadata.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/__init__.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/buckets.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/config.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/helpers.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/list.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/objects.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/website.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/plugins.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/region-table.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/ssh.py +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/pyproject.toml +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/setup.cfg +0 -0
- {linode_cli-5.66.0 → linode_cli-5.68.0}/setup.py +0 -0
|
@@ -41,6 +41,7 @@ linodecli/plugins/firewall-editor.py
|
|
|
41
41
|
linodecli/plugins/get-kubeconfig.py
|
|
42
42
|
linodecli/plugins/image-upload.py
|
|
43
43
|
linodecli/plugins/metadata.py
|
|
44
|
+
linodecli/plugins/monitor-api.py
|
|
44
45
|
linodecli/plugins/plugins.py
|
|
45
46
|
linodecli/plugins/region-table.py
|
|
46
47
|
linodecli/plugins/ssh.py
|
|
@@ -305,8 +305,56 @@ class CLI: # pylint: disable=too-many-instance-attributes
|
|
|
305
305
|
with CLI._get_spec_file_reader(spec_location) as f:
|
|
306
306
|
parsed = CLI._parse_spec_file(f)
|
|
307
307
|
|
|
308
|
+
CLI._normalize_content_parameters(parsed)
|
|
309
|
+
|
|
308
310
|
return OpenAPI(parsed)
|
|
309
311
|
|
|
312
|
+
@staticmethod
|
|
313
|
+
def _normalize_content_parameters(parsed: Dict[str, Any]):
|
|
314
|
+
"""
|
|
315
|
+
The openapi3 library does not support the OpenAPI 3.0 ``content``
|
|
316
|
+
form for Parameter objects. This method converts any such
|
|
317
|
+
parameters (in components and inline on paths/operations) to use
|
|
318
|
+
a top-level ``schema`` field so they can be parsed normally.
|
|
319
|
+
|
|
320
|
+
:param parsed: The raw spec dict to mutate in-place.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
def _fix_param(param):
|
|
324
|
+
if not isinstance(param, dict):
|
|
325
|
+
return
|
|
326
|
+
if "content" in param and "schema" not in param:
|
|
327
|
+
content = param.pop("content")
|
|
328
|
+
for media_obj in content.values():
|
|
329
|
+
if isinstance(media_obj, dict) and "schema" in media_obj:
|
|
330
|
+
param["schema"] = media_obj["schema"]
|
|
331
|
+
break
|
|
332
|
+
|
|
333
|
+
for param in (
|
|
334
|
+
parsed.get("components", {}).get("parameters", {}).values()
|
|
335
|
+
):
|
|
336
|
+
_fix_param(param)
|
|
337
|
+
|
|
338
|
+
for path_item in parsed.get("paths", {}).values():
|
|
339
|
+
if not isinstance(path_item, dict):
|
|
340
|
+
continue
|
|
341
|
+
for p in path_item.get("parameters", []):
|
|
342
|
+
_fix_param(p)
|
|
343
|
+
for method in (
|
|
344
|
+
"get",
|
|
345
|
+
"put",
|
|
346
|
+
"post",
|
|
347
|
+
"delete",
|
|
348
|
+
"options",
|
|
349
|
+
"head",
|
|
350
|
+
"patch",
|
|
351
|
+
"trace",
|
|
352
|
+
):
|
|
353
|
+
operation = path_item.get(method)
|
|
354
|
+
if isinstance(operation, dict):
|
|
355
|
+
for p in operation.get("parameters", []):
|
|
356
|
+
_fix_param(p)
|
|
357
|
+
|
|
310
358
|
@staticmethod
|
|
311
359
|
@contextlib.contextmanager
|
|
312
360
|
def _get_spec_file_reader(
|
|
Binary file
|
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This plugin provides access to the Linode Monitor API.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
get-metrics: Query metrics from the monitoring service for various services.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
linode-cli monitor-api get-metrics <service> [options]
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from argparse import ArgumentParser
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import List, Optional
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
|
|
20
|
+
from linodecli.exit_codes import ExitCodes
|
|
21
|
+
from linodecli.help_formatter import SortingHelpFormatter
|
|
22
|
+
from linodecli.helpers import register_debug_arg
|
|
23
|
+
|
|
24
|
+
PLUGIN_BASE = "linode-cli monitor-api"
|
|
25
|
+
|
|
26
|
+
# API Configuration
|
|
27
|
+
API_BASE_URL = "https://monitor-api.linode.com"
|
|
28
|
+
API_VERSION = "v2"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_auth_token():
|
|
32
|
+
"""
|
|
33
|
+
Get authentication token from JWE_TOKEN environment variable
|
|
34
|
+
Raises an error if the environment variable is not set
|
|
35
|
+
"""
|
|
36
|
+
token = os.getenv("JWE_TOKEN")
|
|
37
|
+
if not token:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
"JWE_TOKEN environment variable is required but not set. "
|
|
40
|
+
"Please set it with: export JWE_TOKEN='your_token_here'"
|
|
41
|
+
)
|
|
42
|
+
return token
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Aggregate functions
|
|
46
|
+
AGGREGATE_FUNCTIONS = ["sum", "avg", "max", "min", "count"]
|
|
47
|
+
|
|
48
|
+
# Allowed time units
|
|
49
|
+
ALLOWED_TIME_UNITS = ["min", "hr", "days"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class MetricsConfig:
|
|
54
|
+
"""Configuration for metrics request"""
|
|
55
|
+
|
|
56
|
+
service_name: str
|
|
57
|
+
entity_ids: List
|
|
58
|
+
duration: Optional[int]
|
|
59
|
+
duration_unit: Optional[str]
|
|
60
|
+
start_time: Optional[str]
|
|
61
|
+
end_time: Optional[str]
|
|
62
|
+
metrics: List[str]
|
|
63
|
+
granularity: Optional[int]
|
|
64
|
+
granularity_unit: Optional[str]
|
|
65
|
+
filters: Optional[List[str]] = None
|
|
66
|
+
group_by: Optional[List[str]] = None
|
|
67
|
+
entity_region: Optional[str] = None
|
|
68
|
+
associated_entity_region: Optional[str] = None
|
|
69
|
+
token: Optional[str] = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def make_api_request(
|
|
73
|
+
service_name: str,
|
|
74
|
+
endpoint: str,
|
|
75
|
+
method: str = "POST",
|
|
76
|
+
data: Optional[dict] = None,
|
|
77
|
+
token: Optional[str] = None,
|
|
78
|
+
) -> tuple:
|
|
79
|
+
"""
|
|
80
|
+
Make an API request to the monitoring service
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
service_name: The service name (nodebalancer, netloadbalancer, etc.)
|
|
84
|
+
endpoint: The API endpoint to call
|
|
85
|
+
method: HTTP method
|
|
86
|
+
data: Request payload for POST requests
|
|
87
|
+
token: Bearer token for authentication
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Tuple of (status_code, response_data)
|
|
91
|
+
"""
|
|
92
|
+
url = f"{API_BASE_URL}/{API_VERSION}/monitor/services/{service_name}/{endpoint}"
|
|
93
|
+
|
|
94
|
+
headers = {
|
|
95
|
+
"Authorization": f"Bearer {token or get_auth_token()}",
|
|
96
|
+
"Authentication-type": "jwe",
|
|
97
|
+
"Pragma": "akamai-x-get-extracted-values",
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
if method.upper() == "POST":
|
|
103
|
+
response = requests.post(
|
|
104
|
+
url, headers=headers, json=data, timeout=30, verify=True
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
response = requests.get(
|
|
108
|
+
url, headers=headers, timeout=30, verify=True
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Try to parse JSON response, fallback to text if it fails
|
|
112
|
+
try:
|
|
113
|
+
response_data = response.json() if response.content else {}
|
|
114
|
+
except json.JSONDecodeError:
|
|
115
|
+
response_data = {"error": f"Non-JSON response: {response.text}"}
|
|
116
|
+
|
|
117
|
+
return response.status_code, response_data
|
|
118
|
+
except requests.exceptions.RequestException as e:
|
|
119
|
+
print(f"Error making API request: {e}", file=sys.stderr)
|
|
120
|
+
return 500, {"error": str(e)}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def parse_metrics(metrics: List[str]) -> List[dict]:
|
|
124
|
+
"""Parse metrics with mandatory aggregate functions"""
|
|
125
|
+
parsed_metrics = []
|
|
126
|
+
for metric in metrics:
|
|
127
|
+
if ":" in metric:
|
|
128
|
+
metric_name, agg_func = metric.split(":", 1)
|
|
129
|
+
metric_name = metric_name.strip()
|
|
130
|
+
agg_func = agg_func.strip().lower()
|
|
131
|
+
if not metric_name:
|
|
132
|
+
print(
|
|
133
|
+
f"Metric name is required for metric '{metric}'",
|
|
134
|
+
file=sys.stderr,
|
|
135
|
+
)
|
|
136
|
+
print(
|
|
137
|
+
f"Format: 'metric_name:function' where function is one of: "
|
|
138
|
+
f"{', '.join(AGGREGATE_FUNCTIONS)}",
|
|
139
|
+
file=sys.stderr,
|
|
140
|
+
)
|
|
141
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
142
|
+
if agg_func not in AGGREGATE_FUNCTIONS:
|
|
143
|
+
print(
|
|
144
|
+
f"Invalid aggregate function '{agg_func}' for metric '{metric}'",
|
|
145
|
+
file=sys.stderr,
|
|
146
|
+
)
|
|
147
|
+
print(
|
|
148
|
+
f"Aggregate function must be one of: "
|
|
149
|
+
f"{', '.join(AGGREGATE_FUNCTIONS)}",
|
|
150
|
+
file=sys.stderr,
|
|
151
|
+
)
|
|
152
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
153
|
+
parsed_metrics.append(
|
|
154
|
+
{"aggregate_function": agg_func, "name": metric_name}
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
print(
|
|
158
|
+
f"Aggregate function required for metric '{metric}'",
|
|
159
|
+
file=sys.stderr,
|
|
160
|
+
)
|
|
161
|
+
print(
|
|
162
|
+
f"Format: 'metric_name:function' where function is one of: "
|
|
163
|
+
f"{', '.join(AGGREGATE_FUNCTIONS)}",
|
|
164
|
+
file=sys.stderr,
|
|
165
|
+
)
|
|
166
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
167
|
+
return parsed_metrics
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def build_payload( # pylint: disable=too-many-branches
|
|
171
|
+
config: MetricsConfig,
|
|
172
|
+
) -> dict:
|
|
173
|
+
"""Build API request payload from configuration"""
|
|
174
|
+
parsed_metrics = parse_metrics(config.metrics)
|
|
175
|
+
payload = {"metrics": parsed_metrics}
|
|
176
|
+
|
|
177
|
+
if config.entity_ids:
|
|
178
|
+
payload["entity_ids"] = config.entity_ids
|
|
179
|
+
if config.group_by:
|
|
180
|
+
payload["group_by"] = config.group_by
|
|
181
|
+
if config.entity_region:
|
|
182
|
+
payload["entity_region"] = config.entity_region
|
|
183
|
+
if config.associated_entity_region:
|
|
184
|
+
payload["associated_entity_region"] = config.associated_entity_region
|
|
185
|
+
|
|
186
|
+
# Add time duration - either relative or absolute
|
|
187
|
+
if config.start_time and config.end_time:
|
|
188
|
+
payload["absolute_time_duration"] = {
|
|
189
|
+
"start": config.start_time,
|
|
190
|
+
"end": config.end_time,
|
|
191
|
+
}
|
|
192
|
+
elif config.duration is not None and config.duration_unit is not None:
|
|
193
|
+
payload["relative_time_duration"] = {
|
|
194
|
+
"unit": config.duration_unit,
|
|
195
|
+
"value": config.duration,
|
|
196
|
+
}
|
|
197
|
+
else:
|
|
198
|
+
print(
|
|
199
|
+
"Either (--duration and --duration-unit) or "
|
|
200
|
+
"(--start-time and --end-time) must be provided",
|
|
201
|
+
file=sys.stderr,
|
|
202
|
+
)
|
|
203
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
204
|
+
|
|
205
|
+
# Add time_granularity if provided
|
|
206
|
+
if config.granularity is not None and config.granularity_unit is not None:
|
|
207
|
+
payload["time_granularity"] = {
|
|
208
|
+
"unit": config.granularity_unit,
|
|
209
|
+
"value": config.granularity,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# Add filters if provided
|
|
213
|
+
if config.filters:
|
|
214
|
+
parsed_filters = []
|
|
215
|
+
for filter_str in config.filters:
|
|
216
|
+
parts = filter_str.split(":", 2)
|
|
217
|
+
if len(parts) != 3:
|
|
218
|
+
print(
|
|
219
|
+
f"Invalid filter format: '{filter_str}'. "
|
|
220
|
+
"Expected format: 'dimension:operator:value'",
|
|
221
|
+
file=sys.stderr,
|
|
222
|
+
)
|
|
223
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
224
|
+
|
|
225
|
+
dimension_label, operator, value = parts
|
|
226
|
+
operator = operator.strip()
|
|
227
|
+
dimension_label = dimension_label.strip()
|
|
228
|
+
if operator == "in":
|
|
229
|
+
# Expand 'in' into multiple 'eq' filters (one per value)
|
|
230
|
+
for v in value.split(","):
|
|
231
|
+
parsed_filters.append(
|
|
232
|
+
{
|
|
233
|
+
"dimension_label": dimension_label,
|
|
234
|
+
"operator": "eq",
|
|
235
|
+
"value": v.strip(),
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
parsed_filters.append(
|
|
240
|
+
{
|
|
241
|
+
"dimension_label": dimension_label,
|
|
242
|
+
"operator": operator,
|
|
243
|
+
"value": value.strip(),
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
payload["filters"] = parsed_filters
|
|
247
|
+
|
|
248
|
+
return payload
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_metrics(config: MetricsConfig, debug: bool = False):
|
|
252
|
+
"""Get metrics for specified service entities"""
|
|
253
|
+
payload = build_payload(config)
|
|
254
|
+
|
|
255
|
+
if config.entity_ids:
|
|
256
|
+
print(
|
|
257
|
+
f"Fetching metrics for {config.service_name} entities: {config.entity_ids}"
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
print(f"Fetching metrics for {config.service_name} (all entities)")
|
|
261
|
+
|
|
262
|
+
if debug:
|
|
263
|
+
print(
|
|
264
|
+
f"Request payload: {json.dumps(payload, indent=2)}",
|
|
265
|
+
file=sys.stderr,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
status, response = make_api_request(
|
|
270
|
+
config.service_name, "metrics", "POST", payload, config.token
|
|
271
|
+
)
|
|
272
|
+
except ValueError as e:
|
|
273
|
+
print(f"Authentication Error: {e}", file=sys.stderr)
|
|
274
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
275
|
+
|
|
276
|
+
if status != 200:
|
|
277
|
+
print(f"API request failed with status {status}", file=sys.stderr)
|
|
278
|
+
print(
|
|
279
|
+
f"Error response: {json.dumps(response, indent=2)}", file=sys.stderr
|
|
280
|
+
)
|
|
281
|
+
print("Exiting due to API error...", file=sys.stderr)
|
|
282
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
283
|
+
|
|
284
|
+
print_metrics_response(response)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def print_metrics_response(data: dict):
|
|
288
|
+
"""
|
|
289
|
+
Print metrics data as formatted JSON
|
|
290
|
+
"""
|
|
291
|
+
if not data:
|
|
292
|
+
print("No response received")
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
if data.get("status") == "success":
|
|
296
|
+
metrics_data = data.get("data", {}).get("result", [])
|
|
297
|
+
stats = data.get("stats", {})
|
|
298
|
+
|
|
299
|
+
if not metrics_data:
|
|
300
|
+
print("No metrics data found for the specified parameters")
|
|
301
|
+
print(f"Execution time: {stats.get('executionTimeMsec', 0)}ms")
|
|
302
|
+
print(f"Series fetched: {stats.get('seriesFetched', 0)}")
|
|
303
|
+
else:
|
|
304
|
+
print(f"Series fetched: {stats.get('seriesFetched', 0)}")
|
|
305
|
+
print("\nMetrics Data:")
|
|
306
|
+
print(json.dumps(data.get("data"), indent=2))
|
|
307
|
+
else:
|
|
308
|
+
print(f"API returned error status: {data.get('status', 'unknown')}")
|
|
309
|
+
if "error" in data:
|
|
310
|
+
print(f"Error: {data['error']}")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def print_help(parser: ArgumentParser):
|
|
314
|
+
"""
|
|
315
|
+
Print help information
|
|
316
|
+
"""
|
|
317
|
+
parser.print_help()
|
|
318
|
+
|
|
319
|
+
print("\nAuthentication:")
|
|
320
|
+
print(
|
|
321
|
+
" This plugin requires a JWE token set via the JWE_TOKEN environment variable."
|
|
322
|
+
)
|
|
323
|
+
print(" To generate a JWE token:")
|
|
324
|
+
print(
|
|
325
|
+
" POST https://api.linode.com/v4beta/monitor/services/{service}/token"
|
|
326
|
+
)
|
|
327
|
+
print(" Authorization: Bearer <your-PAT-token>")
|
|
328
|
+
print(' Body: {"entity_ids": [<entity_id>]}')
|
|
329
|
+
print(
|
|
330
|
+
" Replace {service} with the service name (e.g. dbaas, nodebalancer, netloadbalancer)"
|
|
331
|
+
)
|
|
332
|
+
print(" and <entity_id> with the ID of the entity to monitor.")
|
|
333
|
+
print(" See: https://www.linode.com/docs/api/monitor/")
|
|
334
|
+
print(" Then: export JWE_TOKEN='<token>'")
|
|
335
|
+
|
|
336
|
+
print("\nExamples:")
|
|
337
|
+
print(" # Get metrics with relative time duration")
|
|
338
|
+
print(
|
|
339
|
+
" linode-cli monitor-api get-metrics dbaas --entity-ids 123 --duration 15 "
|
|
340
|
+
"--duration-unit min --metrics cpu_usage:avg"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
print(
|
|
344
|
+
"\n # Get metrics for all entities "
|
|
345
|
+
"(only allowed for objectstorage service)"
|
|
346
|
+
)
|
|
347
|
+
print(
|
|
348
|
+
" linode-cli monitor-api get-metrics objectstorage --duration 15 "
|
|
349
|
+
"--duration-unit min --metrics obj_requests_num:avg "
|
|
350
|
+
"--entity-region us-east-1"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
print("\n # Get metrics with absolute time duration")
|
|
354
|
+
print(
|
|
355
|
+
" linode-cli monitor-api get-metrics dbaas --entity-ids 123 "
|
|
356
|
+
"--start-time 2024-10-10T00:00:01Z --end-time 2024-10-10T23:59:59Z "
|
|
357
|
+
"--metrics cpu_usage:avg,memory_usage:sum"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
print("\n # Get metrics with filters")
|
|
361
|
+
print(
|
|
362
|
+
" linode-cli monitor-api get-metrics dbaas --entity-ids 123 --duration 15 "
|
|
363
|
+
"--duration-unit min --metrics cpu_usage:avg "
|
|
364
|
+
"--filters 'node_type:in:primary,secondary'"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
print("\n # Get metrics with multiple filters")
|
|
368
|
+
print(
|
|
369
|
+
" linode-cli monitor-api get-metrics dbaas --entity-ids 123 --duration 15 "
|
|
370
|
+
"--duration-unit min --metrics cpu_usage:avg "
|
|
371
|
+
"--filters 'node_type:in:primary,secondary'"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
print("\n # Get metrics with granularity")
|
|
375
|
+
print(
|
|
376
|
+
" linode-cli monitor-api get-metrics netloadbalancer --entity-ids 123 "
|
|
377
|
+
"--duration 1 --duration-unit hr --metrics nlb_ingress_traffic:sum "
|
|
378
|
+
"--granularity 10 --granularity-unit min"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
print("\n # Get metrics with entity region (required ObjectStorage)")
|
|
382
|
+
print(
|
|
383
|
+
" linode-cli monitor-api get-metrics objectstorage --entity-region us-east-1 "
|
|
384
|
+
"--duration 15 --duration-unit min --metrics obj_requests_num:sum"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
print(
|
|
388
|
+
"\n # Get metrics with associated entity region "
|
|
389
|
+
"(mandatory for cloud firewall service)"
|
|
390
|
+
)
|
|
391
|
+
print(
|
|
392
|
+
" linode-cli monitor-api get-metrics firewall --entity-region us-east-1 "
|
|
393
|
+
"--associated-entity-region us-west-1 --duration 15 "
|
|
394
|
+
"--duration-unit min --metrics fw_active_connections:sum"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def get_metrics_parser():
|
|
399
|
+
"""
|
|
400
|
+
Build argument parser for metrics plugin
|
|
401
|
+
"""
|
|
402
|
+
parser = ArgumentParser(
|
|
403
|
+
PLUGIN_BASE, add_help=False, formatter_class=SortingHelpFormatter
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
register_debug_arg(parser)
|
|
407
|
+
|
|
408
|
+
# Command as first positional argument
|
|
409
|
+
parser.add_argument(
|
|
410
|
+
"command",
|
|
411
|
+
nargs="?",
|
|
412
|
+
help="Command to execute (get-metrics)",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Service name as second positional argument
|
|
416
|
+
parser.add_argument(
|
|
417
|
+
"service",
|
|
418
|
+
nargs="?",
|
|
419
|
+
help="Service name (Dbaas, Nodebalancer, NetLoadBalancer, Linode, "
|
|
420
|
+
"Firewall, ObjectStorage, Blockstorage, LKE)",
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Optional arguments for get-metrics functionality
|
|
424
|
+
parser.add_argument(
|
|
425
|
+
"--entity-ids",
|
|
426
|
+
help="Comma-separated list of entity IDs "
|
|
427
|
+
"(can be integers or strings depending on service type)",
|
|
428
|
+
required=False,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
parser.add_argument(
|
|
432
|
+
"--entity-region",
|
|
433
|
+
help="Region for entities (required for services like ObjectStorage)",
|
|
434
|
+
required=False,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
parser.add_argument(
|
|
438
|
+
"--associated-entity-region",
|
|
439
|
+
help="Associated region for entities (Required for cloud firewall service)",
|
|
440
|
+
required=False,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Time duration options - either relative or absolute
|
|
444
|
+
parser.add_argument(
|
|
445
|
+
"--duration",
|
|
446
|
+
type=int,
|
|
447
|
+
help="Relative time duration to look back (e.g., 15 for 15 minutes)",
|
|
448
|
+
)
|
|
449
|
+
parser.add_argument(
|
|
450
|
+
"--duration-unit", help="Unit for relative duration: min, hr, days"
|
|
451
|
+
)
|
|
452
|
+
parser.add_argument(
|
|
453
|
+
"--start-time",
|
|
454
|
+
help="Absolute start time (ISO format: 2024-10-10T00:00:01Z)",
|
|
455
|
+
)
|
|
456
|
+
parser.add_argument(
|
|
457
|
+
"--end-time",
|
|
458
|
+
help="Absolute end time (ISO format: 2024-10-10T23:59:59Z)",
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
parser.add_argument(
|
|
462
|
+
"--metrics",
|
|
463
|
+
help="Comma-separated list of metrics with mandatory aggregate functions. "
|
|
464
|
+
"Format: 'metric1:function1,metric2:function2' "
|
|
465
|
+
"(e.g., 'cpu_usage:avg,memory_usage:sum')",
|
|
466
|
+
)
|
|
467
|
+
parser.add_argument(
|
|
468
|
+
"--granularity",
|
|
469
|
+
type=int,
|
|
470
|
+
help="Time granularity for data points (optional)",
|
|
471
|
+
)
|
|
472
|
+
parser.add_argument(
|
|
473
|
+
"--granularity-unit",
|
|
474
|
+
help="Unit for granularity: min, hr, days (optional)",
|
|
475
|
+
)
|
|
476
|
+
parser.add_argument(
|
|
477
|
+
"--filters",
|
|
478
|
+
help="Optional filters in format 'dimension:operator:value'. "
|
|
479
|
+
"Multiple filters separated by semicolons. "
|
|
480
|
+
"Operators: eq, neq, startswith, endswith, in. "
|
|
481
|
+
"For 'in' operator, separate values with commas. "
|
|
482
|
+
"Example: 'node_type:in:primary,secondary'",
|
|
483
|
+
)
|
|
484
|
+
parser.add_argument(
|
|
485
|
+
"--group_by",
|
|
486
|
+
help="Comma-separated list of fields to group by (default: entity_id)",
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
return parser
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def validate_arguments(parsed):
|
|
493
|
+
"""Validate required arguments"""
|
|
494
|
+
if not parsed.metrics:
|
|
495
|
+
print(
|
|
496
|
+
"Missing required arguments for metrics retrieval:", file=sys.stderr
|
|
497
|
+
)
|
|
498
|
+
print(" --metrics: required", file=sys.stderr)
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
# Validate entity_ids requirement (only objectstorage allows querying all entities)
|
|
502
|
+
if not parsed.entity_ids and parsed.service.lower() != "objectstorage":
|
|
503
|
+
print(
|
|
504
|
+
f"--entity-ids is required for service '{parsed.service}'",
|
|
505
|
+
file=sys.stderr,
|
|
506
|
+
)
|
|
507
|
+
print(
|
|
508
|
+
"Only 'objectstorage' service allows querying all entities without --entity-ids",
|
|
509
|
+
file=sys.stderr,
|
|
510
|
+
)
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
# Validate time duration arguments
|
|
514
|
+
has_relative = (
|
|
515
|
+
parsed.duration is not None and parsed.duration_unit is not None
|
|
516
|
+
)
|
|
517
|
+
has_absolute = parsed.start_time is not None and parsed.end_time is not None
|
|
518
|
+
|
|
519
|
+
if not has_relative and not has_absolute:
|
|
520
|
+
print("Time duration required:", file=sys.stderr)
|
|
521
|
+
print(" Either: --duration and --duration-unit", file=sys.stderr)
|
|
522
|
+
print(" Or: --start-time and --end-time", file=sys.stderr)
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
if has_relative and has_absolute:
|
|
526
|
+
print(
|
|
527
|
+
"Cannot specify both relative and absolute time duration",
|
|
528
|
+
file=sys.stderr,
|
|
529
|
+
)
|
|
530
|
+
return False
|
|
531
|
+
|
|
532
|
+
# Validate duration-unit if provided
|
|
533
|
+
if parsed.duration is not None and parsed.duration_unit is not None:
|
|
534
|
+
if parsed.duration_unit not in ALLOWED_TIME_UNITS:
|
|
535
|
+
print(
|
|
536
|
+
f"Invalid duration unit '{parsed.duration_unit}'",
|
|
537
|
+
file=sys.stderr,
|
|
538
|
+
)
|
|
539
|
+
print(
|
|
540
|
+
f"Allowed units: {', '.join(ALLOWED_TIME_UNITS)}",
|
|
541
|
+
file=sys.stderr,
|
|
542
|
+
)
|
|
543
|
+
return False
|
|
544
|
+
|
|
545
|
+
# Validate granularity arguments
|
|
546
|
+
if (parsed.granularity is not None) != (
|
|
547
|
+
parsed.granularity_unit is not None
|
|
548
|
+
):
|
|
549
|
+
print(
|
|
550
|
+
"Both --granularity and --granularity-unit must be provided together",
|
|
551
|
+
file=sys.stderr,
|
|
552
|
+
)
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
# Validate granularity-unit if provided
|
|
556
|
+
if parsed.granularity_unit is not None:
|
|
557
|
+
if parsed.granularity_unit not in ALLOWED_TIME_UNITS:
|
|
558
|
+
print(
|
|
559
|
+
f"Invalid granularity unit '{parsed.granularity_unit}'",
|
|
560
|
+
file=sys.stderr,
|
|
561
|
+
)
|
|
562
|
+
print(
|
|
563
|
+
f"Allowed units: {', '.join(ALLOWED_TIME_UNITS)}",
|
|
564
|
+
file=sys.stderr,
|
|
565
|
+
)
|
|
566
|
+
return False
|
|
567
|
+
|
|
568
|
+
return True
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def parse_entity_ids(entity_ids_str: Optional[str]) -> List:
|
|
572
|
+
"""Parse entity IDs from string"""
|
|
573
|
+
entity_ids = []
|
|
574
|
+
if entity_ids_str:
|
|
575
|
+
for entity_id in entity_ids_str.split(","):
|
|
576
|
+
entity_id = entity_id.strip()
|
|
577
|
+
try:
|
|
578
|
+
entity_ids.append(int(entity_id))
|
|
579
|
+
except ValueError:
|
|
580
|
+
entity_ids.append(entity_id)
|
|
581
|
+
return entity_ids
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def call(args, context=None): # pylint: disable=unused-argument
|
|
585
|
+
"""The entrypoint for this plugin"""
|
|
586
|
+
parser = get_metrics_parser()
|
|
587
|
+
parsed, remaining_args = parser.parse_known_args(args)
|
|
588
|
+
|
|
589
|
+
# Handle help cases
|
|
590
|
+
if (
|
|
591
|
+
not parsed.command
|
|
592
|
+
or parsed.command == "help"
|
|
593
|
+
or "--help" in args
|
|
594
|
+
or "-h" in args
|
|
595
|
+
):
|
|
596
|
+
print_help(parser)
|
|
597
|
+
sys.exit(ExitCodes.SUCCESS)
|
|
598
|
+
|
|
599
|
+
# Validate command
|
|
600
|
+
if parsed.command != "get-metrics":
|
|
601
|
+
print(f"Unknown command: {parsed.command}", file=sys.stderr)
|
|
602
|
+
print("Available commands: get-metrics", file=sys.stderr)
|
|
603
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
604
|
+
|
|
605
|
+
# Validate service is provided
|
|
606
|
+
if not parsed.service:
|
|
607
|
+
print("Service name is required", file=sys.stderr)
|
|
608
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
609
|
+
|
|
610
|
+
if remaining_args:
|
|
611
|
+
print(f"Unknown arguments: {' '.join(remaining_args)}", file=sys.stderr)
|
|
612
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
613
|
+
|
|
614
|
+
# Validate arguments
|
|
615
|
+
if not validate_arguments(parsed):
|
|
616
|
+
print("Invalid arguments. Run with --help for usage.", file=sys.stderr)
|
|
617
|
+
sys.exit(ExitCodes.REQUEST_FAILED)
|
|
618
|
+
|
|
619
|
+
# Parse arguments
|
|
620
|
+
entity_ids = parse_entity_ids(parsed.entity_ids)
|
|
621
|
+
metrics = [x.strip() for x in parsed.metrics.split(",")]
|
|
622
|
+
group_by = None
|
|
623
|
+
if parsed.group_by:
|
|
624
|
+
group_by = [x.strip() for x in parsed.group_by.split(",")]
|
|
625
|
+
filters = None
|
|
626
|
+
if parsed.filters:
|
|
627
|
+
filters = [x.strip() for x in parsed.filters.split(";")]
|
|
628
|
+
|
|
629
|
+
# Create config and call get_metrics
|
|
630
|
+
config = MetricsConfig(
|
|
631
|
+
service_name=parsed.service,
|
|
632
|
+
entity_ids=entity_ids,
|
|
633
|
+
duration=parsed.duration,
|
|
634
|
+
duration_unit=parsed.duration_unit,
|
|
635
|
+
start_time=parsed.start_time,
|
|
636
|
+
end_time=parsed.end_time,
|
|
637
|
+
metrics=metrics,
|
|
638
|
+
granularity=parsed.granularity,
|
|
639
|
+
granularity_unit=parsed.granularity_unit,
|
|
640
|
+
filters=filters,
|
|
641
|
+
group_by=group_by,
|
|
642
|
+
entity_region=parsed.entity_region,
|
|
643
|
+
associated_entity_region=parsed.associated_entity_region,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
get_metrics(config, debug=parsed.debug)
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|