remotivelabs-cli 0.5.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. remotivelabs/cli/__init__.py +0 -0
  2. remotivelabs/cli/api/cloud/tokens.py +62 -0
  3. remotivelabs/cli/broker/__init__.py +33 -0
  4. remotivelabs/cli/broker/defaults.py +1 -0
  5. remotivelabs/cli/broker/discovery.py +43 -0
  6. remotivelabs/cli/broker/export.py +92 -0
  7. remotivelabs/cli/broker/files.py +119 -0
  8. remotivelabs/cli/broker/lib/__about__.py +4 -0
  9. remotivelabs/cli/broker/lib/broker.py +625 -0
  10. remotivelabs/cli/broker/lib/client.py +224 -0
  11. remotivelabs/cli/broker/lib/helper.py +277 -0
  12. remotivelabs/cli/broker/lib/signalcreator.py +196 -0
  13. remotivelabs/cli/broker/license_flows.py +167 -0
  14. remotivelabs/cli/broker/licenses.py +98 -0
  15. remotivelabs/cli/broker/playback.py +117 -0
  16. remotivelabs/cli/broker/record.py +41 -0
  17. remotivelabs/cli/broker/recording_session/__init__.py +3 -0
  18. remotivelabs/cli/broker/recording_session/client.py +67 -0
  19. remotivelabs/cli/broker/recording_session/cmd.py +254 -0
  20. remotivelabs/cli/broker/recording_session/time.py +49 -0
  21. remotivelabs/cli/broker/scripting.py +129 -0
  22. remotivelabs/cli/broker/signals.py +220 -0
  23. remotivelabs/cli/broker/version.py +31 -0
  24. remotivelabs/cli/cloud/__init__.py +17 -0
  25. remotivelabs/cli/cloud/auth/__init__.py +3 -0
  26. remotivelabs/cli/cloud/auth/cmd.py +128 -0
  27. remotivelabs/cli/cloud/auth/login.py +283 -0
  28. remotivelabs/cli/cloud/auth_tokens.py +149 -0
  29. remotivelabs/cli/cloud/brokers.py +109 -0
  30. remotivelabs/cli/cloud/configs.py +109 -0
  31. remotivelabs/cli/cloud/licenses/__init__.py +0 -0
  32. remotivelabs/cli/cloud/licenses/cmd.py +14 -0
  33. remotivelabs/cli/cloud/organisations.py +112 -0
  34. remotivelabs/cli/cloud/projects.py +44 -0
  35. remotivelabs/cli/cloud/recordings.py +580 -0
  36. remotivelabs/cli/cloud/recordings_playback.py +274 -0
  37. remotivelabs/cli/cloud/resumable_upload.py +87 -0
  38. remotivelabs/cli/cloud/sample_recordings.py +25 -0
  39. remotivelabs/cli/cloud/service_account_tokens.py +62 -0
  40. remotivelabs/cli/cloud/service_accounts.py +72 -0
  41. remotivelabs/cli/cloud/storage/__init__.py +5 -0
  42. remotivelabs/cli/cloud/storage/cmd.py +76 -0
  43. remotivelabs/cli/cloud/storage/copy.py +86 -0
  44. remotivelabs/cli/cloud/storage/uri_or_path.py +45 -0
  45. remotivelabs/cli/cloud/uri.py +113 -0
  46. remotivelabs/cli/connect/__init__.py +0 -0
  47. remotivelabs/cli/connect/connect.py +118 -0
  48. remotivelabs/cli/connect/protopie/protopie.py +185 -0
  49. remotivelabs/cli/py.typed +0 -0
  50. remotivelabs/cli/remotive.py +123 -0
  51. remotivelabs/cli/settings/__init__.py +20 -0
  52. remotivelabs/cli/settings/config_file.py +113 -0
  53. remotivelabs/cli/settings/core.py +333 -0
  54. remotivelabs/cli/settings/migration/__init__.py +0 -0
  55. remotivelabs/cli/settings/migration/migrate_all_token_files.py +80 -0
  56. remotivelabs/cli/settings/migration/migrate_config_file.py +64 -0
  57. remotivelabs/cli/settings/migration/migrate_legacy_dirs.py +50 -0
  58. remotivelabs/cli/settings/migration/migrate_token_file.py +52 -0
  59. remotivelabs/cli/settings/migration/migration_tools.py +38 -0
  60. remotivelabs/cli/settings/state_file.py +67 -0
  61. remotivelabs/cli/settings/token_file.py +128 -0
  62. remotivelabs/cli/tools/__init__.py +0 -0
  63. remotivelabs/cli/tools/can/__init__.py +0 -0
  64. remotivelabs/cli/tools/can/can.py +78 -0
  65. remotivelabs/cli/tools/tools.py +9 -0
  66. remotivelabs/cli/topology/__init__.py +28 -0
  67. remotivelabs/cli/topology/all.py +322 -0
  68. remotivelabs/cli/topology/cli/__init__.py +3 -0
  69. remotivelabs/cli/topology/cli/run_in_docker.py +58 -0
  70. remotivelabs/cli/topology/cli/topology_cli.py +16 -0
  71. remotivelabs/cli/topology/cmd.py +130 -0
  72. remotivelabs/cli/topology/start_trial.py +134 -0
  73. remotivelabs/cli/typer/__init__.py +0 -0
  74. remotivelabs/cli/typer/typer_utils.py +27 -0
  75. remotivelabs/cli/utils/__init__.py +0 -0
  76. remotivelabs/cli/utils/console.py +99 -0
  77. remotivelabs/cli/utils/rest_helper.py +369 -0
  78. remotivelabs/cli/utils/time.py +11 -0
  79. remotivelabs/cli/utils/versions.py +120 -0
  80. remotivelabs_cli-0.5.0a1.dist-info/METADATA +51 -0
  81. remotivelabs_cli-0.5.0a1.dist-info/RECORD +84 -0
  82. remotivelabs_cli-0.5.0a1.dist-info/WHEEL +4 -0
  83. remotivelabs_cli-0.5.0a1.dist-info/entry_points.txt +3 -0
  84. remotivelabs_cli-0.5.0a1.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,274 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import json
5
+ import os
6
+ import sys
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any, List, Union
10
+
11
+ import grpc
12
+ import typer
13
+ from rich.progress import Progress, SpinnerColumn, TextColumn
14
+
15
+ from remotivelabs.cli.broker.lib.broker import Broker, SubscribableSignal
16
+ from remotivelabs.cli.typer import typer_utils
17
+ from remotivelabs.cli.utils.console import print_generic_error, print_generic_message, print_grpc_error, print_hint, print_unformatted
18
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
19
+
20
+ app = typer_utils.create_typer(
21
+ help="""
22
+ Support for playback of a recording on a cloud broker, make sure to always mount a recording first
23
+ """
24
+ )
25
+
26
+
27
+ @app.command()
28
+ def play(
29
+ recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
30
+ broker: str = typer.Option(None, help="Broker to use"),
31
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
32
+ show_progress: bool = typer.Option(False, help="Show progress after started playing"),
33
+ repeat: bool = typer.Option(False, help="Repeat recording - must keep command running in terminal"),
34
+ ) -> None:
35
+ """
36
+ Start playing a recording.
37
+ There is no problem invoking play multiple times since if it is already playing the command will be ignored.
38
+ Use --repeat to have the recording replayed when it reaches the end.
39
+ """
40
+
41
+ _do_change_playback_mode("play", recording_session, broker, project, progress_on_play=show_progress, repeat=repeat)
42
+
43
+
44
+ @app.command()
45
+ def pause(
46
+ recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
47
+ broker: str = typer.Option(None, help="Broker to use"),
48
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
49
+ ) -> None:
50
+ """
51
+ Pause a recording
52
+ """
53
+ _do_change_playback_mode("pause", recording_session, broker, project)
54
+
55
+
56
+ @app.command()
57
+ def progress(
58
+ recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
59
+ broker: str = typer.Option(None, help="Broker to use"),
60
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
61
+ ) -> None:
62
+ """
63
+ Shows progress of the recording playing.
64
+ Use --repeat to have the recording replayed when it reaches the end.
65
+ """
66
+ _do_change_playback_mode("status", recording_session, broker, project)
67
+
68
+
69
+ @app.command()
70
+ def seek(
71
+ recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
72
+ seconds: int = typer.Option(..., min=0, help="Target offset in seconds"),
73
+ broker: str = typer.Option(None, help="Broker to use"),
74
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
75
+ ) -> None:
76
+ """
77
+ Seek seconds into a recording
78
+ """
79
+ _do_change_playback_mode("seek", recording_session, broker, project, seconds)
80
+
81
+
82
+ @app.command()
83
+ def stop(
84
+ recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
85
+ broker: str = typer.Option(None, help="Broker to use"),
86
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
87
+ ) -> None:
88
+ """
89
+ Stop playing
90
+ """
91
+ _do_change_playback_mode("stop", recording_session, broker, project)
92
+
93
+
94
+ # Copied from signals.py
95
+ def read_scripted_code_file(file_path: Path) -> bytes:
96
+ # typer checks that the Path exists
97
+ with open(file_path, "rb") as file:
98
+ return file.read()
99
+
100
+
101
+ @app.command()
102
+ def subscribe( # noqa: PLR0913
103
+ recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
104
+ broker: str = typer.Option(None, help="Broker to use"),
105
+ signal: List[str] = typer.Option(None, help="Signal names to subscribe to, mandatory when not using script"),
106
+ script: Path = typer.Option(
107
+ None,
108
+ exists=True,
109
+ file_okay=True,
110
+ dir_okay=False,
111
+ writable=False,
112
+ readable=True,
113
+ resolve_path=True,
114
+ help="Supply a path to Lua script that to use for signal transformation",
115
+ ),
116
+ on_change_only: bool = typer.Option(default=False, help="Only get signal if value is changed"),
117
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
118
+ ) -> None:
119
+ """
120
+ Allows you to subscribe to signals based on a mounted recording without knowing the broker URL.
121
+ This simplifies when playing recordings from the cloud.
122
+
123
+ Terminal plotting is not yet supported here so we refer to remotive broker signals subscribe --x-plot for this.
124
+ """
125
+ if script is None:
126
+ if len(signal) == 0:
127
+ print_generic_error("You must use include at least one signal and one namespace or use script when subscribing")
128
+ sys.exit(1)
129
+ if script is not None:
130
+ if len(signal) > 0:
131
+ print_generic_error("You must must not specify --signal when using --script")
132
+ sys.exit(1)
133
+
134
+ broker_client = _get_broker_info(project, recording_session, broker, "subscribe")
135
+
136
+ try:
137
+ if script is not None:
138
+ script_src = read_scripted_code_file(script)
139
+ broker_client.subscribe_on_script(script_src, lambda sig: print_generic_message(json.dumps(list(sig))), on_change_only)
140
+ else:
141
+
142
+ def to_subscribable_signal(sig: str):
143
+ arr = sig.split(":")
144
+ if len(arr) != 2:
145
+ print_hint(f"--signal must have format namespace:signal ({sig})")
146
+ sys.exit(1)
147
+ return SubscribableSignal(namespace=arr[0], name=arr[1])
148
+
149
+ signals_to_subscribe_to = list(map(to_subscribable_signal, signal))
150
+ broker_client.long_name_subscribe(
151
+ signals_to_subscribe_to, lambda sig: print_generic_message(json.dumps(list(sig))), on_change_only
152
+ )
153
+ print_generic_message("Subscribing to signals, press Ctrl+C to exit")
154
+ except grpc.RpcError as rpc_error:
155
+ print_grpc_error(rpc_error)
156
+
157
+ except Exception as e:
158
+ print_generic_error(str(e))
159
+ sys.exit(1)
160
+
161
+
162
+ def _do_change_playback_mode( # noqa: C901, PLR0913, PLR0912
163
+ mode: str,
164
+ recording_session: str,
165
+ brokerstr: str,
166
+ project: str,
167
+ seconds: int | None = None,
168
+ progress_on_play: bool = False,
169
+ repeat: bool = False,
170
+ ) -> None:
171
+ response = Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
172
+ if response is None:
173
+ return
174
+ r = json.loads(response.text)
175
+ enabled_recordings: List[Any] = [r for r in r["recordings"] if r["enabled"]]
176
+ files = [{"recording": rec["fileName"], "namespace": rec["metadata"]["namespace"]} for rec in enabled_recordings]
177
+
178
+ broker_name = brokerstr if brokerstr is not None else "personal"
179
+ response = Rest.handle_get(f"/api/project/{project}/brokers/{broker_name}", return_response=True, allow_status_codes=[404])
180
+ if response is None:
181
+ return
182
+ if response.status_code == 404:
183
+ broker_arg = ""
184
+ if brokerstr is not None:
185
+ broker_arg = f" --broker {brokerstr} --ensure-broker-started"
186
+ print_generic_error("You need to mount the recording before you play")
187
+ print_hint(f"remotive cloud recordings mount {recording_session}{broker_arg} --project {project}")
188
+ sys.exit(1)
189
+
190
+ broker_info = json.loads(response.text)
191
+ broker = Broker(broker_info["url"], None)
192
+
193
+ _verify_recording_on_broker(broker, recording_session, mode, project)
194
+
195
+ if mode == "pause":
196
+ broker.pause_play(files, True)
197
+ elif mode == "play":
198
+ broker.play(files, True)
199
+ if progress_on_play or repeat:
200
+ _track_progress(broker, repeat, files)
201
+ elif mode == "seek":
202
+ if seconds is not None:
203
+ broker.seek(files, int(seconds * 1000000), True)
204
+ else:
205
+ broker.seek(files, 0, True)
206
+ elif mode == "stop":
207
+ broker.seek(files, 0, True)
208
+ elif mode == "status":
209
+ _track_progress(broker, repeat, files)
210
+ else:
211
+ raise ValueError(f"Illegal command {mode}")
212
+
213
+
214
+ def _track_progress(broker: Broker, repeat: bool, files: List[Any]) -> None:
215
+ p = Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True)
216
+ t = p.add_task("label", total=1)
217
+ if repeat:
218
+ print_unformatted(":point_right: Keep this command running in terminal to keep the recording play with repeat")
219
+ with p:
220
+
221
+ def print_progress(offset: int, total: int, current_mode: str) -> None:
222
+ p.update(
223
+ t,
224
+ description=f"{(datetime.timedelta(seconds=offset))} / {(datetime.timedelta(seconds=total))} ({current_mode})",
225
+ )
226
+
227
+ broker.listen_on_playback(repeat, files, print_progress)
228
+
229
+
230
+ def _verify_recording_on_broker(broker: Broker, recording_session: str, mode: str, project: str) -> None:
231
+ try:
232
+ # Here we try to verify that we are operating on a recording that is mounted on the
233
+ # broker so we can verify this before we try playback and can also present some good
234
+ # error messages
235
+ tmp = os.path.join(tempfile.gettempdir(), os.urandom(24).hex())
236
+ broker.download(".cloud.context", tmp, True)
237
+ with open(tmp, "r", encoding="utf8") as f:
238
+ json_context = json.loads(f.read())
239
+ if json_context["recordingSessionId"] != recording_session:
240
+ print_generic_error(
241
+ f"The recording id mounted is '{json_context['recordingSessionId']}' "
242
+ f"which not the same as you are trying to {mode}, use cmd below to mount this recording"
243
+ )
244
+ print_hint(f"remotive cloud recordings mount {recording_session} --project {project}")
245
+ sys.exit(1)
246
+ except grpc.RpcError as rpc_error:
247
+ if rpc_error.code() == grpc.StatusCode.NOT_FOUND:
248
+ print_generic_error(f"You must use mount to prepare a recording before you can use {mode}")
249
+ print_hint(f"remotive cloud recordings mount {recording_session} --project {project}")
250
+ else:
251
+ print_grpc_error(rpc_error)
252
+ sys.exit(1)
253
+
254
+
255
+ def _get_broker_info(project: str, recording_session: str, broker: Union[str, None], mode: str) -> Broker:
256
+ # Verify it exists
257
+ Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
258
+
259
+ broker_name = broker if broker is not None else "personal"
260
+ response = Rest.handle_get(f"/api/project/{project}/brokers/{broker_name}", return_response=True, allow_status_codes=[404])
261
+ if response is None:
262
+ print_generic_error(f"No response from: /api/project/{project}/brokers/{broker_name}")
263
+ sys.exit(1)
264
+ if response.status_code == 404:
265
+ broker_arg = ""
266
+ if broker is not None:
267
+ broker_arg = f"--broker {broker} --ensure-broker-started"
268
+ print_generic_error("You need to mount the recording before you play")
269
+ print_hint(f"remotive cloud recordings mount {recording_session} {broker_arg} --project {project}")
270
+ sys.exit(1)
271
+ broker_info = json.loads(response.text)
272
+ broker_client = Broker(broker_info["url"], None)
273
+ _verify_recording_on_broker(broker_client, recording_session, mode, project)
274
+ return broker_client
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Dict
7
+
8
+ import requests
9
+ from rich.progress import wrap_file
10
+
11
+ from remotivelabs.cli.utils.console import print_generic_error, print_success
12
+
13
+
14
+ def __get_uploaded_bytes(upload_url: str) -> int:
15
+ headers = {"Content-Range": "bytes */*"}
16
+ response = requests.put(upload_url, headers=headers, timeout=60)
17
+ if response.status_code != 308:
18
+ raise ValueError(f"Failed to retrieve upload status: {response.status_code} {response.text}")
19
+
20
+ # Parse the Range header to get the last byte uploaded
21
+ range_header = response.headers.get("Range")
22
+ if range_header:
23
+ last_byte = int(range_header.split("-")[1])
24
+ return last_byte + 1
25
+ return 0
26
+
27
+
28
+ def with_resumable_upload_signed_url(signed_url: str, source_file_name: str, content_type: str) -> None:
29
+ """
30
+ Upload file to file storage with signed url and resumable upload.
31
+ Resumable upload will only work with the same URL and not if a new signed URL is requested with the
32
+ same object id.
33
+ :param content_type:
34
+ :param signed_url:
35
+ :param source_file_name:
36
+ :return:
37
+ """
38
+
39
+ file_size = os.path.getsize(source_file_name)
40
+ headers = {"x-goog-resumable": "start", "content-type": content_type}
41
+ response = requests.post(signed_url, headers=headers, timeout=60)
42
+ if response.status_code not in (200, 201, 308):
43
+ print_generic_error(f"Failed to upload file: {response.status_code} - {response.text}")
44
+ sys.exit(1)
45
+
46
+ upload_url = response.headers["Location"]
47
+
48
+ # Check how many bytes have already been uploaded
49
+ uploaded_bytes = __get_uploaded_bytes(upload_url)
50
+
51
+ # Upload the remaining file in chunks
52
+ # Not sure what a good chunk size is or if we even should have resumable uploads here, probably not..
53
+ chunk_size = 256 * 1024 * 10
54
+ # Upload the file in chunks
55
+ with open(source_file_name, "rb") as f:
56
+ with wrap_file(f, os.stat(source_file_name).st_size, description=f"Uploading {source_file_name}...") as file:
57
+ file.seek(uploaded_bytes) # Seek to the position of the last uploaded byte
58
+ for chunk_start in range(uploaded_bytes, file_size, chunk_size):
59
+ chunk_end = min(chunk_start + chunk_size, file_size) - 1
60
+ chunk = file.read(chunk_end - chunk_start + 1)
61
+ headers = {"Content-Range": f"bytes {chunk_start}-{chunk_end}/{file_size}"}
62
+ response = requests.put(upload_url, headers=headers, data=chunk, timeout=60)
63
+ if response.status_code not in (200, 201, 308):
64
+ print_generic_error(f"Failed to upload file: {response.status_code} - {response.text}")
65
+ sys.exit(1)
66
+
67
+ print_success(f"File {source_file_name} uploaded successfully.")
68
+
69
+
70
+ def upload_signed_url(signed_url: str, source_file_name: Path, headers: Dict[str, str]) -> None:
71
+ """
72
+ Upload file to file storage with signed url and resumable upload.
73
+ Resumable upload will only work with the same URL and not if a new signed URL is requested with the
74
+ same object id.
75
+ :param headers:
76
+ :param signed_url:
77
+ :param source_file_name:
78
+ :return:
79
+ """
80
+ with open(source_file_name, "rb") as file:
81
+ with wrap_file(file, os.stat(source_file_name).st_size, description=f"Uploading {source_file_name}...") as f:
82
+ response = requests.put(signed_url, headers=headers, timeout=60, data=f)
83
+ if response.status_code not in (200, 201, 308):
84
+ print_generic_error(f"Failed to upload file: {response.status_code} - {response.text}")
85
+ sys.exit(1)
86
+
87
+ print_success(f"File {source_file_name} uploaded successfully.")
@@ -0,0 +1,25 @@
1
+ import json
2
+
3
+ import typer
4
+
5
+ from remotivelabs.cli.typer import typer_utils
6
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
7
+
8
+ app = typer_utils.create_typer()
9
+
10
+
11
+ @app.command(name="import", help="Import sample recording into project")
12
+ def do_import(
13
+ recording_session: str = typer.Argument(..., help="Recording session id"),
14
+ project: str = typer.Option(..., help="Project to import sample recording into", envvar="REMOTIVE_CLOUD_PROJECT"),
15
+ ) -> None:
16
+ Rest.handle_post(url=f"/api/samples/files/recording/{recording_session}/copy", body=json.dumps({"projectUid": project}))
17
+
18
+
19
+ @app.command("list")
20
+ def list() -> None:
21
+ """
22
+ List available sample recordings
23
+ """
24
+
25
+ Rest.handle_get("/api/samples/files/recording")
@@ -0,0 +1,62 @@
1
+ import json
2
+
3
+ import typer
4
+
5
+ from remotivelabs.cli.settings import settings
6
+ from remotivelabs.cli.typer import typer_utils
7
+ from remotivelabs.cli.utils.console import print_generic_message, print_success
8
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
9
+
10
+ app = typer_utils.create_typer()
11
+
12
+
13
+ # TODO: add add interactive flag to set target directory
14
+ @app.command(name="create", help="Create and download a new service account access token")
15
+ def create(
16
+ expire_in_days: int = typer.Option(default=365, help="Number of this token is valid"),
17
+ service_account: str = typer.Option(..., help="Service account name"),
18
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
19
+ ) -> None:
20
+ response = Rest.handle_post(
21
+ url=f"/api/project/{project}/admin/accounts/{service_account}/keys",
22
+ return_response=True,
23
+ body=json.dumps({"daysUntilExpiry": expire_in_days}),
24
+ )
25
+
26
+ sat = settings.add_service_account_token(response.text)
27
+ print_success(f"Service account access token added: {sat.name}")
28
+ print_generic_message("This file contains secrets and must be kept safe")
29
+
30
+
31
+ @app.command(name="list", help="List service account access tokens")
32
+ def list_tokens(
33
+ service_account: str = typer.Option(..., help="Service account name"),
34
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
35
+ ) -> None:
36
+ Rest.handle_get(f"/api/project/{project}/admin/accounts/{service_account}/keys")
37
+
38
+
39
+ @app.command(name="describe", help="Describe service account access token")
40
+ def describe(
41
+ name: str = typer.Argument(..., help="Access token name"),
42
+ service_account: str = typer.Option(..., help="Service account name"),
43
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
44
+ ) -> None:
45
+ Rest.handle_get(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}")
46
+
47
+
48
+ @app.command(name="revoke", help="Revoke service account access token")
49
+ def revoke(
50
+ name: str = typer.Argument(..., help="Access token name"),
51
+ delete: bool = typer.Option(True, help="Also deletes the token after revocation"),
52
+ service_account: str = typer.Option(..., help="Service account name"),
53
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
54
+ ) -> None:
55
+ res = Rest.handle_get(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}", return_response=True)
56
+ if not res.json()["revoked"]:
57
+ Rest.handle_patch(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}/revoke", quiet=True)
58
+ if delete:
59
+ Rest.handle_delete(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}", quiet=True)
60
+ token_with_name = [token for token in settings.list_service_account_token_files() if token.name == name]
61
+ if len(token_with_name) > 0:
62
+ settings.remove_token_file(name)
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import List
5
+
6
+ import typer
7
+
8
+ from remotivelabs.cli.cloud import service_account_tokens
9
+ from remotivelabs.cli.typer import typer_utils
10
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
11
+
12
+ app = typer_utils.create_typer()
13
+
14
+
15
+ @app.command(name="list", help="List service-accounts")
16
+ def list_service_accounts(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
17
+ Rest.handle_get(f"/api/project/{project}/admin/accounts")
18
+
19
+
20
+ ROLES_DESCRIPTION = """
21
+ [bold]Supported roles [/bold]
22
+ project/admin - Full project support, view, edit, delete and manage users
23
+ project/user - View, edit, upload, delete but no admin
24
+ project/viewer - View only
25
+ project/storageCreator - Can upload to storage but not view, overwrite or delete
26
+ org/topologyRunner - Can start RemotiveTopology
27
+ """
28
+
29
+
30
+ @app.command(
31
+ name="create",
32
+ help=f"""
33
+ Create a new service account with one or more roles.
34
+
35
+ [bold]Must only use lowercase letters, digits, or dashes, and must not contain -- or end with a dash.[/bold]
36
+
37
+ remotive cloud service-accounts create --role project/user --role project/storageCreator
38
+ {ROLES_DESCRIPTION}
39
+ """,
40
+ )
41
+ def create_service_account(
42
+ name: str,
43
+ role: List[str] = typer.Option(..., help="Roles to apply"),
44
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
45
+ ) -> None:
46
+ data = {"name": name, "roles": role}
47
+ Rest.handle_post(url=f"/api/project/{project}/admin/accounts", body=json.dumps(data))
48
+
49
+
50
+ @app.command(
51
+ name="update",
52
+ help=f"""
53
+ Update an existing service account with one or more roles.
54
+
55
+ remotive cloud service-accounts update --role project/user --role project/storageCreator
56
+ {ROLES_DESCRIPTION}
57
+ """,
58
+ )
59
+ def update_service_account(
60
+ service_account: str = typer.Argument(..., help="Service account name"),
61
+ role: List[str] = typer.Option(..., help="Roles to apply"),
62
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
63
+ ) -> None:
64
+ Rest.handle_put(url=f"/api/project/{project}/admin/accounts/{service_account}", body=json.dumps({"roles": role}))
65
+
66
+
67
+ @app.command(name="delete", help="Delete service account")
68
+ def delete_service_account(name: str, project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
69
+ Rest.handle_delete(url=f"/api/project/{project}/admin/accounts/{name}")
70
+
71
+
72
+ app.add_typer(service_account_tokens.app, name="tokens", help="Manage project service account access tokens")
@@ -0,0 +1,5 @@
1
+ from remotivelabs.cli.cloud.storage.cmd import app
2
+ from remotivelabs.cli.cloud.storage.uri_or_path import UriOrPath
3
+ from remotivelabs.cli.cloud.uri import URI
4
+
5
+ __all__ = ["URI", "UriOrPath", "app"]
@@ -0,0 +1,76 @@
1
+ import sys
2
+
3
+ import typer
4
+ from typing_extensions import Annotated
5
+
6
+ from remotivelabs.cli.cloud.storage.copy import copy
7
+ from remotivelabs.cli.cloud.storage.uri_or_path import UriOrPath
8
+ from remotivelabs.cli.cloud.storage.uri_or_path import uri as uri_parser
9
+ from remotivelabs.cli.cloud.uri import URI, InvalidURIError, JoinURIError
10
+ from remotivelabs.cli.typer import typer_utils
11
+ from remotivelabs.cli.utils.console import print_hint
12
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
13
+
14
+ HELP = """
15
+ Manage files ([yellow]Beta feature not available for all customers[/yellow])
16
+
17
+ Copy file from local to remote storage and vice versa, list and delete files.
18
+ """
19
+
20
+ app = typer_utils.create_typer(rich_markup_mode="rich", help=HELP)
21
+
22
+
23
+ @app.command(name="ls")
24
+ def list_files(
25
+ project: Annotated[str, typer.Option(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT", show_default=False)],
26
+ uri: Annotated[URI, typer.Argument(help="Remote storage path", parser=URI)] = "rcs://", # type: ignore
27
+ ) -> None:
28
+ """
29
+ Listing remote files
30
+
31
+ This will list files and directories in project top level directory
32
+ remotive cloud storage ls rcs://
33
+
34
+ This will list all files and directories matching the path
35
+ remotive cloud storage ls rcs://fileOrDirectoryPrefix
36
+
37
+ This will list all files and directories in the specified directory
38
+ remotive cloud storage ls rcs://fileOrDirectory/
39
+ """
40
+ Rest.handle_get(f"/api/project/{project}/files/storage{uri.path}")
41
+
42
+
43
+ @app.command(name="rm")
44
+ def delete_file(
45
+ project: Annotated[str, typer.Option(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT", show_default=False)],
46
+ uri: Annotated[URI, typer.Argument(help="Remote storage path", parser=URI, show_default=False)],
47
+ ) -> None:
48
+ """
49
+ [red]Deletes[/red] a file from remote storage, this cannot be undone :fire:
50
+
51
+ [white]remotive cloud storage rm rcs://directory/filename[/white]
52
+ """
53
+ Rest.handle_delete(f"/api/project/{project}/files/storage{uri.path}")
54
+
55
+
56
+ @app.command(name="cp")
57
+ def copy_file(
58
+ project: Annotated[str, typer.Option(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT", show_default=False)],
59
+ source: Annotated[UriOrPath, typer.Argument(help="Remote or local path to source file", parser=uri_parser, show_default=False)],
60
+ dest: Annotated[UriOrPath, typer.Argument(help="Remote or local path to destination file", parser=uri_parser, show_default=False)],
61
+ overwrite: Annotated[bool, typer.Option(help="Overwrite existing file on RCS")] = False,
62
+ ) -> None:
63
+ """
64
+ Copies a file to or from remote storage
65
+
66
+ remotive cloud storage cp rcs://dir/filename .
67
+ remotive cloud storage cp rcs://dir/filename filename
68
+
69
+ remotive cloud storage cp filename rcs://dir/
70
+ remotive cloud storage cp filename rcs://dir/filename
71
+ """
72
+ try:
73
+ return copy(project=project, source=source.value, dest=dest.value, overwrite=overwrite)
74
+ except (InvalidURIError, JoinURIError, ValueError, FileNotFoundError, FileExistsError) as e:
75
+ print_hint(str(e))
76
+ sys.exit(1)