Statistics
| Branch: | Tag: | Revision:

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

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
        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
        diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
275
    )
276

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

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

    
312

    
313
class PersonalityArgument(KeyValueArgument):
314

    
315
    terms = (
316
        ('local-path', 'contents'),
317
        ('server-path', 'path'),
318
        ('owner', 'owner'),
319
        ('group', 'group'),
320
        ('mode', 'mode'))
321

    
322
    @property
323
    def value(self):
324
        return getattr(self, '_value', [])
325

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

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

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

    
356
            if not exists(path):
357
                raise CLIInvalidArgument(
358
                    '--personality: File %s does not exist' % path,
359
                    details=howto_personality)
360

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

    
380

    
381
class NetworkArgument(RepeatableArgument):
382
    """[id=]NETWORK_ID[,[ip=]IP]"""
383

    
384
    @property
385
    def value(self):
386
        return getattr(self, '_value', self.default)
387

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

    
422

    
423
@command(server_cmds)
424
class server_create(_init_cyclades, _optional_json, _server_wait):
425
    """Create a server (aka Virtual Machine)"""
426

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

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

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

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

    
523

    
524
class FirewallProfileArgument(ValueArgument):
525

    
526
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
527

    
528
    @property
529
    def value(self):
530
        return getattr(self, '_value', None)
531

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

    
543

    
544
@command(server_cmds)
545
class server_modify(_init_cyclades, _optional_output_cmd):
546
    """Modify attributes of a virtual server"""
547

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

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

    
584
    def main(self, server_id):
585
        super(self.__class__, self)._run()
586
        self._run(server_id=server_id)
587

    
588

    
589
@command(server_cmds)
590
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
591
    """Delete a virtual server"""
592

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

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

    
606
        @errors.cyclades.server_id
607
        def _check_server_id(self, server_id):
608
            return server_id
609

    
610
        return [_check_server_id(self, server_id=server_var), ]
611

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

    
620
            r = self.client.delete_server(server_id)
621
            self._optional_output(r)
622

    
623
            if self['wait']:
624
                self._wait(server_id, status)
625

    
626
    def main(self, server_id_or_cluster_prefix):
627
        super(self.__class__, self)._run()
628
        self._run(server_id_or_cluster_prefix)
629

    
630

    
631
@command(server_cmds)
632
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
633
    """Reboot a virtual server"""
634

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

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

    
662
        r = self.client.reboot_server(int(server_id), hard_reboot)
663
        self._optional_output(r)
664

    
665
        if self['wait']:
666
            self._wait(server_id, 'REBOOT')
667

    
668
    def main(self, server_id):
669
        super(self.__class__, self)._run()
670
        self._run(server_id=server_id)
671

    
672

    
673
@command(server_cmds)
674
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
675
    """Start an existing virtual server"""
676

    
677
    arguments = dict(
678
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
679
    )
680

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

    
692
        r = self.client.start_server(int(server_id))
693
        self._optional_output(r)
694

    
695
        if self['wait']:
696
            self._wait(server_id, status)
697

    
698
    def main(self, server_id):
699
        super(self.__class__, self)._run()
700
        self._run(server_id=server_id)
701

    
702

    
703
@command(server_cmds)
704
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
705
    """Shutdown an active virtual server"""
706

    
707
    arguments = dict(
708
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
709
    )
710

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

    
722
        r = self.client.shutdown_server(int(server_id))
723
        self._optional_output(r)
724

    
725
        if self['wait']:
726
            self._wait(server_id, status)
727

    
728
    def main(self, server_id):
729
        super(self.__class__, self)._run()
730
        self._run(server_id=server_id)
731

    
732

    
733
@command(server_cmds)
734
class server_addr(_init_cyclades):
735
    """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
736

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

    
742

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

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

    
752

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

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

    
762

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

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

    
772

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

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

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

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

    
799

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

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

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

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

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

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

    
862

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

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

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

    
880

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