Statistics
| Branch: | Tag: | Revision:

root / tools / snf-admin @ cf8482f2

History | View | Annotate | Download (16.9 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):
82
    for item in items:
83
        print '%d %s' % (item.id, item.name)
84
        if detail:
85
            print_dict(item.__dict__, exclude=('id', 'name'))
86
            print
87

    
88

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

    
129

    
130
# Server commands
131

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

    
162

    
163
# User commands
164

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

    
187

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

    
210

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

    
222

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

    
240

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

    
284

    
285
# Image commands
286

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

    
304

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

    
334

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

    
390

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

    
435

    
436
# Flavor commands
437

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

    
464

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

    
478

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

    
496

    
497
def print_usage(exe, groups, group=None, shortcut=False):
498
    nop = Command(exe, [])
499
    nop.parser.print_help()
500
    if group:
501
        groups = {group: groups[group]}
502

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

    
514

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

    
547

    
548
if __name__ == '__main__':
549
    main()