linode-cli 5.63.0__tar.gz → 5.65.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.63.0/linode_cli.egg-info → linode_cli-5.65.0}/PKG-INFO +2 -1
- {linode_cli-5.63.0 → linode_cli-5.65.0/linode_cli.egg-info}/PKG-INFO +2 -1
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/requires.txt +1 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/configuration/config.py +57 -12
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/data-3 +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/get-kubeconfig.py +5 -2
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/__init__.py +231 -13
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/version.py +1 -1
- {linode_cli-5.63.0 → linode_cli-5.65.0}/pyproject.toml +2 -1
- {linode_cli-5.63.0 → linode_cli-5.65.0}/LICENSE +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/MANIFEST.in +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/README.md +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/SOURCES.txt +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/dependency_links.txt +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/entry_points.txt +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linode_cli.egg-info/top_level.txt +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/__init__.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/__main__.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/api_request.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/arg_helpers.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/__init__.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/operation.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/parsing.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/request.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/response.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/baked/util.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/cli.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/completion.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/configuration/__init__.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/configuration/auth.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/configuration/helpers.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/exit_codes.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/help_formatter.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/help_pages.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/helpers.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/oauth-landing-page.html +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/output/__init__.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/output/helpers.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/output/output_handler.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/overrides.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/__init__.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/firewall-editor.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/image-upload.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/metadata.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/buckets.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/config.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/helpers.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/list.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/objects.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/obj/website.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/plugins.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/region-table.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/linodecli/plugins/ssh.py +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/setup.cfg +0 -0
- {linode_cli-5.63.0 → linode_cli-5.65.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linode-cli
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.65.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
|
|
@@ -14,6 +14,7 @@ Requires-Dist: packaging
|
|
|
14
14
|
Requires-Dist: rich
|
|
15
15
|
Requires-Dist: urllib3<3
|
|
16
16
|
Requires-Dist: linode-metadata>=0.3.0
|
|
17
|
+
Requires-Dist: pytimeparse
|
|
17
18
|
Provides-Extra: obj
|
|
18
19
|
Requires-Dist: boto3>=1.36.0; extra == "obj"
|
|
19
20
|
Provides-Extra: dev
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linode-cli
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.65.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
|
|
@@ -14,6 +14,7 @@ Requires-Dist: packaging
|
|
|
14
14
|
Requires-Dist: rich
|
|
15
15
|
Requires-Dist: urllib3<3
|
|
16
16
|
Requires-Dist: linode-metadata>=0.3.0
|
|
17
|
+
Requires-Dist: pytimeparse
|
|
17
18
|
Provides-Extra: obj
|
|
18
19
|
Requires-Dist: boto3>=1.36.0; extra == "obj"
|
|
19
20
|
Provides-Extra: dev
|
|
@@ -5,7 +5,7 @@ Contains logic for loading, updating, and saving Linode CLI configurations.
|
|
|
5
5
|
import argparse
|
|
6
6
|
import os
|
|
7
7
|
import sys
|
|
8
|
-
from typing import Any, Dict, List, Optional
|
|
8
|
+
from typing import Any, Dict, List, Optional, Type, TypeVar, cast
|
|
9
9
|
|
|
10
10
|
from linodecli.exit_codes import ExitCodes
|
|
11
11
|
|
|
@@ -27,6 +27,8 @@ from .helpers import (
|
|
|
27
27
|
|
|
28
28
|
ENV_TOKEN_NAME = "LINODE_CLI_TOKEN"
|
|
29
29
|
|
|
30
|
+
T = TypeVar("T")
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
class CLIConfig:
|
|
32
34
|
"""
|
|
@@ -216,15 +218,12 @@ class CLIConfig:
|
|
|
216
218
|
:param value: The value to set for this key
|
|
217
219
|
:type value: any
|
|
218
220
|
"""
|
|
219
|
-
if self.running_plugin is None:
|
|
220
|
-
raise RuntimeError(
|
|
221
|
-
"No running plugin to retrieve configuration for!"
|
|
222
|
-
)
|
|
223
|
-
|
|
224
221
|
username = self.username or self.default_username()
|
|
225
|
-
self.config.set(username,
|
|
222
|
+
self.config.set(username, self._get_plugin_key(key), value)
|
|
226
223
|
|
|
227
|
-
def plugin_get_value(
|
|
224
|
+
def plugin_get_value(
|
|
225
|
+
self, key: str, default: Optional[T] = None, value_type: Type[T] = str
|
|
226
|
+
) -> Optional[T]:
|
|
228
227
|
"""
|
|
229
228
|
Retrieves and returns a config value previously set for a plugin. Your
|
|
230
229
|
plugin should have set this value in the past. If this value does not
|
|
@@ -235,18 +234,54 @@ class CLIConfig:
|
|
|
235
234
|
:param key: The key of the value to return
|
|
236
235
|
:type key: str
|
|
237
236
|
|
|
237
|
+
:param default: The default value to return if the key is not set
|
|
238
|
+
:type default: T
|
|
239
|
+
|
|
240
|
+
:param value_type: The type to which the value should be cast
|
|
241
|
+
:type value_type: Type[T]
|
|
242
|
+
|
|
238
243
|
:returns: The value for this plugin for this key, or None if not set
|
|
239
244
|
:rtype: any
|
|
240
245
|
"""
|
|
246
|
+
username = self.username or self.default_username() or "DEFAULT"
|
|
247
|
+
value = self.config.get(
|
|
248
|
+
username, self._get_plugin_key(key), fallback=None
|
|
249
|
+
)
|
|
250
|
+
if value is None:
|
|
251
|
+
return default
|
|
252
|
+
|
|
253
|
+
if value_type == str:
|
|
254
|
+
return value
|
|
255
|
+
|
|
256
|
+
if value_type == bool:
|
|
257
|
+
bool_value = self.parse_boolean(value)
|
|
258
|
+
return bool_value if bool_value is not None else default
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
return cast(T, value_type(value))
|
|
262
|
+
except (ValueError, TypeError):
|
|
263
|
+
print(
|
|
264
|
+
f"Could not cast config value {value} to {value_type}.",
|
|
265
|
+
file=sys.stderr,
|
|
266
|
+
)
|
|
267
|
+
return default
|
|
268
|
+
|
|
269
|
+
def plugin_remove_option(self, key: str):
|
|
270
|
+
"""
|
|
271
|
+
Removes a plugin configuration option.
|
|
272
|
+
|
|
273
|
+
:param key: The key of the option to remove
|
|
274
|
+
"""
|
|
275
|
+
username = self.username or self.default_username()
|
|
276
|
+
self.config.remove_option(username, self._get_plugin_key(key))
|
|
277
|
+
|
|
278
|
+
def _get_plugin_key(self, key: str) -> str:
|
|
241
279
|
if self.running_plugin is None:
|
|
242
280
|
raise RuntimeError(
|
|
243
281
|
"No running plugin to retrieve configuration for!"
|
|
244
282
|
)
|
|
245
283
|
|
|
246
|
-
|
|
247
|
-
full_key = f"plugin-{self.running_plugin}-{key}"
|
|
248
|
-
|
|
249
|
-
return self.config.get(username, full_key, fallback=None)
|
|
284
|
+
return f"plugin-{self.running_plugin}-{key}"
|
|
250
285
|
|
|
251
286
|
# TODO: this is more of an argparsing function than it is a config function
|
|
252
287
|
# might be better to move this to argparsing during refactor and just have
|
|
@@ -654,3 +689,13 @@ class CLIConfig:
|
|
|
654
689
|
if (self.config.has_section("custom_aliases"))
|
|
655
690
|
else {}
|
|
656
691
|
)
|
|
692
|
+
|
|
693
|
+
def parse_boolean(self, value: str) -> Optional[bool]:
|
|
694
|
+
"""
|
|
695
|
+
Parses a string config value into a boolean. Returns None if the value
|
|
696
|
+
cannot be parsed as a boolean.
|
|
697
|
+
|
|
698
|
+
:param value: The string value to parse.
|
|
699
|
+
:return: The parsed boolean value.
|
|
700
|
+
"""
|
|
701
|
+
return self.config.BOOLEAN_STATES.get(value.lower(), None)
|
|
Binary file
|
|
@@ -15,6 +15,7 @@ import yaml
|
|
|
15
15
|
|
|
16
16
|
from linodecli.exit_codes import ExitCodes
|
|
17
17
|
from linodecli.help_formatter import SortingHelpFormatter
|
|
18
|
+
from linodecli.plugins import inherit_plugin_args
|
|
18
19
|
|
|
19
20
|
PLUGIN_BASE = "linode-cli get-kubeconfig"
|
|
20
21
|
|
|
@@ -23,8 +24,10 @@ def call(args, context):
|
|
|
23
24
|
"""
|
|
24
25
|
The entrypoint for this plugin
|
|
25
26
|
"""
|
|
26
|
-
parser =
|
|
27
|
-
|
|
27
|
+
parser = inherit_plugin_args(
|
|
28
|
+
argparse.ArgumentParser(
|
|
29
|
+
PLUGIN_BASE, add_help=True, formatter_class=SortingHelpFormatter
|
|
30
|
+
)
|
|
28
31
|
)
|
|
29
32
|
|
|
30
33
|
group = parser.add_mutually_exclusive_group()
|
|
@@ -4,6 +4,7 @@ CLI Plugin for handling OBJ
|
|
|
4
4
|
"""
|
|
5
5
|
import getpass
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
7
8
|
import socket
|
|
8
9
|
import sys
|
|
9
10
|
import time
|
|
@@ -11,13 +12,13 @@ from argparse import ArgumentParser
|
|
|
11
12
|
from contextlib import suppress
|
|
12
13
|
from datetime import datetime
|
|
13
14
|
from math import ceil
|
|
14
|
-
from typing import List
|
|
15
|
+
from typing import List, Optional
|
|
15
16
|
|
|
17
|
+
from pytimeparse import parse as parse_time
|
|
16
18
|
from rich import print as rprint
|
|
17
19
|
from rich.table import Table
|
|
18
20
|
|
|
19
21
|
from linodecli.cli import CLI
|
|
20
|
-
from linodecli.configuration import _do_get_request
|
|
21
22
|
from linodecli.configuration.helpers import _default_text_input
|
|
22
23
|
from linodecli.exit_codes import ExitCodes
|
|
23
24
|
from linodecli.help_formatter import SortingHelpFormatter
|
|
@@ -65,6 +66,17 @@ except ImportError:
|
|
|
65
66
|
HAS_BOTO = False
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
CLUSTER_KEY = "cluster"
|
|
70
|
+
KEY_CLEANUP_ENABLED_KEY = "key-cleanup-enabled"
|
|
71
|
+
KEY_LIFESPAN_KEY = "key-lifespan"
|
|
72
|
+
KEY_ROTATION_PERIOD_KEY = "key-rotation-period"
|
|
73
|
+
KEY_CLEANUP_BATCH_SIZE_KEY = "key-cleanup-batch-size"
|
|
74
|
+
LAST_KEY_CLEANUP_TIMESTAMP_KEY = "last-key-cleanup-timestamp"
|
|
75
|
+
ACCESS_KEY_KEY = "access-key"
|
|
76
|
+
SECRET_KEY_KEY = "secret-key"
|
|
77
|
+
TOKEN_KEY = "token"
|
|
78
|
+
|
|
79
|
+
|
|
68
80
|
def generate_url(get_client, args, **kwargs): # pylint: disable=unused-argument
|
|
69
81
|
"""
|
|
70
82
|
Generates a URL to an object
|
|
@@ -314,13 +326,44 @@ def get_obj_args_parser():
|
|
|
314
326
|
metavar="COMMAND",
|
|
315
327
|
nargs="?",
|
|
316
328
|
type=str,
|
|
317
|
-
help="The command to execute in object storage",
|
|
329
|
+
help="The command to execute in object storage.",
|
|
318
330
|
)
|
|
319
331
|
parser.add_argument(
|
|
320
332
|
"--cluster",
|
|
321
333
|
metavar="CLUSTER",
|
|
322
334
|
type=str,
|
|
323
|
-
help="The cluster to use for the operation",
|
|
335
|
+
help="The cluster to use for the operation.",
|
|
336
|
+
)
|
|
337
|
+
parser.add_argument(
|
|
338
|
+
"--force-key-cleanup",
|
|
339
|
+
action="store_true",
|
|
340
|
+
help="Performs cleanup of old linode-cli generated Object Storage keys"
|
|
341
|
+
" before executing the Object Storage command. It overrides"
|
|
342
|
+
" the --perform-key-cleanup option.",
|
|
343
|
+
)
|
|
344
|
+
parser.add_argument(
|
|
345
|
+
"--key-cleanup-enabled",
|
|
346
|
+
choices=["yes", "no"],
|
|
347
|
+
help="If set to 'yes', performs cleanup of old linode-cli generated Object Storage"
|
|
348
|
+
" keys before executing the Object Storage command. Cleanup occurs"
|
|
349
|
+
" at most once every 24 hours.",
|
|
350
|
+
)
|
|
351
|
+
parser.add_argument(
|
|
352
|
+
"--key-lifespan",
|
|
353
|
+
type=str,
|
|
354
|
+
help="Specifies the lifespan of linode-cli generated Object Storage keys"
|
|
355
|
+
" (e.g. 30d for 30 days). Used only during key cleanup.",
|
|
356
|
+
)
|
|
357
|
+
parser.add_argument(
|
|
358
|
+
"--key-rotation-period",
|
|
359
|
+
type=str,
|
|
360
|
+
help="Specifies the period after which the linode-cli generated Object Storage"
|
|
361
|
+
" key must be rotated (e.g. 10d for 10 days). Used only during key cleanup.",
|
|
362
|
+
)
|
|
363
|
+
parser.add_argument(
|
|
364
|
+
"--key-cleanup-batch-size",
|
|
365
|
+
type=int,
|
|
366
|
+
help="Number of old linode-cli generated Object Storage keys to clean up at once.",
|
|
324
367
|
)
|
|
325
368
|
|
|
326
369
|
return parser
|
|
@@ -400,8 +443,9 @@ def call(
|
|
|
400
443
|
access_key = None
|
|
401
444
|
secret_key = None
|
|
402
445
|
|
|
403
|
-
# make a client, but only if we weren't printing help
|
|
446
|
+
# make a client and clean-up keys, but only if we weren't printing help
|
|
404
447
|
if not is_help:
|
|
448
|
+
_cleanup_keys(context.client, parsed)
|
|
405
449
|
access_key, secret_key = get_credentials(context.client)
|
|
406
450
|
|
|
407
451
|
cluster = parsed.cluster
|
|
@@ -497,8 +541,8 @@ def _get_s3_creds(client: CLI, force: bool = False):
|
|
|
497
541
|
:returns: The access key and secret key for this user
|
|
498
542
|
:rtype: tuple(str, str)
|
|
499
543
|
"""
|
|
500
|
-
access_key = client.config.plugin_get_value(
|
|
501
|
-
secret_key = client.config.plugin_get_value(
|
|
544
|
+
access_key = client.config.plugin_get_value(ACCESS_KEY_KEY)
|
|
545
|
+
secret_key = client.config.plugin_get_value(SECRET_KEY_KEY)
|
|
502
546
|
|
|
503
547
|
if force or access_key is None:
|
|
504
548
|
# this means there are no stored s3 creds for this user - set them up
|
|
@@ -507,7 +551,7 @@ def _get_s3_creds(client: CLI, force: bool = False):
|
|
|
507
551
|
# being provided by the environment, but if the CLI is running without a
|
|
508
552
|
# config, we shouldn't generate new keys (or we'd end up doing so with each
|
|
509
553
|
# request) - instead ask for them to be set up.
|
|
510
|
-
if client.config.get_value(
|
|
554
|
+
if client.config.get_value(TOKEN_KEY) is None:
|
|
511
555
|
print(
|
|
512
556
|
"You are running the Linode CLI without a configuration file, but "
|
|
513
557
|
"object storage keys were not configured. "
|
|
@@ -571,8 +615,8 @@ def _get_s3_creds(client: CLI, force: bool = False):
|
|
|
571
615
|
access_key = resp["access_key"]
|
|
572
616
|
secret_key = resp["secret_key"]
|
|
573
617
|
|
|
574
|
-
client.config.plugin_set_value(
|
|
575
|
-
client.config.plugin_set_value(
|
|
618
|
+
client.config.plugin_set_value(ACCESS_KEY_KEY, access_key)
|
|
619
|
+
client.config.plugin_set_value(SECRET_KEY_KEY, secret_key)
|
|
576
620
|
client.config.write_config()
|
|
577
621
|
|
|
578
622
|
return access_key, secret_key
|
|
@@ -580,14 +624,188 @@ def _get_s3_creds(client: CLI, force: bool = False):
|
|
|
580
624
|
|
|
581
625
|
def _configure_plugin(client: CLI):
|
|
582
626
|
"""
|
|
583
|
-
Configures
|
|
627
|
+
Configures Object Storage plugin.
|
|
584
628
|
"""
|
|
585
|
-
|
|
586
629
|
cluster = _default_text_input( # pylint: disable=protected-access
|
|
587
630
|
"Default cluster for operations (e.g. `us-mia-1`)",
|
|
588
631
|
optional=False,
|
|
589
632
|
)
|
|
590
633
|
|
|
591
634
|
if cluster:
|
|
592
|
-
client.config.plugin_set_value(
|
|
635
|
+
client.config.plugin_set_value(CLUSTER_KEY, cluster)
|
|
636
|
+
|
|
593
637
|
client.config.write_config()
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _cleanup_keys(client: CLI, options) -> None:
|
|
641
|
+
"""
|
|
642
|
+
Cleans up stale linode-cli generated object storage keys.
|
|
643
|
+
"""
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
current_timestamp = int(time.time())
|
|
647
|
+
if not _should_perform_key_cleanup(client, options, current_timestamp):
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
cleanup_message = (
|
|
651
|
+
"Cleaning up old linode-cli generated Object Storage keys."
|
|
652
|
+
)
|
|
653
|
+
if not options.force_key_cleanup and not options.key_cleanup_enabled:
|
|
654
|
+
cleanup_message += (
|
|
655
|
+
" To disable this, use the '--key-cleanup-enabled no' option."
|
|
656
|
+
)
|
|
657
|
+
print(cleanup_message, file=sys.stderr)
|
|
658
|
+
|
|
659
|
+
status, keys = client.call_operation("object-storage", "keys-list")
|
|
660
|
+
if status != 200:
|
|
661
|
+
print(
|
|
662
|
+
"Failed to list object storage keys for cleanup",
|
|
663
|
+
file=sys.stderr,
|
|
664
|
+
)
|
|
665
|
+
return
|
|
666
|
+
|
|
667
|
+
key_lifespan = _get_key_lifespan(client, options)
|
|
668
|
+
key_rotation_period = _get_key_rotation_period(client, options)
|
|
669
|
+
cleanup_batch_size = _get_cleanup_batch_size(client, options)
|
|
670
|
+
|
|
671
|
+
linode_cli_keys = _get_linode_cli_keys(
|
|
672
|
+
keys["data"], key_lifespan, key_rotation_period, current_timestamp
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
_rotate_current_key_if_needed(client, linode_cli_keys)
|
|
676
|
+
_delete_stale_keys(client, linode_cli_keys, cleanup_batch_size)
|
|
677
|
+
|
|
678
|
+
client.config.plugin_set_value(
|
|
679
|
+
LAST_KEY_CLEANUP_TIMESTAMP_KEY, str(current_timestamp)
|
|
680
|
+
)
|
|
681
|
+
client.config.write_config()
|
|
682
|
+
|
|
683
|
+
except Exception as e:
|
|
684
|
+
print(
|
|
685
|
+
f"Unable to clean up stale linode-cli Object Storage keys: {e}",
|
|
686
|
+
file=sys.stderr,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _should_perform_key_cleanup(
|
|
691
|
+
client: CLI, options, current_timestamp
|
|
692
|
+
) -> bool:
|
|
693
|
+
if options.force_key_cleanup:
|
|
694
|
+
return True
|
|
695
|
+
if not _is_key_cleanup_enabled(client, options):
|
|
696
|
+
return False
|
|
697
|
+
|
|
698
|
+
last_cleanup = client.config.plugin_get_value(
|
|
699
|
+
LAST_KEY_CLEANUP_TIMESTAMP_KEY
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
# if we did a cleanup in the last 24 hours, skip it this time
|
|
703
|
+
return (
|
|
704
|
+
last_cleanup is None
|
|
705
|
+
or int(last_cleanup) <= current_timestamp - 24 * 60 * 60
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _is_key_cleanup_enabled(client, options) -> bool:
|
|
710
|
+
if options.key_cleanup_enabled in ["yes", "no"]:
|
|
711
|
+
return options.key_cleanup_enabled == "yes"
|
|
712
|
+
return client.config.plugin_get_value(KEY_CLEANUP_ENABLED_KEY, True, bool)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _get_key_lifespan(client, options) -> str:
|
|
716
|
+
return options.key_lifespan or client.config.plugin_get_value(
|
|
717
|
+
KEY_LIFESPAN_KEY, "30d"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _get_key_rotation_period(client, options) -> str:
|
|
722
|
+
return options.key_rotation_period or client.config.plugin_get_value(
|
|
723
|
+
KEY_ROTATION_PERIOD_KEY, "10d"
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def _get_cleanup_batch_size(client, options) -> int:
|
|
728
|
+
return options.key_cleanup_batch_size or client.config.plugin_get_value(
|
|
729
|
+
KEY_CLEANUP_BATCH_SIZE_KEY, 10, int
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _get_linode_cli_keys(
|
|
734
|
+
keys_data: list,
|
|
735
|
+
key_lifespan: str,
|
|
736
|
+
key_rotation_period: str,
|
|
737
|
+
current_timestamp: int,
|
|
738
|
+
) -> list:
|
|
739
|
+
stale_threshold = current_timestamp - parse_time(key_lifespan)
|
|
740
|
+
rotation_threshold = current_timestamp - parse_time(key_rotation_period)
|
|
741
|
+
|
|
742
|
+
def extract_key_info(key: dict) -> Optional[dict]:
|
|
743
|
+
match = re.match(r"^linode-cli-.+@.+-(\d{10,})$", key["label"])
|
|
744
|
+
if not match:
|
|
745
|
+
return None
|
|
746
|
+
|
|
747
|
+
created_timestamp = int(match.group(1))
|
|
748
|
+
is_stale = created_timestamp < stale_threshold
|
|
749
|
+
needs_rotation = is_stale or created_timestamp <= rotation_threshold
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
"id": key["id"],
|
|
753
|
+
"label": key["label"],
|
|
754
|
+
"access_key": key["access_key"],
|
|
755
|
+
"created_timestamp": created_timestamp,
|
|
756
|
+
"is_stale": is_stale,
|
|
757
|
+
"needs_rotation": needs_rotation,
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return sorted(
|
|
761
|
+
[info for key in keys_data if (info := extract_key_info(key))],
|
|
762
|
+
key=lambda k: k["created_timestamp"],
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _rotate_current_key_if_needed(client: CLI, linode_cli_keys: list) -> None:
|
|
767
|
+
current_access_key = client.config.plugin_get_value(ACCESS_KEY_KEY)
|
|
768
|
+
|
|
769
|
+
key_to_rotate = next(
|
|
770
|
+
(
|
|
771
|
+
key_info
|
|
772
|
+
for key_info in linode_cli_keys
|
|
773
|
+
if key_info["access_key"] == current_access_key
|
|
774
|
+
and key_info["needs_rotation"]
|
|
775
|
+
),
|
|
776
|
+
None,
|
|
777
|
+
)
|
|
778
|
+
if key_to_rotate:
|
|
779
|
+
_delete_key(client, key_to_rotate["id"], key_to_rotate["label"])
|
|
780
|
+
linode_cli_keys.remove(key_to_rotate)
|
|
781
|
+
client.config.plugin_remove_option(ACCESS_KEY_KEY)
|
|
782
|
+
client.config.plugin_remove_option(SECRET_KEY_KEY)
|
|
783
|
+
client.config.write_config()
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _delete_stale_keys(
|
|
787
|
+
client: CLI, linode_cli_keys: list, batch_size: int
|
|
788
|
+
) -> None:
|
|
789
|
+
stale_keys = [k for k in linode_cli_keys if k["is_stale"]]
|
|
790
|
+
for key_info in stale_keys[:batch_size]:
|
|
791
|
+
_delete_key(client, key_info["id"], key_info["label"])
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _delete_key(client: CLI, key_id: str, label: str) -> None:
|
|
795
|
+
try:
|
|
796
|
+
print(
|
|
797
|
+
f"Deleting linode-cli Object Storage key: {label}", file=sys.stderr
|
|
798
|
+
)
|
|
799
|
+
status, _ = client.call_operation(
|
|
800
|
+
"object-storage", "keys-delete", [str(key_id)]
|
|
801
|
+
)
|
|
802
|
+
if status != 200:
|
|
803
|
+
print(
|
|
804
|
+
f"Failed to delete key: {label}; status {status}",
|
|
805
|
+
file=sys.stderr,
|
|
806
|
+
)
|
|
807
|
+
except Exception as e:
|
|
808
|
+
print(
|
|
809
|
+
f"Exception occurred while deleting key: {label}; {e}",
|
|
810
|
+
file=sys.stderr,
|
|
811
|
+
)
|
|
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
|