├── LICENSE ├── README.md └── attach_volume.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Phil Hendren 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ASG Persistence 2 | ================ 3 | 4 | Persisting things in Autoscaling Groups 5 | 6 | **attach_volume.py** - attach an EBS volume by tag info. Script will search for a volume with given name in it's AZ and attach it for you 7 | 8 | usage: attach_volume.py [-h] [--tag TAG] --value VALUE --attach_as ATTACH_AS 9 | 10 | Attach EBS Volume 11 | 12 | optional arguments: 13 | -h, --help show this help message and exit 14 | --tag TAG Tag key, defaults to Name 15 | --value VALUE The tag value to search for 16 | --attach_as ATTACH_AS 17 | device path e.g. /dev/xvdb 18 | 19 | -------------------------------------------------------------------------------- /attach_volume.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Retrieve available EBS volume in AZ.""" 3 | import sys 4 | import os 5 | import boto3 6 | import argparse 7 | import time 8 | import urllib2 9 | 10 | 11 | def parse_args(): 12 | """Argument needs to be parsed.""" 13 | parser = argparse.ArgumentParser(description='Attach EBS Volume') 14 | parser.add_argument('--tag', action='store', default='Name', 15 | help='Tag key, defaults to Name') 16 | parser.add_argument('--value', action='store', required=True, 17 | help='The tag value to search for') 18 | parser.add_argument('--attach_as', action='store', required=True, 19 | help='device path e.g. /dev/xvdb') 20 | parser.add_argument('--skip_check', action='store', required=False, 21 | help='skip the check and just continue') 22 | parser.add_argument('--wait', action='store_true', required=False, 23 | help='If no available volume is found, wait.') 24 | return parser.parse_args() 25 | 26 | 27 | def utils(endpoint): 28 | """Replacing boto.utils, read from instance metadata directly.""" 29 | return urllib2.urlopen( 30 | 'http://169.254.169.254/latest/meta-data/%s' % endpoint 31 | ).read() 32 | 33 | 34 | def instance_id(): 35 | """Retrieve current Instance's ID.""" 36 | return utils('instance-id') 37 | 38 | 39 | def region(): 40 | """Retrieve current Instance's Region.""" 41 | return zone()[:-1] 42 | 43 | 44 | def zone(): 45 | """Retrieve current Instance's Zone.""" 46 | return utils('placement/availability-zone') 47 | 48 | 49 | def filters(tag, val): 50 | """Helper method for parsing filters.""" 51 | return { 52 | 'Name': 'tag:%s' % tag, 53 | 'Values': ['%s' % val] 54 | } 55 | 56 | 57 | def find(tag, val, client=None): 58 | """Locate a free volume for this instance.""" 59 | c = client or boto3.client('ec2', region()) 60 | try: 61 | for x in c.describe_volumes(Filters=[filters(tag, val)])['Volumes']: 62 | if x['AvailabilityZone'] == zone(): 63 | if x['State'] == 'available': 64 | return x 65 | except Exception, e: 66 | print(e) 67 | sys.exit(2) 68 | 69 | 70 | def attach(vol_id, attach_as, client=None): 71 | """Attach EBS volume to an Instance.""" 72 | c = client or boto3.client('ec2', region()) 73 | if not vol_id: 74 | raise Exception('No volumes available') 75 | sys.exit(4) 76 | 77 | try: 78 | c.attach_volume( 79 | VolumeId=vol_id, 80 | InstanceId=instance_id(), 81 | Device=attach_as) 82 | except Exception, e: 83 | print(e) 84 | sys.exit(3) 85 | 86 | 87 | def check(attach_as): 88 | """Resolve attach_as path.""" 89 | return os.path.exists(attach_as) 90 | 91 | 92 | def already_attached(args): 93 | """Check if the disk is actually already attached.""" 94 | ec2 = boto3.resource('ec2', region()) 95 | instance = ec2.Instance(instance_id()) 96 | for v in instance.volumes.all(): 97 | volume = ec2.Volume(v.id) 98 | if volume.tags: 99 | for tag in volume.tags: 100 | if tag['Key'] == args.tag and tag['Value'] == args.value: 101 | print('Disk is already attached!') 102 | sys.exit(0) 103 | 104 | 105 | def main(args): 106 | """Initialize find+attach action.""" 107 | already_attached(args) 108 | 109 | client = boto3.client('ec2', region()) 110 | volume_id = None 111 | volume = find(args.tag, args.value, client) 112 | if volume: 113 | volume_id = volume['VolumeId'] 114 | if volume['State'] != 'available' and args.wait: 115 | volume_waiter = client.get_waiter('volume_available') 116 | volume_waiter.wait(VolumeIds=[volume_id]) 117 | 118 | attach(volume_id, args.attach_as, client) 119 | 120 | if not args.skip_check: 121 | counter = 0 122 | while not check(args.attach_as): 123 | counter = counter + 5 124 | time.sleep(5) 125 | if counter > 60: 126 | raise Exception('Timeout waiting for attachment') 127 | sys.exit(2) 128 | sys.exit(0) 129 | 130 | 131 | if __name__ == "__main__": 132 | main(parse_args()) 133 | --------------------------------------------------------------------------------