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.
Files changed (57) hide show
  1. {linode_cli-5.66.0/linode_cli.egg-info → linode_cli-5.68.0}/PKG-INFO +1 -1
  2. {linode_cli-5.66.0 → linode_cli-5.68.0/linode_cli.egg-info}/PKG-INFO +1 -1
  3. {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/SOURCES.txt +1 -0
  4. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/cli.py +48 -0
  5. linode_cli-5.68.0/linodecli/data-3 +0 -0
  6. linode_cli-5.68.0/linodecli/plugins/monitor-api.py +646 -0
  7. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/version.py +1 -1
  8. linode_cli-5.66.0/linodecli/data-3 +0 -0
  9. {linode_cli-5.66.0 → linode_cli-5.68.0}/LICENSE +0 -0
  10. {linode_cli-5.66.0 → linode_cli-5.68.0}/MANIFEST.in +0 -0
  11. {linode_cli-5.66.0 → linode_cli-5.68.0}/README.md +0 -0
  12. {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/dependency_links.txt +0 -0
  13. {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/entry_points.txt +0 -0
  14. {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/requires.txt +0 -0
  15. {linode_cli-5.66.0 → linode_cli-5.68.0}/linode_cli.egg-info/top_level.txt +0 -0
  16. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/__init__.py +0 -0
  17. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/__main__.py +0 -0
  18. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/api_request.py +0 -0
  19. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/arg_helpers.py +0 -0
  20. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/__init__.py +0 -0
  21. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/operation.py +0 -0
  22. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/parsing.py +0 -0
  23. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/request.py +0 -0
  24. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/response.py +0 -0
  25. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/baked/util.py +0 -0
  26. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/completion.py +0 -0
  27. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/configuration/__init__.py +0 -0
  28. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/configuration/auth.py +0 -0
  29. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/configuration/config.py +0 -0
  30. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/configuration/helpers.py +0 -0
  31. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/exit_codes.py +0 -0
  32. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/help_formatter.py +0 -0
  33. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/help_pages.py +0 -0
  34. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/helpers.py +0 -0
  35. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/oauth-landing-page.html +0 -0
  36. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/output/__init__.py +0 -0
  37. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/output/helpers.py +0 -0
  38. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/output/output_handler.py +0 -0
  39. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/overrides.py +0 -0
  40. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/__init__.py +0 -0
  41. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/firewall-editor.py +0 -0
  42. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/get-kubeconfig.py +0 -0
  43. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/image-upload.py +0 -0
  44. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/metadata.py +0 -0
  45. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/__init__.py +0 -0
  46. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/buckets.py +0 -0
  47. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/config.py +0 -0
  48. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/helpers.py +0 -0
  49. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/list.py +0 -0
  50. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/objects.py +0 -0
  51. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/obj/website.py +0 -0
  52. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/plugins.py +0 -0
  53. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/region-table.py +0 -0
  54. {linode_cli-5.66.0 → linode_cli-5.68.0}/linodecli/plugins/ssh.py +0 -0
  55. {linode_cli-5.66.0 → linode_cli-5.68.0}/pyproject.toml +0 -0
  56. {linode_cli-5.66.0 → linode_cli-5.68.0}/setup.cfg +0 -0
  57. {linode_cli-5.66.0 → linode_cli-5.68.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linode-cli
3
- Version: 5.66.0
3
+ Version: 5.68.0
4
4
  Summary: The official command-line interface for interacting with the Linode API.
5
5
  Author-email: "Akamai Technologies Inc." <developers@linode.com>
6
6
  License-Expression: BSD-3-Clause
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: linode-cli
3
- Version: 5.66.0
3
+ Version: 5.68.0
4
4
  Summary: The official command-line interface for interacting with the Linode API.
5
5
  Author-email: "Akamai Technologies Inc." <developers@linode.com>
6
6
  License-Expression: BSD-3-Clause
@@ -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)
@@ -2,4 +2,4 @@
2
2
  The version of the Linode CLI.
3
3
  """
4
4
 
5
- __version__ = "v5.66.0"
5
+ __version__ = "v5.68.0"
Binary file
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes