radlab-launcher/radlab.py (687 lines of code) (raw):
#!/usr/bin/env python3
# Copyright 2023 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
#
# https://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.
# PREREQ: installer_prereq.py
import os
import re
import sys
import json
import glob
import shutil
import string
import random
import requests
import argparse
import platform
import subprocess
from art import *
from os import path
from pprint import pprint
from google.cloud import storage
from googleapiclient import discovery
from colorama import Fore, Back, Style
from python_terraform import Terraform
from oauth2client.client import GoogleCredentials
ACTION_CREATE_DEPLOYMENT = "1"
ACTION_UPDATE_DEPLOYMENT = "2"
ACTION_DELETE_DEPLOYMENT = "3"
ACTION_LIST_DEPLOYMENT = "4"
def main(varcontents={}, module_name=None, action=None, projid=None, tfbucket=None, check=None):
orgid = ""
folderid = ""
billing_acc = ""
currentusr = ""
setup_path = os.getcwd()
# Setting "gcloud auth application-default" to deploy RAD Lab Modules
currentusr = radlabauth(currentusr)
# Setting up Project-ID
projid = set_proj(projid)
# Checking for User Permissions
if check == True:
launcherperm(projid, currentusr)
# Listing / Selecting from available RAD Lab modules
if module_name is None:
module_name = list_modules()
# Checking Module specific permissions
if check == True:
moduleperm(projid, module_name, currentusr)
# Validating user input Terraform variables against selected module
validate_tfvars(varcontents, module_name)
# Select Action to perform
if action is None or action == "":
action = select_action().strip()
# Setting up required attributes for any RAD Lab module deployment
env_path, tfbucket, orgid, billing_acc, folderid, randomid = module_deploy_common_settings(action, module_name, setup_path, varcontents, projid, tfbucket)
# Utilizing Terraform Wrapper for init / apply / destroy
env(action, orgid, billing_acc, folderid, env_path, randomid, tfbucket, projid)
print("\nGCS Bucket storing Terrafrom Configs: " + tfbucket + "\n")
print("\nTERRAFORM DEPLOYMENT COMPLETED!!!\n")
def radlabauth(currentusr):
try:
token = subprocess.Popen(["gcloud auth application-default print-access-token"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.read().strip().decode('utf-8')
r = requests.get('https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=' + token)
currentusr = r.json()["email"]
# Setting Credentials for non Cloud Shell CLI
if (platform.system() != 'Linux' and platform.processor() != '' and not platform.system().startswith('cs-')):
# countdown(5)
# Adding Execution handling if GOOGLE_APPLICATION_CREDENTIALS is set to Empty.
try:
del os.environ['GOOGLE_APPLICATION_CREDENTIALS']
except:
pass
x = input("\nWould you like to proceed the RAD Lab deployment with user - " + Fore.YELLOW + currentusr + Style.RESET_ALL + ' ?\n[1] Yes\n[2] No\n' + Fore.YELLOW + Style.BRIGHT + 'Choose a number : ' + Style.RESET_ALL).strip()
if (x == '1'):
pass
elif (x == '2'):
print("\nLogin with User account with which you would like to deploy RAD Lab Modules...\n")
os.system("gcloud auth application-default login")
else:
currentusr = '0'
except:
# Adding Execution handling if GOOGLE_APPLICATION_CREDENTIALS is set to Empty.
if (platform.system() != 'Linux' and platform.processor() != '' and not platform.system().startswith('cs-')):
try:
del os.environ['GOOGLE_APPLICATION_CREDENTIALS']
except:
pass
print("\nLogin with User account with which you would like to deploy RAD Lab Modules...\n")
os.system("gcloud auth application-default login")
finally:
if (currentusr == '0'):
sys.exit(Fore.RED + "\nError Occured - INVALID choice.\n")
else:
token = subprocess.Popen(["gcloud auth application-default print-access-token"], shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout.read().strip().decode('utf-8')
r = requests.get('https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=' + token)
currentusr = r.json()["email"]
os.system("gcloud config set account " + currentusr)
print(
"\nUser to deploy RAD Lab Modules (Selected) : " + Fore.GREEN + Style.BRIGHT + currentusr + Style.RESET_ALL)
return currentusr
def set_proj(projid):
if projid is None:
projid = os.popen("gcloud config list --format 'value(core.project)' 2>/dev/null").read().strip()
if (projid != ""):
select_proj = input("\nWhich Project would you like to use for RAD Lab management (Example - Creating/Utilizing GCS bucket where Terraform states will be stored) ? :" + "\n[1] Currently set project - " + Fore.GREEN + projid + Style.RESET_ALL + "\n[2] Enter a different Project ID" + Fore.YELLOW + Style.BRIGHT + "\nChoose a number for the RAD Lab management Project" + Style.RESET_ALL + ': ').strip()
if (select_proj == '2'):
projid = input(Fore.YELLOW + Style.BRIGHT + "Enter the Project ID" + Style.RESET_ALL + ': ').strip()
elif (select_proj != '1' and select_proj != '2'):
sys.exit(Fore.RED + "\nError Occured - INVALID choice.\n")
else:
projid = input(Fore.YELLOW + Style.BRIGHT + "\nEnter the Project ID for RAD Lab management" + Style.RESET_ALL + ': ').strip()
else:
pass
os.system("gcloud config set project " + projid)
os.system("gcloud auth application-default set-quota-project " + projid )
print("\nProject ID (Selected) : " + Fore.GREEN + Style.BRIGHT + projid + Style.RESET_ALL)
return projid
def launcherperm(projid, currentusr):
# Hardcoded Project level required RAD Lab Launcher roles
launcherprojroles = ['roles/storage.admin', 'roles/serviceusage.serviceUsageConsumer']
# Hardcoded Org level required RAD Lab Launcher roles
launcherorgroles = ['roles/iam.organizationRoleViewer']
credentials = GoogleCredentials.get_application_default()
service0 = discovery.build('cloudresourcemanager', 'v3', credentials=credentials)
request0 = service0.projects().getIamPolicy(resource='projects/' + projid)
response0 = request0.execute()
projiam = True
for role in launcherprojroles:
rolefound = False
ownerrole = False
for y in range(len(response0['bindings'])):
# print("ROLE --->")
# print(response0['bindings'][y]['role'])
# print("MEMBERS --->")
# print(response0['bindings'][y]['members'])
# Check for Owner role on RAD Lab Management Project
if (response0['bindings'][y]['role'] == 'roles/owner' and 'user:' + currentusr in response0['bindings'][y]['members']):
rolefound = True
ownerrole = True
print("\n" + currentusr + " has roles/owner role for RAD Lab Management Project: " + projid)
break
# Check for Required roles on RAD Lab Management Project
elif (response0['bindings'][y]['role'] == role):
rolefound = True
if ('user:' + currentusr not in response0['bindings'][y]['members']):
projiam = False
sys.exit(
Fore.RED + "\nError Occured - RADLAB LAUNCHER PERMISSION ISSUE | " + role + " permission missing...\n(Review https://github.com/GoogleCloudPlatform/rad-lab/tree/main/radlab-launcher#iam-permissions-prerequisites for more details)\n" + Style.RESET_ALL)
else:
pass
if rolefound == False:
sys.exit(
Fore.RED + "\nError Occured - RADLAB LAUNCHER PERMISSION ISSUE | " + role + " permission missing...\n(Review https://github.com/GoogleCloudPlatform/rad-lab/tree/main/radlab-launcher#iam-permissions-prerequisites for more details)\n" + Style.RESET_ALL)
if (ownerrole == True):
break
if projiam == True:
print(Fore.GREEN + '\nRADLAB LAUNCHER - Project Permission check passed' + Style.RESET_ALL)
service1 = discovery.build('cloudresourcemanager', 'v3', credentials=credentials)
request1 = service1.projects().get(name='projects/' + projid)
response1 = request1.execute()
if 'parent' in response1.keys():
service2 = discovery.build('cloudresourcemanager', 'v3', credentials=credentials)
org = findorg(response1['parent'])
request2 = service2.organizations().getIamPolicy(resource=org)
response2 = request2.execute()
orgiam = True
for role in launcherorgroles:
rolefound = False
for x in range(len(response2['bindings'])):
# print("ROLE --->")
# print(response2['bindings'][x]['role'])
# print("MEMBERS --->")
# print(response2['bindings'][x]['members'])
if (role == response2['bindings'][x]['role']):
rolefound = True
if ('user:' + currentusr not in response2['bindings'][x]['members']):
orgiam = False
sys.exit(Fore.RED + "\nError Occured - RADLAB LAUNCHER PERMISSION ISSUE | " + role + " permission missing...\n(Review https://github.com/GoogleCloudPlatform/rad-lab/tree/main/radlab-launcher#iam-permissions-prerequisites for more details)\n" + Style.RESET_ALL)
else:
pass
if rolefound == False:
sys.exit(Fore.RED + "\nError Occured - RADLAB LAUNCHER PERMISSION ISSUE | " + role + " permission missing...\n(Review https://github.com/GoogleCloudPlatform/rad-lab/tree/main/radlab-launcher#iam-permissions-prerequisites for more details)\n" + Style.RESET_ALL)
if orgiam == True:
print(Fore.GREEN + '\nRADLAB LAUNCHER - Organization Permission check passed' + Style.RESET_ALL)
else:
print(Fore.YELLOW + '\nRADLAB LAUNCHER - Skipping Organization Permission check. No Organization associated with the project: ' + projid + Style.RESET_ALL)
def findorg(parent):
if 'folders' in parent:
credentials = GoogleCredentials.get_application_default()
s = discovery.build('cloudresourcemanager', 'v3', credentials=credentials)
req = s.folders().get(name=parent)
res = req.execute()
return findorg(res['parent'])
else:
# print(Fore.GREEN + "Org identified: " + Style.BRIGHT + parent + Style.RESET_ALL)
return parent
def moduleperm(projid, module_name, currentusr):
# Check if any of the org policy is used in orgpolicy.tf
setorgpolicy = True
try:
## Finding policy variables in orgpolicy.tf
with open(os.path.dirname(os.getcwd()) + '/modules/' + module_name + '/orgpolicy.tf', "r") as file:
policy_vars = []
for line in file:
if ('count' in line and 'var.' in line and '||' not in line):
policy_vars.append(line[line.find("var.") + len("var."):line.find("?")].strip())
# print("Org Policy Variables:")
# print(policy_vars)
## [CHECK 1] Checking for commented orgpolicy resource in orgpolicy.tf
numCommentedOrgPolicy = 0
for policy in policy_vars:
with open(os.path.dirname(os.getcwd()) + '/modules/' + module_name + '/orgpolicy.tf', "r") as file:
for line in file:
# Finding Org policy resource block
if ('count' in line and 'var.' + policy in line and '?' in line):
# Checking for commented resource block line
if (line.startswith('#') or line.startswith('//')):
numCommentedOrgPolicy = numCommentedOrgPolicy + 1
# If No. of commented Org Policies are equal to total policies; No Org policy set
if (numCommentedOrgPolicy == len(policy_vars)):
setorgpolicy = False
## [CHECK 2] Checking if policy variables in variables.tf are set to 'false'
numDisabledOrgPolicyVar = 0
for var in policy_vars:
varblock = ""
block = False
with open(os.path.dirname(os.getcwd()) + '/modules/' + module_name + '/variables.tf', "r") as file:
for line in file:
if (var in line):
block = True
elif ('}' in line):
block = False
if (block == True):
varblock = varblock + line
# print(varblock + '}')
# Count number of disabled policies
if ('false' in varblock.split('default')[1]):
numDisabledOrgPolicyVar = numDisabledOrgPolicyVar + 1
# If No. of disabled Org Policies are equal to total policies; No Org policy set
if (numDisabledOrgPolicyVar == len(policy_vars)):
setorgpolicy = False
## [CHECK 3] Checking if policy variables in variables.tf are commented
numCommentedOrgPolicyVar = 0
for var in policy_vars:
with open(os.path.dirname(os.getcwd()) + '/modules/' + module_name + '/variables.tf', "r") as file:
for line in file:
# Finding Org policy resource block
if ('variable' in line and policy in line):
# Checking for commented resource block line
if (line.startswith('#') or line.startswith('//') or line.startswith('/*')):
numCommentedOrgPolicyVar = numCommentedOrgPolicyVar + 1
# If No. of commented Org Policies Variables are equal to total policies; No Org policy set
if (numCommentedOrgPolicyVar == len(policy_vars)):
setorgpolicy = False
except:
setorgpolicy = False
# Check if reusing project
create_project = True
try:
## Finding 'create_project' variable in variables.tf
varblock = ""
block = False
with open(os.path.dirname(os.getcwd()) + '/modules/' + module_name + '/variables.tf', "r") as file:
for line in file:
if ('create_project' in line):
block = True
elif ('}' in line):
block = False
if (block == True):
varblock = varblock + line
# print(varblock + '}')
if ('false' in varblock.split('default')[1]):
create_project = False
except Exception as e:
print(e)
print("\nSET ORG POLICY: " + str(setorgpolicy))
print("CREATE PROJECT: " + str(create_project))
# Scrape out Module specific permissions for the module
try:
with open(os.path.dirname(os.getcwd()) + '/modules/' + module_name + '/README.md', "r") as file:
section = False
orgroles = []
projroles = []
for line in file:
if (line.startswith("## IAM Permissions Prerequisites")):
section = True
# Identifying Roles if New Project is supposed to be created
if (create_project == True):
if (section == True and line.startswith('- Parent: `')):
orgroles.append(re.search("\`(.*?)\`", line).group(1))
if (section == True and line.startswith('- Project: `')):
projroles.append(re.search("\`(.*?)\`", line).group(1))
# Identifying Roles if Reusing any Existing project
else:
if (section == True and (line.startswith('- `') or line.startswith('- `'))):
projroles.append(re.search("\`(.*?)\`", line).group(1))
if (line.startswith('#') and not line.startswith("## IAM Permissions Prerequisites")):
section = False
# Removing optional role 'roles/orgpolicy.policyAdmin' if Org Policy is not set
if (setorgpolicy == False and 'roles/orgpolicy.policyAdmin' in orgroles):
orgroles.remove('roles/orgpolicy.policyAdmin')
except:
print(Fore.RED + 'IAM Permissions Prerequisites are missing in the README.md or the README.md file do not exisits for module : ' + module_name + Style.RESET_ALL)
# Check Module permissions permission
credentials = GoogleCredentials.get_application_default()
service = discovery.build('cloudresourcemanager', 'v3', credentials=credentials)
# Check Project level permissions
if len(projroles) != 0:
# print("Project Roles to check:")
# print(projroles)
# print("/*************** PROJECT IAM POLICY *************/")
request1 = service.projects().getIamPolicy(resource='projects/' + projid)
response1 = request1.execute()
projiam = True
for role in projroles:
rolefound = False
for y in range(len(response1['bindings'])):
# print("ROLE --->")
# print(response1['bindings'][y]['role'])
# print("MEMBERS --->")
# print(response1['bindings'][y]['members'])
if (role == response1['bindings'][y]['role']):
rolefound = True
if ('user:' + currentusr not in response1['bindings'][y]['members']):
projiam = False
sys.exit(Fore.RED + "\nError Occured - RADLAB MODULE PERMISSION ISSUE | " + role + " permission missing...\n(Review https://github.com/GoogleCloudPlatform/rad-lab/tree/main/modules/" + module_name + "#iam-permissions-prerequisites for more details)\n" + Style.RESET_ALL)
else:
pass
if rolefound == False:
sys.exit(Fore.RED + "\nError Occured - RADLAB MODULE PERMISSION ISSUE | " + role + " permission missing...\n(Review https://github.com/GoogleCloudPlatform/rad-lab/tree/main/modules/" + module_name + "#iam-permissions-prerequisites for more details)\n" + Style.RESET_ALL)
if projiam == True:
print(Fore.GREEN + '\nRADLAB MODULE (' + module_name + ')- Project Permission check passed' + Style.RESET_ALL)
# Check Org level permissions
if len(orgroles) != 0:
# print("Org Roles to check:")
# print(orgroles)
request = service.projects().get(name='projects/' + projid)
response = request.execute()
if 'parent' in response.keys():
# print("/*************** ORG IAM POLICY *************/")
org = findorg(response['parent'])
request2 = service.organizations().getIamPolicy(resource=org)
response2 = request2.execute()
# pprint(response2)
orgiam = True
for role in orgroles:
rolefound = False
for x in range(len(response2['bindings'])):
# print("ROLE --->")
# print(response2['bindings'][x]['role'])
# print("MEMBERS --->")
# print(response2['bindings'][x]['members'])
if (role == response2['bindings'][x]['role']):
rolefound = True
if ('user:' + currentusr not in response2['bindings'][x]['members']):
orgiam = False
sys.exit(Fore.RED + "\nError Occured - RADLAB MODULE (" + module_name + ") PERMISSION ISSUE | " + role + " permission missing...\n(Review https://github.com/GoogleCloudPlatform/rad-lab/tree/main/modules/" + module_name + "#iam-permissions-prerequisites for more details)\n" + Style.RESET_ALL)
else:
pass
if rolefound == False:
sys.exit(Fore.RED + "\nError Occured - RADLAB MODULE (" + module_name + ") PERMISSION ISSUE | " + role + " permission missing...\n(Review https://github.com/GoogleCloudPlatform/rad-lab/tree/main/modules/" + module_name + "#iam-permissions-prerequisites for more details)\n" + Style.RESET_ALL)
if orgiam == True:
print(Fore.GREEN + '\nRADLAB MODULE (' + module_name + ') - Organization Permission check passed' + Style.RESET_ALL)
else:
print(Fore.YELLOW + '\nRADLAB LAUNCHER - Skipping Organization Permission check. No Organization associated with the project: ' + projid + Style.RESET_ALL)
def env(action, orgid, billing_acc, folderid, env_path, deployment_id, tfbucket, projid):
tr = Terraform(working_dir=env_path)
return_code, stdout, stderr = tr.init_cmd(capture_output=False)
if (action == ACTION_CREATE_DEPLOYMENT or action == ACTION_UPDATE_DEPLOYMENT):
return_code, stdout, stderr = tr.apply_cmd(capture_output=False, auto_approve=True, var={'organization_id': orgid, 'billing_account_id': billing_acc, 'deployment_id': deployment_id})
return_code, stdout, stderr = tr.apply_cmd(refresh=True, capture_output=False, auto_approve=True, var={'organization_id': orgid, 'billing_account_id': billing_acc, 'deployment_id': deployment_id})
elif (action == ACTION_DELETE_DEPLOYMENT):
return_code, stdout, stderr = tr.destroy_cmd(capture_output=False, auto_approve=True, var={'organization_id': orgid, 'billing_account_id': billing_acc, 'deployment_id': deployment_id})
# return_code - 0 Success & 1 Error
if (return_code == 1):
print(stderr)
sys.exit(Fore.RED + Style.BRIGHT + "\nError Occured - Deployment failed for ID: " + deployment_id + "\n" + "Retry using above Deployment ID" + Style.RESET_ALL)
else:
target_path = 'radlab/' + env_path.split('/')[len(env_path.split('/')) - 1] + '/deployments'
if (action == ACTION_CREATE_DEPLOYMENT or action == ACTION_UPDATE_DEPLOYMENT):
if glob.glob(env_path + '/*.tf'):
upload_from_directory(projid, env_path, '/*.tf', tfbucket, target_path)
if glob.glob(env_path + '/*.json'):
upload_from_directory(projid, env_path, '/*.json', tfbucket, target_path)
if glob.glob(env_path + '/elk'):
upload_from_directory(projid, env_path, '/elk/**', tfbucket, target_path)
if glob.glob(env_path + '/scripts'):
upload_from_directory(projid, env_path, '/scripts/**', tfbucket, target_path)
if glob.glob(env_path + '/templates'):
upload_from_directory(projid, env_path, '/templates/**', tfbucket, target_path)
elif (action == ACTION_DELETE_DEPLOYMENT):
deltfgcs(tfbucket, 'radlab/' + env_path.split('/')[len(env_path.split('/')) - 1], projid)
# Deleting Local deployment config
shutil.rmtree(env_path)
def upload_from_directory(projid, directory_path: str, content: str, dest_bucket_name: str, dest_blob_name: str):
rel_paths = glob.glob(directory_path + content, recursive=True)
bucket = storage.Client(project=projid).get_bucket(dest_bucket_name)
for local_file in rel_paths:
file = local_file.replace(directory_path, '')
remote_path = f'{dest_blob_name}/{"/".join(file.split(os.sep)[1:])}'
if os.path.isfile(local_file):
blob = bucket.blob(remote_path)
blob.upload_from_filename(local_file)
def select_action():
action = input(
"\nAction to perform for RAD Lab Deployment ?\n[1] Create New\n[2] Update\n[3] Delete\n[4] List\n" + Fore.YELLOW + Style.BRIGHT + "Choose a number for the RAD Lab Module Deployment Action" + Style.RESET_ALL + ': ').strip()
if (action == ACTION_CREATE_DEPLOYMENT or action == ACTION_UPDATE_DEPLOYMENT or action == ACTION_DELETE_DEPLOYMENT or action == ACTION_LIST_DEPLOYMENT):
return action
else:
sys.exit(Fore.RED + "\nError Occured - INVALID choice.\n")
def basic_input(orgid, billing_acc, folderid, randomid):
print("\nEnter following info to start the setup and use the user which have Project Owner & Billing Account User roles:-")
# Selecting Org ID
if (orgid == ''):
orgid = getorgid()
# Org ID Validation
if (orgid.strip() and orgid.strip().isdecimal() == False):
sys.exit(Fore.RED + "\nError Occured - INVALID ORG ID\n")
print("\nOrg ID (Selected) : " + Fore.GREEN + Style.BRIGHT + orgid + Style.RESET_ALL)
# Selecting Folder ID
if (folderid == ''):
x = input("\nSet Folder ID ?\n[1] Enter Manually\n[2] Skip setting Folder ID\n" + Fore.YELLOW + Style.BRIGHT + "Choose a number for your choice" + Style.RESET_ALL + ': ').strip()
if (x == '1'):
folderid = input(Fore.YELLOW + Style.BRIGHT + "\nFolder ID" + Style.RESET_ALL + ': ').strip()
elif (x == '2'):
print("Skipped setting Folder ID...")
else:
sys.exit(Fore.RED + "\nError Occured - INVALID CHOICE\n" + Style.RESET_ALL)
# Folder ID Validation
if (folderid.strip() and folderid.strip().isdecimal() == False):
sys.exit(Fore.RED + "\nError Occured - INVALID FOLDER ID ACCOUNT\n")
print("\nFolder ID (Selected) : " + Fore.GREEN + Style.BRIGHT + folderid + Style.RESET_ALL)
# Selecting Billing Account
if (billing_acc == ''):
billing_acc = getbillingacc()
print("\nBilling Account (Selected) : " + Fore.GREEN + Style.BRIGHT + billing_acc + Style.RESET_ALL)
# Billing Account Validation
if (billing_acc.count('-') != 2):
sys.exit(Fore.RED + "\nError Occured - INVALID Billing Account\n")
# Create Random Deployment ID
if (randomid == ''):
randomid = get_random_alphanumeric_string(4)
return orgid, billing_acc, folderid, randomid
def create_env(env_path, orgid, billing_acc, folderid):
my_path = env_path + '/env.json'
envjson = [
{
"orgid": orgid,
"billing_acc": billing_acc,
"folderid": folderid
}
]
with open(my_path, 'w') as file:
json.dump(envjson, file, indent=4)
def get_env(env_path):
# Read orgid / billing acc / folder id from env.json
my_path = env_path + '/env.json'
# Opening JSON file
f = open(my_path, )
# returns JSON object as a dictionary
data = json.load(f)
orgid = data[0]['orgid']
billing_acc = data[0]['billing_acc']
folderid = data[0]['folderid']
# Closing file
f.close()
return orgid, billing_acc, folderid
def setlocaldeployment(tfbucket, prefix, env_path, projid):
if (blob_exists(tfbucket, prefix, projid)):
# Checking if 'deployment' folder exist in local. If YES, delete the same.
delifexist(env_path)
# Creating Local directory
os.makedirs(env_path)
# Copy Terraform deployment configs from GCS to Local
if (download_blob(projid, tfbucket, prefix, env_path) == True):
print("Terraform state downloaded to local...")
else:
print(Fore.RED + "\nError Occured whiled downloading Deployment Configs from GCS. Checking if the deployment exist locally...\n")
elif (os.path.isdir(env_path)):
print("Terraform state exist locally...")
else:
sys.exit(Fore.RED + "\nThe deployment with the entered ID do not exist !\n")
def download_blob(projid, tfbucket, prefix, env_path):
"""Downloads a blob from the bucket."""
try:
bucket_dir = 'radlab/' + prefix + '/deployments/'
local_dir = env_path + '/'
storage_client = storage.Client(project=projid)
bucket = storage_client.get_bucket(tfbucket)
blobs = bucket.list_blobs(prefix=bucket_dir) # Get list of files
for blob in blobs:
content = blob.name.replace(bucket_dir, '')
# Create Nested Folders structure in Local Directory
if '/' in content:
if (os.path.isdir(local_dir + os.path.dirname(content)) == False):
os.makedirs(local_dir + os.path.dirname(content))
# Download file
blob.download_to_filename(local_dir + content) # Download
return True
except:
return False
def get_random_alphanumeric_string(length):
letters_and_digits = string.ascii_lowercase + string.digits
result_str = ''.join((random.choice(letters_and_digits) for i in range(length)))
# print("Random alphanumeric String is:", result_str)
return result_str
def getbillingacc():
x = input("\nSet Billing Account ?\n[1] Enter Manually\n[2] Select from the List\n" + Fore.YELLOW + Style.BRIGHT + "Choose a number for your choice" + Style.RESET_ALL + ': ').strip()
if (x == '1'):
billing_acc = input(Fore.YELLOW + Style.BRIGHT + "Enter the Billing Account ( Example format - ABCDEF-GHIJKL-MNOPQR )" + Style.RESET_ALL + ': ').strip()
return billing_acc
elif (x == '2'):
credentials = GoogleCredentials.get_application_default()
service = discovery.build('cloudbilling', 'v1', credentials=credentials)
request = service.billingAccounts().list()
response = request.execute()
# print(response['billingAccounts'])
print("\nList of Billing account you have access to: \n")
billing_accounts = []
# Print out Billing accounts
for x in range(len(response['billingAccounts'])):
print("[" + str(x + 1) + "] " + response['billingAccounts'][x]['name'] + " " + response['billingAccounts'][x]['displayName'])
billing_accounts.append(response['billingAccounts'][x]['name'])
# Take user input and get the corresponding item from the list
inp = int(input(Fore.YELLOW + Style.BRIGHT + "Choose a number for Billing Account" + Style.RESET_ALL + ': '))
if inp in range(1, len(billing_accounts) + 1):
inp = billing_accounts[inp - 1]
billing_acc = inp.split('/')
# print(billing_acc[1])
return billing_acc[1]
else:
sys.exit(Fore.RED + "\nError Occured - INVALID BILLING ACCOUNT\n")
else:
sys.exit(Fore.RED + "\nError Occured - INVALID CHOICE\n" + Style.RESET_ALL)
def getorgid():
x = input("\nSet Org ID ?\n[1] Enter Manually\n[2] Select from the List\n[3] Skip setting Org ID\n" + Fore.YELLOW + Style.BRIGHT + "Choose a number for your choice" + Style.RESET_ALL + ': ').strip()
if (x == '1'):
orgid = input(Fore.YELLOW + Style.BRIGHT + "Enter the Org ID ( Example format - 1234567890 )" + Style.RESET_ALL + ': ').strip()
return orgid
elif (x == '2'):
credentials = GoogleCredentials.get_application_default()
service = discovery.build('cloudresourcemanager', 'v1beta1', credentials=credentials)
request = service.organizations().list()
response = request.execute()
# pprint(response)
print("\nList of Org ID you have access to: \n")
org_ids = []
# Print out Org IDs accounts
for x in range(len(response['organizations'])):
print("[" + str(x + 1) + "] " + response['organizations'][x]['organizationId'] + " " + response['organizations'][x]['displayName'] + " " + response['organizations'][x]['lifecycleState'])
org_ids.append(response['organizations'][x]['organizationId'])
# Take user input and get the corresponding item from the list
inp = int(input(Fore.YELLOW + Style.BRIGHT + "Choose a number for Organization ID" + Style.RESET_ALL + ': '))
if inp in range(1, len(org_ids) + 1):
orgid = org_ids[inp - 1]
# print(orgid)
return orgid
else:
sys.exit(Fore.RED + "\nError Occured - INVALID ORG ID SELECTED\n" + Style.RESET_ALL)
elif (x == '3'):
print("Skipped setting Org ID...")
return ''
else:
sys.exit(Fore.RED + "\nError Occured - INVALID CHOICE\n" + Style.RESET_ALL)
def delifexist(env_path):
# print(os.path.isdir(env_path))
if (os.path.isdir(env_path)):
shutil.rmtree(env_path)
def getbucket(action, projid):
"""Lists all buckets."""
storage_client = storage.Client(project=projid)
bucketoption = ''
if (action == ACTION_CREATE_DEPLOYMENT):
bucketoption = input("\nWant to use existing GCS Bucket for Terraform configs or Create Bucket ?:\n[1] Use Existing Bucket\n[2] Create New Bucket\n" + Fore.YELLOW + Style.BRIGHT + "Choose a number for your choice" + Style.RESET_ALL + ': ').strip()
if (bucketoption == '1' or action == ACTION_UPDATE_DEPLOYMENT or action == ACTION_DELETE_DEPLOYMENT or action == ACTION_LIST_DEPLOYMENT):
try:
buckets = storage_client.list_buckets()
barray = []
x = 0
print("\nSelect a bucket for Terraform Configs & States... \n")
# Print out Buckets in the default project
for bucket in buckets:
print("[" + str(x + 1) + "] " + bucket.name)
barray.append(bucket.name)
x = x + 1
# Take user input and get the corresponding item from the list
try:
inp = int(input(Fore.YELLOW + Style.BRIGHT + "Choose a number for Bucket Name" + Style.RESET_ALL + ': '))
except:
print(Fore.RED + "\nINVALID or NO OPTION SELECTED FOR BUCKET NAME.\n\nEnter the Bucket name Manually...\n" + Style.RESET_ALL)
if inp in range(1, len(barray) + 1):
tfbucket = barray[inp - 1]
return tfbucket
else:
print(Fore.RED + "\nINVALID or NO OPTION SELECTED FOR BUCKET NAME.\n\nEnter the Bucket name Manually...\n" + Style.RESET_ALL)
sys.exit(1)
except Exception as e:
print(e)
# except:
# tfbucket = input(Fore.YELLOW + Style.BRIGHT +"Enter the GCS Bucket name where Terraform Configs & States will be stored"+ Style.RESET_ALL + ": ")
# tfbucket = tfbucket.lower().strip()
# return tfbucket
elif (bucketoption == '2'):
print("CREATE BUCKET")
bucketprefix = input(Fore.YELLOW + Style.BRIGHT + "\nEnter the prefix for the bucket name i.e. radlab-[PREFIX] " + Style.RESET_ALL + ': ')
# Creates the new bucket
# Note: These samples create a bucket in the default US multi-region with a default storage class of Standard Storage.
# To create a bucket outside these defaults, see [Creating storage buckets](https://cloud.google.com/storage/docs/creating-buckets).
bucket = storage_client.create_bucket('radlab-' + bucketprefix)
print("Bucket {} created.".format(bucket.name))
return bucket.name
else:
sys.exit(Fore.RED + "\nInvalid Choice")
def settfstategcs(env_path, prefix, tfbucket, projid):
prefix = "radlab/" + prefix + "/terraform_state"
# Validate Terraform Bucket ID
client = storage.Client(project=projid)
try:
bucket = client.get_bucket(tfbucket)
# print(bucket)
except:
sys.exit(Fore.RED + "\nError Occured - INVALID BUCKET NAME or NO ACCESS\n" + Style.RESET_ALL)
# Create backend.tf file
f = open(env_path + '/backend.tf', 'w+')
f.write('terraform {\n backend "gcs"{\n bucket="' + tfbucket + '"\n prefix="' + prefix + '"\n }\n}')
f.close()
def deltfgcs(tfbucket, prefix, projid):
storage_client = storage.Client(project=projid)
bucket = storage_client.get_bucket(tfbucket)
blobs = bucket.list_blobs(prefix=prefix)
for blob in blobs:
blob.delete()
def blob_exists(tfbucket, prefix, projid):
storage_client = storage.Client(project=projid)
bucket = storage_client.get_bucket(tfbucket)
blob = bucket.blob('radlab/' + prefix + '/deployments/main.tf')
# print(blob.exists())
return blob.exists()
def list_radlab_deployments(tfbucket, module_name, projid):
"""Lists all the blobs in the bucket that begin with the prefix."""
storage_client = storage.Client(project=projid)
bucket = storage_client.get_bucket(tfbucket)
iterator = bucket.list_blobs(prefix='radlab/', delimiter='/')
response = iterator._get_next_page_response()
print("\nPlease find the list of existing " + module_name + " module deployments below:\n")
for prefix in response['prefixes']:
if module_name in prefix:
print(Fore.GREEN + Style.BRIGHT + prefix.split('/')[1] + Style.RESET_ALL)
def list_modules():
modules = [s.replace(os.path.dirname(os.getcwd()) + '/modules/', "") for s in glob.glob(os.path.dirname(os.getcwd()) + '/modules/*')]
modules = sorted(modules)
c = 1
print_list = ''
# Printing List of Modules
for module in modules:
first_line = ''
# Fetch Module name
try:
with open(os.path.dirname(os.getcwd()) + '/modules/' + module + '/README.md', "r") as file:
first_line = file.readline()
except:
print(Fore.RED + 'Missing README.md file for module: ' + module + Style.RESET_ALL)
print_list = print_list + "[" + str(c) + "] " + first_line.strip() + Fore.GREEN + " (" + module + ")\n" + Style.RESET_ALL
c = c + 1
# Selecting Module
try:
selected_module = input("\nList of available RAD Lab modules:\n" + print_list + "[" + str(c) + "] Exit\n" + Fore.YELLOW + Style.BRIGHT + "Choose a number for the RAD Lab Module" + Style.RESET_ALL + ': ').strip()
selected_module = int(selected_module)
except:
sys.exit(Fore.RED + "\nInvalid module")
# Validating User Module selection
if selected_module > 0 and selected_module < c:
# print(modules)
module_name = modules[selected_module - 1]
print("\nRAD Lab Module (selected) : " + Fore.GREEN + Style.BRIGHT + module_name + Style.RESET_ALL)
return module_name
elif selected_module == c:
sys.exit(Fore.GREEN + "\nExiting Installer")
else:
sys.exit(Fore.RED + "\nInvalid module")
def module_deploy_common_settings(action, module_name, setup_path, varcontents, projid, tfbucket):
# Get Terraform Bucket Details
if tfbucket is None:
tfbucket = getbucket(action, projid)
print("\nGCS bucket for Terraform config & state (Selected) : " + Fore.GREEN + Style.BRIGHT + tfbucket + Style.RESET_ALL)
# Setting Org ID, Billing Account, Folder ID
if (action == ACTION_CREATE_DEPLOYMENT):
# Check for any overides of basic inputs from terraform.tfvars file
orgid, billing_acc, folderid, randomid = check_basic_inputs_tfvars(varcontents)
# Getting Base Inputs
orgid, billing_acc, folderid, randomid = basic_input(orgid, billing_acc, folderid, randomid)
# Set environment path as deployment directory
prefix = module_name + '_' + randomid
env_path = setup_path + '/deployments/' + prefix
# Checking if 'deployment' folder exist in local. If YES, delete the same.
delifexist(env_path)
# Copy module directory
shutil.copytree(os.path.dirname(os.getcwd()) + '/modules/' + module_name, env_path)
# Set Terraform states remote backend as GCS
settfstategcs(env_path, prefix, tfbucket, projid)
# Create file with billing/org/folder details
create_tfvars(env_path, varcontents)
# Create file with billing/org/folder details
create_env(env_path, orgid, billing_acc, folderid)
print("\nCREATING DEPLOYMENT...")
return env_path, tfbucket, orgid, billing_acc, folderid, randomid
elif (action == ACTION_UPDATE_DEPLOYMENT or action == ACTION_DELETE_DEPLOYMENT):
# List Existing Deployments
list_radlab_deployments(tfbucket, module_name, projid)
# Get Deployment ID
randomid = input(Fore.YELLOW + Style.BRIGHT + "\nEnter RAD Lab Module Deployment ID (example 'l8b3' is the id for module deployment with name - data_science_l8b3)" + Style.RESET_ALL + ': ')
randomid = randomid.strip()
# Validating Deployment ID
if (len(randomid) == 4 and randomid.isalnum()):
# Set environment path as deployment directory
prefix = module_name + '_' + randomid
env_path = setup_path + '/deployments/' + prefix
# Setting Local Deployment
setlocaldeployment(tfbucket, prefix, env_path, projid)
else:
sys.exit(Fore.RED + "\nInvalid deployment ID!\n")
# Get env values
orgid, billing_acc, folderid = get_env(env_path)
# Set Terraform states remote backend as GCS
settfstategcs(env_path, prefix, tfbucket, projid)
# Create file with billing/org/folder details and user input variables
if os.path.exists(env_path + '/terraform.tfvars'):
os.remove(env_path + '/terraform.tfvars')
create_tfvars(env_path, varcontents)
if (action == ACTION_UPDATE_DEPLOYMENT):
print("\nUPDATING DEPLOYMENT...")
if (action == ACTION_DELETE_DEPLOYMENT):
print("\nDELETING DEPLOYMENT...")
return env_path, tfbucket, orgid, billing_acc, folderid, randomid
elif (action == ACTION_LIST_DEPLOYMENT):
list_radlab_deployments(tfbucket, module_name, projid)
sys.exit()
else:
sys.exit(Fore.RED + "\nInvalid RAD Lab Module Action selected")
def validate_tfvars(varcontents, module_name):
keys = list(varcontents.keys())
if keys:
print("Variables in file:")
print(keys)
for key in keys:
status = False
try:
with open(os.path.dirname(os.getcwd()) + '/modules/' + module_name + '/variables.tf', 'r') as myfile:
for line in myfile:
if ('variable "' + key + '"' in line):
# print (key + ": Found")
status = True
break
except:
sys.exit(Fore.RED + 'variables.tf missing for module: ' + module_name)
# Check if an invalid variable is passed!
if (status == False):
sys.exit(
Fore.RED + 'Variable: ' + key + ' passed in input file, do not exist in variables.tf file of ' + module_name + ' module.')
# print(varcontents)
return True
def create_tfvars(env_path, varcontents):
# Check if any variable exist
if (bool(varcontents) == True):
# Creating terraform.tfvars file in deployment folder
f = open(env_path + "/terraform.tfvars", "w+")
for var in varcontents:
f.write(var.strip() + "=" + varcontents[var].strip() + "\n")
f.close()
else:
print("Skipping creation of terraform.tfvars as no input file for variables...")
def check_basic_inputs_tfvars(varcontents):
try:
orgid = varcontents['organization_id'].strip('"')
except:
orgid = ''
try:
billing_acc = varcontents['billing_account_id'].strip('"')
except:
billing_acc = ''
try:
folderid = varcontents['folder_id'].strip('"')
except:
folderid = ''
try:
randomid = varcontents['deployment_id'].strip('"')
except:
randomid = ''
return orgid, billing_acc, folderid, randomid
def fetchvariables(filecontents):
variables = {}
# Check if there is any variable; If NOT do not create terraform.tfvars file
for x in filecontents:
# Skipping for commented lines
if x.startswith('#') or x.startswith('//'):
continue
elif (len(x.split("=")) == 2):
x = x.strip()
# print(x)
variables[x.split("=")[0].strip()] = x.split("=")[1].strip()
# print(variables)
if (bool(variables) == True):
return variables
else:
sys.exit(Fore.RED + 'No variables in the input file')
if __name__ == "__main__":
try:
print('\n' + text2art("RADLAB", font="larry3d"))
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--rad-project', dest="projid", help="RAD Lab management GCP Project.", required=False)
parser.add_argument('-b', '--rad-bucket', dest="tfbucket", help="RAD Lab management GCS Bucket where Terraform states for the modules will be stored.", required=False)
parser.add_argument('-m', '--module', dest="module_name", choices=sorted([s.replace(os.path.dirname(os.getcwd()) + '/modules/', "") for s in glob.glob(os.path.dirname(os.getcwd()) + '/modules/*')]), help="RADLab Module name under ../../modules folder", required=False)
parser.add_argument('-a', '--action', dest="action", choices=['create', 'update', 'delete', 'list'], help="Type of action you want to perform for the selected RADLab module.", required=False)
parser.add_argument('-f', '--varfile', dest="file", type=argparse.FileType('r', encoding='UTF-8'), help="Input file (with complete path) for terraform.tfvars contents.", required=False)
parser.add_argument('-dc', '--disable-perm-check', dest="disable_perm_check", action='store_false', help="Flag to disable RAD Lab permissions pre-check.", required=False)
args = parser.parse_args()
# File Argument
if args.file is not None:
print("Checking input file...")
filecontents = args.file.readlines()
variables = fetchvariables(filecontents)
else:
variables = {}
# Action Argument
if args.action == 'create':
action = ACTION_CREATE_DEPLOYMENT
elif args.action == 'update':
action = ACTION_UPDATE_DEPLOYMENT
elif args.action == 'delete':
action = ACTION_DELETE_DEPLOYMENT
elif args.action == 'list':
action = ACTION_LIST_DEPLOYMENT
else:
action = None
main(variables, args.module_name, action, args.projid, args.tfbucket, args.disable_perm_check)
except Exception as e:
print(e)