snapctl 0.49.2__tar.gz → 0.49.3__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.

Potentially problematic release.


This version of snapctl might be problematic. Click here for more details.

Files changed (40) hide show
  1. {snapctl-0.49.2 → snapctl-0.49.3}/PKG-INFO +7 -2
  2. {snapctl-0.49.2 → snapctl-0.49.3}/README.md +6 -1
  3. {snapctl-0.49.2 → snapctl-0.49.3}/pyproject.toml +1 -1
  4. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/commands/byows.py +133 -70
  5. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/config/constants.py +3 -1
  6. snapctl-0.49.3/snapctl/data/releases/beta-0.49.3.mdx +7 -0
  7. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/main.py +3 -1
  8. {snapctl-0.49.2 → snapctl-0.49.3}/LICENSE +0 -0
  9. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/__init__.py +0 -0
  10. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/__main__.py +0 -0
  11. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/commands/__init__.py +0 -0
  12. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/commands/byogs.py +0 -0
  13. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/commands/byosnap.py +0 -0
  14. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/commands/game.py +0 -0
  15. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/commands/generate.py +0 -0
  16. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/commands/release_notes.py +0 -0
  17. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/commands/snapend.py +0 -0
  18. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/config/__init__.py +0 -0
  19. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/config/endpoints.py +0 -0
  20. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/config/hashes.py +0 -0
  21. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/__init__.py +0 -0
  22. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/profiles/__init__.py +0 -0
  23. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/profiles/snapser-byosnap-profile.json +0 -0
  24. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/profiles/snapser-byosnap-profile.yaml +0 -0
  25. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/profiles/snapser-byosnap-profile.yml +0 -0
  26. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/__init__.py +0 -0
  27. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/beta-0.46.0.mdx +0 -0
  28. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/beta-0.46.4.mdx +0 -0
  29. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/beta-0.47.0.mdx +0 -0
  30. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/beta-0.47.1.mdx +0 -0
  31. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/beta-0.47.2.mdx +0 -0
  32. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/beta-0.48.0.mdx +0 -0
  33. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/beta-0.49.0.mdx +0 -0
  34. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/beta-0.49.1.mdx +0 -0
  35. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/data/releases/beta-0.49.2.mdx +0 -0
  36. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/types/__init__.py +0 -0
  37. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/types/definitions.py +0 -0
  38. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/utils/__init__.py +0 -0
  39. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/utils/echo.py +0 -0
  40. {snapctl-0.49.2 → snapctl-0.49.3}/snapctl/utils/helper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: snapctl
3
- Version: 0.49.2
3
+ Version: 0.49.3
4
4
  Summary: Snapser CLI Tool
5
5
  Author: Ajinkya Apte
6
6
  Author-email: aj@snapser.com
@@ -34,10 +34,13 @@ Snapctl will be the best way for game studios to integrate Snapser into their bu
34
34
  1. Renamed SDK type `server` to `api-key` to be consistent with the Snapser Web app.
35
35
 
36
36
  ### Features
37
- 1. Added support for Bring your own Workstation commands.
37
+ 1. Added support for Bring your own Workstation commands. This enables local debugging for your BYOSnaps while clients continue to access your cloud Snapend APIs.
38
38
  ```bash
39
39
  snapctl byows attach --snapend-id $snapend_id --byosnap-id $byosnap_id --http-port $http_port
40
40
  ```
41
+ ```bash
42
+ snapctl byows reset --snapend-id $snapend_id
43
+ ```
41
44
 
42
45
  ## Requirements
43
46
  ### Python 3.X and Pip
@@ -845,4 +848,6 @@ snapctl generate credentials --category "ecr" --out-path $output_path
845
848
  | Error Code | Description |
846
849
  |------------|----------------------------------------------------------|
847
850
  | 95 | Generic byows error |
851
+ | 96 | Byows attach error |
852
+ | 97 | Byows reset error |
848
853
 
@@ -14,10 +14,13 @@ Snapctl will be the best way for game studios to integrate Snapser into their bu
14
14
  1. Renamed SDK type `server` to `api-key` to be consistent with the Snapser Web app.
15
15
 
16
16
  ### Features
17
- 1. Added support for Bring your own Workstation commands.
17
+ 1. Added support for Bring your own Workstation commands. This enables local debugging for your BYOSnaps while clients continue to access your cloud Snapend APIs.
18
18
  ```bash
19
19
  snapctl byows attach --snapend-id $snapend_id --byosnap-id $byosnap_id --http-port $http_port
20
20
  ```
21
+ ```bash
22
+ snapctl byows reset --snapend-id $snapend_id
23
+ ```
21
24
 
22
25
  ## Requirements
23
26
  ### Python 3.X and Pip
@@ -825,3 +828,5 @@ snapctl generate credentials --category "ecr" --out-path $output_path
825
828
  | Error Code | Description |
826
829
  |------------|----------------------------------------------------------|
827
830
  | 95 | Generic byows error |
831
+ | 96 | Byows attach error |
832
+ | 97 | Byows reset error |
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "snapctl"
3
- version = "0.49.2"
3
+ version = "0.49.3"
4
4
  description = "Snapser CLI Tool"
5
5
  authors = ["Ajinkya Apte <aj@snapser.com>"]
6
6
  readme = "README.md"
@@ -6,23 +6,26 @@ import platform
6
6
  import subprocess
7
7
  import shutil
8
8
  import time
9
- import json
10
- from pathlib import Path
9
+ import signal
10
+ import functools
11
+ import sys
11
12
  from typing import Union
12
13
  import threading
13
14
  import requests
14
15
  from requests.exceptions import RequestException, HTTPError
15
16
  from rich.progress import Progress, SpinnerColumn, TextColumn
16
17
  from snapctl.config.constants import SERVER_CALL_TIMEOUT, SNAPCTL_INPUT_ERROR, \
17
- SNAPCTL_BYOWS_GENERIC_ERROR, SNAPCTL_DEPENDENCY_MISSING
18
+ SNAPCTL_BYOWS_GENERIC_ERROR, SNAPCTL_DEPENDENCY_MISSING, \
19
+ SNAPCTL_BYOWS_ATTACH_ERROR, SNAPCTL_BYOWS_RESET_ERROR
18
20
  from snapctl.utils.helper import snapctl_error, snapctl_success, get_dot_snapser_dir
19
21
  from snapctl.utils.echo import info, warning
20
22
 
23
+
21
24
  class Byows:
22
25
  """
23
26
  CLI commands exposed for a Bring your own Workstation
24
27
  """
25
- SUBCOMMANDS = ['attach']
28
+ SUBCOMMANDS = ['attach', 'reset']
26
29
  SSH_FILE = 'id_snapser_byows_attach_ed25519'
27
30
  PORT_FORWARD_TTL = 3600 * 24 * 7
28
31
  BYOWS_ENV_FILE = 'byows_env_setup'
@@ -41,6 +44,7 @@ class Byows:
41
44
  self.byosnap_id: Union[str, None] = byosnap_id
42
45
  self.http_port: Union[int, None] = http_port
43
46
  self.grpc_port: Union[int, None] = grpc_port
47
+ self._ssh_process = None
44
48
  self.validate_input()
45
49
 
46
50
  def validate_input(self) -> None:
@@ -74,9 +78,14 @@ class Byows:
74
78
  snapctl_error(
75
79
  message="Missing Input. One of --http-port=$your_local_server_port or --grpc-port=$your_local_server_port is required.",
76
80
  code=SNAPCTL_INPUT_ERROR)
77
-
81
+ elif self.subcommand == 'reset':
82
+ if self.snapend_id is None or self.snapend_id == '':
83
+ snapctl_error(
84
+ message="Missing Input --snapend-id=$your_snapend_id",
85
+ code=SNAPCTL_INPUT_ERROR)
78
86
 
79
87
  # Static methods
88
+
80
89
  @staticmethod
81
90
  def stream_output(pipe):
82
91
  '''
@@ -174,6 +183,58 @@ class Byows:
174
183
  return f"HTTP Error {http_err.response.status_code}: {http_err.response.reason}\nResponse: {response.text.strip()}"
175
184
 
176
185
  # Private methods
186
+ def _terminate_ssh_process(self):
187
+ if hasattr(self, "_ssh_process") and self._ssh_process:
188
+ try:
189
+ self._ssh_process.terminate()
190
+ if self._ssh_process.poll() is None:
191
+ self._ssh_process.wait(timeout=5)
192
+ except subprocess.TimeoutExpired:
193
+ warning("SSH process did not shut down gracefully.")
194
+
195
+ def _cleanup_files(self):
196
+ try:
197
+ dot_snapser_dir = get_dot_snapser_dir()
198
+ id_file = dot_snapser_dir / Byows.SSH_FILE
199
+ pub_file = dot_snapser_dir / f"{Byows.SSH_FILE}.pub"
200
+ if id_file.exists():
201
+ id_file.unlink()
202
+ if pub_file.exists():
203
+ pub_file.unlink()
204
+ except Exception as e:
205
+ warning(f"Cleanup warning: Failed to delete SSH key files – {e}")
206
+
207
+ def _handle_signal(self, signum, frame):
208
+ print(f"\nReceived signal {signum}. Cleaning up...", flush=True)
209
+ self._terminate_ssh_process()
210
+ self._cleanup_files()
211
+ try:
212
+ url = f"{self.base_url}/v1/snapser-api/byows/snapends/" + \
213
+ f"{self.snapend_id}/snaps/{self.byosnap_id}/enabled"
214
+ res = requests.put(
215
+ url, headers={'api-key': self.api_key}, json=False, timeout=SERVER_CALL_TIMEOUT)
216
+ res.raise_for_status()
217
+ info('Forwarding disabled')
218
+ except HTTPError as http_err:
219
+ snapctl_error(
220
+ message=Byows._format_portal_http_error(
221
+ "Unable to disable BYOWs", http_err, res),
222
+ code=SNAPCTL_BYOWS_GENERIC_ERROR
223
+ )
224
+ except RequestException as req_err:
225
+ warning(
226
+ f"Response Status Code: {res.status_code}, Body: {res.text}")
227
+ snapctl_error(
228
+ message=f"Request Exception: Unable to disable BYOWs {req_err}",
229
+ code=SNAPCTL_BYOWS_GENERIC_ERROR
230
+ )
231
+ except Exception as e:
232
+ snapctl_error(
233
+ message=f"Unexpected error occurred: {e}",
234
+ code=SNAPCTL_BYOWS_GENERIC_ERROR
235
+ )
236
+ sys.exit(0)
237
+
177
238
  def _setup_port_forward(
178
239
  self, private_key, public_key, snapend_id, snap_id, port, reverse_port, reverse_grpc_port,
179
240
  incoming_http, incoming_grpc, outgoing_http, snap_ids, ssh_connect_addr) -> bool:
@@ -220,7 +281,7 @@ class Byows:
220
281
  ssh_command = [
221
282
  'ssh',
222
283
  '-q',
223
- '-4', # use IPv4
284
+ '-4', # use IPv4
224
285
  '-o', 'ServerAliveInterval=60',
225
286
  '-o', 'StrictHostKeyChecking=no',
226
287
  '-o', 'UserKnownHostsFile=/dev/null',
@@ -231,12 +292,13 @@ class Byows:
231
292
  '-R', f'{reverse_port}:localhost:{incoming_http}',
232
293
  ]
233
294
  if incoming_grpc:
234
- ssh_command += ['-R', f'{reverse_grpc_port}:localhost:{incoming_grpc}']
295
+ ssh_command += ['-R',
296
+ f'{reverse_grpc_port}:localhost:{incoming_grpc}']
235
297
  ssh_command += [ssh_host]
236
298
 
237
- process = None
299
+ # process = None
238
300
  try:
239
- process = subprocess.Popen(
301
+ self._ssh_process = subprocess.Popen(
240
302
  ssh_command,
241
303
  shell=False,
242
304
  stdout=subprocess.PIPE,
@@ -247,9 +309,9 @@ class Byows:
247
309
  timeout_seconds = 5
248
310
  start_time = time.time()
249
311
  while True:
250
- if process.poll() is not None:
312
+ if self._ssh_process.poll() is not None:
251
313
  # Process exited early — likely an error
252
- stderr_output = process.stderr.read().decode().strip()
314
+ stderr_output = self._ssh_process.stderr.read().decode().strip()
253
315
  if stderr_output:
254
316
  info(f"[SSH Error] {stderr_output}")
255
317
  else:
@@ -263,60 +325,16 @@ class Byows:
263
325
 
264
326
  # Start background thread to stream live stderr
265
327
  threading.Thread(target=Byows.stream_output, args=(
266
- process.stderr,), daemon=True).start()
328
+ self._ssh_process.stderr,), daemon=True).start()
267
329
 
268
330
  # Now block for the full tunnel lifetime
269
- process.wait(timeout=Byows.PORT_FORWARD_TTL)
331
+ self._ssh_process.wait(timeout=Byows.PORT_FORWARD_TTL)
270
332
  except KeyboardInterrupt:
271
- process.terminate()
272
- try:
273
- process.wait(timeout=5)
274
- except subprocess.TimeoutExpired:
275
- warning('SSH process did not shut down gracefully.')
276
-
277
- info('Shutting down port forward...')
278
-
279
- try:
280
- url = f"{self.base_url}/v1/snapser-api/byows/snapends/" + \
281
- f"{self.snapend_id}/snaps/{self.byosnap_id}/enabled"
282
- res = requests.put(
283
- url, headers={'api-key': self.api_key}, json=False, timeout=SERVER_CALL_TIMEOUT)
284
- res.raise_for_status()
285
- info('Forwarding disabled')
286
- except HTTPError as http_err:
287
- snapctl_error(
288
- message=Byows._format_portal_http_error(
289
- "Unable to disable BYOWs", http_err, res),
290
- code=SNAPCTL_BYOWS_GENERIC_ERROR
291
- )
292
- return False
293
- except RequestException as req_err:
294
- warning(
295
- f"Response Status Code: {res.status_code}, Body: {res.text}")
296
- snapctl_error(
297
- message=f"Request Exception: Unable to disable BYOWs {req_err}",
298
- code=SNAPCTL_BYOWS_GENERIC_ERROR
299
- )
300
- return False
301
- except Exception as e:
302
- snapctl_error(
303
- message=f"Unexpected error occurred: {e}",
304
- code=SNAPCTL_BYOWS_GENERIC_ERROR
305
- )
306
- return False
307
-
333
+ self._handle_signal(signal.SIGINT, None)
334
+ return False
308
335
  except Exception as e:
309
336
  print('Error running SSH command:', e)
310
337
  return False
311
- finally:
312
- # Cleanup the SSH key files
313
- # Only remove the files if they exist
314
- try:
315
- id_file.unlink()
316
- pub_file.unlink()
317
- except Exception as e:
318
- warning(
319
- f"Cleanup warning: Failed to delete SSH key files – {e}")
320
338
  return True
321
339
 
322
340
  # Commands
@@ -330,7 +348,8 @@ class Byows:
330
348
  transient=True,
331
349
  )
332
350
  progress.start()
333
- progress.add_task(description='Setting up BYOWs port forward...', total=None)
351
+ progress.add_task(
352
+ description='Setting up BYOWs port forward...', total=None)
334
353
  try:
335
354
  url = f"{self.base_url}/v1/snapser-api/byows/snapends/" + \
336
355
  f"{self.snapend_id}/snaps/{self.byosnap_id}"
@@ -347,7 +366,6 @@ class Byows:
347
366
  res.raise_for_status()
348
367
  response_json = res.json()
349
368
 
350
-
351
369
  if res.ok and 'workstationPort' in response_json and \
352
370
  'workstationReversePort' in response_json and \
353
371
  'snapendId' in response_json and \
@@ -364,6 +382,16 @@ class Byows:
364
382
 
365
383
  progress.stop()
366
384
 
385
+ info('Setting up signal handling')
386
+ # Set up signal handling for cleanup
387
+ signal.signal(signal.SIGINT, functools.partial(
388
+ self._handle_signal))
389
+ signal.signal(signal.SIGTERM, functools.partial(
390
+ self._handle_signal))
391
+ if hasattr(signal, "SIGBREAK"):
392
+ signal.signal(signal.SIGBREAK,
393
+ functools.partial(self._handle_signal))
394
+ # Set up port forward
367
395
  self._setup_port_forward(
368
396
  response_json['proxyPrivateKey'],
369
397
  response_json['proxyPublicKey'],
@@ -382,21 +410,56 @@ class Byows:
382
410
  message='complete', progress=progress)
383
411
  snapctl_error(
384
412
  message='Unable to setup port forward.',
385
- code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
413
+ code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
386
414
  except HTTPError as http_err:
387
415
  snapctl_error(
388
416
  message=Byows._format_portal_http_error(
389
417
  "Unable to setup port forward", http_err, res),
390
- code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
391
- snapctl_error(
392
- message=f"Server Error: Unable to setup port forward {http_err}",
393
- code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
418
+ code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
394
419
  except RequestException as e:
395
420
  snapctl_error(
396
421
  message=f"Exception: Unable to setup port forward {e}",
397
- code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
422
+ code=SNAPCTL_BYOWS_ATTACH_ERROR, progress=progress)
423
+ finally:
424
+ progress.stop()
425
+
426
+ def reset(self) -> bool:
427
+ """
428
+ BYOWs reset
429
+ """
430
+ progress = Progress(
431
+ SpinnerColumn(),
432
+ TextColumn("[progress.description]{task.description}"),
433
+ transient=True,
434
+ )
435
+ progress.start()
436
+ progress.add_task(
437
+ description='Resetting BYOWS for the Snapend...', total=None)
438
+ try:
439
+ url = f"{self.base_url}/v1/snapser-api/byows/snapends/" + \
440
+ f"{self.snapend_id}"
441
+ res = requests.delete(
442
+ url,
443
+ headers={'api-key': self.api_key},
444
+ timeout=SERVER_CALL_TIMEOUT,
445
+ json={
446
+ "snapend_id": self.snapend_id,
447
+ }
448
+ )
449
+ res.raise_for_status()
450
+ if res.ok:
451
+ return snapctl_success(
452
+ message='Reset complete', progress=progress)
453
+ snapctl_error(
454
+ message='Unable to reset BYOWS.',
455
+ code=SNAPCTL_BYOWS_RESET_ERROR, progress=progress)
456
+ except HTTPError as http_err:
457
+ snapctl_error(
458
+ message=f"Server Error: Unable to reset BYOWS {http_err}",
459
+ code=SNAPCTL_BYOWS_RESET_ERROR, progress=progress)
460
+ except RequestException as e:
461
+ snapctl_error(
462
+ message=f"Exception: Unable to reset BYOWS {e}",
463
+ code=SNAPCTL_BYOWS_RESET_ERROR, progress=progress)
398
464
  finally:
399
465
  progress.stop()
400
- snapctl_error(
401
- message='Failed to setup port forward.',
402
- code=SNAPCTL_BYOWS_GENERIC_ERROR, progress=progress)
@@ -3,7 +3,7 @@ Constants used by snapctl
3
3
  """
4
4
  COMPANY_NAME = 'Snapser'
5
5
  VERSION_PREFIX = 'beta-'
6
- VERSION = '0.49.2'
6
+ VERSION = '0.49.3'
7
7
  CONFIG_FILE_MAC = '~/.snapser/config'
8
8
  CONFIG_FILE_WIN = '%homepath%\\.snapser\\config'
9
9
 
@@ -111,3 +111,5 @@ SNAPCTL_GENERATE_CREDENTIALS_ERROR = 81
111
111
 
112
112
  # BYOWs Errors - 95 - 99
113
113
  SNAPCTL_BYOWS_GENERIC_ERROR = 95
114
+ SNAPCTL_BYOWS_ATTACH_ERROR = 96
115
+ SNAPCTL_BYOWS_RESET_ERROR = 97
@@ -0,0 +1,7 @@
1
+ ## beta-0.49.3
2
+ ##### May 13, 2025
3
+
4
+ ### Feature
5
+ #### Bring your own workstation (BYOW)
6
+ - Now supports a new subcommand `snapctl byows reset --snapend-id $snapendId` to reset the BYOW configuration. Useful when you want a clean slate.
7
+
@@ -110,6 +110,7 @@ def get_base_url(api_key: Union[str, None]) -> str:
110
110
  return END_POINTS['PLAYTEST']
111
111
  return END_POINTS['PROD']
112
112
 
113
+
113
114
  def get_base_snapend_url(api_key: Union[str, None]) -> str:
114
115
  """
115
116
  Returns the base url for snapend based on the api_key
@@ -124,6 +125,7 @@ def get_base_snapend_url(api_key: Union[str, None]) -> str:
124
125
  return GATEWAY_END_POINTS['SANDBOX']
125
126
  return GATEWAY_END_POINTS['LIVE']
126
127
 
128
+
127
129
  def validate_command_context(
128
130
  ctx: typer.Context,
129
131
  ):
@@ -692,7 +694,7 @@ def byows(
692
694
  # attach
693
695
  snapend_id: str = typer.Option(
694
696
  None, "--snapend-id",
695
- help=("(req: attach) Your Snapend Id")
697
+ help=("(req: attach, reset) Your Snapend Id")
696
698
  ),
697
699
  byosnap_id: str = typer.Option(
698
700
  None, "--byosnap-id",
File without changes
File without changes
File without changes
File without changes