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,17 @@
1
+ from remotivelabs.cli.cloud import auth, brokers, configs, organisations, projects, recordings, sample_recordings, service_accounts, storage
2
+ from remotivelabs.cli.cloud.licenses.cmd import licenses
3
+ from remotivelabs.cli.typer import typer_utils
4
+
5
+ app = typer_utils.create_typer()
6
+
7
+ app.command(name="licenses", help="List licenses for an organization")(licenses)
8
+
9
+ app.add_typer(organisations.app, name="organizations", help="Manage organizations")
10
+ app.add_typer(projects.app, name="projects", help="Manage projects")
11
+ app.add_typer(auth.app, name="auth")
12
+ app.add_typer(brokers.app, name="brokers", help="Manage cloud broker lifecycle")
13
+ app.add_typer(recordings.app, name="recordings", help="Manage recordings")
14
+ app.add_typer(configs.app, name="signal-databases", help="Manage signal databases")
15
+ app.add_typer(storage.app, name="storage")
16
+ app.add_typer(service_accounts.app, name="service-accounts", help="Manage project service account keys")
17
+ app.add_typer(sample_recordings.app, name="samples", help="Manage sample recordings")
@@ -0,0 +1,3 @@
1
+ from remotivelabs.cli.cloud.auth.cmd import app
2
+
3
+ __all__ = ["app"]
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from remotivelabs.cli.cloud import auth_tokens
11
+ from remotivelabs.cli.cloud.auth.login import login as do_login
12
+ from remotivelabs.cli.settings import settings
13
+ from remotivelabs.cli.typer import typer_utils
14
+ from remotivelabs.cli.utils.console import print_generic_error, print_generic_message, print_success, print_unformatted
15
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
16
+
17
+ console = Console(stderr=False)
18
+
19
+ HELP = """
20
+ Manage how you authenticate with our cloud platform
21
+ """
22
+ app = typer_utils.create_typer(help=HELP)
23
+
24
+
25
+ @app.command(name="login")
26
+ def login(browser: bool = typer.Option(default=True, help="Does not automatically open browser, instead shows a link")) -> None:
27
+ """
28
+ Login to the cli using browser
29
+
30
+ If not able to open a browser it will show fallback to headless login and show a link that
31
+ users can copy into any browser when this is unsupported where running the cli - such as in docker,
32
+ virtual machine or ssh sessions.
33
+ """
34
+ do_login(headless=not browser)
35
+
36
+
37
+ @app.command()
38
+ def whoami() -> None:
39
+ """
40
+ Validates authentication and fetches your account information
41
+ """
42
+ Rest.handle_get("/api/whoami")
43
+
44
+
45
+ @app.command()
46
+ def print_access_token(
47
+ account: str = typer.Option(None, help="Email of the account you want to print access token for, defaults to active"),
48
+ ) -> None:
49
+ """
50
+ Print current active access token or the token for the specified account
51
+ """
52
+ if not account:
53
+ active_token = settings.get_active_token() or os.getenv("REMOTIVE_CLOUD_ACCESS_TOKEN", None)
54
+ if not active_token:
55
+ print_generic_error("You have no active account")
56
+ sys.exit(1)
57
+
58
+ print_generic_message(active_token)
59
+ return
60
+
61
+ accounts = settings.list_accounts()
62
+ if account not in accounts:
63
+ print_generic_error(f"No account for {account} was found")
64
+ sys.exit(1)
65
+
66
+ token_file_name = accounts[account].credentials_file
67
+ token_file = settings.get_token_file(token_file_name)
68
+ if not token_file:
69
+ print_generic_error(f"Token file for {account} could not be found")
70
+ sys.exit(1)
71
+
72
+ print_generic_message(token_file.token)
73
+
74
+
75
+ def print_access_token_file() -> None:
76
+ """
77
+ Print current active token and its metadata
78
+ """
79
+ active_token_file = settings.get_active_token_file()
80
+ if not active_token_file:
81
+ print_generic_error("You have no active account")
82
+ sys.exit(1)
83
+
84
+ print_generic_message(str(active_token_file))
85
+
86
+
87
+ @app.command(name="deactivate")
88
+ def deactivate() -> None:
89
+ """
90
+ Clears active account
91
+ """
92
+ settings.clear_active_account()
93
+ print_success("Account no longer active")
94
+
95
+
96
+ @app.command("activate")
97
+ def activate(token_name: str = typer.Argument(None, help="Name, filename or path to a credentials file")) -> None:
98
+ """
99
+ Set the active account
100
+ """
101
+ auth_tokens.do_activate(token_name)
102
+
103
+
104
+ @app.command(name="list")
105
+ def list() -> None:
106
+ """
107
+ Lists available credential files on filesystem
108
+
109
+ TODO: Support output format
110
+ """
111
+ accounts = settings.list_accounts()
112
+
113
+ table = Table("#", "Active", "Type", "Token", "Account", "Organization", "Created", "Expires")
114
+ for idx, (email, account) in enumerate(accounts.items(), start=1):
115
+ token_file = settings.get_token_file_by_email(email)
116
+ is_active = settings.is_active_account(email)
117
+
118
+ table.add_row(
119
+ f"[yellow]{idx}",
120
+ ":white_check_mark:" if is_active else "",
121
+ "unknown" if not token_file else "user" if token_file.type == "authorized_user" else "sa",
122
+ token_file.name if token_file else "",
123
+ f"[bold]{email}[/bold]",
124
+ account.default_organization if account.default_organization else "",
125
+ str(token_file.created) if token_file else "",
126
+ str(token_file.expires) if token_file else "",
127
+ )
128
+ print_unformatted(table)
@@ -0,0 +1,283 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import os
6
+ import secrets
7
+ import sys
8
+ import time
9
+ import webbrowser
10
+ from http.server import BaseHTTPRequestHandler, HTTPServer
11
+ from threading import Thread
12
+ from typing import Any, Optional, Tuple
13
+ from urllib.parse import parse_qs, urlparse
14
+
15
+ import typer
16
+ from typing_extensions import override
17
+
18
+ from remotivelabs.cli.cloud.auth_tokens import do_activate, prompt_to_set_org
19
+ from remotivelabs.cli.settings import settings
20
+ from remotivelabs.cli.settings.token_file import TokenFile
21
+ from remotivelabs.cli.utils.console import print_generic_error, print_newline, print_success, print_unformatted, print_url
22
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
23
+
24
+ httpd: HTTPServer
25
+
26
+
27
+ def generate_pkce_pair() -> Tuple[str, str]:
28
+ """
29
+ PKCE is used for all cli login flows, both headless and browser.
30
+ """
31
+ code_verifier_ = secrets.token_urlsafe(64) # High-entropy string
32
+ code_challenge_ = base64.urlsafe_b64encode(hashlib.sha256(code_verifier_.encode("ascii")).digest()).rstrip(b"=").decode("ascii")
33
+ return code_verifier_, code_challenge_
34
+
35
+
36
+ code_verifier, code_challenge = generate_pkce_pair()
37
+ state = secrets.token_urlsafe(16)
38
+
39
+ short_lived_token = None
40
+
41
+
42
+ class S(BaseHTTPRequestHandler):
43
+ def _set_response(self) -> None:
44
+ self.send_response(200)
45
+ self.send_header("Content-type", "text/html")
46
+ self.end_headers()
47
+
48
+ @override
49
+ def log_message(self, format: Any, *args: Any) -> None:
50
+ return
51
+
52
+ # Please do not change this into lowercase!
53
+ @override
54
+ def do_GET(self) -> None: # type: ignore # noqa: PLR0912
55
+ self._set_response()
56
+
57
+ parsed_url = urlparse(self.path)
58
+
59
+ # Get query parameters as a dict
60
+ query_params = parse_qs(parsed_url.query)
61
+
62
+ # Example: Get the value of the "error" parameter if it exists
63
+ error_value = query_params.get("error", [None])[0]
64
+ path = self.path
65
+ auth_code = path[1:] # Remotive /
66
+ time.sleep(1)
67
+ httpd.server_close()
68
+
69
+ killerthread = Thread(target=httpd.shutdown)
70
+ killerthread.start()
71
+ if error_value is None:
72
+ res = Rest.handle_get(
73
+ f"/api/open/token?code={auth_code}&code_verifier={code_verifier}",
74
+ return_response=True,
75
+ skip_access_token=True,
76
+ allow_status_codes=[401, 400],
77
+ )
78
+ if res.status_code != 200:
79
+ print_generic_error(
80
+ "Failed to fetch token. Please try again, if the problem persists please reach out to support@remotivelabs.com"
81
+ )
82
+ self.wfile.write(
83
+ "Failed to fetch token. Please try again, if the problem persists please reach out to support@remotivelabs.com".encode(
84
+ "utf-8"
85
+ )
86
+ )
87
+ sys.exit(1)
88
+
89
+ # TODO - This is written before we are done...
90
+ self.wfile.write(
91
+ """Successfully setup CLI, you may close this window now. Return to your terminal to continue""".encode("utf-8")
92
+ )
93
+ access_token = res.json()["access_token"]
94
+ global short_lived_token # noqa: PLW0603
95
+ short_lived_token = access_token
96
+
97
+ else:
98
+ if error_value == "no_consent":
99
+ self.wfile.write(
100
+ """
101
+ Authorization was cancelled.<br/>
102
+ To use RemotiveCLI, you need to grant access to your RemotiveCloud account.
103
+ <br/><br/>
104
+ Run `remotive cloud auth login` to try again.
105
+ """.encode("utf-8")
106
+ )
107
+ print_generic_error("You did not grant access to RemotiveCloud, login aborted")
108
+ elif error_value == "user_not_exists":
109
+ self.wfile.write(
110
+ """
111
+ It seems like you do not have an account at RemotiveCloud with that user<br/>
112
+ To use RemotiveCLI you must first sign up at <a href="https://cloud.remotivelabs.com">cloud.remotivelabs.com</a>
113
+ and approve our agreements.<br/>
114
+ <br/><br/>
115
+ Once you are signed up, Run `remotive cloud auth login` again.
116
+ """.encode("utf-8")
117
+ )
118
+ print_generic_error(
119
+ "To use RemotiveCLI you must first sign up at https://cloud.remotivelabs.com and approve our agreements"
120
+ )
121
+ else:
122
+ self.wfile.write(f"Unknown error {error_value}, please contact support@remotivelabs.com".encode("utf-8"))
123
+ print_generic_error(f"Unexpected error {error_value}, please contact support@remotivelabs.com")
124
+ sys.exit(1)
125
+
126
+
127
+ def prepare_local_webserver(server_class: type = HTTPServer, handler_class: type = S, port: Optional[int] = None) -> None:
128
+ if port is None:
129
+ env_val = os.getenv("REMOTIVE_LOGIN_CALLBACK_PORT" or "")
130
+ port = int(env_val) if env_val and env_val.isdigit() else 0
131
+
132
+ server_address = ("", port)
133
+ global httpd # noqa: PLW0603
134
+ httpd = server_class(server_address, handler_class)
135
+
136
+
137
+ def create_personal_token() -> None:
138
+ response = Rest.handle_post(
139
+ url="/api/me/keys",
140
+ return_response=True,
141
+ access_token=short_lived_token,
142
+ # TODO - add body with alias
143
+ )
144
+ token = response.json()
145
+ email = token["account"]["email"]
146
+ existing_file = settings.get_token_file_by_email(email=email)
147
+ if existing_file is not None:
148
+ res = Rest.handle_patch(
149
+ f"/api/me/keys/{existing_file.name}/revoke",
150
+ quiet=True,
151
+ access_token=short_lived_token,
152
+ allow_status_codes=[400, 404],
153
+ )
154
+ if res is not None and res.status_code == 204:
155
+ Rest.handle_delete(
156
+ f"/api/me/keys/{existing_file.name}",
157
+ quiet=True,
158
+ access_token=short_lived_token,
159
+ )
160
+ settings.remove_token_file(existing_file.name)
161
+
162
+ settings.add_personal_token(response.text, activate=True)
163
+
164
+ print_success("Logged in")
165
+
166
+
167
+ def _do_prompt_to_use_existing_credentials() -> Optional[TokenFile]:
168
+ token_files = settings.list_personal_token_files()
169
+ if len(token_files) > 0:
170
+ should_select_token = typer.confirm(
171
+ "You have credentials available already, would you like to choose one of these instead?", default=True
172
+ )
173
+ if should_select_token:
174
+ token = do_activate(token_name=None)
175
+ if token is not None:
176
+ return token
177
+ # TODO - fix so this is not needed
178
+ sys.exit(0)
179
+ return None
180
+
181
+
182
+ def login(headless: bool = False) -> bool: # noqa: C901, PLR0915
183
+ """
184
+ Initiate login
185
+ """
186
+
187
+ newly_activated_token_file = _do_prompt_to_use_existing_credentials()
188
+ if newly_activated_token_file:
189
+ return True
190
+
191
+ prepare_local_webserver()
192
+
193
+ def force_use_webserver_callback() -> bool:
194
+ env_val = os.getenv("REMOTIVE_LOGIN_FORCE_CALLBACK" or "no")
195
+ if env_val and env_val == "yes":
196
+ return True
197
+ return False
198
+
199
+ def login_with_callback_but_copy_url() -> None:
200
+ """
201
+ This will print a url the will trigger a callback later so the webserver must be up and running.
202
+ """
203
+ print_unformatted("Copy the following link in a browser to login to cloud, and complete the sign-in prompts:")
204
+ print_newline()
205
+
206
+ url = (
207
+ f"{Rest.get_base_frontend_url()}/login"
208
+ f"?state={state}"
209
+ f"&cli_version={Rest.get_cli_version()}"
210
+ f"&response_type=code"
211
+ f"&code_challenge={code_challenge}"
212
+ f"&redirect_uri=http://localhost:{httpd.server_address[1]}"
213
+ )
214
+ print_url(url)
215
+ httpd.serve_forever()
216
+
217
+ def login_headless() -> None:
218
+ """
219
+ Full headless, opens a browser and expects a auth code to be entered and exchanged for the token
220
+ """
221
+ print_unformatted("Copy the following link in a browser to login to cloud, and complete the sign-in prompts:")
222
+ print_newline()
223
+
224
+ url = (
225
+ f"{Rest.get_base_frontend_url()}/login"
226
+ f"?state={state}"
227
+ f"&cli_version={Rest.get_cli_version()}"
228
+ f"&response_type=code"
229
+ f"&code_challenge={code_challenge}"
230
+ )
231
+ print_url(url)
232
+
233
+ code = typer.prompt(
234
+ "Once finished, enter the verification code provided in your browser",
235
+ hide_input=False,
236
+ )
237
+ res = Rest.handle_get(
238
+ f"/api/open/token?code={code}&code_verifier={code_verifier}",
239
+ return_response=True,
240
+ skip_access_token=True,
241
+ allow_status_codes=[401],
242
+ )
243
+ if res.status_code == 401:
244
+ print_generic_error(
245
+ "Failed to fetch token. Please try again, if the problem persists please reach out to support@remotivelabs.com"
246
+ )
247
+ sys.exit(1)
248
+ access_token = res.json()["access_token"]
249
+ global short_lived_token # noqa: PLW0603
250
+ short_lived_token = access_token
251
+ create_personal_token()
252
+ prompt_to_set_org()
253
+
254
+ if headless and not force_use_webserver_callback():
255
+ login_headless()
256
+ elif headless and force_use_webserver_callback():
257
+ login_with_callback_but_copy_url()
258
+ else:
259
+ could_open = webbrowser.open_new_tab(
260
+ f"{Rest.get_base_frontend_url()}/login"
261
+ f"?state={state}"
262
+ f"&cli_version={Rest.get_cli_version()}"
263
+ f"&response_type=code"
264
+ f"&code_challenge={code_challenge}"
265
+ f"&redirect_uri=http://localhost:{httpd.server_address[1]}"
266
+ )
267
+
268
+ if not could_open:
269
+ print_generic_error(
270
+ "Could not open a browser on this machine, this is likely because you are in an environment where no browser is available"
271
+ )
272
+ print_newline()
273
+ if force_use_webserver_callback():
274
+ login_with_callback_but_copy_url()
275
+ create_personal_token()
276
+ else:
277
+ login_headless()
278
+ else:
279
+ httpd.serve_forever()
280
+ create_personal_token()
281
+ prompt_to_set_org()
282
+
283
+ return True
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional
4
+
5
+ import typer
6
+ from rich.table import Table
7
+
8
+ from remotivelabs.cli.cloud.organisations import do_select_default_org
9
+ from remotivelabs.cli.settings import settings
10
+ from remotivelabs.cli.settings.token_file import TokenFile
11
+ from remotivelabs.cli.utils.console import (
12
+ print_generic_error,
13
+ print_generic_message,
14
+ print_hint,
15
+ print_newline,
16
+ print_success,
17
+ print_unformatted,
18
+ )
19
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
20
+
21
+
22
+ def _prompt_choice( # noqa: C901
23
+ choices: List[TokenFile],
24
+ skip_prompt: bool = False,
25
+ info_message: Optional[str] = None,
26
+ ) -> Optional[TokenFile]:
27
+ accounts = settings.list_accounts()
28
+
29
+ table = Table("#", "Active", "Type", "Token ID", "Account", "Created", "Expires")
30
+
31
+ included_tokens: list[TokenFile] = []
32
+ excluded_tokens: list[TokenFile] = []
33
+
34
+ for token in choices:
35
+ account = accounts.get(token.account.email)
36
+ if account and account.credentials_file:
37
+ token_file = settings.get_token_file(account.credentials_file)
38
+ if token_file and token_file.name in (token.name or ""):
39
+ included_tokens.append(token)
40
+ else:
41
+ excluded_tokens.append(token)
42
+ else:
43
+ excluded_tokens.append(token)
44
+
45
+ if len(included_tokens) == 0:
46
+ return None
47
+
48
+ included_tokens.sort(key=lambda token: token.created, reverse=True)
49
+
50
+ active_token = settings.get_active_token_file()
51
+ active_token_index = None
52
+ for idx, choice in enumerate(included_tokens, start=1):
53
+ is_active = active_token is not None and active_token.name == choice.name
54
+ active_token_index = idx if is_active else active_token_index
55
+
56
+ table.add_row(
57
+ f"[yellow]{idx}",
58
+ ":white_check_mark:" if is_active else "",
59
+ "user" if choice.type == "authorized_user" else "sa",
60
+ choice.name,
61
+ f"[bold]{choice.account.email if choice.account else 'unknown'}[/bold]",
62
+ str(choice.created),
63
+ str(choice.expires),
64
+ )
65
+ print_unformatted(table)
66
+ print_unformatted(
67
+ ":point_right: To get the access token for your activated account, use [bold]remotive cloud auth print-access-token[/bold]"
68
+ )
69
+
70
+ if skip_prompt:
71
+ return None
72
+
73
+ print_newline()
74
+ if info_message:
75
+ print_generic_message(info_message)
76
+
77
+ selection = typer.prompt(
78
+ f"Enter the number(# 1-{len(included_tokens)}) of the account to select (q to quit)",
79
+ default=f"{active_token_index}" if active_token_index is not None else None,
80
+ )
81
+
82
+ if selection == "q":
83
+ return None
84
+ try:
85
+ index = int(selection) - 1
86
+ if 0 <= index < len(included_tokens):
87
+ return included_tokens[index]
88
+ raise ValueError
89
+ except ValueError:
90
+ typer.echo("Invalid choice, please try again")
91
+ return _prompt_choice(included_tokens, skip_prompt, info_message)
92
+
93
+
94
+ def prompt_to_set_org() -> None:
95
+ active_account = settings.get_active_account()
96
+ if active_account and not active_account.default_organization:
97
+ set_default_organisation = typer.confirm(
98
+ "You have not set a default organization\nWould you like to choose one now?",
99
+ abort=False,
100
+ default=True,
101
+ )
102
+ if set_default_organisation:
103
+ do_select_default_org(get=False)
104
+
105
+
106
+ def do_activate(token_name: Optional[str]) -> Optional[TokenFile]:
107
+ if token_name:
108
+ token_file = settings.get_token_file(token_name)
109
+ if not token_file:
110
+ print_generic_error(f"Token with filename or name {token_name} could not be found")
111
+ return None
112
+ return settings.activate_token(token_file)
113
+
114
+ token_files = settings.list_personal_token_files()
115
+ token_files.extend(settings.list_service_account_token_files())
116
+ if len(token_files) > 0:
117
+ token_selected = list_and_select_personal_token(include_service_accounts=True)
118
+ if token_selected is not None:
119
+ is_logged_in = Rest.has_access("/api/whoami")
120
+ if not is_logged_in:
121
+ print_generic_error("Could not access RemotiveCloud with selected token")
122
+ else:
123
+ print_success("Access to RemotiveCloud granted")
124
+ # Only select default if activate was done with selection and successful
125
+ # and not SA since SA cannot list available organizations
126
+ if token_selected.type == "authorized_user":
127
+ prompt_to_set_org()
128
+ return token_selected
129
+
130
+ print_hint("No credentials available, login to activate credentials")
131
+ return None
132
+
133
+
134
+ def list_and_select_personal_token(
135
+ skip_prompt: bool = False,
136
+ include_service_accounts: bool = False,
137
+ info_message: Optional[str] = None,
138
+ ) -> Optional[TokenFile]:
139
+ personal_tokens = settings.list_personal_token_files()
140
+
141
+ if include_service_accounts:
142
+ sa_tokens = settings.list_service_account_token_files()
143
+ personal_tokens.extend(sa_tokens)
144
+
145
+ selected_token = _prompt_choice(personal_tokens, skip_prompt=skip_prompt, info_message=info_message)
146
+ if selected_token is not None:
147
+ settings.activate_token(selected_token)
148
+
149
+ return selected_token
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import signal as os_signal
6
+ from typing import Any, Union
7
+
8
+ import requests
9
+ import typer
10
+ import websocket
11
+
12
+ from remotivelabs.cli.typer import typer_utils
13
+ from remotivelabs.cli.utils.console import print_generic_message
14
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
15
+
16
+ app = typer_utils.create_typer()
17
+
18
+
19
+ @app.command("list", help="Lists brokers in project")
20
+ def list(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
21
+ Rest.handle_get(f"/api/project/{project}/brokers")
22
+
23
+
24
+ @app.command("describe", help="Shows details about a specific broker")
25
+ def describe(name: str, project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
26
+ Rest.handle_get(f"/api/project/{project}/brokers/{name}")
27
+
28
+
29
+ @app.command("delete", help="Stops and deletes a broker from project")
30
+ def stop(name: str, project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
31
+ Rest.handle_delete(f"/api/project/{project}/brokers/{name}")
32
+
33
+
34
+ @app.command(help="Deletes your personal broker from project")
35
+ def delete_personal(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
36
+ Rest.handle_delete(f"/api/project/{project}/brokers/personal")
37
+
38
+
39
+ def do_start(name: str, project: str, api_key: str, tag: str, return_response: bool = False) -> Union[requests.Response, None]:
40
+ if tag == "":
41
+ tag_to_use = None
42
+ else:
43
+ tag_to_use = tag
44
+
45
+ if api_key == "":
46
+ body = {"size": "S", "tag": tag_to_use}
47
+ else:
48
+ body = {"size": "S", "apiKey": api_key, "tag": tag_to_use}
49
+ return Rest.handle_post(
50
+ f"/api/project/{project}/brokers/{name}",
51
+ body=json.dumps(body),
52
+ return_response=return_response,
53
+ progress_label=f"Starting {name}...",
54
+ )
55
+
56
+
57
+ @app.command(name="create", help="Create a broker in project")
58
+ def start(
59
+ name: str,
60
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
61
+ tag: str = typer.Option("", help="Optional specific tag/version"),
62
+ silent: bool = typer.Option(False, help="Optional specific tag/version"),
63
+ api_key: str = typer.Option("", help="Start with your own api-key"),
64
+ ) -> None:
65
+ do_start(name, project, api_key, tag, return_response=silent)
66
+
67
+
68
+ @app.command(name="logs")
69
+ def logs(
70
+ broker_name: str = typer.Argument(..., help="Broker name to see logs for"),
71
+ tail: bool = typer.Option(False, help="Tail the broker log"),
72
+ history_minutes: str = typer.Option(10, help="History in minutes minutes to fetch"),
73
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
74
+ ) -> None:
75
+ """
76
+ Exposes broker logs history or real-time tail of the broker.
77
+
78
+ When using --tail option, --history always skipped even if supplied
79
+ """
80
+
81
+ def exit_on_ctrlc(_sig: Any, _frame: Any) -> None:
82
+ wsapp.close()
83
+ os._exit(0)
84
+
85
+ os_signal.signal(os_signal.SIGINT, exit_on_ctrlc)
86
+
87
+ def on_message(_wsapp: Any, message: str) -> None:
88
+ # TODO: use log instead of print for debug information?
89
+ print_generic_message(message)
90
+
91
+ def on_error(_wsapp: Any, err: str) -> None:
92
+ # TODO: use log instead of print for debug information?
93
+ print_generic_message(f"Error encountered: {err}")
94
+
95
+ Rest.ensure_auth_token()
96
+ # This will work with both http -> ws and https -> wss
97
+ ws_url = Rest.get_base_url().replace("http", "ws")
98
+
99
+ if tail:
100
+ q = "?tail=yes"
101
+ elif history_minutes != "10":
102
+ q = f"?history={history_minutes}"
103
+ else:
104
+ q = ""
105
+
106
+ wsapp = websocket.WebSocketApp(
107
+ f"{ws_url}/api/project/{project}/brokers/{broker_name}/logs{q}", header=Rest.get_headers(), on_message=on_message, on_error=on_error
108
+ )
109
+ wsapp.run_forever()