Deleting VM ports on cluster delete flow

- modified delete port task to take a list of ports and delete all
  specified ports as well as taking a single port
- delete port task also does not throw an exception upon failure to
  delete a port, it instead logs a warning
- modified delete cluster flow to retrieve a list of ports for a VM and
  attempt to delete each port after deleting the VM

Change-Id: Ib311b23fdf3a58bc4b79257ed412e9d53d039013
closes-bug: 1448140
This commit is contained in:
dagnello 2015-05-13 15:05:11 -07:00 committed by Min Pae
parent 440bbf9eac
commit 0dc7db6e80
9 changed files with 287 additions and 22 deletions

View File

@ -19,6 +19,7 @@ import cue.client as client
from cue.db.sqlalchemy import models
import cue.taskflow.task as cue_tasks
import os_tasklib.common as os_common
import os_tasklib.neutron as neutron
import os_tasklib.nova as nova
@ -40,6 +41,7 @@ def delete_cluster_node(cluster_id, node_number, node_id):
node_name = "cluster[%s].node[%d]" % (cluster_id, node_number)
extract_vm_id = lambda node: node['instance_id']
extract_port_ids = lambda interfaces: [i['port_id'] for i in interfaces]
deleted_node_values = {'status': models.Status.DELETED,
'deleted': True}
@ -57,10 +59,24 @@ def delete_cluster_node(cluster_id, node_number, node_id):
name="extract vm id %s" % node_name,
rebind={'node': "node_%d" % node_number},
provides="vm_id_%d" % node_number),
nova.ListVmInterfaces(
os_client=client.nova_client(),
name="list vm interfaces %s" % node_name,
rebind={'server': "vm_id_%d" % node_number},
provides="vm_interfaces_%d" % node_number),
os_common.Lambda(
extract_port_ids,
name="extract port ids %s" % node_name,
rebind={'interfaces': "vm_interfaces_%d" % node_number},
provides="vm_port_list_%d" % node_number),
nova.DeleteVm(
os_client=client.nova_client(),
name="delete vm %s" % node_name,
rebind={'server': "vm_id_%d" % node_number}),
neutron.DeletePorts(
os_client=client.neutron_client(),
name="delete vm %s ports" % node_name,
rebind={'port_ids': "vm_port_list_%d" % node_number}),
cue_tasks.UpdateNode(
name="update node %s" % node_name,
inject={'node_id': node_id,

View File

@ -96,7 +96,7 @@ class NeutronClient(base.BaseFixture):
body['port']['id'] = port_id
body['port']['fixed_ips'] = []
body['port']['fixed_ips'].append({'ip_address': '0.0.0.0'})
self._port_list[port_id] = body['port']
self._port_list[port_id] = body['port'].copy()
return body
def create_network(self, body=None):

View File

@ -23,12 +23,14 @@ import cue.tests.functional.fixtures.base as base
class VmDetails(object):
def __init__(self, vm_id, name, flavor, image, status=None):
def __init__(self, vm_id, name, flavor, image,
port_list=None, status=None):
self.id = vm_id
self.name = name
self.flavor = flavor
self.image = image
self.status = status if status else 'ACTIVE'
self.port_list = port_list
def to_dict(self):
return {
@ -69,6 +71,12 @@ class FlavorDetails(object):
self.vcpus = vcpus or 2
class InterfaceDetails(object):
def __init__(self, net_id=None, port_id=None):
self.net_id = net_id
self.port_id = port_id
class VmStatusDetails(object):
vm_status_list = []
@ -146,6 +154,7 @@ class NovaClient(base.BaseFixture):
v2_client.servers.delete = self.delete_vm
v2_client.servers.get = self.get_vm
v2_client.servers.list = self.list_vms
v2_client.servers.interface_list = self.list_interfaces
v2_client.images.find = self.find_images
v2_client.images.list = self.list_images
v2_client.flavors.find = self.find_flavors
@ -178,6 +187,7 @@ class NovaClient(base.BaseFixture):
if image_id not in self._image_list:
raise nova_exc.BadRequest(400)
port_list = list()
if nics is not None:
neutron_client = client.neutron_client()
for nic in nics:
@ -188,8 +198,22 @@ class NovaClient(base.BaseFixture):
'networks' not in network_list or
len(network_list['networks']) == 0):
raise nova_exc.BadRequest(400)
else:
body_value = {
"port": {
"admin_state_up": True,
"name": "test port",
"network_id": nic['net-id'],
}
}
port_info = neutron_client.create_port(
body=body_value)
port_id = port_info['port']['id']
port_list.append(InterfaceDetails(net_id=nic['net-id'],
port_id=port_id))
if 'port-id' in nic:
pass
port_list.append(InterfaceDetails(port_id=nic['port-id']))
if security_groups is not None:
missing = set(security_groups) - set(self._security_group_list)
@ -198,6 +222,7 @@ class NovaClient(base.BaseFixture):
newVm = VmDetails(vm_id=uuid.uuid4(), name=name,
flavor=flavor, image=image,
port_list=port_list,
status='BUILDING')
self._vm_list[str(newVm.id)] = newVm
@ -241,7 +266,7 @@ class NovaClient(base.BaseFixture):
return server
def list_vms(self, retrieve_all=True, **_params):
def list_vms(self, retrieve_all=True, **kwargs):
"""Mock'd version of novaclient...list_vms().
List available vms.
@ -263,7 +288,7 @@ class NovaClient(base.BaseFixture):
if image_detail.name == name:
return image_detail
def list_images(self, retrieve_all=True, **_params):
def list_images(self, retrieve_all=True, **kwargs):
"""Mock'd version of novaclient...list_images().
List available images.
@ -283,4 +308,25 @@ class NovaClient(base.BaseFixture):
"""
for flavor_detail in self._flavor_list.itervalues():
if flavor_detail.name == name:
return flavor_detail
return flavor_detail
def list_interfaces(self, server, **kwargs):
"""Mock'd version of novaclient...interface_list().
Lists all interfaces attached to the specified Nova VM.
:param server:
:param kwargs:
:return:
"""
try:
server_id = server.id
except AttributeError:
server_id = server
try:
server = self._vm_list[str(server_id)]
except KeyError:
raise nova_exc.NotFound(404)
return server.port_list

View File

@ -53,23 +53,23 @@ class DeletePortTests(base.FunctionalTestCase):
# Delete port using DeletePort task
self.flow = linear_flow.Flow('create port').add(
neutron_task.DeletePort(os_client=self.neutron_client))
neutron_task.DeletePorts(os_client=self.neutron_client))
def test_delete_existing_port(self):
def test_delete_existing_port_single(self):
# create port
body_value = {
"port": {
"admin_state_up": True,
"name": "test port",
"network_id": self.valid_network['id'],
}
}
}
port_info = self.neutron_client.create_port(body=body_value)
port_id = port_info['port']['id']
# populate task_store with port-id of port created for this test
task_store = {
'port_id': port_id
'port_ids': port_id
}
# retrieve port list prior to delete
@ -89,12 +89,50 @@ class DeletePortTests(base.FunctionalTestCase):
"port-id %s found in neutron port after "
"delete" % port_id)
def test_delete_existing_port_multi(self):
# create port
body_value = {
"port": {
"admin_state_up": True,
"name": "test port",
"network_id": self.valid_network['id'],
}
}
port_id_list = list()
for i in range(0, 10):
port_info = self.neutron_client.create_port(body=body_value)
port_id = port_info['port']['id']
port_id_list.append(port_id)
# populate task_store with port-id of port created for this test
task_store = {
'port_ids': port_id_list
}
# retrieve port list prior to delete
pre_port_list = self.neutron_client.list_ports()
# search for created port in port list
for port_id in port_id_list:
self.assertEqual(True, _find_port(port_id, pre_port_list),
"port-id %s not found" % port_id)
engines.run(self.flow, store=task_store)
# retrieve port list after delete
post_port_list = self.neutron_client.list_ports()
# search for deleted port in port list
self.assertEqual(False, _find_port(port_id, post_port_list),
"port-id %s found in neutron port after "
"delete" % port_id)
def test_delete_nonexistent_port(self):
# generate random port_id
port_id = uuid.uuid4().hex
task_store = {
'port_id': port_id
'port_ids': port_id
}
# retrieve current port list

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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.
import uuid
from cue import client
from cue.tests.functional import base
from cue.tests.functional.fixtures import neutron
from cue.tests.functional.fixtures import nova
import os_tasklib.nova.list_vm_interfaces as list_vm_interfaces
from taskflow import engines
from taskflow.patterns import linear_flow
class GetVmInterfacesTests(base.FunctionalTestCase):
additional_fixtures = [
neutron.NeutronClient,
nova.NovaClient,
]
def setUp(self):
super(GetVmInterfacesTests, self).setUp()
flavor_name = "m1.tiny"
self.nova_client = client.nova_client()
self.neutron_client = client.neutron_client()
self.valid_vm_name = str(uuid.uuid4())
image_list = self.nova_client.images.list()
for image in image_list:
if (image.name.startswith("cirros")) and (
image.name.endswith("kernel")):
break
valid_image = image
valid_flavor = self.nova_client.flavors.find(name=flavor_name)
network_name = "private"
networks = self.neutron_client.list_networks(name=network_name)
network = networks['networks'][0]
self.valid_net_id = network['id']
nics = [{'net-id': self.valid_net_id}]
new_vm = self.nova_client.servers.create(name=self.valid_vm_name,
image=valid_image,
flavor=valid_flavor,
nics=nics)
self.valid_vm_id = new_vm.id
self.flow = linear_flow.Flow("create vm flow")
self.flow.add(
list_vm_interfaces.ListVmInterfaces(
os_client=self.nova_client,
requires=('server',),
provides=('interface_list')
)
)
def test_get_valid_vm_interfaces(self):
flow_store = {
'server': self.valid_vm_id
}
result = engines.run(self.flow, store=flow_store)
interface_list = result['interface_list']
self.assertEqual(self.valid_net_id, interface_list[0]['net_id'])
def tearDown(self):
if self.valid_vm_id is not None:
self.nova_client.servers.delete(self.valid_vm_id)
super(GetVmInterfacesTests, self).tearDown()

View File

@ -15,4 +15,4 @@
from create_port import CreatePort # noqa
from delete_port import DeletePort # noqa
from delete_ports import DeletePorts # noqa

View File

@ -13,7 +13,13 @@
# License for the specific language governing permissions and limitations
# under the License.
from cue.common.i18n import _LW # noqa
import collections
import neutronclient.common.exceptions as neutron_exc
from oslo_log import log as logging
import six
import os_tasklib
@ -21,19 +27,31 @@ import os_tasklib
LOG = logging.getLogger(__name__)
class DeletePort(os_tasklib.BaseTask):
class DeletePorts(os_tasklib.BaseTask):
"""DeletePort Task
This task interfaces with Neutron API and creates a port based on the
This task interfaces with Neutron API and deletes a port based on the
parameters provided to the Task.
"""
def execute(self, port_id, **kwargs):
def execute(self, port_ids, **kwargs):
"""Main execute method
:param port_id: Port ID of the port being deleted
:type port_id: string
:param port_ids: Port ID of the port being deleted
:type port_id: string or list
"""
self.os_client.delete_port(port=port_id)
if (not isinstance(port_ids, six.string_types) and
isinstance(port_ids, collections.Iterable)):
for port_id in port_ids:
self._delete_port(port_id)
else:
self._delete_port(port_ids)
def _delete_port(self, port_id):
try:
self.os_client.delete_port(port=port_id)
except neutron_exc.NotFound:
LOG.warning(_LW("Port was not found %s") % port_id)

View File

@ -13,7 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from create_vm import CreateVm # noqa
from delete_vm import DeleteVm # noqa
from get_vm import GetVm # noqa
from get_vm_status import GetVmStatus # noqa
from create_vm import CreateVm # noqa
from delete_vm import DeleteVm # noqa
from get_vm import GetVm # noqa
from get_vm_status import GetVmStatus # noqa
from list_vm_interfaces import ListVmInterfaces # noqa

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# 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.
import os_tasklib
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
_INTERFACE_ATTRIBUTES = [
'fixed_ips',
'mac_addr',
'net_id',
'port_id',
'port_state'
]
class ListVmInterfaces(os_tasklib.BaseTask):
"""ListVmInterfaces Task
This task interfaces with Nova API and retrieves the list of interfaces
attached to the indicated VM.
"""
def execute(self, server, **kwargs):
"""Main execute method
:param server: vm id to get list of attached interfaces to
:type server: string
:return: list of interfaces
"""
raw_list = self.os_client.servers.interface_list(server=server)
# The interface returned by Nova API has circular references which
# break serialization of the list to storage, so a subset of the data
# is being extracted and returned.
interface_list = list()
for interface in raw_list:
interface_entry = {k: getattr(interface, k)
for k in _INTERFACE_ATTRIBUTES
if hasattr(interface, k)}
interface_list.append(interface_entry)
return interface_list