├── .gitignore ├── .travis.yml ├── README.md ├── fabric_aws └── __init__.py ├── setup.py └── tests ├── test_boto_integration.py └── test_decorators.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | 5 | install: pip install -e .[test] 6 | script: nosetests --with-coverage --cover-package=fabric_aws --cover-erase 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/EverythingMe/fabric-aws.svg?branch=master)](https://travis-ci.org/EverythingMe/fabric-aws) 2 | 3 | # Fabric AWS integration 4 | 5 | Ever used pychef's fabric integration and loved it? 6 | This project aims to integrate AWS with fabric. You can decorate your fabric tasks to run on an Autoscaling group (which can also be a part of a Cloudformation stack), or on various EC2 instance attribues (tags, instance-type, instance id, etc.) 7 | 8 | ## Installation 9 | * `pip install fabric-aws` 10 | * [Configure boto](http://boto.readthedocs.org/en/latest/boto_config_tut.html) 11 | 12 | ## Example fabfile 13 | 14 | ```python 15 | from fabric.api import * 16 | from fabric_aws import * 17 | 18 | 19 | @autoscaling_group('us-east-1', 'my-autoscaling-group') 20 | @task 21 | def uptime_asg(): 22 | run('uptime') 23 | 24 | @cloudformation_autoscaling_group('us-east-1', 'my-cloudformation', 25 | 'AutoScalingGroup') 26 | @task 27 | def uptime_cfn(): 28 | run('uptime') 29 | 30 | @ec2('us-east-1', instance_ids=['i-01010101', 'i-10101010']) 31 | @task 32 | def uptime_ec2_instance_ids(): 33 | run('uptime') 34 | 35 | @ec2('us-east-1', filters={'tag:Name': 'jenkins'}) 36 | @task 37 | def uptime_jenkins(): 38 | run('uptime') 39 | 40 | # see the documentation for boto.ec2.get_all_instances() 41 | # http://boto.readthedocs.org/en/latest/ref/ec2.html#boto.ec2.connection.EC2Connection.get_all_instances 42 | @ec2('us-east-1', filters={'tag:Name': 'my-vpc-instance'}, 43 | hostname_attribute='private_ip_address') 44 | @task 45 | def uptime_vpc(): 46 | run('uptime') 47 | ``` 48 | -------------------------------------------------------------------------------- /fabric_aws/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 DoAT. All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY DoAT ``AS IS'' AND ANY EXPRESS OR IMPLIED 15 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 17 | # EVENT SHALL DoAT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 19 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 20 | # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 21 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | # 25 | # The views and conclusions contained in the software and documentation are 26 | # those of the authors and should not be interpreted as representing official 27 | # policies, either expressed or implied, of DoAT 28 | 29 | from __future__ import absolute_import 30 | 31 | # noinspection PyProtectedMember 32 | from fabric.decorators import wraps, _wrap_as_new 33 | import boto.cloudformation 34 | import boto.ec2 35 | import boto.ec2.autoscale 36 | 37 | 38 | def _list_annotating_decorator(attribute, *values): 39 | # based on fabric.decorators._list_annotating_decorator 40 | # it is exactcly the same as the original, except the fact that it generates the list lazily 41 | def attach_list(func): 42 | @wraps(func) 43 | def inner_decorator(*args, **kwargs): 44 | return func(*args, **kwargs) # pragma: no cover 45 | 46 | _values = values 47 | # Allow for single iterable argument as well as *args 48 | if len(_values) == 1 and not isinstance(_values[0], basestring): 49 | _values = _values[0] 50 | setattr(inner_decorator, attribute, _values) 51 | # Don't replace @task new-style task objects with inner_decorator by 52 | # itself -- wrap in a new Task object first. 53 | inner_decorator = _wrap_as_new(func, inner_decorator) 54 | return inner_decorator 55 | 56 | return attach_list 57 | 58 | 59 | def cloudformation_logical_to_physical(region, cfn_stack_name, logical_resource_id): 60 | """ 61 | Convert cloudformation logical resource name to physical resource name 62 | 63 | :param region: AWS region 64 | :type region: str 65 | :param cfn_stack_name: Cloudformation stack name 66 | :type cfn_stack_name: str 67 | :param logical_resource_id: Cloudformation logical resource id 68 | :type logical_resource_id: str 69 | :return: Physical resource id 70 | :rtype: str 71 | """ 72 | 73 | cfn = boto.cloudformation.connect_to_region(region) 74 | resource = cfn.describe_stack_resource(cfn_stack_name, logical_resource_id) 75 | 76 | return resource.get('DescribeStackResourceResponse', {}). \ 77 | get('DescribeStackResourceResult', {}). \ 78 | get('StackResourceDetail', {}). \ 79 | get('PhysicalResourceId') 80 | 81 | 82 | def autoscaling_group_instance_ids(region, autoscaling_group_name): 83 | """ 84 | Return a list of instance ids for all instances inside autoscaling group `autoscaling_group_name` 85 | 86 | :param region: AWS region 87 | :type region: str 88 | :param autoscaling_group_name: Autoscaling group name 89 | :type autoscaling_group_name: str 90 | :return: list of instance ids inside `autoscaling_group_name` 91 | :rtype: list[str] 92 | """ 93 | 94 | autoscale_connection = boto.ec2.autoscale.connect_to_region(region) 95 | asg = autoscale_connection.get_all_groups(names=[autoscaling_group_name])[0] 96 | 97 | return [instance.instance_id for instance in asg.instances] 98 | 99 | 100 | def cloudformation_autoscaling_group_generator(region, cfn_stack_name, asg_resource_name, 101 | hostname_attribute='public_dns_name'): 102 | """ 103 | Hosts generator for running a task on all instances inside an autoscaling group that is a part of a CFN stack 104 | Please decorate your functions with `cloudformation_autoscaling_group` 105 | 106 | :param region: AWS region 107 | :type region: str 108 | :param cfn_stack_name: Cloudformation stack name 109 | :type cfn_stack_name: str 110 | :param asg_resource_name: Autoscaling group logical resource name inside `cfn_stack_name` 111 | :type asg_resource_name: str 112 | :param hostname_attribute: `boto.ec2.instance.Instance` attribute to use as hostname for fabric connection 113 | :type hostname_attribute: str 114 | :return: Generates a list of hosts 115 | :rtype: list[str] 116 | """ 117 | 118 | physical_resource_id = cloudformation_logical_to_physical(region, cfn_stack_name, asg_resource_name) 119 | 120 | hosts = autoscaling_group_generator(region, physical_resource_id, hostname_attribute) 121 | 122 | # this will make our function lazy 123 | for host in hosts: 124 | yield host 125 | 126 | 127 | def autoscaling_group_generator(region, autoscaling_group_name, hostname_attribute='public_dns_name'): 128 | """ 129 | Hosts generator for running a task on all instances inside an autoscaling group 130 | Please decorate your functions with `autoscaling_group` 131 | 132 | :param region: AWS region 133 | :type region: str 134 | :param autoscaling_group_name: Autoscaling group logical resource name inside `cfn_stack_name` 135 | :type autoscaling_group_name: str 136 | :param hostname_attribute: `boto.ec2.instance.Instance` attribute to use as hostname for fabric connection 137 | :type hostname_attribute: str 138 | :return: Generates a list of hosts 139 | :rtype: list[str] 140 | """ 141 | 142 | instance_ids = autoscaling_group_instance_ids(region, autoscaling_group_name) 143 | 144 | hosts = ec2_generator(region, 145 | hostname_attribute=hostname_attribute, 146 | instance_ids=instance_ids) 147 | 148 | # this will make our function lazy 149 | for host in hosts: 150 | yield host 151 | 152 | 153 | def ec2_generator(region, *args, **kwargs): 154 | """ 155 | Hosts generator for running a task on all instances matching `*args` and `**kwargs` 156 | `*args` and `**kwargs` are passed-through to the underlying boto.ec2.get_all_instances() function 157 | 158 | :param region: AWS region 159 | :type region: str 160 | :param hostname_attribute: `boto.ec2.instance.Instance` attribute to use as hostname for fabric connection 161 | :type hostname_attribute: str 162 | :param *args: pass-through arguments to the underlying boto.ec2.get_all_instances() function 163 | :param *kwargs: pass-through keyword arguments to the underlying boto.ec2.get_all_instances() function 164 | :return: Generates a list of hosts 165 | :rtype: list[str] 166 | """ 167 | 168 | hostname_attribute = kwargs.pop('hostname_attribute', 'public_dns_name') 169 | ec2_connection = boto.ec2.connect_to_region(region) 170 | 171 | reservations = ec2_connection.get_all_instances(*args, **kwargs) 172 | 173 | hosts = [getattr(instance, hostname_attribute) 174 | for reservation in reservations 175 | for instance in reservation.instances] 176 | 177 | # this will make our function lazy 178 | for host in hosts: 179 | yield host 180 | 181 | 182 | @wraps(ec2_generator) 183 | def ec2(*args, **kwargs): 184 | """ 185 | Fabric decorator for running a task on all instances returned by `boto.ec2.get_all_instances()` when called with 186 | *args, **kwargs. 187 | 188 | :param region: AWS region 189 | :type region: str 190 | :param hostname_attribute: `boto.ec2.instance.Instance` attribute to use as hostname for fabric connection 191 | """ 192 | 193 | return _list_annotating_decorator('hosts', ec2_generator(*args, **kwargs)) 194 | 195 | @wraps(autoscaling_group_generator) 196 | def autoscaling_group(*args, **kwargs): 197 | """ 198 | Fabric decorator for running a task on all instances inside an autoscaling group 199 | 200 | :param region: AWS region 201 | :type region: str 202 | :param autoscaling_group_name: Autoscaling group logical resource name inside `cfn_stack_name` 203 | :type autoscaling_group_name: str 204 | :param hostname_attribute: `boto.ec2.instance.Instance` attribute to use as hostname for fabric connection 205 | :type hostname_attribute: str 206 | """ 207 | 208 | return _list_annotating_decorator('hosts', autoscaling_group_generator(*args, **kwargs)) 209 | 210 | 211 | @wraps(cloudformation_autoscaling_group_generator) 212 | def cloudformation_autoscaling_group(*args, **kwargs): 213 | """ 214 | Decorator for running a task on all instances inside an autoscaling group that is a part of a CFN stack 215 | 216 | :param region: AWS region 217 | :type region: str 218 | :param cfn_stack_name: Cloudformation stack name 219 | :type cfn_stack_name: str 220 | :param asg_resource_name: Autoscaling group logical resource name inside `cfn_stack_name` 221 | :type asg_group_name: str 222 | :param hostname_attribute: `boto.ec2.instance.Instance` attribute to use as hostname for fabric connection 223 | :type hostname_attribute: str 224 | """ 225 | 226 | return _list_annotating_decorator('hosts', cloudformation_autoscaling_group_generator(*args, **kwargs)) 227 | 228 | 229 | __all__ = ['cloudformation_autoscaling_group', 'autoscaling_group', 'ec2'] 230 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 DoAT. All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY DoAT ``AS IS'' AND ANY EXPRESS OR IMPLIED 15 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 17 | # EVENT SHALL DoAT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 19 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 20 | # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 21 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | # 25 | # The views and conclusions contained in the software and documentation are 26 | # those of the authors and should not be interpreted as representing official 27 | # policies, either expressed or implied, of DoAT 28 | 29 | from setuptools import setup, find_packages 30 | 31 | setup( 32 | name='fabric-aws', 33 | version='0.3', 34 | description='Fabric integration with AWS Autoscaling Groups (+Cloudformation)', 35 | author='EverythingMe', 36 | author_email='omrib@everything.me', 37 | url='http://github.com/EverythingMe/fabric-aws', 38 | packages=find_packages(), 39 | install_requires=['boto', 'fabric'], 40 | 41 | extras_require={ 42 | 'test': ['nose', 'coverage', 'mock'] 43 | }, 44 | 45 | classifiers=[ 46 | 'Development Status :: 5 - Production/Stable', 47 | 'Environment :: Console', 48 | 'Intended Audience :: Developers', 49 | 'Intended Audience :: System Administrators', 50 | 'Operating System :: OS Independent', 51 | 'Programming Language :: Python :: 2.7', 52 | 'Topic :: System :: Clustering', 53 | 'Topic :: System :: Systems Administration', 54 | 'Topic :: Utilities' 55 | ] 56 | ) 57 | 58 | -------------------------------------------------------------------------------- /tests/test_boto_integration.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 DoAT. All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY DoAT ``AS IS'' AND ANY EXPRESS OR IMPLIED 15 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 17 | # EVENT SHALL DoAT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 19 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 20 | # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 21 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | # 25 | # The views and conclusions contained in the software and documentation are 26 | # those of the authors and should not be interpreted as representing official 27 | # policies, either expressed or implied, of DoAT 28 | 29 | from fabric_aws import autoscaling_group_instance_ids, cloudformation_logical_to_physical, ec2_generator 30 | import unittest 31 | import mock 32 | 33 | def mock_environment(): 34 | mock_autoscale_connection = mock.MagicMock(**{ 35 | 'get_all_groups.return_value': [mock.MagicMock(instances=[mock.Mock(instance_id='i-00000001'), 36 | mock.Mock(instance_id='i-00000002'), 37 | mock.Mock(instance_id='i-00000003'), 38 | mock.Mock(instance_id='i-00000004')])] 39 | }) 40 | mock_autoscale = mock.MagicMock(**{'connect_to_region.return_value': mock_autoscale_connection}) 41 | 42 | mock_ec2_connection = mock.MagicMock(**{ 43 | 'get_all_instances.return_value': [ 44 | mock.MagicMock(instances=[ 45 | mock.Mock(instance_id='i-00000001', public_dns_name='a.a.a', private_ip_address='10.0.0.1'), 46 | mock.Mock(instance_id='i-00000002', public_dns_name='b.b.b', private_ip_address='10.0.0.2')]), 47 | mock.MagicMock(instances=[ 48 | mock.Mock(instance_id='i-00000003', public_dns_name='c.c.c', private_ip_address='10.0.0.3'), 49 | mock.Mock(instance_id='i-00000004', public_dns_name='d.d.d', private_ip_address='10.0.0.4')]) 50 | ] 51 | }) 52 | mock_ec2 = mock.MagicMock(**{'connect_to_region.return_value': mock_ec2_connection, 'autoscale': mock_autoscale}) 53 | 54 | mock_cloudformation_connection = mock.MagicMock(**{ 55 | 'describe_stack_resource.return_value': { 56 | 'DescribeStackResourceResponse': { 57 | 'DescribeStackResourceResult': { 58 | 'StackResourceDetail': { 59 | 'PhysicalResourceId': 'my-awesome-physical-resource' 60 | } 61 | } 62 | } 63 | } 64 | }) 65 | mock_cloudformation = mock.MagicMock(**{'connect_to_region.return_value': mock_cloudformation_connection}) 66 | 67 | return mock_cloudformation, mock_ec2 68 | 69 | 70 | class TestBotoIntegration(unittest.TestCase): 71 | def test_autoscaling_group_instances(self): 72 | mock_cloudformation, mock_ec2 = mock_environment() 73 | 74 | with mock.patch('boto.ec2', mock_ec2), mock.patch('boto.cloudformation', mock_cloudformation): 75 | instance_ids = autoscaling_group_instance_ids('us-east-1', 'dummy-asg-name') 76 | 77 | mock_ec2.autoscale.connect_to_region.assert_called_once_with('us-east-1') 78 | 79 | mock_autoscale_connection = mock_ec2.autoscale.connect_to_region.return_value 80 | mock_autoscale_connection.get_all_groups.assert_called_once_with(names=['dummy-asg-name']) 81 | 82 | self.assertListEqual( 83 | ['i-00000001', 'i-00000002', 'i-00000003', 'i-00000004'], 84 | instance_ids 85 | ) 86 | 87 | def test_cloudformation_logical_to_physical(self): 88 | mock_cloudformation, mock_ec2 = mock_environment() 89 | 90 | with mock.patch('boto.ec2', mock_ec2), mock.patch('boto.cloudformation', mock_cloudformation): 91 | physical = cloudformation_logical_to_physical('us-east-1', 'stack-name', 'resource-name') 92 | 93 | mock_cloudformation.connect_to_region.assert_called_once_with('us-east-1') 94 | 95 | mock_cfn_connection = mock_cloudformation.connect_to_region.return_value 96 | mock_cfn_connection.describe_stack_resource.assert_called_once_with('stack-name', 'resource-name') 97 | 98 | self.assertEqual( 99 | 'my-awesome-physical-resource', 100 | physical 101 | ) 102 | 103 | def test_ec2_generator(self): 104 | mock_cloudformation, mock_ec2 = mock_environment() 105 | with mock.patch('boto.ec2', mock_ec2), mock.patch('boto.cloudformation', mock_cloudformation): 106 | instance_hosts = list(ec2_generator('us-east-1', 107 | instance_ids=['i-00000001', 'i-00000002', 'i-00000003', 'i-00000004'])) 108 | 109 | mock_ec2.connect_to_region.assert_called_once_with('us-east-1') 110 | 111 | mock_ec2_connection = mock_ec2.connect_to_region.return_value 112 | mock_ec2_connection.get_all_instances.assert_called_once_with( 113 | instance_ids=['i-00000001', 'i-00000002', 'i-00000003', 'i-00000004']) 114 | 115 | self.assertListEqual( 116 | ['a.a.a', 'b.b.b', 'c.c.c', 'd.d.d'], 117 | instance_hosts 118 | ) 119 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 DoAT. All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 7 | # 1. Redistributions of source code must retain the above copyright notice, 8 | # this list of conditions and the following disclaimer. 9 | # 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | # 14 | # THIS SOFTWARE IS PROVIDED BY DoAT ``AS IS'' AND ANY EXPRESS OR IMPLIED 15 | # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 17 | # EVENT SHALL DoAT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 18 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 19 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 20 | # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 21 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 23 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | # 25 | # The views and conclusions contained in the software and documentation are 26 | # those of the authors and should not be interpreted as representing official 27 | # policies, either expressed or implied, of DoAT 28 | 29 | from fabric_aws import * 30 | from fabric.api import task 31 | import unittest 32 | import mock 33 | from test_boto_integration import mock_environment 34 | 35 | 36 | class TestDecorators(unittest.TestCase): 37 | def test_laziness(self): 38 | mock_cloudformation, mock_ec2 = mock_environment() 39 | 40 | with mock.patch('boto.ec2', mock_ec2), mock.patch('boto.cloudformation', mock_cloudformation): 41 | # noinspection PyUnusedLocal 42 | @cloudformation_autoscaling_group('region', 'stack-name', 43 | 'logical-autoscaling-group-name', 'private_ip_address') 44 | @task 45 | def dummy1(): 46 | pass 47 | 48 | # noinspection PyUnusedLocal 49 | @autoscaling_group('region', 'autoscaling-group-name') 50 | @task 51 | def dummy2(): 52 | pass 53 | 54 | # noinspection PyUnusedLocal 55 | @ec2('region', instance_ids=['i-00000001', 'i-00000002', 'i-00000003', 'i-00000004']) 56 | @task 57 | def dummy3(): 58 | pass 59 | 60 | self.assertFalse(mock_ec2.connect_to_region.called) 61 | self.assertFalse(mock_ec2.autoscale.return_value.connect_to_region.called) 62 | self.assertFalse(mock_cloudformation.connect_to_region.called) 63 | 64 | def test_cloudformation_autoscaling_group(self): 65 | mock_cloudformation, mock_ec2 = mock_environment() 66 | 67 | with mock.patch('boto.ec2', mock_ec2), mock.patch('boto.cloudformation', mock_cloudformation): 68 | @cloudformation_autoscaling_group('region', 'stack-name', 69 | 'logical-autoscaling-group-name', 'private_ip_address') 70 | @task 71 | def dummy(): 72 | pass 73 | 74 | hosts = list(dummy.hosts) 75 | 76 | self.assertListEqual( 77 | ['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4'], 78 | hosts 79 | ) 80 | 81 | def test_autoscaling_group(self): 82 | mock_cloudformation, mock_ec2 = mock_environment() 83 | 84 | with mock.patch('boto.ec2', mock_ec2), mock.patch('boto.cloudformation', mock_cloudformation): 85 | @autoscaling_group('region', 'autoscaling-group-name') 86 | @task 87 | def dummy(): 88 | pass 89 | 90 | hosts = list(dummy.hosts) 91 | 92 | self.assertListEqual( 93 | ['a.a.a', 'b.b.b', 'c.c.c', 'd.d.d'], 94 | hosts 95 | ) 96 | 97 | def test_ec2(self): 98 | mock_cloudformation, mock_ec2 = mock_environment() 99 | 100 | with mock.patch('boto.ec2', mock_ec2), mock.patch('boto.cloudformation', mock_cloudformation): 101 | @ec2('region', instance_ids=['i-00000001', 'i-00000002', 'i-00000003', 'i-00000004']) 102 | @task 103 | def dummy(): 104 | pass 105 | 106 | hosts = list(dummy.hosts) 107 | 108 | self.assertListEqual( 109 | ['a.a.a', 'b.b.b', 'c.c.c', 'd.d.d'], 110 | hosts 111 | ) 112 | --------------------------------------------------------------------------------