|
1 |
# Copyright 2013 GRNET S.A. All rights reserved.
|
|
2 |
#
|
|
3 |
# Redistribution and use in source and binary forms, with or
|
|
4 |
# without modification, are permitted provided that the following
|
|
5 |
# conditions are met:
|
|
6 |
#
|
|
7 |
# 1. Redistributions of source code must retain the above
|
|
8 |
# copyright notice, this list of conditions and the following
|
|
9 |
# disclaimer.
|
|
10 |
#
|
|
11 |
# 2. Redistributions in binary form must reproduce the above
|
|
12 |
# copyright notice, this list of conditions and the following
|
|
13 |
# disclaimer in the documentation and/or other materials
|
|
14 |
# provided with the distribution.
|
|
15 |
#
|
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
|
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
|
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
|
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
|
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
27 |
# POSSIBILITY OF SUCH DAMAGE.
|
|
28 |
#
|
|
29 |
# The views and conclusions contained in the software and
|
|
30 |
# documentation are those of the authors and should not be
|
|
31 |
# interpreted as representing official policies, either expressed
|
|
32 |
# or implied, of GRNET S.A.
|
|
33 |
|
|
34 |
|
|
35 |
import itertools
|
|
36 |
import operator
|
|
37 |
import datetime
|
|
38 |
import json
|
|
39 |
import string
|
|
40 |
|
|
41 |
from optparse import make_option
|
|
42 |
|
|
43 |
from collections import defaultdict # , OrderedDict
|
|
44 |
from django.conf import settings
|
|
45 |
from django.db.models import Count
|
|
46 |
from snf_django.management.utils import pprint_table, parse_bool
|
|
47 |
|
|
48 |
from synnefo.db.models import Backend
|
|
49 |
from snf_django.management.commands import SynnefoCommand, CommandError
|
|
50 |
from snf_django.lib.astakos import UserCache
|
|
51 |
from synnefo.api.util import get_image
|
|
52 |
from synnefo.db.models import VirtualMachine, Network
|
|
53 |
from synnefo.logic import backend as backend_mod
|
|
54 |
from synnefo.management.common import get_backend
|
|
55 |
|
|
56 |
|
|
57 |
class Command(SynnefoCommand):
|
|
58 |
help = "Get available statistics of Cyclades service"
|
|
59 |
can_import_settings = True
|
|
60 |
|
|
61 |
option_list = SynnefoCommand.option_list + (
|
|
62 |
make_option("--backend",
|
|
63 |
dest="backend",
|
|
64 |
help="Include statistics only for this backend."),
|
|
65 |
make_option("--clusters",
|
|
66 |
dest="clusters",
|
|
67 |
default="True",
|
|
68 |
metavar="True|False",
|
|
69 |
choices=["True", "False"],
|
|
70 |
help="Include statistics about clusters."),
|
|
71 |
make_option("--servers",
|
|
72 |
dest="servers",
|
|
73 |
default="True",
|
|
74 |
metavar="True|False",
|
|
75 |
choices=["True", "False"],
|
|
76 |
help="Include statistics about servers."),
|
|
77 |
make_option("--resources",
|
|
78 |
dest="resources",
|
|
79 |
default="True",
|
|
80 |
metavar="True|False",
|
|
81 |
choices=["True", "False"],
|
|
82 |
help="Include statistics about resources "
|
|
83 |
" (CPU, RAM, DISK)."),
|
|
84 |
make_option("--networks",
|
|
85 |
dest="networks",
|
|
86 |
default="True",
|
|
87 |
metavar="True|False",
|
|
88 |
choices=["True", "False"],
|
|
89 |
help="Include statistics about networks."),
|
|
90 |
make_option("--images",
|
|
91 |
dest="images",
|
|
92 |
default="False",
|
|
93 |
metavar="True|False",
|
|
94 |
choices=["True", "False"],
|
|
95 |
help="Include statistics about images."),
|
|
96 |
)
|
|
97 |
|
|
98 |
def handle(self, *args, **options):
|
|
99 |
if options["backend"] is not None:
|
|
100 |
backend = get_backend(options["backend"])
|
|
101 |
else:
|
|
102 |
backend = None
|
|
103 |
|
|
104 |
clusters = parse_bool(options["clusters"])
|
|
105 |
servers = parse_bool(options["servers"])
|
|
106 |
resources = parse_bool(options["resources"])
|
|
107 |
networks = parse_bool(options["networks"])
|
|
108 |
images = parse_bool(options["images"])
|
|
109 |
|
|
110 |
stats = get_cyclades_stats(backend, clusters, servers, resources,
|
|
111 |
networks, images)
|
|
112 |
|
|
113 |
output_format = options["output_format"]
|
|
114 |
if output_format == "json":
|
|
115 |
self.stdout.write(json.dumps(stats, indent=4) + "\n")
|
|
116 |
elif output_format == "pretty":
|
|
117 |
pretty_print_stats(stats, self.stdout)
|
|
118 |
else:
|
|
119 |
raise CommandError("Output format '%s' not supported." %
|
|
120 |
output_format)
|
|
121 |
|
|
122 |
|
|
123 |
def get_cyclades_stats(backend=None, clusters=True, servers=True,
|
|
124 |
resources=True, networks=True, images=True):
|
|
125 |
stats = {"datetime": datetime.datetime.now().strftime("%c")}
|
|
126 |
if clusters:
|
|
127 |
stats["clusters"] = get_cluster_stats(backend=backend)
|
|
128 |
if servers:
|
|
129 |
stats["servers"] = get_servers_stats(backend=backend)
|
|
130 |
if resources:
|
|
131 |
stats["resources"] = get_resources_stats(backend=backend)
|
|
132 |
if networks:
|
|
133 |
stats["networks"] = get_networks_stats()
|
|
134 |
if images:
|
|
135 |
stats["images"] = get_images_stats(backend=None)
|
|
136 |
return stats
|
|
137 |
|
|
138 |
|
|
139 |
def columns_from_fields(fields, values):
|
|
140 |
return zip(map(string.lower, fields), [values.get(f, 0) for f in fields])
|
|
141 |
|
|
142 |
|
|
143 |
def pretty_print_stats(stats, stdout):
|
|
144 |
newline = lambda: stdout.write("\n")
|
|
145 |
|
|
146 |
datetime = stats.get("datetime")
|
|
147 |
stdout.write("datetime: %s\n" % datetime)
|
|
148 |
newline()
|
|
149 |
|
|
150 |
clusters = stats.get("clusters")
|
|
151 |
if clusters is not None:
|
|
152 |
fields = ["total", "drained", "offline"]
|
|
153 |
table = columns_from_fields(fields, clusters)
|
|
154 |
pprint_table(stdout, table, None,
|
|
155 |
title="Statistics for Ganeti Clusters")
|
|
156 |
newline()
|
|
157 |
|
|
158 |
servers = stats.get("servers")
|
|
159 |
if servers is not None:
|
|
160 |
fields = ["total", "STARTED", "STOPPED", "BUILD", "ERROR", "DESTROYED"]
|
|
161 |
table = columns_from_fields(fields, servers)
|
|
162 |
pprint_table(stdout, table, None,
|
|
163 |
title="Statistics for Virtual Servers")
|
|
164 |
newline()
|
|
165 |
|
|
166 |
networks = stats.get("networks")
|
|
167 |
if networks is not None:
|
|
168 |
public_ips = networks.pop("public_ips")
|
|
169 |
networks["total_public_ips"] = public_ips.get("total", 0)
|
|
170 |
networks["free_public_ips"] = public_ips.get("free", 0)
|
|
171 |
fields = ["total", "ACTIVE", "DELETED", "ERROR"]
|
|
172 |
table = columns_from_fields(fields, networks)
|
|
173 |
pprint_table(stdout, table, None,
|
|
174 |
title="Statistics for Virtual Networks")
|
|
175 |
newline()
|
|
176 |
|
|
177 |
resources = stats.get("resources")
|
|
178 |
if resources is not None:
|
|
179 |
for resource_name, resource in sorted(resources.items()):
|
|
180 |
fields = ["total", "allocated"]
|
|
181 |
for res, num in sorted(resource.pop("servers", {}).items()):
|
|
182 |
name = "servers_with_%s" % res
|
|
183 |
resource[name] = num
|
|
184 |
fields.append(name)
|
|
185 |
table = columns_from_fields(fields, resources)
|
|
186 |
pprint_table(stdout, table, None,
|
|
187 |
title="Statistics for %s" % resource_name)
|
|
188 |
newline()
|
|
189 |
|
|
190 |
images = stats.get("images")
|
|
191 |
if images is not None:
|
|
192 |
pprint_table(stdout, sorted(images.items()), None,
|
|
193 |
title="Statistics for Images")
|
|
194 |
newline()
|
|
195 |
|
|
196 |
|
|
197 |
def get_cluster_stats(backend):
|
|
198 |
total = Backend.objects.all()
|
|
199 |
stats = {"total": total.count(),
|
|
200 |
"drained": total.filter(drained=True).count(),
|
|
201 |
"offline": total.filter(offline=True).count()}
|
|
202 |
return stats
|
|
203 |
|
|
204 |
|
|
205 |
def _get_total_servers(backend=None):
|
|
206 |
total_servers = VirtualMachine.objects.all()
|
|
207 |
if backend is not None:
|
|
208 |
total_servers = total_servers.filter(backend=backend)
|
|
209 |
return total_servers
|
|
210 |
|
|
211 |
|
|
212 |
def get_servers_stats(backend=None):
|
|
213 |
total_servers = _get_total_servers(backend=backend)
|
|
214 |
per_state = total_servers.values("operstate")\
|
|
215 |
.annotate(count=Count("operstate"))
|
|
216 |
stats = {"total": 0}
|
|
217 |
for x in per_state:
|
|
218 |
stats[x["operstate"]] = x["count"]
|
|
219 |
stats["total"] += x["count"]
|
|
220 |
return stats
|
|
221 |
|
|
222 |
|
|
223 |
def get_resources_stats(backend=None):
|
|
224 |
total_servers = _get_total_servers(backend=backend)
|
|
225 |
active_servers = total_servers.filter(deleted=False)
|
|
226 |
|
|
227 |
allocated = {}
|
|
228 |
server_count = {}
|
|
229 |
for res in ["cpu", "ram", "disk", "disk_template"]:
|
|
230 |
server_count[res] = {}
|
|
231 |
allocated[res] = 0
|
|
232 |
val = "flavor__%s" % res
|
|
233 |
results = active_servers.values(val).annotate(count=Count(val))
|
|
234 |
for result in results:
|
|
235 |
server_count[res][result[val]] = result["count"]
|
|
236 |
if res != "disk_template":
|
|
237 |
allocated[res] += result["count"]
|
|
238 |
|
|
239 |
resources_stats = get_backend_stats(backend=backend)
|
|
240 |
for res in ["cpu", "ram", "disk", "disk_template"]:
|
|
241 |
if res not in resources_stats:
|
|
242 |
resources_stats[res] = {}
|
|
243 |
resources_stats[res]["servers"] = server_count[res]
|
|
244 |
resources_stats[res]["allocated"] = allocated[res]
|
|
245 |
|
|
246 |
return resources_stats
|
|
247 |
|
|
248 |
|
|
249 |
def get_images_stats(backend=None):
|
|
250 |
total_servers = _get_total_servers(backend=backend)
|
|
251 |
active_servers = total_servers.filter(deleted=False)
|
|
252 |
|
|
253 |
active_servers_images = active_servers.values("imageid", "userid")\
|
|
254 |
.annotate(number=Count("imageid"))
|
|
255 |
image_cache = ImageCache()
|
|
256 |
image_stats = defaultdict(int)
|
|
257 |
for result in active_servers_images:
|
|
258 |
imageid = image_cache.get_image(result["imageid"], result["userid"])
|
|
259 |
image_stats[imageid] += result["number"]
|
|
260 |
return dict(image_stats)
|
|
261 |
|
|
262 |
|
|
263 |
def get_networks_stats():
|
|
264 |
total_networks = Network.objects.all()
|
|
265 |
stats = {"public_ips": get_ip_stats(),
|
|
266 |
"total": 0}
|
|
267 |
per_state = total_networks.values("state")\
|
|
268 |
.annotate(count=Count("state"))
|
|
269 |
for x in per_state:
|
|
270 |
stats[x["state"]] = x["count"]
|
|
271 |
stats["total"] += x["count"]
|
|
272 |
return stats
|
|
273 |
|
|
274 |
|
|
275 |
def group_by_resource(objects, resource):
|
|
276 |
stats = {}
|
|
277 |
key = operator.attrgetter("flavor."+resource)
|
|
278 |
grouped = itertools.groupby(sorted(objects, key=key), key)
|
|
279 |
for val, group in grouped:
|
|
280 |
stats[val] = len(list(group))
|
|
281 |
return stats
|
|
282 |
|
|
283 |
|
|
284 |
def get_ip_stats():
|
|
285 |
total, free = 0, 0,
|
|
286 |
for network in Network.objects.filter(public=True, deleted=False):
|
|
287 |
try:
|
|
288 |
net_total, net_free = network.ip_count()
|
|
289 |
except AttributeError:
|
|
290 |
# TODO: Check that this works..
|
|
291 |
pool = network.get_pool(locked=False)
|
|
292 |
net_total = pool.pool_size
|
|
293 |
net_free = pool.count_available()
|
|
294 |
if not network.drained:
|
|
295 |
total += net_total
|
|
296 |
free += net_free
|
|
297 |
return {"total": total,
|
|
298 |
"free": free}
|
|
299 |
|
|
300 |
|
|
301 |
def get_backend_stats(backend=None):
|
|
302 |
if backend is None:
|
|
303 |
backends = Backend.objects.filter(offline=False)
|
|
304 |
else:
|
|
305 |
if backend.offline:
|
|
306 |
return {}
|
|
307 |
backends = [backend]
|
|
308 |
[backend_mod.update_backend_resources(b) for b in backends]
|
|
309 |
resources = {}
|
|
310 |
for attr in ("dfree", "dtotal", "mfree", "mtotal", "ctotal"):
|
|
311 |
resources[attr] = 0
|
|
312 |
for b in backends:
|
|
313 |
resources[attr] += getattr(b, attr)
|
|
314 |
|
|
315 |
return {"disk": {"free": resources["dfree"], "total": resources["dtotal"]},
|
|
316 |
"ram": {"free": resources["mfree"], "total": resources["mtotal"]},
|
|
317 |
"cpu": {"free": resources["ctotal"], "total": resources["ctotal"]},
|
|
318 |
"disk_template": {"free": 0, "total": 0}}
|
|
319 |
|
|
320 |
|
|
321 |
class ImageCache(object):
|
|
322 |
def __init__(self):
|
|
323 |
self.images = {}
|
|
324 |
usercache = UserCache(settings.ASTAKOS_BASE_URL,
|
|
325 |
settings.CYCLADES_SERVICE_TOKEN)
|
|
326 |
self.system_user_uuid = \
|
|
327 |
usercache.get_uuid(settings.SYSTEM_IMAGES_OWNER)
|
|
328 |
|
|
329 |
def get_image(self, imageid, userid):
|
|
330 |
if not imageid in self.images:
|
|
331 |
try:
|
|
332 |
image = get_image(imageid, userid)
|
|
333 |
owner = image["owner"]
|
|
334 |
owner = "system" if image["owner"] == self.system_user_uuid\
|
|
335 |
else "user"
|
|
336 |
self.images[imageid] = owner + ":" + image["name"]
|
|
337 |
except Exception:
|
|
338 |
self.images[imageid] = "unknown:unknown"
|
|
339 |
|
|
340 |
return self.images[imageid]
|