diff --git a/graffiti_monkey/core.py b/graffiti_monkey/core.py index 6d562f4..6f044b8 100644 --- a/graffiti_monkey/core.py +++ b/graffiti_monkey/core.py @@ -16,8 +16,8 @@ from exceptions import * -import boto -from boto import ec2 +import boto3 +import botocore import time @@ -72,14 +72,16 @@ def __init__(self, region, profile, instance_tags_to_propagate, volume_tags_to_p log.info("Options: dryrun %s, append %s, novolumes %s, nosnapshots %s", self._dryrun, self._append, self._novolumes, self._nosnapshots) log.info("Connecting to region %s using profile %s", self._region, self._profile) try: - self._conn = ec2.connect_to_region(self._region, profile_name=self._profile) - except boto.exception.NoAuthHandlerFound: + session = boto3.Session(profile_name=profile) + self._conn = session.client('ec2', region_name=self._region) + except botocore.exceptions.ProfileNotFound: raise GraffitiMonkeyException('No AWS credentials found - check your credentials') - except boto.provider.ProfileNotFoundError: + except (botocore.exceptions.NoCredentialsError, botocore.exceptions.PartialCredentialsError, botocore.exceptions.CredentialRetrievalError): log.info("Connecting to region %s using default credentials", self._region) try: - self._conn = ec2.connect_to_region(self._region) - except boto.exception.NoAuthHandlerFound: + session = boto3.Session() + self._conn = session.client('ec2', region_name=self._region) + except botocore.exceptions.ProfileNotFound: raise GraffitiMonkeyException('No AWS credentials found - check your credentials') @@ -91,7 +93,7 @@ def propagate_tags(self): if not self._novolumes: volumes = self.tag_volumes() - volumes = { v.id: v for v in volumes } + volumes = { v["VolumeId"]: v for v in volumes } if not self._nosnapshots: self.tag_snapshots(volumes) @@ -101,7 +103,7 @@ def tag_volumes(self): them ''' storage_counter = 0 - volumes = [] + volumes = [] instances = {} if self._volumes_to_tag: @@ -138,11 +140,28 @@ def tag_volumes(self): else: log.info('Getting list of all volumes') - volumes = self._conn.get_all_volumes() - reservations = self._conn.get_all_instances() - for reservation in reservations: - for instance in reservation.instances: - instances[instance.id] = instance + results = "" + kwargs = { } + while True: + if "NextToken" in results: + kwargs["NextToken"] = results["NextToken"] + results = self._conn.describe_volumes(**kwargs) + for volume in results["Volumes"]: + volumes.append(volume) + if "NextToken" not in results: + break + + results = "" + kwargs = { } + while True: + if "NextToken" in results: + kwargs["NextToken"] = results["NextToken"] + results = self._conn.describe_instances(**kwargs) + for reservation in results["Reservations"]: + for instance in reservation["Instances"]: + instances[instance["InstanceId"]] = instance + if "NextToken" not in results: + break if not volumes: log.info('No volumes found') @@ -154,20 +173,20 @@ def tag_volumes(self): this_vol = 0 for volume in volumes: this_vol += 1 - storage_counter += volume.size + storage_counter += volume["Size"] log.info ('Processing volume %d of %d total volumes', this_vol, total_vols) - if volume.status != 'in-use': - log.debug('Skipping %s as it is not attached to an EC2 instance, so there is nothing to propagate', volume.id) + if volume["State"] != 'in-use': + log.debug('Skipping %s as it is not attached to an EC2 instance, so there is nothing to propagate', volume["VolumeId"]) continue for attempt in range(5): try: self.tag_volume(volume, instances) - except boto.exception.EC2ResponseError, e: + except botocore.exceptions.ClientError as e: log.error("Encountered Error %s on volume %s", e.error_code, volume.id) break - except boto.exception.BotoServerError, e: + except (botocore.exceptions.EndpointConnectionError, botocore.exceptions.ConnectionClosedError) as e: log.error("Encountered Error %s on volume %s, waiting %d seconds then retrying", e.error_code, volume.id, attempt) time.sleep(attempt) else: @@ -186,22 +205,27 @@ def tag_volume(self, volume, instances): ''' Tags a specific volume ''' instance_id = None - if volume.attach_data.instance_id: - instance_id = volume.attach_data.instance_id + if volume["Attachments"][0]["InstanceId"]: + instance_id = volume["Attachments"][0]["InstanceId"] device = None - if volume.attach_data.device: - device = volume.attach_data.device + if volume["Attachments"][0]["Device"]: + device = volume["Attachments"][0]["Device"] - instance_tags = instances[instance_id].tags + if "Tags" in instances[instance_id]: + instance_tags = instances[instance_id]["Tags"] + else: + instance_tags = [] tags_to_set = {} if self._append: tags_to_set = volume.tags for tag_name in self._instance_tags_to_propagate: log.debug('Trying to propagate instance tag: %s', tag_name) - if tag_name in instance_tags: - value = instance_tags[tag_name] - tags_to_set[tag_name] = value + + for tag_set in instance_tags: + if tag_name in tag_set["Key"]: + value = tag_set["Value"] + tags_to_set[tag_name] = value # Additional tags tags_to_set['instance_id'] = instance_id @@ -213,9 +237,9 @@ def tag_volume(self, volume, instances): tags_to_set[tag['key']] = tag['value'] if self._dryrun: - log.info('DRYRUN: Volume %s would have been tagged %s', volume.id, tags_to_set) + log.info('DRYRUN: Volume %s would have been tagged %s', volume["VolumeId"], tags_to_set) else: - self._set_resource_tags(volume, tags_to_set) + self._set_resource_tags(volume, "VolumeId", tags_to_set) return True @@ -243,22 +267,31 @@ def tag_snapshots(self, volumes): self._snapshots_to_tag.remove(snapshot) else: log.info('Getting list of all snapshots') - snapshots = self._conn.get_all_snapshots(owner='self') + results = "" + kwargs = {"OwnerIds": ["self"]} + while True: + if "NextToken" in results: + kwargs["NextToken"] = results["NextToken"] + results = self._conn.describe_snapshots(**kwargs) + for snapshot in results["Snapshots"]: + snapshots.append(snapshot) + if "NextToken" not in results: + break if not snapshots: log.info('No snapshots found') return True - all_volume_ids = set(s.volume_id for s in snapshots) + all_volume_ids = set(s["VolumeId"] for s in snapshots) extra_volume_ids = [id for id in all_volume_ids if id not in volumes] ''' Fetch any extra volumes that weren't carried over from tag_volumes() (if any) ''' for chunk in (extra_volume_ids[n:n+200] for n in xrange(0, len(extra_volume_ids), 200)): - extra_volumes = self._conn.get_all_volumes( - filters = { 'volume-id': chunk } + extra_volumes = self._conn.describe_volumes( + Filters=[{"Name": "volume-id", "Values": chunk}] ) - for vol in extra_volumes: - volumes[vol.id] = vol + for vol in extra_volumes["Volumes"]: + volumes[vol["VolumeId"]] = vol log.debug('Snapshot list >%s<', snapshots) total_snaps = len(snapshots) @@ -271,10 +304,10 @@ def tag_snapshots(self, volumes): for attempt in range(5): try: self.tag_snapshot(snapshot, volumes) - except boto.exception.EC2ResponseError, e: + except botocore.exceptions.ClientError as e: log.error("Encountered Error %s on snapshot %s", e.error_code, snapshot.id) break - except boto.exception.BotoServerError, e: + except (botocore.exceptions.EndpointConnectionError, botocore.exceptions.ConnectionClosedError) as e: log.error("Encountered Error %s on snapshot %s, waiting %d seconds then retrying", e.error_code, snapshot.id, attempt) time.sleep(attempt) else: @@ -287,21 +320,29 @@ def tag_snapshots(self, volumes): def tag_snapshot(self, snapshot, volumes): ''' Tags a specific snapshot ''' - volume_id = snapshot.volume_id + volume_id = snapshot["VolumeId"] + volume_tags = [] if volume_id not in volumes: - log.info("Snapshot %s volume %s not found. Snapshot will not be tagged", snapshot.id, volume_id) + log.info("Snapshot %s volume %s not found. Snapshot will not be tagged", snapshot["SnapshotId"], volume_id) return - volume_tags = volumes[volume_id].tags + if "Tags" in volumes[volume_id]: + for volume_tag_set in volumes[volume_id]["Tags"]: + volume_tag_key = volume_tag_set["Key"] + volume_tag_value = volume_tag_set["Value"] + volume_tags.append({"Key": volume_tag_key, "Value": volume_tag_value}) tags_to_set = {} if self._append: tags_to_set = snapshot.tags for tag_name in self._volume_tags_to_propagate: log.debug('Trying to propagate volume tag: %s', tag_name) - if tag_name in volume_tags: - tags_to_set[tag_name] = volume_tags[tag_name] + + for tag_set in volume_tags: + if tag_name == tag_set["Key"]: + value = tag_set["Value"] + tags_to_set[tag_name] = value # Set default tags for snapshot for tag in self._snapshot_tags_to_be_set: @@ -309,31 +350,52 @@ def tag_snapshot(self, snapshot, volumes): tags_to_set[tag['key']] = tag['value'] if self._dryrun: - log.info('DRYRUN: Snapshot %s would have been tagged %s', snapshot.id, tags_to_set) + log.info('DRYRUN: Snapshot %s would have been tagged %s', snapshot["SnapshotId"], tags_to_set) else: - self._set_resource_tags(snapshot, tags_to_set) + self._set_resource_tags(snapshot, "SnapshotId", tags_to_set) return True - def _set_resource_tags(self, resource, tags): + def _set_resource_tags(self, resource, resource_id, tags): ''' Sets the tags on the given AWS resource ''' - if not isinstance(resource, ec2.ec2object.TaggedEC2Object): - msg = 'Resource %s is not an instance of TaggedEC2Object' % resource - raise GraffitiMonkeyException(msg) - + resource_tags = {} delta_tags = {} - for tag_key, tag_value in tags.iteritems(): - if not tag_key in resource.tags or resource.tags[tag_key] != tag_value: - delta_tags[tag_key] = tag_value + if "Tags" in resource: + for tag_set in resource["Tags"]: + resource_tags[tag_set["Key"]] = tag_set["Value"] + for tag_key, tag_value in tags.iteritems(): + if not tag_key in resource_tags or resource_tags[tag_key] != tag_value: + delta_tags[tag_key] = tag_value + else: + delta_tags = tags if len(delta_tags) == 0: return - log.info('Tagging %s with [%s]', resource.id, delta_tags) - resource.add_tags(delta_tags) + log.info('Tagging %s with [%s]', resource[resource_id], delta_tags) + boto3_formatted_tags = [] + for key in delta_tags.keys(): + boto3_formatted_tags.append({ 'Key': key, 'Value' : delta_tags[key]}) + self._conn.create_tags(Resources=[resource[resource_id]], Tags=boto3_formatted_tags) + # Need to replace tags in the resource variable + if "Tags" not in resource: + resource["Tags"] = boto3_formatted_tags + else: + resource_keys = [] + for tag_set in resource["Tags"]: + resource_keys.append(tag_set["Key"]) + + for key in delta_tags.keys(): + tag_key = key + tag_value = delta_tags[key] + if tag_key in resource_keys: + tag_index = resource_keys.index(tag_key) + resource["Tags"][tag_index] = {"Key": tag_key, "Value": tag_value} + else: + resource["Tags"].append({"Key": tag_key, "Value": tag_value}) class Logging(object): @@ -354,8 +416,8 @@ def configure(self, verbosity = None): # Configure Boto's logging output if verbosity >= 4: - logging.getLogger('boto').setLevel(logging.DEBUG) + logging.getLogger('boto3').setLevel(logging.DEBUG) elif verbosity >= 3: - logging.getLogger('boto').setLevel(logging.INFO) + logging.getLogger('boto3').setLevel(logging.INFO) else: - logging.getLogger('boto').setLevel(logging.CRITICAL) + logging.getLogger('boto3').setLevel(logging.CRITICAL) diff --git a/requirements.txt b/requirements.txt index c500146..6b7639f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -boto>=2.7 +boto3>=1.3.1 +botocore>=1.4.41 # Following is only needed when using a yaml config file PyYAML>=3.11