locust-cloud 1.9.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
@@ -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["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
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,29 @@ def register_auth(environment: locust.env.Environment):
131
132
  {
132
133
  "label": "Username",
133
134
  "name": "username",
135
+ "is_required": True,
136
+ },
137
+ {
138
+ "label": "Full Name",
139
+ "name": "full_name",
140
+ "is_required": True,
134
141
  },
135
142
  {
136
143
  "label": "Password",
137
144
  "name": "password",
138
145
  "is_secret": True,
146
+ "is_required": True,
139
147
  },
140
148
  {
141
149
  "label": "Access Code",
142
150
  "name": "access_code",
151
+ "is_required": True,
143
152
  },
144
153
  {
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.",
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.",
146
155
  "name": "consent",
147
156
  "default_value": False,
157
+ "is_required": True,
148
158
  },
149
159
  ],
150
160
  "callback_url": f"{web_base_path}/create-account",
@@ -164,25 +174,20 @@ def register_auth(environment: locust.env.Environment):
164
174
 
165
175
  @auth_blueprint.route("/create-account", methods=["POST"])
166
176
  def create_account():
167
- if not ALLOW_SIGNUP:
177
+ if not environment.parsed_options.allow_signup:
168
178
  return redirect(url_for("locust.login"))
169
179
 
170
180
  session["auth_sign_up_error"] = ""
171
181
  session["auth_info"] = ""
172
182
 
173
183
  username = request.form.get("username", "")
184
+ full_name = request.form.get("full_name", "")
174
185
  password = request.form.get("password")
175
186
  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
187
 
183
188
  try:
184
189
  auth_response = requests.post(
185
- f"{DEPLOYER_URL}/auth/signup",
190
+ f"{environment.parsed_options.deployer_url}/auth/signup",
186
191
  json={"username": username, "password": password, "access_code": access_code},
187
192
  )
188
193
 
@@ -190,6 +195,7 @@ def register_auth(environment: locust.env.Environment):
190
195
 
191
196
  session["user_sub"] = auth_response.json().get("user_sub")
192
197
  session["username"] = username
198
+ session["full_name"] = full_name
193
199
  session["auth_info"] = (
194
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)"
195
201
  )
@@ -206,7 +212,7 @@ def register_auth(environment: locust.env.Environment):
206
212
  def resend_code():
207
213
  try:
208
214
  auth_response = requests.post(
209
- "http://localhost:8000/1/auth/resend-confirmation",
215
+ f"{environment.parsed_options.deployer_url}/1/auth/resend-confirmation",
210
216
  json={"username": session["username"]},
211
217
  )
212
218
 
@@ -225,7 +231,7 @@ def register_auth(environment: locust.env.Environment):
225
231
 
226
232
  @auth_blueprint.route("/confirm-signup", methods=["POST"])
227
233
  def confirm_signup():
228
- if not ALLOW_SIGNUP:
234
+ if not environment.parsed_options.allow_signup:
229
235
  return redirect(url_for("locust.login"))
230
236
 
231
237
  session["auth_sign_up_error"] = ""
@@ -233,9 +239,10 @@ def register_auth(environment: locust.env.Environment):
233
239
 
234
240
  try:
235
241
  auth_response = requests.post(
236
- f"{DEPLOYER_URL}/auth/confirm-signup",
242
+ f"{environment.parsed_options.deployer_url}/auth/confirm-signup",
237
243
  json={
238
244
  "username": session.get("username"),
245
+ "full_name": session.get("full_name"),
239
246
  "user_sub": session["user_sub"],
240
247
  "confirmation_code": confirmation_code,
241
248
  },
@@ -247,7 +254,7 @@ def register_auth(environment: locust.env.Environment):
247
254
  session["auth_info"] = "Account created successfully!"
248
255
  session["auth_sign_up_error"] = ""
249
256
 
250
- return redirect(url_for("locust.login"))
257
+ return redirect("https://docs.locust.cloud/")
251
258
  except requests.exceptions.HTTPError as e:
252
259
  message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
253
260
  session["auth_info"] = ""
locust_cloud/cloud.py CHANGED
@@ -102,7 +102,7 @@ advanced.add_argument(
102
102
  advanced.add_argument(
103
103
  "--region",
104
104
  type=str,
105
- default=os.environ.get("AWS_DEFAULT_REGION"),
105
+ default="eu-north-1", # temp setup for pycon # os.environ.get("AWS_DEFAULT_REGION")
106
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.",
107
107
  )
108
108
  parser.add_argument(
@@ -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
@@ -203,17 +209,18 @@ def main() -> None:
203
209
  aws_session_token = credentials.get("token", "")
204
210
 
205
211
  if options.delete:
206
- delete(s3_bucket, credential_manager)
212
+ delete(credential_manager)
207
213
  return
208
214
 
209
215
  logger.info(f"Uploading {options.locustfile}")
210
216
  logger.debug(f"... to {s3_bucket}")
211
217
  s3 = credential_manager.session.client("s3")
212
218
  try:
213
- 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)
214
221
  locustfile_url = s3.generate_presigned_url(
215
222
  ClientMethod="get_object",
216
- Params={"Bucket": s3_bucket, "Key": os.path.basename(options.locustfile)},
223
+ Params={"Bucket": s3_bucket, "Key": filename},
217
224
  ExpiresIn=3600,
218
225
  )
219
226
  logger.debug(f"Uploaded {options.locustfile} successfully")
@@ -228,10 +235,11 @@ def main() -> None:
228
235
  if options.requirements:
229
236
  logger.info(f"Uploading {options.requirements}")
230
237
  try:
231
- s3.upload_file(options.requirements, s3_bucket, "requirements.txt")
238
+ filename = options.username + "__" + "requirements.txt"
239
+ s3.upload_file(options.requirements, s3_bucket, filename)
232
240
  requirements_url = s3.generate_presigned_url(
233
241
  ClientMethod="get_object",
234
- Params={"Bucket": s3_bucket, "Key": "requirements.txt"},
242
+ Params={"Bucket": s3_bucket, "Key": filename},
235
243
  ExpiresIn=3600,
236
244
  )
237
245
  logger.debug(f"Uploaded {options.requirements} successfully")
@@ -268,6 +276,7 @@ def main() -> None:
268
276
  ],
269
277
  "user_count": options.users,
270
278
  "image_tag": options.image_tag,
279
+ "mock_server": options.mock_server,
271
280
  }
272
281
  if options.workers is not None:
273
282
  payload["worker_count"] = options.workers
@@ -377,10 +386,10 @@ def main() -> None:
377
386
  logger.exception(e)
378
387
  sys.exit(1)
379
388
  finally:
380
- delete(s3_bucket, credential_manager)
389
+ delete(credential_manager)
381
390
 
382
391
 
383
- def delete(s3_bucket, credential_manager):
392
+ def delete(credential_manager):
384
393
  try:
385
394
  logger.info("Tearing down Locust cloud...")
386
395
  credential_manager.refresh_credentials()
@@ -412,9 +421,9 @@ def delete(s3_bucket, credential_manager):
412
421
 
413
422
  try:
414
423
  logger.debug("Cleaning up locustfiles")
415
- s3 = credential_manager.session.resource("s3")
416
- bucket = s3.Bucket(s3_bucket)
417
- bucket.objects.all().delete()
424
+ # s3 = credential_manager.session.resource("s3")
425
+ # bucket = s3.Bucket(s3_bucket)
426
+ # bucket.objects.all().delete()
418
427
  except ClientError as e:
419
428
  logger.debug(f"Failed to clean up locust files: {e}")
420
429
  # 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
  """