Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33 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
        stats=FlagArgument('Get URLs for server statistics', '--stats'),
273
        diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
274
    )
275

    
276
    @errors.generic.all
277
    @errors.cyclades.connection
278
    @errors.cyclades.server_id
279
    def _run(self, server_id):
280
        vm = self.client.get_server_nics(server_id)
281
        if self['nics']:
282
            self._print(vm.get('attachments', []))
283
        elif self['network_id']:
284
            self._print(
285
                self.client.get_server_network_nics(
286
                    server_id, self['network_id']), self.print_dict)
287
        elif self['stats']:
288
            self._print(
289
                self.client.get_server_stats(server_id), self.print_dict)
290
        else:
291
            uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
292
            vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
293
            vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
294
            self._print(vm, self.print_dict)
295

    
296
    def main(self, server_id):
297
        super(self.__class__, self)._run()
298
        choose_one = ('nics', 'vnc', 'stats')
299
        count = len([a for a in choose_one if self[a]])
300
        if count > 1:
301
            raise CLIInvalidArgument('Invalid argument compination', details=[
302
                'Arguments %s cannot be used simultaneously' % ', '.join(
303
                    [self.arguments[a].lvalue for a in choose_one])])
304
        self._run(server_id=server_id)
305

    
306

    
307
class PersonalityArgument(KeyValueArgument):
308

    
309
    terms = (
310
        ('local-path', 'contents'),
311
        ('server-path', 'path'),
312
        ('owner', 'owner'),
313
        ('group', 'group'),
314
        ('mode', 'mode'))
315

    
316
    @property
317
    def value(self):
318
        return getattr(self, '_value', [])
319

    
320
    @value.setter
321
    def value(self, newvalue):
322
        if newvalue == self.default:
323
            return self.value
324
        self._value, input_dict = [], {}
325
        for i, terms in enumerate(newvalue):
326
            termlist = terms.split(',')
327
            if len(termlist) > len(self.terms):
328
                msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
329
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
330

    
331
            for k, v in self.terms:
332
                prefix = '%s=' % k
333
                for item in termlist:
334
                    if item.lower().startswith(prefix):
335
                        input_dict[k] = item[len(k) + 1:]
336
                        break
337
                    item = None
338
                if item:
339
                    termlist.remove(item)
340

    
341
            try:
342
                path = input_dict['local-path']
343
            except KeyError:
344
                path = termlist.pop(0)
345
                if not path:
346
                    raise CLIInvalidArgument(
347
                        '--personality: No local path specified',
348
                        details=howto_personality)
349

    
350
            if not exists(path):
351
                raise CLIInvalidArgument(
352
                    '--personality: File %s does not exist' % path,
353
                    details=howto_personality)
354

    
355
            self._value.append(dict(path=path))
356
            with open(expanduser(path)) as f:
357
                self._value[i]['contents'] = b64encode(f.read())
358
            for k, v in self.terms[1:]:
359
                try:
360
                    self._value[i][v] = input_dict[k]
361
                except KeyError:
362
                    try:
363
                        self._value[i][v] = termlist.pop(0)
364
                    except IndexError:
365
                        continue
366
                if k in ('mode', ) and self._value[i][v]:
367
                    try:
368
                        self._value[i][v] = int(self._value[i][v], 8)
369
                    except ValueError as ve:
370
                        raise CLIInvalidArgument(
371
                            'Personality mode must be in octal', details=[
372
                                '%s' % ve])
373

    
374

    
375
class NetworkArgument(RepeatableArgument):
376
    """[id=]NETWORK_ID[,[ip=]IP]"""
377

    
378
    @property
379
    def value(self):
380
        return getattr(self, '_value', self.default)
381

    
382
    @value.setter
383
    def value(self, new_value):
384
        for v in new_value or []:
385
            part1, sep, part2 = v.partition(',')
386
            netid, ip = '', ''
387
            if part1.startswith('id='):
388
                netid = part1[len('id='):]
389
            elif part1.startswith('ip='):
390
                ip = part1[len('ip='):]
391
            else:
392
                netid = part1
393
            if part2:
394
                if (part2.startswith('id=') and netid) or (
395
                        part2.startswith('ip=') and ip):
396
                    raise CLIInvalidArgument(
397
                        'Invalid network argument %s' % v, details=[
398
                        'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
399
                if part2.startswith('id='):
400
                    netid = part2[len('id='):]
401
                elif part2.startswith('ip='):
402
                    ip = part2[len('ip='):]
403
                elif netid:
404
                    ip = part2
405
                else:
406
                    netid = part2
407
            if not netid:
408
                raise CLIInvalidArgument(
409
                    'Invalid network argument %s' % v, details=[
410
                    'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
411
            self._value = getattr(self, '_value', [])
412
            self._value.append(dict(uuid=netid))
413
            if ip:
414
                self._value[-1]['fixed_ip'] = ip
415

    
416

    
417
@command(server_cmds)
418
class server_create(_init_cyclades, _optional_json, _server_wait):
419
    """Create a server (aka Virtual Machine)"""
420

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

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

    
485
    @errors.generic.all
486
    @errors.cyclades.connection
487
    @errors.plankton.id
488
    @errors.cyclades.flavor_id
489
    def _run(self, name, flavor_id, image_id):
490
        for r in self._create_cluster(
491
                name, flavor_id, image_id, size=self['cluster_size'] or 1):
492
            if not r:
493
                self.error('Create %s: server response was %s' % (name, r))
494
                continue
495
            usernames = self._uuids2usernames(
496
                [r['user_id'], r['tenant_id']])
497
            r['user_id'] += ' (%s)' % usernames[r['user_id']]
498
            r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
499
            self._print(r, self.print_dict)
500
            if self['wait']:
501
                self._wait(r['id'], r['status'])
502
            self.writeln(' ')
503

    
504
    def main(self):
505
        super(self.__class__, self)._run()
506
        if self['no_network'] and self['network']:
507
            raise CLIInvalidArgument(
508
                'Invalid argument compination', importance=2, details=[
509
                'Arguments %s and %s are mutually exclusive' % (
510
                    self.arguments['no_network'].lvalue,
511
                    self.arguments['network'].lvalue)])
512
        self._run(
513
            name=self['server_name'],
514
            flavor_id=self['flavor_id'],
515
            image_id=self['image_id'])
516

    
517

    
518
class FirewallProfileArgument(ValueArgument):
519

    
520
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
521

    
522
    @property
523
    def value(self):
524
        return getattr(self, '_value', None)
525

    
526
    @value.setter
527
    def value(self, new_profile):
528
        if new_profile:
529
            new_profile = new_profile.upper()
530
            if new_profile in self.profiles:
531
                self._value = new_profile
532
            else:
533
                raise CLIInvalidArgument(
534
                    'Invalid firewall profile %s' % new_profile,
535
                    details=['Valid values: %s' % ', '.join(self.profiles)])
536

    
537

    
538
@command(server_cmds)
539
class server_modify(_init_cyclades, _optional_output_cmd):
540
    """Modify attributes of a virtual server"""
541

    
542
    arguments = dict(
543
        server_name=ValueArgument('The new name', '--name'),
544
        flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
545
        firewall_profile=FirewallProfileArgument(
546
            'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
547
            '--firewall'),
548
        metadata_to_set=KeyValueArgument(
549
            'Set metadata in key=value form (can be repeated)',
550
            '--metadata-set'),
551
        metadata_to_delete=RepeatableArgument(
552
            'Delete metadata by key (can be repeated)', '--metadata-del')
553
    )
554
    required = [
555
        'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
556
        'metadata_to_delete']
557

    
558
    @errors.generic.all
559
    @errors.cyclades.connection
560
    @errors.cyclades.server_id
561
    def _run(self, server_id):
562
        if self['server_name']:
563
            self.client.update_server_name((server_id), self['server_name'])
564
        if self['flavor_id']:
565
            self.client.resize_server(server_id, self['flavor_id'])
566
        if self['firewall_profile']:
567
            self.client.set_firewall_profile(
568
                server_id=server_id, profile=self['firewall_profile'])
569
        if self['metadata_to_set']:
570
            self.client.update_server_metadata(
571
                server_id, **self['metadata_to_set'])
572
        for key in (self['metadata_to_delete'] or []):
573
            errors.cyclades.metadata(
574
                self.client.delete_server_metadata)(server_id, key=key)
575
        if self['with_output']:
576
            self._optional_output(self.client.get_server_details(server_id))
577

    
578
    def main(self, server_id):
579
        super(self.__class__, self)._run()
580
        self._run(server_id=server_id)
581

    
582

    
583
@command(server_cmds)
584
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
585
    """Delete a virtual server"""
586

    
587
    arguments = dict(
588
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
589
        cluster=FlagArgument(
590
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
591
            'prefix. In that case, the prefix replaces the server id',
592
            '--cluster')
593
    )
594

    
595
    def _server_ids(self, server_var):
596
        if self['cluster']:
597
            return [s['id'] for s in self.client.list_servers() if (
598
                s['name'].startswith(server_var))]
599

    
600
        @errors.cyclades.server_id
601
        def _check_server_id(self, server_id):
602
            return server_id
603

    
604
        return [_check_server_id(self, server_id=server_var), ]
605

    
606
    @errors.generic.all
607
    @errors.cyclades.connection
608
    def _run(self, server_var):
609
        for server_id in self._server_ids(server_var):
610
            if self['wait']:
611
                details = self.client.get_server_details(server_id)
612
                status = details['status']
613

    
614
            r = self.client.delete_server(server_id)
615
            self._optional_output(r)
616

    
617
            if self['wait']:
618
                self._wait(server_id, status)
619

    
620
    def main(self, server_id_or_cluster_prefix):
621
        super(self.__class__, self)._run()
622
        self._run(server_id_or_cluster_prefix)
623

    
624

    
625
@command(server_cmds)
626
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
627
    """Reboot a virtual server"""
628

    
629
    arguments = dict(
630
        hard=FlagArgument(
631
            'perform a hard reboot (deprecated)', ('-f', '--force')),
632
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
633
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
634
    )
635

    
636
    @errors.generic.all
637
    @errors.cyclades.connection
638
    @errors.cyclades.server_id
639
    def _run(self, server_id):
640
        hard_reboot = self['hard']
641
        if hard_reboot:
642
            self.error(
643
                'WARNING: -f/--force will be deprecated in version 0.12\n'
644
                '\tIn the future, please use --type=hard instead')
645
        if self['type']:
646
            if self['type'].lower() in ('soft', ):
647
                hard_reboot = False
648
            elif self['type'].lower() in ('hard', ):
649
                hard_reboot = True
650
            else:
651
                raise CLISyntaxError(
652
                    'Invalid reboot type %s' % self['type'],
653
                    importance=2, details=[
654
                        '--type values are either SOFT (default) or HARD'])
655

    
656
        r = self.client.reboot_server(int(server_id), hard_reboot)
657
        self._optional_output(r)
658

    
659
        if self['wait']:
660
            self._wait(server_id, 'REBOOT')
661

    
662
    def main(self, server_id):
663
        super(self.__class__, self)._run()
664
        self._run(server_id=server_id)
665

    
666

    
667
@command(server_cmds)
668
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
669
    """Start an existing virtual server"""
670

    
671
    arguments = dict(
672
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
673
    )
674

    
675
    @errors.generic.all
676
    @errors.cyclades.connection
677
    @errors.cyclades.server_id
678
    def _run(self, server_id):
679
        status = 'ACTIVE'
680
        if self['wait']:
681
            details = self.client.get_server_details(server_id)
682
            status = details['status']
683
            if status in ('ACTIVE', ):
684
                return
685

    
686
        r = self.client.start_server(int(server_id))
687
        self._optional_output(r)
688

    
689
        if self['wait']:
690
            self._wait(server_id, status)
691

    
692
    def main(self, server_id):
693
        super(self.__class__, self)._run()
694
        self._run(server_id=server_id)
695

    
696

    
697
@command(server_cmds)
698
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
699
    """Shutdown an active virtual server"""
700

    
701
    arguments = dict(
702
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
703
    )
704

    
705
    @errors.generic.all
706
    @errors.cyclades.connection
707
    @errors.cyclades.server_id
708
    def _run(self, server_id):
709
        status = 'STOPPED'
710
        if self['wait']:
711
            details = self.client.get_server_details(server_id)
712
            status = details['status']
713
            if status in ('STOPPED', ):
714
                return
715

    
716
        r = self.client.shutdown_server(int(server_id))
717
        self._optional_output(r)
718

    
719
        if self['wait']:
720
            self._wait(server_id, status)
721

    
722
    def main(self, server_id):
723
        super(self.__class__, self)._run()
724
        self._run(server_id=server_id)
725

    
726

    
727
@command(server_cmds)
728
class server_nics(_init_cyclades):
729
    """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
730

    
731
    def main(self, *args):
732
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
733
            'Replaced by',
734
            '  [kamaki] server info <SERVER_ID> --nics'])
735

    
736

    
737
@command(server_cmds)
738
class server_console(_init_cyclades, _optional_json):
739
    """Create a VMC console and show connection information"""
740

    
741
    @errors.generic.all
742
    @errors.cyclades.connection
743
    @errors.cyclades.server_id
744
    def _run(self, server_id):
745
        self.error('The following credentials will be invalidated shortly')
746
        self._print(
747
            self.client.get_server_console(server_id), self.print_dict)
748

    
749
    def main(self, server_id):
750
        super(self.__class__, self)._run()
751
        self._run(server_id=server_id)
752

    
753

    
754
@command(server_cmds)
755
class server_rename(_init_cyclades, _optional_json):
756
    """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
757

    
758
    def main(self, *args):
759
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
760
            'Replaced by',
761
            '  [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
762

    
763

    
764
@command(server_cmds)
765
class server_stats(_init_cyclades, _optional_json):
766
    """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
767

    
768
    def main(self, *args):
769
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
770
            'Replaced by',
771
            '  [kamaki] server info <SERVER_ID> --stats'])
772

    
773

    
774
@command(server_cmds)
775
class server_wait(_init_cyclades, _server_wait):
776
    """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
777

    
778
    arguments = dict(
779
        timeout=IntArgument(
780
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
781
    )
782

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

    
796
    def main(self, server_id, current_status='BUILD'):
797
        super(self.__class__, self)._run()
798
        self._run(server_id=server_id, current_status=current_status)
799

    
800

    
801
@command(flavor_cmds)
802
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
803
    """List available hardware flavors"""
804

    
805
    PERMANENTS = ('id', 'name')
806

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

    
821
    def _apply_common_filters(self, flavors):
822
        common_filters = dict()
823
        if self['ram']:
824
            common_filters['ram'] = self['ram']
825
        if self['vcpus']:
826
            common_filters['vcpus'] = self['vcpus']
827
        if self['disk']:
828
            common_filters['disk'] = self['disk']
829
        if self['disk_template']:
830
            common_filters['SNF:disk_template'] = self['disk_template']
831
        return filter_dicts_by_dict(flavors, common_filters)
832

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

    
859
    def main(self):
860
        super(self.__class__, self)._run()
861
        self._run()
862

    
863

    
864
@command(flavor_cmds)
865
class flavor_info(_init_cyclades, _optional_json):
866
    """Detailed information on a hardware flavor
867
    To get a list of available flavors and flavor ids, try /flavor list
868
    """
869

    
870
    @errors.generic.all
871
    @errors.cyclades.connection
872
    @errors.cyclades.flavor_id
873
    def _run(self, flavor_id):
874
        self._print(
875
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
876

    
877
    def main(self, flavor_id):
878
        super(self.__class__, self)._run()
879
        self._run(flavor_id=flavor_id)
880

    
881

    
882
def _add_name(self, net):
883
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
884
        if user_id:
885
            uuids.append(user_id)
886
        if tenant_id:
887
            uuids.append(tenant_id)
888
        if uuids:
889
            usernames = self._uuids2usernames(uuids)
890
            if user_id:
891
                net['user_id'] += ' (%s)' % usernames[user_id]
892
            if tenant_id:
893
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]