├── tests ├── __init__.py ├── credentials.csv ├── test_connection.py ├── base.py ├── test_types.py └── test_helpers.py ├── MANIFEST.in ├── dev_requirements.txt ├── setup.cfg ├── .travis.yml ├── Makefile ├── .gitignore ├── example.py ├── ec2 ├── __init__.py ├── types.py ├── connection.py ├── base.py └── helpers.py ├── LICENSE ├── setup.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py README.md LICENSE MANIFEST.in 2 | -------------------------------------------------------------------------------- /tests/credentials.csv: -------------------------------------------------------------------------------- 1 | "Access Key Id","Secret Access Key" 2 | foo,bar 3 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | boto 2 | mock 3 | pytest 4 | pytest-cov 5 | flake8 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [pytest] 5 | addopts = -v 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "pypy" 6 | install: 7 | - make bootstrap 8 | script: 9 | - make lint 10 | - py.test --cov ec2 --cov-report term-missing 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | bootstrap: 2 | pip install -r dev_requirements.txt 3 | pip install -e . 4 | 5 | publish: 6 | python setup.py sdist bdist_wheel upload 7 | 8 | clean: 9 | rm -rf build dist *.egg-info 10 | 11 | test: lint 12 | py.test 13 | 14 | lint: 15 | flake8 ec2 16 | 17 | .PHONY: bootstrap publish clean test lint 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | 29 | # virtualenv 30 | env 31 | 32 | # vim 33 | *.swp 34 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import ec2 2 | 3 | ec2.credentials.ACCESS_KEY_ID = 'xxx' 4 | ec2.credentials.SECRET_ACCESS_KEY = 'xxx' 5 | ec2.credentials.REGION_NAME = 'us-west-2' 6 | # ec2.credentials.from_file('credentials.csv') 7 | 8 | print ec2.instances.all() 9 | for i in ec2.instances.filter(state__iexact='rUnning', name__endswith='01', name__startswith='production'): 10 | print i.tags['Name'] 11 | print ec2.instances.filter(id__iregex=r'^I\-') 12 | 13 | print ec2.security_groups.all() 14 | for g in ec2.security_groups.filter(name__istartswith='production'): 15 | print g.description 16 | -------------------------------------------------------------------------------- /ec2/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ec2 3 | ~~~ 4 | 5 | :copyright: (c) 2014 by Matt Robenolt. 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | try: 10 | __version__ = __import__('pkg_resources') \ 11 | .get_distribution('ec2').version 12 | except Exception: # pragma: no cover 13 | __version__ = 'unknown' 14 | 15 | __author__ = 'Matt Robenolt ' 16 | __license__ = 'BSD' 17 | __all__ = ('credentials', 'instances', 'security_groups', 'vpcs') 18 | 19 | from .connection import credentials # noqa 20 | from .types import instances, security_groups, vpcs # noqa 21 | -------------------------------------------------------------------------------- /ec2/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | ec2.types 3 | ~~~~~~~~~ 4 | 5 | :copyright: (c) 2014 by Matt Robenolt. 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | from ec2.connection import get_connection, get_vpc_connection 10 | from ec2.base import objects_base 11 | 12 | 13 | class instances(objects_base): 14 | "Singleton to stem off queries for instances" 15 | 16 | @classmethod 17 | def _all(cls): 18 | "Grab all AWS instances" 19 | return [ 20 | i for r in get_connection().get_all_instances() 21 | for i in r.instances 22 | ] 23 | 24 | 25 | class security_groups(objects_base): 26 | "Singleton to stem off queries for security groups" 27 | 28 | @classmethod 29 | def _all(cls): 30 | "Grab all AWS Security Groups" 31 | return get_connection().get_all_security_groups() 32 | 33 | 34 | class vpcs(objects_base): 35 | "Singleton to stem off queries for virtual private clouds" 36 | 37 | @classmethod 38 | def _all(cls): 39 | "Grab all AWS Virtual Private Clouds" 40 | return get_vpc_connection().get_all_vpcs() 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Matt Robenolt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTestCase 2 | from mock import patch 3 | 4 | import ec2 5 | 6 | 7 | class ConnectionTestCase(BaseTestCase): 8 | def test_connect(self): 9 | with patch('boto.ec2.connect_to_region') as mock: 10 | ec2.connection.get_connection() 11 | mock.assert_called_once_with(aws_access_key_id='abc', aws_secret_access_key='xyz', region_name='us-east-1') 12 | 13 | with patch('boto.vpc.connect_to_region') as mock: 14 | ec2.connection.get_vpc_connection() 15 | mock.assert_called_once_with(aws_access_key_id='abc', aws_secret_access_key='xyz', region_name='us-east-1') 16 | 17 | 18 | class CredentialsTestCase(BaseTestCase): 19 | def test_credentials(self): 20 | self.assertEquals(dict(**ec2.credentials()), {'aws_access_key_id': 'abc', 'aws_secret_access_key': 'xyz', 'region_name': 'us-east-1'}) 21 | 22 | def test_from_bad_file(self): 23 | self.assertRaises( 24 | IOError, 25 | ec2.credentials.from_file, 26 | 'tests/base.py' 27 | ) 28 | 29 | def test_from_file(self): 30 | ec2.credentials.from_file('tests/credentials.csv') 31 | self.assertEquals(dict(**ec2.credentials()), {'aws_access_key_id': 'foo', 'aws_secret_access_key': 'bar', 'region_name': 'us-east-1'}) 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | ec2 4 | === 5 | 6 | A light-weight wrapper around boto to query for AWS EC2 instances, 7 | security groups, and VPCs in a sane way. 8 | """ 9 | 10 | from setuptools import setup, find_packages 11 | from setuptools.command.test import test as TestCommand 12 | 13 | 14 | install_requires = [ 15 | 'boto', 16 | ] 17 | 18 | tests_require = [ 19 | 'mock', 20 | 'pytest', 21 | 'pytest-cov', 22 | ] 23 | 24 | 25 | class PyTest(TestCommand): 26 | def finalize_options(self): 27 | TestCommand.finalize_options(self) 28 | self.test_suite = True 29 | 30 | def run_tests(self): 31 | import pytest 32 | import sys 33 | sys.exit(pytest.main(self.test_args)) 34 | 35 | 36 | setup( 37 | name='ec2', 38 | version='0.4.0', 39 | author='Matt Robenolt', 40 | author_email='matt@ydekproductions.com', 41 | url='https://github.com/mattrobenolt/ec2', 42 | description='Query for AWS EC2 instances, security groups, and VPCs simply', 43 | long_description=__doc__, 44 | packages=find_packages(exclude=('tests',)), 45 | install_requires=install_requires, 46 | tests_require=tests_require, 47 | license='BSD', 48 | cmdclass={'test': PyTest}, 49 | test_suite='tests', 50 | zip_safe=False, 51 | classifiers=[ 52 | 'Development Status :: 4 - Beta', 53 | 'Intended Audience :: Developers', 54 | 'Operating System :: OS Independent', 55 | 'Topic :: Software Development', 56 | ], 57 | ) 58 | -------------------------------------------------------------------------------- /ec2/connection.py: -------------------------------------------------------------------------------- 1 | """ 2 | ec2.connection 3 | ~~~~~~~~~~~~~~ 4 | 5 | :copyright: (c) 2014 by Matt Robenolt. 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | import os 10 | import boto.ec2 11 | import boto.vpc 12 | 13 | _connection = None 14 | _vpc_connection = None 15 | 16 | 17 | def get_connection(): 18 | "Cache a global connection object to be used by all classes" 19 | global _connection 20 | if _connection is None: 21 | _connection = boto.ec2.connect_to_region(**credentials()) 22 | return _connection 23 | 24 | 25 | def get_vpc_connection(): 26 | global _vpc_connection 27 | if _vpc_connection is None: 28 | _vpc_connection = boto.vpc.connect_to_region(**credentials()) 29 | return _vpc_connection 30 | 31 | 32 | class credentials(object): 33 | """ 34 | Simple credentials singleton that holds our fun AWS info 35 | and masquerades as a dict 36 | """ 37 | ACCESS_KEY_ID = None 38 | SECRET_ACCESS_KEY = None 39 | REGION_NAME = 'us-east-1' 40 | 41 | def keys(self): 42 | return ['aws_access_key_id', 'aws_secret_access_key', 'region_name'] 43 | 44 | def __getitem__(self, item): 45 | item = item.upper() 46 | return ( 47 | os.environ.get(item) or 48 | getattr(self, item, None) or 49 | getattr(self, item[4:]) 50 | ) 51 | 52 | @classmethod 53 | def from_file(cls, filename): 54 | """ 55 | Load ACCESS_KEY_ID and SECRET_ACCESS_KEY from csv 56 | generated by Amazon's IAM. 57 | 58 | >>> ec2.credentials.from_file('credentials.csv') 59 | """ 60 | import csv 61 | with open(filename, 'r') as f: 62 | reader = csv.DictReader(f) 63 | row = reader.next() # Only one row in the file 64 | try: 65 | cls.ACCESS_KEY_ID = row['Access Key Id'] 66 | cls.SECRET_ACCESS_KEY = row['Secret Access Key'] 67 | except KeyError: 68 | raise IOError('Invalid credentials format') 69 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | from boto.ec2.instance import Instance, InstanceState 2 | 3 | from boto.ec2.securitygroup import SecurityGroup 4 | from boto.vpc.vpc import VPC 5 | from mock import MagicMock, patch 6 | import unittest 7 | 8 | import ec2 9 | 10 | RUNNING_STATE = InstanceState(16, 'running') 11 | STOPPED_STATE = InstanceState(64, 'stopped') 12 | 13 | 14 | class BaseTestCase(unittest.TestCase): 15 | def setUp(self): 16 | ec2.credentials.ACCESS_KEY_ID = 'abc' 17 | ec2.credentials.SECRET_ACCESS_KEY = 'xyz' 18 | 19 | # Build up two reservations, with two instances each, totalling 4 instances 20 | # Two running, two stopped 21 | reservations = [] 22 | instance_count = 0 23 | for i in xrange(2): 24 | i1 = Instance() 25 | i1.id = 'i-abc%d' % instance_count 26 | i1._state = RUNNING_STATE 27 | i1.tags = {'Name': 'instance-%d' % instance_count} 28 | instance_count += 1 29 | i2 = Instance() 30 | i2.id = 'i-abc%d' % instance_count 31 | i2._state = STOPPED_STATE 32 | i2.tags = {'Name': 'instance-%d' % instance_count} 33 | instance_count += 1 34 | reservation = MagicMock() 35 | reservation.instances.__iter__ = MagicMock(return_value=iter([i1, i2])) 36 | reservations.append(reservation) 37 | 38 | security_groups = [] 39 | for i in xrange(2): 40 | sg = SecurityGroup() 41 | sg.id = 'sg-abc%d' % i 42 | sg.name = 'group-%d' % i 43 | sg.description = 'Group %d' % i 44 | security_groups.append(sg) 45 | 46 | vpcs = [] 47 | for i in xrange(2): 48 | vpc = VPC() 49 | vpc.id = 'vpc-abc%d' % i 50 | if i % 2: 51 | vpc.state = 'pending' 52 | vpc.is_default = False 53 | vpc.instance_tenancy = 'default' 54 | else: 55 | vpc.state = 'available' 56 | vpc.is_default = True 57 | vpc.instance_tenancy = 'dedicated' 58 | vpc.cidr_block = '10.%d.0.0/16' % i 59 | vpc.dhcp_options_id = 'dopt-abc%d' % i 60 | vpcs.append(vpc) 61 | 62 | self.connection = MagicMock() 63 | self.connection.get_all_instances = MagicMock(return_value=reservations) 64 | self.connection.get_all_security_groups = MagicMock(return_value=security_groups) 65 | 66 | self.vpc_connection = MagicMock() 67 | self.vpc_connection.get_all_vpcs = MagicMock(return_value=vpcs) 68 | 69 | def tearDown(self): 70 | ec2.credentials.ACCESS_KEY_ID = None 71 | ec2.credentials.SECRET_ACCESS_KEY = None 72 | ec2.credentials.REGION_NAME = 'us-east-1' 73 | ec2.instances.clear() 74 | ec2.security_groups.clear() 75 | ec2.vpcs.clear() 76 | 77 | def _patch_connection(self): 78 | return patch('ec2.types.get_connection', return_value=self.connection) 79 | 80 | def _patch_vpc_connection(self): 81 | return patch('ec2.types.get_vpc_connection', return_value=self.vpc_connection) 82 | -------------------------------------------------------------------------------- /ec2/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | ec2.base 3 | ~~~~~~~~ 4 | 5 | :copyright: (c) 2014 by Matt Robenolt. 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | from ec2.helpers import make_compare 10 | 11 | 12 | class _EC2MetaClass(type): 13 | "Metaclass for all EC2 filter type classes" 14 | 15 | def __new__(cls, name, bases, attrs): 16 | # Append MultipleObjectsReturned and DoesNotExist exceptions 17 | for contrib in ('MultipleObjectsReturned', 'DoesNotExist'): 18 | attrs[contrib] = type(contrib, (Exception,), {}) 19 | return super(_EC2MetaClass, cls).__new__(cls, name, bases, attrs) 20 | 21 | 22 | class objects_base(object): 23 | "Base class for all EC2 filter type classes" 24 | 25 | __metaclass__ = _EC2MetaClass 26 | 27 | @classmethod 28 | def all(cls): 29 | """ 30 | Wrapper around _all() to cache and return all results of something 31 | 32 | >>> ec2.instances.all() 33 | [ ... ] 34 | """ 35 | if not hasattr(cls, '_cache'): 36 | cls._cache = cls._all() 37 | return cls._cache 38 | 39 | @classmethod 40 | def get(cls, **kwargs): 41 | """ 42 | Generic get() for one item only 43 | 44 | >>> ec2.instances.get(name='production-web-01') 45 | 46 | """ 47 | things = cls.filter(**kwargs) 48 | if len(things) > 1: 49 | # Raise an exception if more than one object is matched 50 | raise cls.MultipleObjectsReturned 51 | elif len(things) == 0: 52 | # Rase an exception if no objects were matched 53 | raise cls.DoesNotExist 54 | return things[0] 55 | 56 | @classmethod 57 | def filter(cls, **kwargs): 58 | """ 59 | The meat. Filtering using Django model style syntax. 60 | 61 | All kwargs are translated into attributes on the underlying objects. 62 | If the attribute is not found, it looks for a similar key 63 | in the tags. 64 | 65 | There are a couple comparisons to check against as well: 66 | exact: check strict equality 67 | iexact: case insensitive exact 68 | like: check against regular expression 69 | ilike: case insensitive like 70 | contains: check if string is found with attribute 71 | icontains: case insensitive contains 72 | startswith: check if attribute value starts with the string 73 | istartswith: case insensitive startswith 74 | endswith: check if attribute value ends with the string 75 | iendswith: case insensitive startswith 76 | isnull: check if the attribute does not exist 77 | 78 | >>> ec2.instances.filter(name__startswith='production') 79 | [ ... ] 80 | """ 81 | qs = cls.all() 82 | for key in kwargs: 83 | qs = filter(lambda i: make_compare(key, kwargs[key], i), qs) 84 | return qs 85 | 86 | @classmethod 87 | def clear(cls): 88 | "Clear the cached instances" 89 | try: 90 | del cls._cache 91 | except AttributeError: 92 | pass 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon EC2 2 | Ever try to query for some instances with boto? It sucks. 3 | 4 | ```python 5 | >>> import ec2 6 | >>> ec2.instances.filter(state='running', name__startswith='production') 7 | [...] 8 | ``` 9 | 10 | ## Install 11 | `$ pip install ec2` 12 | 13 | ## Usage 14 | ### AWS credentials 15 | Credentials are defined as a global state, either through an environment variable, or in Python. 16 | ```python 17 | ec2.credentials.ACCESS_KEY_ID = 'xxx' 18 | ec2.credentials.SECRET_ACCESS_KEY = 'xxx' 19 | ec2.credentials.REGION_NAME = 'us-west-2' # (optional) defaults to us-east-1 20 | ``` 21 | 22 | Credentials can also be loaded from a CSV file generated by Amazon's IAM. 23 | **Note**: `REGION_NAME` still needs to be specified. 24 | ```python 25 | ec2.credentials.from_file('credentials.csv') 26 | ``` 27 | 28 | ## Querying 29 | ### All instances 30 | ```python 31 | ec2.instances.all() 32 | ``` 33 | 34 | ### All Security Groups 35 | ```python 36 | ec2.security_groups.all() 37 | ``` 38 | 39 | ### All Virtual Private Clouds 40 | ```python 41 | ec2.vpcs.all() 42 | ``` 43 | 44 | ### Filtering 45 | *Filter style is based on Django's ORM* 46 | All filters map directly to instance/security group properties. 47 | ```python 48 | ec2.instances.filter(id='i-xxx') # Exact instance id 49 | ec2.instances.filter(state='running') # Exact instance state 50 | ``` 51 | 52 | Filters will also dig into tags. 53 | ```python 54 | ec2.instances.filter(name='production-web') # Exact "Name" tag 55 | ``` 56 | 57 | Filters support many types of comparisons, similar to Django's ORM filters. 58 | ```python 59 | ec2.instances.filter(name__exact='production-web-01') # idential to `name='...'` 60 | ec2.instances.filter(name__iexact='PRODUCTION-WEB-01') # Case insensitive "exact" 61 | ec2.instances.filter(name__like=r'^production-web-\d+$') # Match against a regular expression 62 | ec2.instances.filter(name__ilike=r'^production-web-\d+$') # Case insensitive "like" 63 | ec2.instances.filter(name__contains='web') # Field contains the search string 64 | ec2.instances.filter(name__icontains='WEB') # Case insensitive "contains" 65 | ec2.instances.filter(name__startswith='production') # Fields starts with the search string 66 | ec2.instances.filter(name__istartswith='PRODUCTION') # Case insensitive "startswith" 67 | ec2.instances.filter(name__endswith='01') # Fields ends with the search string 68 | ec2.instances.filter(name__iendswith='01') # Case insensitive "endswith" 69 | ec2.instances.filter(name__isnull=False) # Match if the field exists 70 | ``` 71 | 72 | Filters can also be chained. 73 | ```python 74 | ec2.instances.filter(state='running', name__startswith='production') 75 | ``` 76 | 77 | Filters can also be used with security groups. 78 | ```python 79 | ec2.security_groups.filter(name__iexact='PRODUCTION-WEB') 80 | ``` 81 | 82 | Filters can also be used with virtual private clouds. 83 | ```python 84 | ec2.vpcs.filter(cidr_blocks__startswith='10.10') 85 | ``` 86 | 87 | `get()` works exactly the same as `filter()`, except it returns just one instance and raises an exception for anything else. 88 | ```python 89 | ec2.instances.get(name='production-web-01') # Return a single instance 90 | ec2.instances.get(name='i-dont-exist') # Raises an `ec2.instances.DoesNotExist` exception 91 | ec2.instances.get(name__like=r'^production-web-\d+$') # Raises an `ec2.instances.MultipleObjectsReturned` exception if matched more than one instance 92 | ec2.security_groups.get(name__startswith='production') # Raises an `ec2.security_groups.MultipleObjectsReturned` exception 93 | ec2.vpcs.get(cidr_block='10.10.0.0/16') 94 | ``` 95 | 96 | ### Search fields 97 | #### Instances 98 | * id *(Instance id)* 99 | * state *(running, terminated, pending, shutting-down, stopping, stopped)* 100 | * public_dns_name 101 | * ip_address 102 | * private_dns_name 103 | * private_ip_address 104 | * root_device_type *(ebs, instance-store)* 105 | * key_name *(name of the SSH key used on the instance)* 106 | * image_id *(Id of the AMI)* 107 | 108 | All fields can be found at: https://github.com/boto/boto/blob/d91ed8/boto/ec2/instance.py#L157-204 109 | 110 | #### Security Groups 111 | * id *(Security Group id)* 112 | * name 113 | * vpc_id 114 | 115 | #### Virtual Private Clouds 116 | * id *(Virtual Private Cloud id)* 117 | * cidr_block *(CIDR Network Block of the VPC)* 118 | * state *(Current state of the VPC, creation is not instant)* 119 | * is_default 120 | * instance_tenancy 121 | * dhcp_options_id *(DHCP options id)* 122 | 123 | 124 | ## Examples 125 | ### Get public ip addresses from all running instances who are named production-web-{number} 126 | ```python 127 | import ec2 128 | ec2.credentials.ACCESS_KEY_ID = 'xxx' 129 | ec2.credentials.SECRET_ACCESS_KEY = 'xxx' 130 | 131 | for instance in ec2.instances.filter(state='running', name__like=r'^production-web-\d+$'): 132 | print instance.ip_address 133 | ``` 134 | 135 | ### Add a role to a security group 136 | ```python 137 | import ec2 138 | ec2.credentials.ACCESS_KEY_ID = 'xxx' 139 | ec2.credentials.SECRET_ACCESS_KEY = 'xxx' 140 | 141 | try: 142 | group = ec2.security_groups.get(name='production-web') 143 | except ec2.security_groups.DoesNotExist: 144 | import sys 145 | sys.stderr.write('Not found.') 146 | sys.exit(1) 147 | group.authorize('tcp', 80, 80, cidr_ip='0.0.0.0/0') 148 | ``` 149 | -------------------------------------------------------------------------------- /ec2/helpers.py: -------------------------------------------------------------------------------- 1 | """ 2 | ec2.helpers 3 | ~~~~~~~~~~~ 4 | 5 | :copyright: (c) 2014 by Matt Robenolt. 6 | :license: BSD, see LICENSE for more details. 7 | """ 8 | 9 | import re 10 | 11 | 12 | def make_compare(key, value, obj): 13 | "Map a key name to a specific comparison function" 14 | if '__' not in key: 15 | # If no __ exists, default to doing an "exact" comparison 16 | key, comp = key, 'exact' 17 | else: 18 | key, comp = key.rsplit('__', 1) 19 | # Check if comp is valid 20 | if hasattr(Compare, comp): 21 | return getattr(Compare, comp)(key, value, obj) 22 | raise AttributeError("No comparison '%s'" % comp) 23 | 24 | 25 | class Compare(object): 26 | "Private class, namespacing comparison functions." 27 | 28 | @staticmethod 29 | def exact(key, value, obj): 30 | try: 31 | return getattr(obj, key) == value 32 | except AttributeError: 33 | # Fall back to checking tags 34 | if hasattr(obj, 'tags'): 35 | for tag in obj.tags: 36 | if key == tag.lower(): 37 | return obj.tags[tag] == value 38 | # There is no tag found either 39 | return False 40 | 41 | @staticmethod 42 | def iexact(key, value, obj): 43 | value = value.lower() 44 | try: 45 | return getattr(obj, key).lower() == value 46 | except AttributeError: 47 | # Fall back to checking tags 48 | if hasattr(obj, 'tags'): 49 | for tag in obj.tags: 50 | if key == tag.lower(): 51 | return obj.tags[tag].lower() == value 52 | # There is no tag found either 53 | return False 54 | 55 | @staticmethod 56 | def like(key, value, obj): 57 | if isinstance(value, basestring): 58 | # If a string is passed in, 59 | # we want to convert it to a pattern object 60 | value = re.compile(value) 61 | try: 62 | return bool(value.match(getattr(obj, key))) 63 | except AttributeError: 64 | # Fall back to checking tags 65 | if hasattr(obj, 'tags'): 66 | for tag in obj.tags: 67 | if key == tag.lower(): 68 | return bool(value.match(obj.tags[tag])) 69 | # There is no tag found either 70 | return False 71 | # Django alias 72 | regex = like 73 | 74 | @staticmethod 75 | def ilike(key, value, obj): 76 | return Compare.like(key, re.compile(value, re.I), obj) 77 | # Django alias 78 | iregex = ilike 79 | 80 | @staticmethod 81 | def contains(key, value, obj): 82 | try: 83 | return value in getattr(obj, key) 84 | except AttributeError: 85 | # Fall back to checking tags 86 | if hasattr(obj, 'tags'): 87 | for tag in obj.tags: 88 | if key == tag.lower(): 89 | return value in obj.tags[tag] 90 | # There is no tag found either 91 | return False 92 | 93 | @staticmethod 94 | def icontains(key, value, obj): 95 | value = value.lower() 96 | try: 97 | return value in getattr(obj, key).lower() 98 | except AttributeError: 99 | # Fall back to checking tags 100 | if hasattr(obj, 'tags'): 101 | for tag in obj.tags: 102 | if key == tag.lower(): 103 | return value in obj.tags[tag] 104 | # There is no tag found either 105 | return False 106 | 107 | @staticmethod 108 | def startswith(key, value, obj): 109 | try: 110 | return getattr(obj, key).startswith(value) 111 | except AttributeError: 112 | # Fall back to checking tags 113 | if hasattr(obj, 'tags'): 114 | for tag in obj.tags: 115 | if key == tag.lower(): 116 | return obj.tags[tag].startswith(value) 117 | # There is no tag found either 118 | return False 119 | 120 | @staticmethod 121 | def istartswith(key, value, obj): 122 | value = value.lower() 123 | try: 124 | return getattr(obj, key).startswith(value) 125 | except AttributeError: 126 | # Fall back to checking tags 127 | if hasattr(obj, 'tags'): 128 | for tag in obj.tags: 129 | if key == tag.lower(): 130 | return obj.tags[tag].lower().startswith(value) 131 | # There is no tag found either 132 | return False 133 | 134 | @staticmethod 135 | def endswith(key, value, obj): 136 | try: 137 | return getattr(obj, key).endswith(value) 138 | except AttributeError: 139 | # Fall back to checking tags 140 | if hasattr(obj, 'tags'): 141 | for tag in obj.tags: 142 | if key == tag.lower(): 143 | return obj.tags[tag].endswith(value) 144 | # There is no tag found either 145 | return False 146 | 147 | @staticmethod 148 | def iendswith(key, value, obj): 149 | value = value.lower() 150 | try: 151 | return getattr(obj, key).endswith(value) 152 | except AttributeError: 153 | # Fall back to checking tags 154 | if hasattr(obj, 'tags'): 155 | for tag in obj.tags: 156 | if key == tag.lower(): 157 | return obj.tags[tag].lower().endswith(value) 158 | # There is no tag found either 159 | return False 160 | 161 | @staticmethod 162 | def isnull(key, value, obj): 163 | try: 164 | return (getattr(obj, key) is None) == value 165 | except AttributeError: 166 | # Fall back to checking tags 167 | if hasattr(obj, 'tags'): 168 | for tag in obj.tags: 169 | if key == tag.lower(): 170 | return (obj.tags[tag] is None) and value 171 | # There is no tag found either, so must be null 172 | return True and value 173 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | from .base import BaseTestCase 2 | 3 | import ec2 4 | 5 | 6 | class InstancesTestCase(BaseTestCase): 7 | def test_all(self): 8 | "instances.all() should iterate over all reservations and collect all instances, then cache the results" 9 | with self._patch_connection() as mock: 10 | instances = ec2.instances.all() 11 | self.assertEquals(4, len(instances)) 12 | # all() should cache the connection and list of instances 13 | # so when calling a second time, _connect() shouldn't 14 | # be called 15 | ec2.instances.all() 16 | mock.assert_called_once() # Should only be called once from the initial _connect 17 | 18 | def test_filters_integration(self): 19 | with self._patch_connection(): 20 | instances = ec2.instances.filter(state='crap') 21 | self.assertEquals(0, len(instances)) 22 | 23 | instances = ec2.instances.filter(state='running') 24 | self.assertEquals(2, len(instances)) 25 | self.assertEquals('running', instances[0].state) 26 | self.assertEquals('running', instances[1].state) 27 | 28 | instances = ec2.instances.filter(state='stopped') 29 | self.assertEquals(2, len(instances)) 30 | self.assertEquals('stopped', instances[0].state) 31 | self.assertEquals('stopped', instances[1].state) 32 | 33 | instances = ec2.instances.filter(id__exact='i-abc0') 34 | self.assertEquals(1, len(instances)) 35 | 36 | instances = ec2.instances.filter(id__iexact='I-ABC0') 37 | self.assertEquals(1, len(instances)) 38 | 39 | instances = ec2.instances.filter(id__like=r'^i\-abc\d$') 40 | self.assertEquals(4, len(instances)) 41 | 42 | instances = ec2.instances.filter(id__ilike=r'^I\-ABC\d$') 43 | self.assertEquals(4, len(instances)) 44 | 45 | instances = ec2.instances.filter(id__contains='1') 46 | self.assertEquals(1, len(instances)) 47 | 48 | instances = ec2.instances.filter(id__icontains='ABC') 49 | self.assertEquals(4, len(instances)) 50 | 51 | instances = ec2.instances.filter(id__startswith='i-') 52 | self.assertEquals(4, len(instances)) 53 | 54 | instances = ec2.instances.filter(id__istartswith='I-') 55 | self.assertEquals(4, len(instances)) 56 | 57 | instances = ec2.instances.filter(id__endswith='c0') 58 | self.assertEquals(1, len(instances)) 59 | 60 | instances = ec2.instances.filter(id__iendswith='C0') 61 | self.assertEquals(1, len(instances)) 62 | 63 | instances = ec2.instances.filter(id__startswith='i-', name__endswith='-0') 64 | self.assertEquals(1, len(instances)) 65 | 66 | instances = ec2.instances.filter(id__isnull=False) 67 | self.assertEquals(4, len(instances)) 68 | 69 | instances = ec2.instances.filter(id__isnull=True) 70 | self.assertEquals(0, len(instances)) 71 | 72 | def test_get_raises(self): 73 | with self._patch_connection(): 74 | self.assertRaises( 75 | ec2.instances.MultipleObjectsReturned, 76 | ec2.instances.get, 77 | id__startswith='i' 78 | ) 79 | 80 | self.assertRaises( 81 | ec2.instances.DoesNotExist, 82 | ec2.instances.get, 83 | name='crap' 84 | ) 85 | 86 | def test_get(self): 87 | with self._patch_connection(): 88 | self.assertEquals(ec2.instances.get(id='i-abc0').id, 'i-abc0') 89 | 90 | 91 | class SecurityGroupsTestCase(BaseTestCase): 92 | def test_all(self): 93 | with self._patch_connection() as mock: 94 | groups = ec2.security_groups.all() 95 | self.assertEquals(2, len(groups)) 96 | # all() should cache the connection and list of instances 97 | # so when calling a second time, _connect() shouldn't 98 | # be called 99 | ec2.security_groups.all() 100 | mock.assert_called_once() 101 | 102 | def test_filters_integration(self): 103 | with self._patch_connection(): 104 | groups = ec2.security_groups.filter(name='crap') 105 | self.assertEquals(0, len(groups)) 106 | 107 | groups = ec2.security_groups.filter(id__exact='sg-abc0') 108 | self.assertEquals(1, len(groups)) 109 | 110 | groups = ec2.security_groups.filter(id__iexact='SG-ABC0') 111 | self.assertEquals(1, len(groups)) 112 | 113 | groups = ec2.security_groups.filter(id__like=r'^sg\-abc\d$') 114 | self.assertEquals(2, len(groups)) 115 | 116 | groups = ec2.security_groups.filter(id__ilike=r'^SG\-ABC\d$') 117 | self.assertEquals(2, len(groups)) 118 | 119 | groups = ec2.security_groups.filter(id__contains='1') 120 | self.assertEquals(1, len(groups)) 121 | 122 | groups = ec2.security_groups.filter(id__icontains='ABC') 123 | self.assertEquals(2, len(groups)) 124 | 125 | groups = ec2.security_groups.filter(id__startswith='sg-') 126 | self.assertEquals(2, len(groups)) 127 | 128 | groups = ec2.security_groups.filter(id__istartswith='SG-') 129 | self.assertEquals(2, len(groups)) 130 | 131 | groups = ec2.security_groups.filter(id__endswith='c0') 132 | self.assertEquals(1, len(groups)) 133 | 134 | groups = ec2.security_groups.filter(id__iendswith='C0') 135 | self.assertEquals(1, len(groups)) 136 | 137 | groups = ec2.security_groups.filter(id__startswith='sg-', name__endswith='-0') 138 | self.assertEquals(1, len(groups)) 139 | 140 | groups = ec2.security_groups.filter(id__isnull=False) 141 | self.assertEquals(2, len(groups)) 142 | 143 | groups = ec2.security_groups.filter(id__isnull=True) 144 | self.assertEquals(0, len(groups)) 145 | 146 | def test_get_raises(self): 147 | with self._patch_connection(): 148 | self.assertRaises( 149 | ec2.security_groups.MultipleObjectsReturned, 150 | ec2.security_groups.get, 151 | id__startswith='sg' 152 | ) 153 | 154 | self.assertRaises( 155 | ec2.security_groups.DoesNotExist, 156 | ec2.security_groups.get, 157 | name='crap' 158 | ) 159 | 160 | def test_get(self): 161 | with self._patch_connection(): 162 | self.assertEquals(ec2.security_groups.get(id='sg-abc0').id, 'sg-abc0') 163 | 164 | 165 | class VPCTestCase(BaseTestCase): 166 | def test_all(self): 167 | with self._patch_vpc_connection() as mock: 168 | vpcs = ec2.vpcs.all() 169 | self.assertEquals(2, len(vpcs)) 170 | ec2.vpcs.all() 171 | mock.assert_called_once() 172 | 173 | def test_filters_integration(self): 174 | with self._patch_vpc_connection(): 175 | groups = ec2.vpcs.filter(id__exact='vpc-abc0') 176 | self.assertEquals(1, len(groups)) 177 | 178 | groups = ec2.vpcs.filter(id__iexact='VPC-ABC0') 179 | self.assertEquals(1, len(groups)) 180 | 181 | groups = ec2.vpcs.filter(id__like=r'^vpc\-abc\d$') 182 | self.assertEquals(2, len(groups)) 183 | 184 | groups = ec2.vpcs.filter(id__ilike=r'^VPC\-ABC\d$') 185 | self.assertEquals(2, len(groups)) 186 | 187 | groups = ec2.vpcs.filter(id__contains='1') 188 | self.assertEquals(1, len(groups)) 189 | 190 | groups = ec2.vpcs.filter(id__icontains='ABC') 191 | self.assertEquals(2, len(groups)) 192 | 193 | groups = ec2.vpcs.filter(id__startswith='vpc-') 194 | self.assertEquals(2, len(groups)) 195 | 196 | groups = ec2.vpcs.filter(id__istartswith='vpc-') 197 | self.assertEquals(2, len(groups)) 198 | 199 | groups = ec2.vpcs.filter(id__endswith='c0') 200 | self.assertEquals(1, len(groups)) 201 | 202 | groups = ec2.vpcs.filter(id__iendswith='C0') 203 | self.assertEquals(1, len(groups)) 204 | 205 | groups = ec2.vpcs.filter(id__startswith='vpc-', dhcp_options_id__endswith='abc0') 206 | self.assertEquals(1, len(groups)) 207 | 208 | groups = ec2.vpcs.filter(id__isnull=False) 209 | self.assertEquals(2, len(groups)) 210 | 211 | groups = ec2.vpcs.filter(id__isnull=True) 212 | self.assertEquals(0, len(groups)) 213 | 214 | def test_get_raises(self): 215 | with self._patch_vpc_connection(): 216 | self.assertRaises( 217 | ec2.vpcs.MultipleObjectsReturned, 218 | ec2.vpcs.get, 219 | id__startswith='vpc' 220 | ) 221 | 222 | self.assertRaises( 223 | ec2.vpcs.DoesNotExist, 224 | ec2.vpcs.get, 225 | name='crap' 226 | ) 227 | 228 | def test_get(self): 229 | with self._patch_vpc_connection(): 230 | self.assertEquals(ec2.vpcs.get(id='vpc-abc0').id, 'vpc-abc0') 231 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | from .base import RUNNING_STATE 2 | from boto.ec2.instance import Instance 3 | from mock import patch 4 | import unittest 5 | import re 6 | 7 | import ec2 8 | 9 | 10 | class ComparisonTests(unittest.TestCase): 11 | def setUp(self): 12 | self.instance = Instance() 13 | self.instance._state = RUNNING_STATE 14 | self.instance.id = 'i-abc' 15 | self.instance.tags = {'Name': 'awesome'} 16 | 17 | def test_comp(self): 18 | i = self.instance 19 | 20 | self.assertRaises(AttributeError, ec2.helpers.make_compare, 'state__nope', 'running', i) 21 | 22 | with patch('ec2.helpers.Compare.exact') as mock: 23 | ec2.helpers.make_compare('state', 'running', i) 24 | mock.assert_called_once_with('state', 'running', i) 25 | 26 | with patch('ec2.helpers.Compare.exact') as mock: 27 | ec2.helpers.make_compare('state__exact', 'running', i) 28 | mock.assert_called_once_with('state', 'running', i) 29 | 30 | with patch('ec2.helpers.Compare.iexact') as mock: 31 | ec2.helpers.make_compare('state__iexact', 'running', i) 32 | mock.assert_called_once_with('state', 'running', i) 33 | 34 | with patch('ec2.helpers.Compare.like') as mock: 35 | ec2.helpers.make_compare('state__like', 'running', i) 36 | mock.assert_called_once_with('state', 'running', i) 37 | 38 | with patch('ec2.helpers.Compare.regex') as mock: 39 | ec2.helpers.make_compare('state__regex', 'running', i) 40 | mock.assert_called_once_with('state', 'running', i) 41 | 42 | with patch('ec2.helpers.Compare.ilike') as mock: 43 | ec2.helpers.make_compare('state__ilike', 'running', i) 44 | mock.assert_called_once_with('state', 'running', i) 45 | 46 | with patch('ec2.helpers.Compare.iregex') as mock: 47 | ec2.helpers.make_compare('state__iregex', 'running', i) 48 | mock.assert_called_once_with('state', 'running', i) 49 | 50 | with patch('ec2.helpers.Compare.contains') as mock: 51 | ec2.helpers.make_compare('state__contains', 'running', i) 52 | mock.assert_called_once_with('state', 'running', i) 53 | 54 | with patch('ec2.helpers.Compare.icontains') as mock: 55 | ec2.helpers.make_compare('state__icontains', 'running', i) 56 | mock.assert_called_once_with('state', 'running', i) 57 | 58 | with patch('ec2.helpers.Compare.startswith') as mock: 59 | ec2.helpers.make_compare('state__startswith', 'running', i) 60 | mock.assert_called_once_with('state', 'running', i) 61 | 62 | with patch('ec2.helpers.Compare.istartswith') as mock: 63 | ec2.helpers.make_compare('state__istartswith', 'running', i) 64 | mock.assert_called_once_with('state', 'running', i) 65 | 66 | with patch('ec2.helpers.Compare.endswith') as mock: 67 | ec2.helpers.make_compare('state__endswith', 'running', i) 68 | mock.assert_called_once_with('state', 'running', i) 69 | 70 | with patch('ec2.helpers.Compare.iendswith') as mock: 71 | ec2.helpers.make_compare('state__iendswith', 'running', i) 72 | mock.assert_called_once_with('state', 'running', i) 73 | 74 | with patch('ec2.helpers.Compare.isnull') as mock: 75 | ec2.helpers.make_compare('state__isnull', True, i) 76 | mock.assert_called_once_with('state', True, i) 77 | 78 | def test_exact(self): 79 | i = self.instance 80 | self.assertTrue(ec2.helpers.Compare.exact('state', 'running', i)) 81 | self.assertFalse(ec2.helpers.Compare.exact('state', 'notrunning', i)) 82 | self.assertTrue(ec2.helpers.Compare.exact('name', 'awesome', i)) 83 | self.assertFalse(ec2.helpers.Compare.exact('name', 'notawesome', i)) 84 | 85 | def test_iexact(self): 86 | i = self.instance 87 | self.assertTrue(ec2.helpers.Compare.iexact('state', 'RUNNING', i)) 88 | self.assertFalse(ec2.helpers.Compare.iexact('state', 'NOTRUNNING', i)) 89 | self.assertTrue(ec2.helpers.Compare.iexact('name', 'AWESOME', i)) 90 | self.assertFalse(ec2.helpers.Compare.iexact('name', 'NOTAWESOME', i)) 91 | 92 | def test_like(self): 93 | i = self.instance 94 | self.assertTrue(ec2.helpers.Compare.like('state', r'^r.+g$', i)) 95 | self.assertTrue(ec2.helpers.Compare.like('state', re.compile(r'^r.+g$'), i)) 96 | self.assertFalse(ec2.helpers.Compare.like('state', r'^n.+g$', i)) 97 | self.assertFalse(ec2.helpers.Compare.like('state', re.compile(r'^n.+g$'), i)) 98 | self.assertTrue(ec2.helpers.Compare.like('name', r'^a.+e$', i)) 99 | self.assertTrue(ec2.helpers.Compare.like('name', re.compile(r'^a.+e$'), i)) 100 | self.assertFalse(ec2.helpers.Compare.like('name', r'^n.+e$', i)) 101 | self.assertFalse(ec2.helpers.Compare.like('name', re.compile(r'^n.+e$'), i)) 102 | 103 | def test_regex(self): 104 | i = self.instance 105 | self.assertTrue(ec2.helpers.Compare.regex('state', r'^r.+g$', i)) 106 | self.assertTrue(ec2.helpers.Compare.regex('state', re.compile(r'^r.+g$'), i)) 107 | self.assertFalse(ec2.helpers.Compare.regex('state', r'^n.+g$', i)) 108 | self.assertFalse(ec2.helpers.Compare.regex('state', re.compile(r'^n.+g$'), i)) 109 | self.assertTrue(ec2.helpers.Compare.regex('name', r'^a.+e$', i)) 110 | self.assertTrue(ec2.helpers.Compare.regex('name', re.compile(r'^a.+e$'), i)) 111 | self.assertFalse(ec2.helpers.Compare.regex('name', r'^n.+e$', i)) 112 | self.assertFalse(ec2.helpers.Compare.regex('name', re.compile(r'^n.+e$'), i)) 113 | 114 | def test_ilike(self): 115 | i = self.instance 116 | self.assertTrue(ec2.helpers.Compare.ilike('state', r'^R.+G$', i)) 117 | self.assertFalse(ec2.helpers.Compare.ilike('state', r'^N.+G$', i)) 118 | self.assertTrue(ec2.helpers.Compare.ilike('name', r'^A.+E$', i)) 119 | self.assertFalse(ec2.helpers.Compare.ilike('name', r'^N.+E$', i)) 120 | 121 | def test_iregex(self): 122 | i = self.instance 123 | self.assertTrue(ec2.helpers.Compare.iregex('state', r'^R.+G$', i)) 124 | self.assertFalse(ec2.helpers.Compare.iregex('state', r'^N.+G$', i)) 125 | self.assertTrue(ec2.helpers.Compare.iregex('name', r'^A.+E$', i)) 126 | self.assertFalse(ec2.helpers.Compare.iregex('name', r'^N.+E$', i)) 127 | 128 | def test_contains(self): 129 | i = self.instance 130 | self.assertTrue(ec2.helpers.Compare.contains('state', 'unn', i)) 131 | self.assertFalse(ec2.helpers.Compare.contains('state', 'notunn', i)) 132 | self.assertTrue(ec2.helpers.Compare.contains('name', 'wes', i)) 133 | self.assertFalse(ec2.helpers.Compare.contains('name', 'notwes', i)) 134 | 135 | def test_icontains(self): 136 | i = self.instance 137 | self.assertTrue(ec2.helpers.Compare.icontains('state', 'UNN', i)) 138 | self.assertFalse(ec2.helpers.Compare.icontains('state', 'NOTUNN', i)) 139 | self.assertTrue(ec2.helpers.Compare.icontains('name', 'WES', i)) 140 | self.assertFalse(ec2.helpers.Compare.icontains('name', 'NOTWES', i)) 141 | 142 | def test_startswith(self): 143 | i = self.instance 144 | self.assertTrue(ec2.helpers.Compare.startswith('state', 'run', i)) 145 | self.assertFalse(ec2.helpers.Compare.startswith('state', 'notrun', i)) 146 | self.assertTrue(ec2.helpers.Compare.startswith('name', 'awe', i)) 147 | self.assertFalse(ec2.helpers.Compare.startswith('name', 'notawe', i)) 148 | 149 | def test_istartswith(self): 150 | i = self.instance 151 | self.assertTrue(ec2.helpers.Compare.istartswith('state', 'RUN', i)) 152 | self.assertFalse(ec2.helpers.Compare.istartswith('state', 'NOTRUN', i)) 153 | self.assertTrue(ec2.helpers.Compare.istartswith('name', 'AWE', i)) 154 | self.assertFalse(ec2.helpers.Compare.istartswith('name', 'NOTAWE', i)) 155 | 156 | def test_endswith(self): 157 | i = self.instance 158 | self.assertTrue(ec2.helpers.Compare.endswith('state', 'ing', i)) 159 | self.assertFalse(ec2.helpers.Compare.endswith('state', 'noting', i)) 160 | self.assertTrue(ec2.helpers.Compare.endswith('name', 'some', i)) 161 | self.assertFalse(ec2.helpers.Compare.endswith('name', 'notsome', i)) 162 | 163 | def test_iendswith(self): 164 | i = self.instance 165 | self.assertTrue(ec2.helpers.Compare.iendswith('state', 'ING', i)) 166 | self.assertFalse(ec2.helpers.Compare.iendswith('state', 'NOTING', i)) 167 | self.assertTrue(ec2.helpers.Compare.iendswith('name', 'SOME', i)) 168 | self.assertFalse(ec2.helpers.Compare.iendswith('name', 'NOTSOME', i)) 169 | 170 | def test_isnull(self): 171 | i = self.instance 172 | self.assertTrue(ec2.helpers.Compare.isnull('foo', True, i)) 173 | self.assertFalse(ec2.helpers.Compare.isnull('foo', False, i)) 174 | self.assertFalse(ec2.helpers.Compare.isnull('name', True, i)) 175 | self.assertFalse(ec2.helpers.Compare.isnull('name', False, i)) 176 | 177 | def test_unknown_key(self): 178 | i = self.instance 179 | for attr in ('exact', 'iexact', 'like', 'ilike', 'contains', 'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith'): 180 | self.assertFalse(getattr(ec2.helpers.Compare, attr)('lol', 'foo', i)) 181 | --------------------------------------------------------------------------------