cyclades.py 42.4 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
66
    'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
    '  PATH: local file to be injected (relative or absolute)',
67
    '  SERVER_PATH: destination location inside server Image',
68
69
    '  OWNER: virtual servers user id of the remote destination file',
    '  GROUP: virtual servers group id or name of the destination file',
70
    '  MODEL: permition in octal (e.g., 0777 or o+rwx)']
71

72

73
class _service_wait(object):
74
75
76

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

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

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


103
class _server_wait(_service_wait):
104

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


112
113
class _network_wait(_service_wait):

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


120
121
122
123
124
125
126
127
128
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)


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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
156

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

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

165
    arguments = dict(
166
        detail=FlagArgument('show detailed output', ('-l', '--details')),
167
        since=DateArgument(
168
            'show only items since date (\' d/m/Y H:M:S \')',
169
            '--since'),
170
171
        limit=IntArgument(
            'limit number of listed virtual servers', ('-n', '--number')),
172
        more=FlagArgument(
173
            'output results in pages (-n to set items per page, default 10)',
174
            '--more'),
175
176
177
        enum=FlagArgument('Enumerate results', '--enumerate'),
        flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
        image_id=ValueArgument('filter by image id', ('--image-id')),
178
179
180
181
182
        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')),
183
184
185
186
        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')),
187
188
    )

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

198
199
200
201
202
203
204
205
206
    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)

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

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

216
    def _filter_by_metadata(self, servers):
217
218
219
220
221
222
223
224
225
226
227
228
229
230
        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

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

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

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

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
278

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

289
290
    @errors.generic.all
    @errors.cyclades.connection
Stavros Sachtouris's avatar
Stavros Sachtouris committed
291
    @errors.cyclades.server_id
292
    def _run(self, server_id):
293
294
295
296
        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']]
297
        self._print(vm, self.print_dict)
298

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
303

304
class PersonalityArgument(KeyValueArgument):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
305
    @property
306
    def value(self):
307
        return self._value if hasattr(self, '_value') else []
Stavros Sachtouris's avatar
Stavros Sachtouris committed
308
309

    @value.setter
310
311
312
    def value(self, newvalue):
        if newvalue == self.default:
            return self.value
313
314
315
316
        self._value = []
        for i, terms in enumerate(newvalue):
            termlist = terms.split(',')
            if len(termlist) > 5:
317
318
                msg = 'Wrong number of terms (should be 1 to 5)'
                raiseCLIError(CLISyntaxError(msg), details=howto_personality)
319
320
            path = termlist[0]
            if not exists(path):
321
322
                raiseCLIError(
                    None,
323
                    '--personality: File %s does not exist' % path,
324
                    importance=1, details=howto_personality)
325
326
327
328
329
330
331
332
333
334
            self._value.append(dict(path=path))
            with open(path) as f:
                self._value[i]['contents'] = b64encode(f.read())
            try:
                self._value[i]['path'] = termlist[1]
                self._value[i]['owner'] = termlist[2]
                self._value[i]['group'] = termlist[3]
                self._value[i]['mode'] = termlist[4]
            except IndexError:
                pass
335

Stavros Sachtouris's avatar
Stavros Sachtouris committed
336

337
@command(server_cmds)
338
class server_create(_init_cyclades, _optional_json, _server_wait):
339
340
    """Create a server (aka Virtual Machine)
    Parameters:
341
342
343
    - name: (single quoted text)
    - flavor id: Hardware flavor. Pick one from: /flavor list
    - image id: OS images. Pick one from: /image list
344
    """
345

346
347
    arguments = dict(
        personality=PersonalityArgument(
348
349
            (80 * ' ').join(howto_personality), ('-p', '--personality')),
        wait=FlagArgument('Wait server to build', ('-w', '--wait'))
350
    )
351

352
353
354
355
356
    @errors.generic.all
    @errors.cyclades.connection
    @errors.plankton.id
    @errors.cyclades.flavor_id
    def _run(self, name, flavor_id, image_id):
357
        r = self.client.create_server(
358
            name, int(flavor_id), image_id, personality=self['personality'])
359
360
361
        usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
        r['user_id'] += ' (%s)' % usernames[r['user_id']]
        r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
362
        self._print(r, self.print_dict)
363
364
        if self['wait']:
            self._wait(r['id'], r['status'])
365

366
    def main(self, name, flavor_id, image_id):
367
368
        super(self.__class__, self)._run()
        self._run(name=name, flavor_id=flavor_id, image_id=image_id)
369

Stavros Sachtouris's avatar
Stavros Sachtouris committed
370

371
@command(server_cmds)
372
class server_rename(_init_cyclades, _optional_output_cmd):
373
374
375
    """Set/update a virtual server name
    virtual server names are not unique, therefore multiple servers may share
    the same name
376
    """
377

Stavros Sachtouris's avatar
Stavros Sachtouris committed
378
379
380
381
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id, new_name):
382
383
        self._optional_output(
            self.client.update_server_name(int(server_id), new_name))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
384

385
    def main(self, server_id, new_name):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
386
387
        super(self.__class__, self)._run()
        self._run(server_id=server_id, new_name=new_name)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
388

389

390
@command(server_cmds)
391
class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
392
    """Delete a virtual server"""
393

394
395
396
397
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
398
399
400
401
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
402
403
404
405
406
407
408
409
410
411
            status = 'DELETED'
            if self['wait']:
                details = self.client.get_server_details(server_id)
                status = details['status']

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

            if self['wait']:
                self._wait(server_id, status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
412
413
414
415

    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
416

417

418
@command(server_cmds)
419
class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
420
    """Reboot a virtual server"""
421

422
    arguments = dict(
423
424
425
        hard=FlagArgument(
            'perform a hard reboot (deprecated)', ('-f', '--force')),
        type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
426
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
427
    )
428

Stavros Sachtouris's avatar
Stavros Sachtouris committed
429
430
431
432
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
        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)
450
451
452
453
        self._optional_output(r)

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

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

459

460
@command(server_cmds)
461
class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
462
    """Start an existing virtual server"""
463

464
465
466
467
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
468
469
470
471
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
472
473
474
475
476
477
478
479
480
481
482
483
        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
484

485
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
486
487
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
488

489

490
@command(server_cmds)
491
class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
492
    """Shutdown an active virtual server"""
493

494
495
496
497
    arguments = dict(
        wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
498
499
500
501
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
502
503
504
505
506
507
508
509
510
511
512
513
        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
514

515
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
516
517
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
518

519

520
@command(server_cmds)
521
class server_console(_init_cyclades, _optional_json):
522
    """Get a VNC console to access an existing virtual server
523
    Console connection information provided (at least):
524
    - host: (url or address) a VNC host
525
    - port: (int) the gateway to enter virtual server on host
526
    - password: for VNC authorization
527
    """
528

Stavros Sachtouris's avatar
Stavros Sachtouris committed
529
530
531
532
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
533
        self._print(
534
            self.client.get_server_console(int(server_id)), self.print_dict)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
535

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

Stavros Sachtouris's avatar
Stavros Sachtouris committed
540

541
542
543
544
@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:
545
546
    /server list
    /flavor list
547
548
549
550
551
552
553
554
555
556
557
558
559
560
    """

    @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)


561
@command(server_cmds)
562
class server_firewall(_init_cyclades):
563
    """Manage virtual server firewall profiles for public networks"""
564
565
566


@command(server_cmds)
567
568
class server_firewall_set(
        _init_cyclades, _optional_output_cmd, _firewall_wait):
569
    """Set the firewall profile on virtual server public network
570
    Values for profile:
571
572
573
    - DISABLED: Shutdown firewall
    - ENABLED: Firewall in normal mode
    - PROTECTED: Firewall in secure mode
574
    """
575

576
577
578
579
580
581
582
    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
583
584
585
586
587
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.firewall
    def _run(self, server_id, profile):
588
589
590
591
592
593
594
595
596
597
598
599
        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
600

601
    def main(self, server_id, profile):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
602
603
        super(self.__class__, self)._run()
        self._run(server_id=server_id, profile=profile)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
604

605

606
@command(server_cmds)
607
class server_firewall_get(_init_cyclades):
608
    """Get the firewall profile for a virtual servers' public network"""
609
610
611
612
613

    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
614
        self.writeln(self.client.get_firewall_profile(server_id))
615
616
617
618
619
620

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


621
@command(server_cmds)
622
class server_addr(_init_cyclades, _optional_json):
623
    """List the addresses of all network interfaces on a virtual server"""
624

625
    arguments = dict(
626
        enum=FlagArgument('Enumerate results', '--enumerate')
627
628
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
629
630
631
632
633
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
        reply = self.client.list_server_nics(int(server_id))
634
        self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
635

Stavros Sachtouris's avatar
Stavros Sachtouris committed
636
637
638
639
    def main(self, server_id):
        super(self.__class__, self)._run()
        self._run(server_id=server_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
640

641
@command(server_cmds)
642
643
644
645
646
class server_metadata(_init_cyclades):
    """Manage Server metadata (key:value pairs of server attributes)"""


@command(server_cmds)
647
class server_metadata_list(_init_cyclades, _optional_json):
648
    """Get server metadata"""
649

Stavros Sachtouris's avatar
Stavros Sachtouris committed
650
651
652
653
654
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.metadata
    def _run(self, server_id, key=''):
655
        self._print(
656
657
            self.client.get_server_metadata(int(server_id), key),
            self.print_dict)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
658

659
    def main(self, server_id, key=''):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
660
661
        super(self.__class__, self)._run()
        self._run(server_id=server_id, key=key)
662

Stavros Sachtouris's avatar
Stavros Sachtouris committed
663

664
@command(server_cmds)
665
class server_metadata_set(_init_cyclades, _optional_json):
666
    """Set / update virtual server metadata
667
    Metadata should be given in key/value pairs in key=value format
668
    For example: /server metadata set <server id> key1=value1 key2=value2
669
    Old, unreferenced metadata will remain intact
670
    """
671

Stavros Sachtouris's avatar
Stavros Sachtouris committed
672
673
674
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
675
    def _run(self, server_id, keyvals):
676
        assert keyvals, 'Please, add some metadata ( key=value)'
677
678
679
680
681
682
683
684
685
686
687
688
689
        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'])
690
        self._print(
691
            self.client.update_server_metadata(int(server_id), **metadata),
692
            self.print_dict)
693
694

    def main(self, server_id, *key_equals_val):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
695
        super(self.__class__, self)._run()
696
        self._run(server_id=server_id, keyvals=key_equals_val)
697

Stavros Sachtouris's avatar
Stavros Sachtouris committed
698

699
@command(server_cmds)
700
class server_metadata_delete(_init_cyclades, _optional_output_cmd):
701
    """Delete virtual server metadata"""
702

Stavros Sachtouris's avatar
Stavros Sachtouris committed
703
704
705
706
707
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    @errors.cyclades.metadata
    def _run(self, server_id, key):
708
709
        self._optional_output(
            self.client.delete_server_metadata(int(server_id), key))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
710

711
    def main(self, server_id, key):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
712
713
        super(self.__class__, self)._run()
        self._run(server_id=server_id, key=key)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
714

715

716
@command(server_cmds)
717
class server_stats(_init_cyclades, _optional_json):
718
    """Get virtual server statistics"""
719

Stavros Sachtouris's avatar
Stavros Sachtouris committed
720
721
722
723
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
    def _run(self, server_id):
724
725
        self._print(
            self.client.get_server_stats(int(server_id)), self.print_dict)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
726

727
    def main(self, server_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
728
729
        super(self.__class__, self)._run()
        self._run(server_id=server_id)
730

Stavros Sachtouris's avatar
Stavros Sachtouris committed
731

732
@command(server_cmds)
733
class server_wait(_init_cyclades, _server_wait):
734
735
    """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""

736
737
738
739
740
    arguments = dict(
        timeout=IntArgument(
            'Wait limit in seconds (default: 60)', '--timeout', default=60)
    )

Stavros Sachtouris's avatar
Stavros Sachtouris committed
741
742
743
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.server_id
744
745
746
    def _run(self, server_id, current_status):
        r = self.client.get_server_details(server_id)
        if r['status'].lower() == current_status.lower():
747
            self._wait(server_id, current_status, timeout=self['timeout'])
748
749
750
751
752
        else:
            self.error(
                'Server %s: Cannot wait for status %s, '
                'status is already %s' % (
                    server_id, current_status, r['status']))
753

754
    def main(self, server_id, current_status='BUILD'):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
755
        super(self.__class__, self)._run()
756
        self._run(server_id=server_id, current_status=current_status)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
757

758

759
@command(flavor_cmds)
760
class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
761
    """List available hardware flavors"""
762

763
764
    PERMANENTS = ('id', 'name')

765
    arguments = dict(
766
767
        detail=FlagArgument('show detailed output', ('-l', '--details')),
        limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
768
        more=FlagArgument(
769
            'output results in pages (-n to set items per page, default 10)',
770
            '--more'),
771
772
773
774
775
776
        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'))
777
    )
778

779
780
781
782
783
784
785
786
787
788
789
790
    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
791
792
793
    @errors.generic.all
    @errors.cyclades.connection
    def _run(self):
794
795
796
797
        withcommons = self['ram'] or self['vcpus'] or (
            self['disk'] or self['disk_template'])
        detail = self['detail'] or withcommons
        flavors = self.client.list_flavors(detail)
798
799
        flavors = self._filter_by_name(flavors)
        flavors = self._filter_by_id(flavors)
800
801
        if withcommons:
            flavors = self._apply_common_filters(flavors)
802
803
        if not (self['detail'] or (
                self['json_output'] or self['output_format'])):
804
            remove_from_items(flavors, 'links')
805
806
807
808
        if detail and not self['detail']:
            for flv in flavors:
                for key in set(flv).difference(self.PERMANENTS):
                    flv.pop(key)
809
        kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
810
        self._print(
811
            flavors,
812
813
814
815
            with_redundancy=self['detail'], with_enumeration=self['enum'],
            **kwargs)
        if self['more']:
            pager(kwargs['out'].getvalue())
Stavros Sachtouris's avatar
Stavros Sachtouris committed
816

817
    def main(self):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
818
819
        super(self.__class__, self)._run()
        self._run()
820

Stavros Sachtouris's avatar
Stavros Sachtouris committed
821

822
@command(flavor_cmds)
823
class flavor_info(_init_cyclades, _optional_json):
824
    """Detailed information on a hardware flavor
825
826
    To get a list of available flavors and flavor ids, try /flavor list
    """
827

Stavros Sachtouris's avatar
Stavros Sachtouris committed
828
829
830
831
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.flavor_id
    def _run(self, flavor_id):
832
        self._print(
833
            self.client.get_flavor_details(int(flavor_id)), self.print_dict)
834

Stavros Sachtouris's avatar
Stavros Sachtouris committed
835
836
837
838
    def main(self, flavor_id):
        super(self.__class__, self)._run()
        self._run(flavor_id=flavor_id)

Stavros Sachtouris's avatar
Stavros Sachtouris committed
839

840
841
842
843
844
845
846
847
848
849
850
851
852
853
def _add_name(self, net):
        user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
        if user_id:
            uuids.append(user_id)
        if tenant_id:
            uuids.append(tenant_id)
        if uuids:
            usernames = self._uuids2usernames(uuids)
            if user_id:
                net['user_id'] += ' (%s)' % usernames[user_id]
            if tenant_id:
                net['tenant_id'] += ' (%s)' % usernames[tenant_id]


854
@command(network_cmds)
855
class network_info(_init_cyclades, _optional_json):
856
857
858
    """Detailed information on a network
    To get a list of available networks and network ids, try /network list
    """
859

Stavros Sachtouris's avatar
Stavros Sachtouris committed
860
861
862
863
864
    @errors.generic.all
    @errors.cyclades.connection
    @errors.cyclades.network_id
    def _run(self, network_id):
        network = self.client.get_network_details(int(network_id))
865
        _add_name(self, network)
866
        self._print(network, self.print_dict, exclude=('id'))
Stavros Sachtouris's avatar
Stavros Sachtouris committed
867

868
    def main(self, network_id):
Stavros Sachtouris's avatar
Stavros Sachtouris committed
869
870
        super(self.__class__, self)._run()
        self._run(network_id=network_id)
871
872


873
@command(network_cmds)
874
class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):