Commit c0649302 authored by Ilias Tsitsimpis's avatar Ilias Tsitsimpis Committed by Giorgos Korfiatis
Browse files

burnin: Rewrite check_quotas functionality

This patch enhances check_quotas functionality for better supporting
projects quotas.
parent 03583243
...@@ -63,6 +63,17 @@ KB = 2**10 ...@@ -63,6 +63,17 @@ KB = 2**10
MB = 2**20 MB = 2**20
GB = 2**30 GB = 2**30
QADD = 1
QREMOVE = -1
QDISK = "cyclades.disk"
QVM = "cyclades.vm"
QPITHOS = "pithos.diskspace"
QRAM = "cyclades.ram"
QIP = "cyclades.floating_ip"
QCPU = "cyclades.cpu"
QNET = "cyclades.network.private"
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# BurninTestResult class # BurninTestResult class
...@@ -217,6 +228,7 @@ class BurninTests(unittest.TestCase): ...@@ -217,6 +228,7 @@ class BurninTests(unittest.TestCase):
failfast = None failfast = None
quotas = Proper(value=None) quotas = Proper(value=None)
uuid = Proper(value=None)
@classmethod @classmethod
def setUpClass(cls): # noqa def setUpClass(cls): # noqa
...@@ -288,15 +300,17 @@ class BurninTests(unittest.TestCase): ...@@ -288,15 +300,17 @@ class BurninTests(unittest.TestCase):
def error(self, msg, *args): def error(self, msg, *args):
"""Pass the section value to logger""" """Pass the section value to logger"""
logger.error(self.suite_name, msg, *args) logger.error(self.suite_name, msg, *args)
self.fail(msg % args)
# ---------------------------------- # ----------------------------------
# Helper functions that every testsuite may need # Helper functions that every testsuite may need
def _get_uuid(self): def _get_uuid(self):
"""Get our uuid""" """Get our uuid"""
if self.uuid is None:
authenticate = self.clients.astakos.authenticate() authenticate = self.clients.astakos.authenticate()
uuid = authenticate['access']['user']['id'] self.uuid = authenticate['access']['user']['id']
self.info("User's uuid is %s", uuid) self.info("User's uuid is %s", self.uuid)
return uuid return self.uuid
def _get_username(self): def _get_username(self):
"""Get our User Name""" """Get our User Name"""
...@@ -337,7 +351,6 @@ class BurninTests(unittest.TestCase): ...@@ -337,7 +351,6 @@ class BurninTests(unittest.TestCase):
return su_value return su_value
else: else:
self.error("Unrecognized system-user type %s", su_type) self.error("Unrecognized system-user type %s", su_type)
self.fail("Unrecognized system-user type")
except ValueError: except ValueError:
msg = "Invalid system-user format: %s. Must be [id|name]:.+" msg = "Invalid system-user format: %s. Must be [id|name]:.+"
self.warning(msg, self.system_user) self.warning(msg, self.system_user)
...@@ -412,7 +425,6 @@ class BurninTests(unittest.TestCase): ...@@ -412,7 +425,6 @@ class BurninTests(unittest.TestCase):
[f for f in flavors if str(f['id']) == flv_value] [f for f in flavors if str(f['id']) == flv_value]
else: else:
self.error("Unrecognized flavor type %s", flv_type) self.error("Unrecognized flavor type %s", flv_type)
self.fail("Unrecognized flavor type")
# Append and continue # Append and continue
ret_flavors.extend(filtered_flvs) ret_flavors.extend(filtered_flvs)
...@@ -483,7 +495,6 @@ class BurninTests(unittest.TestCase): ...@@ -483,7 +495,6 @@ class BurninTests(unittest.TestCase):
i['id'].lower() == img_value.lower()] i['id'].lower() == img_value.lower()]
else: else:
self.error("Unrecognized image type %s", img_type) self.error("Unrecognized image type %s", img_type)
self.fail("Unrecognized image type")
# Append and continue # Append and continue
ret_images.extend(filtered_imgs) ret_images.extend(filtered_imgs)
...@@ -536,72 +547,36 @@ class BurninTests(unittest.TestCase): ...@@ -536,72 +547,36 @@ class BurninTests(unittest.TestCase):
# pylint: disable=invalid-name # pylint: disable=invalid-name
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def _check_quotas(self, puuid=None, disk=None, vm=None, diskspace=None, def _check_quotas(self, changes):
ram=None, ip=None, cpu=None, network=None):
"""Check that quotas' changes are consistent """Check that quotas' changes are consistent
@param puuid: The uuid of the project, quotas are assigned to @param changes: A dict of the changes that have been made in quotas
""" """
if not changes:
assert any(v is None for v in return
[disk, vm, diskspace, ram, ip, cpu, network]), \
"_check_quotas require arguments"
self.info("Check that quotas' changes are consistent") self.info("Check that quotas' changes are consistent")
old_quotas = self.quotas old_quotas = self.quotas
new_quotas = self._get_quotas() new_quotas = self._get_quotas()
self.quotas = new_quotas self.quotas = new_quotas
user_uuid = self._get_uuid()
if puuid is None:
puuid = user_uuid
self.assertListEqual(sorted(old_quotas.keys()), self.assertListEqual(sorted(old_quotas.keys()),
sorted(new_quotas.keys())) sorted(new_quotas.keys()))
for project in old_quotas.keys():
# Check Disk usage # Take old_quotas and apply changes
project_name = self._get_project_name(project, user_uuid) for prj, values in changes.items():
self._check_quotas_aux(old_quotas[project], new_quotas[project], self.assertIn(prj, old_quotas.keys())
project_name, "cyclades.disk", for q_name, q_mult, q_value, q_unit in values:
disk, project == puuid) if q_unit is None:
# Check VM usage q_unit = 1
self._check_quotas_aux(old_quotas[project], new_quotas[project], q_value = q_mult*int(q_value)*q_unit
project_name, "cyclades.vm", assert isinstance(q_value, int), \
vm, project == puuid) "Project %s: %s value has to be integer" % (prj, q_name)
# Check DiskSpace usage old_quotas[prj][q_name]['usage'] += q_value
self._check_quotas_aux(old_quotas[project], new_quotas[project], old_quotas[prj][q_name]['project_usage'] += q_value
project_name, "pithos.diskspace",
diskspace, project == puuid) self.assertEqual(old_quotas, new_quotas)
# Check Ram usage
self._check_quotas_aux(old_quotas[project], new_quotas[project],
project_name, "cyclades.ram",
ram, project == puuid)
# Check Floating IPs usage
self._check_quotas_aux(old_quotas[project], new_quotas[project],
project_name, "cyclades.floating_ip",
ip, project == puuid)
# Check CPU usage
self._check_quotas_aux(old_quotas[project], new_quotas[project],
project_name, "cyclades.cpu",
cpu, project == puuid)
# Check Network usage
self._check_quotas_aux(old_quotas[project], new_quotas[project],
project_name, "cyclades.network.private",
network, project == puuid)
def _check_quotas_aux(self, old_quotas, new_quotas,
project_name, resource, value, check):
"""Auxiliary function for _check_quotas"""
old_value = old_quotas[resource]['usage']
new_value = new_quotas[resource]['usage']
if check and value is not None:
assert isinstance(value, int), \
"%s value has to be integer" % resource
old_value += value
self.assertEqual(old_value, new_value,
"Project %s: %s quotas don't match" %
(project_name, resource))
# ---------------------------------- # ----------------------------------
# Projects # Projects
......
...@@ -49,12 +49,38 @@ import subprocess ...@@ -49,12 +49,38 @@ import subprocess
from kamaki.clients import ClientError from kamaki.clients import ClientError
from synnefo_tools.burnin.common import BurninTests, MB, GB from synnefo_tools.burnin.common import BurninTests, MB, GB, QADD, QREMOVE, \
QDISK, QVM, QRAM, QIP, QCPU, QNET
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
class CycladesTests(BurninTests): class CycladesTests(BurninTests):
"""Extends the BurninTests class for Cyclades""" """Extends the BurninTests class for Cyclades"""
def _parse_images(self):
"""Find images given to command line"""
if self.images is None:
self.info("No --images given. Will use the default %s",
"^Debian Base$")
filters = ["name:^Debian Base$"]
else:
filters = self.images
avail_images = self._find_images(filters)
self.info("Found %s images to choose from", len(avail_images))
return avail_images
def _parse_flavors(self):
"""Find flavors given to command line"""
flavors = self._get_list_of_flavors(detail=True)
if self.flavors is None:
self.info("No --flavors given. Will use all of them")
avail_flavors = flavors
else:
avail_flavors = self._find_flavors(self.flavors, flavors=flavors)
self.info("Found %s flavors to choose from", len(avail_flavors))
return avail_flavors
def _try_until_timeout_expires(self, opmsg, check_fun): def _try_until_timeout_expires(self, opmsg, check_fun):
"""Try to perform an action until timeout expires""" """Try to perform an action until timeout expires"""
assert callable(check_fun), "Not a function" assert callable(check_fun), "Not a function"
...@@ -113,10 +139,12 @@ class CycladesTests(BurninTests): ...@@ -113,10 +139,12 @@ class CycladesTests(BurninTests):
server['name'], server['id']) server['name'], server['id'])
return self.clients.cyclades.get_server_details(server['id']) return self.clients.cyclades.get_server_details(server['id'])
def _create_server(self, image, flavor, personality=None, network=False): # pylint: disable=too-many-arguments
def _create_server(self, image, flavor, personality=None,
network=False, project_id=None):
"""Create a new server""" """Create a new server"""
if network: if network:
fip = self._create_floating_ip() fip = self._create_floating_ip(project_id=project_id)
port = self._create_port(fip['floating_network_id'], port = self._create_port(fip['floating_network_id'],
floating_ip=fip) floating_ip=fip)
networks = [{'port': port['id']}] networks = [{'port': port['id']}]
...@@ -129,7 +157,8 @@ class CycladesTests(BurninTests): ...@@ -129,7 +157,8 @@ class CycladesTests(BurninTests):
self.info("Using flavor %s with id %s", flavor['name'], flavor['id']) self.info("Using flavor %s with id %s", flavor['name'], flavor['id'])
server = self.clients.cyclades.create_server( server = self.clients.cyclades.create_server(
servername, flavor['id'], image['id'], servername, flavor['id'], image['id'],
personality=personality, networks=networks) personality=personality, networks=networks,
project=project_id)
self.info("Server id: %s", server['id']) self.info("Server id: %s", server['id'])
self.info("Server password: %s", server['adminPass']) self.info("Server password: %s", server['adminPass'])
...@@ -138,12 +167,18 @@ class CycladesTests(BurninTests): ...@@ -138,12 +167,18 @@ class CycladesTests(BurninTests):
self.assertEqual(server['flavor']['id'], flavor['id']) self.assertEqual(server['flavor']['id'], flavor['id'])
self.assertEqual(server['image']['id'], image['id']) self.assertEqual(server['image']['id'], image['id'])
self.assertEqual(server['status'], "BUILD") self.assertEqual(server['status'], "BUILD")
if project_id is None:
project_id = self._get_uuid()
self.assertEqual(server['tenant_id'], project_id)
# Verify quotas # Verify quotas
self._check_quotas(disk=+int(flavor['disk'])*GB, changes = \
vm=+1, {project_id:
ram=+int(flavor['ram'])*MB, [(QDISK, QADD, flavor['disk'], GB),
cpu=+int(flavor['vcpus'])) (QVM, QADD, 1, None),
(QRAM, QADD, flavor['ram'], MB),
(QCPU, QADD, flavor['vcpus'], None)]}
self._check_quotas(changes)
return server return server
...@@ -180,26 +215,25 @@ class CycladesTests(BurninTests): ...@@ -180,26 +215,25 @@ class CycladesTests(BurninTests):
self.assertNotIn(srv['id'], new_servers) self.assertNotIn(srv['id'], new_servers)
# Verify quotas # Verify quotas
flavors = \ self._verify_quotas_deleted(servers)
[self.clients.compute.get_flavor_details(srv['flavor']['id'])
for srv in servers]
self._verify_quotas_deleted(flavors)
def _verify_quotas_deleted(self, flavors): def _verify_quotas_deleted(self, servers):
"""Verify quotas for a number of deleted servers""" """Verify quotas for a number of deleted servers"""
used_disk = 0 changes = dict()
used_vm = 0 for server in servers:
used_ram = 0 project = server['tenant_id']
used_cpu = 0 if project not in changes:
for flavor in flavors: changes[project] = []
used_disk += int(flavor['disk']) * GB flavor = \
used_vm += 1 self.clients.compute.get_flavor_details(server['flavor']['id'])
used_ram += int(flavor['ram']) * MB new_changes = [
used_cpu += int(flavor['vcpus']) (QDISK, QREMOVE, flavor['disk'], GB),
self._check_quotas(disk=-used_disk, (QVM, QREMOVE, 1, None),
vm=-used_vm, (QRAM, QREMOVE, flavor['ram'], MB),
ram=-used_ram, (QCPU, QREMOVE, flavor['vcpus'], None)]
cpu=-used_cpu) changes[project].extend(new_changes)
self._check_quotas(changes)
def _get_connection_username(self, server): def _get_connection_username(self, server):
"""Determine the username to use to connect to the server""" """Determine the username to use to connect to the server"""
...@@ -320,7 +354,7 @@ class CycladesTests(BurninTests): ...@@ -320,7 +354,7 @@ class CycladesTests(BurninTests):
"Can not get IPs from server attachments") "Can not get IPs from server attachments")
for addr in addrs: for addr in addrs:
self.assertEquals(IPy.IP(addr).version(), version) self.assertEqual(IPy.IP(addr).version(), version)
if network is None: if network is None:
msg = "Server's public IPv%s is %s" msg = "Server's public IPv%s is %s"
...@@ -411,21 +445,28 @@ class CycladesTests(BurninTests): ...@@ -411,21 +445,28 @@ class CycladesTests(BurninTests):
# ---------------------------------- # ----------------------------------
# Networks # Networks
def _create_network(self, cidr="10.0.1.0/28", dhcp=True): def _create_network(self, cidr="10.0.1.0/28", dhcp=True,
project_id=None):
"""Create a new private network""" """Create a new private network"""
name = self.run_id name = self.run_id
network = self.clients.network.create_network( network = self.clients.network.create_network(
"MAC_FILTERED", name=name, shared=False) "MAC_FILTERED", name=name, shared=False,
project=project_id)
self.info("Network with id %s created", network['id']) self.info("Network with id %s created", network['id'])
subnet = self.clients.network.create_subnet( subnet = self.clients.network.create_subnet(
network['id'], cidr=cidr, enable_dhcp=dhcp) network['id'], cidr=cidr, enable_dhcp=dhcp)
self.info("Subnet with id %s created", subnet['id']) self.info("Subnet with id %s created", subnet['id'])
# Verify quotas # Verify quotas
self._check_quotas(network=+1) if project_id is None:
project_id = self._get_uuid()
changes = \
{project_id: [(QNET, QADD, 1, None)]}
self._check_quotas(changes)
#Test if the right name is assigned #Test if the right name is assigned
self.assertEqual(network['name'], name) self.assertEqual(network['name'], name)
self.assertEqual(network['tenant_id'], project_id)
return network return network
...@@ -450,7 +491,9 @@ class CycladesTests(BurninTests): ...@@ -450,7 +491,9 @@ class CycladesTests(BurninTests):
self.assertNotIn(net['id'], new_networks) self.assertNotIn(net['id'], new_networks)
# Verify quotas # Verify quotas
self._check_quotas(network=-len(networks)) changes = \
{self._get_uuid(): [(QNET, QREMOVE, len(networks), None)]}
self._check_quotas(changes)
def _get_public_network(self, networks=None): def _get_public_network(self, networks=None):
"""Get the public network""" """Get the public network"""
...@@ -462,20 +505,27 @@ class CycladesTests(BurninTests): ...@@ -462,20 +505,27 @@ class CycladesTests(BurninTests):
return net return net
self.fail("Could not find a public network to use") self.fail("Could not find a public network to use")
def _create_floating_ip(self): def _create_floating_ip(self, project_id=None):
"""Create a new floating ip""" """Create a new floating ip"""
pub_net = self._get_public_network() pub_net = self._get_public_network()
self.info("Creating a new floating ip for network with id %s", self.info("Creating a new floating ip for network with id %s",
pub_net['id']) pub_net['id'])
fip = self.clients.network.create_floatingip(pub_net['id']) fip = self.clients.network.create_floatingip(
pub_net['id'], project=project_id)
# Verify that floating ip has been created # Verify that floating ip has been created
fips = self.clients.network.list_floatingips() fips = self.clients.network.list_floatingips()
fips = [f['id'] for f in fips] fips = [f['id'] for f in fips]
self.assertIn(fip['id'], fips) self.assertIn(fip['id'], fips)
# Verify quotas # Verify quotas
self._check_quotas(ip=+1) if project_id is None:
project_id = self._get_uuid()
changes = \
{project_id: [(QIP, QADD, 1, None)]}
self._check_quotas(changes)
# Check that IP is IPv4 # Check that IP is IPv4
self.assertEquals(IPy.IP(fip['floating_ip_address']).version(), 4) self.assertEqual(IPy.IP(fip['floating_ip_address']).version(), 4)
self.assertEqual(fip['tenant_id'], project_id)
self.info("Floating IP %s with id %s created", self.info("Floating IP %s with id %s created",
fip['floating_ip_address'], fip['id']) fip['floating_ip_address'], fip['id'])
...@@ -588,8 +638,51 @@ class CycladesTests(BurninTests): ...@@ -588,8 +638,51 @@ class CycladesTests(BurninTests):
list_ips = [f['id'] for f in self.clients.network.list_floatingips()] list_ips = [f['id'] for f in self.clients.network.list_floatingips()]
for fip in fips: for fip in fips:
self.assertNotIn(fip['id'], list_ips) self.assertNotIn(fip['id'], list_ips)
# Verify quotas # Verify quotas
self._check_quotas(ip=-len(fips)) changes = dict()
for fip in fips:
project = fip['tenant_id']
if project not in changes:
changes[project] = []
changes[project].append((QIP, QREMOVE, 1, None))
self._check_quotas(changes)
def _find_project(self, flavors, projects=None):
"""Return a pair of flavor, project that we can use"""
if projects is None:
projects = self.quotas.keys()
# XXX: Well there seems to be no easy way to find how many resources
# we have left in a project (we have to substract usage from limit,
# check both per_user and project quotas, blah, blah). For now
# just return the first flavor with the first project and lets hope
# that it fits.
return (flavors[0], projects[0])
# # Get only the quotas for the given 'projects'
# quotas = dict()
# for prj, qts in self.quotas.items():
# if prj in projects:
# quotas[prj] = qts
#
# results = []
# for flv in flavors:
# for prj, qts in quotas.items():
# self.debug("Testing flavor %s, project %s", flv['name'], prj)
# condition = \
# (flv['ram'] <= qts['cyclades.ram']['usage'] and
# flv['vcpus'] <= qts['cyclades.cpu']['usage'] and
# flv['disk'] <= qts['cyclades.disk']['usage'] and
# qts['cyclades.vm']['usage'] >= 1)
# if condition:
# results.append((flv, prj))
#
# if not results:
# msg = "Couldn't find a suitable flavor to use for current qutoas"
# self.error(msg)
#
# return random.choice(results)
class Retry(Exception): class Retry(Exception):
......
...@@ -42,7 +42,8 @@ import shutil ...@@ -42,7 +42,8 @@ import shutil
from kamaki.clients import ClientError from kamaki.clients import ClientError
from synnefo_tools.burnin.common import BurninTests, Proper from synnefo_tools.burnin.common import BurninTests, Proper, \
QPITHOS, QADD, QREMOVE
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
...@@ -171,7 +172,9 @@ class ImagesTestSuite(BurninTests): ...@@ -171,7 +172,9 @@ class ImagesTestSuite(BurninTests):
self.clients.pithos.upload_object(self.temp_image_name, fin) self.clients.pithos.upload_object(self.temp_image_name, fin)
# Verify quotas # Verify quotas
self._check_quotas(diskspace=file_size) changes = \
{self._get_uuid(): [(QPITHOS, QADD, file_size, None)]}
self._check_quotas(changes)
def test_009_register_image(self): def test_009_register_image(self):
"""Register image to Plankton""" """Register image to Plankton"""
...@@ -198,7 +201,9 @@ class ImagesTestSuite(BurninTests): ...@@ -198,7 +201,9 @@ class ImagesTestSuite(BurninTests):
self.clients.pithos.del_object(self.temp_image_name) self.clients.pithos.del_object(self.temp_image_name)
# Verify quotas # Verify quotas
file_size = os.path.getsize(self.temp_image_file) file_size = os.path.getsize(self.temp_image_file)
self._check_quotas(diskspace=-file_size) changes = \
{self._get_uuid(): [(QPITHOS, QREMOVE, file_size, None)]}
self._check_quotas(changes)
self.temp_image_name = None self.temp_image_name = None
# Remove temp directory # Remove temp directory
self.info("Deleting temp directory %s", self.temp_dir) self.info("Deleting temp directory %s", self.temp_dir)
......
...@@ -53,27 +53,11 @@ class NetworkTestSuite(CycladesTests): ...@@ -53,27 +53,11 @@ class NetworkTestSuite(CycladesTests):
def test_001_images_to_use(self): def test_001_images_to_use(self):
"""Find images to be used to create our machines""" """Find images to be used to create our machines"""
if self.images is None: self.avail_images = self._parse_images()
self.info("No --images given. Will use the default %s",