locust-cloud 1.0.17__py3-none-any.whl → 1.0.18__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 +109 -64
- locust_cloud/constants.py +2 -1
- locust_cloud/credential_manager.py +0 -4
- locust_cloud/webui/dist/assets/{index-BG65KOq7.js → index-Cj0_NN88.js} +84 -84
- locust_cloud/webui/dist/index.html +1 -1
- {locust_cloud-1.0.17.dist-info → locust_cloud-1.0.18.dist-info}/METADATA +1 -1
- {locust_cloud-1.0.17.dist-info → locust_cloud-1.0.18.dist-info}/RECORD +9 -9
- {locust_cloud-1.0.17.dist-info → locust_cloud-1.0.18.dist-info}/WHEEL +0 -0
- {locust_cloud-1.0.17.dist-info → locust_cloud-1.0.18.dist-info}/entry_points.txt +0 -0
locust_cloud/cloud.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
|
+
import math
|
3
4
|
import os
|
4
5
|
import sys
|
5
6
|
import time
|
@@ -13,19 +14,13 @@ import requests
|
|
13
14
|
from botocore.exceptions import ClientError
|
14
15
|
from locust_cloud.constants import (
|
15
16
|
DEFAULT_CLUSTER_NAME,
|
17
|
+
DEFAULT_LAMBDA_URL,
|
16
18
|
DEFAULT_NAMESPACE,
|
17
19
|
DEFAULT_REGION_NAME,
|
18
|
-
|
20
|
+
USERS_PER_WORKER,
|
19
21
|
)
|
20
22
|
from locust_cloud.credential_manager import CredentialError, CredentialManager
|
21
23
|
|
22
|
-
logging.basicConfig(
|
23
|
-
format="[LOCUST-CLOUD] %(levelname)s: %(message)s",
|
24
|
-
level=logging.INFO,
|
25
|
-
)
|
26
|
-
logger = logging.getLogger(__name__)
|
27
|
-
|
28
|
-
|
29
24
|
LOCUST_ENV_VARIABLE_IGNORE_LIST = ["LOCUST_BUILD_PATH", "LOCUST_SKIP_MONKEY_PATCH"]
|
30
25
|
|
31
26
|
|
@@ -67,9 +62,9 @@ parser = configargparse.ArgumentParser(
|
|
67
62
|
configargparse.DefaultConfigFileParser,
|
68
63
|
]
|
69
64
|
),
|
70
|
-
description="""
|
65
|
+
description="""Launches distributed Locust runs on locust.cloud infrastructure.
|
71
66
|
|
72
|
-
Example: locust-cloud -f
|
67
|
+
Example: locust-cloud -f my_locustfile.py --aws-region-name us-east-1 --users 1000""",
|
73
68
|
epilog="""Any parameters not listed here are forwarded to locust master unmodified, so go ahead and use things like --users, --host, --run-time, ...
|
74
69
|
Locust config can also be set using config file (~/.locust.conf, locust.conf, pyproject.toml, ~/.cloud.conf or cloud.conf).
|
75
70
|
Parameters specified on command line override env vars, which in turn override config files.""",
|
@@ -113,61 +108,104 @@ parser.add_argument(
|
|
113
108
|
help="Sets the namespace for scoping the deployed cluster",
|
114
109
|
env_var="KUBE_NAMESPACE",
|
115
110
|
)
|
111
|
+
parser.add_argument(
|
112
|
+
"--lambda-url",
|
113
|
+
type=str,
|
114
|
+
default=DEFAULT_LAMBDA_URL,
|
115
|
+
help="Sets the namespace for scoping the deployed cluster",
|
116
|
+
env_var="LOCUST_CLOUD_LAMBDA",
|
117
|
+
)
|
116
118
|
parser.add_argument(
|
117
119
|
"--aws-access-key-id",
|
118
120
|
type=str,
|
119
|
-
help=
|
121
|
+
help=configargparse.SUPPRESS,
|
120
122
|
env_var="AWS_ACCESS_KEY_ID",
|
123
|
+
default=None,
|
121
124
|
)
|
122
125
|
parser.add_argument(
|
123
126
|
"--aws-secret-access-key",
|
124
127
|
type=str,
|
125
|
-
help=
|
128
|
+
help=configargparse.SUPPRESS,
|
126
129
|
env_var="AWS_SECRET_ACCESS_KEY",
|
130
|
+
default=None,
|
131
|
+
)
|
132
|
+
parser.add_argument(
|
133
|
+
"--username",
|
134
|
+
type=str,
|
135
|
+
help="Authentication for deploying with Locust Cloud",
|
136
|
+
env_var="LOCUST_CLOUD_USERNAME",
|
137
|
+
default=None,
|
138
|
+
)
|
139
|
+
parser.add_argument(
|
140
|
+
"--password",
|
141
|
+
type=str,
|
142
|
+
help="Authentication for deploying with Locust Cloud",
|
143
|
+
env_var="LOCUST_CLOUD_PASSWORD",
|
144
|
+
default=None,
|
145
|
+
)
|
146
|
+
parser.add_argument(
|
147
|
+
"--loglevel",
|
148
|
+
"-L",
|
149
|
+
type=str,
|
150
|
+
help="Log level",
|
151
|
+
env_var="LOCUST_CLOUD_LOGLEVEL",
|
152
|
+
default="INFO",
|
153
|
+
)
|
154
|
+
parser.add_argument(
|
155
|
+
"--workers",
|
156
|
+
type=str,
|
157
|
+
help="Number of workers to use for the deployment",
|
158
|
+
env_var="LOCUST_CLOUD_WORKERS",
|
159
|
+
default=None,
|
127
160
|
)
|
128
161
|
|
129
162
|
options, locust_options = parser.parse_known_args()
|
130
|
-
|
131
|
-
|
132
|
-
|
163
|
+
logging.basicConfig(
|
164
|
+
format="[LOCUST-CLOUD] %(levelname)s: %(message)s",
|
165
|
+
level=options.loglevel.upper(),
|
166
|
+
)
|
167
|
+
logger = logging.getLogger(__name__)
|
168
|
+
# Restore log level for other libs. Yes, this can be done more nicely
|
169
|
+
logging.getLogger("botocore").setLevel(logging.INFO)
|
170
|
+
logging.getLogger("boto3").setLevel(logging.INFO)
|
171
|
+
logging.getLogger("s3transfer").setLevel(logging.INFO)
|
172
|
+
logging.getLogger("requests").setLevel(logging.INFO)
|
173
|
+
logging.getLogger("urllib3").setLevel(logging.INFO)
|
133
174
|
|
134
175
|
|
135
176
|
def main() -> None:
|
136
177
|
s3_bucket = f"{options.kube_cluster_name}-{options.kube_namespace}"
|
137
178
|
deployed_pods: list[Any] = []
|
179
|
+
worker_count = options.workers or math.ceil(locust_options.users / USERS_PER_WORKER)
|
180
|
+
worker_count = worker_count if worker_count > 2 else 2
|
138
181
|
|
139
182
|
try:
|
140
|
-
if
|
141
|
-
|
142
|
-
|
143
|
-
region_name=options.aws_region_name,
|
144
|
-
access_key=options.aws_access_key_id,
|
145
|
-
secret_key=options.aws_secret_access_key,
|
146
|
-
)
|
147
|
-
elif username and password:
|
148
|
-
credential_manager = CredentialManager(
|
149
|
-
lambda_url=LAMBDA_URL,
|
150
|
-
username=username,
|
151
|
-
password=password,
|
152
|
-
region_name=options.aws_region_name,
|
153
|
-
)
|
154
|
-
else:
|
183
|
+
if not (
|
184
|
+
(options.username and options.password) or (options.aws_access_key_id and options.aws_secret_access_key)
|
185
|
+
):
|
155
186
|
logger.error(
|
156
|
-
"Authentication is required to use Locust Cloud.
|
187
|
+
"Authentication is required to use Locust Cloud. Please ensure the LOCUST_CLOUD_USERNAME and LOCUST_CLOUD_PASSWORD environment variables are set."
|
157
188
|
)
|
158
189
|
sys.exit(1)
|
159
190
|
|
191
|
+
logger.info(f"Logging you into Locust Cloud ({options.lambda_url}, {options.aws_region_name})")
|
192
|
+
|
193
|
+
credential_manager = CredentialManager(
|
194
|
+
lambda_url=options.lambda_url,
|
195
|
+
access_key=options.aws_access_key_id,
|
196
|
+
secret_key=options.aws_secret_access_key,
|
197
|
+
username=options.username,
|
198
|
+
password=options.password,
|
199
|
+
region_name=options.aws_region_name,
|
200
|
+
)
|
201
|
+
|
160
202
|
credentials = credential_manager.get_current_credentials()
|
161
|
-
cognito_client_id_token
|
203
|
+
cognito_client_id_token = credentials["cognito_client_id_token"]
|
162
204
|
aws_access_key_id = credentials.get("access_key")
|
163
205
|
aws_secret_access_key = credentials.get("secret_key")
|
164
|
-
aws_session_token = credentials.get("token")
|
165
|
-
|
166
|
-
if not all([aws_access_key_id, aws_secret_access_key]):
|
167
|
-
logger.error("Authentication failed: Missing AWS credentials.")
|
168
|
-
sys.exit(1)
|
206
|
+
aws_session_token = credentials.get("token", "")
|
169
207
|
|
170
|
-
logger.info(f"Uploading {options.locustfile}
|
208
|
+
logger.info(f"Uploading {options.locustfile}")
|
171
209
|
s3 = credential_manager.session.client("s3")
|
172
210
|
try:
|
173
211
|
s3.upload_file(options.locustfile, s3_bucket, os.path.basename(options.locustfile))
|
@@ -176,17 +214,17 @@ def main() -> None:
|
|
176
214
|
Params={"Bucket": s3_bucket, "Key": os.path.basename(options.locustfile)},
|
177
215
|
ExpiresIn=3600,
|
178
216
|
)
|
179
|
-
logger.
|
217
|
+
logger.debug(f"Uploaded {options.locustfile} successfully")
|
180
218
|
except FileNotFoundError:
|
181
219
|
logger.error(f"File not found: {options.locustfile}")
|
182
220
|
sys.exit(1)
|
183
221
|
except ClientError as e:
|
184
|
-
logger.error(f"Failed to upload {options.locustfile}
|
222
|
+
logger.error(f"Failed to upload {options.locustfile}: {e}")
|
185
223
|
sys.exit(1)
|
186
224
|
|
187
225
|
requirements_url = ""
|
188
226
|
if options.requirements:
|
189
|
-
logger.info(f"Uploading {options.requirements}
|
227
|
+
logger.info(f"Uploading {options.requirements}")
|
190
228
|
try:
|
191
229
|
s3.upload_file(options.requirements, s3_bucket, "requirements.txt")
|
192
230
|
requirements_url = s3.generate_presigned_url(
|
@@ -194,44 +232,46 @@ def main() -> None:
|
|
194
232
|
Params={"Bucket": s3_bucket, "Key": "requirements.txt"},
|
195
233
|
ExpiresIn=3600,
|
196
234
|
)
|
197
|
-
logger.
|
235
|
+
logger.debug(f"Uploaded {options.requirements} successfully")
|
198
236
|
except FileNotFoundError:
|
199
237
|
logger.error(f"File not found: {options.requirements}")
|
200
238
|
sys.exit(1)
|
201
239
|
except ClientError as e:
|
202
|
-
logger.error(f"Failed to upload {options.requirements}
|
240
|
+
logger.error(f"Failed to upload {options.requirements}: {e}")
|
203
241
|
sys.exit(1)
|
204
242
|
|
205
|
-
logger.info("Deploying load generators
|
243
|
+
logger.info("Deploying load generators")
|
206
244
|
locust_env_variables = [
|
207
245
|
{"name": env_variable, "value": str(os.environ[env_variable])}
|
208
246
|
for env_variable in os.environ
|
209
247
|
if env_variable.startswith("LOCUST_") and os.environ[env_variable]
|
210
248
|
]
|
211
|
-
deploy_endpoint = f"{
|
249
|
+
deploy_endpoint = f"{options.lambda_url}/{options.kube_cluster_name}"
|
212
250
|
payload = {
|
213
251
|
"locust_args": [
|
214
252
|
{"name": "LOCUST_LOCUSTFILE", "value": locustfile_url},
|
215
253
|
{"name": "LOCUST_REQUIREMENTS_URL", "value": requirements_url},
|
216
254
|
{"name": "LOCUST_FLAGS", "value": " ".join(locust_options)},
|
255
|
+
{"name": "LOCUST_WEB_HOST_DISPLAY_NAME", "value": "_____"},
|
217
256
|
*locust_env_variables,
|
218
|
-
]
|
257
|
+
],
|
258
|
+
"worker_count": worker_count,
|
219
259
|
}
|
220
260
|
headers = {
|
221
261
|
"Authorization": f"Bearer {cognito_client_id_token}",
|
222
262
|
"Content-Type": "application/json",
|
223
263
|
"AWS_ACCESS_KEY_ID": aws_access_key_id,
|
224
264
|
"AWS_SECRET_ACCESS_KEY": aws_secret_access_key,
|
225
|
-
"AWS_SESSION_TOKEN": aws_session_token
|
265
|
+
"AWS_SESSION_TOKEN": aws_session_token,
|
226
266
|
}
|
227
267
|
try:
|
228
268
|
response = requests.post(deploy_endpoint, json=payload, headers=headers)
|
229
269
|
except requests.exceptions.RequestException as e:
|
230
|
-
logger.error(f"
|
270
|
+
logger.error(f"Failed to deploy the load generators: {e}")
|
231
271
|
sys.exit(1)
|
272
|
+
|
232
273
|
if response.status_code == 200:
|
233
274
|
deployed_pods = response.json().get("pods", [])
|
234
|
-
logger.info("Load generators deployed successfully.")
|
235
275
|
else:
|
236
276
|
logger.error(
|
237
277
|
f"HTTP {response.status_code}/{response.reason} - Response: {response.text} - URL: {response.request.url}"
|
@@ -241,13 +281,20 @@ def main() -> None:
|
|
241
281
|
logger.error(f"Credential error: {ce}")
|
242
282
|
sys.exit(1)
|
243
283
|
|
284
|
+
log_group_name = f"/eks/{options.kube_cluster_name}-{options.kube_namespace}"
|
285
|
+
master_pod_name = next((pod for pod in deployed_pods if "master" in pod), None)
|
286
|
+
|
287
|
+
if not master_pod_name:
|
288
|
+
logger.error(
|
289
|
+
"Master pod not found among deployed pods. Something went wrong during the load generator deployment, please try again or contact an administrator for assistance"
|
290
|
+
)
|
291
|
+
sys.exit(1)
|
292
|
+
|
293
|
+
logger.debug("Load generators deployed successfully!")
|
294
|
+
|
244
295
|
try:
|
245
296
|
logger.info("Waiting for pods to be ready...")
|
246
|
-
|
247
|
-
master_pod_name = next((pod for pod in deployed_pods if "master" in pod), None)
|
248
|
-
if not master_pod_name:
|
249
|
-
logger.error("Master pod not found among deployed pods.")
|
250
|
-
sys.exit(1)
|
297
|
+
|
251
298
|
log_stream: str | None = None
|
252
299
|
while log_stream is None:
|
253
300
|
try:
|
@@ -264,7 +311,7 @@ def main() -> None:
|
|
264
311
|
except ClientError as e:
|
265
312
|
logger.error(f"Error describing log streams: {e}")
|
266
313
|
time.sleep(5)
|
267
|
-
logger.
|
314
|
+
logger.debug("Pods are ready, switching to Locust logs")
|
268
315
|
|
269
316
|
timestamp = int((datetime.now(UTC) - timedelta(minutes=5)).timestamp() * 1000)
|
270
317
|
|
@@ -291,10 +338,10 @@ def main() -> None:
|
|
291
338
|
except ClientError as e:
|
292
339
|
error_code = e.response.get("Error", {}).get("Code", "")
|
293
340
|
if error_code == "ExpiredTokenException":
|
294
|
-
logger.
|
341
|
+
logger.debug("AWS session token expired during log streaming. Refreshing credentials.")
|
295
342
|
time.sleep(5)
|
296
343
|
except KeyboardInterrupt:
|
297
|
-
logger.debug("Interrupted by user
|
344
|
+
logger.debug("Interrupted by user")
|
298
345
|
except Exception as e:
|
299
346
|
logger.exception(e)
|
300
347
|
sys.exit(1)
|
@@ -315,28 +362,26 @@ def main() -> None:
|
|
315
362
|
headers["AWS_SESSION_TOKEN"] = token
|
316
363
|
|
317
364
|
response = requests.delete(
|
318
|
-
f"{
|
365
|
+
f"{options.lambda_url}/{options.kube_cluster_name}",
|
319
366
|
headers=headers,
|
320
367
|
params={"namespace": options.kube_namespace} if options.kube_namespace else {},
|
321
368
|
)
|
322
369
|
|
323
370
|
if response.status_code != 200:
|
324
371
|
logger.error(
|
325
|
-
f"HTTP {response.status_code}/{response.reason} - Response: {response.text} - URL: {response.request.url}"
|
372
|
+
f"Could not automatically tear down Locust Cloud: HTTP {response.status_code}/{response.reason} - Response: {response.text} - URL: {response.request.url}"
|
326
373
|
)
|
327
|
-
else:
|
328
|
-
logger.info("Cluster teardown initiated successfully.")
|
329
374
|
except Exception as e:
|
330
375
|
logger.error(f"Could not automatically tear down Locust Cloud: {e}")
|
331
376
|
|
332
377
|
try:
|
333
|
-
logger.info(
|
378
|
+
logger.info("Cleaning up locust files")
|
334
379
|
s3 = credential_manager.session.resource("s3")
|
335
380
|
bucket = s3.Bucket(s3_bucket)
|
336
381
|
bucket.objects.all().delete()
|
337
|
-
logger.info(
|
382
|
+
logger.info("Done! ✨")
|
338
383
|
except ClientError as e:
|
339
|
-
logger.error(f"Failed to clean up
|
384
|
+
logger.error(f"Failed to clean up locust files: {e}")
|
340
385
|
sys.exit(1)
|
341
386
|
|
342
387
|
|
locust_cloud/constants.py
CHANGED
@@ -10,10 +10,6 @@ from botocore.credentials import RefreshableCredentials
|
|
10
10
|
from botocore.session import Session as BotocoreSession
|
11
11
|
from locust_cloud.constants import DEFAULT_REGION_NAME
|
12
12
|
|
13
|
-
logging.basicConfig(
|
14
|
-
format="[LOCUST-CLOUD] %(levelname)s: %(message)s",
|
15
|
-
level=logging.INFO,
|
16
|
-
)
|
17
13
|
logger = logging.getLogger(__name__)
|
18
14
|
|
19
15
|
|