from os.path import exists, join, expanduser, abspath
from pprint import pformat, pprint
from boto.ec2 import connect_to_region
from fabric.api import local, env
from ..conf import awsfab_settings
[docs]def ec2_rsync(local_dir, remote_dir, rsync_args='-av', sync_content=False):
"""
rsync ``local_dir`` into ``remote_dir`` on the current EC2 instance (the
one returned by :meth:`Ec2InstanceWrapper.get_from_host_string`).
:param sync_content: Normally the function automatically makes sure
``local_dir`` is not suffixed with ``/``, which makes rsync copy
``local_dir`` into ``remote_dir``. With ``sync_content=True``,
the content of ``local_dir`` is synced into ``remote_dir`` instead.
"""
instance = Ec2InstanceWrapper.get_from_host_string()
ssh_uri = instance.get_ssh_uri()
key_filename = instance.get_ssh_key_filename()
extra_ssh_args = awsfab_settings.EXTRA_SSH_ARGS
if sync_content:
if not local_dir.endswith('/'):
local_dir = local_dir + '/'
else:
if local_dir.endswith('/'):
local_dir = local_dir.rstrip('/')
rsync_cmd = 'rsync {rsync_args} -e "ssh -i {key_filename} {extra_ssh_args}" {local_dir} {ssh_uri}:{remote_dir}'.format(**vars())
local(rsync_cmd)
def _parse_instanceident(instanceid_with_optional_region):
if ':' in instanceid_with_optional_region:
region, instanceid = instanceid_with_optional_region.split(':', 1)
else:
instanceid = instanceid_with_optional_region
region = awsfab_settings.DEFAULT_REGION
return region, instanceid
[docs]def parse_instanceid(instanceid_with_optional_region):
"""
Parse instance id with an optional region-name prefixed. Region name
is specified by prefixing the instanceid with ``<regionname>:``.
:return: (region, instanceid) where region defaults to
``awsfab_settings.DEFAULT_REGION`` if not prefixed to the id.
"""
return _parse_instanceident(instanceid_with_optional_region)
[docs]def parse_instancename(instancename_with_optional_region):
"""
Just like :func:`parse_instanceid`, however this is for instance names.
We keep them as separate functions in case they diverge in the future.
:return: (region, instanceid) where region defaults to
``awsfab_settings.DEFAULT_REGION`` if not prefixed to the name.
"""
return _parse_instanceident(instancename_with_optional_region)
[docs]class Ec2RegionConnectionError(Exception):
"""
Raised when we fail to connect to a region.
"""
def __init__(self, region):
self.region = region
msg = 'Could not connect to region: {region}'.format(**vars())
super(Ec2RegionConnectionError, self).__init__(msg)
[docs]class Ec2InstanceWrapper(object):
"""
Wraps a :class:`boto.ec2.instance.Instance` with convenience functions.
:ivar instance: The :class:`boto.ec2.instance.Instance`.
"""
def __init__(self, instance):
"""
:param instance: A :class:`boto.ec2.instance.Instance` object.
"""
self.instance = instance
def __getitem__(self, key):
"""
Provides easy access to attributes in ``self.instance``.
"""
return getattr(self.instance, key)
def __str__(self):
return 'Ec2InstanceWrapper:{0}'.format(self)
[docs] def prettyname(self):
"""
Return a pretty-formatted name for this instance, using the Name-tag if
the instance is tagged with it.
"""
instanceid = self.instance.id
name = self.instance.tags.get('Name')
if name:
return '{instanceid} (name={name})'.format(**vars())
else:
return instanceid
[docs] def get_ssh_uri(self):
"""
Get the SSH URI for the instance.
:return: "<instance.tags['awsfab-ssh-user']>@<instance.public_dns_name>"
"""
user = self['tags'].get('awsfab-ssh-user', awsfab_settings.EC2_INSTANCE_DEFAULT_SSHUSER)
host = self['public_dns_name']
return '{user}@{host}'.format(**vars())
[docs] def get_ssh_key_filename(self):
"""
Get the SSH indentify filename (.pem-file) for the instance. Searches
``awsfab_settings.KEYPAIR_PATH`` for ``"<instance.key_name>.pem"``.
:raise LookupError: If the key is not found.
"""
path = awsfab_settings.KEYPAIR_PATH
key_name = self.instance.key_name + '.pem'
for dirpath in path:
filename = abspath(join(expanduser(dirpath), key_name))
if exists(filename):
return filename
raise LookupError('Could not find {key_name} in awsfab_settings.KEYPAIR_PATH: {path!r}'.format(**vars()))
[docs] def add_instance_to_env(self):
"""
Add ``self`` to ``fabric.api.env.ec2instances[self.get_ssh_uri()]``,
and register the key-pair for the instance in
``fabric.api.env.key_filename``.
"""
if not 'ec2instances' in env:
env['ec2instances'] = {}
env['ec2instances'][self.get_ssh_uri()] = self
if not env.key_filename:
env.key_filename = []
key_filename = self.get_ssh_key_filename()
if not key_filename in env.key_filename:
env.key_filename.append(key_filename)
@classmethod
@classmethod
[docs] def get_by_tagvalue(cls, tags={}, region=None):
"""
Connect to AWS and get the EC2 instance with the given tag:value pairs.
:param tags
A string like 'role=testing,fake=yes' to AND a set of ec2
instance tags
:param region:
optional.
:raise Ec2RegionConnectionError: If connecting to the region fails.
:raise LookupError: If no matching instance was found in the region.
:return: A list of :class:`Ec2InstanceWrapper`s containing the
matching instances.
"""
region = region is None and awsfab_settings.DEFAULT_REGION or region
connection = connect_to_region(region_name=region, **awsfab_settings.AUTH)
if not connection:
raise Ec2RegionConnectionError(region)
tags = dict((('tag:%s' % oldk, v) for (oldk, v) in tags.iteritems()))
reservations = connection.get_all_instances(filters=tags)
if len(reservations) == 0:
raise LookupError('No ec2 instances with tags{0}'.format(tags))
insts = []
for r in reservations:
for instance in r.instances:
insts.append(cls(instance))
return insts
@classmethod
[docs] def get_by_instanceid(cls, instanceid):
"""
Connect to AWS and get the EC2 instance with the given instance ID.
:param instanceid_with_optional_region:
Parsed with :func:`parse_instanceid` to find the region and name.
:raise Ec2RegionConnectionError: If connecting to the region fails.
:raise LookupError: If the requested instance was not found in the region.
:return: A :class:`Ec2InstanceWrapper` contaning the requested instance.
"""
region, instanceid = parse_instanceid(instanceid)
connection = connect_to_region(region_name=region, **awsfab_settings.AUTH)
if not connection:
raise Ec2RegionConnectionError(region)
reservations = connection.get_all_instances([instanceid])
if len(reservations) == 0:
raise LookupError('No ec2 instances with instanceid={0}'.format(instanceid))
reservation = reservations[0]
if len(reservation.instances) != 1:
raise LookupError('Did not get exactly one instance with instanceid={0}'.format(instanceid))
return cls(reservation.instances[0])
@classmethod
[docs] def get_from_host_string(cls):
"""
If an instance has been registered in ``fabric.api.env`` using
:meth:`add_instance_to_env`, this method can be used to get
the instance identified by ``fabric.api.env.host_string``.
"""
return env.ec2instances[env.host_string]
[docs]class WaitForStateError(Exception):
"""
Raises when :func:`wait_for_state` times out.
"""
[docs]def wait_for_state(instanceid, state_name, sleep_intervals=[15, 5], last_sleep_repeat=40):
"""
Poll the instance with ``instanceid`` until its ``state_name`` matches the
desired ``state_name``.
The first poll is performed without any delay, and the rest of the polls are
performed according to ``sleep_intervals``.
:param instanceid: ID of an instance.
:param state_name: The state_name to wait for.
:param sleep_intervals: List of seconds to wait between each poll for state. The first poll
is made immediately, then we wait for sleep_intervals[0] seconds before the next poll,
and repeat for each item in sleep_intervals. Then we repeat for ``last_sleep_repeat``
using the last item in ``sleep_intervals`` as the timout for each wait.
:param last_sleep_repeat:
Number of times to repeat the last item in ``sleep_intervals``. If this
is 20, we will wait for a maximum of ``sum(sleep_intervals) + sleep_intervals[-1]*20``.
"""
from time import sleep
region, instanceid = parse_instanceid(instanceid)
sleep_intervals.extend([sleep_intervals[-1] for x in xrange(last_sleep_repeat)])
max_wait_sec = sum(sleep_intervals)
print 'Waiting for {instanceid} to change state to: "{state_name}". Will try for {max_wait_sec}s.'.format(**vars())
sleep_intervals_len = len(sleep_intervals)
for index, sleep_sec in enumerate(sleep_intervals):
instancewrapper = Ec2InstanceWrapper.get_by_instanceid(instanceid)
current_state_name = instancewrapper['state']
if current_state_name == state_name:
print '.. OK'
return
index_n1 = index + 1
print '.. Current state: "{current_state_name}". Next poll ({index_n1}/{sleep_intervals_len}) for "{state_name}"-state in {sleep_sec}s.'.format(**vars())
sleep(sleep_sec)
raise WaitForStateError('Desired state, "{state_name}", not achieved in {max_wait_sec}s.'.format(**vars()))
[docs]def wait_for_stopped_state(instanceid, **kwargs):
"""
Shortcut for ``wait_for_state(instanceid, 'stopped', **kwargs)``.
"""
wait_for_state(instanceid, 'stopped', **kwargs)
[docs]def wait_for_running_state(instanceid, **kwargs):
"""
Shortcut for ``wait_for_state(instanceid, 'running', **kwargs)``.
"""
wait_for_state(instanceid, 'running', **kwargs)
[docs]def print_ec2_instance(instance, full=False, indentspaces=3):
"""
Print attributes of an ec2 instance.
:param instance: A :class:`boto.ec2.instance.Instance` object.
:param full: Print all attributes? If not, a subset of the attributes are printed.
:param indentspaces: Number of spaces to indent each line in the output.
"""
indent = ' ' * indentspaces
if full:
attrnames = sorted(instance.__dict__.keys())
else:
attrnames = ['state', 'instance_type', 'ip_address', 'public_dns_name',
'private_dns_name', 'private_ip_address',
'key_name', 'tags', 'placement']
for attrname in attrnames:
if attrname.startswith('_'):
continue
value = instance.__dict__[attrname]
if not isinstance(value, (str, unicode, bool, int)):
value = pformat(value)
print '{indent}{attrname}: {value}'.format(**vars())
hostsfile_template = """
127.0.0.1 localhost
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts
{custom_hosts}
"""
def _nametags_to_hostslist(nametags=[], suffix='.ec2private'):
hostslist = []
for nametag in nametags:
instancewrapper = Ec2InstanceWrapper.get_by_nametag(nametag)
private_ip_address = instancewrapper.instance.private_ip_address
hostslist.append('{private_ip_address} {nametag}{suffix}'.format(**vars()))
return hostslist