Skip to content

Commit

Permalink
Merge pull request claffin#38 from dusancz/feature/google-cloud-provider
Browse files Browse the repository at this point in the history
Add Google Cloud provider
  • Loading branch information
claffin authored Jul 4, 2021
2 parents 872a98c + dc8a87d commit f7bfdb4
Show file tree
Hide file tree
Showing 8 changed files with 336 additions and 42 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ CloudProxy exposes an API with the IPs and credentials of the provisioned proxie
### Providers supported:
* [DigitalOcean](docs/digitalocean.md)
* [AWS](docs/aws.md)
* [Google Cloud](docs/gcp.md)
* [Hetzner](docs/hetzner.md)

### Planned:
* Google Cloud
* Azure
* Scaleway
* Vultr
Expand Down Expand Up @@ -138,7 +138,7 @@ You can scale up and down your proxies and remove them for each provider via the

["Proxy <{IP}> to be destroyed"]

### Restart proxy server (AWS only)
### Restart proxy server (AWS & GCP only)
#### Request

`DELETE /restart`
Expand Down
49 changes: 13 additions & 36 deletions cloudproxy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,42 +42,19 @@ def main():

def get_ip_list():
ip_list = []
if settings.config["providers"]["digitalocean"]["ips"]:
for ip in settings.config["providers"]["digitalocean"]["ips"]:
if ip not in delete_queue and ip not in restart_queue:
ip_list.append(
"http://"
+ settings.config["auth"]["username"]
+ ":"
+ settings.config["auth"]["password"]
+ "@"
+ ip
+ ":8899"
)
if settings.config["providers"]["aws"]["ips"]:
for ip in settings.config["providers"]["aws"]["ips"]:
if ip not in delete_queue and ip not in restart_queue:
ip_list.append(
"http://"
+ settings.config["auth"]["username"]
+ ":"
+ settings.config["auth"]["password"]
+ "@"
+ ip
+ ":8899"
)
if settings.config["providers"]["hetzner"]["ips"]:
for ip in settings.config["providers"]["hetzner"]["ips"]:
if ip not in delete_queue and ip not in restart_queue:
ip_list.append(
"http://"
+ settings.config["auth"]["username"]
+ ":"
+ settings.config["auth"]["password"]
+ "@"
+ ip
+ ":8899"
)
for provider in ['digitalocean', 'aws', 'gcp', 'hetzner']:
if settings.config["providers"][provider]["ips"]:
for ip in settings.config["providers"][provider]["ips"]:
if ip not in delete_queue and ip not in restart_queue:
ip_list.append(
"http://"
+ settings.config["auth"]["username"]
+ ":"
+ settings.config["auth"]["password"]
+ "@"
+ ip
+ ":8899"
)
return ip_list


Expand Down
113 changes: 113 additions & 0 deletions cloudproxy/providers/gcp/functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import json
import uuid

from loguru import logger

import googleapiclient.discovery
from google.oauth2 import service_account

from cloudproxy.providers.config import set_auth
from cloudproxy.providers.settings import config

gcp = config["providers"]["gcp"]
try:
credentials = service_account.Credentials.from_service_account_info(
json.loads(gcp["secrets"]["service_account_key"])
)
compute = googleapiclient.discovery.build('compute', 'v1', credentials=credentials)
except TypeError:
logger.error("GCP -> Invalid service account key")


def create_proxy():
image_response = compute.images().getFromFamily(
project=gcp["image_project"],
family=gcp["image_family"]
).execute()
source_disk_image = image_response['selfLink']

body = {
'name': 'cloudproxy-' + str(uuid.uuid4()),
'machineType':
f"zones/{gcp['zone']}/machineTypes/{gcp['size']}",
'tags': {
'items': [
'cloudproxy'
]
},
"labels": {
'cloudproxy': 'cloudproxy'
},
'disks': [
{
'boot': True,
'autoDelete': True,
'initializeParams': {
'sourceImage': source_disk_image,
}
}
],
'networkInterfaces': [{
'network': 'global/networks/default',
'accessConfigs': [
{
'name': 'External NAT',
'type': 'ONE_TO_ONE_NAT',
'networkTier': 'STANDARD'
}
]
}],
'metadata': {
'items': [{
'key': 'startup-script',
'value': set_auth(config["auth"]["username"], config["auth"]["password"])
}]
}
}

return compute.instances().insert(
project=gcp["project"],
zone=gcp["zone"],
body=body
).execute()

def delete_proxy(name):
try:
return compute.instances().delete(
project=gcp["project"],
zone=gcp["zone"],
instance=name
).execute()
except(googleapiclient.errors.HttpError):
logger.info(f"GCP --> HTTP Error when trying to delete proxy {name}. Probably has already been deleted.")
return None

def stop_proxy(name):
try:
return compute.instances().stop(
project=gcp["project"],
zone=gcp["zone"],
instance=name
).execute()
except(googleapiclient.errors.HttpError):
logger.info(f"GCP --> HTTP Error when trying to stop proxy {name}. Probably has already been deleted.")
return None

def start_proxy(name):
try:
return compute.instances().start(
project=gcp["project"],
zone=gcp["zone"],
instance=name
).execute()
except(googleapiclient.errors.HttpError):
logger.info(f"GCP --> HTTP Error when trying to start proxy {name}. Probably has already been deleted.")
return None

def list_instances():
result = compute.instances().list(
project=gcp["project"],
zone=gcp["zone"],
filter='labels.cloudproxy eq cloudproxy'
).execute()
return result['items'] if 'items' in result else []
109 changes: 109 additions & 0 deletions cloudproxy/providers/gcp/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import datetime
import itertools

from loguru import logger

from cloudproxy.check import check_alive
from cloudproxy.providers.gcp.functions import (
list_instances,
create_proxy,
delete_proxy,
stop_proxy,
start_proxy,
)
from cloudproxy.providers.settings import delete_queue, restart_queue, config

def gcp_deployment(min_scaling):
total_instances = len(list_instances())
if min_scaling < total_instances:
logger.info("Overprovisioned: GCP destroying.....")
for instance in itertools.islice(
list_instances(), 0, (total_instances - min_scaling)
):
access_configs = instance['networkInterfaces'][0]['accessConfigs'][0]
msg = f"{instance['name']} {access_configs['natIP']}"
delete_proxy(instance['name'])
logger.info("Destroyed: GCP -> " + msg)
if min_scaling - total_instances < 1:
logger.info("Minimum GCP instances met")
else:
total_deploy = min_scaling - total_instances
logger.info("Deploying: " + str(total_deploy) + " GCP instances")
for _ in range(total_deploy):
create_proxy()
logger.info("Deployed")
return len(list_instances())

def gcp_check_alive():
ip_ready = []
for instance in list_instances():
try:
elapsed = datetime.datetime.now(
datetime.timezone.utc
) - datetime.datetime.strptime(instance["creationTimestamp"], '%Y-%m-%dT%H:%M:%S.%f%z')

if config["age_limit"] > 0 and elapsed > datetime.timedelta(seconds=config["age_limit"]):
access_configs = instance['networkInterfaces'][0]['accessConfigs'][0]
msg = f"{instance['name']} {access_configs['natIP'] if 'natIP' in access_configs else ''}"
delete_proxy(instance['name'])
logger.info("Recycling instance, reached age limit -> " + msg)

elif instance['status'] == "TERMINATED":
logger.info("Waking up: GCP -> Instance " + instance['name'])
started = start_proxy(instance['name'])
if not started:
logger.info("Could not wake up, trying again later.")

elif instance['status'] == "STOPPING":
access_configs = instance['networkInterfaces'][0]['accessConfigs'][0]
msg = f"{instance['name']} {access_configs['natIP'] if 'natIP' in access_configs else ''}"
logger.info("Stopping: GCP -> " + msg)

elif instance['status'] == "PROVISIONING" or instance['status'] == "STAGING":
access_configs = instance['networkInterfaces'][0]['accessConfigs'][0]
msg = f"{instance['name']} {access_configs['natIP'] if 'natIP' in access_configs else ''}"
logger.info("Provisioning: GCP -> " + msg)

# If none of the above, check if alive or not.
elif check_alive(instance['networkInterfaces'][0]['accessConfigs'][0]['natIP']):
access_configs = instance['networkInterfaces'][0]['accessConfigs'][0]
msg = f"{instance['name']} {access_configs['natIP']}"
logger.info("Alive: GCP -> " + msg)
ip_ready.append(access_configs['natIP'])

else:
access_configs = instance['networkInterfaces'][0]['accessConfigs'][0]
msg = f"{instance['name']} {access_configs['natIP']}"
if elapsed > datetime.timedelta(minutes=10):
delete_proxy(instance['name'])
logger.info("Destroyed: took too long GCP -> " + msg)
else:
logger.info("Waiting: GCP -> " + msg)
except (TypeError, KeyError):
logger.info("Pending: GCP -> Allocating IP")
return ip_ready

def gcp_check_delete():
for instance in list_instances():
access_configs = instance['networkInterfaces'][0]['accessConfigs'][0]
if 'natIP' in access_configs and access_configs['natIP'] in delete_queue:
msg = f"{instance['name']}, {access_configs['natIP']}"
delete_proxy(instance['name'])
logger.info("Destroyed: not wanted -> " + msg)
delete_queue.remove(access_configs['natIP'])

def gcp_check_stop():
for instance in list_instances():
access_configs = instance['networkInterfaces'][0]['accessConfigs'][0]
if 'natIP' in access_configs and access_configs['natIP'] in restart_queue:
msg = f"{instance['name']}, {access_configs['natIP']}"
stop_proxy(instance['name'])
logger.info("Stopped: getting new IP -> " + msg)
restart_queue.remove(access_configs['natIP'])

def gcp_start():
gcp_check_delete()
gcp_check_stop()
gcp_deployment(config["providers"]["gcp"]["scaling"]["min_scaling"])
ip_ready = gcp_check_alive()
return ip_ready
12 changes: 10 additions & 2 deletions cloudproxy/providers/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from loguru import logger
from cloudproxy.providers import settings
from cloudproxy.providers.aws.main import aws_start
from cloudproxy.providers.gcp.main import gcp_start
from cloudproxy.providers.digitalocean.main import do_start
from cloudproxy.providers.hetzner.main import hetzner_start

Expand All @@ -11,12 +12,15 @@ def do_manager():
settings.config["providers"]["digitalocean"]["ips"] = [ip for ip in ip_list]
return ip_list


def aws_manager():
ip_list = aws_start()
settings.config["providers"]["aws"]["ips"] = [ip for ip in ip_list]
return ip_list

def gcp_manager():
ip_list = gcp_start()
settings.config["providers"]["gcp"]["ips"] = [ip for ip in ip_list]
return ip_list

def hetzner_manager():
ip_list = hetzner_start()
Expand All @@ -35,7 +39,11 @@ def init_schedule():
sched.add_job(aws_manager, "interval", seconds=20)
else:
logger.info("AWS not enabled")
if settings.config["providers"]["gcp"]["enabled"] == 'True':
sched.add_job(gcp_manager, "interval", seconds=20)
else:
logger.info("GCP not enabled")
if settings.config["providers"]["hetzner"]["enabled"] == 'True':
sched.add_job(hetzner_manager, "interval", seconds=20)
else:
logger.info("Hetzner not enabled")
logger.info("Hetzner not enabled")
29 changes: 28 additions & 1 deletion cloudproxy/providers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@
"secrets": {"access_key_id": "", "secret_access_key": ""},
"spot": False,
},
"gcp": {
"enabled": False,
"project": "",
"ips": [],
"scaling": {"min_scaling": 0, "max_scaling": 0},
"size": "",
"zone": "",
"image_project": "",
"image_family": "",
"secrets": {"service_account_key": ""},
},
"hetzner": {
"enabled": False,
"ips": [],
Expand Down Expand Up @@ -82,9 +93,25 @@
config["providers"]["aws"]["size"] = os.environ.get("AWS_SIZE", "t2.micro")
config["providers"]["aws"]["region"] = os.environ.get("AWS_REGION", "eu-west-2")
config["providers"]["aws"]["spot"] = os.environ.get("AWS_SPOT", False)
config["providers"]["aws"]["ami"] = os.environ.get("AWS_AMI", "ami-096cb92bb3580c759")

# Set GCP Config
config["providers"]["gcp"]["enabled"] = os.environ.get("GCP_ENABLED", False)
config["providers"]["gcp"]["project"] = os.environ.get("GCP_PROJECT")
config["providers"]["gcp"]["secrets"]["service_account_key"] = os.environ.get(
"GCP_SERVICE_ACCOUNT_KEY"
)

config["providers"]["aws"]["ami"] = os.environ.get("AWS_AMI", "ami-096cb92bb3580c759")
config["providers"]["gcp"]["scaling"]["min_scaling"] = int(
os.environ.get("GCP_MIN_SCALING", 2)
)
config["providers"]["gcp"]["scaling"]["max_scaling"] = int(
os.environ.get("GCP_MAX_SCALING", 2)
)
config["providers"]["gcp"]["size"] = os.environ.get("GCP_SIZE", "f1-micro")
config["providers"]["gcp"]["zone"] = os.environ.get("GCP_REGION", "us-central1-a")
config["providers"]["gcp"]["image_project"] = os.environ.get("GCP_IMAGE_PROJECT", "ubuntu-os-cloud")
config["providers"]["gcp"]["image_family"] = os.environ.get("GCP_IMAGE_FAMILY", "ubuntu-minimal-2004-lts")

# Set Hetzner config
config["providers"]["hetzner"]["enabled"] = os.environ.get(
Expand Down
Loading

0 comments on commit f7bfdb4

Please sign in to comment.