-
Notifications
You must be signed in to change notification settings - Fork 79
A New FSI focussed workload based on Bank of Anthos #793
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mohit-sheth
wants to merge
6
commits into
cloud-bulldozer:master
Choose a base branch
from
mohit-sheth:crw-fsi
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
37c9cff
add fsi bank of anthos workload
mohit-sheth 59cf85d
add indexing support
mohit-sheth 394878b
use public ip for frontend in loadgen
mohit-sheth c1e05f3
modify resources
mohit-sheth 858c362
affinity and node-selectors
mohit-sheth 06b5a67
remove initcontainer
mohit-sheth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # Base Python image (pinned digest for reproducibility) | ||
| FROM python:3.12.6-slim@sha256:ad48727987b259854d52241fac3bc633574364867b8e20aec305e6e7f4028b26 AS base | ||
|
|
||
| # Builder stage | ||
| FROM base AS builder | ||
| WORKDIR /app | ||
|
|
||
| # Install build tools & git (only here, not in final) | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| git \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Clone Bank of Anthos repo to get requirements | ||
| RUN git clone https://github.com/GoogleCloudPlatform/bank-of-anthos.git /tmp/bank-of-anthos | ||
|
|
||
| # Upgrade pip and install requirements into /install | ||
| RUN pip install --no-cache-dir --upgrade pip setuptools wheel \ | ||
| && pip install --prefix="/install" -r /tmp/bank-of-anthos/src/loadgenerator/requirements.txt | ||
|
|
||
| # Final stage | ||
| FROM base | ||
| WORKDIR /app | ||
|
|
||
| # Copy installed packages from builder | ||
| COPY --from=builder /install /usr/local | ||
|
|
||
| # Copy application code | ||
| COPY locustfile.py indexing.py requirements.txt . | ||
| COPY --chmod=755 entrypoint.sh /app/entrypoint.sh | ||
|
|
||
| # Install overlay-specific requirements | ||
| RUN pip install --no-cache-dir --upgrade pip setuptools wheel \ | ||
| && pip install --no-cache-dir -r requirements.txt | ||
|
|
||
| # Create non-root user | ||
| RUN useradd -m locust | ||
| USER locust | ||
|
|
||
| # Show Python logs as they occur | ||
| ENV PYTHONUNBUFFERED=0 | ||
| ENV GEVENT_SUPPORT=True | ||
| ENV LOG_LEVEL=info | ||
| ENV TEST=yes | ||
|
|
||
| # Entrypoint script | ||
| ENTRYPOINT ["/app/entrypoint.sh"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| #!/bin/sh | ||
| echo "Delaying start for 90s..." | ||
| sleep 90 | ||
|
|
||
| locust --host=http://${FRONTEND_ADDR} \ | ||
| --loglevel "${LOG_LEVEL}" \ | ||
| --headless \ | ||
| --users="${USERS:-100}" \ | ||
| --run-time 1m \ | ||
| --print-stats \ | ||
| --csv=/tmp/locust-results | ||
|
|
||
| echo "Running indexing.py..." | ||
| python /app/indexing.py | ||
|
|
||
| echo "Sleeping for 1 hour..." | ||
| sleep 3600 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| #!/usr/bin/env python3 | ||
| import csv | ||
| import json | ||
| import os | ||
| import uuid | ||
| from datetime import datetime | ||
| from opensearchpy import OpenSearch, helpers | ||
| from urllib.parse import urlparse | ||
|
|
||
| # OpenSearch connection via single ES_SERVER env var | ||
| ES_SERVER = os.getenv("ES_SERVER", "http://opensearch:9200") | ||
| parsed = urlparse(ES_SERVER) | ||
|
|
||
| if parsed.username and parsed.password: | ||
| es_client = OpenSearch( | ||
| f"{parsed.scheme}://{parsed.hostname}:{parsed.port or 9200}", | ||
| http_auth=(parsed.username, parsed.password), | ||
| verify_certs=False | ||
| ) | ||
| else: | ||
| es_client = OpenSearch(ES_SERVER, verify_certs=False) | ||
|
|
||
| ES_INDEX = os.getenv("ES_INDEX", "locust-results") | ||
| csv_file = "/tmp/locust-results_stats.csv" | ||
|
|
||
| def csv_to_json(csv_path): | ||
| """Read CSV and yield JSON objects.""" | ||
| combined = {} | ||
| with open(csv_path, newline="") as f: | ||
| reader = csv.DictReader(f) | ||
| for row in reader: | ||
| name = row.get("Name", f"row_{len(combined)}") | ||
|
|
||
| # Handle endpoint naming | ||
| if name == "/": | ||
| name = "home" # rename "/" to "home" | ||
| elif name.startswith("/"): | ||
| name = name[1:] # strip leading slash for other endpoints | ||
|
|
||
| # Convert numeric fields from string to float/int | ||
| for key in row: | ||
| try: | ||
| if "." in row[key]: | ||
| row[key] = float(row[key]) | ||
| else: | ||
| row[key] = int(row[key]) | ||
| except (ValueError, TypeError): | ||
| pass | ||
|
|
||
| combined[name] = row | ||
| return combined | ||
|
|
||
| def index_to_es(doc): | ||
| """Index a single document to OpenSearch.""" | ||
| # Add UUID, timestamp, worker_count and ocp_version from environment | ||
| doc["uuid"] = os.environ.get("UUID", str(uuid.uuid4())) | ||
| doc["timestamp"] = datetime.utcnow().isoformat() | ||
| doc["worker_count"] = int(os.environ.get("WORKER_COUNT", "0")) | ||
| doc["ocp_version"] = os.environ.get("OCP_VERSION", "") | ||
|
|
||
| json_doc = json.dumps(doc, indent=2) | ||
| print("Indexing combined document:\n", json_doc) | ||
| es_client.index(index=ES_INDEX, body=doc) | ||
| print(f"Indexed combined document to {ES_INDEX}") | ||
|
|
||
| if __name__ == "__main__": | ||
| combined_doc = csv_to_json(csv_file) | ||
| if combined_doc: | ||
| index_to_es(combined_doc) | ||
| else: | ||
| print("No data to index") | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| #!/usr/bin/python | ||
| # | ||
| # Copyright 2020 Google LLC | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| """ | ||
| Exercises the frontend endpoints for the system | ||
| """ | ||
|
|
||
|
|
||
| import json | ||
| import logging | ||
| import sys | ||
| from string import ascii_letters, digits | ||
| from random import randint, random, choice | ||
|
|
||
| from locust import HttpUser, TaskSet, SequentialTaskSet, task, between, constant | ||
|
|
||
|
|
||
| logging.basicConfig(level=logging.INFO) | ||
| logging.info("Locust started with arguments: %s", sys.argv) | ||
|
|
||
|
|
||
| MASTER_PASSWORD = "password" | ||
|
|
||
| TRANSACTION_ACCT_LIST = [str(randint(1111100000, 1111199999)) for _ in range(50)] | ||
|
|
||
| def signup_helper(locust, username): | ||
| """ | ||
| create a new user account in the system | ||
| succeeds if token was returned | ||
| """ | ||
| userdata = {"username":username, | ||
| "password":MASTER_PASSWORD, | ||
| "password-repeat":MASTER_PASSWORD, | ||
| "firstname": username, | ||
| "lastname":"TestAccount", | ||
| "birthday":"01/01/2000", | ||
| "timezone":"82", | ||
| "address":"1021 Valley St", | ||
| "city":"Seattle", | ||
| "state":"WA", | ||
| "zip":"98103", | ||
| "ssn":"111-22-3333"} | ||
| with locust.client.post("/signup", data=userdata, catch_response=True) as response: | ||
| for r_hist in response.history: | ||
| if r_hist.cookies.get('token') is not None: | ||
| response.success() | ||
| logging.debug("created user = %s", username) | ||
| return True | ||
|
|
||
| response.failure("login failed") | ||
| return False | ||
|
|
||
| def generate_username(): | ||
| """ | ||
| generates random 15 character | ||
| alphanumeric username | ||
| """ | ||
| return ''.join(choice(ascii_letters + digits) for _ in range(15)) | ||
|
|
||
| class AllTasks(SequentialTaskSet): | ||
| """ | ||
| wrapper for UnauthenticatedTasks and AuthenticatedTasks sets | ||
| """ | ||
| @task | ||
| class UnauthenticatedTasks(TaskSet): | ||
| """ | ||
| set of tasks to run before obtaining an auth token | ||
| """ | ||
| @task(5) | ||
| def view_login(self): | ||
| """ | ||
| load the /login page | ||
| fails if already logged on (redirects to /home) | ||
| """ | ||
| with self.client.get("/login", catch_response=True) as response: | ||
| for r_hist in response.history: | ||
| if r_hist.status_code > 200 and r_hist.status_code < 400: | ||
| response.failure("Logged on: Got redirect to /home") | ||
|
|
||
| @task(5) | ||
| def view_signup(self): | ||
| """ | ||
| load the /signup page | ||
| fails if not logged on (redirects to /home) | ||
| """ | ||
| with self.client.get("/signup", catch_response=True) as response: | ||
| for r_hist in response.history: | ||
| if r_hist.status_code > 200 and r_hist.status_code < 400: | ||
| response.failure("Logged on: Got redirect to /home") | ||
|
|
||
| @task(1) | ||
| def signup(self): | ||
| """ | ||
| sends POST request to /signup to create a new user | ||
| on success, exits UnauthenticatedTasks | ||
| """ | ||
| # sign up | ||
| new_username = generate_username() | ||
| success = signup_helper(self, new_username) | ||
| if success: | ||
| # go to AuthenticatedTasks | ||
| self.user.username = new_username | ||
| self.interrupt() | ||
|
|
||
| @task | ||
| class AuthenticatedTasks(TaskSet): | ||
| """ | ||
| set of tasks to run after obtaining an auth token | ||
| """ | ||
| def on_start(self): | ||
| """ | ||
| on start, deposit a large balance into each account | ||
| to ensure all payments are covered | ||
| """ | ||
| self.deposit(1000000) | ||
|
|
||
| @task(10) | ||
| def view_index(self): | ||
| """ | ||
| load the / page | ||
| fails if not logged on (redirects to /login) | ||
| """ | ||
| with self.client.get("/", catch_response=True) as response: | ||
| for r_hist in response.history: | ||
| if r_hist.status_code > 200 and r_hist.status_code < 400: | ||
| response.failure("Not logged on: Got redirect to /login") | ||
|
|
||
| @task(10) | ||
| def view_home(self): | ||
| """ | ||
| load the /home page (identical to /) | ||
| fails if not logged on (redirects to /login) | ||
| """ | ||
| with self.client.get("/home", catch_response=True) as response: | ||
| for r_hist in response.history: | ||
| if r_hist.status_code > 200 and r_hist.status_code < 400: | ||
| response.failure("Not logged on: Got redirect to /login") | ||
|
|
||
| @task(5) | ||
| def payment(self, amount=None): | ||
| """ | ||
| POST to /payment, sending money to other account | ||
| """ | ||
| if amount is None: | ||
| amount = random() * 1000 | ||
| transaction = {"account_num": choice(TRANSACTION_ACCT_LIST), | ||
| "amount": amount, | ||
| "uuid": generate_username()} | ||
| with self.client.post("/payment", | ||
| data=transaction, | ||
| catch_response=True) as response: | ||
| if response.url is None or "failed" in response.url: | ||
| response.failure("payment failed") | ||
|
|
||
| @task(5) | ||
| def deposit(self, amount=None): | ||
| """ | ||
| POST to /deposit, depositing external money into account | ||
| """ | ||
| if amount is None: | ||
| amount = random() * 1000 | ||
| acct_info = {"account_num": choice(TRANSACTION_ACCT_LIST), | ||
| "routing_num":"111111111"} | ||
| transaction = {"account": json.dumps(acct_info), | ||
| "amount": amount, | ||
| "uuid": generate_username()} | ||
| with self.client.post("/deposit", | ||
| data=transaction, | ||
| catch_response=True) as response: | ||
| if response.url is None or "failed" in response.url: | ||
| response.failure("deposit failed") | ||
|
|
||
| @task(5) | ||
| def login(self): | ||
| """ | ||
| sends POST request to /login with stored credentials | ||
| succeeds if a token was returned | ||
| """ | ||
| with self.client.post("/login", {"username":self.user.username, | ||
| "password":MASTER_PASSWORD}, | ||
| catch_response=True) as response: | ||
| for r_hist in response.history: | ||
| if r_hist.cookies.get('token') is not None: | ||
| response.success() | ||
| return | ||
| response.failure("login failed") | ||
|
|
||
| @task(1) | ||
| def logout(self): | ||
| """ | ||
| sends a /logout POST request | ||
| fails if not logged in | ||
| exits AuthenticatedTasks | ||
| """ | ||
| with self.client.post("/logout", catch_response=True) as response: | ||
| for r_hist in response.history: | ||
| if r_hist.status_code > 200 and r_hist.status_code < 400: | ||
| response.success() | ||
| self.user.username = None | ||
| # go to UnauthenticatedTasks | ||
| self.interrupt() | ||
|
|
||
|
|
||
| class WebsiteUser(HttpUser): | ||
| """ | ||
| Locust class to simulate HTTP users | ||
| """ | ||
| tasks = [AllTasks] | ||
| wait_time = constant(0) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| opensearch-py>=2.0.0 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.