Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 264a13f7

History | View | Annotate | Download (34.7 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, ClientError
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
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
56
ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
57
_commands = [server_cmds, flavor_cmds, network_cmds, ip_cmds]
58

    
59

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

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

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

    
121

    
122
class _firewall_wait(_service_wait):
123

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

    
130

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

    
155
    def main(self):
156
        self._run()
157

    
158

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

    
165
    PERMANENTS = ('id', 'name')
166

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

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

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

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

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

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

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

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

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

    
276
    def main(self):
277
        super(self.__class__, self)._run()
278
        self._run()
279

    
280

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

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

    
305

    
306
class PersonalityArgument(KeyValueArgument):
307

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

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

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

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

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

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

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

    
373

    
374
class NetworkIpArgument(RepeatableArgument):
375

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

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

    
393

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

    
398
    arguments = dict(
399
        server_name=ValueArgument('The name of the new server', '--name'),
400
        flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
401
        image_id=IntArgument('The ID of the hardware image', '--image-id'),
402
        personality=PersonalityArgument(
403
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
404
        wait=FlagArgument('Wait server to build', ('-w', '--wait')),
405
        cluster_size=IntArgument(
406
            'Create a cluster of servers of this size. In this case, the name'
407
            'parameter is the prefix of each server in the cluster (e.g.,'
408
            'srv1, srv2, etc.',
409
            '--cluster-size'),
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
    )
417
    required = ('server_name', 'flavor_id', 'image_id')
418

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

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

    
471
    def main(self):
472
        super(self.__class__, self)._run()
473
        self._run(
474
            name=self['server_name'],
475
            flavor_id=self['flavor_id'],
476
            image_id=self['image_id'])
477

    
478

    
479
class FirewallProfileArgument(ValueArgument):
480

    
481
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
482

    
483
    @property
484
    def value(self):
485
        return getattr(self, '_value', None)
486

    
487
    @value.setter
488
    def value(self, new_profile):
489
        if new_profile:
490
            new_profile = new_profile.upper()
491
            if new_profile in self.profiles:
492
                self._value = new_profile
493
            else:
494
                raise CLIInvalidArgument(
495
                    'Invalid firewall profile %s' % new_profile,
496
                    details=['Valid values: %s' % ', '.join(self.profiles)])
497

    
498

    
499
@command(server_cmds)
500
class server_modify(_init_cyclades, _optional_output_cmd):
501
    """Modify attributes of a virtual server"""
502

    
503
    arguments = dict(
504
        server_name=ValueArgument('The new name', '--name'),
505
        flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
506
        firewall_profile=FirewallProfileArgument(
507
            'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
508
            '--firewall'),
509
        metadata_to_set=KeyValueArgument(
510
            'Set metadata in key=value form (can be repeated)',
511
            '--metadata-set'),
512
        metadata_to_delete=RepeatableArgument(
513
            'Delete metadata by key (can be repeated)', '--metadata-del')
514
    )
515
    required = [
516
        'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
517
        'metadata_to_delete']
518

    
519
    @errors.generic.all
520
    @errors.cyclades.connection
521
    @errors.cyclades.server_id
522
    def _run(self, server_id):
523
        if self['server_name']:
524
            self.client.update_server_name((server_id), self['server_name'])
525
        if self['flavor_id']:
526
            self.client.resize_server(server_id, self['flavor_id'])
527
        if self['firewall_profile']:
528
            self.client.set_firewall_profile(
529
                server_id=server_id, profile=self['firewall_profile'])
530
        if self['metadata_to_set']:
531
            self.client.update_server_metadata(
532
                server_id, **self['metadata_to_set'])
533
        for key in self['metadata_to_delete']:
534
            errors.cyclades.metadata(
535
                self.client.delete_server_metadata)(server_id, key=key)
536
        if self['with_output']:
537
            self._optional_output(self.client.get_server_details(server_id))
538

    
539
    def main(self, server_id):
540
        super(self.__class__, self)._run()
541
        self._run(server_id=server_id)
542

    
543

    
544
@command(server_cmds)
545
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
546
    """Delete a virtual server"""
547

    
548
    arguments = dict(
549
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
550
        cluster=FlagArgument(
551
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
552
            'prefix. In that case, the prefix replaces the server id',
553
            '--cluster')
554
    )
555

    
556
    def _server_ids(self, server_var):
557
        if self['cluster']:
558
            return [s['id'] for s in self.client.list_servers() if (
559
                s['name'].startswith(server_var))]
560

    
561
        @errors.cyclades.server_id
562
        def _check_server_id(self, server_id):
563
            return server_id
564

    
565
        return [_check_server_id(self, server_id=server_var), ]
566

    
567
    @errors.generic.all
568
    @errors.cyclades.connection
569
    def _run(self, server_var):
570
        for server_id in self._server_ids(server_var):
571
            if self['wait']:
572
                details = self.client.get_server_details(server_id)
573
                status = details['status']
574

    
575
            r = self.client.delete_server(server_id)
576
            self._optional_output(r)
577

    
578
            if self['wait']:
579
                self._wait(server_id, status)
580

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

    
585

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

    
590
    arguments = dict(
591
        hard=FlagArgument(
592
            'perform a hard reboot (deprecated)', ('-f', '--force')),
593
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
594
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
595
    )
596

    
597
    @errors.generic.all
598
    @errors.cyclades.connection
599
    @errors.cyclades.server_id
600
    def _run(self, server_id):
601
        hard_reboot = self['hard']
602
        if hard_reboot:
603
            self.error(
604
                'WARNING: -f/--force will be deprecated in version 0.12\n'
605
                '\tIn the future, please use --type=hard instead')
606
        if self['type']:
607
            if self['type'].lower() in ('soft', ):
608
                hard_reboot = False
609
            elif self['type'].lower() in ('hard', ):
610
                hard_reboot = True
611
            else:
612
                raise CLISyntaxError(
613
                    'Invalid reboot type %s' % self['type'],
614
                    importance=2, details=[
615
                        '--type values are either SOFT (default) or HARD'])
616

    
617
        r = self.client.reboot_server(int(server_id), hard_reboot)
618
        self._optional_output(r)
619

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

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

    
627

    
628
@command(server_cmds)
629
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
630
    """Start an existing virtual server"""
631

    
632
    arguments = dict(
633
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
634
    )
635

    
636
    @errors.generic.all
637
    @errors.cyclades.connection
638
    @errors.cyclades.server_id
639
    def _run(self, server_id):
640
        status = 'ACTIVE'
641
        if self['wait']:
642
            details = self.client.get_server_details(server_id)
643
            status = details['status']
644
            if status in ('ACTIVE', ):
645
                return
646

    
647
        r = self.client.start_server(int(server_id))
648
        self._optional_output(r)
649

    
650
        if self['wait']:
651
            self._wait(server_id, status)
652

    
653
    def main(self, server_id):
654
        super(self.__class__, self)._run()
655
        self._run(server_id=server_id)
656

    
657

    
658
@command(server_cmds)
659
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
660
    """Shutdown an active virtual server"""
661

    
662
    arguments = dict(
663
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
664
    )
665

    
666
    @errors.generic.all
667
    @errors.cyclades.connection
668
    @errors.cyclades.server_id
669
    def _run(self, server_id):
670
        status = 'STOPPED'
671
        if self['wait']:
672
            details = self.client.get_server_details(server_id)
673
            status = details['status']
674
            if status in ('STOPPED', ):
675
                return
676

    
677
        r = self.client.shutdown_server(int(server_id))
678
        self._optional_output(r)
679

    
680
        if self['wait']:
681
            self._wait(server_id, status)
682

    
683
    def main(self, server_id):
684
        super(self.__class__, self)._run()
685
        self._run(server_id=server_id)
686

    
687

    
688
@command(server_cmds)
689
class server_console(_init_cyclades, _optional_json):
690
    """Get a VNC console to access an existing virtual server
691
    Console connection information provided (at least):
692
    - host: (url or address) a VNC host
693
    - port: (int) the gateway to enter virtual server on host
694
    - password: for VNC authorization
695
    """
696

    
697
    @errors.generic.all
698
    @errors.cyclades.connection
699
    @errors.cyclades.server_id
700
    def _run(self, server_id):
701
        self._print(
702
            self.client.get_server_console(int(server_id)), self.print_dict)
703

    
704
    def main(self, server_id):
705
        super(self.__class__, self)._run()
706
        self._run(server_id=server_id)
707

    
708

    
709
@command(server_cmds)
710
class server_addr(_init_cyclades, _optional_json):
711
    """List the addresses of all network interfaces on a virtual server"""
712

    
713
    arguments = dict(
714
        enum=FlagArgument('Enumerate results', '--enumerate')
715
    )
716

    
717
    @errors.generic.all
718
    @errors.cyclades.connection
719
    @errors.cyclades.server_id
720
    def _run(self, server_id):
721
        reply = self.client.list_server_nics(int(server_id))
722
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
723

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

    
728

    
729
@command(server_cmds)
730
class server_stats(_init_cyclades, _optional_json):
731
    """Get virtual server statistics"""
732

    
733
    @errors.generic.all
734
    @errors.cyclades.connection
735
    @errors.cyclades.server_id
736
    def _run(self, server_id):
737
        self._print(
738
            self.client.get_server_stats(int(server_id)), self.print_dict)
739

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

    
744

    
745
@command(server_cmds)
746
class server_wait(_init_cyclades, _server_wait):
747
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
748

    
749
    arguments = dict(
750
        timeout=IntArgument(
751
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
752
    )
753

    
754
    @errors.generic.all
755
    @errors.cyclades.connection
756
    @errors.cyclades.server_id
757
    def _run(self, server_id, current_status):
758
        r = self.client.get_server_details(server_id)
759
        if r['status'].lower() == current_status.lower():
760
            self._wait(server_id, current_status, timeout=self['timeout'])
761
        else:
762
            self.error(
763
                'Server %s: Cannot wait for status %s, '
764
                'status is already %s' % (
765
                    server_id, current_status, r['status']))
766

    
767
    def main(self, server_id, current_status='BUILD'):
768
        super(self.__class__, self)._run()
769
        self._run(server_id=server_id, current_status=current_status)
770

    
771

    
772
@command(flavor_cmds)
773
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
774
    """List available hardware flavors"""
775

    
776
    PERMANENTS = ('id', 'name')
777

    
778
    arguments = dict(
779
        detail=FlagArgument('show detailed output', ('-l', '--details')),
780
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
781
        more=FlagArgument(
782
            'output results in pages (-n to set items per page, default 10)',
783
            '--more'),
784
        enum=FlagArgument('Enumerate results', '--enumerate'),
785
        ram=ValueArgument('filter by ram', ('--ram')),
786
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
787
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
788
        disk_template=ValueArgument(
789
            'filter by disk_templace', ('--disk-template'))
790
    )
791

    
792
    def _apply_common_filters(self, flavors):
793
        common_filters = dict()
794
        if self['ram']:
795
            common_filters['ram'] = self['ram']
796
        if self['vcpus']:
797
            common_filters['vcpus'] = self['vcpus']
798
        if self['disk']:
799
            common_filters['disk'] = self['disk']
800
        if self['disk_template']:
801
            common_filters['SNF:disk_template'] = self['disk_template']
802
        return filter_dicts_by_dict(flavors, common_filters)
803

    
804
    @errors.generic.all
805
    @errors.cyclades.connection
806
    def _run(self):
807
        withcommons = self['ram'] or self['vcpus'] or (
808
            self['disk'] or self['disk_template'])
809
        detail = self['detail'] or withcommons
810
        flavors = self.client.list_flavors(detail)
811
        flavors = self._filter_by_name(flavors)
812
        flavors = self._filter_by_id(flavors)
813
        if withcommons:
814
            flavors = self._apply_common_filters(flavors)
815
        if not (self['detail'] or (
816
                self['json_output'] or self['output_format'])):
817
            remove_from_items(flavors, 'links')
818
        if detail and not self['detail']:
819
            for flv in flavors:
820
                for key in set(flv).difference(self.PERMANENTS):
821
                    flv.pop(key)
822
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
823
        self._print(
824
            flavors,
825
            with_redundancy=self['detail'], with_enumeration=self['enum'],
826
            **kwargs)
827
        if self['more']:
828
            pager(kwargs['out'].getvalue())
829

    
830
    def main(self):
831
        super(self.__class__, self)._run()
832
        self._run()
833

    
834

    
835
@command(flavor_cmds)
836
class flavor_info(_init_cyclades, _optional_json):
837
    """Detailed information on a hardware flavor
838
    To get a list of available flavors and flavor ids, try /flavor list
839
    """
840

    
841
    @errors.generic.all
842
    @errors.cyclades.connection
843
    @errors.cyclades.flavor_id
844
    def _run(self, flavor_id):
845
        self._print(
846
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
847

    
848
    def main(self, flavor_id):
849
        super(self.__class__, self)._run()
850
        self._run(flavor_id=flavor_id)
851

    
852

    
853
def _add_name(self, net):
854
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
855
        if user_id:
856
            uuids.append(user_id)
857
        if tenant_id:
858
            uuids.append(tenant_id)
859
        if uuids:
860
            usernames = self._uuids2usernames(uuids)
861
            if user_id:
862
                net['user_id'] += ' (%s)' % usernames[user_id]
863
            if tenant_id:
864
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]
865

    
866

    
867
@command(network_cmds)
868
class network_wait(_init_cyclades, _network_wait):
869
    """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
870

    
871
    arguments = dict(
872
        timeout=IntArgument(
873
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
874
    )
875

    
876
    @errors.generic.all
877
    @errors.cyclades.connection
878
    @errors.cyclades.network_id
879
    def _run(self, network_id, current_status):
880
        net = self.client.get_network_details(network_id)
881
        if net['status'].lower() == current_status.lower():
882
            self._wait(network_id, current_status, timeout=self['timeout'])
883
        else:
884
            self.error(
885
                'Network %s: Cannot wait for status %s, '
886
                'status is already %s' % (
887
                    network_id, current_status, net['status']))
888

    
889
    def main(self, network_id, current_status='PENDING'):
890
        super(self.__class__, self)._run()
891
        self._run(network_id=network_id, current_status=current_status)
892

    
893

    
894
@command(ip_cmds)
895
class ip_pools(_init_cyclades, _optional_json):
896
    """List pools of floating IPs"""
897

    
898
    @errors.generic.all
899
    @errors.cyclades.connection
900
    def _run(self):
901
        r = self.client.get_floating_ip_pools()
902
        self._print(r if self['json_output'] or self['output_format'] else r[
903
            'floating_ip_pools'])
904

    
905
    def main(self):
906
        super(self.__class__, self)._run()
907
        self._run()
908

    
909

    
910
@command(ip_cmds)
911
class ip_list(_init_cyclades, _optional_json):
912
    """List reserved floating IPs"""
913

    
914
    @errors.generic.all
915
    @errors.cyclades.connection
916
    def _run(self):
917
        r = self.client.get_floating_ips()
918
        self._print(r if self['json_output'] or self['output_format'] else r[
919
            'floating_ips'])
920

    
921
    def main(self):
922
        super(self.__class__, self)._run()
923
        self._run()
924

    
925

    
926
@command(ip_cmds)
927
class ip_info(_init_cyclades, _optional_json):
928
    """Details for an IP"""
929

    
930
    @errors.generic.all
931
    @errors.cyclades.connection
932
    def _run(self, ip):
933
        self._print(self.client.get_floating_ip(ip), self.print_dict)
934

    
935
    def main(self, IP):
936
        super(self.__class__, self)._run()
937
        self._run(ip=IP)
938

    
939

    
940
@command(ip_cmds)
941
class ip_reserve(_init_cyclades, _optional_json):
942
    """Reserve a floating IP
943
    An IP is reserved from an IP pool. The default IP pool is chosen
944
    automatically, but there is the option if specifying an explicit IP pool.
945
    """
946

    
947
    arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
948

    
949
    @errors.generic.all
950
    @errors.cyclades.connection
951
    def _run(self, ip=None):
952
        self._print([self.client.alloc_floating_ip(self['pool'], ip)])
953

    
954
    def main(self, requested_IP=None):
955
        super(self.__class__, self)._run()
956
        self._run(ip=requested_IP)
957

    
958

    
959
@command(ip_cmds)
960
class ip_release(_init_cyclades, _optional_output_cmd):
961
    """Release a floating IP
962
    The release IP is "returned" to the IP pool it came from.
963
    """
964

    
965
    @errors.generic.all
966
    @errors.cyclades.connection
967
    def _run(self, ip):
968
        self._optional_output(self.client.delete_floating_ip(ip))
969

    
970
    def main(self, IP):
971
        super(self.__class__, self)._run()
972
        self._run(ip=IP)
973

    
974

    
975
@command(ip_cmds)
976
class ip_attach(_init_cyclades, _optional_output_cmd):
977
    """DEPRECATED, use /port create"""
978

    
979
    def main(self):
980
        raise CLISyntaxError('DEPRECATED, replaced by kamaki port create')
981

    
982

    
983
@command(ip_cmds)
984
class ip_detach(_init_cyclades, _optional_output_cmd):
985
    """DEPRECATED, use /port delete"""
986

    
987
    def main(self):
988
        raise CLISyntaxError('DEPRECATED, replaced by kamaki port delete')