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 +31 -21
- locust_cloud/auth.py +187 -17
- locust_cloud/cloud.py +46 -60
- locust_cloud/credential_manager.py +5 -2
- locust_cloud/timescale/exporter.py +2 -3
- locust_cloud/timescale/queries.py +1 -1
- locust_cloud/timescale/query.py +10 -4
- locust_cloud/webui/.eslintrc +1 -1
- locust_cloud/webui/dist/assets/{index-DQyGe4ep.js → index-DkFumpSS.js} +89 -89
- locust_cloud/webui/dist/index.html +1 -1
- locust_cloud/webui/package.json +2 -2
- locust_cloud/webui/tsconfig.tsbuildinfo +1 -1
- locust_cloud/webui/yarn.lock +6 -131
- {locust_cloud-1.8.0.dist-info → locust_cloud-1.10.0.dist-info}/METADATA +2 -2
- locust_cloud-1.10.0.dist-info/RECORD +23 -0
- {locust_cloud-1.8.0.dist-info → locust_cloud-1.10.0.dist-info}/WHEEL +1 -1
- locust_cloud/constants.py +0 -5
- locust_cloud-1.8.0.dist-info/RECORD +0 -24
- {locust_cloud-1.8.0.dist-info → locust_cloud-1.10.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
@@ -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
|
-
|
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
|
-
@
|
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"{
|
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
|
-
|
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
|
-
|
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
|
-
|
92
|
-
|
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
|
-
|
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"
|
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=
|
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
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
206
|
-
)
|
207
|
-
|
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
|
-
|
213
|
-
logger.
|
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=
|
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(
|
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
|
-
|
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":
|
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
|
-
|
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":
|
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"{
|
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":
|
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 =
|
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(
|
389
|
+
delete(credential_manager)
|
403
390
|
|
404
391
|
|
405
|
-
def delete(
|
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"{
|
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
|
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}"
|
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,
|
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
|
)
|
locust_cloud/timescale/query.py
CHANGED
@@ -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
|
-
|
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)
|