Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 4c33b869

History | View | Annotate | Download (35.2 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
        stats=FlagArgument('Get URLs for server statistics', '--stats'),
305
        diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
306
    )
307

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

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

    
334

    
335
class PersonalityArgument(KeyValueArgument):
336

    
337
    terms = (
338
        ('local-path', 'contents'),
339
        ('server-path', 'path'),
340
        ('owner', 'owner'),
341
        ('group', 'group'),
342
        ('mode', 'mode'))
343

    
344
    @property
345
    def value(self):
346
        return getattr(self, '_value', [])
347

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

    
359
            for k, v in self.terms:
360
                prefix = '%s=' % k
361
                for item in termlist:
362
                    if item.lower().startswith(prefix):
363
                        input_dict[k] = item[len(k) + 1:]
364
                        break
365
                    item = None
366
                if item:
367
                    termlist.remove(item)
368

    
369
            try:
370
                path = input_dict['local-path']
371
            except KeyError:
372
                path = termlist.pop(0)
373
                if not path:
374
                    raise CLIInvalidArgument(
375
                        '--personality: No local path specified',
376
                        details=howto_personality)
377

    
378
            if not exists(path):
379
                raise CLIInvalidArgument(
380
                    '--personality: File %s does not exist' % path,
381
                    details=howto_personality)
382

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

    
402

    
403
class NetworkArgument(RepeatableArgument):
404
    """[id=]NETWORK_ID[,[ip=]IP]"""
405

    
406
    @property
407
    def value(self):
408
        return getattr(self, '_value', self.default)
409

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

    
444

    
445
@command(server_cmds)
446
class server_create(_init_cyclades, _optional_json, _server_wait):
447
    """Create a server (aka Virtual Machine)"""
448

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

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

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

    
528
    def main(self):
529
        super(self.__class__, self)._run()
530
        if self['no_network'] and self['network_configuration']:
531
            raise CLIInvalidArgument(
532
                'Invalid argument compination', importance=2, details=[
533
                'Arguments %s and %s are mutually exclusive' % (
534
                    self.arguments['no_network'].lvalue,
535
                    self.arguments['network_configuration'].lvalue)])
536
        self._run(
537
            name=self['server_name'],
538
            flavor_id=self['flavor_id'],
539
            image_id=self['image_id'])
540

    
541

    
542
class FirewallProfileArgument(ValueArgument):
543

    
544
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
545

    
546
    @property
547
    def value(self):
548
        return getattr(self, '_value', None)
549

    
550
    @value.setter
551
    def value(self, new_profile):
552
        if new_profile:
553
            new_profile = new_profile.upper()
554
            if new_profile in self.profiles:
555
                self._value = new_profile
556
            else:
557
                raise CLIInvalidArgument(
558
                    'Invalid firewall profile %s' % new_profile,
559
                    details=['Valid values: %s' % ', '.join(self.profiles)])
560

    
561

    
562
@command(server_cmds)
563
class server_modify(_init_cyclades, _optional_output_cmd):
564
    """Modify attributes of a virtual server"""
565

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

    
584
    def _set_firewall_profile(self, server_id):
585
        vm = self._restruct_server_info(
586
            self.client.get_server_details(server_id))
587
        ports = [p for p in vm['ports'] if 'firewallProfile' in p]
588
        pick_port = self.arguments['public_network_port_id']
589
        if pick_port.value:
590
            ports = [p for p in ports if pick_port.value in (
591
                '*', '%s' % p['id'])]
592
        elif len(ports) > 1:
593
            port_strings = ['Server %s ports to public networks:' % server_id]
594
            for p in ports:
595
                port_strings.append('  %s' % p['id'])
596
                for k in ('network_id', 'ipv4', 'ipv6', 'firewallProfile'):
597
                    v = p.get(k)
598
                    if v:
599
                        port_strings.append('\t%s: %s' % (k, v))
600
            raiseCLIError(
601
                'Multiple public connections on server %s' % (
602
                    server_id), details=port_strings + [
603
                        'To select one:',
604
                        '  %s <port id>' % pick_port.lvalue,
605
                        'To set all:',
606
                        '  %s *' % pick_port.lvalue, ])
607
        if not ports:
608
            pp = pick_port.value
609
            raiseCLIError(
610
                'No *public* networks attached on server %s%s' % (
611
                    server_id, ' through port %s' % pp if pp else ''),
612
                details=[
613
                    'To see all networks:',
614
                    '  kamaki network list',
615
                    'To connect to a network:',
616
                    '  kamaki network connect <net id> --device-id %s' % (
617
                        server_id)])
618
        for port in ports:
619
            self.error('Set port %s firewall to %s' % (
620
                port['id'], self['firewall_profile']))
621
            self.client.set_firewall_profile(
622
                server_id=server_id,
623
                profile=self['firewall_profile'],
624
                port_id=port['id'])
625

    
626
    @errors.generic.all
627
    @errors.cyclades.connection
628
    @errors.cyclades.server_id
629
    def _run(self, server_id):
630
        if self['server_name'] is not None:
631
            self.client.update_server_name((server_id), self['server_name'])
632
        if self['flavor_id']:
633
            self.client.resize_server(server_id, self['flavor_id'])
634
        if self['firewall_profile']:
635
            self._set_firewall_profile(server_id)
636
        if self['metadata_to_set']:
637
            self.client.update_server_metadata(
638
                server_id, **self['metadata_to_set'])
639
        for key in (self['metadata_to_delete'] or []):
640
            errors.cyclades.metadata(
641
                self.client.delete_server_metadata)(server_id, key=key)
642
        if self['with_output']:
643
            self._optional_output(self.client.get_server_details(server_id))
644

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

    
655

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

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

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

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

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

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

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

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

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

    
697

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

    
702
    arguments = dict(
703
        hard=FlagArgument(
704
            'perform a hard reboot (deprecated)', ('-f', '--force')),
705
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
706
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
707
    )
708

    
709
    @errors.generic.all
710
    @errors.cyclades.connection
711
    @errors.cyclades.server_id
712
    def _run(self, server_id):
713
        hard_reboot = self['hard']
714
        if hard_reboot:
715
            self.error(
716
                'WARNING: -f/--force will be deprecated in version 0.12\n'
717
                '\tIn the future, please use --type=hard instead')
718
        if self['type']:
719
            if self['type'].lower() in ('soft', ):
720
                hard_reboot = False
721
            elif self['type'].lower() in ('hard', ):
722
                hard_reboot = True
723
            else:
724
                raise CLISyntaxError(
725
                    'Invalid reboot type %s' % self['type'],
726
                    importance=2, details=[
727
                        '--type values are either SOFT (default) or HARD'])
728

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

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

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

    
739

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

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

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

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

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

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

    
769

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

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

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

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

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

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

    
799

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

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

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

    
816

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

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

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

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

    
850

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

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

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

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

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

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

    
913

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

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

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

    
931

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