locust-cloud 1.12.3__py3-none-any.whl → 1.13.0__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.
- locust_cloud/cloud.py +268 -133
- {locust_cloud-1.12.3.dist-info → locust_cloud-1.13.0.dist-info}/METADATA +3 -4
- locust_cloud-1.13.0.dist-info/RECORD +5 -0
- locust_cloud/__init__.py +0 -133
- locust_cloud/auth.py +0 -443
- locust_cloud/credential_manager.py +0 -141
- locust_cloud/idle_exit.py +0 -38
- locust_cloud/socket_logging.py +0 -127
- locust_cloud/timescale/exporter.py +0 -313
- locust_cloud/timescale/queries.py +0 -321
- locust_cloud/timescale/query.py +0 -74
- locust_cloud/webui/.gitignore +0 -4
- locust_cloud/webui/.prettierrc +0 -9
- locust_cloud/webui/dist/assets/index-D3YieuNV.js +0 -329
- locust_cloud/webui/dist/index.html +0 -20
- locust_cloud/webui/eslint.config.mjs +0 -83
- locust_cloud/webui/index.html +0 -20
- locust_cloud/webui/package.json +0 -52
- locust_cloud/webui/tsconfig.json +0 -27
- locust_cloud/webui/tsconfig.tsbuildinfo +0 -1
- locust_cloud/webui/vite.config.ts +0 -9
- locust_cloud/webui/vitest.config.ts +0 -16
- locust_cloud/webui/yarn.lock +0 -5816
- locust_cloud-1.12.3.dist-info/RECORD +0 -25
- {locust_cloud-1.12.3.dist-info → locust_cloud-1.13.0.dist-info}/WHEEL +0 -0
- {locust_cloud-1.12.3.dist-info → locust_cloud-1.13.0.dist-info}/entry_points.txt +0 -0
locust_cloud/cloud.py
CHANGED
@@ -1,21 +1,29 @@
|
|
1
1
|
import base64
|
2
2
|
import gzip
|
3
|
+
import importlib.metadata
|
4
|
+
import json
|
3
5
|
import logging
|
4
6
|
import os
|
7
|
+
import pathlib
|
5
8
|
import sys
|
9
|
+
import threading
|
6
10
|
import time
|
7
11
|
import tomllib
|
8
12
|
import urllib.parse
|
13
|
+
import webbrowser
|
9
14
|
from argparse import Namespace
|
10
15
|
from collections import OrderedDict
|
16
|
+
from dataclasses import dataclass
|
11
17
|
from typing import IO, Any
|
12
18
|
|
13
19
|
import configargparse
|
20
|
+
import jwt
|
21
|
+
import platformdirs
|
14
22
|
import requests
|
15
23
|
import socketio
|
16
24
|
import socketio.exceptions
|
17
|
-
|
18
|
-
|
25
|
+
|
26
|
+
__version__ = importlib.metadata.version("locust-cloud")
|
19
27
|
|
20
28
|
|
21
29
|
class LocustTomlConfigParser(configargparse.TomlConfigParser):
|
@@ -49,7 +57,7 @@ parser = configargparse.ArgumentParser(
|
|
49
57
|
"cloud.conf",
|
50
58
|
],
|
51
59
|
auto_env_var_prefix="LOCUSTCLOUD_",
|
52
|
-
formatter_class=configargparse.
|
60
|
+
formatter_class=configargparse.RawTextHelpFormatter,
|
53
61
|
config_file_parser_class=configargparse.CompositeConfigParser(
|
54
62
|
[
|
55
63
|
LocustTomlConfigParser(["tool.locust"]),
|
@@ -108,36 +116,16 @@ advanced.add_argument(
|
|
108
116
|
help="Optional requirements.txt file that contains your external libraries.",
|
109
117
|
)
|
110
118
|
advanced.add_argument(
|
111
|
-
"--
|
112
|
-
|
113
|
-
default=
|
114
|
-
help="Sets the AWS region to use for the deployed cluster, e.g. us-east-1. It defaults to use AWS_DEFAULT_REGION env var, like AWS tools.",
|
115
|
-
)
|
116
|
-
parser.add_argument(
|
117
|
-
"--aws-access-key-id",
|
118
|
-
type=str,
|
119
|
-
help=configargparse.SUPPRESS,
|
120
|
-
env_var="AWS_ACCESS_KEY_ID",
|
121
|
-
default=None,
|
122
|
-
)
|
123
|
-
parser.add_argument(
|
124
|
-
"--aws-secret-access-key",
|
125
|
-
type=str,
|
126
|
-
help=configargparse.SUPPRESS,
|
127
|
-
env_var="AWS_SECRET_ACCESS_KEY",
|
128
|
-
default=None,
|
129
|
-
)
|
130
|
-
parser.add_argument(
|
131
|
-
"--username",
|
132
|
-
type=str,
|
119
|
+
"--login",
|
120
|
+
action="store_true",
|
121
|
+
default=False,
|
133
122
|
help=configargparse.SUPPRESS,
|
134
|
-
default=os.getenv("LOCUST_CLOUD_USERNAME", None), # backwards compatitibility for dmdb
|
135
123
|
)
|
136
|
-
|
137
|
-
"--
|
138
|
-
|
139
|
-
|
140
|
-
|
124
|
+
advanced.add_argument(
|
125
|
+
"--non-interactive",
|
126
|
+
action="store_true",
|
127
|
+
default=False,
|
128
|
+
help="This can be set when, for example, running in a CI/CD environment to ensure no interactive steps while executing.\nRequires that LOCUSTCLOUD_USERNAME, LOCUSTCLOUD_PASSWORD and LOCUSTCLOUD_REGION environment variables are set.",
|
141
129
|
)
|
142
130
|
parser.add_argument(
|
143
131
|
"--workers",
|
@@ -153,7 +141,7 @@ parser.add_argument(
|
|
153
141
|
parser.add_argument(
|
154
142
|
"--image-tag",
|
155
143
|
type=str,
|
156
|
-
default=
|
144
|
+
default=None,
|
157
145
|
help=configargparse.SUPPRESS, # overrides the locust-cloud docker image tag. for internal use
|
158
146
|
)
|
159
147
|
parser.add_argument(
|
@@ -169,6 +157,7 @@ parser.add_argument(
|
|
169
157
|
)
|
170
158
|
|
171
159
|
options, locust_options = parser.parse_known_args()
|
160
|
+
|
172
161
|
options: Namespace
|
173
162
|
locust_options: list
|
174
163
|
|
@@ -183,51 +172,222 @@ logging.getLogger("boto3").setLevel(logging.INFO)
|
|
183
172
|
logging.getLogger("requests").setLevel(logging.INFO)
|
184
173
|
logging.getLogger("urllib3").setLevel(logging.INFO)
|
185
174
|
|
175
|
+
cloud_conf_file = pathlib.Path(platformdirs.user_config_dir(appname="locust-cloud")) / "config"
|
176
|
+
valid_regions = ["us-east-1", "eu-north-1"]
|
186
177
|
|
187
|
-
api_url = os.environ.get("LOCUSTCLOUD_DEPLOYER_URL", f"https://api.{options.region}.locust.cloud/1")
|
188
178
|
|
179
|
+
def get_api_url(region):
|
180
|
+
return os.environ.get("LOCUSTCLOUD_DEPLOYER_URL", f"https://api.{region}.locust.cloud/1")
|
189
181
|
|
190
|
-
def main() -> None:
|
191
|
-
if options.version:
|
192
|
-
print(f"locust-cloud version {__version__}")
|
193
|
-
sys.exit(0)
|
194
182
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
183
|
+
@dataclass
|
184
|
+
class CloudConfig:
|
185
|
+
id_token: str | None = None
|
186
|
+
refresh_token: str | None = None
|
187
|
+
refresh_token_expires: int = 0
|
188
|
+
region: str | None = None
|
189
|
+
|
190
|
+
|
191
|
+
def read_cloud_config() -> CloudConfig:
|
192
|
+
if cloud_conf_file.exists():
|
193
|
+
with open(cloud_conf_file) as f:
|
194
|
+
return CloudConfig(**json.load(f))
|
195
|
+
|
196
|
+
return CloudConfig()
|
197
|
+
|
198
|
+
|
199
|
+
def write_cloud_config(config: CloudConfig) -> None:
|
200
|
+
cloud_conf_file.parent.mkdir(parents=True, exist_ok=True)
|
201
|
+
|
202
|
+
with open(cloud_conf_file, "w") as f:
|
203
|
+
json.dump(config.__dict__, f)
|
204
|
+
|
205
|
+
|
206
|
+
def web_login() -> None:
|
207
|
+
print("Enter the number for the region to authenticate against")
|
208
|
+
print()
|
209
|
+
for i, valid_region in enumerate(valid_regions, start=1):
|
210
|
+
print(f" {i}. {valid_region}")
|
211
|
+
print()
|
212
|
+
choice = input("> ")
|
213
|
+
try:
|
214
|
+
region_index = int(choice) - 1
|
215
|
+
assert 0 <= region_index < len(valid_regions)
|
216
|
+
except (ValueError, AssertionError):
|
217
|
+
print(f"Not a valid choice: '{choice}'")
|
199
218
|
sys.exit(1)
|
200
|
-
if options.region:
|
201
|
-
os.environ["AWS_DEFAULT_REGION"] = options.region
|
202
219
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
)
|
220
|
+
region = valid_regions[region_index]
|
221
|
+
|
222
|
+
try:
|
223
|
+
response = requests.post(f"{get_api_url(region)}/cli-auth")
|
224
|
+
response.raise_for_status()
|
225
|
+
response_data = response.json()
|
226
|
+
authentication_url = response_data["authentication_url"]
|
227
|
+
result_url = response_data["result_url"]
|
228
|
+
except Exception as e:
|
229
|
+
print("Something went wrong trying to authorize the locust-cloud CLI:", str(e))
|
207
230
|
sys.exit(1)
|
231
|
+
|
232
|
+
message = f"""
|
233
|
+
Attempting to automatically open the SSO authorization page in your default browser.
|
234
|
+
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:
|
235
|
+
|
236
|
+
{authentication_url}
|
237
|
+
""".strip()
|
238
|
+
print()
|
239
|
+
print(message)
|
240
|
+
|
241
|
+
webbrowser.open_new_tab(authentication_url)
|
242
|
+
|
243
|
+
while True: # Should there be some kind of timeout?
|
244
|
+
response = requests.get(result_url)
|
245
|
+
|
246
|
+
if not response.ok:
|
247
|
+
print("Oh no!")
|
248
|
+
print(response.text)
|
249
|
+
sys.exit(1)
|
250
|
+
|
251
|
+
data = response.json()
|
252
|
+
|
253
|
+
if data["state"] == "pending":
|
254
|
+
time.sleep(1)
|
255
|
+
continue
|
256
|
+
elif data["state"] == "failed":
|
257
|
+
print(f"\nFailed to authorize CLI: {data['reason']}")
|
258
|
+
sys.exit(1)
|
259
|
+
elif data["state"] == "authorized":
|
260
|
+
print("\nAuthorization succeded")
|
261
|
+
break
|
262
|
+
else:
|
263
|
+
print("\nGot unexpected response when authorizing CLI")
|
264
|
+
sys.exit(1)
|
265
|
+
|
266
|
+
config = CloudConfig(
|
267
|
+
id_token=data["id_token"],
|
268
|
+
refresh_token=data["refresh_token"],
|
269
|
+
refresh_token_expires=data["refresh_token_expires"],
|
270
|
+
region=region,
|
271
|
+
)
|
272
|
+
write_cloud_config(config)
|
273
|
+
|
274
|
+
|
275
|
+
class ApiSession(requests.Session):
|
276
|
+
def __init__(self) -> None:
|
277
|
+
super().__init__()
|
278
|
+
|
279
|
+
if options.non_interactive:
|
280
|
+
username = os.getenv("LOCUSTCLOUD_USERNAME")
|
281
|
+
password = os.getenv("LOCUSTCLOUD_PASSWORD")
|
282
|
+
region = os.getenv("LOCUSTCLOUD_REGION")
|
283
|
+
|
284
|
+
if not all([username, password, region]):
|
285
|
+
print(
|
286
|
+
"Running with --non-interaction requires that LOCUSTCLOUD_USERNAME, LOCUSTCLOUD_PASSWORD and LOCUSTCLOUD_REGION environment variables are set."
|
287
|
+
)
|
288
|
+
sys.exit(1)
|
289
|
+
|
290
|
+
if region not in valid_regions:
|
291
|
+
print("Environment variable LOCUSTCLOUD_REGION needs to be set to one of", ", ".join(valid_regions))
|
292
|
+
sys.exit(1)
|
293
|
+
|
294
|
+
self.__configure_for_region(region)
|
295
|
+
response = requests.post(
|
296
|
+
self.__login_url,
|
297
|
+
json={"username": username, "password": password},
|
298
|
+
headers={"X-Client-Version": __version__},
|
299
|
+
)
|
300
|
+
if not response.ok:
|
301
|
+
print(f"Authentication failed: {response.text}")
|
302
|
+
sys.exit(1)
|
303
|
+
|
304
|
+
self.__refresh_token = response.json()["refresh_token"]
|
305
|
+
id_token = response.json()["cognito_client_id_token"]
|
306
|
+
|
307
|
+
else:
|
308
|
+
config = read_cloud_config()
|
309
|
+
|
310
|
+
if config.refresh_token_expires < time.time() + 24 * 60 * 60:
|
311
|
+
message = "You need to authenticate before proceeding. Please run:\n locust-cloud --login"
|
312
|
+
print(message)
|
313
|
+
sys.exit(1)
|
314
|
+
|
315
|
+
assert config.region
|
316
|
+
self.__configure_for_region(config.region)
|
317
|
+
self.__refresh_token = config.refresh_token
|
318
|
+
id_token = config.id_token
|
319
|
+
|
320
|
+
assert id_token
|
321
|
+
|
322
|
+
decoded = jwt.decode(id_token, options={"verify_signature": False})
|
323
|
+
self.__expiry_time = decoded["exp"] - 60 # Refresh 1 minute before expiry
|
324
|
+
self.headers["Authorization"] = f"Bearer {id_token}"
|
325
|
+
|
326
|
+
self.__sub = decoded["sub"]
|
327
|
+
self.headers["X-Client-Version"] = __version__
|
328
|
+
|
329
|
+
def __configure_for_region(self, region: str) -> None:
|
330
|
+
self.__region = region
|
331
|
+
self.api_url = get_api_url(region)
|
332
|
+
self.__login_url = f"{self.api_url}/auth/login"
|
333
|
+
|
334
|
+
logger.debug(f"Lambda url: {self.api_url}")
|
335
|
+
|
336
|
+
def __ensure_valid_authorization_header(self) -> None:
|
337
|
+
if self.__expiry_time > time.time():
|
338
|
+
return
|
339
|
+
|
340
|
+
logger.info(f"Authenticating ({self.__region}, v{__version__})")
|
341
|
+
|
342
|
+
response = requests.post(
|
343
|
+
self.__login_url,
|
344
|
+
json={"user_sub_id": self.__sub, "refresh_token": self.__refresh_token},
|
345
|
+
headers={"X-Client-Version": __version__},
|
346
|
+
)
|
347
|
+
|
348
|
+
if not response.ok:
|
349
|
+
logger.error(f"Authentication failed: {response.text}")
|
350
|
+
sys.exit(1)
|
351
|
+
|
352
|
+
# TODO: Technically the /login endpoint can return a challenge for you
|
353
|
+
# to change your password. Don't know how we should handle that
|
354
|
+
# in the cli.
|
355
|
+
|
356
|
+
id_token = response.json()["cognito_client_id_token"]
|
357
|
+
decoded = jwt.decode(id_token, options={"verify_signature": False})
|
358
|
+
self.__expiry_time = decoded["exp"] - 60 # Refresh 1 minute before expiry
|
359
|
+
self.headers["Authorization"] = f"Bearer {id_token}"
|
360
|
+
|
361
|
+
if not options.non_interactive:
|
362
|
+
config = read_cloud_config()
|
363
|
+
config.id_token = id_token
|
364
|
+
write_cloud_config(config)
|
365
|
+
|
366
|
+
def request(self, method, url, *args, **kwargs) -> requests.Response:
|
367
|
+
self.__ensure_valid_authorization_header()
|
368
|
+
return super().request(method, f"{self.api_url}{url}", *args, **kwargs)
|
369
|
+
|
370
|
+
|
371
|
+
def main() -> None:
|
372
|
+
if options.version:
|
373
|
+
print(f"locust-cloud version {__version__}")
|
374
|
+
sys.exit(0)
|
208
375
|
if not options.locustfile:
|
209
376
|
logger.error("A locustfile is required to run a test.")
|
210
377
|
sys.exit(1)
|
211
378
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
secret_key=options.aws_secret_access_key,
|
219
|
-
username=options.username,
|
220
|
-
password=options.password,
|
221
|
-
)
|
379
|
+
if options.login:
|
380
|
+
try:
|
381
|
+
web_login()
|
382
|
+
except KeyboardInterrupt:
|
383
|
+
pass
|
384
|
+
sys.exit()
|
222
385
|
|
223
|
-
|
224
|
-
cognito_client_id_token = credentials["cognito_client_id_token"]
|
225
|
-
aws_access_key_id = credentials.get("access_key")
|
226
|
-
aws_secret_access_key = credentials.get("secret_key")
|
227
|
-
aws_session_token = credentials.get("token", "")
|
386
|
+
session = ApiSession()
|
228
387
|
|
388
|
+
try:
|
229
389
|
if options.delete:
|
230
|
-
delete(
|
390
|
+
delete(session)
|
231
391
|
return
|
232
392
|
|
233
393
|
try:
|
@@ -249,47 +409,41 @@ def main() -> None:
|
|
249
409
|
|
250
410
|
logger.info("Deploying load generators")
|
251
411
|
locust_env_variables = [
|
252
|
-
{"name": env_variable, "value":
|
412
|
+
{"name": env_variable, "value": os.environ[env_variable]}
|
253
413
|
for env_variable in os.environ
|
254
414
|
if env_variable.startswith("LOCUST_")
|
255
|
-
and
|
256
|
-
in [
|
415
|
+
and env_variable
|
416
|
+
not in [
|
257
417
|
"LOCUST_LOCUSTFILE",
|
258
418
|
"LOCUST_USERS",
|
259
419
|
"LOCUST_WEB_HOST_DISPLAY_NAME",
|
260
420
|
"LOCUST_SKIP_MONKEY_PATCH",
|
261
421
|
]
|
262
|
-
and os.environ[env_variable]
|
263
422
|
]
|
264
|
-
deploy_endpoint = f"{api_url}/deploy"
|
265
423
|
payload = {
|
266
424
|
"locust_args": [
|
267
425
|
{"name": "LOCUST_USERS", "value": str(options.users)},
|
268
426
|
{"name": "LOCUST_FLAGS", "value": " ".join(locust_options)},
|
269
|
-
{"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": api_url},
|
427
|
+
{"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": session.api_url},
|
270
428
|
{"name": "LOCUSTCLOUD_PROFILE", "value": options.profile},
|
271
429
|
*locust_env_variables,
|
272
430
|
],
|
273
431
|
"locustfile": {"filename": options.locustfile, "data": locustfile_data},
|
274
432
|
"user_count": options.users,
|
275
|
-
"image_tag": options.image_tag,
|
276
433
|
"mock_server": options.mock_server,
|
277
434
|
}
|
435
|
+
|
436
|
+
if options.image_tag is not None:
|
437
|
+
payload["image_tag"] = options.image_tag
|
438
|
+
|
278
439
|
if options.workers is not None:
|
279
440
|
payload["worker_count"] = options.workers
|
441
|
+
|
280
442
|
if options.requirements:
|
281
443
|
payload["requirements"] = {"filename": options.requirements, "data": requirements_data}
|
282
|
-
|
283
|
-
"Authorization": f"Bearer {cognito_client_id_token}",
|
284
|
-
"Content-Type": "application/json",
|
285
|
-
"AWS_ACCESS_KEY_ID": aws_access_key_id,
|
286
|
-
"AWS_SECRET_ACCESS_KEY": aws_secret_access_key,
|
287
|
-
"AWS_SESSION_TOKEN": aws_session_token,
|
288
|
-
"X-Client-Version": __version__,
|
289
|
-
}
|
444
|
+
|
290
445
|
try:
|
291
|
-
|
292
|
-
response = requests.post(deploy_endpoint, json=payload, headers=headers)
|
446
|
+
response = session.post("/deploy", json=payload)
|
293
447
|
except requests.exceptions.RequestException as e:
|
294
448
|
logger.error(f"Failed to deploy the load generators: {e}")
|
295
449
|
sys.exit(1)
|
@@ -304,32 +458,28 @@ def main() -> None:
|
|
304
458
|
f"HTTP {response.status_code}/{response.reason} - Response: {response.text} - URL: {response.request.url}"
|
305
459
|
)
|
306
460
|
sys.exit(1)
|
307
|
-
|
308
|
-
logger.error(f"Credential error: {ce}")
|
309
|
-
sys.exit(1)
|
461
|
+
|
310
462
|
except KeyboardInterrupt:
|
463
|
+
# TODO: This would potentially leave a deployment running, combine with try-catch below?
|
311
464
|
logger.debug("Interrupted by user")
|
312
465
|
sys.exit(0)
|
313
466
|
|
314
467
|
logger.debug("Load generators deployed successfully!")
|
315
468
|
logger.info("Waiting for pods to be ready...")
|
316
469
|
|
470
|
+
shutdown_allowed = threading.Event()
|
471
|
+
shutdown_allowed.set()
|
472
|
+
reconnect_aborted = threading.Event()
|
473
|
+
connect_timeout = threading.Timer(2 * 60, reconnect_aborted.set)
|
474
|
+
sio = socketio.Client(handle_sigint=False)
|
475
|
+
|
317
476
|
try:
|
318
477
|
ws_connection_info = urllib.parse.urlparse(log_ws_url)
|
319
|
-
sio = socketio.Client(handle_sigint=False)
|
320
|
-
|
321
|
-
run = True
|
322
|
-
|
323
|
-
def wait():
|
324
|
-
logger.debug("Waiting for shutdown event")
|
325
|
-
while run:
|
326
|
-
time.sleep(0.1)
|
327
|
-
|
328
|
-
logger.debug("Shutting down websocket connection")
|
329
|
-
sio.shutdown()
|
330
478
|
|
331
479
|
@sio.event
|
332
480
|
def connect():
|
481
|
+
shutdown_allowed.clear()
|
482
|
+
connect_timeout.cancel()
|
333
483
|
logger.debug("Websocket connection established, switching to Locust logs")
|
334
484
|
|
335
485
|
@sio.event
|
@@ -350,55 +500,40 @@ def main() -> None:
|
|
350
500
|
if message:
|
351
501
|
print(message)
|
352
502
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
wait()
|
503
|
+
shutdown_allowed.set()
|
504
|
+
|
505
|
+
# The _reconnect_abort value on the socketio client will be populated with a newly created threading.Event if it's not already set.
|
506
|
+
# There is no way to set this by passing it in the constructor.
|
507
|
+
# This event is the only way to interupt the retry logic when the connection is attempted.
|
508
|
+
sio._reconnect_abort = reconnect_aborted
|
509
|
+
connect_timeout.start()
|
510
|
+
sio.connect(
|
511
|
+
f"{ws_connection_info.scheme}://{ws_connection_info.netloc}",
|
512
|
+
socketio_path=ws_connection_info.path,
|
513
|
+
retry=True,
|
514
|
+
)
|
515
|
+
logger.debug("Waiting for shutdown")
|
516
|
+
shutdown_allowed.wait()
|
369
517
|
|
370
518
|
except KeyboardInterrupt:
|
371
519
|
logger.debug("Interrupted by user")
|
372
|
-
delete(
|
373
|
-
wait()
|
520
|
+
delete(session)
|
521
|
+
shutdown_allowed.wait(timeout=90)
|
374
522
|
except Exception as e:
|
375
523
|
logger.exception(e)
|
376
|
-
delete(
|
524
|
+
delete(session)
|
377
525
|
sys.exit(1)
|
378
526
|
else:
|
379
|
-
delete(
|
527
|
+
delete(session)
|
528
|
+
finally:
|
529
|
+
sio.shutdown()
|
380
530
|
|
381
531
|
|
382
|
-
def delete(
|
532
|
+
def delete(session):
|
383
533
|
try:
|
384
534
|
logger.info("Tearing down Locust cloud...")
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
headers = {
|
389
|
-
"AWS_ACCESS_KEY_ID": refreshed_credentials.get("access_key", ""),
|
390
|
-
"AWS_SECRET_ACCESS_KEY": refreshed_credentials.get("secret_key", ""),
|
391
|
-
"Authorization": f"Bearer {refreshed_credentials.get('cognito_client_id_token', '')}",
|
392
|
-
"X-Client-Version": __version__,
|
393
|
-
}
|
394
|
-
|
395
|
-
token = refreshed_credentials.get("token")
|
396
|
-
if token:
|
397
|
-
headers["AWS_SESSION_TOKEN"] = token
|
398
|
-
|
399
|
-
response = requests.delete(
|
400
|
-
f"{api_url}/teardown",
|
401
|
-
headers=headers,
|
535
|
+
response = session.delete(
|
536
|
+
"/teardown",
|
402
537
|
)
|
403
538
|
|
404
539
|
if response.status_code == 200:
|
@@ -410,7 +545,7 @@ def delete(credential_manager):
|
|
410
545
|
except Exception as e:
|
411
546
|
logger.error(f"Could not automatically tear down Locust Cloud: {e.__class__.__name__}:{e}")
|
412
547
|
|
413
|
-
logger.info("Done! ✨")
|
548
|
+
logger.info("Done! ✨") # FIXME: Should probably not say it's done since at this point it could still be running
|
414
549
|
|
415
550
|
|
416
551
|
if __name__ == "__main__":
|
@@ -1,13 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: locust-cloud
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.13.0
|
4
4
|
Summary: Locust Cloud
|
5
5
|
Project-URL: Homepage, https://locust.cloud
|
6
6
|
Requires-Python: >=3.11
|
7
7
|
Requires-Dist: boto3==1.34.125
|
8
|
-
Requires-Dist:
|
9
|
-
Requires-Dist:
|
10
|
-
Requires-Dist: psycopg[binary,pool]>=3.2.1
|
8
|
+
Requires-Dist: configargparse==1.7
|
9
|
+
Requires-Dist: platformdirs>=4.3.6
|
11
10
|
Requires-Dist: pyjwt>=2.0
|
12
11
|
Requires-Dist: python-socketio[client]==5.11.4
|
13
12
|
Description-Content-Type: text/markdown
|
@@ -0,0 +1,5 @@
|
|
1
|
+
locust_cloud/cloud.py,sha256=r0lWoP1QiT1Klkrecsr6LyPD_l1GMW2BFGRCRVkLowM,18124
|
2
|
+
locust_cloud-1.13.0.dist-info/METADATA,sha256=STWxyZOw0qE003r-Pf-8RU3KPFK3yjH_NsSo5ffoHh0,496
|
3
|
+
locust_cloud-1.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
4
|
+
locust_cloud-1.13.0.dist-info/entry_points.txt,sha256=PGyAb4e3aTsGS3N3VGShDl6VzJaXy7QwsEgsLOC7V00,57
|
5
|
+
locust_cloud-1.13.0.dist-info/RECORD,,
|