Statistics
| Branch: | Tag: | Revision:

root / tools / snf-admin @ 1d133009

History | View | Annotate | Download (17 kB)

1
#!/usr/bin/env python
2

    
3
# Copyright 2011 GRNET S.A. All rights reserved.
4
# 
5
# Redistribution and use in source and binary forms, with or
6
# without modification, are permitted provided that the following
7
# conditions are met:
8
# 
9
#   1. Redistributions of source code must retain the above
10
#      copyright notice, this list of conditions and the following
11
#      disclaimer.
12
# 
13
#   2. Redistributions in binary form must reproduce the above
14
#      copyright notice, this list of conditions and the following
15
#      disclaimer in the documentation and/or other materials
16
#      provided with the distribution.
17
# 
18
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29
# POSSIBILITY OF SUCH DAMAGE.
30
# 
31
# The views and conclusions contained in the software and
32
# documentation are those of the authors and should not be
33
# interpreted as representing official policies, either expressed
34
# or implied, of GRNET S.A.
35

    
36
from django.core.management import setup_environ
37
try:
38
    from synnefo import settings
39
except ImportError:
40
    raise Exception("Cannot import settings, make sure PYTHONPATH contains "
41
                    "the parent directory of the Synnefo Django project.")
42
setup_environ(settings)
43

    
44
import inspect
45
import logging
46
import sys
47

    
48
from collections import defaultdict
49
from itertools import product
50
from optparse import OptionParser
51
from os.path import basename
52

    
53
from synnefo.db import models
54
from synnefo.invitations.invitations import add_invitation, send_invitation
55
from synnefo.logic.users import _register_user, delete_user
56

    
57

    
58
def get_user(uid):
59
    try:
60
        uid = int(uid)
61
        return models.SynnefoUser.objects.get(id=uid)
62
    except ValueError:
63
        return None
64
    except models.SynnefoUser.DoesNotExist:
65
        return None
66

    
67
def print_dict(d, exclude=()):
68
    if not d:
69
        return
70
    margin = max(len(key) for key in d) + 1
71

    
72
    for key, val in sorted(d.items()):
73
        if key in exclude or key.startswith('_'):
74
            continue
75
        print '%s: %s' % (key.rjust(margin), val)
76

    
77
def print_item(item):
78
    print '%d %s' % (item.id, item.name)
79
    print_dict(item.__dict__, exclude=('id', 'name'))
80

    
81
def print_items(items, detail=False, keys=None):
82
    keys = keys or ('id', 'name')
83
    for item in items:
84
        for key in keys:
85
            print getattr(item, key),
86
        print
87
        
88
        if detail:
89
            print_dict(item.__dict__, exclude=keys)
90
            print
91

    
92

    
93
class Command(object):
94
    group = '<group>'
95
    name = '<command>'
96
    syntax = ''
97
    description = ''
98
    hidden = False
99
    
100
    def __init__(self, exe, argv):
101
        parser = OptionParser()
102
        syntax = '%s [options]' % self.syntax if self.syntax else '[options]'
103
        parser.usage = '%s %s %s' % (exe, self.name, syntax)
104
        parser.description = self.description
105
        self.add_options(parser)
106
        options, self.args = parser.parse_args(argv)
107
        
108
        # Add options to self
109
        for opt in parser.option_list:
110
            key = opt.dest
111
            if key:
112
                val = getattr(options, key)
113
                setattr(self, key, val)
114
        
115
        ch = logging.StreamHandler()
116
        ch.setFormatter(logging.Formatter('%(message)s'))
117
        logger = logging.getLogger()
118
        logger.addHandler(ch)
119
        level = logging.WARNING
120
        logger.setLevel(level)
121
        
122
        self.parser = parser
123
    
124
    def add_options(self, parser):
125
        pass
126
    
127
    def execute(self):
128
        try:
129
            self.main(*self.args)
130
        except TypeError:
131
            self.parser.print_help()
132

    
133

    
134
# Server commands
135

    
136
class ListServers(Command):
137
    group = 'server'
138
    name = 'list'
139
    syntax = '[server id]'
140
    description = 'list servers'
141
    
142
    def add_options(self, parser):
143
        parser.add_option('-a', action='store_true', dest='show_deleted',
144
                        default=False, help='also list deleted servers')
145
        parser.add_option('-l', action='store_true', dest='detail',
146
                        default=False, help='show detailed output')
147
        parser.add_option('-u', dest='uid', metavar='UID',
148
                            help='show servers of user with id UID')
149
    
150
    def main(self, server_id=None):
151
        servers = models.VirtualMachine.objects.order_by('id')
152
        if server_id:
153
            servers = servers.filter(id=server_id)
154
        if not self.show_deleted:
155
            servers = servers.filter(deleted=False)
156
        if self.uid:
157
            user = get_user(self.uid)
158
            if user:
159
                servers = servers.filter(owner=user)
160
            else:
161
                print 'Unknown user id'
162
                return
163
        
164
        print_items(servers, self.detail)
165

    
166

    
167
# User commands
168

    
169
class CreateUser(Command):
170
    group = 'user'
171
    name = 'create'
172
    syntax = '<username> <email>'
173
    description = 'create a user'
174
    
175
    def add_options(self, parser):
176
        parser.add_option('--realname', dest='realname', metavar='NAME',
177
                            help='set real name of user')
178
        parser.add_option('--type', dest='type', metavar='TYPE',
179
                            help='set user type')
180
    
181
    def main(self, username, email):
182
        realname = self.realname or username
183
        type = self.type or 'USER'
184
        if type not in [x[0] for x in models.SynnefoUser.ACCOUNT_TYPE]:
185
            print 'Invalid type'
186
            return
187
        
188
        user = _register_user(realname, username, email, type)
189
        print_item(user)
190

    
191

    
192
class DeleteUser(Command):
193
    group = 'user'
194
    name = 'delete'
195
    syntax = '<user id>'
196
    description = 'delete a user'
197
    hidden = True
198
    
199
    def add_options(self, parser):
200
        parser.add_option('--force', action='store_true', dest='force',
201
                        default=False, help='force deletion (use with care)')
202
    
203
    def main(self, user_id):
204
        if self.force:
205
            user = get_user(user_id)
206
            delete_user(user)
207
        else:
208
            print "WARNING: Deleting a user is a very destructive operation."
209
            print "Any objects with foreign keys pointing to this user will" \
210
                    " be deleted as well."
211
            print "Use --force to force deletion of this user."
212
            print "You have been warned!"
213

    
214

    
215
class InviteUser(Command):
216
    group = 'user'
217
    name = 'invite'
218
    syntax = '<inviter id> <invitee name> <invitee email>'
219
    description = 'invite a user'
220
    
221
    def main(self, inviter_id, name, email):
222
        inviter = get_user(inviter_id)
223
        inv = add_invitation(inviter, name, email)
224
        send_invitation(inv)
225

    
226

    
227
class ListUsers(Command):
228
    group = 'user'
229
    name = 'list'
230
    syntax = '[user id]'
231
    description = 'list users'
232
    
233
    def add_options(self, parser):
234
        parser.add_option('-l', action='store_true', dest='detail',
235
                        default=False, help='show detailed output')
236
    
237
    def main(self, user_id=None):
238
        if user_id:
239
            users = [models.SynnefoUser.objects.get(id=user_id)]
240
        else:
241
            users = models.SynnefoUser.objects.order_by('id')
242
        print_items(users, self.detail, keys=('id', 'name', 'uniq'))
243

    
244

    
245
class ModifyUser(Command):
246
    group = 'user'
247
    name = 'modify'
248
    syntax = '<user id>'
249
    description = 'modify a user'
250
    
251
    def add_options(self, parser):
252
        parser.add_option('--credit', dest='credit', metavar='VALUE',
253
                            help='set user credits')
254
        parser.add_option('--invitations', dest='invitations',
255
                            metavar='VALUE', help='set max invitations')
256
        parser.add_option('--realname', dest='realname', metavar='NAME',
257
                            help='set real name of user')
258
        parser.add_option('--type', dest='type', metavar='TYPE',
259
                            help='set user type')
260
        parser.add_option('--uniq', dest='uniq', metavar='ID',
261
                            help='set external unique ID')
262
        parser.add_option('--username', dest='username', metavar='NAME',
263
                            help='set username')
264
    
265
    def main(self, user_id):
266
        user = get_user(user_id)
267
        
268
        if self.credit:
269
            user.credit = self.credit
270
        if self.invitations:
271
            user.max_invitations = self.invitations
272
        if self.realname:
273
            user.realname = self.realname
274
        if self.type:
275
            allowed = [x[0] for x in models.SynnefoUser.ACCOUNT_TYPE]
276
            if self.type not in allowed:
277
                print 'Invalid type'
278
                return
279
            user.type = self.type
280
        if self.uniq:
281
            user.uniq = self.uniq
282
        if self.username:
283
            user.name = self.username
284
        
285
        user.save()
286
        print_item(user)
287

    
288

    
289
# Image commands
290

    
291
class ListImages(Command):
292
    group = 'image'
293
    name = 'list'
294
    syntax = '[image id]'
295
    description = 'list images'
296
    
297
    def add_options(self, parser):
298
        parser.add_option('-l', action='store_true', dest='detail',
299
                        default=False, help='show detailed output')
300
    
301
    def main(self, image_id=None):
302
        if image_id:
303
            images = [models.Image.objects.get(id=image_id)]
304
        else:
305
            images = models.Image.objects.order_by('id')
306
        print_items(images, self.detail)
307

    
308

    
309
class RegisterImage(Command):
310
    group = 'image'
311
    name = 'register'
312
    syntax = '<name> <backend id>'
313
    description = 'register an image'
314
    
315
    def add_options(self, parser):
316
        parser.add_option('--public', action='store_true', dest='public',
317
                            default=False, help='make image public')
318
        parser.add_option('-u', dest='uid', metavar='UID',
319
                            help='assign image to user with id UID')
320
    
321
    def main(self, name, backend_id):
322
        user = None
323
        if self.uid:
324
            user = get_user(self.uid)
325
            if not user:
326
                print 'Unknown user id'
327
                return
328
        
329
        image = models.Image.objects.create(
330
            name=name,
331
            state='ACTIVE',
332
            owner=user,
333
            backend_id=backend_id,
334
            public=self.public)
335
        
336
        print_item(image)
337

    
338

    
339
class ModifyImage(Command):
340
    group = 'image'
341
    name = 'modify'
342
    syntax = '<image id>'
343
    description = 'modify an image'
344
    
345
    def add_options(self, parser):
346
        parser.add_option('-b', dest='backend_id', metavar='BACKEND_ID',
347
                            help='set image backend id')
348
        parser.add_option('-f', dest='format', metavar='FORMAT',
349
                            help='set image format')
350
        parser.add_option('-n', dest='name', metavar='NAME',
351
                            help='set image name')
352
        parser.add_option('--public', action='store_true', dest='public',
353
                            default=False, help='make image public')
354
        parser.add_option('--nopublic', action='store_true', dest='private',
355
                            default=False, help='make image private')
356
        parser.add_option('-s', dest='state', metavar='STATE',
357
                            default=False, help='set image state')
358
        parser.add_option('-u', dest='uid', metavar='UID',
359
                            help='assign image to user with id UID')
360
    
361
    def main(self, image_id):
362
        try:
363
            image = models.Image.objects.get(id=image_id)
364
        except:
365
            print 'Image not found'
366
            return
367
        
368
        if self.backend_id:
369
            image.backend_id = self.backend_id
370
        if self.format:
371
            allowed = [x[0] for x in models.Image.FORMATS]
372
            if self.format not in allowed:
373
                print 'Invalid format'
374
                return
375
            image.format = self.format
376
        if self.name:
377
            image.name = self.name
378
        if self.public:
379
            image.public = True
380
        if self.private:
381
            image.public = False
382
        if self.state:
383
            allowed = [x[0] for x in models.Image.IMAGE_STATES]
384
            if self.state not in allowed:
385
                print 'Invalid state'
386
                return
387
            image.state = self.state
388
        if self.uid:
389
            image.owner = get_user(self.uid)
390
        
391
        image.save()
392
        print_item(image)
393

    
394

    
395
class ModifyImageMeta(Command):
396
    group = 'image'
397
    name = 'meta'
398
    syntax = '<image id> [key[=val]]'
399
    description = 'get and manipulate image metadata'
400
    
401
    def main(self, image_id, arg=''):
402
        try:
403
            image = models.Image.objects.get(id=image_id)
404
        except:
405
            print 'Image not found'
406
            return
407
        
408
        key, sep, val = arg.partition('=')
409
        if not sep:
410
            val = None
411
        
412
        if not key:
413
            metadata = {}
414
            for meta in image.imagemetadata_set.order_by('meta_key'):
415
                metadata[meta.meta_key] = meta.meta_value
416
            print_dict(metadata)
417
            return
418
        
419
        try:
420
            meta = image.imagemetadata_set.get(meta_key=key)
421
        except models.ImageMetadata.DoesNotExist:
422
            meta = None
423
        
424
        if val is None:
425
            if meta:
426
                print_dict({key: meta.meta_value})
427
            return
428
        
429
        if val:
430
            if not meta:
431
                meta = image.imagemetadata_set.create(meta_key=key)
432
            meta.meta_value = val
433
            meta.save()
434
        else:
435
            # Delete if val is empty
436
            if meta:
437
                meta.delete()
438

    
439

    
440
# Flavor commands
441

    
442
class CreateFlavor(Command):
443
    group = 'flavor'
444
    name = 'create'
445
    syntax = '<cpu>[,<cpu>,...] <ram>[,<ram>,...] <disk>[,<disk>,...]'
446
    description = 'create one or more flavors'
447
    
448
    def main(self, cpu, ram, disk):
449
        cpus = cpu.split(',')
450
        rams = ram.split(',')
451
        disks = disk.split(',')
452
        
453
        flavors = []
454
        for cpu, ram, disk in product(cpus, rams, disks):
455
            try:
456
                flavors.append((int(cpu), int(ram), int(disk)))
457
            except ValueError:
458
                print 'Invalid values'
459
                return
460
        
461
        created = []
462
        for cpu, ram, disk in flavors:
463
            flavor = models.Flavor.objects.create(cpu=cpu, ram=ram, disk=disk)
464
            created.append(flavor)
465
        
466
        print_items(created, detail=True)
467

    
468

    
469
class DeleteFlavor(Command):
470
    group = 'flavor'
471
    name = 'delete'
472
    syntax = '<flavor id> [<flavor id>] [...]'
473
    description = 'delete one or more flavors'
474
    
475
    def main(self, *args):
476
        if not args:
477
            raise TypeError
478
        for flavor_id in args:
479
            flavor = models.Flavor.objects.get(id=int(flavor_id))
480
            flavor.delete()
481

    
482

    
483
class ListFlavors(Command):
484
    group = 'flavor'
485
    name = 'list'
486
    syntax = '[flavor id]'
487
    description = 'list images'
488
    
489
    def add_options(self, parser):
490
        parser.add_option('-l', action='store_true', dest='detail',
491
                        default=False, help='show detailed output')
492
    
493
    def main(self, flavor_id=None):
494
        if flavor_id:
495
            flavors = [models.Flavor.objects.get(id=flavor_id)]
496
        else:
497
            flavors = models.Flavor.objects.order_by('id')
498
        print_items(flavors, self.detail)
499

    
500

    
501
def print_usage(exe, groups, group=None, shortcut=False):
502
    nop = Command(exe, [])
503
    nop.parser.print_help()
504
    if group:
505
        groups = {group: groups[group]}
506

    
507
    print
508
    print 'Commands:'
509
    
510
    for group, commands in sorted(groups.items()):
511
        for command, cls in sorted(commands.items()):
512
            if cls.hidden:
513
                continue
514
            name = '  %s %s' % (group, command)
515
            print '%s %s' % (name.ljust(22), cls.description)
516
        print
517

    
518

    
519
def main():
520
    groups = defaultdict(dict)
521
    module = sys.modules[__name__]
522
    for name, cls in inspect.getmembers(module, inspect.isclass):
523
        if not issubclass(cls, Command) or cls == Command:
524
            continue
525
        groups[cls.group][cls.name] = cls
526
    
527
    argv = list(sys.argv)
528
    exe = basename(argv.pop(0))
529
    prefix, sep, suffix = exe.partition('-')
530
    if sep and prefix == 'snf' and suffix in groups:
531
        # Allow shortcut aliases like snf-image, snf-server, etc
532
        group = suffix
533
    else:
534
        group = argv.pop(0) if argv else None
535
        if group in groups:
536
            exe = '%s %s' % (exe, group)
537
        else:
538
            exe = '%s <group>' % exe
539
            group = None
540
    
541
    command = argv.pop(0) if argv else None
542
    
543
    if group not in groups or command not in groups[group]:
544
        print_usage(exe, groups, group)
545
        sys.exit(1)
546
    
547
    cls = groups[group][command]
548
    cmd = cls(exe, argv)
549
    cmd.execute()
550

    
551

    
552
if __name__ == '__main__':
553
    main()