Created
September 17, 2012 20:25
-
-
Save andrewgross/3739566 to your computer and use it in GitHub Desktop.
Add Server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import subprocess | |
import sys | |
import time | |
from boto.ec2.connection import EC2Connection | |
from chef import ChefAPI, Node | |
SSH_CONFIG_BLOCK = """ | |
host %(host)s | |
User ubuntu | |
hostname %(hostname)s | |
IdentityFile ~/.ssh/id_rsa | |
""" | |
VPC_SSH_CONFIG_BLOCK = """ | |
host %(host)s | |
User ubuntu | |
hostname %(hostname)s | |
ProxyCommand ssh -q -o StrictHostKeyChecking=no vpc_%(environment)s_nat1 nc %%h 22 | |
IdentityFile ~/.ssh/id_rsa | |
""" | |
SERVER_OPTS = { | |
"dev" : | |
{ | |
"machine_size": 'm1.large', | |
"security_groups": ['default', 'linux', 'web'], | |
"subnet": "subnet-1234abcd", | |
}, | |
"staging_web" : | |
{ | |
"machine_size": "m1.large", | |
"security_groups": ['default', 'linux', 'web'], | |
"subnet": "subnet-1234abcd", | |
}, | |
"prod_web" : | |
{ | |
"machine_size": "c1.xlarge", | |
"security_groups": ['default', 'linux', 'web'] | |
}, | |
} | |
SUBNETS = { | |
'public': ['subnet-1234abcd'], | |
'private': ['subnet-abcd1234'] | |
} | |
class Machine(object): | |
""" | |
Class to contain all the information about a new AWS machine configuration | |
in an easy to use format. Additional information about the machine stored | |
by AWS can be retrieved via class methods. | |
""" | |
def get_machine_name(self): | |
""" | |
Get the human readable machine name using the machine type and | |
searching Knife for similar servers | |
""" | |
# This must be called to get the correct Knife Setup for Node() | |
# Even if we don't use the resulting API object directly | |
api = ChefAPI.from_config_file(self.KNIFE_CONFIG) | |
if self.VPC: | |
base_name = "yip_%s%%s" % self.machine_type | |
else: | |
base_name = "yipit_%s%%s" % self.machine_type | |
index = 1 | |
while True: | |
name = base_name % index | |
node = Node(name) | |
if node.exists: | |
index += 1 | |
else: | |
break | |
return name | |
def __init__(self, machine_type, machine_size, security_groups, role=None, subnet=None, mongo_shard=None): | |
super(Machine, self).__init__() | |
self.machine_type = machine_type | |
self.machine_size = machine_size | |
self.security_groups = security_groups | |
# So Knife Doesn't complain when launching into EC2 | |
# We want to keep the original list so we can update groups in VPC | |
if len(self.security_groups) > 1: | |
self.security_groups_as_string = ','.join(self.security_groups).replace("'", "") | |
else: | |
self.security_groups_as_string = str(self.security_groups).strip('[').strip(']') | |
self.role = role | |
self.subnet = subnet | |
# Figure out what environment we will be launching into | |
if self.subnet is not None: | |
self.VPC = True | |
else: | |
self.VPC = False | |
if 'prod' in self.machine_type: | |
self.ENV = 'prod' | |
if self.VPC: | |
self.KNIFE_CONFIG = 'prod/vpc-knife.rb' | |
else: | |
self.KNIFE_CONFIG = 'prod/knife.rb' | |
self.CHEF_ENV = 'production' | |
elif 'staging' in self.machine_type: | |
self.ENV = 'staging' | |
if self.VPC: | |
self.KNIFE_CONFIG = 'staging/vpc-knife.rb' | |
else: | |
self.KNIFE_CONFIG = 'staging/knife.rb' | |
self.CHEF_ENV = 'staging' | |
# Build out our default options | |
self.ami = 'ami-a29943cb' | |
self.ssh_key = "%s_1" % (ENV) | |
self.ssh_key_path = "~/.ssh/%s.pem" % (self.ssh_key) | |
self.user = 'ubuntu' | |
self.ebs_size = 20 | |
self.bootstrap_script = 'ubuntu12.04' | |
self.name = self.get_machine_name() | |
# Default to the machine type as the role | |
if self.role is None: | |
self.role = self.machine_type | |
self.options = { | |
'ami': ('-I', self.ami), | |
'ssh_key': ('-S', self.ssh_key), | |
'ssh_key_path': ('-i', self.ssh_key_path), | |
'user': ('-x', self.user), | |
'ebs_size': ('--ebs-size', self.ebs_size), | |
'bootstrap_script': ('-d', self.bootstrap_script), | |
'name': ('-N', self.name), | |
'knife_key': ('-c', self.KNIFE_CONFIG), | |
'chef_environment': ('-E', self.CHEF_ENV), | |
'machine_size': ('-f', self.machine_size), | |
'security_groups': ('-G', self.security_groups_as_string), | |
'role': ('-r', "role[{}]".format(self.role)), | |
} | |
if self.subnet is not None: | |
self.options.update({'subnet': ('-s', self.subnet)}) | |
def get_knife_command(self): | |
""" | |
Returns the knife command necessary to launch the instance from | |
a subprocess. | |
""" | |
base_command = "knife ec2 server create" | |
opts = "" | |
for k, v in self.options.iteritems(): | |
opts = '{} {} "{}"'.format(opts, v[0], v[1]) | |
return base_command + opts | |
def get_groups_by_name(conn, security_group_names=[]): | |
vpc_security_groups = [] | |
filtered_security_groups = [] | |
# Get All groups and filter ourself since boto doesn't handle | |
# similarly named groups across VPC vs EC2 very well | |
all_groups = conn.get_all_security_groups() | |
# Filter to VPC groups | |
for group in all_groups: | |
if group.vpc_id is not None: | |
vpc_security_groups.append(group) | |
# Filter to Groups we want | |
for group in vpc_security_groups: | |
if group.name.lower() in security_group_names: | |
filtered_security_groups.append(group) | |
return filtered_security_groups | |
def main(machine_type): | |
instance = Machine(machine_type) | |
command = instance.get_knife_command() | |
print "Running %s" % command | |
## Tailing Output | |
ec2_id = None | |
hostname = None | |
public_ip_address = None | |
private_ip_address = None | |
conn = EC2Connection(AWS_ACCESS_KEY, AWS_SECRET_KEY) | |
needs_elastic_ip = True | |
proc = subprocess.Popen(command, shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) | |
while True: | |
line = proc.stdout.readline().rstrip() | |
# Check if we are done with the output | |
if not line: | |
time.sleep(0.1) # Sleep | |
if proc.poll() is None: | |
continue | |
else: | |
break | |
# Check if we need an EIP for this instance | |
# Make sure we dont have one already? | |
# Get a new EIP | |
# Assign to the instance | |
# Add en EIP if we are in a public subnet | |
if instance.VPC and instance.subnet in SUBNETS['public'] and needs_elastic_ip: | |
# We want to know our instance ID before checking | |
# We also want to wait for the private IP address so we know we are in a state | |
# where we can attach an EIP | |
if private_ip_address is not None and ec2_id is not None: | |
# Get our instance's public IP, if it exists | |
# Since we filter by instance ID, we should only have 1 result | |
i = conn.get_all_instances(instance_ids=[ec2_id])[0].instances[0] | |
if i.ip_address is None: | |
allocation = conn.allocate_address(domain='vpc') | |
conn.associate_address(instance_id=ec2_id, allocation_id=allocation.allocation_id) | |
needs_elastic_ip = False | |
# Process the output | |
if "Instance ID" in line: | |
if ec2_id is None: | |
ec2_id = line.split()[-1] | |
if "Public DNS Name" in line: | |
if hostname is None: | |
hostname = line.split()[-1] | |
if "Public IP Address" in line: | |
if public_ip_address is None: | |
public_ip_address = line.split()[-1] | |
if "Private IP Address" in line: | |
if private_ip_address is None: | |
private_ip_address = line.split()[-1] | |
print line | |
created_tags = conn.create_tags([ec2_id], {"Name": instance.name}) | |
print "Rebooting new machine..." | |
# #if proc.returncode == 0: # Knife Return Code is always 0 | |
conn.reboot_instances([ec2_id]) | |
# Update Run List (useful in case a chef-client run doesn't succeed) | |
# Re-adding the same element is idempotent | |
print "Updating Run List..." | |
command = "knife node run_list add {0} 'role[{1}]' -c {2} -E {3}".format(instance.name, instance.role, instance.KNIFE_CONFIG, instance.CHEF_ENV) | |
subprocess.call(command, shell=True) | |
if instance.VPC: | |
# Weird bug where we end with the "recipe[roles] when we manually add the instance role" | |
command = "knife node run_list remove {0} recipe[roles] -c {1} -E {2}".format(instance.name, instance.KNIFE_CONFIG, instance.CHEF_ENV) | |
subprocess.call(command, shell=True) | |
# # Update Security Groups as Knife/Fog fails to do this in VPC | |
if instance.VPC: | |
print "Updating Security Groups..." | |
# Check to see if we already have the group attached to our instance | |
group_ids = [group.id for group in get_groups_by_name(conn, instance.security_groups)] | |
conn.modify_instance_attribute(instance_id=ec2_id, attribute='GroupSet', value=group_ids) | |
print "Security Groups Set to {}".format(instance.security_groups) | |
print "Add to yipit/config/ssh_config:" | |
if instance.VPC: | |
print VPC_SSH_CONFIG_BLOCK % {'hostname': private_ip_address, 'host': instance.name, 'environment': instance.ENV} | |
else: | |
print SSH_CONFIG_BLOCK % {'hostname': hostname, 'host': instance.name} | |
if __name__ == '__main__': | |
machine_type = sys.argv[1] | |
if 'prod' in machine_type: | |
ENV = 'prod' | |
AWS_ACCESS_KEY = "foo" | |
AWS_SECRET_KEY = "bar" | |
KNIFE_CONFIG = 'prod/knife.rb' | |
else: | |
ENV = 'staging' | |
AWS_ACCESS_KEY = 'foo' | |
AWS_SECRET_KEY = 'bar' | |
KNIFE_CONFIG = 'staging/knife.rb' | |
main(machine_type) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment