Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ c77131f4

History | View | Annotate | Download (33.4 kB)

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

    
34
from base64 import b64encode
35
from os.path import exists, expanduser
36
from io import StringIO
37
from pydoc import pager
38

    
39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.utils import remove_from_items, filter_dicts_by_dict
42
from kamaki.cli.errors import (
43
    raiseCLIError, CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
44
from kamaki.clients.cyclades import CycladesClient
45
from kamaki.cli.argument import (
46
    FlagArgument, ValueArgument, KeyValueArgument, RepeatableArgument,
47
    ProgressBarArgument, DateArgument, IntArgument)
48
from kamaki.cli.commands import _command_init, errors, addLogSettings
49
from kamaki.cli.commands import (
50
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
51

    
52

    
53
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
54
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
55
_commands = [server_cmds, flavor_cmds]
56

    
57

    
58
about_authentication = '\nUser Authentication:\
59
    \n* to check authentication: /user authenticate\
60
    \n* to set authentication token: /config set cloud.<cloud>.token <token>'
61

    
62
howto_personality = [
63
    'Defines a file to be injected to virtual servers file system.',
64
    'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
65
    '  [local-path=]PATH: local file to be injected (relative or absolute)',
66
    '  [server-path=]SERVER_PATH: destination location inside server Image',
67
    '  [owner=]OWNER: virtual servers user id for the remote file',
68
    '  [group=]GROUP: virtual servers group id or name for the remote file',
69
    '  [mode=]MODE: permission in octal (e.g., 0777)',
70
    'e.g., -p /tmp/my.file,owner=root,mode=0777']
71

    
72

    
73
class _service_wait(object):
74

    
75
    wait_arguments = dict(
76
        progress_bar=ProgressBarArgument(
77
            'do not show progress bar', ('-N', '--no-progress-bar'), False)
78
    )
79

    
80
    def _wait(
81
            self, service, service_id, status_method, current_status,
82
            countdown=True, timeout=60):
83
        (progress_bar, wait_cb) = self._safe_progress_bar(
84
            '%s %s: status is still %s' % (
85
                service, service_id, current_status),
86
            countdown=countdown, timeout=timeout)
87

    
88
        try:
89
            new_mode = status_method(
90
                service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
91
            if new_mode:
92
                self.error('%s %s: status is now %s' % (
93
                    service, service_id, new_mode))
94
            else:
95
                self.error('%s %s: status is still %s' % (
96
                    service, service_id, current_status))
97
        except KeyboardInterrupt:
98
            self.error('\n- canceled')
99
        finally:
100
            self._safe_progress_bar_finish(progress_bar)
101

    
102

    
103
class _server_wait(_service_wait):
104

    
105
    def _wait(self, server_id, current_status, timeout=60):
106
        super(_server_wait, self)._wait(
107
            'Server', server_id, self.client.wait_server, current_status,
108
            countdown=(current_status not in ('BUILD', )),
109
            timeout=timeout if current_status not in ('BUILD', ) else 100)
110

    
111

    
112
class _init_cyclades(_command_init):
113
    @errors.generic.all
114
    @addLogSettings
115
    def _run(self, service='compute'):
116
        if getattr(self, 'cloud', None):
117
            base_url = self._custom_url(service) or self._custom_url(
118
                'cyclades')
119
            if base_url:
120
                token = self._custom_token(service) or self._custom_token(
121
                    'cyclades') or self.config.get_cloud('token')
122
                self.client = CycladesClient(base_url=base_url, token=token)
123
                return
124
        else:
125
            self.cloud = 'default'
126
        if getattr(self, 'auth_base', False):
127
            cyclades_endpoints = self.auth_base.get_service_endpoints(
128
                self._custom_type('cyclades') or 'compute',
129
                self._custom_version('cyclades') or '')
130
            base_url = cyclades_endpoints['publicURL']
131
            token = self.auth_base.token
132
            self.client = CycladesClient(base_url=base_url, token=token)
133
        else:
134
            raise CLIBaseUrlError(service='cyclades')
135

    
136
    def main(self):
137
        self._run()
138

    
139

    
140
@command(server_cmds)
141
class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
142
    """List virtual servers accessible by user
143
    Use filtering arguments (e.g., --name-like) to manage long server lists
144
    """
145

    
146
    PERMANENTS = ('id', 'name')
147

    
148
    arguments = dict(
149
        detail=FlagArgument('show detailed output', ('-l', '--details')),
150
        since=DateArgument(
151
            'show only items since date (\' d/m/Y H:M:S \')',
152
            '--since'),
153
        limit=IntArgument(
154
            'limit number of listed virtual servers', ('-n', '--number')),
155
        more=FlagArgument(
156
            'output results in pages (-n to set items per page, default 10)',
157
            '--more'),
158
        enum=FlagArgument('Enumerate results', '--enumerate'),
159
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
160
        image_id=ValueArgument('filter by image id', ('--image-id')),
161
        user_id=ValueArgument('filter by user id', ('--user-id')),
162
        user_name=ValueArgument('filter by user name', ('--user-name')),
163
        status=ValueArgument(
164
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
165
            ('--status')),
166
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
167
        meta_like=KeyValueArgument(
168
            'print only if in key=value, the value is part of actual value',
169
            ('--metadata-like')),
170
    )
171

    
172
    def _add_user_name(self, servers):
173
        uuids = self._uuids2usernames(list(set(
174
                [srv['user_id'] for srv in servers] +
175
                [srv['tenant_id'] for srv in servers])))
176
        for srv in servers:
177
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
178
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
179
        return servers
180

    
181
    def _apply_common_filters(self, servers):
182
        common_filters = dict()
183
        if self['status']:
184
            common_filters['status'] = self['status']
185
        if self['user_id'] or self['user_name']:
186
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
187
            common_filters['user_id'] = uuid
188
        return filter_dicts_by_dict(servers, common_filters)
189

    
190
    def _filter_by_image(self, servers):
191
        iid = self['image_id']
192
        return [srv for srv in servers if srv['image']['id'] == iid]
193

    
194
    def _filter_by_flavor(self, servers):
195
        fid = self['flavor_id']
196
        return [srv for srv in servers if (
197
            '%s' % srv['image']['id'] == '%s' % fid)]
198

    
199
    def _filter_by_metadata(self, servers):
200
        new_servers = []
201
        for srv in servers:
202
            if not 'metadata' in srv:
203
                continue
204
            meta = [dict(srv['metadata'])]
205
            if self['meta']:
206
                meta = filter_dicts_by_dict(meta, self['meta'])
207
            if meta and self['meta_like']:
208
                meta = filter_dicts_by_dict(
209
                    meta, self['meta_like'], exact_match=False)
210
            if meta:
211
                new_servers.append(srv)
212
        return new_servers
213

    
214
    @errors.generic.all
215
    @errors.cyclades.connection
216
    @errors.cyclades.date
217
    def _run(self):
218
        withimage = bool(self['image_id'])
219
        withflavor = bool(self['flavor_id'])
220
        withmeta = bool(self['meta'] or self['meta_like'])
221
        withcommons = bool(
222
            self['status'] or self['user_id'] or self['user_name'])
223
        detail = self['detail'] or (
224
            withimage or withflavor or withmeta or withcommons)
225
        servers = self.client.list_servers(detail, self['since'])
226

    
227
        servers = self._filter_by_name(servers)
228
        servers = self._filter_by_id(servers)
229
        servers = self._apply_common_filters(servers)
230
        if withimage:
231
            servers = self._filter_by_image(servers)
232
        if withflavor:
233
            servers = self._filter_by_flavor(servers)
234
        if withmeta:
235
            servers = self._filter_by_metadata(servers)
236

    
237
        if self['detail'] and not (
238
                self['json_output'] or self['output_format']):
239
            servers = self._add_user_name(servers)
240
        elif not (self['detail'] or (
241
                self['json_output'] or self['output_format'])):
242
            remove_from_items(servers, 'links')
243
        if detail and not self['detail']:
244
            for srv in servers:
245
                for key in set(srv).difference(self.PERMANENTS):
246
                    srv.pop(key)
247
        kwargs = dict(with_enumeration=self['enum'])
248
        if self['more']:
249
            kwargs['out'] = StringIO()
250
            kwargs['title'] = ()
251
        if self['limit']:
252
            servers = servers[:self['limit']]
253
        self._print(servers, **kwargs)
254
        if self['more']:
255
            pager(kwargs['out'].getvalue())
256

    
257
    def main(self):
258
        super(self.__class__, self)._run()
259
        self._run()
260

    
261

    
262
@command(server_cmds)
263
class server_info(_init_cyclades, _optional_json):
264
    """Detailed information on a Virtual Machine"""
265

    
266
    arguments = dict(
267
        nics=FlagArgument(
268
            'Show only the network interfaces of this virtual server',
269
            '--nics'),
270
        network_id=ValueArgument(
271
            'Show the connection details to that network', '--network-id'),
272
        vnc=FlagArgument(
273
            'Show VNC connection information (valid for a short period)',
274
            '--vnc-credentials'),
275
        stats=FlagArgument('Get URLs for server statistics', '--stats'),
276
        diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
277
    )
278

    
279
    @errors.generic.all
280
    @errors.cyclades.connection
281
    @errors.cyclades.server_id
282
    def _run(self, server_id):
283
        vm = self.client.get_server_nics(server_id)
284
        if self['nics']:
285
            self._print(vm.get('attachments', []))
286
        elif self['network_id']:
287
            self._print(
288
                self.client.get_server_network_nics(
289
                    server_id, self['network_id']), self.print_dict)
290
        elif self['vnc']:
291
            self.error(
292
                '(!) For security reasons, the following credentials are '
293
                'invalidated\nafter a short time period, depending on the '
294
                'server settings\n')
295
            self._print(
296
                self.client.get_server_console(server_id), self.print_dict)
297
        elif self['stats']:
298
            self._print(
299
                self.client.get_server_stats(server_id), self.print_dict)
300
        elif self['diagnostics']:
301
            self._print(self.client.get_server_diagnostics(server_id))
302
        else:
303
            uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
304
            vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
305
            vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
306
            self._print(vm, self.print_dict)
307

    
308
    def main(self, server_id):
309
        super(self.__class__, self)._run()
310
        choose_one = ('nics', 'vnc', 'stats')
311
        count = len([a for a in choose_one if self[a]])
312
        if count > 1:
313
            raise CLIInvalidArgument('Invalid argument compination', details=[
314
                'Arguments %s cannot be used simultaneously' % ', '.join(
315
                    [self.arguments[a].lvalue for a in choose_one])])
316
        self._run(server_id=server_id)
317

    
318

    
319
class PersonalityArgument(KeyValueArgument):
320

    
321
    terms = (
322
        ('local-path', 'contents'),
323
        ('server-path', 'path'),
324
        ('owner', 'owner'),
325
        ('group', 'group'),
326
        ('mode', 'mode'))
327

    
328
    @property
329
    def value(self):
330
        return getattr(self, '_value', [])
331

    
332
    @value.setter
333
    def value(self, newvalue):
334
        if newvalue == self.default:
335
            return self.value
336
        self._value, input_dict = [], {}
337
        for i, terms in enumerate(newvalue):
338
            termlist = terms.split(',')
339
            if len(termlist) > len(self.terms):
340
                msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
341
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
342

    
343
            for k, v in self.terms:
344
                prefix = '%s=' % k
345
                for item in termlist:
346
                    if item.lower().startswith(prefix):
347
                        input_dict[k] = item[len(k) + 1:]
348
                        break
349
                    item = None
350
                if item:
351
                    termlist.remove(item)
352

    
353
            try:
354
                path = input_dict['local-path']
355
            except KeyError:
356
                path = termlist.pop(0)
357
                if not path:
358
                    raise CLIInvalidArgument(
359
                        '--personality: No local path specified',
360
                        details=howto_personality)
361

    
362
            if not exists(path):
363
                raise CLIInvalidArgument(
364
                    '--personality: File %s does not exist' % path,
365
                    details=howto_personality)
366

    
367
            self._value.append(dict(path=path))
368
            with open(expanduser(path)) as f:
369
                self._value[i]['contents'] = b64encode(f.read())
370
            for k, v in self.terms[1:]:
371
                try:
372
                    self._value[i][v] = input_dict[k]
373
                except KeyError:
374
                    try:
375
                        self._value[i][v] = termlist.pop(0)
376
                    except IndexError:
377
                        continue
378
                if k in ('mode', ) and self._value[i][v]:
379
                    try:
380
                        self._value[i][v] = int(self._value[i][v], 8)
381
                    except ValueError as ve:
382
                        raise CLIInvalidArgument(
383
                            'Personality mode must be in octal', details=[
384
                                '%s' % ve])
385

    
386

    
387
class NetworkArgument(RepeatableArgument):
388
    """[id=]NETWORK_ID[,[ip=]IP]"""
389

    
390
    @property
391
    def value(self):
392
        return getattr(self, '_value', self.default)
393

    
394
    @value.setter
395
    def value(self, new_value):
396
        for v in new_value or []:
397
            part1, sep, part2 = v.partition(',')
398
            netid, ip = '', ''
399
            if part1.startswith('id='):
400
                netid = part1[len('id='):]
401
            elif part1.startswith('ip='):
402
                ip = part1[len('ip='):]
403
            else:
404
                netid = part1
405
            if part2:
406
                if (part2.startswith('id=') and netid) or (
407
                        part2.startswith('ip=') and ip):
408
                    raise CLIInvalidArgument(
409
                        'Invalid network argument %s' % v, details=[
410
                        'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
411
                if part2.startswith('id='):
412
                    netid = part2[len('id='):]
413
                elif part2.startswith('ip='):
414
                    ip = part2[len('ip='):]
415
                elif netid:
416
                    ip = part2
417
                else:
418
                    netid = part2
419
            if not netid:
420
                raise CLIInvalidArgument(
421
                    'Invalid network argument %s' % v, details=[
422
                    'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
423
            self._value = getattr(self, '_value', [])
424
            self._value.append(dict(uuid=netid))
425
            if ip:
426
                self._value[-1]['fixed_ip'] = ip
427

    
428

    
429
@command(server_cmds)
430
class server_create(_init_cyclades, _optional_json, _server_wait):
431
    """Create a server (aka Virtual Machine)"""
432

    
433
    arguments = dict(
434
        server_name=ValueArgument('The name of the new server', '--name'),
435
        flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
436
        image_id=ValueArgument('The ID of the hardware image', '--image-id'),
437
        personality=PersonalityArgument(
438
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
439
        wait=FlagArgument('Wait server to build', ('-w', '--wait')),
440
        cluster_size=IntArgument(
441
            'Create a cluster of servers of this size. In this case, the name'
442
            'parameter is the prefix of each server in the cluster (e.g.,'
443
            'srv1, srv2, etc.',
444
            '--cluster-size'),
445
        max_threads=IntArgument(
446
            'Max threads in cluster mode (default 1)', '--threads'),
447
        network_configuration=NetworkArgument(
448
            'Connect server to network: [id=]NETWORK_ID[,[ip=]IP]        . '
449
            'Use only NETWORK_ID for private networks.        . '
450
            'Use NETWORK_ID,[ip=]IP for networks with IP.        . '
451
            'Can be repeated, mutually exclussive with --no-network',
452
            '--network'),
453
        no_network=FlagArgument(
454
            'Do not create any network NICs on the server.        . '
455
            'Mutually exclusive to --network        . '
456
            'If neither --network or --no-network are used, the default '
457
            'network policy is applied. This policy is configured on the '
458
            'cloud and kamaki is oblivious to it',
459
            '--no-network')
460
    )
461
    required = ('server_name', 'flavor_id', 'image_id')
462

    
463
    @errors.cyclades.cluster_size
464
    def _create_cluster(self, prefix, flavor_id, image_id, size):
465
        networks = self['network_configuration'] or (
466
            None if self['no_network'] else [])
467
        servers = [dict(
468
            name='%s%s' % (prefix, i if size > 1 else ''),
469
            flavor_id=flavor_id,
470
            image_id=image_id,
471
            personality=self['personality'],
472
            networks=networks) for i in range(1, 1 + size)]
473
        if size == 1:
474
            return [self.client.create_server(**servers[0])]
475
        self.client.MAX_THREADS = int(self['max_threads'] or 1)
476
        try:
477
            r = self.client.async_run(self.client.create_server, servers)
478
            return r
479
        except Exception as e:
480
            if size == 1:
481
                raise e
482
            try:
483
                requested_names = [s['name'] for s in servers]
484
                spawned_servers = [dict(
485
                    name=s['name'],
486
                    id=s['id']) for s in self.client.list_servers() if (
487
                        s['name'] in requested_names)]
488
                self.error('Failed to build %s servers' % size)
489
                self.error('Found %s matching servers:' % len(spawned_servers))
490
                self._print(spawned_servers, out=self._err)
491
                self.error('Check if any of these servers should be removed\n')
492
            except Exception as ne:
493
                self.error('Error (%s) while notifying about errors' % ne)
494
            finally:
495
                raise e
496

    
497
    @errors.generic.all
498
    @errors.cyclades.connection
499
    @errors.plankton.id
500
    @errors.cyclades.flavor_id
501
    def _run(self, name, flavor_id, image_id):
502
        for r in self._create_cluster(
503
                name, flavor_id, image_id, size=self['cluster_size'] or 1):
504
            if not r:
505
                self.error('Create %s: server response was %s' % (name, r))
506
                continue
507
            usernames = self._uuids2usernames(
508
                [r['user_id'], r['tenant_id']])
509
            r['user_id'] += ' (%s)' % usernames[r['user_id']]
510
            r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
511
            self._print(r, self.print_dict)
512
            if self['wait']:
513
                self._wait(r['id'], r['status'])
514
            self.writeln(' ')
515

    
516
    def main(self):
517
        super(self.__class__, self)._run()
518
        if self['no_network'] and self['network']:
519
            raise CLIInvalidArgument(
520
                'Invalid argument compination', importance=2, details=[
521
                'Arguments %s and %s are mutually exclusive' % (
522
                    self.arguments['no_network'].lvalue,
523
                    self.arguments['network'].lvalue)])
524
        self._run(
525
            name=self['server_name'],
526
            flavor_id=self['flavor_id'],
527
            image_id=self['image_id'])
528

    
529

    
530
class FirewallProfileArgument(ValueArgument):
531

    
532
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
533

    
534
    @property
535
    def value(self):
536
        return getattr(self, '_value', None)
537

    
538
    @value.setter
539
    def value(self, new_profile):
540
        if new_profile:
541
            new_profile = new_profile.upper()
542
            if new_profile in self.profiles:
543
                self._value = new_profile
544
            else:
545
                raise CLIInvalidArgument(
546
                    'Invalid firewall profile %s' % new_profile,
547
                    details=['Valid values: %s' % ', '.join(self.profiles)])
548

    
549

    
550
@command(server_cmds)
551
class server_modify(_init_cyclades, _optional_output_cmd):
552
    """Modify attributes of a virtual server"""
553

    
554
    arguments = dict(
555
        server_name=ValueArgument('The new name', '--name'),
556
        flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
557
        firewall_profile=FirewallProfileArgument(
558
            'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
559
            '--firewall'),
560
        metadata_to_set=KeyValueArgument(
561
            'Set metadata in key=value form (can be repeated)',
562
            '--metadata-set'),
563
        metadata_to_delete=RepeatableArgument(
564
            'Delete metadata by key (can be repeated)', '--metadata-del')
565
    )
566
    required = [
567
        'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
568
        'metadata_to_delete']
569

    
570
    @errors.generic.all
571
    @errors.cyclades.connection
572
    @errors.cyclades.server_id
573
    def _run(self, server_id):
574
        if self['server_name']:
575
            self.client.update_server_name((server_id), self['server_name'])
576
        if self['flavor_id']:
577
            self.client.resize_server(server_id, self['flavor_id'])
578
        if self['firewall_profile']:
579
            self.client.set_firewall_profile(
580
                server_id=server_id, profile=self['firewall_profile'])
581
        if self['metadata_to_set']:
582
            self.client.update_server_metadata(
583
                server_id, **self['metadata_to_set'])
584
        for key in (self['metadata_to_delete'] or []):
585
            errors.cyclades.metadata(
586
                self.client.delete_server_metadata)(server_id, key=key)
587
        if self['with_output']:
588
            self._optional_output(self.client.get_server_details(server_id))
589

    
590
    def main(self, server_id):
591
        super(self.__class__, self)._run()
592
        self._run(server_id=server_id)
593

    
594

    
595
@command(server_cmds)
596
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
597
    """Delete a virtual server"""
598

    
599
    arguments = dict(
600
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
601
        cluster=FlagArgument(
602
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
603
            'prefix. In that case, the prefix replaces the server id',
604
            '--cluster')
605
    )
606

    
607
    def _server_ids(self, server_var):
608
        if self['cluster']:
609
            return [s['id'] for s in self.client.list_servers() if (
610
                s['name'].startswith(server_var))]
611

    
612
        @errors.cyclades.server_id
613
        def _check_server_id(self, server_id):
614
            return server_id
615

    
616
        return [_check_server_id(self, server_id=server_var), ]
617

    
618
    @errors.generic.all
619
    @errors.cyclades.connection
620
    def _run(self, server_var):
621
        for server_id in self._server_ids(server_var):
622
            if self['wait']:
623
                details = self.client.get_server_details(server_id)
624
                status = details['status']
625

    
626
            r = self.client.delete_server(server_id)
627
            self._optional_output(r)
628

    
629
            if self['wait']:
630
                self._wait(server_id, status)
631

    
632
    def main(self, server_id_or_cluster_prefix):
633
        super(self.__class__, self)._run()
634
        self._run(server_id_or_cluster_prefix)
635

    
636

    
637
@command(server_cmds)
638
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
639
    """Reboot a virtual server"""
640

    
641
    arguments = dict(
642
        hard=FlagArgument(
643
            'perform a hard reboot (deprecated)', ('-f', '--force')),
644
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
645
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
646
    )
647

    
648
    @errors.generic.all
649
    @errors.cyclades.connection
650
    @errors.cyclades.server_id
651
    def _run(self, server_id):
652
        hard_reboot = self['hard']
653
        if hard_reboot:
654
            self.error(
655
                'WARNING: -f/--force will be deprecated in version 0.12\n'
656
                '\tIn the future, please use --type=hard instead')
657
        if self['type']:
658
            if self['type'].lower() in ('soft', ):
659
                hard_reboot = False
660
            elif self['type'].lower() in ('hard', ):
661
                hard_reboot = True
662
            else:
663
                raise CLISyntaxError(
664
                    'Invalid reboot type %s' % self['type'],
665
                    importance=2, details=[
666
                        '--type values are either SOFT (default) or HARD'])
667

    
668
        r = self.client.reboot_server(int(server_id), hard_reboot)
669
        self._optional_output(r)
670

    
671
        if self['wait']:
672
            self._wait(server_id, 'REBOOT')
673

    
674
    def main(self, server_id):
675
        super(self.__class__, self)._run()
676
        self._run(server_id=server_id)
677

    
678

    
679
@command(server_cmds)
680
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
681
    """Start an existing virtual server"""
682

    
683
    arguments = dict(
684
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
685
    )
686

    
687
    @errors.generic.all
688
    @errors.cyclades.connection
689
    @errors.cyclades.server_id
690
    def _run(self, server_id):
691
        status = 'ACTIVE'
692
        if self['wait']:
693
            details = self.client.get_server_details(server_id)
694
            status = details['status']
695
            if status in ('ACTIVE', ):
696
                return
697

    
698
        r = self.client.start_server(int(server_id))
699
        self._optional_output(r)
700

    
701
        if self['wait']:
702
            self._wait(server_id, status)
703

    
704
    def main(self, server_id):
705
        super(self.__class__, self)._run()
706
        self._run(server_id=server_id)
707

    
708

    
709
@command(server_cmds)
710
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
711
    """Shutdown an active virtual server"""
712

    
713
    arguments = dict(
714
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
715
    )
716

    
717
    @errors.generic.all
718
    @errors.cyclades.connection
719
    @errors.cyclades.server_id
720
    def _run(self, server_id):
721
        status = 'STOPPED'
722
        if self['wait']:
723
            details = self.client.get_server_details(server_id)
724
            status = details['status']
725
            if status in ('STOPPED', ):
726
                return
727

    
728
        r = self.client.shutdown_server(int(server_id))
729
        self._optional_output(r)
730

    
731
        if self['wait']:
732
            self._wait(server_id, status)
733

    
734
    def main(self, server_id):
735
        super(self.__class__, self)._run()
736
        self._run(server_id=server_id)
737

    
738

    
739
@command(server_cmds)
740
class server_nics(_init_cyclades):
741
    """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
742

    
743
    def main(self, *args):
744
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
745
            'Replaced by',
746
            '  [kamaki] server info <SERVER_ID> --nics'])
747

    
748

    
749
@command(server_cmds)
750
class server_console(_init_cyclades, _optional_json):
751
    """DEPRECATED, use: [kamaki] server info SERVER_ID --vnc-credentials"""
752

    
753
    def main(self, *args):
754
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
755
            'Replaced by',
756
            '  [kamaki] server info <SERVER_ID> --vnc-credentials'])
757

    
758

    
759
@command(server_cmds)
760
class server_rename(_init_cyclades, _optional_json):
761
    """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
762

    
763
    def main(self, *args):
764
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
765
            'Replaced by',
766
            '  [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
767

    
768

    
769
@command(server_cmds)
770
class server_stats(_init_cyclades, _optional_json):
771
    """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
772

    
773
    def main(self, *args):
774
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
775
            'Replaced by',
776
            '  [kamaki] server info <SERVER_ID> --stats'])
777

    
778

    
779
@command(server_cmds)
780
class server_wait(_init_cyclades, _server_wait):
781
    """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
782

    
783
    arguments = dict(
784
        timeout=IntArgument(
785
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
786
    )
787

    
788
    @errors.generic.all
789
    @errors.cyclades.connection
790
    @errors.cyclades.server_id
791
    def _run(self, server_id, current_status):
792
        r = self.client.get_server_details(server_id)
793
        if r['status'].lower() == current_status.lower():
794
            self._wait(server_id, current_status, timeout=self['timeout'])
795
        else:
796
            self.error(
797
                'Server %s: Cannot wait for status %s, '
798
                'status is already %s' % (
799
                    server_id, current_status, r['status']))
800

    
801
    def main(self, server_id, current_status='BUILD'):
802
        super(self.__class__, self)._run()
803
        self._run(server_id=server_id, current_status=current_status)
804

    
805

    
806
@command(flavor_cmds)
807
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
808
    """List available hardware flavors"""
809

    
810
    PERMANENTS = ('id', 'name')
811

    
812
    arguments = dict(
813
        detail=FlagArgument('show detailed output', ('-l', '--details')),
814
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
815
        more=FlagArgument(
816
            'output results in pages (-n to set items per page, default 10)',
817
            '--more'),
818
        enum=FlagArgument('Enumerate results', '--enumerate'),
819
        ram=ValueArgument('filter by ram', ('--ram')),
820
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
821
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
822
        disk_template=ValueArgument(
823
            'filter by disk_templace', ('--disk-template'))
824
    )
825

    
826
    def _apply_common_filters(self, flavors):
827
        common_filters = dict()
828
        if self['ram']:
829
            common_filters['ram'] = self['ram']
830
        if self['vcpus']:
831
            common_filters['vcpus'] = self['vcpus']
832
        if self['disk']:
833
            common_filters['disk'] = self['disk']
834
        if self['disk_template']:
835
            common_filters['SNF:disk_template'] = self['disk_template']
836
        return filter_dicts_by_dict(flavors, common_filters)
837

    
838
    @errors.generic.all
839
    @errors.cyclades.connection
840
    def _run(self):
841
        withcommons = self['ram'] or self['vcpus'] or (
842
            self['disk'] or self['disk_template'])
843
        detail = self['detail'] or withcommons
844
        flavors = self.client.list_flavors(detail)
845
        flavors = self._filter_by_name(flavors)
846
        flavors = self._filter_by_id(flavors)
847
        if withcommons:
848
            flavors = self._apply_common_filters(flavors)
849
        if not (self['detail'] or (
850
                self['json_output'] or self['output_format'])):
851
            remove_from_items(flavors, 'links')
852
        if detail and not self['detail']:
853
            for flv in flavors:
854
                for key in set(flv).difference(self.PERMANENTS):
855
                    flv.pop(key)
856
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
857
        self._print(
858
            flavors,
859
            with_redundancy=self['detail'], with_enumeration=self['enum'],
860
            **kwargs)
861
        if self['more']:
862
            pager(kwargs['out'].getvalue())
863

    
864
    def main(self):
865
        super(self.__class__, self)._run()
866
        self._run()
867

    
868

    
869
@command(flavor_cmds)
870
class flavor_info(_init_cyclades, _optional_json):
871
    """Detailed information on a hardware flavor
872
    To get a list of available flavors and flavor ids, try /flavor list
873
    """
874

    
875
    @errors.generic.all
876
    @errors.cyclades.connection
877
    @errors.cyclades.flavor_id
878
    def _run(self, flavor_id):
879
        self._print(
880
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
881

    
882
    def main(self, flavor_id):
883
        super(self.__class__, self)._run()
884
        self._run(flavor_id=flavor_id)
885

    
886

    
887
def _add_name(self, net):
888
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
889
        if user_id:
890
            uuids.append(user_id)
891
        if tenant_id:
892
            uuids.append(tenant_id)
893
        if uuids:
894
            usernames = self._uuids2usernames(uuids)
895
            if user_id:
896
                net['user_id'] += ' (%s)' % usernames[user_id]
897
            if tenant_id:
898
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]