Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 7cddd0e7

History | View | Annotate | Download (35.3 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
import cStringIO
34
import codecs
35
from base64 import b64encode
36
from os.path import exists, expanduser
37
from io import StringIO
38
from pydoc import pager
39
from json import dumps
40

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

    
54

    
55
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
56
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
57
_commands = [server_cmds, flavor_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
server_states = ('BUILD', 'ACTIVE', 'STOPPED', 'REBOOT')
75

    
76

    
77
class _service_wait(object):
78

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

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

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

    
106

    
107
class _server_wait(_service_wait):
108

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

    
115

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

    
140
    @dataModification
141
    def _restruct_server_info(self, vm):
142
        if not vm:
143
            return vm
144
        img = vm['image']
145
        try:
146
            img.pop('links', None)
147
            img['name'] = self.client.get_image_details(img['id'])['name']
148
        except Exception:
149
            pass
150
        flv = vm['flavor']
151
        try:
152
            flv.pop('links', None)
153
            flv['name'] = self.client.get_flavor_details(flv['id'])['name']
154
        except Exception:
155
            pass
156
        vm['ports'] = vm.pop('attachments', dict())
157
        for port in vm['ports']:
158
            netid = port.get('network_id')
159
            for k in vm['addresses'].get(netid, []):
160
                k.pop('addr', None)
161
                k.pop('version', None)
162
                port.update(k)
163
        uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
164
        vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
165
        for key in ('addresses', 'tenant_id', 'links'):
166
            vm.pop(key, None)
167
        return vm
168

    
169
    def main(self):
170
        self._run()
171

    
172

    
173
@command(server_cmds)
174
class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
175
    """List virtual servers accessible by user
176
    Use filtering arguments (e.g., --name-like) to manage long server lists
177
    """
178

    
179
    PERMANENTS = ('id', 'name')
180

    
181
    arguments = dict(
182
        detail=FlagArgument('show detailed output', ('-l', '--details')),
183
        since=DateArgument(
184
            'show only items since date (\' d/m/Y H:M:S \')',
185
            '--since'),
186
        limit=IntArgument(
187
            'limit number of listed virtual servers', ('-n', '--number')),
188
        more=FlagArgument(
189
            'output results in pages (-n to set items per page, default 10)',
190
            '--more'),
191
        enum=FlagArgument('Enumerate results', '--enumerate'),
192
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
193
        image_id=ValueArgument('filter by image id', ('--image-id')),
194
        user_id=ValueArgument('filter by user id', ('--user-id')),
195
        user_name=ValueArgument('filter by user name', ('--user-name')),
196
        status=ValueArgument(
197
            'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
198
            ('--status')),
199
        meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
200
        meta_like=KeyValueArgument(
201
            'print only if in key=value, the value is part of actual value',
202
            ('--metadata-like')),
203
    )
204

    
205
    def _add_user_name(self, servers):
206
        uuids = self._uuids2usernames(list(set(
207
                [srv['user_id'] for srv in servers] +
208
                [srv['tenant_id'] for srv in servers])))
209
        for srv in servers:
210
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
211
            srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
212
        return servers
213

    
214
    def _apply_common_filters(self, servers):
215
        common_filters = dict()
216
        if self['status']:
217
            common_filters['status'] = self['status']
218
        if self['user_id'] or self['user_name']:
219
            uuid = self['user_id'] or self._username2uuid(self['user_name'])
220
            common_filters['user_id'] = uuid
221
        return filter_dicts_by_dict(servers, common_filters)
222

    
223
    def _filter_by_image(self, servers):
224
        iid = self['image_id']
225
        return [srv for srv in servers if srv['image']['id'] == iid]
226

    
227
    def _filter_by_flavor(self, servers):
228
        fid = self['flavor_id']
229
        return [srv for srv in servers if (
230
            '%s' % srv['image']['id'] == '%s' % fid)]
231

    
232
    def _filter_by_metadata(self, servers):
233
        new_servers = []
234
        for srv in servers:
235
            if not 'metadata' in srv:
236
                continue
237
            meta = [dict(srv['metadata'])]
238
            if self['meta']:
239
                meta = filter_dicts_by_dict(meta, self['meta'])
240
            if meta and self['meta_like']:
241
                meta = filter_dicts_by_dict(
242
                    meta, self['meta_like'], exact_match=False)
243
            if meta:
244
                new_servers.append(srv)
245
        return new_servers
246

    
247
    @errors.generic.all
248
    @errors.cyclades.connection
249
    @errors.cyclades.date
250
    def _run(self):
251
        withimage = bool(self['image_id'])
252
        withflavor = bool(self['flavor_id'])
253
        withmeta = bool(self['meta'] or self['meta_like'])
254
        withcommons = bool(
255
            self['status'] or self['user_id'] or self['user_name'])
256
        detail = self['detail'] or (
257
            withimage or withflavor or withmeta or withcommons)
258
        servers = self.client.list_servers(detail, self['since'])
259

    
260
        servers = self._filter_by_name(servers)
261
        servers = self._filter_by_id(servers)
262
        servers = self._apply_common_filters(servers)
263
        if withimage:
264
            servers = self._filter_by_image(servers)
265
        if withflavor:
266
            servers = self._filter_by_flavor(servers)
267
        if withmeta:
268
            servers = self._filter_by_metadata(servers)
269

    
270
        if detail and self['detail']:
271
            servers = [self._restruct_server_info(vm) for vm in servers]
272
        else:
273
            for srv in servers:
274
                for key in set(srv).difference(self.PERMANENTS):
275
                    srv.pop(key)
276

    
277
        kwargs = dict(with_enumeration=self['enum'])
278
        if self['more']:
279
            codecinfo = codecs.lookup('utf-8')
280
            kwargs['out'] = codecs.StreamReaderWriter(
281
                cStringIO.StringIO(),
282
                codecinfo.streamreader,
283
                codecinfo.streamwriter)
284
            kwargs['title'] = ()
285
        if self['limit']:
286
            servers = servers[:self['limit']]
287
        self._print(servers, **kwargs)
288
        if self['more']:
289
            pager(kwargs['out'].getvalue())
290

    
291
    def main(self):
292
        super(self.__class__, self)._run()
293
        self._run()
294

    
295

    
296
@command(server_cmds)
297
class server_info(_init_cyclades, _optional_json):
298
    """Detailed information on a Virtual Machine"""
299

    
300
    arguments = dict(
301
        nics=FlagArgument(
302
            'Show only the network interfaces of this virtual server',
303
            '--nics'),
304
        network_id=ValueArgument(
305
            'Show the connection details to that network', '--network-id'),
306
        stats=FlagArgument('Get URLs for server statistics', '--stats'),
307
        diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
308
    )
309

    
310
    @errors.generic.all
311
    @errors.cyclades.connection
312
    @errors.cyclades.server_id
313
    def _run(self, server_id):
314
        if self['nics']:
315
            self._print(
316
                self.client.get_server_nics(server_id), self.print_dict)
317
        elif self['network_id']:
318
            self._print(
319
                self.client.get_server_network_nics(
320
                    server_id, self['network_id']), self.print_dict)
321
        elif self['stats']:
322
            self._print(
323
                self.client.get_server_stats(server_id), self.print_dict)
324
        elif self['diagnostics']:
325
            self._print(self.client.get_server_diagnostics(server_id))
326
        else:
327
            vm = self.client.get_server_details(server_id)
328
            self._print(self._restruct_server_info(vm), self.print_dict)
329

    
330
    def main(self, server_id):
331
        super(self.__class__, self)._run()
332
        choose_one = ('nics', 'stats', 'diagnostics')
333
        count = len([a for a in choose_one if self[a]])
334
        if count > 1:
335
            raise CLIInvalidArgument('Invalid argument combination', details=[
336
                'Arguments %s cannot be used simultaneously' % ', '.join(
337
                    [self.arguments[a].lvalue for a in choose_one])])
338
        self._run(server_id=server_id)
339

    
340

    
341
class PersonalityArgument(KeyValueArgument):
342

    
343
    terms = (
344
        ('local-path', 'contents'),
345
        ('server-path', 'path'),
346
        ('owner', 'owner'),
347
        ('group', 'group'),
348
        ('mode', 'mode'))
349

    
350
    @property
351
    def value(self):
352
        return getattr(self, '_value', [])
353

    
354
    @value.setter
355
    def value(self, newvalue):
356
        if newvalue == self.default:
357
            return self.value
358
        self._value, input_dict = [], {}
359
        for i, terms in enumerate(newvalue):
360
            termlist = terms.split(',')
361
            if len(termlist) > len(self.terms):
362
                msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
363
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
364

    
365
            for k, v in self.terms:
366
                prefix = '%s=' % k
367
                for item in termlist:
368
                    if item.lower().startswith(prefix):
369
                        input_dict[k] = item[len(k) + 1:]
370
                        break
371
                    item = None
372
                if item:
373
                    termlist.remove(item)
374

    
375
            try:
376
                path = input_dict['local-path']
377
            except KeyError:
378
                path = termlist.pop(0)
379
                if not path:
380
                    raise CLIInvalidArgument(
381
                        '--personality: No local path specified',
382
                        details=howto_personality)
383

    
384
            if not exists(path):
385
                raise CLIInvalidArgument(
386
                    '--personality: File %s does not exist' % path,
387
                    details=howto_personality)
388

    
389
            self._value.append(dict(path=path))
390
            with open(expanduser(path)) as f:
391
                self._value[i]['contents'] = b64encode(f.read())
392
            for k, v in self.terms[1:]:
393
                try:
394
                    self._value[i][v] = input_dict[k]
395
                except KeyError:
396
                    try:
397
                        self._value[i][v] = termlist.pop(0)
398
                    except IndexError:
399
                        continue
400
                if k in ('mode', ) and self._value[i][v]:
401
                    try:
402
                        self._value[i][v] = int(self._value[i][v], 8)
403
                    except ValueError as ve:
404
                        raise CLIInvalidArgument(
405
                            'Personality mode must be in octal', details=[
406
                                '%s' % ve])
407

    
408

    
409
class NetworkArgument(RepeatableArgument):
410
    """[id=]NETWORK_ID[,[ip=]IP]"""
411

    
412
    @property
413
    def value(self):
414
        return getattr(self, '_value', self.default)
415

    
416
    @value.setter
417
    def value(self, new_value):
418
        for v in new_value or []:
419
            part1, sep, part2 = v.partition(',')
420
            netid, ip = '', ''
421
            if part1.startswith('id='):
422
                netid = part1[len('id='):]
423
            elif part1.startswith('ip='):
424
                ip = part1[len('ip='):]
425
            else:
426
                netid = part1
427
            if part2:
428
                if (part2.startswith('id=') and netid) or (
429
                        part2.startswith('ip=') and ip):
430
                    raise CLIInvalidArgument(
431
                        'Invalid network argument %s' % v, details=[
432
                        'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
433
                if part2.startswith('id='):
434
                    netid = part2[len('id='):]
435
                elif part2.startswith('ip='):
436
                    ip = part2[len('ip='):]
437
                elif netid:
438
                    ip = part2
439
                else:
440
                    netid = part2
441
            if not netid:
442
                raise CLIInvalidArgument(
443
                    'Invalid network argument %s' % v, details=[
444
                    'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
445
            self._value = getattr(self, '_value', [])
446
            self._value.append(dict(uuid=netid))
447
            if ip:
448
                self._value[-1]['fixed_ip'] = ip
449

    
450

    
451
@command(server_cmds)
452
class server_create(_init_cyclades, _optional_json, _server_wait):
453
    """Create a server (aka Virtual Machine)"""
454

    
455
    arguments = dict(
456
        server_name=ValueArgument('The name of the new server', '--name'),
457
        flavor_id=IntArgument('The ID of the flavor', '--flavor-id'),
458
        image_id=ValueArgument('The ID of the image', '--image-id'),
459
        personality=PersonalityArgument(
460
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
461
        wait=FlagArgument('Wait server to build', ('-w', '--wait')),
462
        cluster_size=IntArgument(
463
            'Create a cluster of servers of this size. In this case, the name'
464
            'parameter is the prefix of each server in the cluster (e.g.,'
465
            'srv1, srv2, etc.',
466
            '--cluster-size'),
467
        max_threads=IntArgument(
468
            'Max threads in cluster mode (default 1)', '--threads'),
469
        network_configuration=NetworkArgument(
470
            'Connect server to network: [id=]NETWORK_ID[,[ip=]IP]        . '
471
            'Use only NETWORK_ID for private networks.        . '
472
            'Use NETWORK_ID,[ip=]IP for networks with IP.        . '
473
            'Can be repeated, mutually exclussive with --no-network',
474
            '--network'),
475
        no_network=FlagArgument(
476
            'Do not create any network NICs on the server.        . '
477
            'Mutually exclusive to --network        . '
478
            'If neither --network or --no-network are used, the default '
479
            'network policy is applied. These policies are set on the cloud, '
480
            'so kamaki is oblivious to them',
481
            '--no-network')
482
    )
483
    required = ('server_name', 'flavor_id', 'image_id')
484

    
485
    @errors.cyclades.cluster_size
486
    def _create_cluster(self, prefix, flavor_id, image_id, size):
487
        networks = self['network_configuration'] or (
488
            [] if self['no_network'] else None)
489
        servers = [dict(
490
            name='%s%s' % (prefix, i if size > 1 else ''),
491
            flavor_id=flavor_id,
492
            image_id=image_id,
493
            personality=self['personality'],
494
            networks=networks) for i in range(1, 1 + size)]
495
        if size == 1:
496
            return [self.client.create_server(**servers[0])]
497
        self.client.MAX_THREADS = int(self['max_threads'] or 1)
498
        try:
499
            r = self.client.async_run(self.client.create_server, servers)
500
            return r
501
        except Exception as e:
502
            if size == 1:
503
                raise e
504
            try:
505
                requested_names = [s['name'] for s in servers]
506
                spawned_servers = [dict(
507
                    name=s['name'],
508
                    id=s['id']) for s in self.client.list_servers() if (
509
                        s['name'] in requested_names)]
510
                self.error('Failed to build %s servers' % size)
511
                self.error('Found %s matching servers:' % len(spawned_servers))
512
                self._print(spawned_servers, out=self._err)
513
                self.error('Check if any of these servers should be removed\n')
514
            except Exception as ne:
515
                self.error('Error (%s) while notifying about errors' % ne)
516
            finally:
517
                raise e
518

    
519
    @errors.generic.all
520
    @errors.cyclades.connection
521
    @errors.plankton.id
522
    @errors.cyclades.flavor_id
523
    def _run(self, name, flavor_id, image_id):
524
        for r in self._create_cluster(
525
                name, flavor_id, image_id, size=self['cluster_size'] or 1):
526
            if not r:
527
                self.error('Create %s: server response was %s' % (name, r))
528
                continue
529
            self._print(self._restruct_server_info(r), self.print_dict)
530
            if self['wait']:
531
                self._wait(r['id'], r['status'] or 'BUILD')
532
            self.writeln(' ')
533

    
534
    def main(self):
535
        super(self.__class__, self)._run()
536
        if self['no_network'] and self['network_configuration']:
537
            raise CLIInvalidArgument(
538
                'Invalid argument compination', importance=2, details=[
539
                'Arguments %s and %s are mutually exclusive' % (
540
                    self.arguments['no_network'].lvalue,
541
                    self.arguments['network_configuration'].lvalue)])
542
        self._run(
543
            name=self['server_name'],
544
            flavor_id=self['flavor_id'],
545
            image_id=self['image_id'])
546

    
547

    
548
class FirewallProfileArgument(ValueArgument):
549

    
550
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
551

    
552
    @property
553
    def value(self):
554
        return getattr(self, '_value', None)
555

    
556
    @value.setter
557
    def value(self, new_profile):
558
        if new_profile:
559
            new_profile = new_profile.upper()
560
            if new_profile in self.profiles:
561
                self._value = new_profile
562
            else:
563
                raise CLIInvalidArgument(
564
                    'Invalid firewall profile %s' % new_profile,
565
                    details=['Valid values: %s' % ', '.join(self.profiles)])
566

    
567

    
568
@command(server_cmds)
569
class server_modify(_init_cyclades, _optional_output_cmd):
570
    """Modify attributes of a virtual server"""
571

    
572
    arguments = dict(
573
        server_name=ValueArgument('The new name', '--name'),
574
        flavor_id=IntArgument('Resize (set another flavor)', '--flavor-id'),
575
        firewall_profile=FirewallProfileArgument(
576
            'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
577
            '--firewall'),
578
        metadata_to_set=KeyValueArgument(
579
            'Set metadata in key=value form (can be repeated)',
580
            '--metadata-set'),
581
        metadata_to_delete=RepeatableArgument(
582
            'Delete metadata by key (can be repeated)', '--metadata-del'),
583
        public_network_port_id=ValueArgument(
584
            'Connection to set new firewall (* for all)', '--port-id'),
585
    )
586
    required = [
587
        'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
588
        'metadata_to_delete']
589

    
590
    @errors.generic.all
591
    @errors.cyclades.connection
592
    @errors.cyclades.server_id
593
    def _run(self, server_id):
594
        if self['server_name'] is not None:
595
            self.client.update_server_name((server_id), self['server_name'])
596
        if self['flavor_id']:
597
            self.client.resize_server(server_id, self['flavor_id'])
598
        if self['firewall_profile']:
599
            vm = self._restruct_server_info(
600
                self.client.get_server_details(server_id))
601
            ports = [p for p in vm['ports'] if 'firewallProfile' in p]
602
            pick_port = self.arguments['public_network_port_id']
603
            if pick_port.value:
604
                ports = [p for p in ports if pick_port.value in (
605
                    '*', '%s' % p['id'])]
606
            elif len(ports) > 1:
607
                raiseCLIError(
608
                    'Multiple public connections on server %s' % (
609
                        server_id), details=[
610
                            'To select one:',
611
                            '  %s <port id>' % pick_port.lvalue,
612
                            'To set all:',
613
                            '  %s *' % pick_port.lvalue,
614
                            'Ports to public networks on server %s:' % (
615
                                server_id),
616
                            ','.join([' %s' % p['id'] for p in ports])])
617
            if not ports:
618
                pp = pick_port.value
619
                raiseCLIError(
620
                    'No *public* networks attached on server %s%s' % (
621
                        server_id, ' through port %s' % pp if pp else ''),
622
                    details=[
623
                        'To see all networks:',
624
                        '  kamaki network list',
625
                        'To connect to a network:',
626
                        '  kamaki network connect <net id> --device-id %s' % (
627
                            server_id)])
628
            for port in ports:
629
                self.error('Set port %s firewall to %s' % (
630
                    port['id'], self['firewall_profile']))
631
                self.client.set_firewall_profile(
632
                    server_id=server_id,
633
                    profile=self['firewall_profile'],
634
                    port_id=port['id'])
635
        if self['metadata_to_set']:
636
            self.client.update_server_metadata(
637
                server_id, **self['metadata_to_set'])
638
        for key in (self['metadata_to_delete'] or []):
639
            errors.cyclades.metadata(
640
                self.client.delete_server_metadata)(server_id, key=key)
641
        if self['with_output']:
642
            self._optional_output(self.client.get_server_details(server_id))
643

    
644
    def main(self, server_id):
645
        super(self.__class__, self)._run()
646
        pnpid = self.arguments['public_network_port_id']
647
        fp = self.arguments['firewall_profile']
648
        if pnpid.value and not fp.value:
649
            raise CLIInvalidArgument('Invalid argument compination', details=[
650
                'Argument %s should always be combined with %s' % (
651
                    pnpid.lvalue, fp.lvalue)])
652
        self._run(server_id=server_id)
653

    
654

    
655
@command(server_cmds)
656
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
657
    """Delete a virtual server"""
658

    
659
    arguments = dict(
660
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
661
        cluster=FlagArgument(
662
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
663
            'prefix. In that case, the prefix replaces the server id',
664
            '--cluster')
665
    )
666

    
667
    def _server_ids(self, server_var):
668
        if self['cluster']:
669
            return [s['id'] for s in self.client.list_servers() if (
670
                s['name'].startswith(server_var))]
671

    
672
        @errors.cyclades.server_id
673
        def _check_server_id(self, server_id):
674
            return server_id
675

    
676
        return [_check_server_id(self, server_id=server_var), ]
677

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

    
686
            r = self.client.delete_server(server_id)
687
            self._optional_output(r)
688

    
689
            if self['wait']:
690
                self._wait(server_id, status)
691

    
692
    def main(self, server_id_or_cluster_prefix):
693
        super(self.__class__, self)._run()
694
        self._run(server_id_or_cluster_prefix)
695

    
696

    
697
@command(server_cmds)
698
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
699
    """Reboot a virtual server"""
700

    
701
    arguments = dict(
702
        hard=FlagArgument(
703
            'perform a hard reboot (deprecated)', ('-f', '--force')),
704
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
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
        hard_reboot = self['hard']
713
        if hard_reboot:
714
            self.error(
715
                'WARNING: -f/--force will be deprecated in version 0.12\n'
716
                '\tIn the future, please use --type=hard instead')
717
        if self['type']:
718
            if self['type'].lower() in ('soft', ):
719
                hard_reboot = False
720
            elif self['type'].lower() in ('hard', ):
721
                hard_reboot = True
722
            else:
723
                raise CLISyntaxError(
724
                    'Invalid reboot type %s' % self['type'],
725
                    importance=2, details=[
726
                        '--type values are either SOFT (default) or HARD'])
727

    
728
        r = self.client.reboot_server(int(server_id), hard_reboot)
729
        self._optional_output(r)
730

    
731
        if self['wait']:
732
            self._wait(server_id, 'REBOOT')
733

    
734
    def main(self, server_id):
735
        super(self.__class__, self)._run()
736
        self._run(server_id=server_id)
737

    
738

    
739
@command(server_cmds)
740
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
741
    """Start an existing virtual server"""
742

    
743
    arguments = dict(
744
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
745
    )
746

    
747
    @errors.generic.all
748
    @errors.cyclades.connection
749
    @errors.cyclades.server_id
750
    def _run(self, server_id):
751
        status = 'ACTIVE'
752
        if self['wait']:
753
            details = self.client.get_server_details(server_id)
754
            status = details['status']
755
            if status in ('ACTIVE', ):
756
                return
757

    
758
        r = self.client.start_server(int(server_id))
759
        self._optional_output(r)
760

    
761
        if self['wait']:
762
            self._wait(server_id, status)
763

    
764
    def main(self, server_id):
765
        super(self.__class__, self)._run()
766
        self._run(server_id=server_id)
767

    
768

    
769
@command(server_cmds)
770
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
771
    """Shutdown an active virtual server"""
772

    
773
    arguments = dict(
774
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
775
    )
776

    
777
    @errors.generic.all
778
    @errors.cyclades.connection
779
    @errors.cyclades.server_id
780
    def _run(self, server_id):
781
        status = 'STOPPED'
782
        if self['wait']:
783
            details = self.client.get_server_details(server_id)
784
            status = details['status']
785
            if status in ('STOPPED', ):
786
                return
787

    
788
        r = self.client.shutdown_server(int(server_id))
789
        self._optional_output(r)
790

    
791
        if self['wait']:
792
            self._wait(server_id, status)
793

    
794
    def main(self, server_id):
795
        super(self.__class__, self)._run()
796
        self._run(server_id=server_id)
797

    
798

    
799
@command(server_cmds)
800
class server_console(_init_cyclades, _optional_json):
801
    """Create a VMC console and show connection information"""
802

    
803
    @errors.generic.all
804
    @errors.cyclades.connection
805
    @errors.cyclades.server_id
806
    def _run(self, server_id):
807
        self.error('The following credentials will be invalidated shortly')
808
        self._print(
809
            self.client.get_server_console(server_id), self.print_dict)
810

    
811
    def main(self, server_id):
812
        super(self.__class__, self)._run()
813
        self._run(server_id=server_id)
814

    
815

    
816
@command(server_cmds)
817
class server_wait(_init_cyclades, _server_wait):
818
    """Wait for server to change its status (default: BUILD)"""
819

    
820
    arguments = dict(
821
        timeout=IntArgument(
822
            'Wait limit in seconds (default: 60)', '--timeout', default=60),
823
        server_status=StatusArgument(
824
            'Status to wait for (%s, default: %s)' % (
825
                ', '.join(server_states), server_states[0]),
826
            '--status',
827
            valid_states=server_states)
828
    )
829

    
830
    @errors.generic.all
831
    @errors.cyclades.connection
832
    @errors.cyclades.server_id
833
    def _run(self, server_id, current_status):
834
        r = self.client.get_server_details(server_id)
835
        if r['status'].lower() == current_status.lower():
836
            self._wait(server_id, current_status, timeout=self['timeout'])
837
        else:
838
            self.error(
839
                'Server %s: Cannot wait for status %s, '
840
                'status is already %s' % (
841
                    server_id, current_status, r['status']))
842

    
843
    def main(self, server_id):
844
        super(self.__class__, self)._run()
845
        self._run(
846
            server_id=server_id,
847
            current_status=self['server_status'] or 'BUILD')
848

    
849

    
850
@command(flavor_cmds)
851
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
852
    """List available hardware flavors"""
853

    
854
    PERMANENTS = ('id', 'name')
855

    
856
    arguments = dict(
857
        detail=FlagArgument('show detailed output', ('-l', '--details')),
858
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
859
        more=FlagArgument(
860
            'output results in pages (-n to set items per page, default 10)',
861
            '--more'),
862
        enum=FlagArgument('Enumerate results', '--enumerate'),
863
        ram=ValueArgument('filter by ram', ('--ram')),
864
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
865
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
866
        disk_template=ValueArgument(
867
            'filter by disk_templace', ('--disk-template'))
868
    )
869

    
870
    def _apply_common_filters(self, flavors):
871
        common_filters = dict()
872
        if self['ram']:
873
            common_filters['ram'] = self['ram']
874
        if self['vcpus']:
875
            common_filters['vcpus'] = self['vcpus']
876
        if self['disk']:
877
            common_filters['disk'] = self['disk']
878
        if self['disk_template']:
879
            common_filters['SNF:disk_template'] = self['disk_template']
880
        return filter_dicts_by_dict(flavors, common_filters)
881

    
882
    @errors.generic.all
883
    @errors.cyclades.connection
884
    def _run(self):
885
        withcommons = self['ram'] or self['vcpus'] or (
886
            self['disk'] or self['disk_template'])
887
        detail = self['detail'] or withcommons
888
        flavors = self.client.list_flavors(detail)
889
        flavors = self._filter_by_name(flavors)
890
        flavors = self._filter_by_id(flavors)
891
        if withcommons:
892
            flavors = self._apply_common_filters(flavors)
893
        if not (self['detail'] or (
894
                self['json_output'] or self['output_format'])):
895
            remove_from_items(flavors, 'links')
896
        if detail and not self['detail']:
897
            for flv in flavors:
898
                for key in set(flv).difference(self.PERMANENTS):
899
                    flv.pop(key)
900
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
901
        self._print(
902
            flavors,
903
            with_redundancy=self['detail'], with_enumeration=self['enum'],
904
            **kwargs)
905
        if self['more']:
906
            pager(kwargs['out'].getvalue())
907

    
908
    def main(self):
909
        super(self.__class__, self)._run()
910
        self._run()
911

    
912

    
913
@command(flavor_cmds)
914
class flavor_info(_init_cyclades, _optional_json):
915
    """Detailed information on a hardware flavor
916
    To get a list of available flavors and flavor ids, try /flavor list
917
    """
918

    
919
    @errors.generic.all
920
    @errors.cyclades.connection
921
    @errors.cyclades.flavor_id
922
    def _run(self, flavor_id):
923
        self._print(
924
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
925

    
926
    def main(self, flavor_id):
927
        super(self.__class__, self)._run()
928
        self._run(flavor_id=flavor_id)
929

    
930

    
931
def _add_name(self, net):
932
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
933
        if user_id:
934
            uuids.append(user_id)
935
        if tenant_id:
936
            uuids.append(tenant_id)
937
        if uuids:
938
            usernames = self._uuids2usernames(uuids)
939
            if user_id:
940
                net['user_id'] += ' (%s)' % usernames[user_id]
941
            if tenant_id:
942
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]