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 +31 -21
- locust_cloud/auth.py +32 -24
- locust_cloud/cloud.py +22 -11
- locust_cloud/timescale/exporter.py +2 -3
- locust_cloud/timescale/queries.py +2 -2
- locust_cloud/webui/dist/assets/{index-DVpRWzFO.js → index-DkFumpSS.js} +34 -34
- locust_cloud/webui/dist/index.html +1 -1
- locust_cloud/webui/tsconfig.tsbuildinfo +1 -1
- {locust_cloud-1.9.0.dist-info → locust_cloud-1.11.0.dist-info}/METADATA +1 -1
- locust_cloud-1.11.0.dist-info/RECORD +23 -0
- {locust_cloud-1.9.0.dist-info → locust_cloud-1.11.0.dist-info}/WHEEL +1 -1
- locust_cloud-1.9.0.dist-info/RECORD +0 -23
- {locust_cloud-1.9.0.dist-info → locust_cloud-1.11.0.dist-info}/entry_points.txt +0 -0
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")
|
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
|
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
|
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
|
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"{
|
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
|
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
|
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
|
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"{
|
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["
|
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
|
-
"
|
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
|
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"{
|
243
|
+
f"{environment.parsed_options.deployer_url}/auth/confirm-signup",
|
237
244
|
json={
|
238
245
|
"username": session.get("username"),
|
239
|
-
"
|
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(
|
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(
|
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
|
-
|
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":
|
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
|
-
|
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":
|
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(
|
391
|
+
delete(credential_manager)
|
381
392
|
|
382
393
|
|
383
|
-
def delete(
|
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,
|
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
|
277
|
+
WHERE id = current_user
|
278
278
|
"""
|
279
279
|
|
280
280
|
queries: dict["str", LiteralString] = {
|