playbooks/roles/cyclecloud/files/configure.py (302 lines of code) (raw):

#!/usr/bin/python3 # Prepare an Azure provider account for CycleCloud usage. import sys import os import argparse import json import re import random import platform from string import ascii_uppercase, ascii_lowercase, digits from subprocess import CalledProcessError, check_output from os import path, listdir, chdir, fdopen, remove from urllib.request import urlopen, Request from shutil import rmtree, copy2, move from tempfile import mkstemp, mkdtemp from time import sleep path_to_cyclecloud="/usr/local/bin/cyclecloud" tmpdir = mkdtemp() print("Creating temp directory {} for installing CycleCloud".format(tmpdir)) cycle_root = "/opt/cycle_server" cs_cmd = cycle_root + "/cycle_server" def clean_up(): rmtree(tmpdir) def _catch_sys_error(cmd_list): try: output = check_output(cmd_list) # print(cmd_list) print(output) return output except CalledProcessError as e: print("Error with cmd: %s" % e.cmd) print("Output: %s" % e.output) raise def create_user(username): import pwd try: pwd.getpwnam(username) except KeyError: print('Creating user {}'.format(username)) _catch_sys_error(["useradd", "-m", "-d", "/home/{}".format(username), username]) _catch_sys_error(["chown", "-R", username + ":" + username, "/home/{}".format(username)]) def create_keypair(username, public_key=None): user_home = "/home/{}".format(username) if not os.path.isdir(user_home+"/.ssh"): _catch_sys_error(["mkdir", "-p", user_home+"/.ssh"]) public_key_file = user_home+"/.ssh/id_rsa.pub" if not os.path.exists(public_key_file): if public_key: with open(public_key_file, 'w') as pubkeyfile: pubkeyfile.write(public_key) pubkeyfile.write("\n") else: _catch_sys_error(["ssh-keygen", "-f", user_home+"/.ssh/id_rsa", "-N", ""]) with open(public_key_file, 'r') as pubkeyfile: public_key = pubkeyfile.read() authorized_key_file = user_home+"/.ssh/authorized_keys" authorized_keys = "" if os.path.exists(authorized_key_file): with open(authorized_key_file, 'r') as authkeyfile: authorized_keys = authkeyfile.read() if public_key not in authorized_keys: with open(authorized_key_file, 'w') as authkeyfile: authkeyfile.write(public_key) authkeyfile.write("\n") _catch_sys_error(["chown", "-R", username + ":" + username, user_home]) return public_key def create_user_credential(username, public_key=None): create_user(username) public_key = create_keypair(username, public_key) credential_record = { "PublicKey": public_key, "AdType": "Credential", "CredentialType": "PublicKey", "Name": username + "/public" } credential_data_file = os.path.join(tmpdir, "credential.json") print("Creating cred file: {}".format(credential_data_file)) with open(credential_data_file, 'w') as fp: json.dump(credential_record, fp) config_path = os.path.join(cycle_root, "config/data/") print("Copying config to {}".format(config_path)) copy2(credential_data_file, config_path) def generate_password_string(): random_pw_chars = ([random.choice(ascii_lowercase) for _ in range(20)] + [random.choice(ascii_uppercase) for _ in range(20)] + [random.choice(digits) for _ in range(10)]) random.shuffle(random_pw_chars) return ''.join(random_pw_chars) def cyclecloud_account_setup(vm_metadata, use_managed_identity, tenant_id, application_id, application_secret, admin_user, azure_cloud, accept_terms, password, storageAccount): print("Setting up azure account in CycleCloud and initializing cyclecloud CLI") accept_terms = True subscription_id = vm_metadata["compute"]["subscriptionId"] location = vm_metadata["compute"]["location"] resource_group = vm_metadata["compute"]["resourceGroupName"] random_suffix = ''.join(random.SystemRandom().choice( ascii_lowercase) for _ in range(14)) cyclecloud_admin_pw = "" if password: print('Password specified, using it as the admin password') cyclecloud_admin_pw = password else: cyclecloud_admin_pw = generate_password_string() if storageAccount: print('Storage account specified, using it as the default locker') storage_account_name = storageAccount else: storage_account_name = 'cyclecloud{}'.format(random_suffix) azure_data = { "Environment": azure_cloud, "AzureRMUseManagedIdentity": use_managed_identity, "AzureResourceGroup": resource_group, "AzureRMApplicationId": application_id, "AzureRMApplicationSecret": application_secret, "AzureRMSubscriptionId": subscription_id, "AzureRMTenantId": tenant_id, "DefaultAccount": True, "Location": location, "Name": "azure", "Provider": "azure", "ProviderId": subscription_id, "RMStorageAccount": storage_account_name, "RMStorageContainer": "cyclecloud" } if use_managed_identity: azure_data["AzureRMUseManagedIdentity"] = True app_setting_installation = { "AdType": "Application.Setting", "Name": "cycleserver.installation.complete", "Value": True } initial_user = { "AdType": "Application.Setting", "Name": "cycleserver.installation.initial_user", "Value": admin_user } account_data = [ initial_user, app_setting_installation ] if accept_terms: # Terms accepted, auto-create login user account as well login_user = { "AdType": "AuthenticatedUser", "Name": admin_user, "RawPassword": cyclecloud_admin_pw, "Superuser": True } account_data.append(login_user) account_data_file = tmpdir + "/account_data.json" azure_data_file = tmpdir + "/azure_data.json" with open(account_data_file, 'w') as fp: json.dump(account_data, fp) with open(azure_data_file, 'w') as fp: json.dump(azure_data, fp) copy2(account_data_file, cycle_root + "/config/data/") # Wait for the data to be imported sleep(5) initialize_cyclecloud_cli(admin_user, cyclecloud_admin_pw) output = _catch_sys_error([path_to_cyclecloud, "account", "show", "azure"]) if 'Credentials: azure' in str(output): print("Account \"azure\" already exists. Skipping account setup...") else: # wait until Managed Identity is ready for use before creating the Account if use_managed_identity: get_vm_managed_identity() # create the cloud provide account print("Registering Azure subscription in CycleCloud") _catch_sys_error([path_to_cyclecloud, "account", "create", "-f", azure_data_file]) # Read a property from the cycle_server.properties file def read_cycle_server_property(property): file_path = cycle_root + "/config/cycle_server.properties" with open(file_path, 'r') as file: for line in file: if line.startswith(property): return line.split('=')[1].strip() def initialize_cyclecloud_cli(admin_user, cyclecloud_admin_pw): print("Setting up azure account in CycleCloud and initializing cyclecloud CLI") # Extract the webServerContextPath configuration webServerContextPath = read_cycle_server_property('webServerContextPath') if webServerContextPath == '/': webServerContextPath = '' print(f'webServerContextPath: {webServerContextPath}') password_flag = ("--password=%s" % cyclecloud_admin_pw) print("Initializing cyclecloud CLI") _catch_sys_error([path_to_cyclecloud, "initialize", "--loglevel=debug", "--batch", "--url=https://localhost:9443%s"% webServerContextPath, "--verify-ssl=false", "--username=%s" % admin_user, password_flag]) def get_vm_metadata(): metadata_url = "http://169.254.169.254/metadata/instance?api-version=2019-08-15" metadata_req = Request(metadata_url, headers={"Metadata": True}) for _ in range(30): print("Fetching metadata") metadata_response = urlopen(metadata_req, timeout=2) try: return json.load(metadata_response) except ValueError as e: print("Failed to get metadata %s" % e) print(" Retrying") sleep(2) continue except: print("Unable to obtain metadata after 30 tries") raise def get_vm_managed_identity(): # Managed Identity may not be available immediately at VM startup... # Test/Pause/Retry to see if it gets assigned metadata_url = ('http://169.254.169.254/metadata/identity/oauth2/token' '?api-version=2018-02-01' '&resource=https://management.azure.com/') metadata_req = Request(metadata_url, headers={"Metadata": True}) for _ in range(30): print("Fetching managed identity") metadata_response = urlopen(metadata_req, timeout=2) try: return json.load(metadata_response) except ValueError as e: print("Failed to get managed identity %s" % e) print(" Retrying") sleep(10) continue except: print("Unable to obtain managed identity after 30 tries") raise def main(): parser = argparse.ArgumentParser(description="usage: %prog [options]") parser.add_argument("--tenantId", dest="tenantId", help="Tenant ID of the Azure subscription") parser.add_argument("--applicationId", dest="applicationId", help="Application ID of the Service Principal") parser.add_argument("--applicationSecret", dest="applicationSecret", help="Application Secret of the Service Principal") parser.add_argument("--username", dest="username", help="The local admin user for the CycleCloud VM") parser.add_argument("--hostname", dest="hostname", help="The short public hostname assigned to this VM (or public IP), used for LetsEncrypt") parser.add_argument("--acceptTerms", dest="acceptTerms", action="store_true", help="Accept Cyclecloud terms and do a silent install") parser.add_argument("--useLetsEncrypt", dest="useLetsEncrypt", action="store_true", help="Automatically fetch certificate from Let's Encrypt. " "(Only suitable for installations with public IP.)") parser.add_argument("--useManagedIdentity", dest="useManagedIdentity", action="store_true", help="Use the first assigned Managed Identity rather than a Service Principal " "for the default account") parser.add_argument("--password", dest="password", help="The password for the CycleCloud UI user") parser.add_argument("--publickey", dest="publickey", help="The public ssh key for the CycleCloud UI user") parser.add_argument("--storageAccount", dest="storageAccount", help="The storage account to use as a CycleCloud locker") parser.add_argument("--resourceGroup", dest="resourceGroup", help="The resource group for CycleCloud cluster resources. " "Resource Group must already exist. (Default: same RG as CycleCloud)") args = parser.parse_args() print("Debugging arguments: %s" % args) vm_metadata = get_vm_metadata() if args.resourceGroup: print("CycleCloud created in resource group: %s" % vm_metadata["compute"]["resourceGroupName"]) print("Cluster resources will be created in resource group: %s" % args.resourceGroup) vm_metadata["compute"]["resourceGroupName"] = args.resourceGroup # Retry await_startup in case it takes much longer than expected # (this is common in local testing with limited compute resources) max_tries = 5 started = False while not started: try: max_tries -= 1 _catch_sys_error([cs_cmd, "await_startup"]) started = True except: if max_tries > 0: # Wait 30s seconds before retrying _catch_sys_error([sleep, "30"]) print("Retrying...") else: print("CycleServer is not started") raise azEnvironment = vm_metadata["compute"]["azEnvironment"] azEnvironment = azEnvironment.lower() print("azEnvironment=%s" % azEnvironment) if azEnvironment == 'azurepubliccloud': azureSovereignCloud = 'public' elif azEnvironment == 'azureusgovernmentcloud': azureSovereignCloud = 'usgov' elif azEnvironment == 'azurechinacloud': azureSovereignCloud = 'china' elif azEnvironment == 'azuregermancloud': azureSovereignCloud = 'germany' else: azureSovereignCloud = 'public' cyclecloud_account_setup(vm_metadata, args.useManagedIdentity, args.tenantId, args.applicationId, args.applicationSecret, args.username, azureSovereignCloud, args.acceptTerms, args.password, args.storageAccount) # Create user requires root privileges create_user_credential(args.username, args.publickey) clean_up() if __name__ == "__main__": try: main() except: sys.exit("Deployment failed...")