Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / cyclades.py @ 75ae8a08

History | View | Annotate | Download (35.9 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
                [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
            pass
273
        else:
274
            for srv in servers:
275
                for key in set(srv).difference(self.PERMANENTS):
276
                    srv.pop(key)
277

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

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

    
296

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

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

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

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

    
336

    
337
class PersonalityArgument(KeyValueArgument):
338

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

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

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

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

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

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

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

    
404

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

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

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

    
446

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

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

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

    
517
    @errors.generic.all
518
    @errors.cyclades.connection
519
    @errors.plankton.id
520
    @errors.cyclades.flavor_id
521
    def _run(self, name, flavor_id, image_id):
522
        for r in self._create_cluster(
523
                name, flavor_id, image_id, size=self['cluster_size'] or 1,
524
                project=self['project']):
525
            if not r:
526
                self.error('Create %s: server response was %s' % (name, r))
527
                continue
528
            #  self._print(self._restruct_server_info(r), self.print_dict)
529
            self._print(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
    def _set_firewall_profile(self, server_id):
591
        vm = self._restruct_server_info(
592
            self.client.get_server_details(server_id))
593
        ports = [p for p in vm['ports'] if 'firewallProfile' in p]
594
        pick_port = self.arguments['public_network_port_id']
595
        if pick_port.value:
596
            ports = [p for p in ports if pick_port.value in (
597
                '*', '%s' % p['id'])]
598
        elif len(ports) > 1:
599
            port_strings = ['Server %s ports to public networks:' % server_id]
600
            for p in ports:
601
                port_strings.append('  %s' % p['id'])
602
                for k in ('network_id', 'ipv4', 'ipv6', 'firewallProfile'):
603
                    v = p.get(k)
604
                    if v:
605
                        port_strings.append('\t%s: %s' % (k, v))
606
            raiseCLIError(
607
                'Multiple public connections on server %s' % (
608
                    server_id), details=port_strings + [
609
                        'To select one:',
610
                        '  %s <port id>' % pick_port.lvalue,
611
                        'To set all:',
612
                        '  %s *' % pick_port.lvalue, ])
613
        if not ports:
614
            pp = pick_port.value
615
            raiseCLIError(
616
                'No *public* networks attached on server %s%s' % (
617
                    server_id, ' through port %s' % pp if pp else ''),
618
                details=[
619
                    'To see all networks:',
620
                    '  kamaki network list',
621
                    'To connect to a network:',
622
                    '  kamaki network connect <net id> --device-id %s' % (
623
                        server_id)])
624
        for port in ports:
625
            self.error('Set port %s firewall to %s' % (
626
                port['id'], self['firewall_profile']))
627
            self.client.set_firewall_profile(
628
                server_id=server_id,
629
                profile=self['firewall_profile'],
630
                port_id=port['id'])
631

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

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

    
661

    
662
@command(server_cmds)
663
class server_reassign(_init_cyclades, _optional_json):
664
    """Assign a VM to a different project
665
    """
666

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

    
673
    def main(self, server_id, project):
674
        super(self.__class__, self)._run()
675
        self._run(server_id=server_id, project=project)
676

    
677

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

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

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

    
695
        @errors.cyclades.server_id
696
        def _check_server_id(self, server_id):
697
            return server_id
698

    
699
        return [_check_server_id(self, server_id=server_var), ]
700

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

    
709
            r = self.client.delete_server(server_id)
710
            self._optional_output(r)
711

    
712
            if self['wait']:
713
                self._wait(server_id, status)
714

    
715
    def main(self, server_id_or_cluster_prefix):
716
        super(self.__class__, self)._run()
717
        self._run(server_id_or_cluster_prefix)
718

    
719

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

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

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

    
751
        r = self.client.reboot_server(int(server_id), hard_reboot)
752
        self._optional_output(r)
753

    
754
        if self['wait']:
755
            self._wait(server_id, 'REBOOT')
756

    
757
    def main(self, server_id):
758
        super(self.__class__, self)._run()
759
        self._run(server_id=server_id)
760

    
761

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

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

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

    
781
        r = self.client.start_server(int(server_id))
782
        self._optional_output(r)
783

    
784
        if self['wait']:
785
            self._wait(server_id, status)
786

    
787
    def main(self, server_id):
788
        super(self.__class__, self)._run()
789
        self._run(server_id=server_id)
790

    
791

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

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

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

    
811
        r = self.client.shutdown_server(int(server_id))
812
        self._optional_output(r)
813

    
814
        if self['wait']:
815
            self._wait(server_id, status)
816

    
817
    def main(self, server_id):
818
        super(self.__class__, self)._run()
819
        self._run(server_id=server_id)
820

    
821

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

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

    
834
    def main(self, server_id):
835
        super(self.__class__, self)._run()
836
        self._run(server_id=server_id)
837

    
838

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

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

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

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

    
872

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

    
877
    PERMANENTS = ('id', 'name')
878

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

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

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

    
931
    def main(self):
932
        super(self.__class__, self)._run()
933
        self._run()
934

    
935

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

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

    
949
    def main(self, flavor_id):
950
        super(self.__class__, self)._run()
951
        self._run(flavor_id=flavor_id)
952

    
953

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