request-vm-on-golem 0.1.53__py3-none-any.whl → 0.1.55__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: request-vm-on-golem
3
- Version: 0.1.53
3
+ Version: 0.1.55
4
4
  Summary: VM on Golem Requestor CLI - Create and manage virtual machines on the Golem Network
5
5
  Keywords: golem,vm,cloud,decentralized,cli
6
6
  Author: Phillip Jensen
@@ -1,8 +1,8 @@
1
1
  requestor/__init__.py,sha256=OqSUAh1uZBMx7GW0MoSMg967PVdmT8XdPJx3QYjwkak,116
2
2
  requestor/api/main.py,sha256=CTnaM7KyBtDwVlyclYbNDy-nGi5_xt9GTcGusRasDVY,2493
3
3
  requestor/cli/__init__.py,sha256=e3E4oEGxmGj-STPtFkQwg_qIWhR0JAiAQdw3G1hXciU,37
4
- requestor/cli/commands.py,sha256=5Nul0oxG_6n-EBqzQX3tO2N10uQpryyrmU9-HxFPovs,49539
5
- requestor/config.py,sha256=GPsr_NSj04MD40mH6xyVY5FB0ysFDwJk9FJ5h9OQuJ0,12716
4
+ requestor/cli/commands.py,sha256=Obr8Z1ZX3f6WThooIInlD7HqMl_pJmOqawzazLmhke0,51253
5
+ requestor/config.py,sha256=FMSRKdyo3nEdn62CRonnwrw4Hsy4VGcuvvfg60aORsI,12809
6
6
  requestor/data/deployments/l2.json,sha256=XTNN2C5LkBfp4YbDKdUKfWMdp1fKnfv8D3TgcwVWxtQ,249
7
7
  requestor/db/__init__.py,sha256=Gm5DfWls6uvCZZ3HGGnyRHswbUQdeA5OGN8yPwH0hc8,88
8
8
  requestor/db/sqlite.py,sha256=l5pWbx2qlHuar1N_a0B9tVnmumLJY1w5rp3yZ7jmsC0,4146
@@ -10,19 +10,19 @@ requestor/errors.py,sha256=wVpHBuYgQx5pTe_SamugfK-k768noikY1RxvPOjQGko,665
10
10
  requestor/payments/blockchain_service.py,sha256=CACvZH2ZstutX7f0L_PXl8K_V5WlIkxNYIaeJuhP5I0,7500
11
11
  requestor/payments/monitor.py,sha256=JtSnh2plFf-f8sJU-bkOpadhoK_R82_ULwkDRmBYSbc,6012
12
12
  requestor/provider/__init__.py,sha256=fmW23aYUVciF8-gmBZkG-PLhn22upmcDzdPfAOLHG6g,103
13
- requestor/provider/client.py,sha256=pfJymufYR13W4kfykHZSVvs6ikRUE5AdHp0W0DB17AE,4130
14
- requestor/run.py,sha256=GqOG6n34szt8Sp3SEqjRV6huWm737yCN6YnBqoiwI-U,1785
13
+ requestor/provider/client.py,sha256=bWj4sNQ8w4F2sSRcMlHbWVjeBLAflDhHxy0BNMqzj8s,5021
14
+ requestor/run.py,sha256=sR9GgylQWbYPc60wRr3rUpemlNrWqIPNFIJ8WCz6YwE,2120
15
15
  requestor/security/faucet.py,sha256=XF_13b66SKAaY0-40hNRcSgC8AZA4mD5gyXl3qaBLpQ,2320
16
16
  requestor/services/__init__.py,sha256=1qSn_6RMn0KB0A7LCnY2IW6_tC3HBQsdfkFeV-h94eM,172
17
17
  requestor/services/database_service.py,sha256=GlSrzzzd7PSYQJNup00sxkB-B2PMr1__04K8k5QSWvs,2996
18
- requestor/services/provider_service.py,sha256=UjCg6YOwhkclJ227gXfI5uCTinhBv6Jjfsgxq0x0ESo,15701
18
+ requestor/services/provider_service.py,sha256=XMZtdkrpEKiGg0iFW3v_cf3zu7BZcYp1wjaphxo2SCU,16250
19
19
  requestor/services/ssh_service.py,sha256=tcOCtk2SlB9Uuv-P2ghR22e7BJ9kigQh5b4zSGdPFns,4280
20
- requestor/services/vm_service.py,sha256=1EUypRbCykdQTVJf4gYiNzkZNk66T3df32Vc51HkSMI,7983
20
+ requestor/services/vm_service.py,sha256=e05OgMZwz3NcCiIwmz4EnB6joMK6S3G_RT3VajD92lw,9438
21
21
  requestor/ssh/__init__.py,sha256=hNgSqJ5s1_AwwxVRyFjUqh_LTBpI4Hmzq0F-f_wXN9g,119
22
22
  requestor/ssh/manager.py,sha256=3jQtbbK7CVC2yD1zCO88jGXh2fBcuv3CzWEqDLuaQVk,9758
23
- requestor/utils/logging.py,sha256=oFNpO8pJboYM8Wp7g3HOU4HFyBTKypVdY15lUiz1a4I,3721
24
- requestor/utils/spinner.py,sha256=PUHJdTD9jpUHur__01_qxXy87WFfNmjQbD_sLG-KlGo,2459
25
- request_vm_on_golem-0.1.53.dist-info/METADATA,sha256=4JOUW7vkkFVjBgNv1zKVLz54hY0xWI2yVJFPc8LSI3E,15780
26
- request_vm_on_golem-0.1.53.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
- request_vm_on_golem-0.1.53.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
28
- request_vm_on_golem-0.1.53.dist-info/RECORD,,
23
+ requestor/utils/logging.py,sha256=lgAswzYvO9M0EOET0cFZvuAsGI4lInh_wln_6bI-fJk,4281
24
+ requestor/utils/spinner.py,sha256=X0jfPfs5ricglTS4_XmacrM2Z1DDHR7zGk2KqYZDpXg,2541
25
+ request_vm_on_golem-0.1.55.dist-info/METADATA,sha256=wepR8dMN4ehyEx4zmQjjTl3fuXs_OdGGiBCZPn3ZZ8I,15780
26
+ request_vm_on_golem-0.1.55.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
+ request_vm_on_golem-0.1.55.dist-info/entry_points.txt,sha256=Z-skRNpJ8aZcIl_En9mEm1ygkp9FKy0bzQoL3zO52-0,44
28
+ request_vm_on_golem-0.1.55.dist-info/RECORD,,
requestor/cli/commands.py CHANGED
@@ -128,6 +128,7 @@ def vm():
128
128
  @click.option('--memory', type=int, help='Minimum memory (GB) required')
129
129
  @click.option('--storage', type=int, help='Minimum disk (GB) required')
130
130
  @click.option('--country', help='Preferred provider country')
131
+ @click.option('--platform', help='Preferred platform/arch (e.g., x86_64, arm64)')
131
132
  @click.option('--driver', type=click.Choice(['central', 'golem-base']), default=None, help='Discovery driver to use')
132
133
  @click.option('--payments-network', type=str, default=None, help='Filter by payments network profile (default: current config)')
133
134
  @click.option('--all-payments', is_flag=True, help='Do not filter by payments network (show all)')
@@ -135,13 +136,15 @@ def vm():
135
136
  @click.option('--network', type=click.Choice(['testnet', 'mainnet']), default=None,
136
137
  help='Override network filter for this command')
137
138
  @async_command
138
- async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Optional[int], country: Optional[str], driver: Optional[str], payments_network: Optional[str] = None, all_payments: bool = False, as_json: bool = False, network: Optional[str] = None):
139
+ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Optional[int], country: Optional[str], platform: Optional[str], driver: Optional[str], payments_network: Optional[str] = None, all_payments: bool = False, as_json: bool = False, network: Optional[str] = None):
139
140
  """List available providers matching requirements."""
140
141
  try:
142
+ if as_json:
143
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
141
144
  if network:
142
145
  config.network = network
143
146
  # Log search criteria if any
144
- if any([cpu, memory, storage, country]):
147
+ if any([cpu, memory, storage, country, platform]):
145
148
  logger.command("🔍 Searching for providers with criteria:")
146
149
  if cpu:
147
150
  logger.detail(f"CPU Cores: {cpu}+")
@@ -151,6 +154,8 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
151
154
  logger.detail(f"Disk: {storage}GB+")
152
155
  if country:
153
156
  logger.detail(f"Country: {country}")
157
+ if platform:
158
+ logger.detail(f"Platform: {platform}")
154
159
 
155
160
  # Determine the discovery driver being used
156
161
  discovery_driver = driver or config.discovery_driver
@@ -169,6 +174,7 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
169
174
  memory=memory,
170
175
  storage=storage,
171
176
  country=country,
177
+ platform=platform,
172
178
  driver=driver,
173
179
  payments_network=eff_pn,
174
180
  include_all_payments=bool(all_payments),
@@ -176,12 +182,15 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
176
182
  except TypeError:
177
183
  # Backward compatibility with older/dummy service stubs in tests
178
184
  providers = await provider_service.find_providers(
179
- cpu=cpu, memory=memory, storage=storage, country=country, driver=driver
185
+ cpu=cpu, memory=memory, storage=storage, country=country, platform=platform, driver=driver
180
186
  )
181
187
 
182
188
  if not providers:
183
189
  logger.warning("No providers found matching criteria")
184
- return {"providers": []}
190
+ result = {"providers": []}
191
+ if as_json:
192
+ click.echo(json.dumps(result, indent=2))
193
+ return result
185
194
 
186
195
  # If JSON requested and full spec provided, include estimates per provider
187
196
  if as_json and cpu and memory and storage:
@@ -216,6 +225,12 @@ async def list_providers(cpu: Optional[int], memory: Optional[int], storage: Opt
216
225
  except Exception as e:
217
226
  logger.error(f"Failed to list providers: {str(e)}")
218
227
  raise click.Abort()
228
+ finally:
229
+ if as_json:
230
+ try:
231
+ del os.environ["GOLEM_SILENCE_LOGS"]
232
+ except Exception:
233
+ pass
219
234
 
220
235
 
221
236
  @vm.command(name='create')
@@ -385,6 +400,8 @@ def vm_stream():
385
400
  async def stream_list(as_json: bool):
386
401
  """List payment stream status for all known VMs."""
387
402
  try:
403
+ if as_json:
404
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
388
405
  vms = await db_service.list_vms()
389
406
  if not vms:
390
407
  logger.warning("No VMs found in local database")
@@ -461,6 +478,12 @@ async def stream_list(as_json: bool):
461
478
  except Exception as e:
462
479
  logger.error(f"Failed to list streams: {e}")
463
480
  raise click.Abort()
481
+ finally:
482
+ if as_json:
483
+ try:
484
+ del os.environ["GOLEM_SILENCE_LOGS"]
485
+ except Exception:
486
+ pass
464
487
 
465
488
 
466
489
  @vm_stream.command('open')
@@ -550,6 +573,8 @@ async def stream_topup(stream_id: int, glm: float | None, hours: int | None):
550
573
  async def stream_status(name: str, as_json: bool):
551
574
  """Show the payment stream status for a VM by name."""
552
575
  try:
576
+ if as_json:
577
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
553
578
  # Resolve VM and provider
554
579
  vm = await db_service.get_vm(name)
555
580
  if not vm:
@@ -580,6 +605,12 @@ async def stream_status(name: str, as_json: bool):
580
605
  except Exception as e:
581
606
  logger.error(f"Failed to fetch stream status: {e}")
582
607
  raise click.Abort()
608
+ finally:
609
+ if as_json:
610
+ try:
611
+ del os.environ["GOLEM_SILENCE_LOGS"]
612
+ except Exception:
613
+ pass
583
614
 
584
615
 
585
616
  @vm_stream.command('inspect')
@@ -589,6 +620,8 @@ async def stream_status(name: str, as_json: bool):
589
620
  async def stream_inspect(stream_id: int, as_json: bool):
590
621
  """Inspect a stream directly on-chain (no provider required)."""
591
622
  try:
623
+ if as_json:
624
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
592
625
  from web3 import Web3
593
626
  from golem_streaming_abi import STREAM_PAYMENT_ABI
594
627
  w3 = Web3(Web3.HTTPProvider(config.polygon_rpc_url))
@@ -632,6 +665,12 @@ async def stream_inspect(stream_id: int, as_json: bool):
632
665
  except Exception as e:
633
666
  logger.error(f"Failed to inspect stream: {e}")
634
667
  raise click.Abort()
668
+ finally:
669
+ if as_json:
670
+ try:
671
+ del os.environ["GOLEM_SILENCE_LOGS"]
672
+ except Exception:
673
+ pass
635
674
 
636
675
 
637
676
  @cli.group()
@@ -726,6 +765,8 @@ def connect_vm(name: str):
726
765
  async def info_vm(name: str, as_json: bool):
727
766
  """Show information about a VM."""
728
767
  try:
768
+ if as_json:
769
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
729
770
  logger.command(f"ℹ️ Getting info for VM '{name}'")
730
771
 
731
772
  # Initialize VM service
@@ -767,6 +808,12 @@ async def info_vm(name: str, as_json: bool):
767
808
  except Exception as e:
768
809
  logger.error(f"Failed to get VM info: {str(e)}")
769
810
  raise click.Abort()
811
+ finally:
812
+ if as_json:
813
+ try:
814
+ del os.environ["GOLEM_SILENCE_LOGS"]
815
+ except Exception:
816
+ pass
770
817
 
771
818
 
772
819
  @vm.command(name='destroy')
@@ -1054,6 +1101,8 @@ def run_api_server(host: str, port: int, reload: bool):
1054
1101
  async def list_vms(as_json: bool):
1055
1102
  """List all VMs."""
1056
1103
  try:
1104
+ if as_json:
1105
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
1057
1106
  logger.command("📋 Listing your VMs")
1058
1107
  logger.process("Fetching VM details")
1059
1108
 
@@ -1086,7 +1135,6 @@ async def list_vms(as_json: bool):
1086
1135
  tablefmt="grid"
1087
1136
  ))
1088
1137
  click.echo("\n" + "─" * 60)
1089
-
1090
1138
  return result
1091
1139
 
1092
1140
  except Exception as e:
@@ -1096,6 +1144,13 @@ async def list_vms(as_json: bool):
1096
1144
  logger.error(f"Failed to list VMs: {error_msg}")
1097
1145
  raise click.Abort()
1098
1146
 
1147
+ finally:
1148
+ if as_json:
1149
+ try:
1150
+ del os.environ["GOLEM_SILENCE_LOGS"]
1151
+ except Exception:
1152
+ pass
1153
+
1099
1154
 
1100
1155
  def main():
1101
1156
  """Entry point for the CLI."""
requestor/config.py CHANGED
@@ -4,6 +4,7 @@ import os
4
4
  from pydantic_settings import BaseSettings, SettingsConfigDict
5
5
  from pydantic import Field, field_validator, ValidationInfo
6
6
  import os
7
+ import sys
7
8
 
8
9
 
9
10
  def ensure_config() -> None:
@@ -33,7 +34,8 @@ def ensure_config() -> None:
33
34
  created = True
34
35
 
35
36
  if created:
36
- print("Using default settings run with --help to customize")
37
+ # Write to stderr so stdout stays clean for JSON outputs
38
+ print("Using default settings – run with --help to customize", file=sys.stderr)
37
39
 
38
40
 
39
41
  ensure_config()
@@ -24,7 +24,7 @@ class ProviderClient:
24
24
  ssh_key: str,
25
25
  stream_id: int | None = None,
26
26
  ) -> Dict:
27
- """Create a VM on the provider."""
27
+ """Create a VM on the provider (async job semantics)."""
28
28
  payload = {
29
29
  "name": name,
30
30
  "resources": {
@@ -37,12 +37,29 @@ class ProviderClient:
37
37
  if stream_id is not None:
38
38
  payload["stream_id"] = int(stream_id)
39
39
  async with self.session.post(
40
- f"{self.provider_url}/api/v1/vms",
40
+ f"{self.provider_url}/api/v1/vms?async=true",
41
41
  json=payload
42
42
  ) as response:
43
43
  if not response.ok:
44
44
  error_text = await response.text()
45
45
  raise Exception(f"Failed to create VM: {error_text}")
46
+ data = await response.json()
47
+ # Normalize: support both old (VMInfo) and new (job) responses
48
+ # New shape: { job_id, vm_id, status }
49
+ # Old shape: { id, ... }
50
+ if isinstance(data, dict) and "job_id" in data:
51
+ return data
52
+ # Fallback: synthesize a job-like envelope from immediate VM info
53
+ vm_id = data.get("id") or data.get("name") or name
54
+ return {"job_id": "", "vm_id": vm_id, "status": data.get("status", "ready"), "_vm": data}
55
+
56
+ async def get_vm_info(self, vm_id: str) -> Dict:
57
+ async with self.session.get(
58
+ f"{self.provider_url}/api/v1/vms/{vm_id}"
59
+ ) as response:
60
+ if not response.ok:
61
+ error_text = await response.text()
62
+ raise Exception(f"Failed to get VM info: {error_text}")
46
63
  return await response.json()
47
64
 
48
65
  async def get_provider_info(self) -> Dict:
requestor/run.py CHANGED
@@ -4,6 +4,16 @@ import sys
4
4
  from pathlib import Path
5
5
  from dotenv import load_dotenv
6
6
 
7
+ if "--json" in sys.argv:
8
+ os.environ["GOLEM_SILENCE_LOGS"] = "1"
9
+ try:
10
+ import logging as _logging
11
+ _logging.getLogger().setLevel(_logging.CRITICAL)
12
+ _logging.getLogger('rlp').setLevel(_logging.CRITICAL)
13
+ _logging.getLogger('rlp.codec').setLevel(_logging.CRITICAL)
14
+ except Exception:
15
+ pass
16
+
7
17
  from requestor.utils.logging import setup_logger
8
18
 
9
19
  # Configure logging with debug mode from environment variable
@@ -66,6 +66,7 @@ class ProviderService:
66
66
  memory: Optional[int] = None,
67
67
  storage: Optional[int] = None,
68
68
  country: Optional[str] = None,
69
+ platform: Optional[str] = None,
69
70
  driver: Optional[str] = None,
70
71
  payments_network: Optional[str] = None,
71
72
  include_all_payments: bool = False,
@@ -82,12 +83,12 @@ class ProviderService:
82
83
  private_key=private_key_bytes,
83
84
  )
84
85
  return await self._find_providers_golem_base(
85
- cpu, memory, storage, country,
86
+ cpu, memory, storage, country, platform,
86
87
  payments_network=payments_network,
87
88
  include_all_payments=include_all_payments,
88
89
  )
89
90
  else:
90
- return await self._find_providers_central(cpu, memory, storage, country)
91
+ return await self._find_providers_central(cpu, memory, storage, country, platform)
91
92
 
92
93
  async def _find_providers_golem_base(
93
94
  self,
@@ -95,6 +96,7 @@ class ProviderService:
95
96
  memory: Optional[int] = None,
96
97
  storage: Optional[int] = None,
97
98
  country: Optional[str] = None,
99
+ platform: Optional[str] = None,
98
100
  payments_network: Optional[str] = None,
99
101
  include_all_payments: bool = False,
100
102
  ) -> List[Dict]:
@@ -124,6 +126,8 @@ class ProviderService:
124
126
  query += f' && golem_storage>={storage}'
125
127
  if country:
126
128
  query += f' && golem_country="{country}"'
129
+ if platform:
130
+ query += f' && golem_platform="{platform}"'
127
131
 
128
132
  results = await self.golem_base_client.query_entities(query)
129
133
 
@@ -142,6 +146,7 @@ class ProviderService:
142
146
  'provider_name': annotations.get('golem_provider_name'),
143
147
  'ip_address': annotations.get('golem_ip_address'),
144
148
  'country': annotations.get('golem_country'),
149
+ 'platform': annotations.get('golem_platform') or None,
145
150
  'payments_network': annotations.get('golem_payments_network'),
146
151
  'resources': {
147
152
  'cpu': int(annotations.get('golem_cpu', 0)),
@@ -171,7 +176,8 @@ class ProviderService:
171
176
  cpu: Optional[int] = None,
172
177
  memory: Optional[int] = None,
173
178
  storage: Optional[int] = None,
174
- country: Optional[str] = None
179
+ country: Optional[str] = None,
180
+ platform: Optional[str] = None
175
181
  ) -> List[Dict]:
176
182
  """Find providers using the central discovery service."""
177
183
  try:
@@ -181,7 +187,8 @@ class ProviderService:
181
187
  'cpu': cpu,
182
188
  'memory': memory,
183
189
  'storage': storage,
184
- 'country': country
190
+ 'country': country,
191
+ 'platform': platform,
185
192
  }.items() if v is not None
186
193
  }
187
194
 
@@ -328,6 +335,7 @@ class ProviderService:
328
335
  usd_storage if usd_storage is not None else '—',
329
336
  est_usd,
330
337
  est_glm,
338
+ (provider.get('platform') or '—'),
331
339
  updated_at_str
332
340
  ]
333
341
 
@@ -358,6 +366,10 @@ class ProviderService:
358
366
  # Format location info
359
367
  row[3] = style(f"🌍 {row[3]}", fg="green", bold=True)
360
368
 
369
+ # Platform column: dim label
370
+ if row[12] != '—':
371
+ row[12] = style(f"{row[12]}", fg="white")
372
+
361
373
  return row
362
374
 
363
375
  @property
@@ -376,5 +388,6 @@ class ProviderService:
376
388
  "USD/GB Disk/mo",
377
389
  "Est. $/mo",
378
390
  "Est. GLM/mo",
391
+ "Platform",
379
392
  "Updated"
380
393
  ]
@@ -39,8 +39,8 @@ class VMService:
39
39
  if existing_vm:
40
40
  raise VMError(f"VM with name '{name}' already exists")
41
41
 
42
- # Create VM on provider
43
- vm = await self.provider_client.create_vm(
42
+ # Create VM on provider (returns job envelope)
43
+ job = await self.provider_client.create_vm(
44
44
  name=name,
45
45
  cpu=cpu,
46
46
  memory=memory,
@@ -49,8 +49,41 @@ class VMService:
49
49
  stream_id=stream_id
50
50
  )
51
51
 
52
- # Get VM access info
53
- access_info = await self.provider_client.get_vm_access(vm['id'])
52
+ vm_id = job.get('vm_id') or name
53
+
54
+ # Save initial record with 'creating' status (no port yet)
55
+ await self.db.save_vm(
56
+ name=name,
57
+ provider_ip=provider_ip,
58
+ vm_id=vm_id,
59
+ config={
60
+ 'cpu': cpu,
61
+ 'memory': memory,
62
+ 'storage': storage,
63
+ **({"stream_id": stream_id} if stream_id is not None else {}),
64
+ },
65
+ status='creating'
66
+ )
67
+
68
+ # Poll provider until VM is ready, then fetch access info
69
+ import asyncio as _asyncio
70
+ deadline = _asyncio.get_event_loop().time() + 600.0 # 10 minutes max
71
+ last_status = 'creating'
72
+ while _asyncio.get_event_loop().time() < deadline:
73
+ try:
74
+ info = await self.provider_client.get_vm_info(vm_id)
75
+ last_status = (info.get('status') or '').lower() or last_status
76
+ if last_status == 'running':
77
+ break
78
+ except Exception:
79
+ # Best-effort: ignore transient errors during startup
80
+ pass
81
+ await _asyncio.sleep(2.0)
82
+ if last_status != 'running':
83
+ raise VMError(f"VM did not become ready in time (status={last_status})")
84
+
85
+ # Get VM access info (ssh port)
86
+ access_info = await self.provider_client.get_vm_access(vm_id)
54
87
 
55
88
  # Preserve any provided stream_id; do not auto-create streams here
56
89
  # Stream creation should be explicit via CLI `vm stream open` command.
@@ -67,7 +100,8 @@ class VMService:
67
100
  name=name,
68
101
  provider_ip=provider_ip,
69
102
  vm_id=access_info['vm_id'],
70
- config=config
103
+ config=config,
104
+ status='running'
71
105
  )
72
106
 
73
107
  return {
@@ -61,6 +61,8 @@ def setup_logger(name: Optional[str] = None) -> logging.Logger:
61
61
 
62
62
  # Check DEBUG environment variable
63
63
  debug = os.getenv('DEBUG', '').lower() in ('1', 'true', 'yes')
64
+ # Global silence switch for JSON/machine outputs
65
+ silence = os.getenv('GOLEM_SILENCE_LOGS', '').lower() in ('1', 'true', 'yes')
64
66
 
65
67
  # Prevent duplicate logs by removing root handlers
66
68
  root = logging.getLogger()
@@ -86,13 +88,22 @@ def setup_logger(name: Optional[str] = None) -> logging.Logger:
86
88
  style='%'
87
89
  )
88
90
  fancy_handler.setFormatter(fancy_formatter)
89
- fancy_handler.addFilter(
90
- lambda record: record.levelno != DEBUG or debug
91
- )
91
+ # Suppress DEBUG unless DEBUG=1; suppress everything if silence
92
+ def _filter(record: logging.LogRecord) -> bool:
93
+ if silence:
94
+ return False
95
+ return (record.levelno != DEBUG) or debug
96
+ fancy_handler.addFilter(_filter)
92
97
  logger.addHandler(fancy_handler)
93
98
  logger.propagate = False # Prevent propagation to avoid duplicates
94
99
 
95
- if debug:
100
+ if silence:
101
+ logger.setLevel(CRITICAL)
102
+ # Silence common libraries and root logger
103
+ logging.getLogger().setLevel(CRITICAL)
104
+ logging.getLogger('asyncio').setLevel(CRITICAL)
105
+ logging.getLogger('aiosqlite').setLevel(CRITICAL)
106
+ elif debug:
96
107
  logger.setLevel(DEBUG)
97
108
  # Enable debug logging for other libraries
98
109
  logging.getLogger('asyncio').setLevel(DEBUG)
@@ -11,26 +11,27 @@ class Spinner:
11
11
  self.busy = False
12
12
  self.spinner_visible = False
13
13
  self.message = message
14
- sys.stdout.write('\033[?25l') # Hide cursor
14
+ # Use stderr so stdout can remain machine-readable (e.g., --json outputs)
15
+ sys.stderr.write('\033[?25l') # Hide cursor
15
16
 
16
17
  def write_next(self):
17
18
  """Write the next spinner frame."""
18
19
  with self._screen_lock:
19
20
  if not self.spinner_visible:
20
- sys.stdout.write(f"\r{next(self.spinner)} {self.message}")
21
+ sys.stderr.write(f"\r{next(self.spinner)} {self.message}")
21
22
  self.spinner_visible = True
22
- sys.stdout.flush()
23
+ sys.stderr.flush()
23
24
 
24
25
  def remove_spinner(self, cleanup=False):
25
26
  """Remove the spinner from the terminal."""
26
27
  with self._screen_lock:
27
28
  if self.spinner_visible:
28
- sys.stdout.write('\r')
29
- sys.stdout.write(' ' * (len(self.message) + 2))
30
- sys.stdout.write('\r')
29
+ sys.stderr.write('\r')
30
+ sys.stderr.write(' ' * (len(self.message) + 2))
31
+ sys.stderr.write('\r')
31
32
  if cleanup:
32
- sys.stdout.write('\033[?25h') # Show cursor
33
- sys.stdout.flush()
33
+ sys.stderr.write('\033[?25h') # Show cursor
34
+ sys.stderr.flush()
34
35
  self.spinner_visible = False
35
36
 
36
37
  def spinner_task(self):
@@ -56,11 +57,11 @@ class Spinner:
56
57
  self.remove_spinner(cleanup=True)
57
58
  if exc_type is None:
58
59
  # Show checkmark on success
59
- sys.stdout.write(f"\r✓ {self.message}\n")
60
+ sys.stderr.write(f"\r✓ {self.message}\n")
60
61
  else:
61
62
  # Show X on failure
62
- sys.stdout.write(f"\r✗ {self.message}\n")
63
- sys.stdout.flush()
63
+ sys.stderr.write(f"\r✗ {self.message}\n")
64
+ sys.stderr.flush()
64
65
 
65
66
  def step(message):
66
67
  """Decorator to add a spinning progress indicator to a function."""