Statistics
| Branch: | Tag: | Revision:

root / tools / snf-admin @ 22f79931

History | View | Annotate | Download (17.1 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
        username = username.decode('utf8')
183
        realname = self.realname or username
184
        type = self.type or 'USER'
185
        if type not in [x[0] for x in models.SynnefoUser.ACCOUNT_TYPE]:
186
            print 'Invalid type'
187
            return
188
        
189
        user = _register_user(realname, username, email, type)
190
        print_item(user)
191

    
192

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

    
215

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

    
228

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

    
246

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

    
290

    
291
# Image commands
292

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

    
310

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

    
340

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

    
396

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

    
441

    
442
# Flavor commands
443

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

    
470

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

    
484

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

    
502

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

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

    
520

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

    
553

    
554
if __name__ == '__main__':
555
    main()