def deploy(ami_id): """ Deploys an AMI as an auto-scaling group (ASG) to AWS. Arguments: ami_id(str): AWS AMI ID Returns: dict(str, str, dict): Returns a dictionary with the keys: 'ami_id' - AMI id used to deploy the AMI 'current_asgs' - Lists of current active ASGs, keyed by cluster. 'disabled_asgs' - Lists of current inactive ASGs, keyed by cluster. Raises: TimeoutException: When the task to bring up the new instance times out. BackendError: When the task to bring up the new instance fails. ASGDoesNotExistException: If the ASG being queried does not exist. """ LOG.info("Processing request to deploy {}.".format(ami_id)) # Pull the EDP from the AMI ID edp = ec2.edp_for_ami(ami_id) # These are all autoscaling groups that match the tags we care about. existing_edp_asgs = ec2.asgs_for_edp(edp, filter_asgs_pending_delete=False) # Find the clusters for all the existing ASGs. existing_clustered_asgs = clusters_for_asgs(existing_edp_asgs) LOG.info("Deploying to cluster(s) {}".format(existing_clustered_asgs.keys())) # Create a new ASG in each cluster. new_clustered_asgs = defaultdict(list) for cluster in existing_clustered_asgs: try: newest_asg = new_asg(cluster, ami_id) new_clustered_asgs[cluster].append(newest_asg) except: msg = "ASG creation failed for cluster '{}' but succeeded for cluster(s) {}." msg = msg.format(cluster, new_clustered_asgs.keys()) LOG.error(msg) raise new_asgs = [asgs[0] for asgs in new_clustered_asgs.values()] LOG.info("New ASGs created: {}".format(new_asgs)) ec2.wait_for_in_service(new_asgs, 300) LOG.info("New ASGs healthy: {}".format(new_asgs)) LOG.info("Enabling traffic to new ASGs for the {} cluster(s).".format(existing_clustered_asgs.keys())) success, enabled_asgs, disabled_asgs = _red_black_deploy(dict(new_clustered_asgs), existing_clustered_asgs) if not success: raise BackendError("Error performing red/black deploy - deploy was unsuccessful. " "enabled_asgs: {} - disabled_asgs: {}".format(enabled_asgs, disabled_asgs)) LOG.info("Woot! Deploy Done!") return {'ami_id': ami_id, 'current_asgs': enabled_asgs, 'disabled_asgs': disabled_asgs}
def deploy(ami_id): """ Deploys an AMI as an auto-scaling group (ASG) to AWS. Arguments: ami_id(str): AWS AMI ID Returns: dict(str, str, dict): Returns a dictionary with the keys: 'ami_id' - AMI id used to deploy the AMI 'current_asgs' - Lists of current active ASGs, keyed by cluster. 'disabled_asgs' - Lists of current inactive ASGs, keyed by cluster. Raises: TimeoutException: When the task to bring up the new instance times out. BackendError: When the task to bring up the new instance fails. ASGDoesNotExistException: If the ASG being queried does not exist. """ LOG.info("Processing request to deploy {}.".format(ami_id)) # Pull the EDP from the AMI ID edp = ec2.edp_for_ami(ami_id) # These are all autoscaling groups that match the tags we care about. existing_edp_asgs = ec2.asgs_for_edp(edp, filter_asgs_pending_delete=False) # Find the clusters for all the existing ASGs. existing_clustered_asgs = clusters_for_asgs(existing_edp_asgs) LOG.info("Deploying to cluster(s) {}".format(existing_clustered_asgs.keys())) # Create a new ASG in each cluster. new_clustered_asgs = defaultdict(list) for cluster in existing_clustered_asgs: try: newest_asg = new_asg(cluster, ami_id) new_clustered_asgs[cluster].append(newest_asg) except: msg = "ASG creation failed for cluster '{}' but succeeded for cluster(s) {}." msg = msg.format(cluster, new_clustered_asgs.keys()) LOG.exception(msg) raise new_asgs = [asgs[0] for asgs in new_clustered_asgs.values()] LOG.info("New ASGs created: {}".format(new_asgs)) ec2.wait_for_in_service(new_asgs, 300) LOG.info("New ASGs healthy: {}".format(new_asgs)) LOG.info("Enabling traffic to new ASGs for the {} cluster(s).".format(existing_clustered_asgs.keys())) success, enabled_asgs, disabled_asgs = _red_black_deploy(dict(new_clustered_asgs), existing_clustered_asgs) if not success: raise BackendError("Error performing red/black deploy - deploy was unsuccessful. " "enabled_asgs: {} - disabled_asgs: {}".format(enabled_asgs, disabled_asgs)) LOG.info("Woot! Deploy Done!") return {'ami_id': ami_id, 'current_asgs': enabled_asgs, 'disabled_asgs': disabled_asgs}
def rollback(current_clustered_asgs, rollback_to_clustered_asgs, ami_id=None): """ Rollback to a particular list of ASGs for one or more clusters. If rollback does not succeed, create new ASGs based on the AMI ID and deploy those ASGs. Arguments: current_clustered_asgs(dict): ASGs currently enabled, grouped by cluster. rollback_to_clustered_asgs(dict): ASGs to rollback to, grouped by cluster. ami_id(str): AWS AMI ID to which to rollback to. Returns: dict(str, str, dict): Returns a dictionary with the keys: 'ami_id' - AMI id used to deploy the AMI, None if unspecified. 'current_ami_id' - The AMI that is running in an environment after a rollback. 'current_asgs' - Lists of current active ASGs, keyed by cluster. 'disabled_ami_id' - The AMI that was running in an environment before the rollback. 'disabled_asgs' - Lists of current inactive ASGs, keyed by cluster. Raises: TimeoutException: When the task to bring up the new instance times out. BackendError: When the task to bring up the new instance fails. ASGDoesNotExistException: If the ASG being queried does not exist. """ # First, ensure that the ASGs to which we'll rollback are not tagged for deletion. # Also, ensure that those same ASGs are not in the process of deletion. rollback_ready = True asgs_tagged_for_deletion = [asg.name for asg in ec2.get_asgs_pending_delete()] for asgs in rollback_to_clustered_asgs.values(): for asg in asgs: try: if asg in asgs_tagged_for_deletion: # ASG is tagged for deletion. Remove the deletion tag. ec2.remove_asg_deletion_tag(asg) if is_asg_pending_delete(asg): # Too late for rollback - this ASG is already pending delete. LOG.info("Rollback ASG '{}' is pending delete. Aborting rollback to ASGs.".format(asg)) rollback_ready = False break except ASGDoesNotExistException: LOG.info("Rollback ASG '{}' has been removed. Aborting rollback to ASGs.".format(asg)) rollback_ready = False break disabled_ami_id = None try: # fetch the currently deployed AMI_ID for the clusters for logging if ami_id: edp = ec2.edp_for_ami(ami_id) disabled_ami_id = ec2.active_ami_for_edp(edp.environment, edp.deployment, edp.play) except (MissingTagException, ImageNotFoundException, MultipleImagesFoundException): # don't want to fail on this info not being available, we'll assign UNKNOWN and # continue with the rollback disabled_ami_id = 'UNKNOWN' if rollback_ready: # Perform the rollback. success, enabled_asgs, disabled_asgs = _red_black_deploy(rollback_to_clustered_asgs, current_clustered_asgs) if not success: LOG.info("Rollback failed for cluster(s) {}.".format(current_clustered_asgs.keys())) else: LOG.info("Woot! Rollback Done!") return { 'ami_id': ami_id, 'current_ami_id': ami_id, 'current_asgs': enabled_asgs, 'disabled_ami_id': disabled_ami_id, 'disabled_asgs': disabled_asgs } # Rollback failed -or- wasn't attempted. Attempt a deploy. if ami_id: LOG.info("Attempting rollback via deploy of AMI {}.".format(ami_id)) return deploy(ami_id) else: LOG.info("No AMI id specified - so no deploy occurred during rollback.") return { 'ami_id': None, 'current_ami_id': ami_id, 'current_asgs': current_clustered_asgs, 'disabled_ami_id': disabled_ami_id, 'disabled_asgs': rollback_to_clustered_asgs }
def test_edp2_for_tagged_ami(self): actual_edp = ec2.edp_for_ami(self._make_fake_ami()) expected_edp = EDP("foo", "bar", "baz") # Happy Path self.assertEqual(expected_edp, actual_edp)