cyclades.py 45.6 KB
Newer Older
1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

34
35
36
37
38
from base64 import b64encode
from os.path import exists
from io import StringIO
from pydoc import pager

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
51

52
53
54
server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
55
56
ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
_commands = [server_cmds, flavor_cmds, network_cmds, ip_cmds]
Stavros Sachtouris's avatar
Stavros Sachtouris committed
57

58

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

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

73

74
class _service_wait(object):
75
76
77

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

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

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


104
class _server_wait(_service_wait):
105

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


113
114
class _network_wait(_service_wait):

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


121
122
123
124
125
126
127
128
129
class _firewall_wait(_service_wait):

    def _wait(self, server_id, current_status, timeout=60):
        super(_firewall_wait, self)._wait(
            'Firewall of server',
            server_id, self.client.wait_firewall, current_status,
            timeout=timeout)


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

154
155
    def main(self):
        self._run()
Stavros Sachtouris's avatar
Stavros Sachtouris committed
156

Stavros Sachtouris's avatar
Stavros Sachtouris committed
157

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

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

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

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

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

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

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

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

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

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

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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
279

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

290
291
    @errors.generic.all
    @errors.cyclades.connection
Stavros Sachtouris's avatar
Stavros Sachtouris committed
292
    @errors.cyclades.server_id
293
    def _run(self, server_id):
294
295
296
297
        vm = self.client.get_server_details(server_id)
        uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
        vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
        vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
298
        self._print(vm, self.print_dict)
299

300
301
302
303
    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
304

305
class PersonalityArgument(KeyValueArgument):
306
307
308
309
310
311
312
313

    terms = (
        ('local-path', 'contents'),
        ('server-path', 'path'),
        ('owner', 'owner'),
        ('group', 'group'),
        ('mode', 'mode'))

Stavros Sachtouris's avatar
Stavros Sachtouris committed
314
    @property
315
    def value(self):
316
        return self._value if hasattr(self, '_value') else []
Stavros Sachtouris's avatar
Stavros Sachtouris committed
317
318

    @value.setter
319
320
321
    def value(self, newvalue):
        if newvalue == self.default:
            return self.value
322
        self._value, input_dict = [], {}
323
324
        for i, terms in enumerate(newvalue):
            termlist = terms.split(',')
325
326
            if len(termlist) > len(self.terms):
                msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
327
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347

            for k, v in self.terms:
                prefix = '%s=' % k
                for item in termlist:
                    if item.lower().startswith(prefix):
                        input_dict[k] = item[len(k) + 1:]
                        break
                    item = None
                if item:
                    termlist.remove(item)

            try:
                path = input_dict['local-path']
            except KeyError:
                path = termlist.pop(0)
                if not path:
                    raise CLIInvalidArgument(
                        '--personality: No local path specified',
                        details=howto_personality)

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

353
354
355
            self._value.append(dict(path=path))
            with open(path) as f:
                self._value[i]['contents'] = b64encode(f.read())
356
357
358
359
360
361
362
363
            for k, v in self.terms[1:]:
                try:
                    self._value[i][v] = input_dict[k]
                except KeyError:
                    try:
                        self._value[i][v] = termlist.pop(0)
                    except IndexError:
                        continue
364

Stavros Sachtouris's avatar
Stavros Sachtouris committed
365

366
@command(server_cmds)
367
class server_create(_init_cyclades, _optional_json, _server_wait):
368
369
    """Create a server (aka Virtual Machine)
    Parameters:
370
371
372
    - name: (single quoted text)
    - flavor id: Hardware flavor. Pick one from: /flavor list
    - image id: OS images. Pick one from: /image list
373
    """
374

375
376
    arguments = dict(
        personality=PersonalityArgument(
377
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
378
379
380
381
382
383
        wait=FlagArgument('Wait server to build', ('-w', '--wait')),
        cluster_size=IntArgument(
            'Create a cluster of servers of this size. In this case, the name'
            'parameter is the prefix of each server in the cluster (e.g.,'
            'srv1, srv2, etc.',
            '--cluster-size')
384
    )
385

386
387
388
389
390
391
392
393
394
    @errors.cyclades.cluster_size
    def _create_cluster(self, prefix, flavor_id, image_id, size):
        servers = [dict(
            name='%s%s' % (prefix, i),
            flavor_id=flavor_id,
            image_id=image_id,
            personality=self['personality']) for i in range(size)]
        if size == 1:
            return [self.client.create_server(**servers[0])]
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
        try:
            return self.client.async_run(self.client.create_server, servers)
        except Exception as e:
            if size == 1:
                raise e
            try:
                requested_names = [s['name'] for s in servers]
                spawned_servers = [dict(
                    name=s['name'],
                    id=s['id']) for s in self.client.list_servers() if (
                        s['name'] in requested_names)]
                self.error('Failed to build %s servers' % size)
                self.error('Found %s servers with a "%s" prefix:' % (
                    len(spawned_servers), prefix))
                self._print(spawned_servers, out=self._err)
                self.error('Check if any of these servers should be removed\n')
            except Exception as ne:
                self.error('Error (%s) while notifying about errors' % ne)
            finally:
                raise e
415

416
417
418
419
420
    @errors.generic.all
    @errors.cyclades.connection
    @errors.plankton.id
    @errors.cyclades.flavor_id
    def _run(self, name, flavor_id, image_id):
421
422
        for r in self._create_cluster(
                name, flavor_id, image_id, size=self['cluster_size'] or 1):
423
424
425
426
427
            if not r:
                self.error('Create %s: server response was %s' % (name, r))
                continue
            usernames = self._uuids2usernames(
                [r['user_id'], r['tenant_id']])
428
429
430
431
432
            r['user_id'] += ' (%s)' % usernames[r['user_id']]
            r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
            self._print(r, self.print_dict)
            if self['wait']:
                self._wait(r['id'], r['status'])
433
            self.writeln('')
434

435
    def main(self, name, flavor_id, image_id):
436
437
        super(self.__class__, self)._run()
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
438

Stavros Sachtouris's avatar
Stavros Sachtouris committed
439

440
@command(server_cmds)
441
class server_rename(_init_cyclades, _optional_output_cmd):
442
443
444
    """Set/update a virtual server name
    virtual server names are not unique, therefore multiple servers may share
    the same name
445
    """
446

Stavros Sachtouris's avatar
Stavros Sachtouris committed
447
448
449
450
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id, new_name):
451
452
        self._optional_output(
            self.client.update_server_name(int(server_id), new_name))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
453

454
    def main(self, server_id, new_name):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
455
456
        super(self.__class__, self)._run()
        self._run(server_id=server_id, new_name=new_name)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
457

458

459
@command(server_cmds)
460
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
461
    """Delete a virtual server"""
462

463
    arguments = dict(
464
465
466
467
468
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
        cluster=FlagArgument(
            '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
            'prefix. In that case, the prefix replaces the server id',
            '--cluster')
469
470
    )

471
472
473
474
475
476
477
478
479
480
481
    def _server_ids(self, server_var):
        if self['cluster']:
            return [s['id'] for s in self.client.list_servers() if (
                s['name'].startswith(server_var))]

        @errors.cyclades.server_id
        def _check_server_id(self, server_id):
            return server_id

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
482
483
    @errors.generic.all
    @errors.cyclades.connection
484
485
    def _run(self, server_var):
        for server_id in self._server_ids(server_var):
486
487
488
489
            if self['wait']:
                details = self.client.get_server_details(server_id)
                status = details['status']

490
            r = self.client.delete_server(server_id)
491
492
493
494
            self._optional_output(r)

            if self['wait']:
                self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
495

496
    def main(self, server_id_or_cluster_prefix):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
497
        super(self.__class__, self)._run()
498
        self._run(server_id_or_cluster_prefix)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
499

500

501
@command(server_cmds)
502
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
503
    """Reboot a virtual server"""
504

505
    arguments = dict(
506
507
508
        hard=FlagArgument(
            'perform a hard reboot (deprecated)', ('-f', '--force')),
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
509
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
510
    )
511

Stavros Sachtouris's avatar
Stavros Sachtouris committed
512
513
514
515
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
        hard_reboot = self['hard']
        if hard_reboot:
            self.error(
                'WARNING: -f/--force will be deprecated in version 0.12\n'
                '\tIn the future, please use --type=hard instead')
        if self['type']:
            if self['type'].lower() in ('soft', ):
                hard_reboot = False
            elif self['type'].lower() in ('hard', ):
                hard_reboot = True
            else:
                raise CLISyntaxError(
                    'Invalid reboot type %s' % self['type'],
                    importance=2, details=[
                        '--type values are either SOFT (default) or HARD'])

        r = self.client.reboot_server(int(server_id), hard_reboot)
533
534
535
536
        self._optional_output(r)

        if self['wait']:
            self._wait(server_id, 'REBOOT')
Stavros Sachtouris's avatar
Stavros Sachtouris committed
537

538
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
539
540
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
541

542

543
@command(server_cmds)
544
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
545
    """Start an existing virtual server"""
546

547
548
549
550
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
551
552
553
554
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
555
556
557
558
559
560
561
562
563
564
565
566
        status = 'ACTIVE'
        if self['wait']:
            details = self.client.get_server_details(server_id)
            status = details['status']
            if status in ('ACTIVE', ):
                return

        r = self.client.start_server(int(server_id))
        self._optional_output(r)

        if self['wait']:
            self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
567

568
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
569
570
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
571

572

573
@command(server_cmds)
574
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
575
    """Shutdown an active virtual server"""
576

577
578
579
580
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
581
582
583
584
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
585
586
587
588
589
590
591
592
593
594
595
596
        status = 'STOPPED'
        if self['wait']:
            details = self.client.get_server_details(server_id)
            status = details['status']
            if status in ('STOPPED', ):
                return

        r = self.client.shutdown_server(int(server_id))
        self._optional_output(r)

        if self['wait']:
            self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
597

598
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
599
600
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
601

602

603
@command(server_cmds)
604
class server_console(_init_cyclades, _optional_json):
605
    """Get a VNC console to access an existing virtual server
606
    Console connection information provided (at least):
607
    - host: (url or address) a VNC host
608
    - port: (int) the gateway to enter virtual server on host
609
    - password: for VNC authorization
610
    """
611

Stavros Sachtouris's avatar
Stavros Sachtouris committed
612
613
614
615
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
616
        self._print(
617
            self.client.get_server_console(int(server_id)), self.print_dict)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
618

619
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
620
621
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
622

Stavros Sachtouris's avatar
Stavros Sachtouris committed
623

624
625
626
627
@command(server_cmds)
class server_resize(_init_cyclades, _optional_output_cmd):
    """Set a different flavor for an existing server
    To get server ids and flavor ids:
628
629
    /server list
    /flavor list
630
631
632
633
634
635
636
637
638
639
640
641
642
643
    """

    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.flavor_id
    def _run(self, server_id, flavor_id):
        self._optional_output(self.client.resize_server(server_id, flavor_id))

    def main(self, server_id, flavor_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id, flavor_id=flavor_id)


644
@command(server_cmds)
645
class server_firewall(_init_cyclades):
646
    """Manage virtual server firewall profiles for public networks"""
647
648
649


@command(server_cmds)
650
651
class server_firewall_set(
        _init_cyclades, _optional_output_cmd, _firewall_wait):
652
    """Set the firewall profile on virtual server public network
653
    Values for profile:
654
655
656
    - DISABLED: Shutdown firewall
    - ENABLED: Firewall in normal mode
    - PROTECTED: Firewall in secure mode
657
    """
658

659
660
661
662
663
664
665
    arguments = dict(
        wait=FlagArgument('Wait server firewall to build', ('-w', '--wait')),
        timeout=IntArgument(
            'Set wait timeout in seconds (default: 60)', '--timeout',
            default=60)
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
666
667
668
669
670
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.firewall
    def _run(self, server_id, profile):
671
672
673
674
675
676
677
678
679
680
681
682
        if self['timeout'] and not self['wait']:
            raise CLIInvalidArgument('Invalid use of --timeout', details=[
                'Timeout is used only along with -w/--wait'])
        old_profile = self.client.get_firewall_profile(server_id)
        if old_profile.lower() == profile.lower():
            self.error('Firewall of server %s: allready in status %s' % (
                server_id, old_profile))
        else:
            self._optional_output(self.client.set_firewall_profile(
                server_id=int(server_id), profile=('%s' % profile).upper()))
            if self['wait']:
                self._wait(server_id, old_profile, timeout=self['timeout'])
Stavros Sachtouris's avatar
Stavros Sachtouris committed
683

684
    def main(self, server_id, profile):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
685
686
        super(self.__class__, self)._run()
        self._run(server_id=server_id, profile=profile)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
687

688

689
@command(server_cmds)
690
class server_firewall_get(_init_cyclades):
691
    """Get the firewall profile for a virtual servers' public network"""
692
693
694
695
696

    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
697
        self.writeln(self.client.get_firewall_profile(server_id))
698
699
700
701
702
703

    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)


704
@command(server_cmds)
705
class server_addr(_init_cyclades, _optional_json):
706
    """List the addresses of all network interfaces on a virtual server"""
707

708
    arguments = dict(
709
        enum=FlagArgument('Enumerate results', '--enumerate')
710
711
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
712
713
714
715
716
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
        reply = self.client.list_server_nics(int(server_id))
717
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
718

Stavros Sachtouris's avatar
Stavros Sachtouris committed
719
720
721
722
    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
723

724
@command(server_cmds)
725
726
727
728
729
class server_metadata(_init_cyclades):
    """Manage Server metadata (key:value pairs of server attributes)"""


@command(server_cmds)
730
class server_metadata_list(_init_cyclades, _optional_json):
731
    """Get server metadata"""
732

Stavros Sachtouris's avatar
Stavros Sachtouris committed
733
734
735
736
737
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.metadata
    def _run(self, server_id, key=''):
738
        self._print(
739
740
            self.client.get_server_metadata(int(server_id), key),
            self.print_dict)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
741

742
    def main(self, server_id, key=''):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
743
744
        super(self.__class__, self)._run()
        self._run(server_id=server_id, key=key)
745

Stavros Sachtouris's avatar
Stavros Sachtouris committed
746

747
@command(server_cmds)
748
class server_metadata_set(_init_cyclades, _optional_json):
749
    """Set / update virtual server metadata
750
    Metadata should be given in key/value pairs in key=value format
751
    For example: /server metadata set <server id> key1=value1 key2=value2
752
    Old, unreferenced metadata will remain intact
753
    """
754

Stavros Sachtouris's avatar
Stavros Sachtouris committed
755
756
757
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
758
    def _run(self, server_id, keyvals):
759
        assert keyvals, 'Please, add some metadata ( key=value)'
760
761
762
763
764
765
766
767
768
769
770
771
772
        metadata = dict()
        for keyval in keyvals:
            k, sep, v = keyval.partition('=')
            if sep and k:
                metadata[k] = v
            else:
                raiseCLIError(
                    'Invalid piece of metadata %s' % keyval,
                    importance=2, details=[
                        'Correct metadata format: key=val',
                        'For example:',
                        '/server metadata set <server id>'
                        'key1=value1 key2=value2'])
773
        self._print(
774
            self.client.update_server_metadata(int(server_id), **metadata),
775
            self.print_dict)
776
777

    def main(self, server_id, *key_equals_val):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
778
        super(self.__class__, self)._run()
779
        self._run(server_id=server_id, keyvals=key_equals_val)
780

Stavros Sachtouris's avatar
Stavros Sachtouris committed
781

782
@command(server_cmds)
783
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
784
    """Delete virtual server metadata"""
785

Stavros Sachtouris's avatar
Stavros Sachtouris committed
786
787
788
789
790
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.metadata
    def _run(self, server_id, key):
791
792
        self._optional_output(
            self.client.delete_server_metadata(int(server_id), key))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
793

794
    def main(self, server_id, key):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
795
796
        super(self.__class__, self)._run()
        self._run(server_id=server_id, key=key)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
797

798

799
@command(server_cmds)
800
class server_stats(_init_cyclades, _optional_json):
801
    """Get virtual server statistics"""
802

Stavros Sachtouris's avatar
Stavros Sachtouris committed
803
804
805
806
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
807
808
        self._print(
            self.client.get_server_stats(int(server_id)), self.print_dict)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
809

810
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
811
812
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
813

Stavros Sachtouris's avatar
Stavros Sachtouris committed
814

815
@command(server_cmds)
816
class server_wait(_init_cyclades, _server_wait):
817
818
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""

819
820
821
822
823
    arguments = dict(
        timeout=IntArgument(
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
824
825
826
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
827
828
829
    def _run(self, server_id, current_status):
        r = self.client.get_server_details(server_id)
        if r['status'].lower() == current_status.lower():
830
            self._wait(server_id, current_status, timeout=self['timeout'])
831
832
833
834
835
        else:
            self.error(
                'Server %s: Cannot wait for status %s, '
                'status is already %s' % (
                    server_id, current_status, r['status']))
836

837
    def main(self, server_id, current_status='BUILD'):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
838
        super(self.__class__, self)._run()
839
        self._run(server_id=server_id, current_status=current_status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
840

841

842
@command(flavor_cmds)
843
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
844
    """List available hardware flavors"""
845

846
847
    PERMANENTS = ('id', 'name')

848
    arguments = dict(
849
850
        detail=FlagArgument('show detailed output', ('-l', '--details')),
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
851
        more=FlagArgument(
852
            'output results in pages (-n to set items per page, default 10)',
853
            '--more'),
854
855
856
857
858
859
        enum=FlagArgument('Enumerate results', '--enumerate'),
        ram=ValueArgument('filter by ram', ('--ram')),
        vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
        disk=ValueArgument('filter by disk size in GB', ('--disk')),
        disk_template=ValueArgument(
            'filter by disk_templace', ('--disk-template'))
860
    )
861

862
863
864
865
866
867
868
869
870
871
872
873
    def _apply_common_filters(self, flavors):
        common_filters = dict()
        if self['ram']:
            common_filters['ram'] = self['ram']
        if self['vcpus']:
            common_filters['vcpus'] = self['vcpus']
        if self['disk']:
            common_filters['disk'] = self['disk']
        if self['disk_template']:
            common_filters['SNF:disk_template'] = self['disk_template']
        return filter_dicts_by_dict(flavors, common_filters)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
874
875
876
    @errors.generic.all
    @errors.cyclades.connection
    def _run(self):
877
878
879
880
        withcommons = self['ram'] or self['vcpus'] or (
            self['disk'] or self['disk_template'])
        detail = self['detail'] or withcommons
        flavors = self.client.list_flavors(detail)
881
882
        flavors = self._filter_by_name(flavors)
        flavors = self._filter_by_id(flavors)
883
884
        if withcommons:
            flavors = self._apply_common_filters(flavors)
885
886
        if not (self['detail'] or (
                self['json_output'] or self['output_format'])):
887
            remove_from_items(flavors, 'links')
888
889
890
891
        if detail and not self['detail']:
            for flv in flavors:
                for key in set(flv).difference(self.PERMANENTS):
                    flv.pop(key)
892
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
893
        self._print(
894
            flavors,
895
896
897
898
            with_redundancy=self['detail'], with_enumeration=self['enum'],
            **kwargs)
        if self['more']:
            pager(kwargs['out'].getvalue())
Stavros Sachtouris's avatar
Stavros Sachtouris committed
899

900
    def main(self):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
901
902
        super(self.__class__, self)._run()
        self._run()
903

Stavros Sachtouris's avatar
Stavros Sachtouris committed
904

905
@command(flavor_cmds)
906
class flavor_info(_init_cyclades, _optional_json):