import tower_cli import json import six import re from tower_cli.exceptions import TowerCLIError, CannotStartJob, JobFailure import tower_cli.cli.transfer.common as common from tower_cli.cli.transfer.logging_command import LoggingCommand from tower_cli.utils import parser from tower_cli.resources.role import ACTOR_FIELDS import click import os import sys from tower_cli.api import client import copy class Sender(LoggingCommand): my_user = None secret_management = 'default' columns = None sorted_assets = {} credential_type_objects = {} def __init__(self, no_color): self.no_color = no_color def send(self, source, prevent, exclude, secret_management): self.secret_management = secret_management self.print_intro() # First lets get all of the assets from the input this will raise if there are errors import_json = self.get_all_objects(source) if self.error_messages != 0: raise TowerCLIError("Unable to get assets from input") # Next we will sort all of the assets by their type again, will raise if there are errors # This is setting sorted_assets self.prep_and_sort_all_assets(import_json, prevent, exclude) for asset_type in common.SEND_ORDER: # If we don't have any of this asset type we can move on if asset_type not in self.sorted_assets or len(self.sorted_assets[asset_type]) == 0: continue identifier = common.get_identity(asset_type) resource = tower_cli.get_resource(asset_type) post_options = common.get_api_options(asset_type) for an_asset in self.sorted_assets[asset_type]: asset_name = an_asset[identifier] self.print_header_row(asset_type, asset_name) # First, validate and resolve all of the dependencies resolve # I.e. change an org name from "Default" to "1" # This has to be done here instead of in the prepAndSort because we may be building an asset if not self.resolve_asset_dependencies(asset_type, an_asset): continue # Next validate all of the fields in the request # Again, we can't do this in the prep because we might be building something that we need # For example, we might build a credential_type that fulfills the type field of a credential if not self.can_object_post(asset_type, an_asset, post_options): continue # See if we can get an existing object with this identifier existing_object = None try: existing_object = resource.get(**{identifier: asset_name}) except TowerCLIError: pass # Extract the relations (we don't want to add them into the objects relations = None if common.ASSET_RELATION_KEY in an_asset: relations = an_asset[common.ASSET_RELATION_KEY] del an_asset[common.ASSET_RELATION_KEY] asset_changed = False # If not, we need to create one, otherwise we can check it for update if existing_object is None: # Here are some special cases if asset_type == 'user' and 'password' not in an_asset: password = self.get_secret( "Enter the user password for {}".format(asset_name), "Setting password of user {}".format(asset_name), 'password' ) an_asset['password'] = password # If we are a credential there are a couple of conditions to match if asset_type == 'credential': # First, if we don't have an org, user or team we need to get our user and set that as user # Otherwise the API will throw an exception if 'organization' not in an_asset and 'user' not in an_asset and 'team' not in an_asset: if self.my_user is None: # First use the API to get the user from tower_cli.api import Client api_client = Client() me_response = api_client.request('GET', 'me/') response_json = me_response.json() if 'results' not in response_json or 'id' not in response_json['results'][0]: raise TowerCLIError("Unable to get user information from Tower") self.my_user = response_json['results'][0]['id'] an_asset['user'] = self.my_user # Second, if this is a custom defined template we need to see if there is a required # password field in it self.set_password_in_custom_credential(an_asset, asset_name) # Third we need to make sure that any required passwords are set if 'inputs' in an_asset: if 'vault_password' in an_asset['inputs'] and an_asset['inputs']['vault_password'] == '': an_asset['inputs']['vault_password'] = self.get_secret( 'Enter vault password for {}'.format(asset_name), "Setting vault_password for {}".format(asset_name), 'password' ) if 'password' in an_asset['inputs'] and an_asset['inputs']['password'] == '': an_asset['inputs']['password'] = self.get_secret( "Enter the password for {}".format(asset_name), "Setting password for {}".format(asset_name), 'password' ) if 'security_token' in an_asset['inputs'] and an_asset['inputs']['security_token'] == '': an_asset['inputs']['security_token'] = self.get_secret( 'Enter the security token for {}'.format(asset_name), "Setting security token for {}".format(asset_name), 'token' ) if 'become_password' in an_asset['inputs'] and an_asset['inputs']['become_password'] == '': an_asset['inputs']['become_password'] = self.get_secret( 'Enter the become password for {}'.format(asset_name), "Setting become password for {}".format(asset_name), 'password' ) if 'secret' in an_asset['inputs'] and an_asset['inputs']['secret'] == '': an_asset['inputs']['secret'] = self.get_secret( 'Enter the secret for {}'.format(asset_name), "Setting secret for {}".format(asset_name), 'secret' ) if 'authorize_password' in an_asset['inputs']\ and an_asset['inputs']['authorize_password'] == '': an_asset['inputs']['authorize_password'] = self.get_secret( 'Enter the authorize password for {}'.format(asset_name), "Setting authorize password for {}".format(asset_name), 'password' ) if 'ssh_key_unlock' in an_asset['inputs'] and an_asset['inputs']['ssh_key_unlock'] == '': self.log_warn("*** Setting ssh key for {} to 'password'".format(asset_name)) an_asset['inputs']['ssh_key_unlock'] = 'password' an_asset['inputs']['ssh_key_data'] = "-----BEGIN RSA PRIVATE KEY-----\n" \ "Proc-Type: 4,ENCRYPTED\n" \ "DEK-Info: AES-128-CBC,410307D0168F2A93EAAD78F85C136ED8\n" \ "\n" \ "Vtjf3RaotxIMjgKLfoDeR3xEksmOWXk8Ei4iK5T8dEWxkRsM/asRe6gMeGyOPv73\n" \ "wxjPunVmovY/09FyXIk7W2HT4gt7kF3Qvz028taTjkF/T1YAvMarBSL6PgPdamCq\n" \ "oXAfPjHoqBdRqEmEcclIZ7WW4bXIEzw1f5ad1mERl+5O2/KNdM0R6EhemporF46/\n" \ "En8jQm+kQefxgI9EDH79WkRK0BhcW7Ho7Jb4EfHEJnBaoo0NGp8rJh0bCNoaa1Q7\n" \ "nlFHJNbZioNDsEaVc7nzZdIfISWx5UAPWj6meuvhylbaZXydxfkUccCI4bP+YYFf\n" \ "y7qfkkfqBpcVelYmO5Ymwd4bLCayUB8HszHpPoKcfyANt/hCFjavb8rffsH31c+r\n" \ "4eSWr2XD1OVFUTQpKizp7NyaoPZMhgi1CUTc23NZOc/YBoVIyAj1NfL8Vw1xafwo\n" \ "VKoePmLO7Gk5FblQKWldDsigcRNtgSzBWTHtzBPcHDzHO9k1H6spxFANgaSS792f\n" \ "h24bUAjq0Q1/nER5k1HyS2ZfWsYUeSh0DEkdZ1LqGA49H/XwAPHpSZSremFm7yuM\n" \ "98EFzIne21/hJ0qefrggPGcy9wHbF+RqpLIDR7K/gSfJKxD/vb9GBaBn1Wl1jskV\n" \ "wOsnlxDk0jBIZ+P6c7Werp8ZyDh9kupMUjYNrl4WaftF/y2dT1RRzAVEgEMt/Y6H\n" \ "giGNQmVnC7Q6Gl7bO6qt6rjcAoc8R0Y+LfgEFXRio3oD8n7K/lq5wAfs9K1SbKTi\n" \ "FBiUrQInsmeioBNlz1AHW3VrRbWCeu7xqCFK8B5ZTcPD4w6DY86SGmzPBIP65Rhd\n" \ "mqxIiXqfC5z+xkZGT9LNFwzO3Ooa7aBMK9g/3l50bqiMeHUcBK0hdWPoZgbRHcOK\n" \ "cNIcbDBy9FeVpYRYtQNh0HKR/B/JmwajegGWGVy1tw3t+JTFsdFh2XbBELvfHvfg\n" \ "W/RRHIaupD+t9GELZNWs2NigOvy0vXXC1rivVvQM6YX3qO0TLwuX0c9jUixK1bga\n" \ "tZy8GLMoeI6jMPDWi1pGEBvtKTHLryWU3WOTr+9UH6Sqh0nf9ErcMHX9GchesjIW\n" \ "EOyQWeVRx/xzGL9d76Wtnl5r07k666G9+6XyoOiPtdc2C6kRBD+RvgHC2iLGiRfW\n" \ "6Su5234VUz5HbGmwRKnfYJiJwmfiSNyP7K6WTDaefczyziN6rY7IpfbM+VysP2xz\n" \ "57inDRvuJCgGSoW0o3zCRSwSpNrf6W6I0HWbD3D7kiuVwukXEYoQUqIaCWjD0+yR\n" \ "ps8Oq/JDBlTqjMY3TbwOEkw0LJaeFtVp9vyz1JLzpTYWAjGJJHZEX1pjgRk2JCHK\n" \ "+Muo8WEv0pW82h0UxlgUkYYdSEItjmPkCDdIyZRCuMoPci1wS4kvlIKjsKoydZJP\n" \ "fR/nifcvaEWGuGHzpzi6kVefmfrB+BvHBvkwSz0Q2xeGkcx46CYpoQ6OvWj0sz7v\n" \ "mQqh574xfKPrJcFmIs/CYpzeh/eCMzYVEKQ/BPhpji2AiLnqjFy25kSw21kNClPP\n" \ "N5uEso3u8YlILmnyfIzA78riIz1EukMinrNQRHMirAeCtuSzfgeE0zUjWZnL1Xus\n" \ "-----END RSA PRIVATE KEY-----\n" elif 'ssh_key_data' in an_asset['inputs'] and an_asset['inputs']['ssh_key_data'] == '': self.log_warn("*** Setting ssh key for {} to some random default".format(asset_name)) an_asset['inputs']['ssh_key_data'] = "-----BEGIN RSA PRIVATE KEY-----\n" \ "MIIEowIBAAKCAQEA0TNd16gyE0o4end0fpUZGMuseXyggUZiFsvkzSgwqMks4Rws\n" \ "khN59Wzz3F1FPPDzUKu3x2DRDMPH8Js7YMaFXE+EY20dP75gPrykWeGZoyJWcWMf\n" \ "PjITj7veNwrYsGxvPZd7qUg4WVo5rVrKiMtLaVAdQdqNiF/lQNCxwyew7HMj6Hkg\n" \ "WpR9XbdSE8SnS8j9R+FA4B2u6oy/FxDRDs5Rq866Tv782kqxG1vcdwTeuc4kWVji\n" \ "zpsOkZ7Ur1pXiyFoAOJ2sDCIja0/D9u+4ey4s+pLCZpeY8NVFu9P4rKKcNJ5O3e0\n" \ "d8nNxHckXALp3nnEVgKGojNhWM1gPQR0PQREBwIDAQABAoIBACosjNKZGd9Bqzkl\n" \ "M9sA+9o/1Tl4onLtWYD3Ad1KKOUeCWooX+PjAUc0+8SFGRw8BxFQTPBo2DwWjAw5\n" \ "fzL3UpNVhH721Fqxan27Ufa8wFhe58ZcEURcnAzx9s5p5V1LvvFPxKJP6Ow6gD4u\n" \ "e34wXbeRaxSHltjTXEhAylVpfwVro0xo9TokAgz3+xAW5d4343aYFrSU2ExrP79U\n" \ "UQaW60OSkHCJ2Dx5hT/u8qJx4rsR8Zjv7PKwMC5qvy1jRwL6E0guIftJP+fT4Ijj\n" \ "dY6yyCiKEYXyhOFO2R2kSGmNMtpiNm1jITTCNQzm4v6McXD6GP1HiviHNKv5MfVZ\n" \ "TWxwj6ECgYEA+mXdCqLIHE7cAn7do5GEtwZYDlt9LgjT+yIDRPahoJTrXPtF4sFf\n" \ "o31wFWjQ/WvtIl80DT7heBbL5Hdql6ppqzFe08T5rf9VoccozghCqGaawDN1nob6\n" \ "dv9vt0UYV/Z5yMMG2a5abBUQDywgtDczGadz5/5qBIfAu3c2J49HmMMCgYEA1eGM\n" \ "eXwYaHrIPnOdWLqynAGaTQML/Hpqe1zA9xKsOsSenfvdg7EumrnRr580ejQiVO3y\n" \ "L83wj5vcrFI9onWpbsvr6qlFrEO1d156ZF5sn8cHRHYdGWJotsmAQP/FNGxzzOw0\n" \ "HiQgrbwxN/WIdWmrE1N6Ys3oQ5DWvXKUWW7rU20CgYEAyAxvz5qDs5IRVfETlCWj\n" \ "WTI5UacoWIn3CfF/mS5NrOStMYkSqXoCtbR2wrQOHBmIx+g1xstRCUd1OB9ryqX8\n" \ "bCgycZAyRh/zwx9Ba3HQB4iJ5Dp4ouGF42JqV4pdS5GAdLPTmkAgv68IOIbxzek3\n" \ "6ywMfvGUs+/dPCie3HYtJk8CgYAe586ip1nnjwZsb8xmy+OPQ3QGeNA8lXvZg5em\n" \ "nB4jB9Jbxc9Gfk3bscoo9HpixjHHz/JVEg8W0VDb3a5mUVZAWlsmt3sH32jTbOWG\n" \ "p1ZO6DWWoPKnfl7fOtK7kbnvT1SUYfVN/a5zLGR4T5R+UtyTmFZw/Iv5Z26ARZRG\n" \ "MA71KQKBgEvMsDIWGl47l12UJ6KBwzV4c5WVjzKf7GUpbsLIyv71pNgfXkhLF6Nf\n" \ "OKb4Dr4esKGodApTIj3dlsU88E1g7zD5jRuHYutbnCen1TEN91DtGgWkeYSuk9/z\n" \ "mZ1uQonJSPmz97kj5L6i/4UZbTDRiyVOyIq5/lTsE/v7258DzKQe\n" \ "-----END RSA PRIVATE KEY-----" # TowerCLI wants extra_vars to be in a list, not a string self.touchup_extra_vars(an_asset) try: existing_object = resource.create(**an_asset) asset_changed = True self.log_change("Created {} {}".format(asset_type, asset_name)) except TowerCLIError as e: self.log_error("Failed to create {} {} : {}".format(asset_type, asset_name, e)) continue else: # First take off the ID object_id = existing_object['id'] # Special cases if asset_type == 'project': common.remove_local_path_from_scm_project(an_asset) common.remove_local_path_from_scm_project(existing_object) # This will compare an_asset to existing_object and update an_asset if needed # For example, if the new Tower instance has additional stuff it will make sure it gets removed reduced_object = copy.deepcopy(an_asset) if self.does_asset_need_update(reduced_object, existing_object, post_options): # When reducing an object, there may be fields we need to copy back in. # For example, if the credential type is the same they will be removed from reduced_object # But the API needs the credential type to be set in order to process the request if asset_type == 'credential': if 'credential_type' not in reduced_object: reduced_object['credential_type'] = an_asset['credential_type'] # TowerCLI wants extra_vars to be in a list, not a string self.touchup_extra_vars(reduced_object) try: resource.write(pk=object_id, **reduced_object) asset_changed = True self.log_change("Updated asset") except TowerCLIError as e: self.log_error("Failed to update {} {} : {}".format(asset_type, asset_name, e)) continue else: self.log_ok("Asset up to date") # If there are relations, import them if relations is not None: # Schedules have to be imported after the survey because adding extra data to a schedule # will cause the API to check the job template for input options. schedules_to_import = [] for a_relation in relations: if a_relation == 'survey_spec': survey = tower_cli.get_resource(asset_type).survey(existing_object['id']) if survey != relations[a_relation]: self.log_change("Updating survey") resource.modify(pk=existing_object['id'], survey_spec=relations[a_relation]) else: self.log_ok("Survey up to date") elif a_relation == 'workflow_nodes': # In can_object_post, we deep copy the initial set of nodes and add it to the # end of the array This enables us to compare the exported nodes from the target # server to the unresolved nodes in the original import request # This has to be done because the can_object_post method resolves all of the dependencies existing_workflow_nodes = common.extract_workflow_nodes(existing_object) new_workflow_nodes_unresolved = relations[a_relation].pop() if not self.are_workflow_nodes_the_same( existing_workflow_nodes, new_workflow_nodes_unresolved ): self.import_workflow_nodes(existing_object, relations[a_relation]) else: self.log_ok("Workflow nodes up to date") elif a_relation == 'host' or a_relation == 'inventory_source': self.import_inventory_relations(existing_object, relations[a_relation], a_relation) elif a_relation == 'group': self.import_inventory_groups(existing_object, relations[a_relation]) elif a_relation in common.NOTIFICATION_TYPES: self.import_notification_relations(existing_object, relations[a_relation], a_relation) elif a_relation == 'credentials': self.import_credentials(existing_object, relations[a_relation]) elif a_relation == 'schedules': schedules_to_import.append(relations[a_relation]) elif a_relation == 'roles': self.import_roles(existing_object, relations[a_relation], asset_type) elif a_relation == 'labels': self.import_labels(existing_object, relations[a_relation], asset_type) else: self.log_error("Relation {} is not supported".format(a_relation)) # Now that everything else was imported, we can import the schedule if there is one for schedule in schedules_to_import: self.import_schedules(existing_object, schedule, asset_type) # Checking for post update actions on the different objects if asset_changed: if asset_type == 'project': try: resource.update(existing_object['id'], wait=True) except CannotStartJob: # Manual projects will raise a CannotStartJob exception pass except JobFailure: self.log_warn("Failed to update project {} : This may cause other errors.".format( existing_object['name']) ) self.print_recap() def get_secret(self, prompt, default_string, default): # Generate a random secret if self.secret_management == 'random': # Generate random string import random import string password = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(12)) self.log_warn("{} to '{}'".format(default_string, password)) return password # prompt for a secret if self.secret_management == 'prompt': # Prompt the user import getpass while True: password = getpass.getpass("{}: ".format(prompt)) password_confirm = getpass.getpass("Enter that again to confirm: ") if password == password_confirm: return password print("") print("Mismatch, please try again") print("") self.log_warn("{} to '{}'".format(default_string, default)) return default # Takes an object and the post definition and validates that all the bits and pieces are there. def can_object_post(self, asset_type, an_asset, post_options): # Somethings might not have post_options, if so we can't check them so just return no errors if post_options is None: return True post_check_succeeded = True if common.get_identity(asset_type) not in an_asset: return False name = an_asset[common.get_identity(asset_type)] # First, make sure that all of the required options were given to us # We have to do this first because later we are going to delete # required to make sure we are not missing an option for option in post_options: if "required" not in post_options[option]: self.log_error("Required is not defined for {}\n{}".format( option, json.dumps(post_options[option], indent=4) )) post_check_succeeded = False continue if "required" in post_options[option] and post_options[option]["required"] and option not in an_asset: self.log_error("{} is required but is not defined for {} {}".format(option, asset_type, name)) post_check_succeeded = False # Next check to make sure that any entries that the user gave us is actually an option for the post method for option in an_asset: # We know these do not matter if option == common.ASSET_TYPE_KEY or option == common.ASSET_RELATION_KEY: continue if option not in post_options: self.log_error("Option {} is not a valid option for {} {}".format(option, asset_type, name)) post_check_succeeded = False continue # If there is a max_length make sure the size is right if "max_length" in post_options[option]: if len(an_asset[option]) > post_options[option]["max_length"]: self.log_error("Option {} has exceeded max length of {} for {} {}".format( option, post_options[option]["max_length"], asset_type, name )) post_check_succeeded = False # If it is an option check to make sure that the value matches if "choices" in post_options[option]: valid_choice = False # Choices is an array like [ [ value, label ], [value, label], ... ] valid_options = [] for a_choice in post_options[option]["choices"]: if isinstance(a_choice, six.string_types): m = re.match(r"^\('(?P.*)',\s'(?P