Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 25a318f0

History | View | Annotate | Download (35.8 kB)

1
# Copyright 2011-2014 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
        for srv in servers:
209
            srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
210
        return servers
211

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

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

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

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

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

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

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

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

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

    
294

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

    
299
    arguments = dict(
300
        nics=FlagArgument(
301
            'Show only the network interfaces of this virtual server',
302
            '--nics'),
303
        stats=FlagArgument('Get URLs for server statistics', '--stats'),
304
        diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
305
    )
306

    
307
    @errors.generic.all
308
    @errors.cyclades.connection
309
    @errors.cyclades.server_id
310
    def _run(self, server_id):
311
        if self['nics']:
312
            self._print(
313
                self.client.get_server_nics(server_id), self.print_dict)
314
        elif self['stats']:
315
            self._print(
316
                self.client.get_server_stats(server_id), self.print_dict)
317
        elif self['diagnostics']:
318
            self._print(self.client.get_server_diagnostics(server_id))
319
        else:
320
            vm = self.client.get_server_details(server_id)
321
            # self._print(self._restruct_server_info(vm), self.print_dict)
322
            self._print(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
        project_id=ValueArgument('Assign server to project', '--project-id'),
477
    )
478
    required = ('server_name', 'flavor_id', 'image_id')
479

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

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

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

    
545

    
546
class FirewallProfileArgument(ValueArgument):
547

    
548
    profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
549

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

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

    
565

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

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

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

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

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

    
659

    
660
@command(server_cmds)
661
class server_reassign(_init_cyclades, _optional_json):
662
    """Assign a virtual server to a different project"""
663

    
664
    arguments = dict(
665
        project_id=ValueArgument('The project to assign', '--project-id')
666
    )
667
    required = ('project_id', )
668

    
669
    @errors.generic.all
670
    @errors.cyclades.connection
671
    @errors.cyclades.server_id
672
    def _run(self, server_id, project):
673
        self.client.reassign_server(server_id, project)
674

    
675
    def main(self, server_id):
676
        super(self.__class__, self)._run()
677
        self._run(server_id=server_id, project=self['project_id'])
678

    
679

    
680
@command(server_cmds)
681
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
682
    """Delete a virtual server"""
683

    
684
    arguments = dict(
685
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
686
        cluster=FlagArgument(
687
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
688
            'prefix. In that case, the prefix replaces the server id',
689
            '--cluster')
690
    )
691

    
692
    def _server_ids(self, server_var):
693
        if self['cluster']:
694
            return [s['id'] for s in self.client.list_servers() if (
695
                s['name'].startswith(server_var))]
696

    
697
        @errors.cyclades.server_id
698
        def _check_server_id(self, server_id):
699
            return server_id
700

    
701
        return [_check_server_id(self, server_id=server_var), ]
702

    
703
    @errors.generic.all
704
    @errors.cyclades.connection
705
    def _run(self, server_var):
706
        for server_id in self._server_ids(server_var):
707
            if self['wait']:
708
                details = self.client.get_server_details(server_id)
709
                status = details['status']
710

    
711
            r = self.client.delete_server(server_id)
712
            self._optional_output(r)
713

    
714
            if self['wait']:
715
                self._wait(server_id, status)
716

    
717
    def main(self, server_id_or_cluster_prefix):
718
        super(self.__class__, self)._run()
719
        self._run(server_id_or_cluster_prefix)
720

    
721

    
722
@command(server_cmds)
723
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
724
    """Reboot a virtual server"""
725

    
726
    arguments = dict(
727
        hard=FlagArgument(
728
            'perform a hard reboot (deprecated)', ('-f', '--force')),
729
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
730
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
731
    )
732

    
733
    @errors.generic.all
734
    @errors.cyclades.connection
735
    @errors.cyclades.server_id
736
    def _run(self, server_id):
737
        hard_reboot = self['hard']
738
        if hard_reboot:
739
            self.error(
740
                'WARNING: -f/--force will be deprecated in version 0.12\n'
741
                '\tIn the future, please use --type=hard instead')
742
        if self['type']:
743
            if self['type'].lower() in ('soft', ):
744
                hard_reboot = False
745
            elif self['type'].lower() in ('hard', ):
746
                hard_reboot = True
747
            else:
748
                raise CLISyntaxError(
749
                    'Invalid reboot type %s' % self['type'],
750
                    importance=2, details=[
751
                        '--type values are either SOFT (default) or HARD'])
752

    
753
        r = self.client.reboot_server(int(server_id), hard_reboot)
754
        self._optional_output(r)
755

    
756
        if self['wait']:
757
            self._wait(server_id, 'REBOOT')
758

    
759
    def main(self, server_id):
760
        super(self.__class__, self)._run()
761
        self._run(server_id=server_id)
762

    
763

    
764
@command(server_cmds)
765
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
766
    """Start an existing virtual server"""
767

    
768
    arguments = dict(
769
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
770
    )
771

    
772
    @errors.generic.all
773
    @errors.cyclades.connection
774
    @errors.cyclades.server_id
775
    def _run(self, server_id):
776
        status = 'ACTIVE'
777
        if self['wait']:
778
            details = self.client.get_server_details(server_id)
779
            status = details['status']
780
            if status in ('ACTIVE', ):
781
                return
782

    
783
        r = self.client.start_server(int(server_id))
784
        self._optional_output(r)
785

    
786
        if self['wait']:
787
            self._wait(server_id, status)
788

    
789
    def main(self, server_id):
790
        super(self.__class__, self)._run()
791
        self._run(server_id=server_id)
792

    
793

    
794
@command(server_cmds)
795
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
796
    """Shutdown an active virtual server"""
797

    
798
    arguments = dict(
799
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
800
    )
801

    
802
    @errors.generic.all
803
    @errors.cyclades.connection
804
    @errors.cyclades.server_id
805
    def _run(self, server_id):
806
        status = 'STOPPED'
807
        if self['wait']:
808
            details = self.client.get_server_details(server_id)
809
            status = details['status']
810
            if status in ('STOPPED', ):
811
                return
812

    
813
        r = self.client.shutdown_server(int(server_id))
814
        self._optional_output(r)
815

    
816
        if self['wait']:
817
            self._wait(server_id, status)
818

    
819
    def main(self, server_id):
820
        super(self.__class__, self)._run()
821
        self._run(server_id=server_id)
822

    
823

    
824
@command(server_cmds)
825
class server_console(_init_cyclades, _optional_json):
826
    """Create a VMC console and show connection information"""
827

    
828
    @errors.generic.all
829
    @errors.cyclades.connection
830
    @errors.cyclades.server_id
831
    def _run(self, server_id):
832
        self.error('The following credentials will be invalidated shortly')
833
        self._print(
834
            self.client.get_server_console(server_id), self.print_dict)
835

    
836
    def main(self, server_id):
837
        super(self.__class__, self)._run()
838
        self._run(server_id=server_id)
839

    
840

    
841
@command(server_cmds)
842
class server_wait(_init_cyclades, _server_wait):
843
    """Wait for server to change its status (default: BUILD)"""
844

    
845
    arguments = dict(
846
        timeout=IntArgument(
847
            'Wait limit in seconds (default: 60)', '--timeout', default=60),
848
        server_status=StatusArgument(
849
            'Status to wait for (%s, default: %s)' % (
850
                ', '.join(server_states), server_states[0]),
851
            '--status',
852
            valid_states=server_states)
853
    )
854

    
855
    @errors.generic.all
856
    @errors.cyclades.connection
857
    @errors.cyclades.server_id
858
    def _run(self, server_id, current_status):
859
        r = self.client.get_server_details(server_id)
860
        if r['status'].lower() == current_status.lower():
861
            self._wait(server_id, current_status, timeout=self['timeout'])
862
        else:
863
            self.error(
864
                'Server %s: Cannot wait for status %s, '
865
                'status is already %s' % (
866
                    server_id, current_status, r['status']))
867

    
868
    def main(self, server_id):
869
        super(self.__class__, self)._run()
870
        self._run(
871
            server_id=server_id,
872
            current_status=self['server_status'] or 'BUILD')
873

    
874

    
875
@command(flavor_cmds)
876
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
877
    """List available hardware flavors"""
878

    
879
    PERMANENTS = ('id', 'name')
880

    
881
    arguments = dict(
882
        detail=FlagArgument('show detailed output', ('-l', '--details')),
883
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
884
        more=FlagArgument(
885
            'output results in pages (-n to set items per page, default 10)',
886
            '--more'),
887
        enum=FlagArgument('Enumerate results', '--enumerate'),
888
        ram=ValueArgument('filter by ram', ('--ram')),
889
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
890
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
891
        disk_template=ValueArgument(
892
            'filter by disk_templace', ('--disk-template'))
893
    )
894

    
895
    def _apply_common_filters(self, flavors):
896
        common_filters = dict()
897
        if self['ram']:
898
            common_filters['ram'] = self['ram']
899
        if self['vcpus']:
900
            common_filters['vcpus'] = self['vcpus']
901
        if self['disk']:
902
            common_filters['disk'] = self['disk']
903
        if self['disk_template']:
904
            common_filters['SNF:disk_template'] = self['disk_template']
905
        return filter_dicts_by_dict(flavors, common_filters)
906

    
907
    @errors.generic.all
908
    @errors.cyclades.connection
909
    def _run(self):
910
        withcommons = self['ram'] or self['vcpus'] or (
911
            self['disk'] or self['disk_template'])
912
        detail = self['detail'] or withcommons
913
        flavors = self.client.list_flavors(detail)
914
        flavors = self._filter_by_name(flavors)
915
        flavors = self._filter_by_id(flavors)
916
        if withcommons:
917
            flavors = self._apply_common_filters(flavors)
918
        if not (self['detail'] or (
919
                self['json_output'] or self['output_format'])):
920
            remove_from_items(flavors, 'links')
921
        if detail and not self['detail']:
922
            for flv in flavors:
923
                for key in set(flv).difference(self.PERMANENTS):
924
                    flv.pop(key)
925
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
926
        self._print(
927
            flavors,
928
            with_redundancy=self['detail'], with_enumeration=self['enum'],
929
            **kwargs)
930
        if self['more']:
931
            pager(kwargs['out'].getvalue())
932

    
933
    def main(self):
934
        super(self.__class__, self)._run()
935
        self._run()
936

    
937

    
938
@command(flavor_cmds)
939
class flavor_info(_init_cyclades, _optional_json):
940
    """Detailed information on a hardware flavor
941
    To get a list of available flavors and flavor ids, try /flavor list
942
    """
943

    
944
    @errors.generic.all
945
    @errors.cyclades.connection
946
    @errors.cyclades.flavor_id
947
    def _run(self, flavor_id):
948
        self._print(
949
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
950

    
951
    def main(self, flavor_id):
952
        super(self.__class__, self)._run()
953
        self._run(flavor_id=flavor_id)
954

    
955

    
956
def _add_name(self, net):
957
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
958
        if user_id:
959
            uuids.append(user_id)
960
        if uuids or tenant_id:
961
            usernames = self._uuids2usernames(uuids)
962
            if user_id:
963
                net['user_id'] += ' (%s)' % usernames[user_id]