locust-cloud 1.8.0__py3-none-any.whl → 1.10.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/__init__.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import importlib.metadata
2
2
  import os
3
+ import sys
3
4
 
4
5
  os.environ["LOCUST_SKIP_MONKEY_PATCH"] = "1"
5
6
  __version__ = importlib.metadata.version("locust-cloud")
6
7
 
7
- import argparse
8
8
  import logging
9
9
 
10
10
  import configargparse
@@ -19,38 +19,29 @@ from locust_cloud.timescale.query import register_query
19
19
  from psycopg.conninfo import make_conninfo
20
20
  from psycopg_pool import ConnectionPool
21
21
 
22
- GRAPH_VIEWER = os.environ.get("GRAPH_VIEWER")
23
22
  logger = logging.getLogger(__name__)
24
23
 
25
24
 
26
25
  @events.init_command_line_parser.add_listener
27
26
  def add_arguments(parser: LocustArgumentParser):
28
- if not (os.environ.get("PGHOST") or GRAPH_VIEWER):
27
+ if not (os.environ.get("PGHOST")):
29
28
  parser.add_argument_group(
30
29
  "locust-cloud",
31
30
  "locust-cloud disabled, because PGHOST was not set - this is normal for local runs",
32
31
  )
33
32
  return
34
33
 
34
+ try:
35
+ REGION = os.environ["AWS_DEFAULT_REGION"]
36
+ except KeyError:
37
+ logger.fatal("Missing AWS_DEFAULT_REGION env var")
38
+ sys.exit(1)
39
+
35
40
  os.environ["LOCUST_BUILD_PATH"] = os.path.join(os.path.dirname(__file__), "webui/dist")
36
41
  locust_cloud = parser.add_argument_group(
37
42
  "locust-cloud",
38
43
  "Arguments for use with Locust cloud",
39
44
  )
40
- locust_cloud.add_argument(
41
- "--exporter",
42
- default=True,
43
- action=argparse.BooleanOptionalAction,
44
- env_var="LOCUST_EXPORTER",
45
- help="Exports Locust stats to Timescale",
46
- )
47
- locust_cloud.add_argument(
48
- "--description",
49
- type=str,
50
- env_var="LOCUST_DESCRIPTION",
51
- default="",
52
- help="Description of the test being run",
53
- )
54
45
  # do not set
55
46
  # used for sending the run id from master to workers
56
47
  locust_cloud.add_argument(
@@ -59,6 +50,27 @@ def add_arguments(parser: LocustArgumentParser):
59
50
  env_var="LOCUSTCLOUD_RUN_ID",
60
51
  help=configargparse.SUPPRESS,
61
52
  )
53
+ locust_cloud.add_argument(
54
+ "--allow-signup",
55
+ env_var="LOCUSTCLOUD_ALLOW_SIGNUP",
56
+ help=configargparse.SUPPRESS,
57
+ default=False,
58
+ action="store_true",
59
+ )
60
+ locust_cloud.add_argument(
61
+ "--graph-viewer",
62
+ env_var="LOCUSTCLOUD_GRAPH_VIEWER",
63
+ help=configargparse.SUPPRESS,
64
+ default=False,
65
+ action="store_true",
66
+ )
67
+ locust_cloud.add_argument(
68
+ "--deployer-url",
69
+ type=str,
70
+ env_var="LOCUSTCLOUD_DEPLOYER_URL",
71
+ help=configargparse.SUPPRESS,
72
+ default=f"https://api.{REGION}.locust.cloud/1",
73
+ )
62
74
 
63
75
 
64
76
  def set_autocommit(conn: psycopg.Connection):
@@ -86,14 +98,12 @@ def on_locust_init(environment: locust.env.Environment, **_args):
86
98
  logger.exception(e)
87
99
  raise
88
100
 
89
- if not GRAPH_VIEWER:
101
+ if not environment.parsed_options.graph_viewer:
90
102
  IdleExit(environment)
91
-
92
- if not GRAPH_VIEWER and environment.parsed_options and environment.parsed_options.exporter:
93
103
  Exporter(environment, pool)
94
104
 
95
105
  if environment.web_ui:
96
- if GRAPH_VIEWER:
106
+ if environment.parsed_options.graph_viewer:
97
107
  environment.web_ui.template_args["isGraphViewer"] = True
98
108
 
99
109
  register_auth(environment)
locust_cloud/auth.py CHANGED
@@ -1,17 +1,15 @@
1
1
  import logging
2
2
  import os
3
3
  from datetime import UTC, datetime, timedelta
4
- from typing import TypedDict
4
+ from typing import Any, TypedDict, cast
5
5
 
6
6
  import locust.env
7
7
  import requests
8
8
  import werkzeug
9
- from flask import redirect, request, url_for
9
+ from flask import Blueprint, redirect, request, session, url_for
10
10
  from flask_login import UserMixin, login_user
11
+ from locust.html import render_template_from
11
12
  from locust_cloud import __version__
12
- from locust_cloud.constants import DEFAULT_DEPLOYER_URL
13
-
14
- DEPLOYER_URL = os.environ.get("LOCUSTCLOUD_DEPLOYER_URL", DEFAULT_DEPLOYER_URL)
15
13
 
16
14
  logger = logging.getLogger(__name__)
17
15
 
@@ -44,9 +42,12 @@ def set_credentials(username: str, credentials: Credentials, response: werkzeug.
44
42
 
45
43
 
46
44
  def register_auth(environment: locust.env.Environment):
47
- environment.web_ui.app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
45
+ environment.web_ui.app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "") + os.getenv("CUSTOMER_ID", "")
48
46
  environment.web_ui.app.debug = False
49
47
 
48
+ web_base_path = environment.parsed_options.web_base_path
49
+ auth_blueprint = Blueprint("locust_cloud_auth", __name__, url_prefix=web_base_path)
50
+
50
51
  def load_user(user_sub_id: str):
51
52
  username = request.cookies.get("username")
52
53
  refresh_token = request.cookies.get("user_token")
@@ -58,18 +59,26 @@ def register_auth(environment: locust.env.Environment):
58
59
  return None
59
60
 
60
61
  environment.web_ui.login_manager.user_loader(load_user)
61
- environment.web_ui.auth_args = {
62
- "username_password_callback": "/authenticate",
63
- }
62
+ environment.web_ui.auth_args = cast(
63
+ Any,
64
+ {
65
+ "username_password_callback": f"{web_base_path}/authenticate",
66
+ },
67
+ )
68
+
69
+ if environment.parsed_options.allow_signup:
70
+ environment.web_ui.auth_args["auth_providers"] = [
71
+ {"label": "Sign Up", "callback_url": f"{web_base_path}/signup"}
72
+ ]
64
73
 
65
- @environment.web_ui.app.route("/authenticate", methods=["POST"])
74
+ @auth_blueprint.route("/authenticate", methods=["POST"])
66
75
  def login_submit():
67
76
  username = request.form.get("username", "")
68
77
  password = request.form.get("password")
69
78
 
70
79
  try:
71
80
  auth_response = requests.post(
72
- f"{DEPLOYER_URL}/auth/login",
81
+ f"{environment.parsed_options.deployer_url}/auth/login",
73
82
  json={"username": username, "password": password},
74
83
  headers={"X-Client-Version": __version__},
75
84
  )
@@ -77,19 +86,180 @@ def register_auth(environment: locust.env.Environment):
77
86
  auth_response.raise_for_status()
78
87
 
79
88
  credentials = auth_response.json()
80
- response = redirect(url_for("index"))
89
+
90
+ if os.getenv("CUSTOMER_ID", "") and credentials["user_sub_id"] != os.getenv("CUSTOMER_ID", ""):
91
+ session["auth_error"] = "Invalid login for this deployment"
92
+ return redirect(url_for("locust.login"))
93
+
94
+ response = redirect(url_for("locust.index"))
81
95
  response = set_credentials(username, credentials, response)
82
96
  login_user(AuthUser(credentials["user_sub_id"]))
83
97
 
84
98
  return response
85
99
  except requests.exceptions.HTTPError as e:
86
100
  if e.response.status_code == 401:
87
- environment.web_ui.auth_args["error"] = "Invalid username or password"
101
+ session["auth_error"] = "Invalid username or password"
88
102
  else:
89
103
  logger.error(f"Unknown response from auth: {e.response.status_code} {e.response.text}")
90
104
 
91
- environment.web_ui.auth_args["error"] = (
92
- "Unknown error during authentication, check logs and/or contact support"
93
- )
105
+ session["auth_error"] = "Unknown error during authentication, check logs and/or contact support"
106
+
107
+ return redirect(url_for("locust.login"))
108
+
109
+ @auth_blueprint.route("/signup")
110
+ def signup():
111
+ if not environment.parsed_options.allow_signup:
112
+ return redirect(url_for("locust.login"))
113
+
114
+ if session.get("username"):
115
+ sign_up_args = {
116
+ "custom_form": {
117
+ "inputs": [
118
+ {
119
+ "label": "Confirmation Code",
120
+ "name": "confirmation_code",
121
+ "is_required": True,
122
+ },
123
+ ],
124
+ "callback_url": f"{web_base_path}/confirm-signup",
125
+ "submit_button_text": "Confirm Email",
126
+ },
127
+ }
128
+ else:
129
+ sign_up_args = {
130
+ "custom_form": {
131
+ "inputs": [
132
+ {
133
+ "label": "Username",
134
+ "name": "username",
135
+ "is_required": True,
136
+ },
137
+ {
138
+ "label": "Full Name",
139
+ "name": "full_name",
140
+ "is_required": True,
141
+ },
142
+ {
143
+ "label": "Password",
144
+ "name": "password",
145
+ "is_secret": True,
146
+ "is_required": True,
147
+ },
148
+ {
149
+ "label": "Access Code",
150
+ "name": "access_code",
151
+ "is_required": True,
152
+ },
153
+ {
154
+ "label": "I consent to:\n\n1. Only test your own website/service or our example target\n\n2. Only use locust-cloud for its intended purpose: to load test other sites/services.\n\n3. Not attempt to circumvent your account limitations (e.g. max user count or max request count)\n\n4. Not use personal data (real names, addresses etc) in your tests.",
155
+ "name": "consent",
156
+ "default_value": False,
157
+ "is_required": True,
158
+ },
159
+ ],
160
+ "callback_url": f"{web_base_path}/create-account",
161
+ "submit_button_text": "Sign Up",
162
+ },
163
+ }
164
+
165
+ if session.get("auth_info"):
166
+ sign_up_args["info"] = session["auth_info"]
167
+ if session.get("auth_sign_up_error"):
168
+ sign_up_args["error"] = session["auth_sign_up_error"]
169
+
170
+ return render_template_from(
171
+ "auth.html",
172
+ auth_args=sign_up_args,
173
+ )
174
+
175
+ @auth_blueprint.route("/create-account", methods=["POST"])
176
+ def create_account():
177
+ if not environment.parsed_options.allow_signup:
178
+ return redirect(url_for("locust.login"))
179
+
180
+ session["auth_sign_up_error"] = ""
181
+ session["auth_info"] = ""
182
+
183
+ username = request.form.get("username", "")
184
+ full_name = request.form.get("full_name", "")
185
+ password = request.form.get("password")
186
+ access_code = request.form.get("access_code")
187
+
188
+ try:
189
+ auth_response = requests.post(
190
+ f"{environment.parsed_options.deployer_url}/auth/signup",
191
+ json={"username": username, "password": password, "access_code": access_code},
192
+ )
193
+
194
+ auth_response.raise_for_status()
195
+
196
+ session["user_sub"] = auth_response.json().get("user_sub")
197
+ session["username"] = username
198
+ session["full_name"] = full_name
199
+ session["auth_info"] = (
200
+ "Please check your email and enter the confirmation code. If you didn't get a code after one minute, you can [request a new one](/resend-code)"
201
+ )
202
+
203
+ return redirect(url_for("locust_cloud_auth.signup"))
204
+ except requests.exceptions.HTTPError as e:
205
+ message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
206
+ session["auth_info"] = ""
207
+ session["auth_sign_up_error"] = message
208
+
209
+ return redirect(url_for("locust_cloud_auth.signup"))
210
+
211
+ @auth_blueprint.route("/resend-code")
212
+ def resend_code():
213
+ try:
214
+ auth_response = requests.post(
215
+ f"{environment.parsed_options.deployer_url}/1/auth/resend-confirmation",
216
+ json={"username": session["username"]},
217
+ )
218
+
219
+ auth_response.raise_for_status()
220
+
221
+ session["auth_sign_up_error"] = ""
222
+ session["auth_info"] = "Confirmation code sent, please check your email."
223
+
224
+ return redirect(url_for("locust_cloud_auth.signup"))
225
+ except requests.exceptions.HTTPError as e:
226
+ message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
227
+ session["auth_info"] = ""
228
+ session["auth_sign_up_error"] = message
229
+
230
+ return redirect(url_for("locust_cloud_auth.signup"))
231
+
232
+ @auth_blueprint.route("/confirm-signup", methods=["POST"])
233
+ def confirm_signup():
234
+ if not environment.parsed_options.allow_signup:
235
+ return redirect(url_for("locust.login"))
236
+
237
+ session["auth_sign_up_error"] = ""
238
+ confirmation_code = request.form.get("confirmation_code")
239
+
240
+ try:
241
+ auth_response = requests.post(
242
+ f"{environment.parsed_options.deployer_url}/auth/confirm-signup",
243
+ json={
244
+ "username": session.get("username"),
245
+ "full_name": session.get("full_name"),
246
+ "user_sub": session["user_sub"],
247
+ "confirmation_code": confirmation_code,
248
+ },
249
+ )
250
+
251
+ auth_response.raise_for_status()
252
+
253
+ session["username"] = None
254
+ session["auth_info"] = "Account created successfully!"
255
+ session["auth_sign_up_error"] = ""
256
+
257
+ return redirect("https://docs.locust.cloud/")
258
+ except requests.exceptions.HTTPError as e:
259
+ message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
260
+ session["auth_info"] = ""
261
+ session["auth_sign_up_error"] = message
262
+
263
+ return redirect(url_for("locust_cloud_auth.signup"))
94
264
 
95
- return redirect(url_for("login"))
265
+ environment.web_ui.app.register_blueprint(auth_blueprint)
locust_cloud/cloud.py CHANGED
@@ -1,6 +1,5 @@
1
1
  import json
2
2
  import logging
3
- import math
4
3
  import os
5
4
  import sys
6
5
  import time
@@ -14,13 +13,6 @@ import configargparse
14
13
  import requests
15
14
  from botocore.exceptions import ClientError
16
15
  from locust_cloud import __version__
17
- from locust_cloud.constants import (
18
- DEFAULT_CLUSTER_NAME,
19
- DEFAULT_DEPLOYER_URL,
20
- DEFAULT_NAMESPACE,
21
- DEFAULT_REGION_NAME,
22
- USERS_PER_WORKER,
23
- )
24
16
  from locust_cloud.credential_manager import CredentialError, CredentialManager
25
17
 
26
18
 
@@ -110,29 +102,9 @@ advanced.add_argument(
110
102
  advanced.add_argument(
111
103
  "--region",
112
104
  type=str,
113
- default=os.environ.get("AWS_DEFAULT_REGION", DEFAULT_REGION_NAME),
105
+ default="eu-north-1", # temp setup for pycon # os.environ.get("AWS_DEFAULT_REGION")
114
106
  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
107
  )
116
- advanced.add_argument(
117
- "--kube-cluster-name",
118
- type=str,
119
- default=DEFAULT_CLUSTER_NAME,
120
- help=configargparse.SUPPRESS,
121
- env_var="KUBE_CLUSTER_NAME",
122
- )
123
- advanced.add_argument(
124
- "--kube-namespace",
125
- type=str,
126
- default=DEFAULT_NAMESPACE,
127
- help=configargparse.SUPPRESS,
128
- env_var="KUBE_NAMESPACE",
129
- )
130
- parser.add_argument(
131
- "--deployer-url",
132
- type=str,
133
- default=DEFAULT_DEPLOYER_URL,
134
- help=configargparse.SUPPRESS,
135
- )
136
108
  parser.add_argument(
137
109
  "--aws-access-key-id",
138
110
  type=str,
@@ -162,7 +134,7 @@ parser.add_argument(
162
134
  parser.add_argument(
163
135
  "--workers",
164
136
  type=int,
165
- help=f"Number of workers to use for the deployment. Defaults to number of users divided by {USERS_PER_WORKER}.",
137
+ help="Number of workers to use for the deployment. Defaults to number of users divided by 500, but the default may be customized for your account.",
166
138
  default=None,
167
139
  )
168
140
  parser.add_argument(
@@ -176,6 +148,12 @@ parser.add_argument(
176
148
  default="latest",
177
149
  help=configargparse.SUPPRESS, # overrides the locust-cloud docker image tag. for internal use
178
150
  )
151
+ parser.add_argument(
152
+ "--mock-server",
153
+ action="store_true",
154
+ default=False,
155
+ help=configargparse.SUPPRESS,
156
+ )
179
157
 
180
158
  options, locust_options = parser.parse_known_args()
181
159
  options: Namespace
@@ -194,25 +172,30 @@ logging.getLogger("requests").setLevel(logging.INFO)
194
172
  logging.getLogger("urllib3").setLevel(logging.INFO)
195
173
 
196
174
 
175
+ api_url = f"https://api.{options.region}.locust.cloud/1"
176
+
177
+
197
178
  def main() -> None:
198
- s3_bucket = f"{options.kube_cluster_name}-{options.kube_namespace}"
179
+ if not options.region:
180
+ logger.error(
181
+ "Setting a region is required to use Locust Cloud. Please ensure the AWS_DEFAULT_REGION env variable or the --region flag is set."
182
+ )
183
+ sys.exit(1)
184
+
185
+ s3_bucket = "dmdb-default" if options.region == "us-east-1" else "locust-default"
199
186
  deployments: list[Any] = []
200
- worker_count: int = max(options.workers or math.ceil(options.users / USERS_PER_WORKER), 2)
201
- os.environ["AWS_DEFAULT_REGION"] = options.region
202
187
 
203
- try:
204
- if not (
205
- (options.username and options.password) or (options.aws_access_key_id and options.aws_secret_access_key)
206
- ):
207
- logger.error(
208
- "Authentication is required to use Locust Cloud. Please ensure the LOCUST_CLOUD_USERNAME and LOCUST_CLOUD_PASSWORD environment variables are set."
209
- )
210
- sys.exit(1)
188
+ if not ((options.username and options.password) or (options.aws_access_key_id and options.aws_secret_access_key)):
189
+ logger.error(
190
+ "Authentication is required to use Locust Cloud. Please ensure the LOCUST_CLOUD_USERNAME and LOCUST_CLOUD_PASSWORD environment variables are set."
191
+ )
192
+ sys.exit(1)
211
193
 
212
- logger.info(f"Authenticating ({os.environ['AWS_DEFAULT_REGION']}, v{__version__})")
213
- logger.debug(f"Lambda url: {options.deployer_url}")
194
+ try:
195
+ logger.info(f"Authenticating ({options.region}, v{__version__})")
196
+ logger.debug(f"Lambda url: {api_url}")
214
197
  credential_manager = CredentialManager(
215
- lambda_url=options.deployer_url,
198
+ lambda_url=api_url,
216
199
  access_key=options.aws_access_key_id,
217
200
  secret_key=options.aws_secret_access_key,
218
201
  username=options.username,
@@ -226,17 +209,18 @@ def main() -> None:
226
209
  aws_session_token = credentials.get("token", "")
227
210
 
228
211
  if options.delete:
229
- delete(s3_bucket, credential_manager)
212
+ delete(credential_manager)
230
213
  return
231
214
 
232
215
  logger.info(f"Uploading {options.locustfile}")
233
216
  logger.debug(f"... to {s3_bucket}")
234
217
  s3 = credential_manager.session.client("s3")
235
218
  try:
236
- s3.upload_file(options.locustfile, s3_bucket, os.path.basename(options.locustfile))
219
+ filename = options.username + "__" + os.path.basename(options.locustfile)
220
+ s3.upload_file(options.locustfile, s3_bucket, filename)
237
221
  locustfile_url = s3.generate_presigned_url(
238
222
  ClientMethod="get_object",
239
- Params={"Bucket": s3_bucket, "Key": os.path.basename(options.locustfile)},
223
+ Params={"Bucket": s3_bucket, "Key": filename},
240
224
  ExpiresIn=3600,
241
225
  )
242
226
  logger.debug(f"Uploaded {options.locustfile} successfully")
@@ -251,10 +235,11 @@ def main() -> None:
251
235
  if options.requirements:
252
236
  logger.info(f"Uploading {options.requirements}")
253
237
  try:
254
- s3.upload_file(options.requirements, s3_bucket, "requirements.txt")
238
+ filename = options.username + "__" + "requirements.txt"
239
+ s3.upload_file(options.requirements, s3_bucket, filename)
255
240
  requirements_url = s3.generate_presigned_url(
256
241
  ClientMethod="get_object",
257
- Params={"Bucket": s3_bucket, "Key": "requirements.txt"},
242
+ Params={"Bucket": s3_bucket, "Key": filename},
258
243
  ExpiresIn=3600,
259
244
  )
260
245
  logger.debug(f"Uploaded {options.requirements} successfully")
@@ -279,20 +264,22 @@ def main() -> None:
279
264
  ]
280
265
  and os.environ[env_variable]
281
266
  ]
282
- deploy_endpoint = f"{options.deployer_url}/{options.kube_cluster_name}"
267
+ deploy_endpoint = f"{api_url}/deploy"
283
268
  payload = {
284
269
  "locust_args": [
285
270
  {"name": "LOCUST_LOCUSTFILE", "value": locustfile_url},
286
271
  {"name": "LOCUST_USERS", "value": str(options.users)},
287
272
  {"name": "LOCUST_FLAGS", "value": " ".join(locust_options)},
288
273
  {"name": "LOCUSTCLOUD_REQUIREMENTS_URL", "value": requirements_url},
289
- {"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": options.deployer_url},
274
+ {"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": api_url},
290
275
  *locust_env_variables,
291
276
  ],
292
- "worker_count": worker_count,
293
277
  "user_count": options.users,
294
278
  "image_tag": options.image_tag,
279
+ "mock_server": options.mock_server,
295
280
  }
281
+ if options.workers is not None:
282
+ payload["worker_count"] = options.workers
296
283
  headers = {
297
284
  "Authorization": f"Bearer {cognito_client_id_token}",
298
285
  "Content-Type": "application/json",
@@ -325,7 +312,7 @@ def main() -> None:
325
312
  logger.debug("Interrupted by user")
326
313
  sys.exit(0)
327
314
 
328
- log_group_name = f"/eks/{options.kube_cluster_name}-{options.kube_namespace}"
315
+ log_group_name = "/eks/dmdb-default" if options.region == "us-east-1" else "/eks/locust-default"
329
316
  master_pod_name = next((deployment for deployment in deployments if "master" in deployment), None)
330
317
 
331
318
  if not master_pod_name:
@@ -399,10 +386,10 @@ def main() -> None:
399
386
  logger.exception(e)
400
387
  sys.exit(1)
401
388
  finally:
402
- delete(s3_bucket, credential_manager)
389
+ delete(credential_manager)
403
390
 
404
391
 
405
- def delete(s3_bucket, credential_manager):
392
+ def delete(credential_manager):
406
393
  try:
407
394
  logger.info("Tearing down Locust cloud...")
408
395
  credential_manager.refresh_credentials()
@@ -420,9 +407,8 @@ def delete(s3_bucket, credential_manager):
420
407
  headers["AWS_SESSION_TOKEN"] = token
421
408
 
422
409
  response = requests.delete(
423
- f"{options.deployer_url}/{options.kube_cluster_name}",
410
+ f"{api_url}/teardown",
424
411
  headers=headers,
425
- params={"namespace": options.kube_namespace} if options.kube_namespace else {},
426
412
  )
427
413
 
428
414
  if response.status_code != 200:
@@ -435,9 +421,9 @@ def delete(s3_bucket, credential_manager):
435
421
 
436
422
  try:
437
423
  logger.debug("Cleaning up locustfiles")
438
- s3 = credential_manager.session.resource("s3")
439
- bucket = s3.Bucket(s3_bucket)
440
- bucket.objects.all().delete()
424
+ # s3 = credential_manager.session.resource("s3")
425
+ # bucket = s3.Bucket(s3_bucket)
426
+ # bucket.objects.all().delete()
441
427
  except ClientError as e:
442
428
  logger.debug(f"Failed to clean up locust files: {e}")
443
429
  # sys.exit(1)
@@ -100,13 +100,16 @@ class CredentialManager:
100
100
 
101
101
  except requests.exceptions.HTTPError as http_err:
102
102
  response = http_err.response
103
- if response is not None and response.status_code == 401:
103
+ if response is None:
104
+ raise CredentialError("Response was None?!") from http_err
105
+
106
+ if response.status_code == 401:
104
107
  raise CredentialError("Incorrect username or password.") from http_err
105
108
  else:
106
109
  if js := response.json():
107
110
  if message := js.get("Message"):
108
111
  raise CredentialError(message)
109
- error_info = f"HTTP {response.status_code} {response.reason}" if response else "No response received."
112
+ error_info = f"HTTP {response.status_code} {response.reason}"
110
113
  raise CredentialError(f"HTTP error occurred while obtaining credentials: {error_info}") from http_err
111
114
  except requests.exceptions.RequestException as req_err:
112
115
  raise CredentialError(f"Request exception occurred while obtaining credentials: {req_err}") from req_err
@@ -217,7 +217,7 @@ class Exporter:
217
217
  cmd = sys.argv[1:]
218
218
  with self.pool.connection() as conn:
219
219
  conn.execute(
220
- "INSERT INTO testruns (id, num_users, worker_count, username, locustfile, description, arguments, customer) VALUES (%s,%s,%s,%s,%s,%s,%s,current_user)",
220
+ "INSERT INTO testruns (id, num_users, worker_count, username, locustfile, arguments, customer) VALUES (%s,%s,%s,%s,%s,%s,current_user)",
221
221
  (
222
222
  self._run_id,
223
223
  self.env.runner.target_user_count if self.env.runner else 1,
@@ -228,8 +228,7 @@ class Exporter:
228
228
  )
229
229
  else 0,
230
230
  self.env.web_ui.template_args.get("username", "") if self.env.web_ui else "",
231
- self.env.parsed_locustfiles[0].split("/")[-1],
232
- self.env.parsed_options.description,
231
+ self.env.parsed_locustfiles[0].split("/")[-1].split("__")[-1],
233
232
  " ".join(cmd),
234
233
  ),
235
234
  )
@@ -262,7 +262,7 @@ ORDER BY a.time
262
262
 
263
263
  total_vuh = """
264
264
  SELECT
265
- SUM((end_time - id) * num_users) AS "totalVuh"
265
+ COALESCE(SUM((end_time - id) * num_users), '0') AS "totalVuh"
266
266
  FROM testruns
267
267
  WHERE id >= date_trunc('month', NOW()) AND NOT refund
268
268
  """
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
 
3
- from flask import make_response, request
3
+ from flask import Blueprint, make_response, request
4
4
  from flask_login import login_required
5
5
  from locust_cloud.timescale.queries import queries
6
6
 
@@ -12,7 +12,11 @@ def adapt_timestamp(result):
12
12
 
13
13
 
14
14
  def register_query(environment, pool):
15
- @environment.web_ui.app.route("/cloud-stats/<query>", methods=["POST"])
15
+ cloud_stats_blueprint = Blueprint(
16
+ "locust_cloud_stats", __name__, url_prefix=environment.parsed_options.web_base_path
17
+ )
18
+
19
+ @cloud_stats_blueprint.route("/cloud-stats/<query>", methods=["POST"])
16
20
  @login_required
17
21
  def query(query):
18
22
  results = []
@@ -57,7 +61,9 @@ def register_query(environment, pool):
57
61
  return results
58
62
  else:
59
63
  logger.warning(f"Received invalid query key: '{query}'")
60
- return make_response("Invalid query key", 401)
64
+ return make_response({"error": "Invalid query key"}, 401)
61
65
  except Exception as e:
62
66
  logger.info(f"Error executing UI query '{query}': {e}", exc_info=True)
63
- return make_response("Error executing query", 401)
67
+ return make_response({"error": "Error executing query"}, 401)
68
+
69
+ environment.web_ui.app.register_blueprint(cloud_stats_blueprint)
@@ -29,7 +29,7 @@
29
29
  "group": "internal",
30
30
  },
31
31
  {
32
- "pattern": "{components,hooks,redux,utils}/**",
32
+ "pattern": "{components,hooks,redux,types,utils}/**",
33
33
  "group": "internal",
34
34
  },
35
35
  ],