Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (31.9 kB)

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

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

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

    
52

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

    
57

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

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

    
72

    
73
class _service_wait(object):
74

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

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

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

    
102

    
103
class _server_wait(_service_wait):
104

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

    
111

    
112
class _network_wait(_service_wait):
113

    
114
    def _wait(self, net_id, current_status, timeout=60):
115
        super(_network_wait, self)._wait(
116
            'Network', net_id, self.client.wait_network, current_status,
117
            timeout=timeout)
118

    
119

    
120
class _firewall_wait(_service_wait):
121

    
122
    def _wait(self, server_id, current_status, timeout=60):
123
        super(_firewall_wait, self)._wait(
124
            'Firewall of server',
125
            server_id, self.client.wait_firewall, current_status,
126
            timeout=timeout)
127

    
128

    
129
class _init_cyclades(_command_init):
130
    @errors.generic.all
131
    @addLogSettings
132
    def _run(self, service='compute'):
133
        if getattr(self, 'cloud', None):
134
            base_url = self._custom_url(service) or self._custom_url(
135
                'cyclades')
136
            if base_url:
137
                token = self._custom_token(service) or self._custom_token(
138
                    'cyclades') or self.config.get_cloud('token')
139
                self.client = CycladesClient(base_url=base_url, token=token)
140
                return
141
        else:
142
            self.cloud = 'default'
143
        if getattr(self, 'auth_base', False):
144
            cyclades_endpoints = self.auth_base.get_service_endpoints(
145
                self._custom_type('cyclades') or 'compute',
146
                self._custom_version('cyclades') or '')
147
            base_url = cyclades_endpoints['publicURL']
148
            token = self.auth_base.token
149
            self.client = CycladesClient(base_url=base_url, token=token)
150
        else:
151
            raise CLIBaseUrlError(service='cyclades')
152

    
153
    def main(self):
154
        self._run()
155

    
156

    
157
@command(server_cmds)
158
class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
159
    """List virtual servers accessible by user
160
    Use filtering arguments (e.g., --name-like) to manage long server lists
161
    """
162

    
163
    PERMANENTS = ('id', 'name')
164

    
165
    arguments = dict(
166
        detail=FlagArgument('show detailed output', ('-l', '--details')),
167
        since=DateArgument(
168
            'show only items since date (\' d/m/Y H:M:S \')',
169
            '--since'),
170
        limit=IntArgument(
171
            'limit number of listed virtual servers', ('-n', '--number')),
172
        more=FlagArgument(
173
            'output results in pages (-n to set items per page, default 10)',
174
            '--more'),
175
        enum=FlagArgument('Enumerate results', '--enumerate'),
176
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
177
        image_id=ValueArgument('filter by image id', ('--image-id')),
178
        user_id=ValueArgument('filter by user id', ('--user-id')),
179
        user_name=ValueArgument('filter by user name', ('--user-name')),
180
        status=ValueArgument(
181
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
182
            ('--status')),
183
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
184
        meta_like=KeyValueArgument(
185
            'print only if in key=value, the value is part of actual value',
186
            ('--metadata-like')),
187
    )
188

    
189
    def _add_user_name(self, servers):
190
        uuids = self._uuids2usernames(list(set(
191
                [srv['user_id'] for srv in servers] +
192
                [srv['tenant_id'] for srv in servers])))
193
        for srv in servers:
194
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
195
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
196
        return servers
197

    
198
    def _apply_common_filters(self, servers):
199
        common_filters = dict()
200
        if self['status']:
201
            common_filters['status'] = self['status']
202
        if self['user_id'] or self['user_name']:
203
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
204
            common_filters['user_id'] = uuid
205
        return filter_dicts_by_dict(servers, common_filters)
206

    
207
    def _filter_by_image(self, servers):
208
        iid = self['image_id']
209
        return [srv for srv in servers if srv['image']['id'] == iid]
210

    
211
    def _filter_by_flavor(self, servers):
212
        fid = self['flavor_id']
213
        return [srv for srv in servers if (
214
            '%s' % srv['image']['id'] == '%s' % fid)]
215

    
216
    def _filter_by_metadata(self, servers):
217
        new_servers = []
218
        for srv in servers:
219
            if not 'metadata' in srv:
220
                continue
221
            meta = [dict(srv['metadata'])]
222
            if self['meta']:
223
                meta = filter_dicts_by_dict(meta, self['meta'])
224
            if meta and self['meta_like']:
225
                meta = filter_dicts_by_dict(
226
                    meta, self['meta_like'], exact_match=False)
227
            if meta:
228
                new_servers.append(srv)
229
        return new_servers
230

    
231
    @errors.generic.all
232
    @errors.cyclades.connection
233
    @errors.cyclades.date
234
    def _run(self):
235
        withimage = bool(self['image_id'])
236
        withflavor = bool(self['flavor_id'])
237
        withmeta = bool(self['meta'] or self['meta_like'])
238
        withcommons = bool(
239
            self['status'] or self['user_id'] or self['user_name'])
240
        detail = self['detail'] or (
241
            withimage or withflavor or withmeta or withcommons)
242
        servers = self.client.list_servers(detail, self['since'])
243

    
244
        servers = self._filter_by_name(servers)
245
        servers = self._filter_by_id(servers)
246
        servers = self._apply_common_filters(servers)
247
        if withimage:
248
            servers = self._filter_by_image(servers)
249
        if withflavor:
250
            servers = self._filter_by_flavor(servers)
251
        if withmeta:
252
            servers = self._filter_by_metadata(servers)
253

    
254
        if self['detail'] and not (
255
                self['json_output'] or self['output_format']):
256
            servers = self._add_user_name(servers)
257
        elif not (self['detail'] or (
258
                self['json_output'] or self['output_format'])):
259
            remove_from_items(servers, 'links')
260
        if detail and not self['detail']:
261
            for srv in servers:
262
                for key in set(srv).difference(self.PERMANENTS):
263
                    srv.pop(key)
264
        kwargs = dict(with_enumeration=self['enum'])
265
        if self['more']:
266
            kwargs['out'] = StringIO()
267
            kwargs['title'] = ()
268
        if self['limit']:
269
            servers = servers[:self['limit']]
270
        self._print(servers, **kwargs)
271
        if self['more']:
272
            pager(kwargs['out'].getvalue())
273

    
274
    def main(self):
275
        super(self.__class__, self)._run()
276
        self._run()
277

    
278

    
279
@command(server_cmds)
280
class server_info(_init_cyclades, _optional_json):
281
    """Detailed information on a Virtual Machine
282
    Contains:
283
    - name, id, status, create/update dates
284
    - network interfaces
285
    - metadata (e.g., os, superuser) and diagnostics
286
    - hardware flavor and os image ids
287
    """
288

    
289
    @errors.generic.all
290
    @errors.cyclades.connection
291
    @errors.cyclades.server_id
292
    def _run(self, server_id):
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
        self._run(server_id=server_id)
302

    
303

    
304
class PersonalityArgument(KeyValueArgument):
305

    
306
    terms = (
307
        ('local-path', 'contents'),
308
        ('server-path', 'path'),
309
        ('owner', 'owner'),
310
        ('group', 'group'),
311
        ('mode', 'mode'))
312

    
313
    @property
314
    def value(self):
315
        return getattr(self, '_value', [])
316

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

    
328
            for k, v in self.terms:
329
                prefix = '%s=' % k
330
                for item in termlist:
331
                    if item.lower().startswith(prefix):
332
                        input_dict[k] = item[len(k) + 1:]
333
                        break
334
                    item = None
335
                if item:
336
                    termlist.remove(item)
337

    
338
            try:
339
                path = input_dict['local-path']
340
            except KeyError:
341
                path = termlist.pop(0)
342
                if not path:
343
                    raise CLIInvalidArgument(
344
                        '--personality: No local path specified',
345
                        details=howto_personality)
346

    
347
            if not exists(path):
348
                raise CLIInvalidArgument(
349
                    '--personality: File %s does not exist' % path,
350
                    details=howto_personality)
351

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

    
371

    
372
class NetworkIpArgument(RepeatableArgument):
373

    
374
    @property
375
    def value(self):
376
        return getattr(self, '_value', [])
377

    
378
    @value.setter
379
    def value(self, new_value):
380
        for v in (new_value or []):
381
            net_and_ip = v.split(',')
382
            if len(net_and_ip) < 2:
383
                raise CLIInvalidArgument(
384
                    'Value "%s" is missing parts' % v,
385
                    details=['Correct format: %s NETWORK_ID,IP' % (
386
                        self.parsed_name[0])])
387
            self._value = getattr(self, '_value', list())
388
            self._value.append(
389
                dict(network=net_and_ip[0], fixed_ip=net_and_ip[1]))
390

    
391

    
392
@command(server_cmds)
393
class server_create(_init_cyclades, _optional_json, _server_wait):
394
    """Create a server (aka Virtual Machine)"""
395

    
396
    arguments = dict(
397
        server_name=ValueArgument('The name of the new server', '--name'),
398
        flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
399
        image_id=ValueArgument('The ID of the hardware image', '--image-id'),
400
        personality=PersonalityArgument(
401
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
402
        wait=FlagArgument('Wait server to build', ('-w', '--wait')),
403
        cluster_size=IntArgument(
404
            'Create a cluster of servers of this size. In this case, the name'
405
            'parameter is the prefix of each server in the cluster (e.g.,'
406
            'srv1, srv2, etc.',
407
            '--cluster-size'),
408
        max_threads=IntArgument(
409
            'Max threads in cluster mode (default 1)', '--threads'),
410
        network_id=RepeatableArgument(
411
            'Connect server to network (can be repeated)', '--network'),
412
        network_id_and_ip=NetworkIpArgument(
413
            'Connect server to network w. floating ip ( NETWORK_ID,IP )'
414
            '(can be repeated)',
415
            '--network-with-ip'),
416
        automatic_ip=FlagArgument(
417
            'Automatically assign an IP to the server', '--automatic-ip')
418
    )
419
    required = ('server_name', 'flavor_id', 'image_id')
420

    
421
    @errors.cyclades.cluster_size
422
    def _create_cluster(self, prefix, flavor_id, image_id, size):
423
        if self['automatic_ip']:
424
            networks = []
425
        else:
426
            networks = [dict(network=netid) for netid in (
427
                (self['network_id'] or []) + (self['network_id_and_ip'] or [])
428
            )] or None
429
        servers = [dict(
430
            name='%s%s' % (prefix, i if size > 1 else ''),
431
            flavor_id=flavor_id,
432
            image_id=image_id,
433
            personality=self['personality'],
434
            networks=networks) for i in range(1, 1 + size)]
435
        if size == 1:
436
            return [self.client.create_server(**servers[0])]
437
        self.client.MAX_THREADS = int(self['max_threads'] or 1)
438
        try:
439
            r = self.client.async_run(self.client.create_server, servers)
440
            return r
441
        except Exception as e:
442
            if size == 1:
443
                raise e
444
            try:
445
                requested_names = [s['name'] for s in servers]
446
                spawned_servers = [dict(
447
                    name=s['name'],
448
                    id=s['id']) for s in self.client.list_servers() if (
449
                        s['name'] in requested_names)]
450
                self.error('Failed to build %s servers' % size)
451
                self.error('Found %s matching servers:' % len(spawned_servers))
452
                self._print(spawned_servers, out=self._err)
453
                self.error('Check if any of these servers should be removed\n')
454
            except Exception as ne:
455
                self.error('Error (%s) while notifying about errors' % ne)
456
            finally:
457
                raise e
458

    
459
    @errors.generic.all
460
    @errors.cyclades.connection
461
    @errors.plankton.id
462
    @errors.cyclades.flavor_id
463
    def _run(self, name, flavor_id, image_id):
464
        for r in self._create_cluster(
465
                name, flavor_id, image_id, size=self['cluster_size'] or 1):
466
            if not r:
467
                self.error('Create %s: server response was %s' % (name, r))
468
                continue
469
            usernames = self._uuids2usernames(
470
                [r['user_id'], r['tenant_id']])
471
            r['user_id'] += ' (%s)' % usernames[r['user_id']]
472
            r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
473
            self._print(r, self.print_dict)
474
            if self['wait']:
475
                self._wait(r['id'], r['status'])
476
            self.writeln(' ')
477

    
478
    def main(self):
479
        super(self.__class__, self)._run()
480
        if self['automatic_ip'] and (
481
                self['network_id'] or self['network_id_and_ip']):
482
            raise CLIInvalidArgument('Invalid argument combination', details=[
483
                'Argument %s should not be combined with other' % (
484
                    self.arguments['automatic_ip'].lvalue),
485
                'network-related arguments i.e., %s or %s' % (
486
                    self.arguments['network_id'].lvalue,
487
                    self.arguments['network_id_and_ip'].lvalue)])
488
        self._run(
489
            name=self['server_name'],
490
            flavor_id=self['flavor_id'],
491
            image_id=self['image_id'])
492

    
493

    
494
class FirewallProfileArgument(ValueArgument):
495

    
496
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
497

    
498
    @property
499
    def value(self):
500
        return getattr(self, '_value', None)
501

    
502
    @value.setter
503
    def value(self, new_profile):
504
        if new_profile:
505
            new_profile = new_profile.upper()
506
            if new_profile in self.profiles:
507
                self._value = new_profile
508
            else:
509
                raise CLIInvalidArgument(
510
                    'Invalid firewall profile %s' % new_profile,
511
                    details=['Valid values: %s' % ', '.join(self.profiles)])
512

    
513

    
514
@command(server_cmds)
515
class server_modify(_init_cyclades, _optional_output_cmd):
516
    """Modify attributes of a virtual server"""
517

    
518
    arguments = dict(
519
        server_name=ValueArgument('The new name', '--name'),
520
        flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
521
        firewall_profile=FirewallProfileArgument(
522
            'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
523
            '--firewall'),
524
        metadata_to_set=KeyValueArgument(
525
            'Set metadata in key=value form (can be repeated)',
526
            '--metadata-set'),
527
        metadata_to_delete=RepeatableArgument(
528
            'Delete metadata by key (can be repeated)', '--metadata-del')
529
    )
530
    required = [
531
        'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
532
        'metadata_to_delete']
533

    
534
    @errors.generic.all
535
    @errors.cyclades.connection
536
    @errors.cyclades.server_id
537
    def _run(self, server_id):
538
        if self['server_name']:
539
            self.client.update_server_name((server_id), self['server_name'])
540
        if self['flavor_id']:
541
            self.client.resize_server(server_id, self['flavor_id'])
542
        if self['firewall_profile']:
543
            self.client.set_firewall_profile(
544
                server_id=server_id, profile=self['firewall_profile'])
545
        if self['metadata_to_set']:
546
            self.client.update_server_metadata(
547
                server_id, **self['metadata_to_set'])
548
        for key in self['metadata_to_delete']:
549
            errors.cyclades.metadata(
550
                self.client.delete_server_metadata)(server_id, key=key)
551
        if self['with_output']:
552
            self._optional_output(self.client.get_server_details(server_id))
553

    
554
    def main(self, server_id):
555
        super(self.__class__, self)._run()
556
        self._run(server_id=server_id)
557

    
558

    
559
@command(server_cmds)
560
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
561
    """Delete a virtual server"""
562

    
563
    arguments = dict(
564
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
565
        cluster=FlagArgument(
566
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
567
            'prefix. In that case, the prefix replaces the server id',
568
            '--cluster')
569
    )
570

    
571
    def _server_ids(self, server_var):
572
        if self['cluster']:
573
            return [s['id'] for s in self.client.list_servers() if (
574
                s['name'].startswith(server_var))]
575

    
576
        @errors.cyclades.server_id
577
        def _check_server_id(self, server_id):
578
            return server_id
579

    
580
        return [_check_server_id(self, server_id=server_var), ]
581

    
582
    @errors.generic.all
583
    @errors.cyclades.connection
584
    def _run(self, server_var):
585
        for server_id in self._server_ids(server_var):
586
            if self['wait']:
587
                details = self.client.get_server_details(server_id)
588
                status = details['status']
589

    
590
            r = self.client.delete_server(server_id)
591
            self._optional_output(r)
592

    
593
            if self['wait']:
594
                self._wait(server_id, status)
595

    
596
    def main(self, server_id_or_cluster_prefix):
597
        super(self.__class__, self)._run()
598
        self._run(server_id_or_cluster_prefix)
599

    
600

    
601
@command(server_cmds)
602
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
603
    """Reboot a virtual server"""
604

    
605
    arguments = dict(
606
        hard=FlagArgument(
607
            'perform a hard reboot (deprecated)', ('-f', '--force')),
608
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
609
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
610
    )
611

    
612
    @errors.generic.all
613
    @errors.cyclades.connection
614
    @errors.cyclades.server_id
615
    def _run(self, server_id):
616
        hard_reboot = self['hard']
617
        if hard_reboot:
618
            self.error(
619
                'WARNING: -f/--force will be deprecated in version 0.12\n'
620
                '\tIn the future, please use --type=hard instead')
621
        if self['type']:
622
            if self['type'].lower() in ('soft', ):
623
                hard_reboot = False
624
            elif self['type'].lower() in ('hard', ):
625
                hard_reboot = True
626
            else:
627
                raise CLISyntaxError(
628
                    'Invalid reboot type %s' % self['type'],
629
                    importance=2, details=[
630
                        '--type values are either SOFT (default) or HARD'])
631

    
632
        r = self.client.reboot_server(int(server_id), hard_reboot)
633
        self._optional_output(r)
634

    
635
        if self['wait']:
636
            self._wait(server_id, 'REBOOT')
637

    
638
    def main(self, server_id):
639
        super(self.__class__, self)._run()
640
        self._run(server_id=server_id)
641

    
642

    
643
@command(server_cmds)
644
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
645
    """Start an existing virtual server"""
646

    
647
    arguments = dict(
648
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
649
    )
650

    
651
    @errors.generic.all
652
    @errors.cyclades.connection
653
    @errors.cyclades.server_id
654
    def _run(self, server_id):
655
        status = 'ACTIVE'
656
        if self['wait']:
657
            details = self.client.get_server_details(server_id)
658
            status = details['status']
659
            if status in ('ACTIVE', ):
660
                return
661

    
662
        r = self.client.start_server(int(server_id))
663
        self._optional_output(r)
664

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

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

    
672

    
673
@command(server_cmds)
674
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
675
    """Shutdown an active virtual server"""
676

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

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

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

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

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

    
702

    
703
@command(server_cmds)
704
class server_console(_init_cyclades, _optional_json):
705
    """Get a VNC console to access an existing virtual server
706
    Console connection information provided (at least):
707
    - host: (url or address) a VNC host
708
    - port: (int) the gateway to enter virtual server on host
709
    - password: for VNC authorization
710
    """
711

    
712
    @errors.generic.all
713
    @errors.cyclades.connection
714
    @errors.cyclades.server_id
715
    def _run(self, server_id):
716
        self._print(
717
            self.client.get_server_console(int(server_id)), self.print_dict)
718

    
719
    def main(self, server_id):
720
        super(self.__class__, self)._run()
721
        self._run(server_id=server_id)
722

    
723

    
724
@command(server_cmds)
725
class server_addr(_init_cyclades, _optional_json):
726
    """List the addresses of all network interfaces on a virtual server"""
727

    
728
    arguments = dict(
729
        enum=FlagArgument('Enumerate results', '--enumerate')
730
    )
731

    
732
    @errors.generic.all
733
    @errors.cyclades.connection
734
    @errors.cyclades.server_id
735
    def _run(self, server_id):
736
        reply = self.client.list_server_nics(int(server_id))
737
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
738

    
739
    def main(self, server_id):
740
        super(self.__class__, self)._run()
741
        self._run(server_id=server_id)
742

    
743

    
744
@command(server_cmds)
745
class server_stats(_init_cyclades, _optional_json):
746
    """Get virtual server statistics"""
747

    
748
    @errors.generic.all
749
    @errors.cyclades.connection
750
    @errors.cyclades.server_id
751
    def _run(self, server_id):
752
        self._print(
753
            self.client.get_server_stats(int(server_id)), self.print_dict)
754

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

    
759

    
760
@command(server_cmds)
761
class server_wait(_init_cyclades, _server_wait):
762
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
763

    
764
    arguments = dict(
765
        timeout=IntArgument(
766
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
767
    )
768

    
769
    @errors.generic.all
770
    @errors.cyclades.connection
771
    @errors.cyclades.server_id
772
    def _run(self, server_id, current_status):
773
        r = self.client.get_server_details(server_id)
774
        if r['status'].lower() == current_status.lower():
775
            self._wait(server_id, current_status, timeout=self['timeout'])
776
        else:
777
            self.error(
778
                'Server %s: Cannot wait for status %s, '
779
                'status is already %s' % (
780
                    server_id, current_status, r['status']))
781

    
782
    def main(self, server_id, current_status='BUILD'):
783
        super(self.__class__, self)._run()
784
        self._run(server_id=server_id, current_status=current_status)
785

    
786

    
787
@command(flavor_cmds)
788
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
789
    """List available hardware flavors"""
790

    
791
    PERMANENTS = ('id', 'name')
792

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

    
807
    def _apply_common_filters(self, flavors):
808
        common_filters = dict()
809
        if self['ram']:
810
            common_filters['ram'] = self['ram']
811
        if self['vcpus']:
812
            common_filters['vcpus'] = self['vcpus']
813
        if self['disk']:
814
            common_filters['disk'] = self['disk']
815
        if self['disk_template']:
816
            common_filters['SNF:disk_template'] = self['disk_template']
817
        return filter_dicts_by_dict(flavors, common_filters)
818

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

    
845
    def main(self):
846
        super(self.__class__, self)._run()
847
        self._run()
848

    
849

    
850
@command(flavor_cmds)
851
class flavor_info(_init_cyclades, _optional_json):
852
    """Detailed information on a hardware flavor
853
    To get a list of available flavors and flavor ids, try /flavor list
854
    """
855

    
856
    @errors.generic.all
857
    @errors.cyclades.connection
858
    @errors.cyclades.flavor_id
859
    def _run(self, flavor_id):
860
        self._print(
861
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
862

    
863
    def main(self, flavor_id):
864
        super(self.__class__, self)._run()
865
        self._run(flavor_id=flavor_id)
866

    
867

    
868
def _add_name(self, net):
869
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
870
        if user_id:
871
            uuids.append(user_id)
872
        if tenant_id:
873
            uuids.append(tenant_id)
874
        if uuids:
875
            usernames = self._uuids2usernames(uuids)
876
            if user_id:
877
                net['user_id'] += ' (%s)' % usernames[user_id]
878
            if tenant_id:
879
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]