Support deletion of keypairs from AWS

Deletion of keypairs from OpenStack does not trigger a key pair deletion
from AWS. Listen to the keypair deletion notifications and delete the
key from AWS when one is received.

Also pulling in some of the local changes.

Change-Id: Iea466533a8a12c0acccf5b6bf08d99b4e7a1b622
Closes-Bug: #1716454
This commit is contained in:
Pushkar Acharya 2017-09-12 15:51:33 -07:00
parent 92ee8cde01
commit 16d4db46e8
5 changed files with 173 additions and 8 deletions

View File

@ -47,6 +47,7 @@ class EC2DriverTestCase(test.NoDBTestCase):
region_name=self.region_name, region_name=self.region_name,
group='AWS') group='AWS')
self.flags(api_servers=['http://localhost:9292'], group='glance') self.flags(api_servers=['http://localhost:9292'], group='glance')
self.flags(rabbit_port='5672')
self.conn = EC2Driver(None, False) self.conn = EC2Driver(None, False)
self.type_data = None self.type_data = None
self.project_id = 'fake' self.project_id = 'fake'

View File

@ -0,0 +1,83 @@
"""
Copyright 2016 Platform9 Systems Inc.
All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
"""
from moto import mock_ec2_deprecated
from nova import test
from nova.virt.ec2.keypair import KeyPairNotifications
import boto
import mock
class KeyPairNotificationsTestCase(test.NoDBTestCase):
@mock_ec2_deprecated
def setUp(self):
super(KeyPairNotificationsTestCase, self).setUp()
fake_access_key = 'aws_access_key'
fake_secret_key = 'aws_secret_key'
region_name = 'us-west-1'
region = boto.ec2.get_region(region_name)
self.fake_aws_conn = boto.ec2.EC2Connection(
aws_access_key_id=fake_access_key,
aws_secret_access_key=fake_secret_key,
region=region)
self.flags(rabbit_port=5672)
self.conn = KeyPairNotifications(self.fake_aws_conn,
transport='memory')
def test_handle_notification_create_event(self):
body = {'event_type': 'keypair.create.start'}
with mock.patch.object(boto.ec2.EC2Connection, 'delete_key_pair') \
as mock_delete:
self.conn.handle_notification(body, None)
mock_delete.assert_not_called()
def test_handle_notifications_no_event_type(self):
body = {}
with mock.patch.object(boto.ec2.EC2Connection, 'delete_key_pair') \
as mock_delete:
self.conn.handle_notification(body, None)
mock_delete.assert_not_called()
@mock_ec2_deprecated
def test_handle_notifications_delete_key(self):
fake_key_name = 'fake_key'
fake_key_data = 'fake_key_data'
self.fake_aws_conn.import_key_pair(fake_key_name, fake_key_data)
body = {'event_type': 'keypair.delete.start',
'payload': {
'key_name': fake_key_name
}
}
self.conn.handle_notification(body, None)
aws_keypairs = self.fake_aws_conn.get_all_key_pairs()
self.assertEqual(len(aws_keypairs), 0)
@mock_ec2_deprecated
def test_handle_notifications_delete_key_with_multiple_keys_in_aws(self):
fake_key_name_1 = 'fake_key_1'
fake_key_data_1 = 'fake_key_data_1'
fake_key_name_2 = 'fake_key_2'
fake_key_data_2 = 'fake_key_data_2'
self.fake_aws_conn.import_key_pair(fake_key_name_1, fake_key_data_1)
self.fake_aws_conn.import_key_pair(fake_key_name_2, fake_key_data_2)
body = {'event_type': 'keypair.delete.start',
'payload': {
'key_name': fake_key_name_1
}
}
self.conn.handle_notification(body, None)
aws_keypairs = self.fake_aws_conn.get_all_key_pairs()
self.assertEqual(len(aws_keypairs), 1)
self.assertEqual(aws_keypairs[0].name, fake_key_name_2)

View File

@ -1,6 +1,6 @@
""" """
Copyright (c) 2014 Thoughtworks. Copyright (c) 2014 Thoughtworks.
Copyright (c) 2016 Platform9 Systems Inc. Copyright (c) 2017 Platform9 Systems Inc.
All Rights reserved All Rights reserved
Licensed under the Apache License, Version 2.0 (the "License"); you may Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain not use this file except in compliance with the License. You may obtain
@ -15,6 +15,7 @@ under the License.
import base64 import base64
import datetime import datetime
import eventlet
import hashlib import hashlib
import json import json
import time import time
@ -34,12 +35,14 @@ from nova.i18n import _
from nova.image import glance from nova.image import glance
from nova.virt import driver from nova.virt import driver
from nova.virt.ec2.exception_handler import Ec2ExceptionHandler from nova.virt.ec2.exception_handler import Ec2ExceptionHandler
from nova.virt.ec2.keypair import KeyPairNotifications
from nova.virt import hardware from nova.virt import hardware
from nova.virt import virtapi from nova.virt import virtapi
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_service import loopingcall from oslo_service import loopingcall
eventlet.monkey_patch()
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
aws_group = cfg.OptGroup(name='AWS', aws_group = cfg.OptGroup(name='AWS',
@ -63,11 +66,12 @@ aws_opts = [
# 1 TB Storage # 1 TB Storage
cfg.IntOpt('max_disk_gb', cfg.IntOpt('max_disk_gb',
default=1024, default=1024,
help='Max storage in GB that can be used') help='Max storage in GB that can be used'),
cfg.BoolOpt('enable_keypair_notifications', default=True,
help='Listen to keypair delete notifications and act on them')
] ]
CONF = cfg.CONF CONF = cfg.CONF
# CONF.import_opt('my_ip', 'nova.netconf')
CONF.register_group(aws_group) CONF.register_group(aws_group)
CONF.register_opts(aws_opts, group=aws_group) CONF.register_opts(aws_opts, group=aws_group)
@ -121,7 +125,8 @@ EC2_FLAVOR_MAP = {
't2.micro': {'memory_mb': 1024.0, 'vcpus': 1}, 't2.micro': {'memory_mb': 1024.0, 'vcpus': 1},
't2.nano': {'memory_mb': 512.0, 'vcpus': 1}, 't2.nano': {'memory_mb': 512.0, 'vcpus': 1},
't2.small': {'memory_mb': 2048.0, 'vcpus': 1}, 't2.small': {'memory_mb': 2048.0, 'vcpus': 1},
'x1.32xlarge': {'memory_mb': 1998848.0, 'vcpus': 128} 'x1.32xlarge': {'memory_mb': 1998848.0, 'vcpus': 128},
't1.micro': {'memory_mb': 613.0, 'vcpus': 1},
} }
_EC2_NODES = None _EC2_NODES = None
@ -190,6 +195,9 @@ class EC2Driver(driver.ComputeDriver):
aws_region, aws_access_key_id=CONF.AWS.access_key, aws_region, aws_access_key_id=CONF.AWS.access_key,
aws_secret_access_key=CONF.AWS.secret_key) aws_secret_access_key=CONF.AWS.secret_key)
# Allow keypair deletion to be controlled by conf
if CONF.AWS.enable_keypair_notifications:
eventlet.spawn(KeyPairNotifications(self.ec2_conn).run)
LOG.info("EC2 driver init with %s region" % aws_region) LOG.info("EC2 driver init with %s region" % aws_region)
if _EC2_NODES is None: if _EC2_NODES is None:
set_nodes([CONF.host]) set_nodes([CONF.host])
@ -257,7 +265,7 @@ class EC2Driver(driver.ComputeDriver):
""" """
image_api = glance.get_default_image_service() image_api = glance.get_default_image_service()
image_meta = image_api._client.call(context, 2, 'get', image_meta = image_api._client.call(context, 2, 'get',
image_lacking_meta['id']) image_lacking_meta.id)
LOG.info("Calling _get_image_ami_id_from_meta Meta: %s" % image_meta) LOG.info("Calling _get_image_ami_id_from_meta Meta: %s" % image_meta)
try: try:
return image_meta['aws_image_id'] return image_meta['aws_image_id']
@ -372,6 +380,8 @@ class EC2Driver(driver.ComputeDriver):
instance['metadata'].update({'ec2_id': ec2_id}) instance['metadata'].update({'ec2_id': ec2_id})
ec2_instance_obj.add_tag("Name", instance['display_name']) ec2_instance_obj.add_tag("Name", instance['display_name'])
ec2_instance_obj.add_tag("openstack_id", instance['uuid']) ec2_instance_obj.add_tag("openstack_id", instance['uuid'])
ec2_instance_obj.add_tag("openstack_project_id", context.project_id)
ec2_instance_obj.add_tag("openstack_user_id", context.user_id)
self._uuid_to_ec2_instance[instance.uuid] = ec2_instance_obj self._uuid_to_ec2_instance[instance.uuid] = ec2_instance_obj
# Fetch Public IP of the instance if it has one # Fetch Public IP of the instance if it has one
@ -684,13 +694,13 @@ class EC2Driver(driver.ComputeDriver):
return True return True
def attach_interface(self, instance, image_meta, vif): def attach_interface(self, instance, image_meta, vif):
LOG.debug("******* ATTTACH INTERFACE *******") LOG.debug("AWS: Attaching interface", instance=instance)
if vif['id'] in self._interfaces: if vif['id'] in self._interfaces:
raise exception.InterfaceAttachFailed('duplicate') raise exception.InterfaceAttachFailed('duplicate')
self._interfaces[vif['id']] = vif self._interfaces[vif['id']] = vif
def detach_interface(self, instance, vif): def detach_interface(self, instance, vif):
LOG.debug("******* DETACH INTERFACE *******") LOG.debug("AWS: Detaching interface", instance=instance)
try: try:
del self._interfaces[vif['id']] del self._interfaces[vif['id']]
except KeyError: except KeyError:

70
nova/virt/ec2/keypair.py Normal file
View File

@ -0,0 +1,70 @@
"""
Copyright (c) 2014 Thoughtworks.
Copyright (c) 2017 Platform9 Systems Inc.
All Rights reserved
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied. See the
License for the specific language governing permissions and limitations
under the License.
"""
import eventlet
eventlet.monkey_patch()
from kombu import Connection
from kombu import Exchange
from kombu import Queue
from kombu.mixins import ConsumerMixin
from oslo_config import cfg
from oslo_log import log as logging
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
rabbit_opts = [
cfg.StrOpt('rabbit_userid'),
cfg.StrOpt('rabbit_password'),
cfg.StrOpt('rabbit_host'),
cfg.StrOpt('rabbit_port'),
]
CONF.register_opts(rabbit_opts)
class KeyPairNotifications(ConsumerMixin):
nova_exchange = 'nova'
routing_key = 'notifications.info'
queue_name = 'notifications.omni.keypair'
events_of_interest = ['keypair.delete.start', 'keypair.delete.end']
def __init__(self, aws_connection, transport='amqp'):
self.ec2_conn = aws_connection
self.broker_uri = \
"{transport}://{username}:{password}@{rabbit_host}:{rabbit_port}"\
.format(transport=transport,
username=CONF.rabbit_userid,
password=CONF.rabbit_password,
rabbit_host=CONF.rabbit_host,
rabbit_port=CONF.rabbit_port)
self.connection = Connection(self.broker_uri)
def get_consumers(self, consumer, channel):
exchange = Exchange(self.nova_exchange, type="topic", durable=False)
queue = Queue(self.queue_name, exchange, routing_key=self.routing_key,
durable=False, auto_delete=True, no_ack=True)
return [consumer(queue, callbacks=[self.handle_notification])]
def handle_notification(self, body, message):
if 'event_type' in body and body['event_type'] in \
self.events_of_interest:
LOG.debug('Body: %r' % body)
key_name = body['payload']['key_name']
try:
LOG.info('Deleting %s keypair', key_name)
self.ec2_conn.delete_key_pair(key_name)
except:
LOG.exception('Could not delete %s', key_name)

View File

@ -25,6 +25,7 @@ DIRECTORY="$WORKSPACE/openstack"
GCE_TEST="test_gce" GCE_TEST="test_gce"
AWS_TEST="test_ec2" AWS_TEST="test_ec2"
AWS_NOVA_TEST="test_ec2.EC2DriverTestCase" AWS_NOVA_TEST="test_ec2.EC2DriverTestCase"
AWS_KEYPAIR_TEST="test_keypair.KeyPairNotificationsTestCase"
declare -A results declare -A results
declare -i fail declare -i fail
declare -i pass declare -i pass
@ -87,7 +88,7 @@ copy_neutron_files
echo "============Running tests============" echo "============Running tests============"
run_tests cinder "$GCE_TEST|$AWS_TEST" & run_tests cinder "$GCE_TEST|$AWS_TEST" &
run_tests nova "$GCE_TEST|$AWS_NOVA_TEST" & run_tests nova "$GCE_TEST|$AWS_NOVA_TEST|$AWS_KEYPAIR_TEST" &
run_tests glance_store "$GCE_TEST" & run_tests glance_store "$GCE_TEST" &
run_tests neutron "$GCE_TEST|$AWS_TEST" & run_tests neutron "$GCE_TEST|$AWS_TEST" &
wait wait