Statistics
| Branch: | Tag: | Revision:

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

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
        addr=FlagArgument(
268
            'Show only the network interfaces of this virtual server',
269
            '--nics'),
270
        vnc=FlagArgument(
271
            'Show VNC connection information (valid for a short period)',
272
            '--vnc-credentials'),
273
        stats=FlagArgument('Get URLs for server statistics', '--stats')
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_details(server_id)
281
        if self['addr']:
282
            self._print(vm.get('attachments', []))
283
        elif self['vnc']:
284
            self.error(
285
                '(!) For security reasons, the following credentials are '
286
                'invalidated\nafter a short time period, depending on the '
287
                'server settings\n')
288
            self._print(
289
                self.client.get_server_console(server_id), self.print_dict)
290
        elif self['stats']:
291
            self._print(
292
                self.client.get_server_stats(server_id), self.print_dict)
293
        else:
294
            uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
295
            vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
296
            vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
297
            self._print(vm, self.print_dict)
298

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

    
309

    
310
class PersonalityArgument(KeyValueArgument):
311

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

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

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

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

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

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

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

    
377

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

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

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

    
419

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

    
424
    arguments = dict(
425
        server_name=ValueArgument('The name of the new server', '--name'),
426
        flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
427
        image_id=ValueArgument('The ID of the hardware image', '--image-id'),
428
        personality=PersonalityArgument(
429
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
430
        wait=FlagArgument('Wait server to build', ('-w', '--wait')),
431
        cluster_size=IntArgument(
432
            'Create a cluster of servers of this size. In this case, the name'
433
            'parameter is the prefix of each server in the cluster (e.g.,'
434
            'srv1, srv2, etc.',
435
            '--cluster-size'),
436
        max_threads=IntArgument(
437
            'Max threads in cluster mode (default 1)', '--threads'),
438
        network_configuration=NetworkArgument(
439
            'Connect server to network: [id=]NETWORK_ID[,[ip=]IP]        . '
440
            'Use only NETWORK_ID for private networks.        . '
441
            'Use NETWORK_ID,[ip=]IP for networks with IP.        . '
442
            'Can be repeated, mutually exclussive with --no-network',
443
            '--network'),
444
        no_network=FlagArgument(
445
            'Do not create any network NICs on the server.        . '
446
            'Mutually exclusive to --network        . '
447
            'If neither --network or --no-network are used, the default '
448
            'network policy is applied. This policy is configured on the '
449
            'cloud and kamaki is oblivious to it',
450
            '--no-network')
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):
456
        networks = self['network_configuration'] or (
457
            None if self['no_network'] else [])
458
        servers = [dict(
459
            name='%s%s' % (prefix, i if size > 1 else ''),
460
            flavor_id=flavor_id,
461
            image_id=image_id,
462
            personality=self['personality'],
463
            networks=networks) for i in range(1, 1 + size)]
464
        if size == 1:
465
            return [self.client.create_server(**servers[0])]
466
        self.client.MAX_THREADS = int(self['max_threads'] or 1)
467
        try:
468
            r = self.client.async_run(self.client.create_server, servers)
469
            return r
470
        except Exception as e:
471
            if size == 1:
472
                raise e
473
            try:
474
                requested_names = [s['name'] for s in servers]
475
                spawned_servers = [dict(
476
                    name=s['name'],
477
                    id=s['id']) for s in self.client.list_servers() if (
478
                        s['name'] in requested_names)]
479
                self.error('Failed to build %s servers' % size)
480
                self.error('Found %s matching servers:' % len(spawned_servers))
481
                self._print(spawned_servers, out=self._err)
482
                self.error('Check if any of these servers should be removed\n')
483
            except Exception as ne:
484
                self.error('Error (%s) while notifying about errors' % ne)
485
            finally:
486
                raise e
487

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

    
507
    def main(self):
508
        super(self.__class__, self)._run()
509
        if self['no_network'] and self['network']:
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'].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_delete(_init_cyclades, _optional_output_cmd, _server_wait):
588
    """Delete a virtual server"""
589

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

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

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

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

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

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

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

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

    
627

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

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

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

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

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

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

    
669

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

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

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

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

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

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

    
699

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

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

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

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

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

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

    
729

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

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

    
739

    
740
@command(server_cmds)
741
class server_console(_init_cyclades, _optional_json):
742
    """DEPRECATED, use: [kamaki] server info SERVER_ID --vnc-credentials"""
743

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

    
749

    
750
@command(server_cmds)
751
class server_rename(_init_cyclades, _optional_json):
752
    """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
753

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

    
759

    
760
@command(server_cmds)
761
class server_stats(_init_cyclades, _optional_json):
762
    """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
763

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

    
769

    
770
@command(server_cmds)
771
class server_wait(_init_cyclades, _server_wait):
772
    """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
773

    
774
    arguments = dict(
775
        timeout=IntArgument(
776
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
777
    )
778

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

    
792
    def main(self, server_id, current_status='BUILD'):
793
        super(self.__class__, self)._run()
794
        self._run(server_id=server_id, current_status=current_status)
795

    
796

    
797
@command(flavor_cmds)
798
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
799
    """List available hardware flavors"""
800

    
801
    PERMANENTS = ('id', 'name')
802

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

    
817
    def _apply_common_filters(self, flavors):
818
        common_filters = dict()
819
        if self['ram']:
820
            common_filters['ram'] = self['ram']
821
        if self['vcpus']:
822
            common_filters['vcpus'] = self['vcpus']
823
        if self['disk']:
824
            common_filters['disk'] = self['disk']
825
        if self['disk_template']:
826
            common_filters['SNF:disk_template'] = self['disk_template']
827
        return filter_dicts_by_dict(flavors, common_filters)
828

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

    
855
    def main(self):
856
        super(self.__class__, self)._run()
857
        self._run()
858

    
859

    
860
@command(flavor_cmds)
861
class flavor_info(_init_cyclades, _optional_json):
862
    """Detailed information on a hardware flavor
863
    To get a list of available flavors and flavor ids, try /flavor list
864
    """
865

    
866
    @errors.generic.all
867
    @errors.cyclades.connection
868
    @errors.cyclades.flavor_id
869
    def _run(self, flavor_id):
870
        self._print(
871
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
872

    
873
    def main(self, flavor_id):
874
        super(self.__class__, self)._run()
875
        self._run(flavor_id=flavor_id)
876

    
877

    
878
def _add_name(self, net):
879
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
880
        if user_id:
881
            uuids.append(user_id)
882
        if tenant_id:
883
            uuids.append(tenant_id)
884
        if uuids:
885
            usernames = self._uuids2usernames(uuids)
886
            if user_id:
887
                net['user_id'] += ' (%s)' % usernames[user_id]
888
            if tenant_id:
889
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]