Commit f941d534 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

cyclades: Refactor statistics mechanism

Major refactoring to Cyclades statistics mechanism. The admin API
endpoint has been updated in order to expose detail statistics about
Ganeti cluster, virtual servers and networks, public IPv4 pools and
images.

The 'stats-cyclades' management command has also been updated in order
to display all available statistics in a user-friendly way.
parent 0e0c746b
......@@ -32,8 +32,6 @@
# or implied, of GRNET S.A.
import itertools
import operator
import datetime
from collections import defaultdict # , OrderedDict
......@@ -42,33 +40,75 @@ from django.conf import settings
from django.db.models import Count, Sum
from snf_django.lib.astakos import UserCache
from synnefo.db.models import VirtualMachine, Network, Backend
from synnefo.db.models import (VirtualMachine, Network, Backend,
pooled_rapi_client, Flavor)
from synnefo.plankton.utils import image_backend
from synnefo.logic import backend as backend_mod
def get_cyclades_stats(backend=None, clusters=True, servers=True,
resources=True, networks=True, images=True):
ip_pools=True, networks=True, images=True):
stats = {"datetime": datetime.datetime.now().strftime("%c")}
if clusters:
stats["clusters"] = get_cluster_stats(backend=backend)
if servers:
stats["servers"] = get_servers_stats(backend=backend)
if resources:
stats["resources"] = get_resources_stats(backend=backend)
stats["servers"] = get_server_stats(backend=backend)
if ip_pools:
stats["ip_pools"] = get_ip_pool_stats()
if networks:
stats["networks"] = get_networks_stats()
stats["networks"] = get_network_stats()
if images:
stats["images"] = get_images_stats(backend=None)
stats["images"] = get_image_stats(backend=None)
return stats
def get_cluster_stats(backend):
total = Backend.objects.all()
stats = {"total": total.count(),
"drained": total.filter(drained=True).count(),
"offline": total.filter(offline=True).count()}
return stats
def _get_cluster_stats(bend):
"""Get information about a Ganeti cluster and all of it's nodes."""
bend_vms = bend.virtual_machines.filter(deleted=False)
vm_stats = bend_vms.aggregate(Sum("flavor__cpu"),
Sum("flavor__ram"),
Sum("flavor__disk"))
cluster_info = {
"drained": bend.drained,
"offline": bend.offline,
"hypervisor": bend.hypervisor,
"disk_templates": bend.disk_templates,
"virtual_servers": bend_vms.count(),
"virtual_cpu": (vm_stats["flavor__cpu__sum"] or 0),
"virtual_ram": (vm_stats["flavor__ram__sum"] or 0) << 20,
"virtual_disk": (vm_stats["flavor__disk__sum"] or 0) << 30,
"nodes": {},
}
nodes = []
if not bend.offline:
with pooled_rapi_client(bend) as c:
nodes = c.GetNodes(bulk=True)
for node in nodes:
_node_stats = {
"drained": node["drained"],
"offline": node["offline"],
"vm_capable": node["vm_capable"],
"instances": node["pinst_cnt"],
"cpu": node["ctotal"],
"ram": {
"total": (node["mtotal"] or 0) << 20,
"free": (node["mfree"] or 0) << 20
},
"disk": {
"total": (node["dtotal"] or 0) << 20,
"free": (node["dfree"] or 0) << 20
},
}
cluster_info["nodes"][node["name"]] = _node_stats
return bend.clustername, cluster_info
def get_cluster_stats(backend=None):
"""Get statistics about all Ganeti clusters."""
if backend is None:
backends = Backend.objects.all()
else:
backends = [backend]
return dict([_get_cluster_stats(bend) for bend in backends])
def _get_total_servers(backend=None):
......@@ -78,48 +118,77 @@ def _get_total_servers(backend=None):
return total_servers
def get_servers_stats(backend=None):
total_servers = _get_total_servers(backend=backend)
per_state = total_servers.values("operstate")\
.annotate(count=Count("operstate"))
stats = {"total": 0}
[stats.setdefault(s[0], 0) for s in VirtualMachine.OPER_STATES]
for x in per_state:
stats[x["operstate"]] = x["count"]
stats["total"] += x["count"]
return stats
def get_resources_stats(backend=None):
total_servers = _get_total_servers(backend=backend)
active_servers = total_servers.filter(deleted=False)
allocated = {}
server_count = {}
for res in ["cpu", "ram", "disk", "disk_template"]:
server_count[res] = {}
allocated[res] = 0
val = "flavor__%s" % res
results = active_servers.values(val).annotate(count=Count(val))
for result in results:
server_count[res][result[val]] = result["count"]
if res != "disk_template":
prod = (result["count"] * int(result[val]))
if res == "disk":
prod = prod << 10
allocated[res] += prod
resources_stats = get_backend_stats(backend=backend)
for res in ["cpu", "ram", "disk", "disk_template"]:
if res not in resources_stats:
resources_stats[res] = {}
resources_stats[res]["servers"] = server_count[res]
resources_stats[res]["allocated"] = allocated[res]
return resources_stats
def get_images_stats(backend=None):
def get_server_stats(backend=None):
servers = VirtualMachine.objects.select_related("flavor")\
.filter(deleted=False)
if backend is not None:
servers = servers.filter(backend=backend)
disk_templates = Flavor.objects.values_list("disk_template", flat=True)\
.distinct()
# Initialize stats
server_stats = defaultdict(dict)
for state in ["started", "stopped", "error"]:
server_stats[state]["count"] = 0
server_stats[state]["cpu"] = defaultdict(int)
server_stats[state]["ram"] = defaultdict(int)
server_stats[state]["disk"] = \
dict([(disk_t, defaultdict(int)) for disk_t in disk_templates])
for s in servers:
if s.operstate in ["STARTED", "BUILD"]:
state = "started"
elif s.operstate == "ERROR":
state = "error"
else:
state = "stopped"
flavor = s.flavor
disk_template = flavor.disk_template
server_stats[state]["count"] += 1
server_stats[state]["cpu"][flavor.cpu] += 1
server_stats[state]["ram"][flavor.ram << 20] += 1
server_stats[state]["disk"][disk_template][flavor.disk << 30] += 1
return server_stats
def get_network_stats():
"""Get statistics about Cycldades Networks."""
network_stats = defaultdict(dict)
for flavor in Network.FLAVORS.keys():
network_stats[flavor] = defaultdict(int)
network_stats[flavor]["active"] = 0
network_stats[flavor]["error"] = 0
networks = Network.objects.filter(deleted=False)
for net in networks:
state = "error" if net.state == "ERROR" else "active"
network_stats[net.flavor][state] += 1
return network_stats
def get_ip_pool_stats():
"""Get statistics about floating IPs."""
ip_stats = {}
for status in ["drained", "active"]:
ip_stats[status] = {
"count": 0,
"total": 0,
"free": 0,
}
ip_pools = Network.objects.filter(deleted=False, floating_ip_pool=True)
for ip_pool in ip_pools:
status = "drained" if ip_pool.drained else "active"
total, free = ip_pool.ip_count()
ip_stats[status]["count"] += 1
ip_stats[status]["total"] += total
ip_stats[status]["free"] += free
return ip_stats
def get_image_stats(backend=None):
total_servers = _get_total_servers(backend=backend)
active_servers = total_servers.filter(deleted=False)
......@@ -134,65 +203,6 @@ def get_images_stats(backend=None):
return dict(image_stats)
def get_networks_stats():
total_networks = Network.objects.all()
stats = {"public_ips": get_ip_stats(),
"total": 0}
per_state = total_networks.values("state")\
.annotate(count=Count("state"))
[stats.setdefault(s[0], 0) for s in Network.OPER_STATES]
for x in per_state:
stats[x["state"]] = x["count"]
stats["total"] += x["count"]
return stats
def group_by_resource(objects, resource):
stats = {}
key = operator.attrgetter("flavor."+resource)
grouped = itertools.groupby(sorted(objects, key=key), key)
for val, group in grouped:
stats[val] = len(list(group))
return stats
def get_ip_stats():
total, free = 0, 0,
for network in Network.objects.filter(public=True, deleted=False):
try:
net_total, net_free = network.ip_count()
except AttributeError:
# TODO: Check that this works..
pool = network.get_pool(locked=False)
net_total = pool.pool_size
net_free = pool.count_available()
if not network.drained:
total += net_total
free += net_free
return {"total": total,
"free": free}
def get_backend_stats(backend=None):
if backend is None:
backends = Backend.objects.filter(offline=False)
else:
if backend.offline:
return {}
backends = [backend]
[backend_mod.update_backend_resources(b) for b in backends]
resources = {}
for attr in ("dfree", "dtotal", "mfree", "mtotal", "ctotal"):
resources[attr] = 0
for b in backends:
resources[attr] += getattr(b, attr)
return {"disk": {"free": resources["dfree"], "total": resources["dtotal"]},
"ram": {"free": resources["mfree"], "total": resources["mtotal"]},
"cpu": {"free": resources["ctotal"], "total": resources["ctotal"]},
"disk_template": {"free": 0, "total": 0}}
class ImageCache(object):
def __init__(self):
self.images = {}
......@@ -202,7 +212,7 @@ class ImageCache(object):
usercache.get_uuid(settings.SYSTEM_IMAGES_OWNER)
def get_image(self, imageid, userid):
if not imageid in self.images:
if imageid not in self.images:
try:
with image_backend(userid) as ib:
image = ib.get_image(imageid)
......@@ -238,12 +248,12 @@ def get_public_stats():
state = VirtualMachine.RSAPI_STATE_FROM_OPER_STATE.get(operstate)
if deleted:
for key in zero_stats.keys():
server_stats["DELETED"][key] += stats.get(key, 0)
server_stats["DELETED"][key] += (stats.get(key, 0) or 0)
elif state:
for key in zero_stats.keys():
server_stats[state][key] += stats.get(key, 0)
server_stats[state][key] += (stats.get(key, 0) or 0)
#Networks
# Networks
net_objects = Network.objects
networks = net_objects.values("deleted", "state")\
.annotate(count=Count("id"))
......@@ -266,3 +276,8 @@ def get_public_stats():
statistics = {"servers": server_stats,
"networks": network_stats}
return statistics
if __name__ == "__main__":
import json
print json.dumps(get_cyclades_stats())
......@@ -36,7 +36,7 @@ from django import http
from django.utils import simplejson as json
from django.conf import settings
from snf_django.lib import api
from snf_django.lib.api import utils, faults
from snf_django.lib.api import faults
from synnefo.db.models import Backend
from synnefo.admin import stats
......@@ -58,32 +58,25 @@ def get_public_stats(request):
@api.user_in_groups(permitted_groups=settings.ADMIN_STATS_PERMITTED_GROUPS,
logger=logger)
def get_cyclades_stats(request):
images = True
servers, networks, ip_pools, images = True, True, True, True,
clusters = True
backend = None
if request.body:
req = utils.get_request_dict(request)
req_stats = utils.get_attribute(req, "stats", required=True,
attr_type=dict)
# Check backend
backend_id = utils.get_attribute(req_stats, "backend", required=False,
attr_type=(basestring, int))
if backend_id is not None:
backend_id = request.GET.get("backend")
if backend_id is not None:
try:
try:
try:
backend_id = int(backend_id)
backend = Backend.objects.get(id=backend_id)
except (ValueError, TypeError):
backend = Backend.objects.get(clustername=backend_id)
except Backend.DoesNotExist:
raise faults.BadRequest("Invalid backend '%s'" % backend_id)
include_images = utils.get_attribute(req_stats, "images",
required=False,
attr_type=bool)
if include_images is not None:
images = include_images
backend_id = int(backend_id)
backend = Backend.objects.get(id=backend_id)
except (ValueError, TypeError):
backend = Backend.objects.get(clustername=backend_id)
except Backend.DoesNotExist:
raise faults.BadRequest("Invalid backend '%s'" % backend_id)
# This stats have no meaning per backend
networks, ip_pools = False, False
_stats = stats.get_cyclades_stats(backend=backend, clusters=True,
servers=True, resources=True,
networks=True, images=images)
_stats = stats.get_cyclades_stats(backend=backend, clusters=clusters,
servers=servers, networks=networks,
ip_pools=ip_pools, images=images)
data = json.dumps(_stats)
return http.HttpResponse(data, status=200, content_type='application/json')
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment