From ea22eaf5e5347b19c34e74d325549a889d08c962 Mon Sep 17 00:00:00 2001 From: Venkat Date: Sun, 5 Jan 2025 21:13:07 +0000 Subject: [PATCH] major: remove AWS lightsail since there are lots of service quota issues feat: add hetzner as lightsail replacement for chisel nodes feat: enable chisel nodes to show up in swagger feat: use secrets library in python instead of random to generate chisel auth token --- Pipfile | 1 + Pipfile.lock | 11 ++- app/main.py | 18 ++--- app/schemas/schemas.py | 8 +- app/util/__init__.py | 0 app/util/aws_lightsail.py | 158 -------------------------------------- app/util/chisel.py | 47 ++++++++++++ app/util/hetzner.py | 91 ++++++++++++++++++++++ 8 files changed, 159 insertions(+), 175 deletions(-) create mode 100644 app/util/__init__.py delete mode 100644 app/util/aws_lightsail.py create mode 100644 app/util/chisel.py create mode 100644 app/util/hetzner.py diff --git a/Pipfile b/Pipfile index f4dcdcc..c70e3d6 100644 --- a/Pipfile +++ b/Pipfile @@ -8,6 +8,7 @@ fastapi = {extras = ["standard"], version = "*"} glueops-helpers = {file = "https://github.com/GlueOps/python-glueops-helpers-library/archive/refs/tags/v0.6.0.zip"} minio = "*" boto3 = "*" +hcloud = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index e167fb1..442a90f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fd18fd4f74b9481cc9fe3367c886a00aa321077f20ef330218a05805e1907dea" + "sha256": "8104b388e0dbe77ed01ce1f9a38881ae48a745802319f0b8999405b9ff50e46d" }, "pipfile-spec": 6, "requires": { @@ -374,6 +374,15 @@ "markers": "python_version >= '3.7'", "version": "==0.14.0" }, + "hcloud": { + "hashes": [ + "sha256:653bd4f53cf92e028c00a04462b9c549249278d3acafc8e6bb9b063ca426877f", + "sha256:e901d298b112f1d2d4568d8300f3ce19dfe782bdef3ff9c417026a81e16ee956" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.3.0" + }, "httpcore": { "hashes": [ "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", diff --git a/app/main.py b/app/main.py index d7bb1d6..48ee261 100644 --- a/app/main.py +++ b/app/main.py @@ -5,8 +5,8 @@ from pydantic import BaseModel, Field from contextlib import asynccontextmanager import os, glueops.setup_logging, traceback, base64, yaml, tempfile, json -from schemas.schemas import Message, CreateLightsailRequest, AwsCredentialsRequest, DeleteLightsailRequest, StorageBucketsRequest, AwsNukeAccountRequest, CaptainDomainNukeDataAndBackupsRequest -from util import storage, aws_lightsail, aws_setup_test_account_credentials, github +from schemas.schemas import Message, AwsCredentialsRequest, StorageBucketsRequest, AwsNukeAccountRequest, CaptainDomainNukeDataAndBackupsRequest, ChiselNodesRequest +from util import storage, aws_setup_test_account_credentials, github, hetzner from fastapi.responses import RedirectResponse @@ -78,23 +78,23 @@ async def nuke_captain_domain_data(request: CaptainDomainNukeDataAndBackupsReque """ return github.nuke_captain_domain_data_and_backups(request.captain_domain) -@app.post("/v1/chisel", response_class=PlainTextResponse, include_in_schema=False) -async def create_chisel_nodes(request: CreateLightsailRequest): +@app.post("/v1/chisel", response_class=PlainTextResponse) +async def create_chisel_nodes(request: ChiselNodesRequest): """ If you are testing within k3ds you will need chisel to provide you with load balancers. For a provided captain_domain this will delete any existing chisel nodes and provision new ones. Note: this will generally result in new IPs being provisioned. """ - return aws_lightsail.create_lightsail_instances(request) + return hetzner.create_instances(request) -@app.delete("/v1/chisel", include_in_schema=False) -async def delete_chisel_nodes(request: DeleteLightsailRequest): +@app.delete("/v1/chisel") +async def delete_chisel_nodes(request: ChiselNodesRequest): """ When you are done testing with k3ds this will delete your chisel nodes and save on costs. """ - response = aws_lightsail.create_lightsail_instances(request) - return JSONResponse(status_code=200, content={"message": response}) + response = hetzner.delete_existing_servers(request) + return JSONResponse(status_code=200, content={"message": "Successfully deleted chisel nodes."}) @app.get("/health", include_in_schema=False) diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py index 162a0b5..09d544f 100644 --- a/app/schemas/schemas.py +++ b/app/schemas/schemas.py @@ -4,14 +4,8 @@ class Message(BaseModel): message: str = Field(...,example = 'Success') - -class CreateLightsailRequest(BaseModel): - captain_domain: str = Field(...,example = 'glueops-captain-foobar') - region: str = Field(...,example = 'us-west-2') - -class DeleteLightsailRequest(BaseModel): +class ChiselNodesRequest(BaseModel): captain_domain: str = Field(...,example = 'nonprod.foobar.onglueops.rocks') - region: str = Field(...,example = 'us-west-2') class StorageBucketsRequest(BaseModel): captain_domain: str = Field(...,example = 'nonprod.foobar.onglueops.rocks') diff --git a/app/util/__init__.py b/app/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/util/aws_lightsail.py b/app/util/aws_lightsail.py deleted file mode 100644 index 2a822f5..0000000 --- a/app/util/aws_lightsail.py +++ /dev/null @@ -1,158 +0,0 @@ -import boto3 -import random -import string -import os -from fastapi import FastAPI, Security, HTTPException, Depends, status, requests, Request -import time - -REGION_NAMES = { - "us-east-1": "US East (N. Virginia)", - "us-east-2": "US East (Ohio)", - "us-west-2": "US West (Oregon)", - "eu-west-1": "EU (Ireland)", - "eu-west-2": "EU (London)", - "eu-west-3": "EU (Paris)", - "eu-central-1": "EU (Frankfurt)", - "ap-southeast-1": "Asia Pacific (Singapore)", - "ap-southeast-2": "Asia Pacific (Sydney)", - "ap-northeast-1": "Asia Pacific (Tokyo)", - "ap-northeast-2": "Asia Pacific (Seoul)", - "ap-south-1": "Asia Pacific (Mumbai)", - "ca-central-1": "Canada (Central)", - "eu-north-1": "EU (Stockholm)" -} - -# Delete existing instances if they exist -suffixes = ["exit1", "exit2"] - -def delete_lightsail_instances(request): - region = request.region - - # Set AWS region - boto3.setup_default_session(region_name=region) - - captain_domain = request.captain_domain.strip() - instance_names = [f"{captain_domain}-{suffix}" for suffix in suffixes] - aws_access_key = os.getenv("AWS_LIGHTSAIL_ACCESS_KEY") - aws_secret_key = os.getenv("AWS_LIGHTSAIL_SECRET_KEY") - - # Fetch Lightsail bundle and availability zone - lightsail_client = boto3.client("lightsail", aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key) - - for instance_name in instance_names: - try: - lightsail_client.delete_instance(instanceName=instance_name) - except lightsail_client.exceptions.NotFoundException: - pass # Instance does not exist, continue - - return "All instances were deleted" - -def create_lightsail_instances(request): - if request.region not in REGION_NAMES: - raise HTTPException(status_code=400, detail="Invalid region.") - - captain_domain = request.captain_domain.strip() - region = request.region - - # Set AWS region - boto3.setup_default_session(region_name=region) - - # Generate chisel credentials - def generate_credentials(): - return ( - "".join(random.choices(string.ascii_letters + string.digits, k=15)) - + ":" - + "".join(random.choices(string.ascii_letters + string.digits, k=15)) - ) - - credentials_for_chisel = generate_credentials() - - aws_access_key = os.getenv("AWS_LIGHTSAIL_ACCESS_KEY") - aws_secret_key = os.getenv("AWS_LIGHTSAIL_SECRET_KEY") - - - - # Fetch Lightsail bundle and availability zone - lightsail_client = boto3.client("lightsail", aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key) - ec2_client = boto3.client("ec2", aws_access_key_id=aws_access_key, aws_secret_access_key=aws_secret_key) - - try: - bundle_id = lightsail_client.get_bundles()["bundles"][0]["bundleId"] - blueprint_id = "debian_12" - first_az = ec2_client.describe_availability_zones()["AvailabilityZones"][0]["ZoneName"] - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error fetching AWS resources: {str(e)}") - - # Define user data - user_data = f"""#!/bin/bash - -# Some regions appear to be problematic on DNS resolution -sleep 15; - -curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh && sudo apt install tmux -y - -# Run chisel -sudo docker run -d --restart always -p 9090:9090 -p 443:443 -p 80:80 -it jpillora/chisel server --reverse --port=9090 --auth='{credentials_for_chisel}' -""" - - instance_names = [f"{captain_domain}-{suffix}" for suffix in suffixes] - ip_addresses = {} - - try: - delete_lightsail_instances(request) - - # Create instances - for instance_name in instance_names: - lightsail_client.create_instances( - instanceNames=[instance_name], - bundleId=bundle_id, - blueprintId=blueprint_id, - availabilityZone=first_az, - userData=user_data, - ) - time.sleep(1) # Ensure the instance is being created before opening ports - - time.sleep(60) # Wait for instances to initialize - - for instance_name in instance_names: - lightsail_client.open_instance_public_ports( - instanceName=instance_name, - portInfo={"fromPort": 0, "toPort": 65535, "protocol": "all"}, - ) - - # Retrieve public IP - instance_info = lightsail_client.get_instance(instanceName=instance_name) - ip_addresses[instance_name] = instance_info["instance"]["publicIpAddress"] - except Exception as e: - raise HTTPException(status_code=500, detail=f"Error creating instances: {str(e)}") - - # Generate Kubernetes manifest - manifest = f""" -kubectl apply -k https://github.com/FyraLabs/chisel-operator?ref=v0.4.1 - -kubectl apply -f - < str: + """ + Converts a multi-line string to a single-line string with `\\n` replacing newlines. + + Args: + input_text (str): The multi-line input string. + + Returns: + str: Single-line string with `\\n` replacing newlines. + """ + return input_text.replace("\n", "\\n") + + +def create_instances(request): + captain_domain = request.captain_domain.strip() + credentials_for_chisel = util.chisel.generate_credentials() + + # Define user data + user_data_readable = f""" +#cloud-config +package_update: true +runcmd: + - curl -fsSL https://get.docker.com -o get-docker.sh && sudo sh get-docker.sh && sudo apt install tmux -y + - sudo docker run -d --restart always -p 9090:9090 -p 443:443 -p 80:80 -it jpillora/chisel:v1.10.1 server --reverse --port=9090 --auth='{credentials_for_chisel}' +""" + + user_data = multiline_to_singleline(user_data_readable) + + suffixes = util.chisel.get_suffixes() + + instance_names = [f"{captain_domain}-{suffix}" for suffix in suffixes] + ip_addresses = {} + + try: + delete_existing_servers(request) + + for instance_name in instance_names: + ip_addresses[instance_name] = create_server(instance_name, captain_domain, user_data) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating instances: {str(e)}") + + return util.chisel.create_chisel_yaml(captain_domain, credentials_for_chisel, ip_addresses, suffixes) + + +def create_server(server_name, captain_domain, user_data_one_line_format): + server_type = ServerType(name="cx22") + image = Image(name="debian-12") + ssh_keys = client.ssh_keys.get_all(name="glueops-default-ssh-key") + location = Location(name="hel1") + server_response = client.servers.create( + server_name, + server_type=server_type, + image=image, + ssh_keys=ssh_keys, + location=location, + user_data=user_data_one_line_format, + labels={"captain_domain": captain_domain}, + public_net=ServerCreatePublicNetwork( + enable_ipv4=True, + enable_ipv6=False + ) + ) + + server = server_response.server + #server_response.action.wait_until_finished() + return(server.public_net.ipv4.ip) + + +def delete_existing_servers(request): + captain_domain = request.captain_domain.strip() + servers = client.servers.get_all(label_selector="captain_domain") + for server in servers: + if server.labels["captain_domain"] == captain_domain: + server.delete() + return True + +