Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 109fc65a

History | View | Annotate | Download (33.5 kB)

1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from base64 import b64encode
35
from os.path import exists, expanduser
36
from io import StringIO
37
from pydoc import pager
38

    
39
from kamaki.cli import command
40
from kamaki.cli.command_tree import CommandTree
41
from kamaki.cli.utils import remove_from_items, filter_dicts_by_dict
42
from kamaki.cli.errors import (
43
    raiseCLIError, CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
44
from kamaki.clients.cyclades import CycladesClient
45
from kamaki.cli.argument import (
46
    FlagArgument, ValueArgument, KeyValueArgument, RepeatableArgument,
47
    ProgressBarArgument, DateArgument, IntArgument, StatusArgument)
48
from kamaki.cli.commands import _command_init, errors, addLogSettings
49
from kamaki.cli.commands import (
50
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
51

    
52

    
53
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
54
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
55
_commands = [server_cmds, flavor_cmds]
56

    
57

    
58
about_authentication = '\nUser Authentication:\
59
    \n* to check authentication: /user authenticate\
60
    \n* to set authentication token: /config set cloud.<cloud>.token <token>'
61

    
62
howto_personality = [
63
    'Defines a file to be injected to virtual servers file system.',
64
    'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
65
    '  [local-path=]PATH: local file to be injected (relative or absolute)',
66
    '  [server-path=]SERVER_PATH: destination location inside server Image',
67
    '  [owner=]OWNER: virtual servers user id for the remote file',
68
    '  [group=]GROUP: virtual servers group id or name for the remote file',
69
    '  [mode=]MODE: permission in octal (e.g., 0777)',
70
    'e.g., -p /tmp/my.file,owner=root,mode=0777']
71

    
72
server_states = ('BUILD', 'ACTIVE', 'STOPPED', 'REBOOT')
73

    
74

    
75
class _service_wait(object):
76

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

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

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

    
104

    
105
class _server_wait(_service_wait):
106

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

    
113

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

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

    
141

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

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

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

    
174
    def _add_user_name(self, servers):
175
        uuids = self._uuids2usernames(list(set(
176
                [srv['user_id'] for srv in servers] +
177
                [srv['tenant_id'] for srv in servers])))
178
        for srv in servers:
179
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
180
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
181
        return servers
182

    
183
    def _apply_common_filters(self, servers):
184
        common_filters = dict()
185
        if self['status']:
186
            common_filters['status'] = self['status']
187
        if self['user_id'] or self['user_name']:
188
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
189
            common_filters['user_id'] = uuid
190
        return filter_dicts_by_dict(servers, common_filters)
191

    
192
    def _filter_by_image(self, servers):
193
        iid = self['image_id']
194
        return [srv for srv in servers if srv['image']['id'] == iid]
195

    
196
    def _filter_by_flavor(self, servers):
197
        fid = self['flavor_id']
198
        return [srv for srv in servers if (
199
            '%s' % srv['image']['id'] == '%s' % fid)]
200

    
201
    def _filter_by_metadata(self, servers):
202
        new_servers = []
203
        for srv in servers:
204
            if not 'metadata' in srv:
205
                continue
206
            meta = [dict(srv['metadata'])]
207
            if self['meta']:
208
                meta = filter_dicts_by_dict(meta, self['meta'])
209
            if meta and self['meta_like']:
210
                meta = filter_dicts_by_dict(
211
                    meta, self['meta_like'], exact_match=False)
212
            if meta:
213
                new_servers.append(srv)
214
        return new_servers
215

    
216
    @errors.generic.all
217
    @errors.cyclades.connection
218
    @errors.cyclades.date
219
    def _run(self):
220
        withimage = bool(self['image_id'])
221
        withflavor = bool(self['flavor_id'])
222
        withmeta = bool(self['meta'] or self['meta_like'])
223
        withcommons = bool(
224
            self['status'] or self['user_id'] or self['user_name'])
225
        detail = self['detail'] or (
226
            withimage or withflavor or withmeta or withcommons)
227
        servers = self.client.list_servers(detail, self['since'])
228

    
229
        servers = self._filter_by_name(servers)
230
        servers = self._filter_by_id(servers)
231
        servers = self._apply_common_filters(servers)
232
        if withimage:
233
            servers = self._filter_by_image(servers)
234
        if withflavor:
235
            servers = self._filter_by_flavor(servers)
236
        if withmeta:
237
            servers = self._filter_by_metadata(servers)
238

    
239
        if self['detail'] and not (
240
                self['json_output'] or self['output_format']):
241
            servers = self._add_user_name(servers)
242
        elif not (self['detail'] or (
243
                self['json_output'] or self['output_format'])):
244
            remove_from_items(servers, 'links')
245
        if detail and not self['detail']:
246
            for srv in servers:
247
                for key in set(srv).difference(self.PERMANENTS):
248
                    srv.pop(key)
249
        kwargs = dict(with_enumeration=self['enum'])
250
        if self['more']:
251
            kwargs['out'] = StringIO()
252
            kwargs['title'] = ()
253
        if self['limit']:
254
            servers = servers[:self['limit']]
255
        self._print(servers, **kwargs)
256
        if self['more']:
257
            pager(kwargs['out'].getvalue())
258

    
259
    def main(self):
260
        super(self.__class__, self)._run()
261
        self._run()
262

    
263

    
264
@command(server_cmds)
265
class server_info(_init_cyclades, _optional_json):
266
    """Detailed information on a Virtual Machine"""
267

    
268
    arguments = dict(
269
        nics=FlagArgument(
270
            'Show only the network interfaces of this virtual server',
271
            '--nics'),
272
        network_id=ValueArgument(
273
            'Show the connection details to that network', '--network-id'),
274
        stats=FlagArgument('Get URLs for server statistics', '--stats'),
275
        diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
276
    )
277

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

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

    
311

    
312
class PersonalityArgument(KeyValueArgument):
313

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

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

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

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

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

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

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

    
379

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

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

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

    
421

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

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

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

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

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

    
522

    
523
class FirewallProfileArgument(ValueArgument):
524

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

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

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

    
542

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

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

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

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

    
587

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

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

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

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

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

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

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

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

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

    
629

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

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

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

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

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

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

    
671

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

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

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

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

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

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

    
701

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

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

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

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

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

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

    
731

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

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

    
741

    
742
@command(server_cmds)
743
class server_console(_init_cyclades, _optional_json):
744
    """Create a VMC console and show connection information"""
745

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

    
754
    def main(self, server_id):
755
        super(self.__class__, self)._run()
756
        self._run(server_id=server_id)
757

    
758

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

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

    
768

    
769
@command(server_cmds)
770
class server_stats(_init_cyclades, _optional_json):
771
    """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
772

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

    
778

    
779
@command(server_cmds)
780
class server_wait(_init_cyclades, _server_wait):
781
    """Wait for server to change its status (default: BUILD)"""
782

    
783
    arguments = dict(
784
        timeout=IntArgument(
785
            'Wait limit in seconds (default: 60)', '--timeout', default=60),
786
        server_status=StatusArgument(
787
            'Status to wait for (%s, default: %s)' % (
788
                ', '.join(server_states), server_states[0]),
789
            '--status',
790
            valid_states=server_states)
791
    )
792

    
793
    @errors.generic.all
794
    @errors.cyclades.connection
795
    @errors.cyclades.server_id
796
    def _run(self, server_id, current_status):
797
        r = self.client.get_server_details(server_id)
798
        if r['status'].lower() == current_status.lower():
799
            self._wait(server_id, current_status, timeout=self['timeout'])
800
        else:
801
            self.error(
802
                'Server %s: Cannot wait for status %s, '
803
                'status is already %s' % (
804
                    server_id, current_status, r['status']))
805

    
806
    def main(self, server_id):
807
        super(self.__class__, self)._run()
808
        self._run(server_id=server_id, current_status=self['server_status'])
809

    
810

    
811
@command(flavor_cmds)
812
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
813
    """List available hardware flavors"""
814

    
815
    PERMANENTS = ('id', 'name')
816

    
817
    arguments = dict(
818
        detail=FlagArgument('show detailed output', ('-l', '--details')),
819
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
820
        more=FlagArgument(
821
            'output results in pages (-n to set items per page, default 10)',
822
            '--more'),
823
        enum=FlagArgument('Enumerate results', '--enumerate'),
824
        ram=ValueArgument('filter by ram', ('--ram')),
825
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
826
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
827
        disk_template=ValueArgument(
828
            'filter by disk_templace', ('--disk-template'))
829
    )
830

    
831
    def _apply_common_filters(self, flavors):
832
        common_filters = dict()
833
        if self['ram']:
834
            common_filters['ram'] = self['ram']
835
        if self['vcpus']:
836
            common_filters['vcpus'] = self['vcpus']
837
        if self['disk']:
838
            common_filters['disk'] = self['disk']
839
        if self['disk_template']:
840
            common_filters['SNF:disk_template'] = self['disk_template']
841
        return filter_dicts_by_dict(flavors, common_filters)
842

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

    
869
    def main(self):
870
        super(self.__class__, self)._run()
871
        self._run()
872

    
873

    
874
@command(flavor_cmds)
875
class flavor_info(_init_cyclades, _optional_json):
876
    """Detailed information on a hardware flavor
877
    To get a list of available flavors and flavor ids, try /flavor list
878
    """
879

    
880
    @errors.generic.all
881
    @errors.cyclades.connection
882
    @errors.cyclades.flavor_id
883
    def _run(self, flavor_id):
884
        self._print(
885
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
886

    
887
    def main(self, flavor_id):
888
        super(self.__class__, self)._run()
889
        self._run(flavor_id=flavor_id)
890

    
891

    
892
def _add_name(self, net):
893
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
894
        if user_id:
895
            uuids.append(user_id)
896
        if tenant_id:
897
            uuids.append(tenant_id)
898
        if uuids:
899
            usernames = self._uuids2usernames(uuids)
900
            if user_id:
901
                net['user_id'] += ' (%s)' % usernames[user_id]
902
            if tenant_id:
903
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]