remotivelabs-cli 0.0.34__tar.gz → 0.0.35__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.
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/PKG-INFO +2 -2
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/lib/broker.py +2 -2
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/auth/cmd.py +27 -5
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/auth/login.py +10 -2
- remotivelabs_cli-0.0.35/cli/cloud/auth_tokens.py +32 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/recordings.py +14 -23
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/rest_helper.py +8 -9
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/service_account_tokens.py +8 -31
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/storage/__init__.py +1 -1
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/storage/cmd.py +1 -1
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/storage/copy.py +2 -2
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/storage/uri_or_path.py +1 -1
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/connect/protopie/protopie.py +2 -2
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/remotive.py +2 -0
- remotivelabs_cli-0.0.35/cli/settings/__init__.py +5 -0
- remotivelabs_cli-0.0.35/cli/settings/cmd.py +71 -0
- remotivelabs_cli-0.0.35/cli/settings/core.py +261 -0
- remotivelabs_cli-0.0.35/cli/settings/token_file.py +22 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/pyproject.toml +2 -2
- remotivelabs_cli-0.0.34/cli/cloud/auth_tokens.py +0 -143
- remotivelabs_cli-0.0.34/cli/settings.py +0 -64
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/LICENSE +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/README.md +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/__init__.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/brokers.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/export.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/files.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/lib/__about__.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/license_flows.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/licenses.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/playback.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/record.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/scripting.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/broker/signals.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/__init__.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/auth/__init__.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/brokers.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/cloud_cli.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/configs.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/organisations.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/projects.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/recordings_playback.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/resumable_upload.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/sample_recordings.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/cloud/service_accounts.py +0 -0
- {remotivelabs_cli-0.0.34/cli/cloud/storage → remotivelabs_cli-0.0.35/cli/cloud}/uri.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/connect/__init__.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/connect/connect.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/errors.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/tools/__init__.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/tools/can/RemotiveLabs.can1.log +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/tools/can/__init__.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/tools/can/can.py +0 -0
- {remotivelabs_cli-0.0.34 → remotivelabs_cli-0.0.35}/cli/tools/tools.py +0 -0
@@ -1,10 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: remotivelabs-cli
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.35
|
4
4
|
Summary: CLI for operating RemotiveCloud and RemotiveBroker
|
5
5
|
Author: Johan Rask
|
6
6
|
Author-email: johan.rask@remotivelabs.com
|
7
|
-
Requires-Python: >=3.8
|
7
|
+
Requires-Python: >=3.8,<4
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Programming Language :: Python :: 3.8
|
10
10
|
Classifier: Programming Language :: Python :: 3.9
|
@@ -21,8 +21,8 @@ import typer
|
|
21
21
|
from google.protobuf.json_format import MessageToDict
|
22
22
|
from rich.console import Console
|
23
23
|
|
24
|
-
from cli import settings
|
25
24
|
from cli.errors import ErrorPrinter
|
25
|
+
from cli.settings import settings
|
26
26
|
|
27
27
|
err_console = Console(stderr=True)
|
28
28
|
|
@@ -51,7 +51,7 @@ class Broker:
|
|
51
51
|
|
52
52
|
if api_key is None or api_key == "":
|
53
53
|
if url.startswith("https"):
|
54
|
-
self.intercept_channel = br.create_channel(url, None, settings.
|
54
|
+
self.intercept_channel = br.create_channel(url, None, settings.get_active_token())
|
55
55
|
# TODO - Temporary solution to print proper error message, remove ENV once api-key is gone
|
56
56
|
os.environ["ACCESS_TOKEN"] = "true"
|
57
57
|
else:
|
@@ -1,8 +1,11 @@
|
|
1
|
+
import sys
|
2
|
+
|
1
3
|
import typer
|
2
4
|
|
3
|
-
from cli import settings
|
4
5
|
from cli.cloud.auth.login import login as do_login
|
5
6
|
from cli.cloud.rest_helper import RestHelper as Rest
|
7
|
+
from cli.errors import ErrorPrinter
|
8
|
+
from cli.settings import TokenNotFoundError, settings
|
6
9
|
|
7
10
|
from .. import auth_tokens
|
8
11
|
|
@@ -29,18 +32,37 @@ def whoami() -> None:
|
|
29
32
|
"""
|
30
33
|
Validates authentication and fetches your user information
|
31
34
|
"""
|
32
|
-
|
35
|
+
try:
|
36
|
+
Rest.handle_get("/api/whoami")
|
37
|
+
except TokenNotFoundError as e:
|
38
|
+
ErrorPrinter.print_hint(str(e))
|
39
|
+
sys.exit(1)
|
33
40
|
|
34
41
|
|
35
42
|
@app.command()
|
36
43
|
def print_access_token() -> None:
|
37
44
|
"""
|
38
|
-
Print current active
|
45
|
+
Print current active token
|
46
|
+
"""
|
47
|
+
try:
|
48
|
+
print(settings.get_active_token())
|
49
|
+
except TokenNotFoundError as e:
|
50
|
+
ErrorPrinter.print_hint(str(e))
|
51
|
+
sys.exit(1)
|
52
|
+
|
53
|
+
|
54
|
+
def print_access_token_file() -> None:
|
55
|
+
"""
|
56
|
+
Print current active token and its metadata
|
39
57
|
"""
|
40
|
-
|
58
|
+
try:
|
59
|
+
print(settings.get_active_token_file())
|
60
|
+
except TokenNotFoundError as e:
|
61
|
+
ErrorPrinter.print_hint(str(e))
|
62
|
+
sys.exit(1)
|
41
63
|
|
42
64
|
|
43
65
|
@app.command(help="Clear access token")
|
44
66
|
def logout() -> None:
|
45
|
-
settings.
|
67
|
+
settings.clear_active_token()
|
46
68
|
print("Access token removed")
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import datetime
|
1
2
|
import time
|
2
3
|
import webbrowser
|
3
4
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
@@ -6,8 +7,9 @@ from typing import Any
|
|
6
7
|
|
7
8
|
from typing_extensions import override
|
8
9
|
|
9
|
-
from cli import settings
|
10
10
|
from cli.cloud.rest_helper import RestHelper as Rest
|
11
|
+
from cli.settings import settings
|
12
|
+
from cli.settings import token_file as tf
|
11
13
|
|
12
14
|
httpd: HTTPServer
|
13
15
|
|
@@ -35,7 +37,13 @@ class S(BaseHTTPRequestHandler):
|
|
35
37
|
killerthread = Thread(target=httpd.shutdown)
|
36
38
|
killerthread.start()
|
37
39
|
|
38
|
-
|
40
|
+
token = tf.TokenFile(
|
41
|
+
name="CLI_login_token",
|
42
|
+
token=path[1:],
|
43
|
+
created=str(datetime.datetime.now().isoformat()),
|
44
|
+
expires="unknown",
|
45
|
+
)
|
46
|
+
settings.add_and_activate_short_lived_cli_token(tf.dumps(token))
|
39
47
|
print("Successfully logged on, you are ready to go with cli")
|
40
48
|
|
41
49
|
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import typer
|
2
|
+
|
3
|
+
from cli.settings import settings
|
4
|
+
|
5
|
+
from .rest_helper import RestHelper as Rest
|
6
|
+
|
7
|
+
app = typer.Typer()
|
8
|
+
|
9
|
+
|
10
|
+
# TODO: add add interactive flag to set target directory # pylint: disable=fixme
|
11
|
+
@app.command(name="create", help="Create and download a new personal access token")
|
12
|
+
def create(activate: bool = typer.Option(False, help="Activate the token for use after download")) -> None: # pylint: disable=W0621
|
13
|
+
response = Rest.handle_post(url="/api/me/keys", return_response=True)
|
14
|
+
pat = settings.add_personal_token(response.text)
|
15
|
+
print(f"Personal access token added: {pat.name}")
|
16
|
+
|
17
|
+
if not activate:
|
18
|
+
print(f"Use 'remotive cloud auth tokens activate {pat.name}' to use this access token from cli")
|
19
|
+
else:
|
20
|
+
settings.activate_token(pat.name)
|
21
|
+
print("Token file activated and ready for use")
|
22
|
+
print("\033[93m This file contains secrets and must be kept safe")
|
23
|
+
|
24
|
+
|
25
|
+
@app.command(name="list", help="List personal access tokens")
|
26
|
+
def list_tokens() -> None:
|
27
|
+
Rest.handle_get("/api/me/keys")
|
28
|
+
|
29
|
+
|
30
|
+
@app.command(name="revoke", help="Revoke personal access token")
|
31
|
+
def revoke(name: str = typer.Argument(help="Access token name")) -> None:
|
32
|
+
Rest.handle_delete(f"/api/me/keys/{name}")
|
@@ -1,5 +1,3 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
import glob
|
4
2
|
import json
|
5
3
|
import os
|
@@ -16,7 +14,9 @@ import grpc
|
|
16
14
|
import requests
|
17
15
|
import typer
|
18
16
|
from rich.progress import Progress, SpinnerColumn, TaskID, TextColumn, track
|
17
|
+
from typing_extensions import Annotated
|
19
18
|
|
19
|
+
from cli.cloud.uri import URI
|
20
20
|
from cli.errors import ErrorPrinter
|
21
21
|
|
22
22
|
from ..broker.lib.broker import Broker
|
@@ -33,6 +33,10 @@ def uid(p: Any) -> Any:
|
|
33
33
|
return p["uid"]
|
34
34
|
|
35
35
|
|
36
|
+
# ruff: noqa: FA100
|
37
|
+
# ruff: noqa: C901
|
38
|
+
|
39
|
+
|
36
40
|
# to be used in options
|
37
41
|
# autocompletion=project_names)
|
38
42
|
def project_names() -> Any:
|
@@ -83,25 +87,19 @@ def describe(
|
|
83
87
|
|
84
88
|
@app.command(name="import")
|
85
89
|
def import_as_recording(
|
90
|
+
uri: Annotated[URI, typer.Argument(help="Remote storage path", parser=URI, show_default=False)],
|
86
91
|
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
87
|
-
path: str = typer.Argument(default=..., help="Remote storage path to file to delete"),
|
88
92
|
) -> None:
|
89
93
|
"""
|
90
94
|
Imports a file from Storage as a recording.
|
95
|
+
|
91
96
|
NOTE that Storage is not yet available to all customers
|
92
97
|
"""
|
93
|
-
|
94
|
-
if path.startswith("rcs://"):
|
95
|
-
final_path = __check_rcs_path(path)
|
96
|
-
else:
|
97
|
-
ErrorPrinter.print_hint("Path must start with rcs://")
|
98
|
-
sys.exit(1)
|
99
|
-
|
100
98
|
Rest.handle_post(
|
101
99
|
url=f"/api/project/{project}/files/recording",
|
102
100
|
return_response=True,
|
103
|
-
progress_label=f"Importing {
|
104
|
-
body=json.dumps({"path":
|
101
|
+
progress_label=f"Importing {uri.path}...",
|
102
|
+
body=json.dumps({"path": uri.path}),
|
105
103
|
)
|
106
104
|
|
107
105
|
ErrorPrinter.print_hint(
|
@@ -109,15 +107,6 @@ def import_as_recording(
|
|
109
107
|
)
|
110
108
|
|
111
109
|
|
112
|
-
# Copied from filestorage
|
113
|
-
def __check_rcs_path(path: str) -> str:
|
114
|
-
rcs_path = path.replace("rcs://", "/")
|
115
|
-
if rcs_path.startswith("/."):
|
116
|
-
ErrorPrinter.print_hint("Invalid path")
|
117
|
-
sys.exit(1)
|
118
|
-
return rcs_path
|
119
|
-
|
120
|
-
|
121
110
|
def do_start(name: str, project: str, api_key: str, return_response: bool = False) -> requests.Response:
|
122
111
|
body = {"size": "S"}
|
123
112
|
if not api_key:
|
@@ -252,7 +241,7 @@ def upload( # noqa: C901
|
|
252
241
|
help="Path to recording file to upload",
|
253
242
|
),
|
254
243
|
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
255
|
-
recording_session: str = typer.Option(default=None, help="Optional existing recording to upload file to"),
|
244
|
+
recording_session: Optional[str] = typer.Option(default=None, help="Optional existing recording to upload file to"),
|
256
245
|
) -> None:
|
257
246
|
# pylint: disable=R0912,R0914,R0915
|
258
247
|
"""
|
@@ -540,7 +529,9 @@ def stop(
|
|
540
529
|
|
541
530
|
|
542
531
|
# pylint: disable-next=C0301
|
543
|
-
def _do_change_playback_mode(
|
532
|
+
def _do_change_playback_mode(
|
533
|
+
mode: str, recording_session: str, broker_name: Optional[str], project: str, seconds: Optional[int] = None
|
534
|
+
) -> None: # noqa: C901
|
544
535
|
# pylint: disable=R0912,R0914,R0915
|
545
536
|
response = Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
|
546
537
|
if response is None:
|
@@ -15,7 +15,8 @@ from requests.exceptions import JSONDecodeError
|
|
15
15
|
from rich.console import Console
|
16
16
|
from rich.progress import Progress, SpinnerColumn, TextColumn, wrap_file
|
17
17
|
|
18
|
-
from cli import
|
18
|
+
from cli.errors import ErrorPrinter
|
19
|
+
from cli.settings import TokenNotFoundError, settings
|
19
20
|
|
20
21
|
err_console = Console(stderr=True)
|
21
22
|
|
@@ -81,14 +82,12 @@ class RestHelper:
|
|
81
82
|
# print('You must first set the organisation id to use: export REMOTIVE_CLOUD_ORGANISATION=organisationUid')
|
82
83
|
# raise typer.Exit()
|
83
84
|
# org = os.environ["REMOTIVE_CLOUD_ORGANISATION"]
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
token = settings.read_secret_token()
|
91
|
-
# os.environ['REMOTIVE_CLOUD_AUTH_TOKEN'] = token
|
85
|
+
# os.environ['REMOTIVE_CLOUD_AUTH_TOKEN'] = token
|
86
|
+
try:
|
87
|
+
token = settings.get_active_token()
|
88
|
+
except TokenNotFoundError:
|
89
|
+
ErrorPrinter.print_hint("you are not logged in, please login using [green]remotive cloud auth login[/green]")
|
90
|
+
sys.exit(1)
|
92
91
|
|
93
92
|
RestHelper.__headers["Authorization"] = "Bearer " + token.strip()
|
94
93
|
RestHelper.__headers["User-Agent"] = f"remotivelabs-cli {RestHelper.get_cli_version()}"
|
@@ -1,16 +1,16 @@
|
|
1
1
|
import json
|
2
|
-
from pathlib import Path
|
3
2
|
|
4
3
|
import typer
|
5
4
|
|
6
|
-
from cli import settings
|
5
|
+
from cli.settings import settings
|
7
6
|
|
8
7
|
from .rest_helper import RestHelper as Rest
|
9
8
|
|
10
9
|
app = typer.Typer()
|
11
10
|
|
12
11
|
|
13
|
-
|
12
|
+
# TODO: add add interactive flag to set target directory # pylint: disable=fixme
|
13
|
+
@app.command(name="create", help="Create and download a new service account access token")
|
14
14
|
def create(
|
15
15
|
expire_in_days: int = typer.Option(default=365, help="Number of this token is valid"),
|
16
16
|
service_account: str = typer.Option(..., help="Service account name"),
|
@@ -22,35 +22,19 @@ def create(
|
|
22
22
|
body=json.dumps({"daysUntilExpiry": expire_in_days}),
|
23
23
|
)
|
24
24
|
|
25
|
-
|
26
|
-
|
25
|
+
sat = settings.add_service_account_token(service_account, response.text)
|
26
|
+
print(f"Service account access token added: {sat.name}")
|
27
|
+
print("\033[93m This file contains secrets and must be kept safe")
|
27
28
|
|
28
|
-
if response.status_code == 200:
|
29
|
-
name = response.json()["name"]
|
30
|
-
write_sa_token(service_account, name, response.text)
|
31
|
-
else:
|
32
|
-
print(f"Got status code: {response.status_code}")
|
33
|
-
print(response.text)
|
34
29
|
|
35
|
-
|
36
|
-
|
37
|
-
def list_keys(
|
30
|
+
@app.command(name="list", help="List service account access tokens")
|
31
|
+
def list_tokens(
|
38
32
|
service_account: str = typer.Option(..., help="Service account name"),
|
39
33
|
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
40
34
|
) -> None:
|
41
35
|
Rest.handle_get(f"/api/project/{project}/admin/accounts/{service_account}/keys")
|
42
36
|
|
43
37
|
|
44
|
-
@app.command(name="list-files")
|
45
|
-
def list_files() -> None:
|
46
|
-
"""
|
47
|
-
List personal access token files in remotivelabs config directory
|
48
|
-
"""
|
49
|
-
sa_files = settings.list_service_account_token_files()
|
50
|
-
for file in sa_files:
|
51
|
-
print(file)
|
52
|
-
|
53
|
-
|
54
38
|
@app.command(name="revoke", help="Revoke service account access token")
|
55
39
|
def revoke(
|
56
40
|
name: str = typer.Argument(..., help="Access token name"),
|
@@ -58,10 +42,3 @@ def revoke(
|
|
58
42
|
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
59
43
|
) -> None:
|
60
44
|
Rest.handle_delete(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}")
|
61
|
-
|
62
|
-
|
63
|
-
# TODO: Move to settings # pylint: disable=W0511
|
64
|
-
def write_sa_token(service_account: str, name: str, token: str) -> Path:
|
65
|
-
file = f"service-account-{service_account}-{name}-token.json"
|
66
|
-
path = settings.CONFIG_DIR_PATH / file
|
67
|
-
return settings._write_settings_file(path, token) # pylint: disable=W0212
|
@@ -5,9 +5,9 @@ from typing_extensions import Annotated
|
|
5
5
|
|
6
6
|
from cli.cloud.rest_helper import RestHelper as Rest
|
7
7
|
from cli.cloud.storage.copy import copy
|
8
|
-
from cli.cloud.storage.uri import URI, InvalidURIError, JoinURIError
|
9
8
|
from cli.cloud.storage.uri_or_path import UriOrPath
|
10
9
|
from cli.cloud.storage.uri_or_path import uri as uri_parser
|
10
|
+
from cli.cloud.uri import URI, InvalidURIError, JoinURIError
|
11
11
|
from cli.errors import ErrorPrinter
|
12
12
|
|
13
13
|
HELP = """
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
5
5
|
|
6
6
|
from cli.cloud.rest_helper import RestHelper as Rest
|
7
7
|
from cli.cloud.resumable_upload import upload_signed_url
|
8
|
-
from cli.cloud.
|
8
|
+
from cli.cloud.uri import URI
|
9
9
|
|
10
10
|
_RCS_STORAGE_PATH = "/api/project/{project}/files/storage{path}"
|
11
11
|
|
@@ -29,7 +29,7 @@ def copy(project: str, source: URI | Path, dest: URI | Path, overwrite: bool = F
|
|
29
29
|
|
30
30
|
def _upload(source: Path, dest: URI, project: str, overwrite: bool = False) -> None:
|
31
31
|
if not source.exists():
|
32
|
-
raise FileNotFoundError(source)
|
32
|
+
raise FileNotFoundError(f"Source file does not exist: {source}")
|
33
33
|
|
34
34
|
files_to_upload = _list_files_for_upload(source, dest)
|
35
35
|
|
@@ -16,9 +16,9 @@ from rich import print as pretty_print
|
|
16
16
|
from rich.console import Console
|
17
17
|
from socketio.exceptions import ConnectionError as SocketIoConnectionError # type: ignore
|
18
18
|
|
19
|
-
from cli import settings
|
20
19
|
from cli.broker.lib.broker import SubscribableSignal
|
21
20
|
from cli.errors import ErrorPrinter
|
21
|
+
from cli.settings import settings
|
22
22
|
|
23
23
|
global PP_CONNECT_APP_NAME
|
24
24
|
PP_CONNECT_APP_NAME = "RemotiveBridge"
|
@@ -167,7 +167,7 @@ def do_connect(
|
|
167
167
|
if broker_url.startswith("https"):
|
168
168
|
if api_key is None:
|
169
169
|
print("No --api-key, reading token from file")
|
170
|
-
x_api_key = settings.
|
170
|
+
x_api_key = settings.get_active_token_secret()
|
171
171
|
else:
|
172
172
|
x_api_key = api_key
|
173
173
|
elif api_key is not None:
|
@@ -9,6 +9,7 @@ from typer.main import get_group
|
|
9
9
|
from .broker.brokers import app as broker_app
|
10
10
|
from .cloud.cloud_cli import app as cloud_app
|
11
11
|
from .connect.connect import app as connect_app
|
12
|
+
from .settings.cmd import app as settings_app
|
12
13
|
from .tools.tools import app as tools_app
|
13
14
|
|
14
15
|
if os.getenv("GRPC_VERBOSITY") is None:
|
@@ -63,5 +64,6 @@ app.add_typer(
|
|
63
64
|
name="cloud",
|
64
65
|
help="Manage resources in RemotiveCloud",
|
65
66
|
)
|
67
|
+
app.add_typer(settings_app, name="config", help="Manage access tokens")
|
66
68
|
app.add_typer(connect_app, name="connect", help="Integrations with other systems")
|
67
69
|
app.add_typer(tools_app, name="tools")
|
@@ -0,0 +1,5 @@
|
|
1
|
+
from cli.settings.cmd import app
|
2
|
+
from cli.settings.core import InvalidSettingsFilePathError, Settings, TokenNotFoundError, settings
|
3
|
+
from cli.settings.token_file import TokenFile
|
4
|
+
|
5
|
+
__all__ = ["app", "settings", "TokenFile", "TokenNotFoundError", "InvalidSettingsFilePathError", "Settings"]
|
@@ -0,0 +1,71 @@
|
|
1
|
+
import typer
|
2
|
+
|
3
|
+
from cli.errors import ErrorPrinter
|
4
|
+
from cli.settings.core import TokenNotFoundError, settings
|
5
|
+
|
6
|
+
app = typer.Typer()
|
7
|
+
|
8
|
+
|
9
|
+
@app.command()
|
10
|
+
def describe(file: str = typer.Argument(help="Token name or file path")) -> None:
|
11
|
+
"""
|
12
|
+
Show contents of specified access token file
|
13
|
+
"""
|
14
|
+
try:
|
15
|
+
print(settings.get_token_file(file))
|
16
|
+
except TokenNotFoundError:
|
17
|
+
ErrorPrinter.print_generic_error(f"Token file {file} not found")
|
18
|
+
|
19
|
+
|
20
|
+
@app.command()
|
21
|
+
def activate(file: str = typer.Argument(..., help="Token name or file path")) -> None:
|
22
|
+
"""
|
23
|
+
Activate a access token file to be used for authentication.
|
24
|
+
|
25
|
+
This will be used as the current access token in all subsequent requests. This would
|
26
|
+
be the same as login with a browser.
|
27
|
+
"""
|
28
|
+
try:
|
29
|
+
settings.activate_token(file)
|
30
|
+
except FileNotFoundError as e:
|
31
|
+
print(f"File could not be found: {e}")
|
32
|
+
|
33
|
+
|
34
|
+
@app.command(name="list-personal-tokens")
|
35
|
+
def list_pats() -> None:
|
36
|
+
"""
|
37
|
+
List personal access token files in remotivelabs config directory
|
38
|
+
"""
|
39
|
+
pats = settings.list_personal_tokens()
|
40
|
+
for pat in pats:
|
41
|
+
print(pat.name)
|
42
|
+
|
43
|
+
|
44
|
+
@app.command(name="list-personal-tokens-files")
|
45
|
+
def list_pats_files() -> None:
|
46
|
+
"""
|
47
|
+
List personal access token files in remotivelabs config directory
|
48
|
+
"""
|
49
|
+
personal_files = settings.list_personal_token_files()
|
50
|
+
for file in personal_files:
|
51
|
+
print(file)
|
52
|
+
|
53
|
+
|
54
|
+
@app.command(name="list-service-account-tokens")
|
55
|
+
def list_sats() -> None:
|
56
|
+
"""
|
57
|
+
List service account access token files in remotivelabs config directory
|
58
|
+
"""
|
59
|
+
sats = settings.list_service_account_tokens()
|
60
|
+
for sat in sats:
|
61
|
+
print(sat.name)
|
62
|
+
|
63
|
+
|
64
|
+
@app.command(name="list-service-account-tokens-files")
|
65
|
+
def list_sats_files() -> None:
|
66
|
+
"""
|
67
|
+
List service account access token files in remotivelabs config directory
|
68
|
+
"""
|
69
|
+
service_account_files = settings.list_service_account_token_files()
|
70
|
+
for file in service_account_files:
|
71
|
+
print(file)
|
@@ -0,0 +1,261 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import datetime
|
4
|
+
import shutil
|
5
|
+
import sys
|
6
|
+
from json import JSONDecodeError
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Tuple
|
9
|
+
|
10
|
+
from rich.console import Console
|
11
|
+
|
12
|
+
from cli.settings import token_file as tf
|
13
|
+
from cli.settings.token_file import TokenFile
|
14
|
+
|
15
|
+
err_console = Console(stderr=True)
|
16
|
+
|
17
|
+
|
18
|
+
CONFIG_DIR_PATH = Path.home() / ".config" / "remotive"
|
19
|
+
INCORRECT_CONFIG_DIR_PATH = Path.home() / ".config" / ".remotive"
|
20
|
+
DEPRECATED_CONFIG_DIR_PATH = Path.home() / ".remotive"
|
21
|
+
|
22
|
+
ACTIVE_TOKEN_FILE_NAME = "cloud.secret.token"
|
23
|
+
PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
|
24
|
+
SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
|
25
|
+
|
26
|
+
|
27
|
+
TokenFileMetadata = Tuple[TokenFile, Path]
|
28
|
+
|
29
|
+
|
30
|
+
class InvalidSettingsFilePathError(Exception):
|
31
|
+
"""Raised when trying to access an invalid settings file or file path"""
|
32
|
+
|
33
|
+
|
34
|
+
class TokenNotFoundError(Exception):
|
35
|
+
"""Raised when a token cannot be found in settings"""
|
36
|
+
|
37
|
+
|
38
|
+
class Settings:
|
39
|
+
"""
|
40
|
+
Settings for the remotive CLI
|
41
|
+
"""
|
42
|
+
|
43
|
+
config_dir: Path
|
44
|
+
|
45
|
+
def __init__(self, config_dir: Path, deprecated_config_dirs: list[Path] | None = None) -> None:
|
46
|
+
self.config_dir = config_dir
|
47
|
+
self._active_secret_token_path = self.config_dir / ACTIVE_TOKEN_FILE_NAME
|
48
|
+
|
49
|
+
# no migration of deprecated config dirs if the new config dir already exists
|
50
|
+
if self.config_dir.exists():
|
51
|
+
return
|
52
|
+
|
53
|
+
# create the config dir and try to migrate legacy config dirs if they exist
|
54
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
55
|
+
if deprecated_config_dirs:
|
56
|
+
for deprecated_config_dir in deprecated_config_dirs:
|
57
|
+
self._migrate_legacy_config_dir(deprecated_config_dir)
|
58
|
+
|
59
|
+
def get_active_token(self) -> str:
|
60
|
+
"""
|
61
|
+
Get the current active token secret
|
62
|
+
"""
|
63
|
+
token_file = self.get_active_token_file()
|
64
|
+
return token_file.token
|
65
|
+
|
66
|
+
def get_active_token_file(self) -> TokenFile:
|
67
|
+
"""
|
68
|
+
Get the current active token file
|
69
|
+
"""
|
70
|
+
if not self._active_secret_token_path.exists():
|
71
|
+
raise TokenNotFoundError("no active token file found")
|
72
|
+
|
73
|
+
return self._read_token_file(self._active_secret_token_path)
|
74
|
+
|
75
|
+
def activate_token(self, name: str) -> None:
|
76
|
+
"""
|
77
|
+
Activate a token by name or path
|
78
|
+
|
79
|
+
The token secret will be set as the current active secret.
|
80
|
+
"""
|
81
|
+
token_file = self.get_token_file(name)
|
82
|
+
self._write_token_file(self._active_secret_token_path, token_file)
|
83
|
+
|
84
|
+
def clear_active_token(self) -> None:
|
85
|
+
"""
|
86
|
+
Clear the current active token
|
87
|
+
"""
|
88
|
+
self._active_secret_token_path.unlink(missing_ok=True)
|
89
|
+
|
90
|
+
def get_token_file(self, name: str) -> TokenFile:
|
91
|
+
"""
|
92
|
+
Get a token file by name or path
|
93
|
+
"""
|
94
|
+
if Path(name).exists():
|
95
|
+
return self._read_token_file(Path(name))
|
96
|
+
|
97
|
+
return self._get_token_by_name(name)[0]
|
98
|
+
|
99
|
+
def remove_token_file(self, name: str) -> None:
|
100
|
+
"""
|
101
|
+
Remove a token file by name or path
|
102
|
+
|
103
|
+
TODO: what about manually downloaded tokens?
|
104
|
+
"""
|
105
|
+
if Path(name).exists():
|
106
|
+
if self.config_dir not in Path(name).parents:
|
107
|
+
raise InvalidSettingsFilePathError(f"cannot remove a token file not located in settings dir {self.config_dir}")
|
108
|
+
return Path(name).unlink()
|
109
|
+
|
110
|
+
# TODO: what about the active token? # pylint: disable=fixme
|
111
|
+
|
112
|
+
path = self._get_token_by_name(name)[1]
|
113
|
+
return path.unlink()
|
114
|
+
|
115
|
+
def add_and_activate_short_lived_cli_token(self, token: str) -> TokenFile:
|
116
|
+
"""
|
117
|
+
Activates a short lived token
|
118
|
+
"""
|
119
|
+
token_file = tf.loads(token)
|
120
|
+
self._write_token_file(self._active_secret_token_path, token_file)
|
121
|
+
return token_file
|
122
|
+
|
123
|
+
def add_personal_token(
|
124
|
+
self,
|
125
|
+
token: str,
|
126
|
+
activate: bool = False,
|
127
|
+
overwrite_if_exists: bool = False,
|
128
|
+
) -> TokenFile:
|
129
|
+
"""
|
130
|
+
Add a personal token
|
131
|
+
"""
|
132
|
+
token_file = tf.loads(token)
|
133
|
+
|
134
|
+
file = f"{PERSONAL_TOKEN_FILE_PREFIX}{token_file.name}.json"
|
135
|
+
path = self.config_dir / file
|
136
|
+
if path.exists() and not overwrite_if_exists:
|
137
|
+
raise FileExistsError(f"Token file already exists: {path}")
|
138
|
+
|
139
|
+
self._write_token_file(path, token_file)
|
140
|
+
|
141
|
+
if activate:
|
142
|
+
self.activate_token(token_file.name)
|
143
|
+
|
144
|
+
return token_file
|
145
|
+
|
146
|
+
def list_personal_tokens(self) -> list[TokenFile]:
|
147
|
+
"""
|
148
|
+
List all personal tokens
|
149
|
+
"""
|
150
|
+
return [f[0] for f in self._list_personal_tokens()]
|
151
|
+
|
152
|
+
def list_personal_token_files(self) -> list[Path]:
|
153
|
+
"""
|
154
|
+
List paths to all personal token files
|
155
|
+
"""
|
156
|
+
return [f[1] for f in self._list_personal_tokens()]
|
157
|
+
|
158
|
+
def add_service_account_token(self, service_account: str, token: str) -> TokenFile:
|
159
|
+
"""
|
160
|
+
Add a service account token to the config directory
|
161
|
+
"""
|
162
|
+
token_file = tf.loads(token)
|
163
|
+
|
164
|
+
file = f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{service_account}-{token_file.name}.json"
|
165
|
+
path = self.config_dir / file
|
166
|
+
if path.exists():
|
167
|
+
raise FileExistsError(f"Token file already exists: {path}")
|
168
|
+
|
169
|
+
self._write_token_file(path, token_file)
|
170
|
+
return token_file
|
171
|
+
|
172
|
+
def list_service_account_tokens(self) -> list[TokenFile]:
|
173
|
+
"""
|
174
|
+
List all service account tokens
|
175
|
+
"""
|
176
|
+
return [f[0] for f in self._list_service_account_tokens()]
|
177
|
+
|
178
|
+
def list_service_account_token_files(self) -> list[Path]:
|
179
|
+
"""
|
180
|
+
List paths to all service account token files
|
181
|
+
"""
|
182
|
+
return [f[1] for f in self._list_service_account_tokens()]
|
183
|
+
|
184
|
+
def _list_personal_tokens(self) -> list[TokenFileMetadata]:
|
185
|
+
return self._list_token_files(prefix=PERSONAL_TOKEN_FILE_PREFIX)
|
186
|
+
|
187
|
+
def _list_service_account_tokens(self) -> list[TokenFileMetadata]:
|
188
|
+
return self._list_token_files(prefix=SERVICE_ACCOUNT_TOKEN_FILE_PREFIX)
|
189
|
+
|
190
|
+
def _get_token_by_name(self, name: str) -> TokenFileMetadata:
|
191
|
+
token_files = self._list_token_files()
|
192
|
+
matches = [token_file for token_file in token_files if token_file[0].name == name]
|
193
|
+
if len(matches) != 1:
|
194
|
+
raise TokenNotFoundError(f"Ambiguous token file name {name}, found {len(matches)} files")
|
195
|
+
return matches[0]
|
196
|
+
|
197
|
+
def _list_token_files(self, prefix: str = "") -> list[TokenFileMetadata]:
|
198
|
+
# list all tokens with the correct prefix in the config dir, but omit the special active token file
|
199
|
+
def is_path_prefixed_and_not_active_secret(path: Path) -> bool:
|
200
|
+
has_correct_prefix = path.is_file() and path.name.startswith(prefix)
|
201
|
+
is_active_secret = path == self._active_secret_token_path
|
202
|
+
return has_correct_prefix and not is_active_secret
|
203
|
+
|
204
|
+
paths = [path for path in self.config_dir.iterdir() if is_path_prefixed_and_not_active_secret(path)]
|
205
|
+
|
206
|
+
return [(self._read_token_file(token_file), token_file) for token_file in paths]
|
207
|
+
|
208
|
+
def _read_token_file(self, path: Path) -> TokenFile:
|
209
|
+
data = self._read_file(path)
|
210
|
+
return tf.loads(data)
|
211
|
+
|
212
|
+
def _read_file(self, path: Path) -> str:
|
213
|
+
if not path.exists():
|
214
|
+
raise TokenNotFoundError(f"File could not be found: {path}")
|
215
|
+
return path.read_text(encoding="utf-8")
|
216
|
+
|
217
|
+
def _write_token_file(self, path: Path, token: TokenFile) -> Path:
|
218
|
+
data = tf.dumps(token)
|
219
|
+
return self._write_file(path, data)
|
220
|
+
|
221
|
+
def _write_file(self, path: Path, data: str) -> Path:
|
222
|
+
if self.config_dir not in path.parents:
|
223
|
+
raise InvalidSettingsFilePathError(f"file {path} not in settings dir {self.config_dir}")
|
224
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
225
|
+
path.write_text(data, encoding="utf8")
|
226
|
+
return path
|
227
|
+
|
228
|
+
def _migrate_legacy_config_dir(self, path: Path) -> None:
|
229
|
+
if not path.exists():
|
230
|
+
return
|
231
|
+
|
232
|
+
sys.stderr.write(f"migrating deprecated config directory {path} to {self.config_dir}\n")
|
233
|
+
shutil.copytree(str(path), str(self.config_dir), dirs_exist_ok=True)
|
234
|
+
secret = path / ACTIVE_TOKEN_FILE_NAME
|
235
|
+
if secret.exists():
|
236
|
+
value = secret.read_text(encoding="utf-8").strip()
|
237
|
+
# The existing token file might either be a token file, or simply a string. We handle both cases...
|
238
|
+
try:
|
239
|
+
token = tf.loads(value)
|
240
|
+
except JSONDecodeError:
|
241
|
+
token = tf.TokenFile(
|
242
|
+
name="MigratedActiveToken",
|
243
|
+
token=value,
|
244
|
+
created=str(datetime.datetime.now().isoformat()),
|
245
|
+
expires="unknown",
|
246
|
+
)
|
247
|
+
self.add_and_activate_short_lived_cli_token(tf.dumps(token))
|
248
|
+
shutil.rmtree(str(path))
|
249
|
+
|
250
|
+
|
251
|
+
def create_settings() -> Settings:
|
252
|
+
"""Create remotive CLI config directory and return its settings instance"""
|
253
|
+
return Settings(CONFIG_DIR_PATH, deprecated_config_dirs=[DEPRECATED_CONFIG_DIR_PATH, INCORRECT_CONFIG_DIR_PATH])
|
254
|
+
|
255
|
+
|
256
|
+
settings = create_settings()
|
257
|
+
"""
|
258
|
+
Global/module-level settings instance. Module-level variables are only loaded once, at import time.
|
259
|
+
|
260
|
+
TODO: Migrate away from singleton instance
|
261
|
+
"""
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import dataclasses
|
4
|
+
import json
|
5
|
+
from dataclasses import dataclass
|
6
|
+
|
7
|
+
|
8
|
+
def loads(data: str) -> TokenFile:
|
9
|
+
d = json.loads(data)
|
10
|
+
return TokenFile(name=d["name"], token=d["token"], created=d["created"], expires=d["expires"])
|
11
|
+
|
12
|
+
|
13
|
+
def dumps(token: TokenFile) -> str:
|
14
|
+
return json.dumps(dataclasses.asdict(token), default=str)
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class TokenFile:
|
19
|
+
name: str
|
20
|
+
token: str
|
21
|
+
created: str
|
22
|
+
expires: str
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "remotivelabs-cli"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.35"
|
4
4
|
description = "CLI for operating RemotiveCloud and RemotiveBroker"
|
5
5
|
authors = ["Johan Rask <johan.rask@remotivelabs.com>"]
|
6
6
|
readme = "README.md"
|
@@ -11,7 +11,7 @@ packages = [{ include = "cli" }]
|
|
11
11
|
remotive = "cli.remotive:app"
|
12
12
|
|
13
13
|
[tool.poetry.dependencies]
|
14
|
-
python = ">=3.8"
|
14
|
+
python = ">=3.8,<4"
|
15
15
|
trogon = ">=0.5.0"
|
16
16
|
typer = "0.12.5"
|
17
17
|
remotivelabs-broker = "~=0.1.17"
|
@@ -1,143 +0,0 @@
|
|
1
|
-
import json
|
2
|
-
import sys
|
3
|
-
from json.decoder import JSONDecodeError
|
4
|
-
from pathlib import Path
|
5
|
-
|
6
|
-
import typer
|
7
|
-
|
8
|
-
from cli import settings
|
9
|
-
|
10
|
-
from .rest_helper import RestHelper as Rest
|
11
|
-
|
12
|
-
app = typer.Typer()
|
13
|
-
|
14
|
-
|
15
|
-
@app.command(name="create", help="Create and download a new personal access token")
|
16
|
-
def get_personal_access_token(activate: bool = typer.Option(False, help="Activate the token for use after download")) -> None: # pylint: disable=W0621
|
17
|
-
Rest.ensure_auth_token()
|
18
|
-
response = Rest.handle_post(url="/api/me/keys", return_response=True)
|
19
|
-
|
20
|
-
if response is None:
|
21
|
-
return
|
22
|
-
|
23
|
-
if response.status_code == 200:
|
24
|
-
name = response.json()["name"]
|
25
|
-
pat_path = write_personal_token(name, response.text)
|
26
|
-
print(f"Personal access token written to {pat_path}")
|
27
|
-
if not activate:
|
28
|
-
print(f"Use 'remotive cloud auth tokens activate {pat_path.name}' to use this access token from cli")
|
29
|
-
else:
|
30
|
-
do_activate(str(pat_path))
|
31
|
-
print("Token file activated and ready for use")
|
32
|
-
print("\033[93m This file contains secrets and must be kept safe")
|
33
|
-
else:
|
34
|
-
print(f"Got status code: {response.status_code}")
|
35
|
-
print(response.text)
|
36
|
-
|
37
|
-
|
38
|
-
@app.command(name="list", help="List personal access tokens")
|
39
|
-
def list_personal_access_tokens() -> None:
|
40
|
-
Rest.ensure_auth_token()
|
41
|
-
Rest.handle_get("/api/me/keys")
|
42
|
-
|
43
|
-
|
44
|
-
@app.command(name="revoke")
|
45
|
-
def revoke(name_or_file: str = typer.Argument(help="Name or file path of the access token to revoke")) -> None:
|
46
|
-
"""
|
47
|
-
Revoke an access token by token name or path to a file containing that token
|
48
|
-
|
49
|
-
Name is found in the json file
|
50
|
-
```
|
51
|
-
{
|
52
|
-
"expires": "2034-07-31",
|
53
|
-
"token": "xxx",
|
54
|
-
"created": "2024-07-31T09:18:50.406+02:00",
|
55
|
-
"name": "token_name"
|
56
|
-
}
|
57
|
-
```
|
58
|
-
"""
|
59
|
-
name = name_or_file
|
60
|
-
if "." in name_or_file:
|
61
|
-
json_str = read_file(name_or_file)
|
62
|
-
try:
|
63
|
-
name = json.loads(json_str)["name"]
|
64
|
-
except JSONDecodeError:
|
65
|
-
sys.stderr.write("Failed to parse json, make sure its a correct access token file\n")
|
66
|
-
sys.exit(1)
|
67
|
-
except KeyError:
|
68
|
-
sys.stderr.write("Json does not contain a name property, make sure its a correct access token file\n")
|
69
|
-
sys.exit(1)
|
70
|
-
Rest.ensure_auth_token()
|
71
|
-
Rest.handle_delete(f"/api/me/keys/{name}")
|
72
|
-
|
73
|
-
|
74
|
-
@app.command()
|
75
|
-
def describe(file: str = typer.Argument(help="File name")) -> None:
|
76
|
-
"""
|
77
|
-
Show contents of specified access token file
|
78
|
-
"""
|
79
|
-
print(read_file(file))
|
80
|
-
|
81
|
-
|
82
|
-
@app.command()
|
83
|
-
def activate(file: str = typer.Argument(..., help="File name")) -> None:
|
84
|
-
"""
|
85
|
-
Activate a access token file to be used for authentication.
|
86
|
-
|
87
|
-
--file
|
88
|
-
|
89
|
-
This will be used as the current access token in all subsequent requests. This would
|
90
|
-
be the same as login with a browser.
|
91
|
-
"""
|
92
|
-
do_activate(file)
|
93
|
-
|
94
|
-
|
95
|
-
# TODO: Move parts of this to settings # pylint: disable=W0511
|
96
|
-
def do_activate(file: str) -> None:
|
97
|
-
# Best effort to read file
|
98
|
-
if Path(file).exists():
|
99
|
-
token_file = json.loads(read_file_with_path(Path(file)))
|
100
|
-
settings.write_secret_token(token_file["token"])
|
101
|
-
elif (settings.CONFIG_DIR_PATH / file).exists():
|
102
|
-
token_file = json.loads(read_file(file))
|
103
|
-
settings.write_secret_token(token_file["token"])
|
104
|
-
else:
|
105
|
-
sys.stderr.write("File could not be found \n")
|
106
|
-
|
107
|
-
|
108
|
-
@app.command(name="list-files")
|
109
|
-
def list_files() -> None:
|
110
|
-
"""
|
111
|
-
List personal access token files in remotivelabs config directory
|
112
|
-
"""
|
113
|
-
personal_files = settings.list_personal_token_files()
|
114
|
-
for file in personal_files:
|
115
|
-
print(file)
|
116
|
-
|
117
|
-
|
118
|
-
# TODO: Move to settings # pylint: disable=W0511
|
119
|
-
def read_file(file: str) -> str:
|
120
|
-
"""
|
121
|
-
Reads a file using file path or if that does not exist check in config directory
|
122
|
-
"""
|
123
|
-
path = Path(file)
|
124
|
-
if not path.exists():
|
125
|
-
path = settings.CONFIG_DIR_PATH / file
|
126
|
-
if not path.exists():
|
127
|
-
sys.stderr.write(f"Failed to find file using {file} or {path}\n")
|
128
|
-
sys.exit(1)
|
129
|
-
|
130
|
-
return read_file_with_path(path)
|
131
|
-
|
132
|
-
|
133
|
-
# TODO: Move to settings # pylint: disable=W0511
|
134
|
-
def read_file_with_path(path: Path) -> str:
|
135
|
-
with open(path, "r", encoding="utf8") as f:
|
136
|
-
return f.read()
|
137
|
-
|
138
|
-
|
139
|
-
# TODO: Move to settings # pylint: disable=W0511
|
140
|
-
def write_personal_token(name: str, token: str) -> Path:
|
141
|
-
file = f"personal-token-{name}.json"
|
142
|
-
path = settings.CONFIG_DIR_PATH / file
|
143
|
-
return settings._write_settings_file(path, token) # pylint: disable=W0212
|
@@ -1,64 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import sys
|
4
|
-
from pathlib import Path
|
5
|
-
|
6
|
-
from rich.console import Console
|
7
|
-
|
8
|
-
err_console = Console(stderr=True)
|
9
|
-
|
10
|
-
# pylint: disable-next=W0511
|
11
|
-
# TODO: We probably want this to be both configurable, and testable. The best solution would probably be to refactor this module into a
|
12
|
-
# proper class, and configure it similar to logging.
|
13
|
-
CONFIG_DIR_PATH = Path.home() / ".config" / ".remotive/"
|
14
|
-
TOKEN_SECRET_FILE_PATH = CONFIG_DIR_PATH / "cloud.secret.token"
|
15
|
-
|
16
|
-
|
17
|
-
class InvalidSettingsFileError(Exception):
|
18
|
-
"""Raised when trying to access an invalid settings file or file path"""
|
19
|
-
|
20
|
-
|
21
|
-
def read_secret_token() -> str:
|
22
|
-
if not TOKEN_SECRET_FILE_PATH.exists():
|
23
|
-
err_console.print(":boom: [bold red]Access failed[/bold red] - No access token found")
|
24
|
-
err_console.print("Login with [italic]remotive cloud auth login[/italic]")
|
25
|
-
err_console.print(
|
26
|
-
"If you have downloaded a personal access token, you can activate "
|
27
|
-
"it with [italic]remotive cloud auth tokens activate [FILE_NAME][/italic]"
|
28
|
-
)
|
29
|
-
sys.exit(1)
|
30
|
-
|
31
|
-
return _read_file(TOKEN_SECRET_FILE_PATH)
|
32
|
-
|
33
|
-
|
34
|
-
def list_personal_token_files() -> list[Path]:
|
35
|
-
return [f for f in CONFIG_DIR_PATH.iterdir() if f.is_file() and f.name.startswith("personal-")]
|
36
|
-
|
37
|
-
|
38
|
-
def list_service_account_token_files() -> list[Path]:
|
39
|
-
return [f for f in CONFIG_DIR_PATH.iterdir() if f.is_file() and f.name.startswith("service-account-")]
|
40
|
-
|
41
|
-
|
42
|
-
def write_secret_token(secret: str) -> Path:
|
43
|
-
return _write_settings_file(TOKEN_SECRET_FILE_PATH, secret)
|
44
|
-
|
45
|
-
|
46
|
-
def clear_secret_token() -> None:
|
47
|
-
TOKEN_SECRET_FILE_PATH.unlink()
|
48
|
-
|
49
|
-
|
50
|
-
def _read_file(path: Path) -> str:
|
51
|
-
with open(path, "r", encoding="utf-8") as f:
|
52
|
-
return f.read()
|
53
|
-
|
54
|
-
|
55
|
-
def _write_settings_file(path: Path, data: str) -> Path:
|
56
|
-
if CONFIG_DIR_PATH not in path.parents:
|
57
|
-
raise InvalidSettingsFileError(f"file {path} not in settings dir {CONFIG_DIR_PATH}")
|
58
|
-
|
59
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
60
|
-
|
61
|
-
with open(path, "w", encoding="utf8") as f:
|
62
|
-
f.write(data)
|
63
|
-
|
64
|
-
return path
|
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
|