Skip to content

Commit

Permalink
Merge a6057da into 9b0a4a2
Browse files Browse the repository at this point in the history
  • Loading branch information
mayn committed Jun 14, 2018
2 parents 9b0a4a2 + a6057da commit d0aca78
Show file tree
Hide file tree
Showing 16 changed files with 133 additions and 101 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# top-most EditorConfig file
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
trim_trailing_whitespace = true

[*.py]
max_line_length = 119
indent_size = 4
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@
*htmlcov
.DS_Store
.localized
*.idea
.tox
.pytest_cache
dist
.cache
9 changes: 5 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
language: python
python:
- "2.7"
- "2.6"
- "3.6"

install:
- python setup.py install
- pip install coveralls
- pip install tox-travis
- pip install tox==2.6.0

before_script:
- export PYTHONPATH=$PYTHONPATH:$PWD

script:
- coverage run --source License2Deploy setup.py test
- tox

after_success:
coveralls
- if [ "$TRAVIS_PYTHON_VERSION" == "2.7" ]; then coveralls; fi
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
v0.0.1 First working version
v0.2.0 python 3 compatibility, add cli entrypoint
9 changes: 0 additions & 9 deletions CustomInstall.py

This file was deleted.

9 changes: 4 additions & 5 deletions License2Deploy/AWSConn.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#!/usr/bin/python

import boto.ec2 as ec2
import boto.ec2.autoscale as a
import boto.ec2.elb as elb
Expand All @@ -8,6 +6,7 @@
import logging
import yaml


class AWSConn(object):

@staticmethod
Expand All @@ -20,7 +19,7 @@ def aws_conn_auto(region, profile='default'):

@staticmethod
def aws_conn_ec2(region, profile='default'):
try:
try:
conn = ec2.connect_to_region(region, profile_name=profile)
return conn
except Exception as e:
Expand All @@ -36,7 +35,7 @@ def aws_conn_elb(region, profile='default'):

@staticmethod
def aws_conn_cloudwatch(region, profile='default'):
try:
try:
conn = cloudwatch.connect_to_region(region, profile_name=profile)
return conn
except Exception as e:
Expand All @@ -51,7 +50,7 @@ def get_boto3_client(client_type, region, profile='default', session=None):
@staticmethod
def load_config(config):
with open(config, 'r') as stream:
return yaml.load(stream)
return yaml.safe_load(stream)

@staticmethod
def determine_region(reg):
Expand Down
1 change: 0 additions & 1 deletion License2Deploy/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
#
54 changes: 28 additions & 26 deletions License2Deploy/rolling_deploy.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#!/usr/bin/env python

import logging
import argparse
from sys import exit, argv
from time import sleep, time
from AWSConn import AWSConn
from set_logging import SetLogging
from .AWSConn import AWSConn
from .set_logging import SetLogging
from retry.api import retry_call


class RollingDeploy(object):

MAX_RETRIES = 10
Expand Down Expand Up @@ -63,7 +62,7 @@ def get_ami_id_state(self, ami_id):
return ami_obj[0]

def wait_ami_availability(self, ami_id, timer=20):
''' Timeout should be in minutes '''
""" Timeout should be in minutes """
timeout = time() + 60 * timer
while True:
ami_state = self.get_ami_id_state(ami_id).state
Expand Down Expand Up @@ -91,10 +90,10 @@ def get_group_info(self, group_name=None):
exit(self.exit_error_code)

def get_autoscale_group_name(self):
''' Search for project in autoscale groups and return autoscale group name '''
""" Search for project in autoscale groups and return autoscale group name """
if self.stack_name:
return self.get_autoscaling_group_name_from_cloudformation()
return next((instance.name for instance in filter(lambda n: n.name, self.get_group_info()) if self.project in instance.name and self.env in instance.name), None)
return next((instance.name for instance in [n for n in self.get_group_info() if n.name] if self.project in instance.name and self.env in instance.name), None)

def get_autoscaling_group_name_from_cloudformation(self):
if not self.autoscaling_group:
Expand All @@ -121,7 +120,7 @@ def get_lb(self):
exit(self.exit_error_code)

def calculate_autoscale_desired_instance_count(self, group_name, desired_state):
''' Search via specific autoscale group name to return modified desired instance count '''
""" Search via specific autoscale group name to return modified desired instance count """
try:
cur_count = int(self.get_group_info(group_name)[0].desired_capacity)
if desired_state == 'increase':
Expand All @@ -133,17 +132,17 @@ def calculate_autoscale_desired_instance_count(self, group_name, desired_state):
except Exception as e:
logging.error("Please make sure the desired_state is set to either increase or decrease: {0}".format(e))
exit(self.exit_error_code)

def double_autoscale_instance_count(self, count):
''' Multiply current count by 2 '''
""" Multiply current count by 2 """
return count * 2

def decrease_autoscale_instance_count(self, count):
''' Divide current count in half '''
""" Divide current count in half """
return count / 2

def set_autoscale_instance_desired_count(self, new_count, group_name):
''' Increase desired count by double '''
""" Increase desired count by double """
try:
logging.info("Set autoscale capacity for {0} to {1}".format(group_name, new_count))
self.conn_auto.set_desired_capacity(group_name, new_count)
Expand All @@ -167,7 +166,7 @@ def get_instance_ip_addrs(self, id_list=[]):
exit(self.exit_error_code)

def get_all_instance_ids(self, group_name):
''' Gather Instance id's of all instances in the autoscale group '''
""" Gather Instance id's of all instances in the autoscale group """
instances = [i for i in self.get_group_info(group_name)[0].instances]
id_list = [instance_id.instance_id for instance_id in instances]
return id_list
Expand All @@ -180,7 +179,7 @@ def get_reservations(self, id_list):
return self.conn_ec2.get_all_reservations(instance_ids=id_list)

def get_instance_ids_by_requested_build_tag(self, id_list, build):
''' Gather Instance id's of all instances in the autoscale group '''
""" Gather Instance id's of all instances in the autoscale group """
reservations = self.get_reservations(id_list)
if self.force_redeploy:
id_list = [id for id in id_list if id not in self.existing_instance_ids]
Expand All @@ -202,7 +201,7 @@ def get_new_instances_count(self):
return self.new_desired_capacity / 2

def wait_for_new_instances(self, instance_ids, retry=10, wait_time=30):
''' Monitor new instances that come up and wait until they are ready '''
""" Monitor new instances that come up and wait until they are ready """
for instance in instance_ids:
count = 0
health = []
Expand All @@ -221,18 +220,18 @@ def wait_for_new_instances(self, instance_ids, retry=10, wait_time=30):
logging.info("{0} is in a healthy state. Moving on...".format(instance))

def lb_healthcheck(self, new_ids):
''' Confirm that the healthchecks report back OK in the LB. '''
""" Confirm that the healthchecks report back OK in the LB. """
instance_ids = self.conn_elb.describe_instance_health(self.load_balancer, new_ids)
status = filter(lambda instance: instance.state != "InService", instance_ids)
status = [instance for instance in instance_ids if instance.state != "InService"]
if status:
raise Exception('Must check load balancer again. Following instance(s) are not "InService": {0}'.format(status))
else:
logging.info('ELB healthcheck OK')
return True

def calculate_max_minutes(self, tries, delay):
return tries * delay / 60

def only_new_instances_check(self):
instance_ids = self.conn_elb.describe_instance_health(self.load_balancer)
for instance in instance_ids:
Expand All @@ -251,10 +250,10 @@ def confirm_lb_has_only_new_instances(self):
exit(self.exit_error_code)

def tag_ami(self, ami_id, env):
''' Tagging AMI with DEPLOYED tag '''
""" Tagging AMI with DEPLOYED tag """
try:
current_tag = self.conn_ec2.get_all_images(image_ids=ami_id)[0].tags.get('deployed')
if not current_tag:
if not current_tag:
logging.info("No DEPLOY tags exist, tagging with {0}".format(env))
self.conn_ec2.create_tags([self.ami_id], {"deployed": env})
elif env not in current_tag:
Expand Down Expand Up @@ -316,7 +315,7 @@ def retrieve_project_cloudwatch_alarms(self):
return project_cloud_watch_alarms

def disable_project_cloudwatch_alarms(self):
''' Disable all the cloud watch alarms '''
""" Disable all the cloud watch alarms """
project_cloud_watch_alarms = self.retrieve_project_cloudwatch_alarms()
for alarm in project_cloud_watch_alarms:
try:
Expand All @@ -327,7 +326,7 @@ def disable_project_cloudwatch_alarms(self):
exit(self.exit_error_code)

def enable_project_cloudwatch_alarms(self):
''' Enable all the cloud watch alarms '''
""" Enable all the cloud watch alarms """
project_cloud_watch_alarms = self.retrieve_project_cloudwatch_alarms()
for alarm in project_cloud_watch_alarms:
logging.info("Found an alarm. {0}".format(alarm))
Expand All @@ -354,7 +353,7 @@ def stop_deploy(self, message='an error has occurred', e=None, error_code=2):

def deploy(self): # pragma: no cover
self.load_balancer = self.get_lb()
''' Rollin Rollin Rollin, Rawhide! '''
""" Rollin Rollin Rollin, Rawhide! """
group_name = self.get_autoscale_group_name()
self.wait_ami_availability(self.ami_id)
logging.info("Build #: {0} ::: Autoscale Group: {1}".format(self.build_number, group_name))
Expand All @@ -373,7 +372,7 @@ def deploy(self): # pragma: no cover
logging.info("Deployment Complete!")

def revert_deployment(self): #pragma: no cover
''' Will revert back to original instances in autoscale group '''
""" Will revert back to original instances in autoscale group """
logging.error("REVERTING: Removing new instances from autoscale group")
group_name = self.get_autoscale_group_name()
new_instance_ids = self.gather_instance_info(group_name)
Expand All @@ -386,6 +385,7 @@ def revert_deployment(self): #pragma: no cover
logging.error("REVERT COMPLETE!")
exit(self.exit_error_code)


def get_args(): # pragma: no cover
parser = argparse.ArgumentParser()
parser.add_argument('-e', '--environment', action='store', dest='env', help='Environment e.g. qa, stg, prd', type=str, required=True)
Expand All @@ -402,11 +402,13 @@ def get_args(): # pragma: no cover
parser.add_argument('-o', '--only-new-wait', action='store', dest='only_new_wait', help='Wait time for old ec2 instances to terminate', type=int, nargs=2, default=[10, 30])
return parser.parse_args()


def main(): # pragma: no cover
args = get_args()
SetLogging.setup_logging()
deployObj = RollingDeploy(args.env, args.project, args.build_number, args.ami_id, args.profile, args.config, args.stack_name, args.force_redeploy, None, args.creation_wait, args.ready_wait, args.health_wait, args.only_new_wait)
deployObj.deploy()



if __name__ == "__main__": # pragma: no cover
main()
5 changes: 1 addition & 4 deletions License2Deploy/set_logging.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
#!/usr/bin/python

import logging


class SetLogging(object):

@staticmethod
def setup_logging(): # pragma: no cover
logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s',level=logging.INFO)
logging.info("Begin Logging...")

if __name__ == '__main__':
SetLogging.setup_logging() # pragma: no cover
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Rolling deploys for AWS AutoScale Groups
What is License2Deploy?
==================

License2Deploy is an automated solution for rolling deployments in AWS written in python.
License2Deploy is an automated solution for rolling deployments in AWS written in python.

The rolling deployment will:
- Double your instances in your autoscale group
Expand All @@ -20,7 +20,7 @@ The rolling deployment will:
Usage
==================
```
usage: rolling_deploy.py [-h] -e ENV -p PROJECT -b BUILD_NUM -a AMI_ID
usage: rolling_deploy [-h] -e ENV -p PROJECT -b BUILD_NUM -a AMI_ID
[-P PROFILE] [-c CONFIG] [-s STACK_NAME] [-f FORCE_REDEPLOY]
[-C CREATION_WAIT] [-r READY_WAIT] [-H HEALTH_WAIT] [-o ONLY_NEW_WAIT]
Expand Down Expand Up @@ -72,7 +72,7 @@ There are a few requirements in order for the automated rolling deployments to w
4. All instances in the autoscale group need to be tagged with a build number
* This is an important step as when the script runs, it will differentiate the old builds
from the new builds based off of the build number that is passed in as a command line parameter
5. The credentials for the user need to be in the ~/.aws/credentials file and if not passed in as a
5. The credentials for the user need to be in the ~/.aws/credentials file and if not passed in as a
command line argument, the script will look at the 'default' profile.
6. The script needs the AMI ID of the instances that will be built.
* The reason for the AMI ID is to ensure that if it was just created, it is not in a pending state
Expand All @@ -84,8 +84,10 @@ There are a few requirements in order for the automated rolling deployments to w
Development
============

python setup.py install
To run unit tests:

python setup.py test
```sh
$ tox
```

python License2Deploy/rolling_deploy.py
13 changes: 13 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[coverage:run]
branch = true
source = License2Deploy

[coverage:report]
omit =
tests
.tox

[tool:pytest]
addopts = --verbose
testpaths =
tests
Loading

0 comments on commit d0aca78

Please sign in to comment.