Networking resource management tool

- Adding the networking Resources tool for managing
  networking related resources like: servers, keypairs
  networks, subnets, ports and security groups.
  Managing involves listing and deleting resources
- Updating the delete_keypairs, wait_for_servers_to_be_deleted
  and _wait_for_compute_delete networking behavior methods
  for better use by the Resources tool.
- Updating the check_response networking base behavior method
  to make optional the check for entity.
- Adding the get_name_list_from_entity_list base behavior method
- Updating the DELETE_KEYPAIR constant

Change-Id: Ifa78e8d59543fbe3721d9e61f44d2e65a9ba884f
This commit is contained in:
Leonardo Maycotte 2016-09-03 21:46:13 -05:00
parent d0863e0b16
commit de93ab7732
6 changed files with 477 additions and 11 deletions

View File

@ -521,14 +521,20 @@ class NetworkingBehaviors(NetworkingBaseBehaviors):
keypair_list = self.list_keypairs(name=name)
log_msg = 'Deleting keypairs: {0}'.format(keypair_list)
self._log.info(log_msg)
failed_deletes = []
if keypair_list:
for key in keypair_list:
resp = self.compute.keypairs.client.delete_keypair(key.name)
resp_check = self.check_response(
resp=resp, status_code=ComputeResponseCodes.DELETE_KEYPAIR,
label=key.name, message='Keypair DELETE failure')
if resp_check and raise_exception:
raise UnableToDeleteKeypair(resp_check)
label=key.name, message='Keypair DELETE failure',
entity_check=False)
if resp_check:
failed_deletes.append(resp_check)
if raise_exception:
raise UnableToDeleteKeypair(resp_check)
return failed_deletes
def wait_for_servers_to_be_active(self, server_id_list,
interval_time=None, timeout=None,
@ -564,26 +570,42 @@ class NetworkingBehaviors(NetworkingBaseBehaviors):
if raise_exception:
raise TimeoutException(msg)
def wait_for_servers_to_be_deleted(self, server_id_list,
def wait_for_servers_to_be_deleted(self, server_id_list=None, name=None,
interval_time=None, timeout=None,
raise_exception=False):
"""
@summary: Waits for multiple servers to be deleted
@param server_id_list: The uuids of the servers to be deleted
@type server_id_list: List
@param name: name or name_starts_with* to filter by
@type name: str
@param interval_time: Seconds to wait between polling
@type interval_time: Integer
@param timeout: The amount of time in seconds to wait before aborting
@type timeout: Integer
@param raise_exception: flag to raise an exception if delete fails
@type raise_exception: bool
"""
self._wait_for_compute_delete(
if self.compute is None:
raise UnavailableComputeInteractionException
if not server_id_list:
if not name:
raise MissingDataException('Missing name pattern')
server_id_list = self.list_server_ids(name=name)
log_msg = 'Deleting servers: {0}'.format(server_id_list)
self._log.info(log_msg)
failed_deletes = self._wait_for_compute_delete(
resource_id_list=server_id_list, resource='servers',
delete_method=self.compute.servers.client.delete_server,
get_method=self.compute.servers.client.get_server,
interval_time=interval_time, timeout=timeout,
raise_exception=raise_exception)
return failed_deletes
def _wait_for_compute_delete(self, resource_id_list, resource,
delete_method, get_method, interval_time=None,
timeout=None, raise_exception=False):
@ -609,6 +631,7 @@ class NetworkingBehaviors(NetworkingBaseBehaviors):
self.compute.servers.config.server_status_interval)
timeout = timeout or self.compute.servers.config.server_build_timeout
end_time = time.time() + timeout
failed_deletes = []
for resource_id in resource_id_list:
delete_method(resource_id)
@ -619,19 +642,31 @@ class NetworkingBehaviors(NetworkingBaseBehaviors):
if (resp.status_code == ComputeResponseCodes.NOT_FOUND and
resource_id in resource_id_list):
resource_id_list.remove(resource_id)
# Retrying to delete servers in ERROR status
elif (hasattr(resp, 'entity') and
hasattr(resp.entity, 'status') and
resp.entity.status == ComputeStatus.ERROR):
delete_method(resource_id)
if not resource_id_list:
break
time.sleep(interval_time)
else:
msg = ('Wait for compute {0} resource delete {0} seconds timeout '
'for the expected resource get HTTP {1} status code for '
'resources: {2}').format(resource, timeout,
msg = ('Wait for compute {0} resource delete {1} seconds timeout '
'for the expected resource get HTTP {2} status code for '
'resources: {3}').format(resource, timeout,
ComputeResponseCodes.NOT_FOUND,
resource_id_list)
self._log.info(msg)
failed_deletes.append(msg)
if raise_exception:
raise TimeoutException(msg)
# keeping the response consistent with the _delete_resources of the
# networking.networks.common.behaviors (failed_deletes)
return failed_deletes
def create_multiple_servers(self, create_by='names', names=None,
images=None, flavors=None,
keypair_name=None, networks=None, ports=None,

View File

@ -48,7 +48,7 @@ class NetworkingBaseBehaviors(BaseBehavior):
self.config = NetworkingBaseConfig()
def check_response(self, resp, status_code, label, message,
network_id=None):
network_id=None, entity_check=True):
"""
@summary: Checks the API response object
@param resp: API call response object
@ -61,6 +61,8 @@ class NetworkingBaseBehaviors(BaseBehavior):
@type message: string
@param network_id: related Network ID (optional)
@type network_id: string
@param entity_check: flag to enable/disable the response entity check
@type entity_check: bool
@return: None if the response is the expected or the error message
@rtype: None or string
"""
@ -89,11 +91,13 @@ class NetworkingBaseBehaviors(BaseBehavior):
expected_status=status_code)
self._log.error(err_msg)
response_msg = err_msg
elif not resp.entity:
elif not resp.entity and entity_check:
err_msg = ('{label} {message}: Unable to get response'
' entity object').format(label=label, message=message)
self._log.error(err_msg)
response_msg = err_msg
elif not entity_check:
response_msg = None
else:
# This should NOT happen, scenarios should be covered by the elifs
@ -159,6 +163,21 @@ class NetworkingBaseBehaviors(BaseBehavior):
id_list = [entity.id for entity in entity_list]
return id_list
def get_name_list_from_entity_list(self, entity_list, name=None):
"""
@summary: Gets a name list from an entity list
@param entity_list: List of instances with the name and id attributes
@type entity_list: list(instances)
@param name: (optional) name or name_starts_with* to filter by
@type name: str
@return: name list
@rtype: list
"""
if name:
entity_list = self.filter_entity_list_by_name(entity_list, name)
name_list = [entity.name for entity in entity_list]
return name_list
def __over_limit_retry(self, resource_type, timeout, poll_interval,
status_code, resp, fn_name, fn_kwargs):
"""

View File

@ -13,6 +13,10 @@ 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 json
from cloudcafe.common.models.configuration import ConfigSectionInterface
@ -115,3 +119,11 @@ class NetworkingBaseConfig(ConfigSectionInterface):
def accepted_packet_loss(self):
"""Accepted packet loss percent for server pings"""
return int(self.get("accepted_packet_loss", 0))
@property
def delete_resources(self):
"""
JSON string that may be used by the delete_networking resource_dict
at: networking.networks.common.tools.resources
"""
return json.loads(self.get("delete_resources", '{}'))

View File

@ -115,7 +115,7 @@ class ComputeResponseCodes(object):
CREATE_KEYPAIR = 200
GET_KEYPAIR = 200
LIST_KEYPAIRS = 200
DELETE_KEYPAIR = 204
DELETE_KEYPAIR = 202
class ComputeStatus(NovaServerStatusTypes):

View File

@ -0,0 +1,15 @@
"""
Copyright 2016 Rackspace
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.
"""

View File

@ -0,0 +1,385 @@
"""
Copyright 2016 Rackspace
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 collections
import os
from prettytable import PrettyTable
from cloudcafe.networking.networks.common.composites import CustomComposite
from cloudcafe.networking.networks.common.exceptions \
import MissingDataException
class Resources(object):
"""Networking resource management class for list and delete methods.
Attributes:
resources (list): networking resources to manage, for ex.
['servers', 'keypairs', 'security_groups',
'ports', 'subnets', 'networks']
all (str): name pattern to be used by all resources. Except for the
resources attributes with value.
servers (str): name pattern used for servers if given.
keypairs (str): name pattern used for keypairs if given.
networks (str): name pattern used for networks if given.
subnets (str): name pattern used for subnets if given.
ports (str): name pattern used for ports if given.
security_groups (str): name pattern used for security groups if given.
failed_servers (list): for tracking delete failures on delete calls.
failed_keypairs (list): for tracking delete failures on delete calls.
failed_networks (list): for tracking delete failures on delete calls.
failed_subnets (list): for tracking delete failures on delete calls.
failed_ports (list): for tracking delete failures on delete calls.
failed_security_groups (list): for tracking delete failures on delete.
The class attributes are set after the set_resources call.
"""
# Key names for resources composite behaviors object and get/delete methods
RES_COM = 'res_com'
GET_FN = 'get_fn'
DEL_FN = 'del_fn'
def __init__(self):
self.resources = None
def set_resources(self, resources, initial_val=None):
"""Setting resource values and failed list as class attributes
Args:
resources (list): resources list for ex.
['servers', 'networks', ...]
"""
self.resources = resources
# Initialize the resources as class attributes with None
# And the resource delete lists as empty lists
self.set_resources_val(initial_val)
self.set_failed_deletes()
def set_resources_val(self, val):
"""Setting attribute values to capture resource info
Args:
val(str): values of class attributes listed in self.resources
"""
for resource in self.resources:
setattr(self, resource, val)
def set_failed_deletes(self):
"""Resetting attribute lists to track resource failed deletes"""
for resource in self.resources:
delete_list = 'failed_{0}'.format(resource)
setattr(self, delete_list, list())
def get_config_file(self, file_path=None):
"""Getting the config file
Args:
file_path(Optional[str]): config file path
"""
if not file_path:
file_path = os.environ.get('CAFE_CONFIG_FILE_PATH')
if not file_path:
msg = ('The file_path must be given or the environment '
'variable CAFE_CONFIG_FILE_PATH must be set')
raise MissingDataException(msg)
if not file_path.endswith('.config'):
config_file = ''.join([file_path, '.config'])
else:
config_file = file_path
return config_file
def list_networking(self, name=None, file_path=None, verbose=True,
resource_show=None, resource_filter=None,
raise_exception=False):
"""Get method for networking related resources
Args:
name (Optional[str]): name pattern to filter by responses.
If not given all resources will be listed.
file_path (Optional[str]): config directory path with file name
(the .config extension may be omitted). If not given the
CAFE_CONFIG_FILE_PATH environment variable value will be used.
verbose (Optional[bool]): flag to print delete msgs to console.
resource_show (Optional[list]): resources to show only.
resource_filter (Optional[list]): resources to skip showing.
Ignored if resource_show given.
raise_exception (Optional [bool]): flag for raising an exception
if get calls throw an error.
Returns:
dict: ONLY if verbose is set to False, otherwise this info
is printed by the method call. The names and id counts by
resource are returned as well as a list of them on a table.
For ex.
{'subnets': {'table': <prettytable.PrettyTable object>
'names': 2, 'ids': 2},
'keypairs': {'table': <prettytable.PrettyTable object>,
'names': 0, 'ids': 0},
'networks': {'table': <prettytable.PrettyTable object>,
'names': 2, 'ids': 2},
'ports': {'table': <prettytable.PrettyTable object,
'names': 0, 'ids': 0},
'security_groups': {'table': <prettytable.PrettyTable object>,
'names': 0, 'ids': 0},
'servers': {'table': <prettytable.PrettyTable object>,
'names': 3, 'ids': 3}}
"""
config_file = self.get_config_file(file_path=file_path)
if verbose:
print 'Using config file {0}'.format(config_file)
# Setting the custom composite
com = CustomComposite(config_file_path=config_file)
com.set_cafe_config_file_path()
com.set_cafe_composites()
# Defining resources get calls.
res_fn = collections.OrderedDict([
('servers', {self.RES_COM: com.net.behaviors,
self.GET_FN: 'list_servers'}),
('keypairs', {self.RES_COM: com.net.behaviors,
self.GET_FN: 'list_keypairs'}),
('security_groups', {self.RES_COM: com.sec.behaviors,
self.GET_FN: 'list_security_groups'}),
('ports', {self.RES_COM: com.ports.behaviors,
self.GET_FN: 'list_ports'}),
('subnets', {self.RES_COM: com.subnets.behaviors,
self.GET_FN: 'list_subnets'}),
('networks', {self.RES_COM: com.networks.behaviors,
self.GET_FN: 'list_networks'})
])
resources = res_fn.keys()
resources_count = dict()
resource_set = set(resources)
if resource_show:
resources = list(resource_set.intersection(resource_show))
elif resource_filter:
resource_filter = list(resource_set.intersection(resource_filter))
filter(resources.remove, resource_filter)
output_list = []
for resource in resources:
resource_table = PrettyTable()
count = dict()
res_com = res_fn[resource][self.RES_COM]
get_fn = res_fn[resource][self.GET_FN]
params_kwargs = dict(raise_exception=raise_exception)
resp = getattr(res_com, get_fn)(**params_kwargs)
# Extracting the nested requests response object in networking
# resources. This is not needed for the compute resources.
if resource in ['networks', 'subnets', 'ports', 'security_groups']:
resp = resp.response.entity
name_list = res_com.get_name_list_from_entity_list(
entity_list=resp, name=name)
resource_name = '{0}_name'.format(resource)
resource_table.add_column(resource_name, name_list)
names_count = len(name_list)
count.update({'names': names_count})
# For resources without ids like Keypairs
if resource not in ['keypairs']:
id_list = res_com.get_id_list_from_entity_list(
entity_list=resp, name=name)
resource_id = '{0}_id'.format(resource)
resource_table.add_column(resource_id, id_list)
else:
id_list = []
ids_count = len(id_list)
count.update({'ids': ids_count})
resource_table.align = 'l'
output_list.append(resource_table)
count.update({'table': resource_table})
resources_count.update({resource: count})
if verbose:
print resource_table
if verbose:
print 'Resources Summary'
for k, v in resources_count.items():
report = '{0}: {1} with name and {2} with id'.format(
k, v['names'], v['ids'])
print report
note = ('NOTE: Some resources like ports, may have an empty string'
' as name. Others, like keypairs, may not have id')
print note
else:
# This is jut in case the printing is desired to be done elsewhere
return resources_count
def delete_networking(self, file_path=None, resource_dict=None,
timeout=None, raise_exception=None, verbose=True):
"""Clean up method for networking related resources
Args:
file_path (Optional[str]): config directory path with file name
(the .config extension may be omitted). If not given the
CAFE_CONFIG_FILE_PATH environment variable value will be used.
resource_dict (Optional[dict]): Resource names to be used for
deletion, all possible keys are,
{'all': 'test*', 'servers': 'test_svr*', 'keypairs': 'test*',
'security_group_rules': 'test*', 'security_groups': 'test*',
'ports': 'ports*', 'subnets': 'test*', 'networks': 'test*'}
The values here are just name pattern examples and should be
set to '*' for all or may have a resource start name like
'test*', that will delete all resources starting with test
on their name. You can also give a resource exact name, or
an non matching pattern name to skip resource deletions.
If the all key is given, all resources will be targeted for
deletion with its value as the name pattern to be used.
If other resource keys are given, in combination with the all
key, these resource values will be used for their name pattern.
If the all key is not given, only resource given keys will be
targeted for deletion.
If the resource dict is not given at all, the default value
will be {'all': '*'} that will delete ALL resources.
timeout (Optional[int]): wait time for server deletion. If not
given default val compute.servers.config.server_build_timeout
raise_exception (Optional [bool]): flag for raising an exception
if servers aren't deleted within the timeout.
verbose (Optional[bool]): flag to print delete msgs to console.
"""
# Defining initial values
config_file = self.get_config_file(file_path=file_path)
if verbose:
print 'Using config file {0}'.format(config_file)
if not resource_dict:
resource_dict = {'all': '*'}
# Setting the custom composite
com = CustomComposite(config_file_path=config_file)
com.set_cafe_config_file_path()
com.set_cafe_composites()
# Defining resources delete calls. Delete order is important.
res_fn = collections.OrderedDict([
('servers', {self.RES_COM: com.net.behaviors,
self.DEL_FN: 'wait_for_servers_to_be_deleted'}),
('keypairs', {self.RES_COM: com.net.behaviors,
self.DEL_FN: 'delete_keypairs'}),
('security_groups', {self.RES_COM: com.sec.behaviors,
self.DEL_FN: 'delete_security_groups'}),
('ports', {self.RES_COM: com.ports.behaviors,
self.DEL_FN: 'delete_ports'}),
('subnets', {self.RES_COM: com.subnets.behaviors,
self.DEL_FN: 'delete_subnets'}),
('networks', {self.RES_COM: com.networks.behaviors,
self.DEL_FN: 'delete_networks'})
])
resources = res_fn.keys()
# Getting the name value for all resources if given
all_resources = resource_dict.get('all')
# Setting the resources as class attrs with initial name value
self.set_resources(resources=resources, initial_val=all_resources)
# Overwriting individual resource name value if given
for k, v in resource_dict.items():
setattr(self, k, v)
for resource in resources:
# Getting resources name patterns from class attrs
name_pattern = getattr(self, resource)
if name_pattern:
if verbose:
msg = 'Deleting {0} with name: {1}'.format(resource,
name_pattern)
if resource == 'servers':
if timeout:
timeout_val = timeout
else:
timeout_val = (
'compute.servers.config.server_build_timeout')
add_msg = 'Within timeout: {0}'.format(timeout_val)
msg = '\n'.join([msg, add_msg])
print msg
res_com = res_fn[resource][self.RES_COM]
del_fn = res_fn[resource][self.DEL_FN]
params_kwargs = dict(name=name_pattern)
if resource == 'servers':
add_params = dict(timeout=timeout,
raise_exception=raise_exception)
params_kwargs.update(add_params)
failed_deletes = getattr(res_com, del_fn)(**params_kwargs)
if verbose and failed_deletes:
msg = '\nUnable to delete the following {0}'.format(
resource)
print msg
for failure in failed_deletes:
print failure
# Updating resource failure list in res object
failed_name = 'failed_{0}'.format(resource)
setattr(self, failed_name, failed_deletes)
if verbose:
print '\n'
print '*'*9
print 'Delete resource summary'
print '"*" implies names starts with'
print 'Resources list are all available resource options'
print 'Failed lists populated with failures if any\n'
print self
print '*'*9
def __repr__(self):
"""Representing resource attributes: str, int and list"""
data = self.__dict__
msg = ['Resources']
for key, value in data.items():
val_type = type(value)
if val_type is str or val_type is int or val_type is list:
s = '{0}: {1}'.format(key, value)
msg.append(s)
if len(msg) < 2:
msg.append('No resources have been set')
res = '\n'.join(msg)
return res