Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33.7 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, StatusArgument)
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
server_states = ('BUILD', 'ACTIVE', 'STOPPED', 'REBOOT')
73

    
74

    
75
class _service_wait(object):
76

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

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

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

    
104

    
105
class _server_wait(_service_wait):
106

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

    
113

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

    
138
    def main(self):
139
        self._run()
140

    
141

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

    
148
    PERMANENTS = ('id', 'name')
149

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

    
174
    def _add_user_name(self, servers):
175
        uuids = self._uuids2usernames(list(set(
176
                    [srv['user_id'] for srv in servers])))
177
        for srv in servers:
178
            srv['user_id'] += ' (%s)' % uuids[srv['user_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
        if self['nics']:
281
            self._print(
282
                self.client.get_server_nics(server_id), self.print_dict)
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
        elif self['diagnostics']:
291
            self._print(self.client.get_server_diagnostics(server_id))
292
        else:
293
            vm = self.client.get_server_details(server_id)
294
            uuids = self._uuids2usernames([vm['user_id']])
295
            vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
296
            self._print(vm, self.print_dict)
297

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

    
308

    
309
class PersonalityArgument(KeyValueArgument):
310

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

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

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

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

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

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

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

    
376

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

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

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

    
418

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

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

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

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

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

    
520

    
521
class FirewallProfileArgument(ValueArgument):
522

    
523
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
524

    
525
    @property
526
    def value(self):
527
        return getattr(self, '_value', None)
528

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

    
540

    
541
@command(server_cmds)
542
class server_modify(_init_cyclades, _optional_output_cmd):
543
    """Modify attributes of a virtual server"""
544

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

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

    
581
    def main(self, server_id):
582
        super(self.__class__, self)._run()
583
        self._run(server_id=server_id)
584

    
585

    
586
@command(server_cmds)
587
class server_reassign(_init_cyclades, _optional_json):
588
    """Assign a VM to a different project
589
    """
590

    
591
    @errors.generic.all
592
    @errors.cyclades.connection
593
    @errors.cyclades.server_id
594
    def _run(self, server_id, project):
595
        self.client.reassign_server(server_id, project)
596

    
597
    def main(self, server_id, project):
598
        super(self.__class__, self)._run()
599
        self._run(server_id=server_id, project=project)
600

    
601

    
602
@command(server_cmds)
603
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
604
    """Delete a virtual server"""
605

    
606
    arguments = dict(
607
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
608
        cluster=FlagArgument(
609
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
610
            'prefix. In that case, the prefix replaces the server id',
611
            '--cluster')
612
    )
613

    
614
    def _server_ids(self, server_var):
615
        if self['cluster']:
616
            return [s['id'] for s in self.client.list_servers() if (
617
                s['name'].startswith(server_var))]
618

    
619
        @errors.cyclades.server_id
620
        def _check_server_id(self, server_id):
621
            return server_id
622

    
623
        return [_check_server_id(self, server_id=server_var), ]
624

    
625
    @errors.generic.all
626
    @errors.cyclades.connection
627
    def _run(self, server_var):
628
        for server_id in self._server_ids(server_var):
629
            if self['wait']:
630
                details = self.client.get_server_details(server_id)
631
                status = details['status']
632

    
633
            r = self.client.delete_server(server_id)
634
            self._optional_output(r)
635

    
636
            if self['wait']:
637
                self._wait(server_id, status)
638

    
639
    def main(self, server_id_or_cluster_prefix):
640
        super(self.__class__, self)._run()
641
        self._run(server_id_or_cluster_prefix)
642

    
643

    
644
@command(server_cmds)
645
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
646
    """Reboot a virtual server"""
647

    
648
    arguments = dict(
649
        hard=FlagArgument(
650
            'perform a hard reboot (deprecated)', ('-f', '--force')),
651
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
652
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
653
    )
654

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

    
675
        r = self.client.reboot_server(int(server_id), hard_reboot)
676
        self._optional_output(r)
677

    
678
        if self['wait']:
679
            self._wait(server_id, 'REBOOT')
680

    
681
    def main(self, server_id):
682
        super(self.__class__, self)._run()
683
        self._run(server_id=server_id)
684

    
685

    
686
@command(server_cmds)
687
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
688
    """Start an existing virtual server"""
689

    
690
    arguments = dict(
691
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
692
    )
693

    
694
    @errors.generic.all
695
    @errors.cyclades.connection
696
    @errors.cyclades.server_id
697
    def _run(self, server_id):
698
        status = 'ACTIVE'
699
        if self['wait']:
700
            details = self.client.get_server_details(server_id)
701
            status = details['status']
702
            if status in ('ACTIVE', ):
703
                return
704

    
705
        r = self.client.start_server(int(server_id))
706
        self._optional_output(r)
707

    
708
        if self['wait']:
709
            self._wait(server_id, status)
710

    
711
    def main(self, server_id):
712
        super(self.__class__, self)._run()
713
        self._run(server_id=server_id)
714

    
715

    
716
@command(server_cmds)
717
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
718
    """Shutdown an active virtual server"""
719

    
720
    arguments = dict(
721
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
722
    )
723

    
724
    @errors.generic.all
725
    @errors.cyclades.connection
726
    @errors.cyclades.server_id
727
    def _run(self, server_id):
728
        status = 'STOPPED'
729
        if self['wait']:
730
            details = self.client.get_server_details(server_id)
731
            status = details['status']
732
            if status in ('STOPPED', ):
733
                return
734

    
735
        r = self.client.shutdown_server(int(server_id))
736
        self._optional_output(r)
737

    
738
        if self['wait']:
739
            self._wait(server_id, status)
740

    
741
    def main(self, server_id):
742
        super(self.__class__, self)._run()
743
        self._run(server_id=server_id)
744

    
745

    
746
@command(server_cmds)
747
class server_nics(_init_cyclades):
748
    """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
749

    
750
    def main(self, *args):
751
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
752
            'Replaced by',
753
            '  [kamaki] server info <SERVER_ID> --nics'])
754

    
755

    
756
@command(server_cmds)
757
class server_console(_init_cyclades, _optional_json):
758
    """Create a VMC console and show connection information"""
759

    
760
    @errors.generic.all
761
    @errors.cyclades.connection
762
    @errors.cyclades.server_id
763
    def _run(self, server_id):
764
        self.error('The following credentials will be invalidated shortly')
765
        self._print(
766
            self.client.get_server_console(server_id), self.print_dict)
767

    
768
    def main(self, server_id):
769
        super(self.__class__, self)._run()
770
        self._run(server_id=server_id)
771

    
772

    
773
@command(server_cmds)
774
class server_rename(_init_cyclades, _optional_json):
775
    """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
776

    
777
    def main(self, *args):
778
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
779
            'Replaced by',
780
            '  [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
781

    
782

    
783
@command(server_cmds)
784
class server_stats(_init_cyclades, _optional_json):
785
    """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
786

    
787
    def main(self, *args):
788
        raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
789
            'Replaced by',
790
            '  [kamaki] server info <SERVER_ID> --stats'])
791

    
792

    
793
@command(server_cmds)
794
class server_wait(_init_cyclades, _server_wait):
795
    """Wait for server to change its status (default: BUILD)"""
796

    
797
    arguments = dict(
798
        timeout=IntArgument(
799
            'Wait limit in seconds (default: 60)', '--timeout', default=60),
800
        server_status=StatusArgument(
801
            'Status to wait for (%s, default: %s)' % (
802
                ', '.join(server_states), server_states[0]),
803
            '--status',
804
            valid_states=server_states)
805
    )
806

    
807
    @errors.generic.all
808
    @errors.cyclades.connection
809
    @errors.cyclades.server_id
810
    def _run(self, server_id, current_status):
811
        r = self.client.get_server_details(server_id)
812
        if r['status'].lower() == current_status.lower():
813
            self._wait(server_id, current_status, timeout=self['timeout'])
814
        else:
815
            self.error(
816
                'Server %s: Cannot wait for status %s, '
817
                'status is already %s' % (
818
                    server_id, current_status, r['status']))
819

    
820
    def main(self, server_id):
821
        super(self.__class__, self)._run()
822
        self._run(server_id=server_id, current_status=self['server_status'])
823

    
824

    
825
@command(flavor_cmds)
826
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
827
    """List available hardware flavors"""
828

    
829
    PERMANENTS = ('id', 'name')
830

    
831
    arguments = dict(
832
        detail=FlagArgument('show detailed output', ('-l', '--details')),
833
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
834
        more=FlagArgument(
835
            'output results in pages (-n to set items per page, default 10)',
836
            '--more'),
837
        enum=FlagArgument('Enumerate results', '--enumerate'),
838
        ram=ValueArgument('filter by ram', ('--ram')),
839
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
840
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
841
        disk_template=ValueArgument(
842
            'filter by disk_templace', ('--disk-template'))
843
    )
844

    
845
    def _apply_common_filters(self, flavors):
846
        common_filters = dict()
847
        if self['ram']:
848
            common_filters['ram'] = self['ram']
849
        if self['vcpus']:
850
            common_filters['vcpus'] = self['vcpus']
851
        if self['disk']:
852
            common_filters['disk'] = self['disk']
853
        if self['disk_template']:
854
            common_filters['SNF:disk_template'] = self['disk_template']
855
        return filter_dicts_by_dict(flavors, common_filters)
856

    
857
    @errors.generic.all
858
    @errors.cyclades.connection
859
    def _run(self):
860
        withcommons = self['ram'] or self['vcpus'] or (
861
            self['disk'] or self['disk_template'])
862
        detail = self['detail'] or withcommons
863
        flavors = self.client.list_flavors(detail)
864
        flavors = self._filter_by_name(flavors)
865
        flavors = self._filter_by_id(flavors)
866
        if withcommons:
867
            flavors = self._apply_common_filters(flavors)
868
        if not (self['detail'] or (
869
                self['json_output'] or self['output_format'])):
870
            remove_from_items(flavors, 'links')
871
        if detail and not self['detail']:
872
            for flv in flavors:
873
                for key in set(flv).difference(self.PERMANENTS):
874
                    flv.pop(key)
875
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
876
        self._print(
877
            flavors,
878
            with_redundancy=self['detail'], with_enumeration=self['enum'],
879
            **kwargs)
880
        if self['more']:
881
            pager(kwargs['out'].getvalue())
882

    
883
    def main(self):
884
        super(self.__class__, self)._run()
885
        self._run()
886

    
887

    
888
@command(flavor_cmds)
889
class flavor_info(_init_cyclades, _optional_json):
890
    """Detailed information on a hardware flavor
891
    To get a list of available flavors and flavor ids, try /flavor list
892
    """
893

    
894
    @errors.generic.all
895
    @errors.cyclades.connection
896
    @errors.cyclades.flavor_id
897
    def _run(self, flavor_id):
898
        self._print(
899
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
900

    
901
    def main(self, flavor_id):
902
        super(self.__class__, self)._run()
903
        self._run(flavor_id=flavor_id)
904

    
905

    
906
def _add_name(self, net):
907
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
908
        if user_id:
909
            uuids.append(user_id)
910
        if uuids or tenant_id:
911
            usernames = self._uuids2usernames(uuids)
912
            if user_id:
913
                net['user_id'] += ' (%s)' % usernames[user_id]