This commit is contained in:
2023-03-19 21:16:21 +02:00
parent 0184229738
commit 943bdf015f

View File

@@ -14,9 +14,9 @@
# 7) list images available for a provider # 7) list images available for a provider
# 8) list sizes available for a provider # 8) list sizes available for a provider
# 9) list locations available for a provider # 9) list locations available for a provider
# 10) run a script during the creation of a new instance # 10) run a script during the creation of a new instance to deploy docker services
# 11) ssh to an instance with a choice across all or one cloud provider # 11) ssh to an instance with a choice across all or one cloud provider
# from the command line using flags # all from the command line using flags
import os import os
import sys import sys
@@ -34,17 +34,18 @@ from azure.mgmt.resource import ResourceManagementClient
from azure.mgmt.network import NetworkManagementClient from azure.mgmt.network import NetworkManagementClient
from azure.mgmt.network.v2022_07_01.models import SecurityRule from azure.mgmt.network.v2022_07_01.models import SecurityRule
# Disable SSL certificate verification
# Disable SHA-2 variants of RSA key verification algorithm for backward compatibility reasons
# Declare the ENV_FILE variable as such to always reside in the same directory as the script # Declare the ENV_FILE variable as such to always reside in the same directory as the script
# We use os.path.join to make sure the path is correct for every OS # We use os.path.join to make sure the path is correct for every OS
# Also do the same for the ssh keys and the script to be used during deployment
ENV_FILE = os.path.join(os.path.dirname(__file__), ".env") ENV_FILE = os.path.join(os.path.dirname(__file__), ".env")
SECDEP_SSH_PUBLIC_KEY = os.path.join(os.path.dirname(__file__), "secdep.pub") SECDEP_SSH_PUBLIC_KEY = os.path.join(os.path.dirname(__file__), "secdep.pub")
SECDEP_SSH_PRIVATE_KEY = os.path.join(os.path.dirname(__file__), "secdep") SECDEP_SSH_PRIVATE_KEY = os.path.join(os.path.dirname(__file__), "secdep")
SECDEP_DEPLOY_SCRIPT = os.path.join(os.path.dirname(__file__), "harden")
# Available choices when the action flag is used
action_choices = ["delete","start","stop","reboot","deleteall","startall","stopall","rebootall"] action_choices = ["delete","start","stop","reboot","deleteall","startall","stopall","rebootall"]
# If no arguements were given let the user know this is not how to use this program
if not len(sys.argv) > 1: if not len(sys.argv) > 1:
print("No arguments passed. Use -h or --help for help") print("No arguments passed. Use -h or --help for help")
exit(0) exit(0)
@@ -71,6 +72,7 @@ parser.add_argument('-p', '--print', help='Also print node, image, location or s
parser.add_argument('-ssh', '--ssh', help='Connect to an instance using ssh', action='store_true') parser.add_argument('-ssh', '--ssh', help='Connect to an instance using ssh', action='store_true')
args = parser.parse_args() args = parser.parse_args()
# If one or both keys don't exist we create them
if not os.path.exists(SECDEP_SSH_PUBLIC_KEY) or not os.path.exists(SECDEP_SSH_PRIVATE_KEY): if not os.path.exists(SECDEP_SSH_PUBLIC_KEY) or not os.path.exists(SECDEP_SSH_PRIVATE_KEY):
# Generate a new SSH key pair # Generate a new SSH key pair
# The key is stored in the current directory and named secdep # The key is stored in the current directory and named secdep
@@ -78,13 +80,12 @@ if not os.path.exists(SECDEP_SSH_PUBLIC_KEY) or not os.path.exists(SECDEP_SSH_PR
# The passphrase is an empty string # The passphrase is an empty string
# The key is a 4096 bit RSA key # The key is a 4096 bit RSA key
# The key's comment is secdep@hostname # The key's comment is secdep@hostname
key = paramiko.RSAKey.generate(4096) key = paramiko.RSAKey.generate(4096)
key.write_private_key_file(SECDEP_SSH_PRIVATE_KEY) key.write_private_key_file(SECDEP_SSH_PRIVATE_KEY)
with open(SECDEP_SSH_PUBLIC_KEY, 'w') as f: with open(SECDEP_SSH_PUBLIC_KEY, 'w') as f:
f.write("%s %s secdep@%s" % (key.get_name(), key.get_base64(), socket.gethostname())) f.write("%s %s secdep@%s" % (key.get_name(), key.get_base64(), socket.gethostname()))
# We first check if it exists already in order to avoid overwriting it # We check if the env file it exists already in order to avoid overwriting it
# and if it doesn't exist, we create it by inserting an empty string # and if it doesn't exist, we create it by inserting an empty string
# When using the with statement, the file is automatically # When using the with statement, the file is automatically
@@ -93,22 +94,22 @@ if not os.path.exists(ENV_FILE):
with open(ENV_FILE, 'w') as f: with open(ENV_FILE, 'w') as f:
f.write('') f.write('')
# The required values for authentication are stored in the .env file # The required values for authentication are stored in the .env file in the form of KEY=VALUE
# These are # These are
# 1) SECDEP_GCE_CLIENT_ID (the service account Email found in project's IAM & Admin section/Service Accounts) # 1) SECDEP_GCE_CLIENT_ID (the service account Email found in project's IAM & Admin section/Service Accounts)
# 2) SECDEP_GCE_CLIENT_SECRET (the service account's private Key ID found in project's IAM & Admin section/Service Accounts) # 2) SECDEP_GCE_CLIENT_SECRET (the service account's private Key ID found in project's IAM & Admin section/Service Accounts)
# 3) SECDEP_GCE_PROJECT_ID (the project ID found in project's dashboard) # 3) SECDEP_GCE_PROJECT_ID (the project ID found in project's dashboard)
# 4) SECDEP_AZURE_TENANT_ID # 4) SECDEP_AZURE_TENANT_ID (the tenant id found when viewing the azure subscription)
# 5) SECDEP_AZURE_SUB_ID # 5) SECDEP_AZURE_SUB_ID (documented in the README)
# 6) SECDEP_AZURE_APP_ID # 6) SECDEP_AZURE_APP_ID (documented in the README)
# 7) SECDEP_AZURE_PASSWORD # 7) SECDEP_AZURE_PASSWORD (documented in the README)
# 8) SECDEP_AWS_ACCESS_KEY # 8) SECDEP_AWS_ACCESS_KEY (that we created in the aws IAM section)
# 9) SECDEP_AWS_SECRET_KEY # 9) SECDEP_AWS_SECRET_KEY (that we viewed only once when we created the key)
# For GCE we need to create a service account (with Owner Role from the IAM section) and download the json file (from # For GCE we need to create a service account (with Owner Role from the IAM section) and download the json file (from
# the Service Account's manage keys section) in the same directory as the script # the Service Account's manage keys section) in the same directory as the script
# We then check if the .env file is empty # We then check if the .env file is empty to determine if it's the first run of the script
if os.stat(ENV_FILE).st_size == 0: if os.stat(ENV_FILE).st_size == 0:
print('You will be asked for each needed value\nIf you want to skip a provider press enter on each of their values because they are all needed for authentication\nIf at some point you delete the provider\'s value entry you will once again be asked to enter it\nIf you pressed enter by mistake or inserted an incorrect value just edit the file directly or delete the corresponding line\nThere is also the choice of using the -v option to have that done interactively') print('You will be asked for each needed value\nIf you want to skip a provider press enter on each of their values because they are all needed for authentication\nIf at some point you delete the provider\'s value entry you will once again be asked to enter it\nIf you pressed enter by mistake or inserted an incorrect value just edit the file directly or delete the corresponding line\nThere is also the choice of using the -v option to have that done interactively')
@@ -235,9 +236,11 @@ def update_env_file():
print("The value for {} was updated successfully".format(entry_name)) print("The value for {} was updated successfully".format(entry_name))
update_env_file() update_env_file()
# Reload the environment variables # Reload the environment variables
# That was setup this way because the initial thought was exiting manually but it will stay that way just in case we do end up making it like so
load_dotenv(ENV_FILE) load_dotenv(ENV_FILE)
get_env_vars() get_env_vars()
# AWS and AZURE have thousands of image choice so we hardcode the ones we want in order to not wait forever during the input validation
AWS_ubuntu22_04_images = { AWS_ubuntu22_04_images = {
"ap-northeast-1": "ami-0cd7ad8676931d727", "ap-northeast-1": "ami-0cd7ad8676931d727",
"ap-south-1": "ami-06984ea821ac0a879", "ap-south-1": "ami-06984ea821ac0a879",
@@ -510,6 +513,7 @@ AZURE_images = {
"OpenSUSE Leap 15.4": "SUSE:opensuse-leap-15-4:gen1:2022.11.04" "OpenSUSE Leap 15.4": "SUSE:opensuse-leap-15-4:gen1:2022.11.04"
} }
# Declare the global variables for the drivers
global gce_driver global gce_driver
global azure_driver global azure_driver
global aws_driver global aws_driver
@@ -536,6 +540,7 @@ def get_aws_driver():
print("Trying to authenticate with amazon...\n") print("Trying to authenticate with amazon...\n")
return driver(SECDEP_AWS_ACCESS_KEY, SECDEP_AWS_SECRET_KEY) return driver(SECDEP_AWS_ACCESS_KEY, SECDEP_AWS_SECRET_KEY)
# We need to know the quantity to print the loading percentage when getting the list of all the nodes
def get_providers_quantity(): def get_providers_quantity():
providers_quantity = 0 providers_quantity = 0
if SECDEP_GCE_CLIENT_SECRET !="" and SECDEP_GCE_PROJECT_ID !="" and SECDEP_GCE_CLIENT_ID !="": if SECDEP_GCE_CLIENT_SECRET !="" and SECDEP_GCE_PROJECT_ID !="" and SECDEP_GCE_CLIENT_ID !="":
@@ -552,6 +557,7 @@ gce_driver = get_gce_driver()
azure_driver = get_azure_driver() azure_driver = get_azure_driver()
aws_driver = get_aws_driver() aws_driver = get_aws_driver()
# We call this function in almost every other function in order to not keep remaking the driver
def get_corresponding_driver(provider): def get_corresponding_driver(provider):
global driver global driver
match provider: match provider:
@@ -566,6 +572,7 @@ def get_corresponding_driver(provider):
assert driver is not None, "You need to set all {} environment variables first".format(provider.upper()) assert driver is not None, "You need to set all {} environment variables first".format(provider.upper())
return driver return driver
# This function takes a provider arguement and lists all the available sizes
def list_provider_sizes(provider): def list_provider_sizes(provider):
print("Getting "+provider+" sizes...") print("Getting "+provider+" sizes...")
driver = get_corresponding_driver(provider) driver = get_corresponding_driver(provider)
@@ -592,6 +599,7 @@ def list_provider_sizes(provider):
print("{}) {}\n\nRam: {}\nDisk: {}\nPrice: {}\n".format(count, size.name, size.ram, size.disk, size.price)) print("{}) {}\n\nRam: {}\nDisk: {}\nPrice: {}\n".format(count, size.name, size.ram, size.disk, size.price))
return sizes return sizes
# This function takes a provider arguement and lists all the available locations
def list_provider_locations(provider): def list_provider_locations(provider):
print("Getting "+provider+" locations...") print("Getting "+provider+" locations...")
driver = get_corresponding_driver(provider) driver = get_corresponding_driver(provider)
@@ -624,14 +632,6 @@ def listAWSregions(list):
return list return list
# This function lists all available images from the providers. # This function lists all available images from the providers.
# Unlike GCE, the equivalent list_images() function requires NodeLocation as an argument so
# we choose the first available one.
# Without additional arguments like publisher, sku and version it takes rediculously
# long to execute because there are over 3000 images available and the code that retrieves them has about 3 nested for loops.
# That is why we use a dictionary to output the available images. The list does not get updated
# but since the options include very old releases as well it is safe to assume these will also
# be kept as available choices
# Same goes for aws
def list_provider_images(provider,images=None): def list_provider_images(provider,images=None):
driver = get_corresponding_driver(provider) driver = get_corresponding_driver(provider)
print("Getting images from " +provider+"...") print("Getting images from " +provider+"...")
@@ -655,6 +655,9 @@ def list_provider_images(provider,images=None):
print("{}) {}\n\n{}\n".format(count, image.name, image.extra['description'])) print("{}) {}\n\n{}\n".format(count, image.name, image.extra['description']))
return images return images
# This function gets called in every get function to create a menu for selection
# It takes a list and a list name as arguements to differentiate how we print each one
# It also validates the user input and keeps asking for a valid one unless the user enters 0 to exit
def choose_from_list(listFromlistFunction,listName): def choose_from_list(listFromlistFunction,listName):
if len(listFromlistFunction) == 0: if len(listFromlistFunction) == 0:
print("No items") print("No items")
@@ -718,21 +721,31 @@ def choose_from_list(listFromlistFunction,listName):
print("Choosing 0 will exit") print("Choosing 0 will exit")
choice = input("Choose the "+listName+" you want to use: ") choice = input("Choose the "+listName+" you want to use: ")
# This function gets a provider location and returns it
def get_provider_location(provider): def get_provider_location(provider):
location = choose_from_list(list_provider_locations(provider),provider+"Location") location = choose_from_list(list_provider_locations(provider),provider+"Location")
return location return location
# This function gets a provider size and returns it
def get_provider_size(provider): def get_provider_size(provider):
size = choose_from_list(list_provider_sizes(provider),provider+"Size") size = choose_from_list(list_provider_sizes(provider),provider+"Size")
return size return size
# This function asks the user which image he wants to use and returns the image name # This function asks the user which image he wants to use and returns it
# If the user enters an invalid value, the program will ask again # If the user enters an invalid value, the program will ask again
# If the user enters 0 the program will exit # If the user enters 0 the program will exit
# For azure, after the user chooses one we take the value which is the image URN # For azure, after the user chooses one we take the value which is the image URN
# and use it to get the actual AzureImage # and use it to get the actual AzureImage
# For aws after we get the image we must select a region to get the ami because # For aws after we get the image we must select a region to get the ami because
# amis are region specific. Then we get the actual image # amis are region specific. Then we get the actual image
# Unlike GCE, the equivalent list_images() function requires NodeLocation as an argument so
# we choose the first available one.
# Without additional arguments like publisher, sku and version it takes rediculously
# long to execute because there are over 3000 images available and the code that retrieves them has about 3 nested for loops.
# That is why we use a dictionary to output the available images. The list does not get updated
# but since the options include very old releases as well it is safe to assume these will also
# be kept as available choices
# Same goes for aws
def get_provider_image(provider): def get_provider_image(provider):
image = choose_from_list(list_provider_images(provider),provider+"Image") image = choose_from_list(list_provider_images(provider),provider+"Image")
if provider == "azure": if provider == "azure":
@@ -751,6 +764,8 @@ def get_provider_image(provider):
image = None image = None
return image return image
# We need the blockPrint and enablePrint functions for when we use the list something ones during the input validation
# and don't actually need to see the lists
def blockPrint(): def blockPrint():
sys.stdout = open(os.devnull, 'w') sys.stdout = open(os.devnull, 'w')
@@ -763,6 +778,7 @@ def getAWSRegionFromAmi(ami):
if ami in image.values(): if ami in image.values():
return list(image.keys())[list(image.values()).index(ami)] return list(image.keys())[list(image.values()).index(ami)]
# This is the most important function of all and uses all the previous ones to validate the input and get the actual objects
def create_node(provider, name=None, location=None, size=None, image=None, confirm=None): def create_node(provider, name=None, location=None, size=None, image=None, confirm=None):
# Get public ssh key value # Get public ssh key value
with open(SECDEP_SSH_PUBLIC_KEY, 'r') as f: with open(SECDEP_SSH_PUBLIC_KEY, 'r') as f:
@@ -963,12 +979,17 @@ def create_node(provider, name=None, location=None, size=None, image=None, confi
# Create the node # Create the node
node = driver.create_node(name=name, size=size, image=image, location=location, auth=auth, ex_user_name="secdep", ex_resource_group=res_group.name, ex_use_managed_disks=True, ex_nic=newnic, ex_os_disk_delete=True) node = driver.create_node(name=name, size=size, image=image, location=location, auth=auth, ex_user_name="secdep", ex_resource_group=res_group.name, ex_use_managed_disks=True, ex_nic=newnic, ex_os_disk_delete=True)
else: else:
# If provider was aws
# Delete all keys since we are just going to upload the same one for the creation
# This doesn't affect already existing nodes because as we said, it it the same one used for the others
keys = driver.list_key_pairs() keys = driver.list_key_pairs()
for key in keys: for key in keys:
driver.delete_key_pair(key) driver.delete_key_pair(key)
driver.import_key_pair_from_string("secdep@"+socket.gethostname(), pubkey) driver.import_key_pair_from_string("secdep@"+socket.gethostname(), pubkey)
driver.ex_authorize_security_group_permissive('default') driver.ex_authorize_security_group_permissive('default')
keyname="secdep@"+socket.gethostname() keyname="secdep@"+socket.gethostname()
# since each ami decides on a different admin user name we can't use the create node
# to end up with a secdep user but we have to use the deploy_node function
SCRIPT = '''#!/usr/bin/env bash SCRIPT = '''#!/usr/bin/env bash
sudo useradd -G sudo -s /bin/bash -m secdep sudo useradd -G sudo -s /bin/bash -m secdep
sudo echo "secdep:secdeppass" | sudo chpasswd sudo echo "secdep:secdeppass" | sudo chpasswd
@@ -1048,12 +1069,17 @@ def create_node(provider, name=None, location=None, size=None, image=None, confi
# Create the node # Create the node
node = driver.create_node(name=name, size=size, image=image, location=location, auth=auth, ex_user_name="secdep", ex_resource_group=res_group.name, ex_use_managed_disks=True, ex_nic=newnic, ex_os_disk_delete=True) node = driver.create_node(name=name, size=size, image=image, location=location, auth=auth, ex_user_name="secdep", ex_resource_group=res_group.name, ex_use_managed_disks=True, ex_nic=newnic, ex_os_disk_delete=True)
else: else:
# If provider was aws
# Delete all keys since we are just going to upload the same one for the creation
# This doesn't affect already existing nodes because as we said, it it the same one used for the others
keys = driver.list_key_pairs() keys = driver.list_key_pairs()
for key in keys: for key in keys:
driver.delete_key_pair(key) driver.delete_key_pair(key)
driver.import_key_pair_from_string("secdep@"+socket.gethostname(), pubkey) driver.import_key_pair_from_string("secdep@"+socket.gethostname(), pubkey)
driver.ex_authorize_security_group_permissive('default') driver.ex_authorize_security_group_permissive('default')
keyname="secdep@"+socket.gethostname() keyname="secdep@"+socket.gethostname()
# since each ami decides on a different admin user name we can't use the create node
# to end up with a secdep user but we have to use the deploy_node function
SCRIPT = '''#!/usr/bin/env bash SCRIPT = '''#!/usr/bin/env bash
sudo useradd -G sudo -s /bin/bash -m secdep sudo useradd -G sudo -s /bin/bash -m secdep
sudo echo "secdep:secdeppass" | sudo chpasswd sudo echo "secdep:secdeppass" | sudo chpasswd