Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 58f4caba

History | View | Annotate | Download (33.1 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
        elif self['diagnostics']:
291
            self._print(self.client.get_server_diagnostics(server_id))
292
        else:
293
            uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
294
            vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
295
            vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_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', 'vnc', 'stats')
301
        count = len([a for a in choose_one if self[a]])
302
        if count > 1:
303
            raise CLIInvalidArgument('Invalid argument compination', 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
    )
451
    required = ('server_name', 'flavor_id', 'image_id')
452

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

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

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

    
519

    
520
class FirewallProfileArgument(ValueArgument):
521

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

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

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

    
539

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

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

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

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

    
584

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

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

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

    
602
        @errors.cyclades.server_id
603
        def _check_server_id(self, server_id):
604
            return server_id
605

    
606
        return [_check_server_id(self, server_id=server_var), ]
607

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

    
616
            r = self.client.delete_server(server_id)
617
            self._optional_output(r)
618

    
619
            if self['wait']:
620
                self._wait(server_id, status)
621

    
622
    def main(self, server_id_or_cluster_prefix):
623
        super(self.__class__, self)._run()
624
        self._run(server_id_or_cluster_prefix)
625

    
626

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

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

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

    
658
        r = self.client.reboot_server(int(server_id), hard_reboot)
659
        self._optional_output(r)
660

    
661
        if self['wait']:
662
            self._wait(server_id, 'REBOOT')
663

    
664
    def main(self, server_id):
665
        super(self.__class__, self)._run()
666
        self._run(server_id=server_id)
667

    
668

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

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

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

    
688
        r = self.client.start_server(int(server_id))
689
        self._optional_output(r)
690

    
691
        if self['wait']:
692
            self._wait(server_id, status)
693

    
694
    def main(self, server_id):
695
        super(self.__class__, self)._run()
696
        self._run(server_id=server_id)
697

    
698

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

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

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

    
718
        r = self.client.shutdown_server(int(server_id))
719
        self._optional_output(r)
720

    
721
        if self['wait']:
722
            self._wait(server_id, status)
723

    
724
    def main(self, server_id):
725
        super(self.__class__, self)._run()
726
        self._run(server_id=server_id)
727

    
728

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

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

    
738

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

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

    
751
    def main(self, server_id):
752
        super(self.__class__, self)._run()
753
        self._run(server_id=server_id)
754

    
755

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

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

    
765

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

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

    
775

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

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

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

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

    
802

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

    
807
    PERMANENTS = ('id', 'name')
808

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

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

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

    
861
    def main(self):
862
        super(self.__class__, self)._run()
863
        self._run()
864

    
865

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

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

    
879
    def main(self, flavor_id):
880
        super(self.__class__, self)._run()
881
        self._run(flavor_id=flavor_id)
882

    
883

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