Revision ed02e7e1

/dev/null
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 httplib import HTTPConnection, HTTPSConnection
37
from optparse import OptionParser
38
from os.path import basename
39
from sys import argv, exit
40
from urlparse import urlparse
41

  
42
import json
43

  
44

  
45
DEFAULT_API_URL = 'http://127.0.0.1:8000/api/v1.1'
46
DEFAULT_TOKEN = '46e427d657b20defe352804f0eb6f8a2'
47

  
48
MARGIN = 14
49

  
50
commands = {}
51

  
52
def command_name(name):
53
    def decorator(cls):
54
        commands[name] = cls
55
        return cls
56
    return decorator
57

  
58

  
59
def print_addresses(networks):
60
    for i, net in enumerate(networks):
61
        key = 'addresses:'.rjust(MARGIN + 1) if i == 0 else ' ' * (MARGIN + 1)
62
        addr = ''
63
        if 'values' in net:
64
            addr = '[%s]' % ' '.join(ip['addr'] for ip in net['values'])
65
        
66
        val = '%s/%s %s %s' % (net['id'], net['name'], net['mac'], addr)
67
        if 'firewallProfile' in net:
68
            val += ' - %s' % net['firewallProfile']
69
        print '%s %s' % (key, val)
70

  
71
def print_dict(d, show_empty=True):
72
    for key, val in sorted(d.items()):
73
        if key == 'metadata':
74
            val = ', '.join('%s="%s"' % x for x in val['values'].items())
75
        elif key == 'addresses':
76
            print_addresses(val['values'])
77
            continue
78
        elif key == 'servers':
79
            val = ', '.join(str(server_id) for server_id in val['values'])
80
        if val or show_empty:
81
            print '%s: %s' % (key.rjust(MARGIN), val)
82

  
83

  
84
class Command(object):
85
    def __init__(self, argv):
86
        parser = OptionParser()
87
        parser.add_option('--apiurl',
88
                            dest='apiurl',
89
                            metavar='URL',
90
                            default=DEFAULT_API_URL,
91
                            help='use api API')
92
        parser.add_option('--token',
93
                            dest='token',
94
                            metavar='TOKEN',
95
                            default=DEFAULT_TOKEN,
96
                            help='use user token TOKEN')
97
        parser.add_option('-v',
98
                            action='store_true',
99
                            dest='verbose',
100
                            default=False,
101
                            help='use verbose output')
102
        self.add_options(parser)
103
        options, args = parser.parse_args(argv)
104
        
105
        # Add options to self
106
        for opt in parser.option_list:
107
            key = opt.dest
108
            if key:
109
                val = getattr(options, key)
110
                setattr(self, key, val)
111
        
112
        self.execute(*args)
113
    
114
    def add_options(self, parser):
115
        pass
116
    
117
    def execute(self, *args):
118
        pass
119
    
120
    def http_cmd(self, method, path, body=None, expected_status=200):
121
        p = urlparse(self.apiurl)
122
        if p.scheme == 'https':
123
            conn = HTTPSConnection(p.netloc)
124
        else:
125
            conn = HTTPConnection(p.netloc)
126

  
127
        kwargs = {}
128
        kwargs['headers'] = {'X-Auth-Token': self.token}
129
        if body:
130
            kwargs['headers']['Content-Type'] = 'application/json'
131
            kwargs['body'] = body
132
        conn.request(method, p.path + path, **kwargs)
133

  
134
        resp = conn.getresponse()
135
        if self.verbose:
136
            print '%d %s' % (resp.status, resp.reason)
137
            for key, val in resp.getheaders():
138
                print '%s: %s' % (key.capitalize(), val)
139
            print
140

  
141
        buf = resp.read() or '{}'
142
        try:
143
            reply = json.loads(buf)
144
        except ValueError:
145
            print 'Invalid response from the server.'
146
            if self.verbose:
147
                print buf
148
            exit(1)
149

  
150
        # If the response status is not the expected one,
151
        # assume an error has occured and treat the body
152
        # as a cloudfault.
153
        if resp.status != expected_status:
154
            if len(reply) == 1:
155
                key = reply.keys()[0]
156
                val = reply[key]
157
                print '%s: %s' % (key, val.get('message', ''))
158
                if self.verbose:
159
                    print val.get('details', '')
160
            else:
161
                print 'Invalid response from the server.'
162
            exit(1)
163

  
164
        return reply
165

  
166
    def http_get(self, path, expected_status=200):
167
        return self.http_cmd('GET', path, None, expected_status)
168
    
169
    def http_post(self, path, body, expected_status=202):
170
        return self.http_cmd('POST', path, body, expected_status)
171

  
172
    def http_put(self, path, body, expected_status=204):
173
        return self.http_cmd('PUT', path, body, expected_status)
174

  
175
    def http_delete(self, path, expected_status=204):
176
        return self.http_cmd('DELETE', path, None, expected_status)
177

  
178

  
179
@command_name('ls')
180
class ListServers(Command):
181
    description = 'list servers'
182
    
183
    def add_options(self, parser):
184
        parser.add_option('-l', action='store_true', dest='detail', default=False,
185
                            help='show detailed output')
186
        parser.add_option('-a', action='store_true', dest='show_empty', default=False,
187
                            help='include empty values')
188
    
189
    def execute(self):
190
        path = '/servers/detail' if self.detail else '/servers'
191
        reply = self.http_get(path)
192

  
193
        for server in reply['servers']['values']:
194
            id = server.pop('id')
195
            name = server.pop('name')
196
            if self.detail:
197
                print '%d %s' % (id, name)
198
                print_dict(server, self.show_empty)
199
                print
200
            else:
201
                print '%3d %s' % (id, name)
202

  
203

  
204
@command_name('info')
205
class GetServerDetails(Command):
206
    description = 'get server details'
207
    syntax = '<server id>'
208
    
209
    def add_options(self, parser):
210
        parser.add_option('-a', action='store_true', dest='show_empty', default=False,
211
                            help='include empty values')
212
    
213
    def execute(self, server_id):
214
        path = '/servers/%d' % int(server_id)
215
        reply = self.http_get(path)
216
        server = reply['server']
217
        server.pop('id')
218
        print_dict(server, self.show_empty)
219
        
220

  
221
@command_name('create')
222
class CreateServer(Command):
223
    description = 'create server'
224
    syntax = '<server name>'
225
    
226
    def add_options(self, parser):
227
        parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1,
228
                            help='use flavor FLAVOR_ID')
229
        parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1,
230
                            help='use image IMAGE_ID')
231
    
232
    def execute(self, name):
233
        server = {
234
            'name': name,
235
            'flavorRef': self.flavor,
236
            'imageRef': self.image}
237
        body = json.dumps({'server': server})
238
        reply = self.http_post('/servers', body)
239
        server = reply['server']
240
        print_dict(server)
241

  
242

  
243
@command_name('rename')
244
class UpdateServerName(Command):
245
    description = 'update server name'
246
    syntax = '<server id> <new name>'
247
    
248
    def execute(self, server_id, name):
249
        path = '/servers/%d' % int(server_id)
250
        body = json.dumps({'server': {'name': name}})
251
        self.http_put(path, body)
252

  
253

  
254
@command_name('delete')
255
class DeleteServer(Command):
256
    description = 'delete server'
257
    syntax = '<server id>'
258
    
259
    def execute(self, server_id):
260
        path = '/servers/%d' % int(server_id)
261
        self.http_delete(path)
262

  
263

  
264
@command_name('reboot')
265
class RebootServer(Command):
266
    description = 'reboot server'
267
    syntax = '<server id>'
268
    
269
    def add_options(self, parser):
270
        parser.add_option('-f', action='store_true', dest='hard', default=False,
271
                            help='perform a hard reboot')
272
    
273
    def execute(self, server_id):
274
        path = '/servers/%d/action' % int(server_id)
275
        type = 'HARD' if self.hard else 'SOFT'
276
        body = json.dumps({'reboot': {'type': type}})
277
        self.http_post(path, body)
278
    
279

  
280
@command_name('start')
281
class StartServer(Command):
282
    description = 'start server'
283
    syntax = '<server id>'
284
    
285
    def execute(self, server_id):
286
        path = '/servers/%d/action' % int(server_id)
287
        body = json.dumps({'start': {}})
288
        self.http_post(path, body)
289

  
290

  
291
@command_name('shutdown')
292
class StartServer(Command):
293
    description = 'shutdown server'
294
    syntax = '<server id>'
295
    
296
    def execute(self, server_id):
297
        path = '/servers/%d/action' % int(server_id)
298
        body = json.dumps({'shutdown': {}})
299
        self.http_post(path, body)
300

  
301

  
302
@command_name('console')
303
class ServerConsole(Command):
304
    description = 'get VNC console'
305
    syntax = '<server id>'
306
    
307
    def execute(self, server_id):
308
        path = '/servers/%d/action' % int(server_id)
309
        body = json.dumps({'console': {'type': 'vnc'}})
310
        reply = self.http_cmd('POST', path, body, 200)
311
        print_dict(reply['console'])
312

  
313

  
314
@command_name('profile')
315
class SetFirewallProfile(Command):
316
    description = 'set the firewall profile'
317
    syntax = '<server id> <profile>'
318
    
319
    def execute(self, server_id, profile):
320
        path = '/servers/%d/action' % int(server_id)
321
        body = json.dumps({'firewallProfile': {'profile': profile}})
322
        self.http_cmd('POST', path, body, 202)
323

  
324

  
325
@command_name('lsaddr')
326
class ListAddresses(Command):
327
    description = 'list server addresses'
328
    syntax = '<server id> [network]'
329
    
330
    def execute(self, server_id, network=None):
331
        path = '/servers/%d/ips' % int(server_id)
332
        if network:
333
            path += '/%s' % network
334
        reply = self.http_get(path)
335
        
336
        addresses = [reply['network']] if network else reply['addresses']['values']
337
        print_addresses(addresses)
338

  
339

  
340
@command_name('lsflv')
341
class ListFlavors(Command):
342
    description = 'list flavors'
343
    
344
    def add_options(self, parser):
345
        parser.add_option('-l', action='store_true', dest='detail', default=False,
346
                            help='show detailed output')
347
    
348
    def execute(self):
349
        path = '/flavors/detail' if self.detail else '/flavors'
350
        reply = self.http_get(path)
351
        
352
        for flavor in reply['flavors']['values']:
353
            id = flavor.pop('id')
354
            name = flavor.pop('name')
355
            details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
356
            print '%3d %s %s' % (id, name, details)
357

  
358

  
359
@command_name('flvinfo')
360
class GetFlavorDetails(Command):
361
    description = 'get flavor details'
362
    syntax = '<flavor id>'
363
    
364
    def execute(self, flavor_id):
365
        path = '/flavors/%d' % int(flavor_id)
366
        reply = self.http_get(path)
367
        
368
        flavor = reply['flavor']
369
        id = flavor.pop('id')
370
        name = flavor.pop('name')
371
        details = ' '.join('%s=%s' % item for item in sorted(flavor.items()))
372
        print '%3d %s %s' % (id, name, details)
373

  
374

  
375
@command_name('lsimg')
376
class ListImages(Command):
377
    description = 'list images'
378
    
379
    def add_options(self, parser):
380
        parser.add_option('-l', action='store_true', dest='detail', default=False,
381
                            help='show detailed output')
382
    
383
    def execute(self):
384
        path = '/images/detail' if self.detail else '/images'
385
        reply = self.http_get(path)
386
        
387
        for image in reply['images']['values']:
388
            id = image.pop('id')
389
            name = image.pop('name')
390
            if self.detail:
391
                print '%d %s' % (id, name)
392
                print_dict(image)
393
                print
394
            else:
395
                print '%3d %s' % (id, name)
396

  
397

  
398
@command_name('imginfo')
399
class GetImageDetails(Command):
400
    description = 'get image details'
401
    syntax = '<image id>'
402
    
403
    def execute(self, image_id):
404
        path = '/images/%d' % int(image_id)
405
        reply = self.http_get(path)
406
        image = reply['image']
407
        image.pop('id')
408
        print_dict(image)
409

  
410

  
411
@command_name('createimg')
412
class CreateImage(Command):
413
    description = 'create image'
414
    syntax = '<server id> <image name>'
415
    
416
    def execute(self, server_id, name):
417
        image = {'name': name, 'serverRef': int(server_id)}
418
        body = json.dumps({'image': image})
419
        reply = self.http_post('/images', body)
420
        print_dict(reply['image'])
421

  
422
@command_name('deleteimg')
423
class DeleteImage(Command):
424
    description = 'delete image'
425
    syntax = '<image id>'
426
    
427
    def execute(self, image_id):
428
        path = '/images/%d' % int(image_id)
429
        self.http_delete(path)
430

  
431
@command_name('lsmeta')
432
class ListServerMeta(Command):
433
    description = 'list server meta'
434
    syntax = '<server id> [key]'
435

  
436
    def execute(self, server_id, key=None):
437
        path = '/servers/%d/meta' % int(server_id)
438
        if key:
439
            path += '/' + key
440
        reply = self.http_get(path)
441
        if key:
442
            print_dict(reply['meta'])
443
        else:
444
            print_dict(reply['metadata']['values'])
445

  
446
@command_name('setmeta')
447
class UpdateServerMeta(Command):
448
    description = 'update server meta'
449
    syntax = '<server id> <key> <val>'
450

  
451
    def execute(self, server_id, key, val):
452
        path = '/servers/%d/meta' % int(server_id)
453
        metadata = {key: val}
454
        body = json.dumps({'metadata': metadata})
455
        reply = self.http_post(path, body, expected_status=201)
456
        print_dict(reply['metadata'])
457

  
458
@command_name('addmeta')
459
class CreateServerMeta(Command):
460
    description = 'add server meta'
461
    syntax = '<server id> <key> <val>'
462

  
463
    def execute(self, server_id, key, val):
464
        path = '/servers/%d/meta/%s' % (int(server_id), key)
465
        meta = {key: val}
466
        body = json.dumps({'meta': meta})
467
        reply = self.http_put(path, body, expected_status=201)
468
        print_dict(reply['meta'])
469

  
470
@command_name('delmeta')
471
class DeleteServerMeta(Command):
472
    description = 'delete server meta'
473
    syntax = '<server id> <key>'
474

  
475
    def execute(self, server_id, key):
476
        path = '/servers/%d/meta/%s' % (int(server_id), key)
477
        reply = self.http_delete(path)
478

  
479
@command_name('lsimgmeta')
480
class ListImageMeta(Command):
481
    description = 'list image meta'
482
    syntax = '<image id> [key]'
483

  
484
    def execute(self, image_id, key=None):
485
        path = '/images/%d/meta' % int(image_id)
486
        if key:
487
            path += '/' + key
488
        reply = self.http_get(path)
489
        if key:
490
            print_dict(reply['meta'])
491
        else:
492
            print_dict(reply['metadata']['values'])
493

  
494
@command_name('setimgmeta')
495
class UpdateImageMeta(Command):
496
    description = 'update image meta'
497
    syntax = '<image id> <key> <val>'
498

  
499
    def execute(self, image_id, key, val):
500
        path = '/images/%d/meta' % int(image_id)
501
        metadata = {key: val}
502
        body = json.dumps({'metadata': metadata})
503
        reply = self.http_post(path, body, expected_status=201)
504
        print_dict(reply['metadata'])
505

  
506
@command_name('addimgmeta')
507
class CreateImageMeta(Command):
508
    description = 'add image meta'
509
    syntax = '<image id> <key> <val>'
510

  
511
    def execute(self, image_id, key, val):
512
        path = '/images/%d/meta/%s' % (int(image_id), key)
513
        meta = {key: val}
514
        body = json.dumps({'meta': meta})
515
        reply = self.http_put(path, body, expected_status=201)
516
        print_dict(reply['meta'])
517

  
518
@command_name('delimgmeta')
519
class DeleteImageMeta(Command):
520
    description = 'delete image meta'
521
    syntax = '<image id> <key>'
522

  
523
    def execute(self, image_id, key):
524
        path = '/images/%d/meta/%s' % (int(image_id), key)
525
        reply = self.http_delete(path)
526

  
527

  
528
@command_name('lsnet')
529
class ListNetworks(Command):
530
    description = 'list networks'
531

  
532
    def add_options(self, parser):
533
        parser.add_option('-l', action='store_true', dest='detail', default=False,
534
                            help='show detailed output')
535

  
536
    def execute(self):
537
        path = '/networks/detail' if self.detail else '/networks'
538
        reply = self.http_get(path)
539

  
540
        for network in reply['networks']['values']:
541
            id = network.pop('id')
542
            name = network.pop('name')
543
            if self.detail:
544
                print '%s %s' % (id, name)
545
                print_dict(network)
546
                print
547
            else:
548
                print '%3s %s' % (id, name)
549

  
550

  
551
@command_name('createnet')
552
class CreateNetwork(Command):
553
    description = 'create network'
554
    syntax = '<network name>'
555

  
556
    def execute(self, name):
557
        network = {'name': name}
558
        body = json.dumps({'network': network})
559
        reply = self.http_post('/networks', body)
560
        print_dict(reply['network'])
561

  
562

  
563
@command_name('netinfo')
564
class GetNetworkDetails(Command):
565
    description = 'get network details'
566
    syntax = '<network id>'
567

  
568
    def execute(self, network_id):
569
        path = '/networks/%d' % int(network_id)
570
        reply = self.http_get(path)
571
        net = reply['network']
572
        name = net.pop('id')
573
        print_dict(net)
574

  
575

  
576
@command_name('renamenet')
577
class UpdateNetworkName(Command):
578
    description = 'update network name'
579
    syntax = '<network_id> <new name>'
580

  
581
    def execute(self, network_id, name):
582
        path = '/networks/%d' % int(network_id)
583
        body = json.dumps({'network': {'name': name}})
584
        self.http_put(path, body)
585

  
586

  
587
@command_name('deletenet')
588
class DeleteNetwork(Command):
589
    description = 'delete network'
590
    syntax = '<network id>'
591

  
592
    def execute(self, network_id):
593
        path = '/networks/%d' % int(network_id)
594
        self.http_delete(path)
595

  
596

  
597
@command_name('connect')
598
class AddNetwork(Command):
599
    description = 'connect a server to a network'
600
    syntax = '<server id> <network id>'
601

  
602
    def execute(self, server_id, network_id):
603
        path = '/networks/%d/action' % int(network_id)
604
        body = json.dumps({'add': {'serverRef': server_id}})
605
        self.http_post(path, body, expected_status=202)
606

  
607

  
608
@command_name('disconnect')
609
class RemoveNetwork(Command):
610
    description = 'disconnect a server from a network'
611
    syntax = '<server id> <network id>'
612

  
613
    def execute(self, server_id, network_id):
614
        path = '/networks/%s/action' % int(network_id)
615
        body = json.dumps({'remove': {'serverRef': server_id}})
616
        self.http_post(path, body, expected_status=202)
617

  
618
@command_name('stats')
619
class ServerStats(Command):
620
    description = 'get server stats'
621
    syntax = '<server id>'
622

  
623
    def execute(self, server_id):
624
        path = '/servers/%d/stats' % int(server_id)
625
        reply = self.http_get(path)
626
        stats = reply['stats']
627
        stats.pop('serverRef')
628
        print_dict(stats)
629

  
630

  
631
def print_usage():
632
    print 'Usage: %s <command>' % basename(argv[0])
633
    print
634
    print 'Commands:'
635
    for name, cls in sorted(commands.items()):
636
        description = getattr(cls, 'description', '')
637
        print '  %s %s' % (name.ljust(12), description)
638

  
639
def main():
640
    try:
641
        name = argv[1]    
642
        cls = commands[name]
643
    except (IndexError, KeyError):
644
        print_usage()
645
        exit(1)
646
    
647
    try:
648
        cls(argv[2:])
649
    except TypeError:
650
        syntax = getattr(cls, 'syntax', '')
651
        if syntax:
652
            print 'Syntax: %s %s' % (name, syntax)
653
        else:
654
            print 'Invalid syntax'
655
        exit(1)
656

  
657

  
658
if __name__ == '__main__':
659
    main()
/dev/null
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 sys
46

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

  
52
from synnefo.db import models
53
from synnefo.invitations.invitations import add_invitation, send_invitation
54
from synnefo.logic import backend, users
55
from synnefo.util.dictconfig import dictConfig
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
    name = getattr(item, 'name', '')
79
    print '%d %s' % (item.id, name)
80
    print_dict(item.__dict__, exclude=('id', 'name'))
81

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

  
93

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

  
127

  
128
# Server commands
129

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

  
161

  
162
# User commands
163

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

  
189

  
190
class InviteUser(Command):
191
    group = 'user'
192
    name = 'invite'
193
    syntax = '<inviter id> <invitee name> <invitee email>'
194
    description = 'invite a user'
195
    
196
    def main(self, inviter_id, name, email):
197
        name = name.decode('utf8')
198
        inviter = get_user(inviter_id)
199
        inv = add_invitation(inviter, name, email)
200
        send_invitation(inv)
201

  
202

  
203
class ListUsers(Command):
204
    group = 'user'
205
    name = 'list'
206
    syntax = '[user id]'
207
    description = 'list users'
208
    
209
    def add_options(self, parser):
210
        parser.add_option('-a', action='store_true', dest='show_deleted',
211
                        default=False, help='also list deleted users')
212
        parser.add_option('-l', action='store_true', dest='detail',
213
                        default=False, help='show detailed output')
214
    
215
    def main(self, user_id=None):
216
        if user_id:
217
            users = [models.SynnefoUser.objects.get(id=user_id)]
218
        else:
219
            users = models.SynnefoUser.objects.order_by('id')
220
            if not self.show_deleted:
221
                users = users.exclude(state='DELETED')
222
        print_items(users, self.detail, keys=('id', 'name', 'uniq'))
223

  
224

  
225
class ModifyUser(Command):
226
    group = 'user'
227
    name = 'modify'
228
    syntax = '<user id>'
229
    description = 'modify a user'
230
    
231
    def add_options(self, parser):
232
        types = ', '.join(x[0] for x in models.SynnefoUser.ACCOUNT_TYPE)
233
        states = ', '.join(x[0] for x in models.SynnefoUser.ACCOUNT_STATE)
234
        
235
        parser.add_option('--credit', dest='credit', metavar='VALUE',
236
                            help='set user credits')
237
        parser.add_option('--invitations', dest='invitations',
238
                            metavar='VALUE', help='set max invitations')
239
        parser.add_option('--realname', dest='realname', metavar='NAME',
240
                            help='set real name of user')
241
        parser.add_option('--type', dest='type', metavar='TYPE',
242
                            help='set user type (%s)' % types)
243
        parser.add_option('--state', dest='state', metavar='STATE',
244
                            help='set user state (%s)' % states)
245
        parser.add_option('--uniq', dest='uniq', metavar='ID',
246
                            help='set external unique ID')
247
        parser.add_option('--username', dest='username', metavar='NAME',
248
                            help='set username')
249
    
250
    def main(self, user_id):
251
        user = get_user(user_id)
252
        
253
        if self.credit:
254
            user.credit = self.credit
255
        if self.invitations:
256
            user.max_invitations = self.invitations
257
        if self.realname:
258
            user.realname = self.realname
259
        if self.type:
260
            allowed = [x[0] for x in models.SynnefoUser.ACCOUNT_TYPE]
261
            if self.type not in allowed:
262
                valid = ', '.join(allowed)
263
                print 'Invalid type. Must be one of:', valid
264
                return
265
            user.type = self.type
266
        if self.state:
267
            allowed = [x[0] for x in models.SynnefoUser.ACCOUNT_STATE]
268
            if self.state not in allowed:
269
                valid = ', '.join(allowed)
270
                print 'Invalid state. Must be one of:', valid
271
                return
272
            user.state = self.state
273
        if self.uniq:
274
            user.uniq = self.uniq
275
        if self.username:
276
            user.name = self.username
277
        
278
        user.save()
279
        print_item(user)
280

  
281

  
282
# Image commands
283

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

  
305

  
306
class RegisterImage(Command):
307
    group = 'image'
308
    name = 'register'
309
    syntax = '<name> <backend id> <format>'
310
    description = 'register an image'
311
    
312
    def add_options(self, parser):
313
        parser.add_option('--meta', dest='meta', action='append',
314
                            metavar='KEY=VAL',
315
                            help='add metadata (can be used multiple times)')
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, format):
322
        formats = [x[0] for x in models.Image.FORMATS]
323
        if format not in formats:
324
            valid = ', '.join(formats)
325
            print 'Invalid format. Must be one of:', valid
326
            return
327
        
328
        user = None
329
        if self.uid:
330
            user = get_user(self.uid)
331
            if not user:
332
                print 'Unknown user id'
333
                return
334
        
335
        image = models.Image.objects.create(
336
            name=name,
337
            state='ACTIVE',
338
            owner=user,
339
            backend_id=backend_id,
340
            format=format,
341
            public=self.public)
342
        
343
        if self.meta:
344
            for m in self.meta:
345
                key, sep, val = m.partition('=')
346
                if key and val:
347
                    image.metadata.create(meta_key=key, meta_value=val)
348
                else:
349
                    print 'WARNING: Ignoring meta', m
350
        
351
        print_item(image)
352

  
353

  
354
class ModifyImage(Command):
355
    group = 'image'
356
    name = 'modify'
357
    syntax = '<image id>'
358
    description = 'modify an image'
359
    
360
    def add_options(self, parser):
361
        states = ', '.join(x[0] for x in models.Image.IMAGE_STATES)
362
        formats = ', '.join(x[0] for x in models.Image.FORMATS)
363

  
364
        parser.add_option('-b', dest='backend_id', metavar='BACKEND_ID',
365
                            help='set image backend id')
366
        parser.add_option('-f', dest='format', metavar='FORMAT',
367
                            help='set image format (%s)' % formats)
368
        parser.add_option('-n', dest='name', metavar='NAME',
369
                            help='set image name')
370
        parser.add_option('--public', action='store_true', dest='public',
371
                            default=False, help='make image public')
372
        parser.add_option('--nopublic', action='store_true', dest='private',
373
                            default=False, help='make image private')
374
        parser.add_option('-s', dest='state', metavar='STATE', default=False,
375
                            help='set image state (%s)' % states)
376
        parser.add_option('-u', dest='uid', metavar='UID',
377
                            help='assign image to user with id UID')
378
    
379
    def main(self, image_id):
380
        try:
381
            image = models.Image.objects.get(id=image_id)
382
        except:
383
            print 'Image not found'
384
            return
385
        
386
        if self.backend_id:
387
            image.backend_id = self.backend_id
388
        if self.format:
389
            allowed = [x[0] for x in models.Image.FORMATS]
390
            if self.format not in allowed:
391
                valid = ', '.join(allowed)
392
                print 'Invalid format. Must be one of:', valid
393
                return
394
            image.format = self.format
395
        if self.name:
396
            image.name = self.name
397
        if self.public:
398
            image.public = True
399
        if self.private:
400
            image.public = False
401
        if self.state:
402
            allowed = [x[0] for x in models.Image.IMAGE_STATES]
403
            if self.state not in allowed:
404
                valid = ', '.join(allowed)
405
                print 'Invalid state. Must be one of:', valid
406
                return
407
            image.state = self.state
408
        if self.uid:
409
            image.owner = get_user(self.uid)
410
        
411
        image.save()
412
        print_item(image)
413

  
414

  
415
class ModifyImageMeta(Command):
416
    group = 'image'
417
    name = 'meta'
418
    syntax = '<image id> [key[=val]]'
419
    description = 'get and manipulate image metadata'
420
    
421
    def main(self, image_id, arg=''):
422
        try:
423
            image = models.Image.objects.get(id=image_id)
424
        except:
425
            print 'Image not found'
426
            return
427
        
428
        key, sep, val = arg.partition('=')
429
        if not sep:
430
            val = None
431
        
432
        if not key:
433
            metadata = {}
434
            for meta in image.metadata.order_by('meta_key'):
435
                metadata[meta.meta_key] = meta.meta_value
436
            print_dict(metadata)
437
            return
438
        
439
        try:
440
            meta = image.metadata.get(meta_key=key)
441
        except models.ImageMetadata.DoesNotExist:
442
            meta = None
443
        
444
        if val is None:
445
            if meta:
446
                print_dict({key: meta.meta_value})
447
            return
448
        
449
        if val:
450
            if not meta:
451
                meta = image.metadata.create(meta_key=key)
452
            meta.meta_value = val
453
            meta.save()
454
        else:
455
            # Delete if val is empty
456
            if meta:
457
                meta.delete()
458

  
459

  
460
# Flavor commands
461

  
462
class CreateFlavor(Command):
463
    group = 'flavor'
464
    name = 'create'
465
    syntax = '<cpu>[,<cpu>,...] <ram>[,<ram>,...] <disk>[,<disk>,...]'
466
    description = 'create one or more flavors'
467
    
468
    def add_options(self, parser):
469
        disk_templates = ', '.join(t for t in settings.GANETI_DISK_TEMPLATES)
470
        parser.add_option('--disk-template',
471
            dest='disk_template',
472
            metavar='TEMPLATE',
473
            default=settings.DEFAULT_GANETI_DISK_TEMPLATE,
474
            help='available disk templates: %s' % disk_templates)
475
    
476
    def main(self, cpu, ram, disk):
477
        cpus = cpu.split(',')
478
        rams = ram.split(',')
479
        disks = disk.split(',')
480
        
481
        flavors = []
482
        for cpu, ram, disk in product(cpus, rams, disks):
483
            try:
484
                flavors.append((int(cpu), int(ram), int(disk)))
485
            except ValueError:
486
                print 'Invalid values'
487
                return
488
        
489
        created = []
490
        
491
        for cpu, ram, disk in flavors:
492
            flavor = models.Flavor.objects.create(
493
                cpu=cpu,
494
                ram=ram,
495
                disk=disk,
496
                disk_template=self.disk_template)
497
            created.append(flavor)
498
        
499
        print_items(created, detail=True)
500

  
501

  
502
class DeleteFlavor(Command):
503
    group = 'flavor'
504
    name = 'delete'
505
    syntax = '<flavor id> [<flavor id>] [...]'
506
    description = 'delete one or more flavors'
507
    
508
    def main(self, *args):
509
        if not args:
510
            raise TypeError
511
        for flavor_id in args:
512
            flavor = models.Flavor.objects.get(id=int(flavor_id))
513
            flavor.deleted = True
514
            flavor.save()
515

  
516

  
517
class ListFlavors(Command):
518
    group = 'flavor'
519
    name = 'list'
520
    syntax = '[flavor id]'
521
    description = 'list images'
522
    
523
    def add_options(self, parser):
524
        parser.add_option('-a', action='store_true', dest='show_deleted',
525
                default=False, help='also list deleted flavors')
526
        parser.add_option('-l', action='store_true', dest='detail',
527
                        default=False, help='show detailed output')
528
    
529
    def main(self, flavor_id=None):
530
        if flavor_id:
531
            flavors = [models.Flavor.objects.get(id=flavor_id)]
532
        else:
533
            flavors = models.Flavor.objects.order_by('id')
534
            if not self.show_deleted:
535
                flavors = flavors.exclude(deleted=True)
536
        print_items(flavors, self.detail)
537

  
538

  
539
class ShowStats(Command):
540
    group = 'stats'
541
    name = None
542
    description = 'show statistics'
543

  
544
    def main(self):
545
        stats = {}
546
        stats['Users'] = models.SynnefoUser.objects.count()
547
        stats['Images'] = models.Image.objects.exclude(state='DELETED').count()
548
        stats['Flavors'] = models.Flavor.objects.count()
549
        stats['VMs'] = models.VirtualMachine.objects.filter(deleted=False).count()
550
        stats['Networks'] = models.Network.objects.exclude(state='DELETED').count()
551
        stats['Invitations'] = models.Invitations.objects.count()
552
        
553
        stats['Ganeti Instances'] = len(backend.get_ganeti_instances())
554
        stats['Ganeti Nodes'] = len(backend.get_ganeti_nodes())
555
        stats['Ganeti Jobs'] = len(backend.get_ganeti_jobs())
556
        
557
        print_dict(stats)
558

  
559

  
560
class ListInvitations(Command):
561
    group = 'invitation'
562
    name = 'list'
563
    syntax = '[invitation id]'
564
    description = 'list invitations'
565
    
566
    def main(self, invitation_id=None):
567
        if invitation_id:
568
            invitations = [models.Invitations.objects.get(id=invitation_id)]
569
        else:
570
            invitations = models.Invitations.objects.order_by('id')
571
        print_items(invitations, detail=True, keys=('id',))
572

  
573

  
574
class ResendInviation(Command):
575
    group = 'invitation'
576
    name = 'resend'
577
    syntax = '<invitation id>'
578
    description = 'resend an invitation'
579

  
580
    def main(self, invitation_id):
581
        invitation = models.Invitations.objects.get(id=invitation_id)
582
        send_invitation(invitation)
583

  
584

  
585
def print_usage(exe, groups, group=None, shortcut=False):
586
    nop = Command(exe, [])
587
    nop.parser.print_help()
588
    if group:
589
        groups = {group: groups[group]}
590

  
591
    print
592
    print 'Commands:'
593
    
594
    for group, commands in sorted(groups.items()):
595
        for command, cls in sorted(commands.items()):
596
            if cls.hidden:
597
                continue
598
            name = '  %s %s' % (group, command or '')
599
            print '%s %s' % (name.ljust(22), cls.description)
600
        print
601

  
602

  
603
def main():
604
    groups = defaultdict(dict)
605
    module = sys.modules[__name__]
606
    for name, cls in inspect.getmembers(module, inspect.isclass):
607
        if not issubclass(cls, Command) or cls == Command:
608
            continue
609
        groups[cls.group][cls.name] = cls
610
    
611
    argv = list(sys.argv)
612
    exe = basename(argv.pop(0))
613
    prefix, sep, suffix = exe.partition('-')
614
    if sep and prefix == 'snf' and suffix in groups:
615
        # Allow shortcut aliases like snf-image, snf-server, etc
616
        group = suffix
617
    else:
618
        group = argv.pop(0) if argv else None
619
        if group in groups:
620
            exe = '%s %s' % (exe, group)
621
        else:
622
            exe = '%s <group>' % exe
623
            group = None
624
    
625
    command = argv.pop(0) if argv else None
626
    
627
    if group not in groups or command not in groups[group]:
628
        print_usage(exe, groups, group)
629
        sys.exit(1)
630
    
631
    cls = groups[group][command]
632
    cmd = cls(exe, argv)
633
    cmd.execute()
634

  
635

  
636
if __name__ == '__main__':
637
    dictConfig(settings.SNFADMIN_LOGGING)
638
    main()
/dev/null
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
"""Perform integration testing on a running Synnefo deployment"""
37

  
38
import __main__
39
import datetime
40
import inspect
41
import logging
42
import os
43
import paramiko
44
import prctl
45
import subprocess
46
import signal
47
import socket
48
import struct
49
import sys
50
import time
51

  
52
from IPy import IP
53
from multiprocessing import Process, Queue
54
from random import choice
55

  
56
from kamaki.client import Client, ClientError
57
from vncauthproxy.d3des import generate_response as d3des_generate_response
58

  
59
# Use backported unittest functionality if Python < 2.7
60
try:
61
    import unittest2 as unittest
62
except ImportError:
63
    if sys.version_info < (2, 7):
64
        raise Exception("The unittest2 package is required for Python < 2.7")
65
    import unittest
66

  
67

  
68
API = None
69
TOKEN = None
70
DEFAULT_API = "http://dev67.dev.grnet.gr:8000/api/v1.1"
71
DEFAULT_TOKEN = "46e427d657b20defe352804f0eb6f8a2"
72
# A unique id identifying this test run
73
TEST_RUN_ID = datetime.datetime.strftime(datetime.datetime.now(),
74
                                         "%Y%m%d%H%M%S")
75
SNF_TEST_PREFIX = "snf-test-"
76

  
77
# Setup logging (FIXME - verigak)
78
logging.basicConfig(format="%(message)s")
79
log = logging.getLogger("burnin")
80
log.setLevel(logging.INFO)
81

  
82

  
83
class UnauthorizedTestCase(unittest.TestCase):
84
    def test_unauthorized_access(self):
85
        """Test access without a valid token fails"""
86
        c = Client(API, "123")
87
        with self.assertRaises(ClientError) as cm:
88
            c.list_servers()
89
        self.assertEqual(cm.exception.status, 401)
90

  
91

  
92
class ImagesTestCase(unittest.TestCase):
93
    """Test image lists for consistency"""
94
    @classmethod
95
    def setUpClass(cls):
96
        """Initialize kamaki, get (detailed) list of images"""
97
        log.info("Getting simple and detailed list of images")
98
        cls.client = Client(API, TOKEN)
99
        cls.images = cls.client.list_images()
100
        cls.dimages = cls.client.list_images(detail=True)
101

  
102
    def test_001_list_images(self):
103
        """Test image list actually returns images"""
104
        self.assertGreater(len(self.images), 0)
105

  
106
    def test_002_list_images_detailed(self):
107
        """Test detailed image list is the same length as list"""
108
        self.assertEqual(len(self.dimages), len(self.images))
109

  
110
    def test_003_same_image_names(self):
111
        """Test detailed and simple image list contain same names"""
112
        names = sorted(map(lambda x: x["name"], self.images))
113
        dnames = sorted(map(lambda x: x["name"], self.dimages))
114
        self.assertEqual(names, dnames)
115

  
116
    def test_004_unique_image_names(self):
117
        """Test images have unique names"""
118
        names = sorted(map(lambda x: x["name"], self.images))
119
        self.assertEqual(sorted(list(set(names))), names)
120

  
121
    def test_005_image_metadata(self):
122
        """Test every image has specific metadata defined"""
123
        keys = frozenset(["OS", "description", "size"])
124
        for i in self.dimages:
125
            self.assertTrue(keys.issubset(i["metadata"]["values"].keys()))
126

  
127

  
128
class FlavorsTestCase(unittest.TestCase):
129
    """Test flavor lists for consistency"""
130
    @classmethod
131
    def setUpClass(cls):
132
        """Initialize kamaki, get (detailed) list of flavors"""
133
        log.info("Getting simple and detailed list of flavors")
134
        cls.client = Client(API, TOKEN)
135
        cls.flavors = cls.client.list_flavors()
136
        cls.dflavors = cls.client.list_flavors(detail=True)
137

  
138
    def test_001_list_flavors(self):
139
        """Test flavor list actually returns flavors"""
140
        self.assertGreater(len(self.flavors), 0)
141

  
142
    def test_002_list_flavors_detailed(self):
143
        """Test detailed flavor list is the same length as list"""
144
        self.assertEquals(len(self.dflavors), len(self.flavors))
145

  
146
    def test_003_same_flavor_names(self):
147
        """Test detailed and simple flavor list contain same names"""
148
        names = sorted(map(lambda x: x["name"], self.flavors))
149
        dnames = sorted(map(lambda x: x["name"], self.dflavors))
150
        self.assertEqual(names, dnames)
151

  
152
    def test_004_unique_flavor_names(self):
153
        """Test flavors have unique names"""
154
        names = sorted(map(lambda x: x["name"], self.flavors))
155
        self.assertEqual(sorted(list(set(names))), names)
156

  
157
    def test_005_well_formed_flavor_names(self):
158
        """Test flavors have names of the form CxxRyyDzz
159

  
160
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
161

  
162
        """
163
        for f in self.dflavors:
164
            self.assertEqual("C%dR%dD%d" % (f["cpu"], f["ram"], f["disk"]),
165
                             f["name"],
166
                             "Flavor %s does not match its specs." % f["name"])
167

  
168

  
169
class ServersTestCase(unittest.TestCase):
170
    """Test server lists for consistency"""
171
    @classmethod
172
    def setUpClass(cls):
173
        """Initialize kamaki, get (detailed) list of servers"""
174
        log.info("Getting simple and detailed list of servers")
175
        cls.client = Client(API, TOKEN)
176
        cls.servers = cls.client.list_servers()
177
        cls.dservers = cls.client.list_servers(detail=True)
178

  
179
    def test_001_list_servers(self):
180
        """Test server list actually returns servers"""
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff