Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 31cf20c5

History | View | Annotate | Download (33.4 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
        else:
293
            vm = self.client.get_server_details(server_id)
294
            uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
295
            vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
296
            vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
297
            self._print(vm, self.print_dict)
298

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

    
309

    
310
class PersonalityArgument(KeyValueArgument):
311

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

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

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

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

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

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

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

    
377

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

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

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

    
419

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

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

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

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

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

    
520

    
521
class FirewallProfileArgument(ValueArgument):
522

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

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

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

    
540

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

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

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

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

    
585

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

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

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

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

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

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

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

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

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

    
627

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

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

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

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

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

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

    
669

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

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

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

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

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

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

    
699

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

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

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

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

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

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

    
729

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

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

    
739

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

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

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

    
756

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

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

    
766

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

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

    
776

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

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

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

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

    
809

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

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

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

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

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

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

    
872

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

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

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

    
890

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