Revision d9f2a9e1

b/snf-cyclades-app/synnefo/logic/management/commands/stats-cyclades.py
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]
b/snf-django-lib/snf_django/management/utils.py
168 168

  
169 169
            t_length = sum(widths) + len(sep) * (len(widths) - 1)
170 170
            if title is not None:
171
                t_length = max(t_length, len(title))
171 172
                out.write("-" * t_length + "\n")
172 173
                out.write(title.center(t_length) + "\n")
173 174
                out.write("-" * t_length + "\n")

Also available in: Unified diff