locust-cloud 1.9.0__py3-none-any.whl → 1.11.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
@@ -11,11 +11,6 @@ from flask_login import UserMixin, login_user
11
11
  from locust.html import render_template_from
12
12
  from locust_cloud import __version__
13
13
 
14
- REGION = os.environ.get("AWS_DEFAULT_REGION")
15
- DEPLOYER_URL = f"https://api.{REGION}.locust.cloud/1"
16
- ALLOW_SIGNUP = os.environ.get("ALLOW_SIGNUP", True)
17
-
18
-
19
14
  logger = logging.getLogger(__name__)
20
15
 
21
16
 
@@ -47,7 +42,7 @@ def set_credentials(username: str, credentials: Credentials, response: werkzeug.
47
42
 
48
43
 
49
44
  def register_auth(environment: locust.env.Environment):
50
- 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", "")
51
46
  environment.web_ui.app.debug = False
52
47
 
53
48
  web_base_path = environment.parsed_options.web_base_path
@@ -71,7 +66,7 @@ def register_auth(environment: locust.env.Environment):
71
66
  },
72
67
  )
73
68
 
74
- if ALLOW_SIGNUP:
69
+ if environment.parsed_options.allow_signup:
75
70
  environment.web_ui.auth_args["auth_providers"] = [
76
71
  {"label": "Sign Up", "callback_url": f"{web_base_path}/signup"}
77
72
  ]
@@ -83,7 +78,7 @@ def register_auth(environment: locust.env.Environment):
83
78
 
84
79
  try:
85
80
  auth_response = requests.post(
86
- f"{DEPLOYER_URL}/auth/login",
81
+ f"{environment.parsed_options.deployer_url}/auth/login",
87
82
  json={"username": username, "password": password},
88
83
  headers={"X-Client-Version": __version__},
89
84
  )
@@ -91,6 +86,11 @@ def register_auth(environment: locust.env.Environment):
91
86
  auth_response.raise_for_status()
92
87
 
93
88
  credentials = auth_response.json()
89
+
90
+ if os.getenv("CUSTOMER_ID", "") and credentials["customer_id"] != os.getenv("CUSTOMER_ID", ""):
91
+ session["auth_error"] = "Invalid login for this deployment"
92
+ return redirect(url_for("locust.login"))
93
+
94
94
  response = redirect(url_for("locust.index"))
95
95
  response = set_credentials(username, credentials, response)
96
96
  login_user(AuthUser(credentials["user_sub_id"]))
@@ -108,7 +108,7 @@ def register_auth(environment: locust.env.Environment):
108
108
 
109
109
  @auth_blueprint.route("/signup")
110
110
  def signup():
111
- if not ALLOW_SIGNUP:
111
+ if not environment.parsed_options.allow_signup:
112
112
  return redirect(url_for("locust.login"))
113
113
 
114
114
  if session.get("username"):
@@ -118,6 +118,7 @@ def register_auth(environment: locust.env.Environment):
118
118
  {
119
119
  "label": "Confirmation Code",
120
120
  "name": "confirmation_code",
121
+ "is_required": True,
121
122
  },
122
123
  ],
123
124
  "callback_url": f"{web_base_path}/confirm-signup",
@@ -131,20 +132,30 @@ def register_auth(environment: locust.env.Environment):
131
132
  {
132
133
  "label": "Username",
133
134
  "name": "username",
135
+ "is_required": True,
136
+ "type": "email",
137
+ },
138
+ {
139
+ "label": "Full Name",
140
+ "name": "customer_name",
141
+ "is_required": True,
134
142
  },
135
143
  {
136
144
  "label": "Password",
137
145
  "name": "password",
138
146
  "is_secret": True,
147
+ "is_required": True,
139
148
  },
140
149
  {
141
150
  "label": "Access Code",
142
151
  "name": "access_code",
152
+ "is_required": True,
143
153
  },
144
154
  {
145
- "label": "I consent to:\n\n1. Only test your own website/service or our example 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
+ "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.",
146
156
  "name": "consent",
147
157
  "default_value": False,
158
+ "is_required": True,
148
159
  },
149
160
  ],
150
161
  "callback_url": f"{web_base_path}/create-account",
@@ -164,32 +175,28 @@ def register_auth(environment: locust.env.Environment):
164
175
 
165
176
  @auth_blueprint.route("/create-account", methods=["POST"])
166
177
  def create_account():
167
- if not ALLOW_SIGNUP:
178
+ if not environment.parsed_options.allow_signup:
168
179
  return redirect(url_for("locust.login"))
169
180
 
170
181
  session["auth_sign_up_error"] = ""
171
182
  session["auth_info"] = ""
172
183
 
173
184
  username = request.form.get("username", "")
185
+ customer_name = request.form.get("customer_name", "")
174
186
  password = request.form.get("password")
175
187
  access_code = request.form.get("access_code")
176
- has_consented = request.form.get("consent")
177
-
178
- if not has_consented:
179
- session["auth_sign_up_error"] = "Please accept the terms and conditions to create an account."
180
-
181
- return redirect(url_for("locust_cloud_auth.signup"))
182
188
 
183
189
  try:
184
190
  auth_response = requests.post(
185
- f"{DEPLOYER_URL}/auth/signup",
191
+ f"{environment.parsed_options.deployer_url}/auth/signup",
186
192
  json={"username": username, "password": password, "access_code": access_code},
187
193
  )
188
194
 
189
195
  auth_response.raise_for_status()
190
196
 
191
- session["user_sub"] = auth_response.json().get("user_sub")
197
+ session["user_sub_id"] = auth_response.json().get("user_sub_id")
192
198
  session["username"] = username
199
+ session["customer_name"] = customer_name
193
200
  session["auth_info"] = (
194
201
  "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)"
195
202
  )
@@ -206,7 +213,7 @@ def register_auth(environment: locust.env.Environment):
206
213
  def resend_code():
207
214
  try:
208
215
  auth_response = requests.post(
209
- "http://localhost:8000/1/auth/resend-confirmation",
216
+ f"{environment.parsed_options.deployer_url}/auth/resend-confirmation",
210
217
  json={"username": session["username"]},
211
218
  )
212
219
 
@@ -225,7 +232,7 @@ def register_auth(environment: locust.env.Environment):
225
232
 
226
233
  @auth_blueprint.route("/confirm-signup", methods=["POST"])
227
234
  def confirm_signup():
228
- if not ALLOW_SIGNUP:
235
+ if not environment.parsed_options.allow_signup:
229
236
  return redirect(url_for("locust.login"))
230
237
 
231
238
  session["auth_sign_up_error"] = ""
@@ -233,10 +240,11 @@ def register_auth(environment: locust.env.Environment):
233
240
 
234
241
  try:
235
242
  auth_response = requests.post(
236
- f"{DEPLOYER_URL}/auth/confirm-signup",
243
+ f"{environment.parsed_options.deployer_url}/auth/confirm-signup",
237
244
  json={
238
245
  "username": session.get("username"),
239
- "user_sub": session["user_sub"],
246
+ "customer_name": session.get("customer_name"),
247
+ "user_sub_id": session["user_sub_id"],
240
248
  "confirmation_code": confirmation_code,
241
249
  },
242
250
  )
@@ -247,7 +255,7 @@ def register_auth(environment: locust.env.Environment):
247
255
  session["auth_info"] = "Account created successfully!"
248
256
  session["auth_sign_up_error"] = ""
249
257
 
250
- return redirect(url_for("locust.login"))
258
+ return redirect("https://docs.locust.cloud/")
251
259
  except requests.exceptions.HTTPError as e:
252
260
  message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
253
261
  session["auth_info"] = ""
locust_cloud/cloud.py CHANGED
@@ -148,6 +148,12 @@ parser.add_argument(
148
148
  default="latest",
149
149
  help=configargparse.SUPPRESS, # overrides the locust-cloud docker image tag. for internal use
150
150
  )
151
+ parser.add_argument(
152
+ "--mock-server",
153
+ action="store_true",
154
+ default=False,
155
+ help=configargparse.SUPPRESS,
156
+ )
151
157
 
152
158
  options, locust_options = parser.parse_known_args()
153
159
  options: Namespace
@@ -166,7 +172,7 @@ logging.getLogger("requests").setLevel(logging.INFO)
166
172
  logging.getLogger("urllib3").setLevel(logging.INFO)
167
173
 
168
174
 
169
- api_url = f"https://api.{options.region}.locust.cloud/1"
175
+ api_url = os.environ.get("LOCUSTCLOUD_DEPLOYER_URL", f"https://api.{options.region}.locust.cloud/1")
170
176
 
171
177
 
172
178
  def main() -> None:
@@ -175,6 +181,8 @@ def main() -> None:
175
181
  "Setting a region is required to use Locust Cloud. Please ensure the AWS_DEFAULT_REGION env variable or the --region flag is set."
176
182
  )
177
183
  sys.exit(1)
184
+ if options.region:
185
+ os.environ["AWS_DEFAULT_REGION"] = options.region
178
186
 
179
187
  s3_bucket = "dmdb-default" if options.region == "us-east-1" else "locust-default"
180
188
  deployments: list[Any] = []
@@ -203,17 +211,18 @@ def main() -> None:
203
211
  aws_session_token = credentials.get("token", "")
204
212
 
205
213
  if options.delete:
206
- delete(s3_bucket, credential_manager)
214
+ delete(credential_manager)
207
215
  return
208
216
 
209
217
  logger.info(f"Uploading {options.locustfile}")
210
218
  logger.debug(f"... to {s3_bucket}")
211
219
  s3 = credential_manager.session.client("s3")
212
220
  try:
213
- s3.upload_file(options.locustfile, s3_bucket, os.path.basename(options.locustfile))
221
+ filename = options.username + "__" + os.path.basename(options.locustfile)
222
+ s3.upload_file(options.locustfile, s3_bucket, filename)
214
223
  locustfile_url = s3.generate_presigned_url(
215
224
  ClientMethod="get_object",
216
- Params={"Bucket": s3_bucket, "Key": os.path.basename(options.locustfile)},
225
+ Params={"Bucket": s3_bucket, "Key": filename},
217
226
  ExpiresIn=3600,
218
227
  )
219
228
  logger.debug(f"Uploaded {options.locustfile} successfully")
@@ -228,10 +237,11 @@ def main() -> None:
228
237
  if options.requirements:
229
238
  logger.info(f"Uploading {options.requirements}")
230
239
  try:
231
- s3.upload_file(options.requirements, s3_bucket, "requirements.txt")
240
+ filename = options.username + "__" + "requirements.txt"
241
+ s3.upload_file(options.requirements, s3_bucket, filename)
232
242
  requirements_url = s3.generate_presigned_url(
233
243
  ClientMethod="get_object",
234
- Params={"Bucket": s3_bucket, "Key": "requirements.txt"},
244
+ Params={"Bucket": s3_bucket, "Key": filename},
235
245
  ExpiresIn=3600,
236
246
  )
237
247
  logger.debug(f"Uploaded {options.requirements} successfully")
@@ -268,6 +278,7 @@ def main() -> None:
268
278
  ],
269
279
  "user_count": options.users,
270
280
  "image_tag": options.image_tag,
281
+ "mock_server": options.mock_server,
271
282
  }
272
283
  if options.workers is not None:
273
284
  payload["worker_count"] = options.workers
@@ -377,10 +388,10 @@ def main() -> None:
377
388
  logger.exception(e)
378
389
  sys.exit(1)
379
390
  finally:
380
- delete(s3_bucket, credential_manager)
391
+ delete(credential_manager)
381
392
 
382
393
 
383
- def delete(s3_bucket, credential_manager):
394
+ def delete(credential_manager):
384
395
  try:
385
396
  logger.info("Tearing down Locust cloud...")
386
397
  credential_manager.refresh_credentials()
@@ -412,9 +423,9 @@ def delete(s3_bucket, credential_manager):
412
423
 
413
424
  try:
414
425
  logger.debug("Cleaning up locustfiles")
415
- s3 = credential_manager.session.resource("s3")
416
- bucket = s3.Bucket(s3_bucket)
417
- bucket.objects.all().delete()
426
+ # s3 = credential_manager.session.resource("s3")
427
+ # bucket = s3.Bucket(s3_bucket)
428
+ # bucket.objects.all().delete()
418
429
  except ClientError as e:
419
430
  logger.debug(f"Failed to clean up locust files: {e}")
420
431
  # sys.exit(1)
@@ -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
  """
@@ -274,7 +274,7 @@ SELECT
274
274
  max_users as "maxUsers",
275
275
  users_per_worker as "usersPerWorker"
276
276
  FROM customers
277
- WHERE customer = current_user
277
+ WHERE id = current_user
278
278
  """
279
279
 
280
280
  queries: dict["str", LiteralString] = {