Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 04c039c8

History | View | Annotate | Download (32.6 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_console(_init_cyclades, _optional_json):
734
    """Create a VMC console and show connection information"""
735

    
736
    @errors.generic.all
737
    @errors.cyclades.connection
738
    @errors.cyclades.server_id
739
    def _run(self, server_id):
740
        self.error('The following credentials will be invalidated shortly')
741
        self._print(
742
            self.client.get_server_console(server_id), self.print_dict)
743

    
744
    def main(self, server_id):
745
        super(self.__class__, self)._run()
746
        self._run(server_id=server_id)
747

    
748

    
749
@command(server_cmds)
750
class server_wait(_init_cyclades, _server_wait):
751
    """Wait for server to change its status (default: BUILD)"""
752

    
753
    arguments = dict(
754
        timeout=IntArgument(
755
            'Wait limit in seconds (default: 60)', '--timeout', default=60),
756
        server_status=StatusArgument(
757
            'Status to wait for (%s, default: %s)' % (
758
                ', '.join(server_states), server_states[0]),
759
            '--status',
760
            valid_states=server_states)
761
    )
762

    
763
    @errors.generic.all
764
    @errors.cyclades.connection
765
    @errors.cyclades.server_id
766
    def _run(self, server_id, current_status):
767
        r = self.client.get_server_details(server_id)
768
        if r['status'].lower() == current_status.lower():
769
            self._wait(server_id, current_status, timeout=self['timeout'])
770
        else:
771
            self.error(
772
                'Server %s: Cannot wait for status %s, '
773
                'status is already %s' % (
774
                    server_id, current_status, r['status']))
775

    
776
    def main(self, server_id):
777
        super(self.__class__, self)._run()
778
        self._run(
779
            server_id=server_id, current_status=self['server_status'] or '')
780

    
781

    
782
@command(flavor_cmds)
783
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
784
    """List available hardware flavors"""
785

    
786
    PERMANENTS = ('id', 'name')
787

    
788
    arguments = dict(
789
        detail=FlagArgument('show detailed output', ('-l', '--details')),
790
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
791
        more=FlagArgument(
792
            'output results in pages (-n to set items per page, default 10)',
793
            '--more'),
794
        enum=FlagArgument('Enumerate results', '--enumerate'),
795
        ram=ValueArgument('filter by ram', ('--ram')),
796
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
797
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
798
        disk_template=ValueArgument(
799
            'filter by disk_templace', ('--disk-template'))
800
    )
801

    
802
    def _apply_common_filters(self, flavors):
803
        common_filters = dict()
804
        if self['ram']:
805
            common_filters['ram'] = self['ram']
806
        if self['vcpus']:
807
            common_filters['vcpus'] = self['vcpus']
808
        if self['disk']:
809
            common_filters['disk'] = self['disk']
810
        if self['disk_template']:
811
            common_filters['SNF:disk_template'] = self['disk_template']
812
        return filter_dicts_by_dict(flavors, common_filters)
813

    
814
    @errors.generic.all
815
    @errors.cyclades.connection
816
    def _run(self):
817
        withcommons = self['ram'] or self['vcpus'] or (
818
            self['disk'] or self['disk_template'])
819
        detail = self['detail'] or withcommons
820
        flavors = self.client.list_flavors(detail)
821
        flavors = self._filter_by_name(flavors)
822
        flavors = self._filter_by_id(flavors)
823
        if withcommons:
824
            flavors = self._apply_common_filters(flavors)
825
        if not (self['detail'] or (
826
                self['json_output'] or self['output_format'])):
827
            remove_from_items(flavors, 'links')
828
        if detail and not self['detail']:
829
            for flv in flavors:
830
                for key in set(flv).difference(self.PERMANENTS):
831
                    flv.pop(key)
832
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
833
        self._print(
834
            flavors,
835
            with_redundancy=self['detail'], with_enumeration=self['enum'],
836
            **kwargs)
837
        if self['more']:
838
            pager(kwargs['out'].getvalue())
839

    
840
    def main(self):
841
        super(self.__class__, self)._run()
842
        self._run()
843

    
844

    
845
@command(flavor_cmds)
846
class flavor_info(_init_cyclades, _optional_json):
847
    """Detailed information on a hardware flavor
848
    To get a list of available flavors and flavor ids, try /flavor list
849
    """
850

    
851
    @errors.generic.all
852
    @errors.cyclades.connection
853
    @errors.cyclades.flavor_id
854
    def _run(self, flavor_id):
855
        self._print(
856
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
857

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

    
862

    
863
def _add_name(self, net):
864
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
865
        if user_id:
866
            uuids.append(user_id)
867
        if tenant_id:
868
            uuids.append(tenant_id)
869
        if uuids:
870
            usernames = self._uuids2usernames(uuids)
871
            if user_id:
872
                net['user_id'] += ' (%s)' % usernames[user_id]
873
            if tenant_id:
874
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]