Skip to content

Commit 9a1347e

Browse files
authored
Merge pull request #30 from Stackmasters/display-deployment-logs
Stream job logs when using `--wait`
2 parents b7eceb1 + dc8398e commit 9a1347e

File tree

4 files changed

+193
-26
lines changed

4 files changed

+193
-26
lines changed

cycleops/client.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import requests
44
import sec
55
import typer
6+
import websockets
67
from requests.models import Response
8+
from websockets.legacy.client import WebSocketClientProtocol
79

810
from .auth import CycleopsAuthentication
911
from .exceptions import APIError, AuthenticationError
@@ -151,7 +153,7 @@ def deploy(self, setup_id: int) -> Optional[Dict[str, Any]]:
151153
description: str = f"Deploying setup: {setup_id}"
152154
type: str = "Deployment"
153155

154-
jobs_client = JobClient(cycleops_client)
156+
jobs_client: JobClient = JobClient(cycleops_client)
155157
return jobs_client.create(description=description, type=type, setup=setup_id)
156158

157159

@@ -266,3 +268,50 @@ def delete(self, hostgroup_id: int) -> Optional[Dict[str, Any]]:
266268

267269

268270
cycleops_client: Client = Client()
271+
272+
273+
class WebSocketClient:
274+
"""
275+
A client for interacting with Cycleops websockets to request and listen for job logs.
276+
"""
277+
278+
def __init__(self, job_id: str):
279+
self.url: str = "wss://cloud.cycleops.io/ansible-worker-ws/ws/ansible-output"
280+
self.job_id: str = job_id
281+
self._jwt: Optional[str] = None
282+
self._job: Optional[Dict[str, Any]] = None
283+
284+
@property
285+
def jwt(self):
286+
if not self._jwt:
287+
self._jwt = self.authenticate()
288+
return self._jwt
289+
290+
@property
291+
def job(self):
292+
if not self._job:
293+
self._job = self.get_job()
294+
return self._job
295+
296+
def authenticate(self) -> Optional[str]:
297+
token: str = cycleops_client._request("POST", f"identity/token")
298+
return token["access_token"]
299+
300+
def get_job(self) -> Optional[Dict[str, Any]]:
301+
job_client: JobClient = JobClient(cycleops_client)
302+
job: Optional[Dict[str, Any]] = job_client.retrieve(self.job_id)
303+
304+
return job
305+
306+
async def get_job_logs(self, websocket: WebSocketClientProtocol) -> None:
307+
message: str = f"id={self.job_id}|jwt={self.jwt}|account={self.job['account']}"
308+
await websocket.send(message)
309+
310+
async def listen(self, websocket: WebSocketClientProtocol) -> None:
311+
while message := await websocket.recv():
312+
print(f"{message}\n")
313+
314+
async def run(self) -> None:
315+
async with websockets.connect(self.url) as websocket:
316+
await self.get_job_logs(websocket)
317+
await self.listen(websocket)

cycleops/setups.py

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import asyncio
12
import time
23
from typing import Any, Dict, List, Optional
34

45
import typer
6+
import websockets
57
from rich import print
68

7-
from .client import JobClient, SetupClient, cycleops_client
9+
from .client import JobClient, SetupClient, WebSocketClient, cycleops_client
810
from .exceptions import NotFound
911
from .utils import display_error_message, display_success_message
1012

@@ -180,33 +182,37 @@ def deploy(
180182

181183
try:
182184
setup = get_setup(setup_identifier)
183-
184185
job = setup_client.deploy(setup["id"])
185-
report_queued = print if wait else display_success_message
186-
report_queued(f"Setup {setup['id']} has been queued for deployment")
187-
188-
while wait:
189-
match status := job["status"]:
190-
case "Initialized":
191-
print(f"Setup {setup['id']} has been initialized")
192-
case "Deploying":
193-
print(f"Setup {setup['id']} is being deployed")
194-
case "Deployed":
195-
display_success_message(
196-
f"Setup {setup['id']} has been deployed successfully"
197-
)
198-
break
199-
case "Failed":
200-
display_error_message(job)
201-
raise Exception(f"Setup {setup['id']} could not be deployed")
202-
case _:
203-
print(f"Setup {setup['id']} is in status {status}")
204-
time.sleep(3)
205-
job = job_client.retrieve(job["id"])
206186
except Exception as error:
207187
display_error_message(error)
208188
raise typer.Abort()
209189

190+
deployment_scheduled_message = (
191+
f"Setup {setup_identifier} has been queued for deployment"
192+
)
193+
194+
if not wait:
195+
display_success_message(deployment_scheduled_message)
196+
return
197+
198+
print(f"{deployment_scheduled_message}\n")
199+
200+
try:
201+
display_job_logs(job["id"])
202+
except websockets.exceptions.ConnectionClosed:
203+
job = job_client.retrieve(job["id"])
204+
205+
match job["status"]:
206+
case "Deployed":
207+
display_success_message(
208+
f"Setup {setup_identifier} has been deployed successfully"
209+
)
210+
case "Failed":
211+
display_error_message(f"Setup {setup_identifier} could not be deployed")
212+
case _:
213+
print(f"Setup {setup_identifier} is in status {job['status']}")
214+
return
215+
210216

211217
def get_setup(setup_identifier: str) -> Optional[Dict[str, Any]]:
212218
"""
@@ -221,3 +227,13 @@ def get_setup(setup_identifier: str) -> Optional[Dict[str, Any]]:
221227
setup = setup_client.retrieve(setup_identifier)
222228

223229
return setup
230+
231+
232+
def display_job_logs(job_id: str) -> None:
233+
"""
234+
Displays the deployements logs of the specified job
235+
"""
236+
237+
websocket_client = WebSocketClient(job_id)
238+
239+
asyncio.get_event_loop().run_until_complete(websocket_client.run())

0 commit comments

Comments
 (0)